From 8113859b1bc2a9a33adc086e5ad01fd3f34414cf Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:19:46 +1000 Subject: [PATCH 01/16] tidying --- CHANGES.md | 5 ++ diagnosticism/__init__.py | 4 +- diagnosticism/contingent_reporting.py | 39 +++++++--- diagnosticism/doomgram.py | 20 +++-- diagnosticism/logging.py | 29 ++++--- diagnosticism/severity.py | 108 ++++++++++++++++---------- diagnosticism/tracing.py | 32 ++++---- diagnosticism/warning.py | 14 +++- setup.py | 2 +- tests/test_conrep.py | 28 ++++--- tests/test_log.py | 58 +++++++------- tests/test_parse_severity.py | 13 ++-- tests/test_trace.py | 4 +- tests/test_warn.py | 23 ++---- 14 files changed, 224 insertions(+), 155 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f7bb7f0..526a2ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # **Diagnosticism.Python** Changes +## 0.15.3 - 26th June 2026 + +* tidying; + + ## 0.15.2 - 27th August 2025 * project boilerplate; diff --git a/diagnosticism/__init__.py b/diagnosticism/__init__.py index 81dad70..482ab93 100644 --- a/diagnosticism/__init__.py +++ b/diagnosticism/__init__.py @@ -9,7 +9,7 @@ __license__ = 'BSD-3-Clause' __maintainer__ = 'Matt Wilson' __status__ = 'Beta' -__version__ = '0.15.2' +__version__ = '0.15.3' import sys @@ -44,8 +44,8 @@ ) if sys.version_info[:2] >= (3, 9): from .tracing import ( - tracefunc, asynctracefunc, + tracefunc, ) from .warning import warn diff --git a/diagnosticism/contingent_reporting.py b/diagnosticism/contingent_reporting.py index b3af37e..b92a59e 100644 --- a/diagnosticism/contingent_reporting.py +++ b/diagnosticism/contingent_reporting.py @@ -25,8 +25,12 @@ def _supports_ansi_sequences(): _OS_IS_POSIX = _supports_ansi_sequences() -STOCK_TRAILING_PROMPT = 'use --help for usage' -"""The trailing prompt used when trailing_prompt=True is passed to abort() (and family) and no default trailing prompt has been set (via set_default_trailing_prompt())""" +STOCK_TRAILING_PROMPT = "use --help for usage" +""" +The trailing prompt used when trailing_prompt=True is passed to abort() (and +family) and no default trailing prompt has been set (via +set_default_trailing_prompt()). +""" _trailing_prompt = None @@ -47,7 +51,8 @@ def set_default_trailing_prompt(prompt): Notes ----- - This method is NOT threadsafe, so in a multithreaded context the set prompt and/or the return value may not be as expected + This method is NOT threadsafe, so in a multithreaded context the set + prompt and/or the return value may not be as expected. """ global _trailing_prompt @@ -196,15 +201,18 @@ def report( file=None, ): """ - Emits the given message (and optional prefix) on the contingent report stream. + Emits the given message (and optional prefix) on the contingent report + stream. Parameters ---------- message : str - The (main body of the) message to be displayed, which is optionally prefixed by program-name + ': ' + The (main body of the) message to be displayed, which is optionally + prefixed by program-name + ': ' show_program_name : bool, optional - Prevents the default prefixing of the message with program-name + ': ' if `False`. Default is `True` + Prevents the default prefixing of the message with program-name + + ': ' if `False`. Default is `True` """ trailing_prompt = False @@ -227,21 +235,30 @@ def abort( file=None, ): """ - Emits the given message (and optional prefix and suffix) on the contingent report stream, and then terminates the process + Emits the given message (and optional prefix and suffix) on the + contingent report stream, and then terminates the process. Parameters ---------- message : str - The (main body of the) message to be displayed, which is optionally prefixed by program-name + ': ' and optionally suffixed by '; ' + trailing-prompt + The (main body of the) message to be displayed, which is optionally + prefixed by program-name + ': ' and optionally suffixed by '; ' + + trailing-prompt do_exit : bool, optional - Prevents default behaviour of a call to `sys.exit(1)` (after printing the abort message) if `False`. Default is `True` + Prevents default behaviour of a call to `sys.exit(1)` (after + printing the abort message) if `False`. Default is `True` show_program_name : bool, optional - Prevents the default prefixing of the message with program-name + ': ' if `False`. Default is `True` + Prevents the default prefixing of the message with program-name + + ': ' if `False`. Default is `True` trailing_prompt : str, bool, optional - Affects the use of the trailing prompt as follows: if `False`, no trailing prompt is shown; if a (non-empty) string, that is used; if `None`, the default trailing prompt, if any, is used; if `True`, the default trailing prompt is used if specified, otherwise `STOCK_TRAILING_PROMPT` is used + Affects the use of the trailing prompt as follows: if `False`, no + trailing prompt is shown; if a (non-empty) string, that is used; if + `None`, the default trailing prompt, if any, is used; if `True`, the + default trailing prompt is used if specified, otherwise + `STOCK_TRAILING_PROMPT` is used """ file = _get_cr_file_or_default(file) diff --git a/diagnosticism/doomgram.py b/diagnosticism/doomgram.py index fa1a159..fda9574 100644 --- a/diagnosticism/doomgram.py +++ b/diagnosticism/doomgram.py @@ -312,15 +312,25 @@ def to_strip(self, **kwargs): === Signature * *Parameters:* - - +options+ (+Hash+, +Integer+) Combination of flags (with behaviour as described below for the +flags+ option), or an options hash; + - +options+ (+Hash+, +Integer+) Combination of flags (with + behaviour as described below for the +flags+ option), or an + options hash; * *Options:* - - +overflow_character+ (+String+) A string (of length 1) that specifies the symbol for counts outside the available range. Defaults to +'*'+; - - +range+ (+String+) A string whose characters specificy the symbols to use for counts in orders of magnitude. Defaults to +'abcdefghijklmnopqrstuvwxyz'+, which caters to the counts 1-9 => +'a', 10-99 => +'b'+, 100-999 => +'c'+, ... 10^25-(10^26-1) => +'z'+; - - +zero_character+ (+String+) A string (of length 1) that specifies the symbol for a count of 0. Defaults to +' '+; + - +overflow_character+ (+String+) A string (of length 1) that + specifies the symbol for counts outside the available range. + Defaults to +'*'+; + - +range+ (+String+) A string whose characters specificy the + symbols to use for counts in orders of magnitude. Defaults to + +'abcdefghijklmnopqrstuvwxyz'+, which caters to the counts 1-9 + => +'a', 10-99 => +'b'+, 100-999 => +'c'+, ... + 10^25-(10^26-1) => +'z'+; + - +zero_character+ (+String+) A string (of length 1) that + specifies the symbol for a count of 0. Defaults to +' '+; === Return - (+String+) A string (of length 12) containing symbols representing the counts in the ranges 1ns, 10ns, ..., 10s, 100+s. + (+String+) A string (of length 12) containing symbols representing + the counts in the ranges 1ns, 10ns, ..., 10s, 100+s. """ ch_zero = kwargs.get('zero', '_') diff --git a/diagnosticism/logging.py b/diagnosticism/logging.py index df92d95..002a76c 100644 --- a/diagnosticism/logging.py +++ b/diagnosticism/logging.py @@ -18,10 +18,12 @@ _REAL_STDERR = sys.stderr + def _get_stderr_dynamic(): return sys.stderr + def _get_stderr_static(): return _REAL_STDERR @@ -81,7 +83,10 @@ def enable_logging(*args): Parameters ---------- *args - 1 or 2 arguments: if 1, then is a `bool` determining whether should be enabled; if 2, then first is name(s) of environment variable(s) to be parsed and second is a `bool` specifying the default if not found in the environment + 1 or 2 arguments: if 1, then is a `bool` determining whether + enabled; if 2, then first is name(s) of environment variable(s) to + be parsed and second is a `bool` specifying the default if not + found in the environment Returns ------- @@ -91,7 +96,8 @@ def enable_logging(*args): Raises ------ ValueError - If the string form of `v` does not contain a recognisable severity level + If the string form of `v` does not contain a recognisable severity + level """ is_enabled = _bool_from_env(args, 'enable_logging') @@ -129,8 +135,7 @@ def is_severity_logged(severity): if _log_filter: - if isinstance(_log_filter, (dict, )): - + if isinstance(_log_filter, (dict,)): r = _log_filter.get(severity, _others_action) if not r: @@ -150,15 +155,20 @@ def set_log_filter( others_action=None, ): """ - Sets a logging filter, which may either specify a threshold severity or a mapping of levels to actions + Sets a logging filter, which may either specify a threshold severity or + a mapping of levels to actions Parameters ---------- log_filter : int, dict - If a dictionary, it is interpreted as a mapping from severity-level to `True`/`False` that controls each level's output; otherwise, treated as a severity-level threshold (and must be convertible to `int`) + If a dictionary, it is interpreted as a mapping from + severity-level to `True`/`False` that controls each level's output; + otherwise, treated as a severity-level threshold (and must be + convertible to `int`) others_action : obj, optional - An object to be passed to be used in the case that the log_filter is a dictionary and the severity is not recognised + An object to be passed to be used in the case that the log_filter is + a dictionary and the severity is not recognised Returns ------- @@ -205,7 +215,7 @@ def _do_log( message = message() # TODO: perf test this - full = prefix + ': ' + message + full = prefix + ": " + message show_program_name = False trailing_prompt = False @@ -234,7 +244,8 @@ def log( The severity associated with the message message : str, callable - A message string to be emitted, or a callable object that will yield an emittable string when called + A message string to be emitted, or a callable object that will yield + an emittable string when called Returns ------- diff --git a/diagnosticism/severity.py b/diagnosticism/severity.py index 78d4730..5bcf847 100644 --- a/diagnosticism/severity.py +++ b/diagnosticism/severity.py @@ -32,42 +32,60 @@ 'severity_to_string', ] -UNSPECIFIED = 0 -VIOLATION = 1 -"""Severity level suitable for use when logging that a design violation has occurred.""" -ALERT = 2 -"""Severity level suitable for use when logging that a fatal program failure has occurred.""" -CRITICAL = 3 -"""Severity level suitable for use when logging that a critical failure has occurred.""" -FAILURE = 4 -"""Severity level suitable for use when logging that a failure has occurred.""" -WARNING = 5 +UNSPECIFIED = 0 +VIOLATION = 1 +""" +Severity level suitable for use when logging that a design violation has +occurred. +""" +ALERT = 2 +""" +Severity level suitable for use when logging that a fatal program failure +has occurred. +""" +CRITICAL = 3 +""" +Severity level suitable for use when logging that a critical failure has +occurred. +""" +FAILURE = 4 +""" +Severity level suitable for use when logging that a failure has occurred. +""" +WARNING = 5 """Severity level suitable for use when issuing a warning.""" -NOTICE = 6 -"""Severity level suitable for use when logging an important normative condition.""" -INFORMATIONAL = 7 +NOTICE = 6 +""" +Severity level suitable for use when logging an important normative +condition. +""" +INFORMATIONAL = 7 """Severity level suitable for use when logging a normative condition.""" -DEBUG0 = 8 +DEBUG0 = 8 """The highest debug severity level.""" -DEBUG1 = 9 +DEBUG1 = 9 """The second highest debug severity level.""" -DEBUG2 = 10 +DEBUG2 = 10 """The third highest debug severity level.""" -DEBUG3 = 11 +DEBUG3 = 11 """The fourth highest debug severity level.""" -DEBUG4 = 12 +DEBUG4 = 12 """The fifth highest debug severity level.""" -DEBUG5 = 13 +DEBUG5 = 13 """The sixth highest debug severity level.""" -TRACE = 14 -"""Severity level suitable at which trace statements are issued.""" -BENCHMARK = 15 -"""Severity level suitable at which benchmark statements are issued.""" - -DEBUG = DEBUG5 -FAIL = FAILURE -WARN = WARNING -INFO = INFORMATIONAL +TRACE = 14 +""" +Severity level suitable at which trace statements are issued. +""" +BENCHMARK = 15 +""" +Severity level suitable at which benchmark statements are issued. +""" + +DEBUG = DEBUG5 +FAIL = FAILURE +WARN = WARNING +INFO = INFORMATIONAL _STOCK_SEVERITY_LEVELS = { @@ -126,11 +144,9 @@ } if _is_python_3_0_or_later(): - - _INTEGER_TYPES = (int, ) + _INTEGER_TYPES = (int,) else: - - _INTEGER_TYPES = (int, long, ) # noqa: F821 + _INTEGER_TYPES = (int, long,) # noqa: F821 def _parse_verbosity( @@ -143,9 +159,12 @@ def _parse_verbosity( Parameters ---------- v : str | int | * - The variable from which to parse the verbosity. If an integer, is taken as is; if not a string then converted to a string, and in either case is then subject to parsing + The variable from which to parse the verbosity. If an integer, is + taken as is; if not a string then converted to a string, and in + either case is then subject to parsing strict_case_comparison : bool - Specifies whether any string comparison should be strict or permissive + Specifies whether any string comparison should be strict or + permissive Returns ------- @@ -155,7 +174,8 @@ def _parse_verbosity( Raises ------ ValueError - If the string form of `v` does not contain a recognisable severity level + If the string form of `v` does not contain a recognisable severity + level """ if isinstance(v, _INTEGER_TYPES): @@ -200,9 +220,13 @@ def parse_verbosity( Parameters ---------- v : str | int | * - The variable from which to parse the verbosity. If an integer, is taken as is; if not a string is converted to a string, and in either case is then subject to parsing + The variable from which to parse the verbosity. If an integer, + is taken as is; if not a string is converted to a string, and + in either case is then subject to parsing kwargs : dict - Keyword arguments. Currently only `strict_case_comparison` is recognised, which defaults to `False` if not specified by the caller + Keyword arguments. Currently only `strict_case_comparison` is + recognised, which defaults to `False` if not specified by the + caller Returns ------- @@ -212,7 +236,8 @@ def parse_verbosity( Raises ------ ValueError - If the string form of `v` does not contain a recognisable severity level + If the string form of `v` does not contain a recognisable + severity level """ strict_case_comparison = kwargs.get('strict_case_comparison', False) @@ -233,7 +258,9 @@ def parse_verbosity( Parameters ---------- v : str | int | * - The variable from which to parse the verbosity. If an integer, is taken as is; if not a string is converted to a string, and in either case is then subject to parsing + The variable from which to parse the verbosity. If an integer, + is taken as is; if not a string is converted to a string, and + in either case is then subject to parsing Returns ------- @@ -243,7 +270,8 @@ def parse_verbosity( Raises ------ ValueError - If the string form of `v` does not contain a recognisable severity level + If the string form of `v` does not contain a recognisable + severity level """ return _parse_verbosity( diff --git a/diagnosticism/tracing.py b/diagnosticism/tracing.py index 7711fe3..0342ba9 100644 --- a/diagnosticism/tracing.py +++ b/diagnosticism/tracing.py @@ -60,18 +60,14 @@ def _dbg( kwnames = list(kwargs) if 0 != len(args): - - s0 = ', '.join(["(%s)=%s" % (type(arg).__name__, arg) for arg in args]) + s0 = ", ".join(["(%s)=%s" % (type(arg).__name__, arg) for arg in args]) else: - - s0 = '' + s0 = "" if 0 != len(kwargs): - - s1 = ', '.join(["%s(%s)=%s" % (name, type(arg).__name__, arg) for name, arg in kwargs.items()]) + s1 = ", ".join(["%s(%s)=%s" % (name, type(arg).__name__, arg) for name, arg in kwargs.items()]) else: - - s1 = '' + s1 = "" if s0 and s1: @@ -84,7 +80,7 @@ def _dbg( s = s1 else: - s = '' + s = "" if show_fileline: @@ -116,8 +112,7 @@ def _flf( file_name = code.co_filename line_number = fr.f_lineno if '' == code.co_name: - - function_name = "" + function_name = '' else: function_name = code.co_name @@ -126,7 +121,7 @@ def _flf( file_name = _basename(file_name) - return [ file_name, line_number, function_name ] + return [file_name, line_number, function_name] finally: del fr @@ -208,7 +203,10 @@ def enable_tracing(*args): Parameters ---------- *args - 1 or 2 arguments: if 1, then is a `bool` determining whether should be enabled; if 2, then first is name(s) of environment variable(s) to be parsed and second is a `bool` specifying the default if not found in the environment + 1 or 2 arguments: if 1, then is a `bool` determining whether + enabled; if 2, then first is name(s) of environment variable(s) to + be parsed and second is a `bool` specifying the default if not + found in the environment Returns ------- @@ -382,10 +380,12 @@ def tracefunc( file=None, ): """ - Decorator function that equates to the receiver function calling `trace()` + Decorator function that equates to the receiver function calling + `trace()`. Returns ------- + callable A wrapper function that calls the decorated function """ @@ -480,10 +480,12 @@ def asynctracefunc( file=None, ): """ - Async decorator function that equates to the receiver function calling `trace()` + Async decorator function that equates to the receiver function + calling `trace()`. Returns ------- + callable A wrapper function that calls the decorated function """ diff --git a/diagnosticism/warning.py b/diagnosticism/warning.py index 051367b..c8d2160 100644 --- a/diagnosticism/warning.py +++ b/diagnosticism/warning.py @@ -55,16 +55,22 @@ def warn( Parameters ---------- message_lines : list[str] - The message line(s) to be emitted to the standard error stream (along with a new-line sequence). If `None`, nothing is emitted + The message line(s) to be emitted to the standard error stream + (along with a new-line sequence). If `None`, nothing is emitted file : file-object, optional - An object with a `write(str)` method, or `None` (or not present), in which case the default of `sys.stderr` will be used + An object with a `write(str)` method, or `None` (or not present), in + which case the default of `sys.stderr` will be used file_cr : file-object, optional - An object with a `write(str)` method, or `None` (or not present), in which case the default given in `file` will be used. Used for contingent report output + An object with a `write(str)` method, or `None` (or not present), in + which case the default given in `file` will be used. Used for + contingent report output file_dl : file-object, optional - An object with a `write(str)` method, or `None` (or not present), in which case the default given in `file` will be used. Used for diagnostic logging output + An object with a `write(str)` method, or `None` (or not present), in + which case the default given in `file` will be used. Used for + diagnostic logging output Returns ------- diff --git a/setup.py b/setup.py index b3785ab..d4d5879 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( name='diagnosticism', - version='0.15.2', + version='0.15.3', author='Matt Wilson', author_email='matthew@synesis.com.au', diff --git a/tests/test_conrep.py b/tests/test_conrep.py index 9c683b2..979ee10 100644 --- a/tests/test_conrep.py +++ b/tests/test_conrep.py @@ -28,7 +28,7 @@ def test_with_program_name(self): set_program_name('myprog1') - conrep('string-1', show_program_name=True) + conrep("string-1", show_program_name=True) self.assertEqual("myprog1: string-1\n", fake_stderr.getvalue()) @@ -36,8 +36,7 @@ def test_with_program_name(self): def test_without_program_name(self): with patch('sys.stderr', new=StringIO()) as fake_stderr: - - conrep('string-1', show_program_name=False) + conrep("string-1", show_program_name=False) self.assertEqual("string-1\n", fake_stderr.getvalue()) @@ -51,7 +50,7 @@ def test_with_program_name(self): set_program_name('myprog1') - report('string-1', show_program_name=True) + report("string-1", show_program_name=True) self.assertEqual("myprog1: string-1\n", fake_stderr.getvalue()) @@ -59,8 +58,7 @@ def test_with_program_name(self): def test_without_program_name(self): with patch('sys.stderr', new=StringIO()) as fake_stderr: - - report('string-1', show_program_name=False) + report("string-1", show_program_name=False) self.assertEqual("string-1\n", fake_stderr.getvalue()) @@ -71,7 +69,7 @@ def test__report__WITH_file_PARAM_AND_show_program_name_False(self): set_program_name('myprog3') - report('string-3', file=file, show_program_name=False) + report("string-3", file=file, show_program_name=False) result = file.getvalue() @@ -84,7 +82,7 @@ def test__report__WITH_file_PARAM_AND_show_program_name_True(self): set_program_name('myprog4') - report('string-4', file=file, show_program_name=True) + report("string-4", file=file, show_program_name=True) result = file.getvalue() @@ -100,7 +98,7 @@ def test_default(self): set_program_name('myprog1') - abort('over and out!', do_exit=False) + abort("over and out!", do_exit=False) self.assertEqual("myprog1: over and out!\n", fake_stderr.getvalue()) @@ -111,7 +109,7 @@ def test_explicit_trailing_prompt(self): set_program_name('myprog1') - abort('over and out!', do_exit=False, trailing_prompt='get over yourself!') + abort("over and out!", do_exit=False, trailing_prompt="get over yourself!") self.assertEqual("myprog1: over and out!; get over yourself!\n", fake_stderr.getvalue()) @@ -122,7 +120,7 @@ def test_stock_trailing_prompt(self): set_program_name('myprog1') - abort('over and out!', do_exit=False, trailing_prompt=True) + abort("over and out!", do_exit=False, trailing_prompt=True) self.assertEqual("myprog1: over and out!; use --help for usage\n", fake_stderr.getvalue()) @@ -133,9 +131,9 @@ def test_set_default_trailing_prompt_1(self): set_program_name('myprog1') - set_default_trailing_prompt('ok, now') + set_default_trailing_prompt("ok, now") - abort('over and out!', do_exit=False, trailing_prompt=True) + abort("over and out!", do_exit=False, trailing_prompt=True) set_default_trailing_prompt(None) @@ -148,9 +146,9 @@ def test_set_default_trailing_prompt_2(self): set_program_name('myprog1') - set_default_trailing_prompt('ok, now') + set_default_trailing_prompt("ok, now") - abort('over and out!', do_exit=False, trailing_prompt=False) + abort("over and out!", do_exit=False, trailing_prompt=False) set_default_trailing_prompt(None) diff --git a/tests/test_log.py b/tests/test_log.py index 8c3f35a..58c2c7f 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -34,9 +34,9 @@ def test_logging_off(self): set_program_name('myprog1') - log(INFORMATIONAL, 'msg-1') + log(INFORMATIONAL, "msg-1") - self.assertEqual('', fake_stderr.getvalue()) + self.assertEqual("", fake_stderr.getvalue()) finally: enable_logging(logging_enabled) @@ -52,7 +52,7 @@ def test_message_as_string(self): set_program_name('myprog1') - log(INFORMATIONAL, 'msg-1') + log(INFORMATIONAL, "msg-1") self.assertRegex(fake_stderr.getvalue(), r'^\[myprog1, \d+, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}, .*INFORMATIONAL.*\]: msg-1') finally: @@ -70,7 +70,7 @@ def test__log__WITH_str_AND_file_PARAM(self): set_program_name('myprog3') - log(INFORMATIONAL, 'msg-3', file=file) + log(INFORMATIONAL, "msg-3", file=file) finally: enable_logging(logging_enabled) @@ -90,7 +90,7 @@ def test_message_as_lambda(self): set_program_name('myprog1') - log(INFORMATIONAL, lambda: 'msg-1') + log(INFORMATIONAL, lambda: "msg-1") self.assertRegex(fake_stderr.getvalue(), r'^\[myprog1, \d+, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}, .*INFORMATIONAL.*\]: msg-1') finally: @@ -110,17 +110,17 @@ def test_threshold_filter(self): set_program_name('myprog1') - log(DEBUG0, 'msg-debug0') - log(INFORMATIONAL, 'msg-informational') - log(NOTICE, 'msg-notice') - log(WARNING, 'msg-warning') - log(FAILURE, 'msg-failure') - log(CRITICAL, 'msg-critical') - log(ALERT, 'msg-alert') + log(DEBUG0, "msg-debug0") + log(INFORMATIONAL, "msg-informational") + log(NOTICE, "msg-notice") + log(WARNING, "msg-warning") + log(FAILURE, "msg-failure") + log(CRITICAL, "msg-critical") + log(ALERT, "msg-alert") r = fake_stderr.getvalue().rstrip() - lines = re.split('[\r\n]', r) + lines = re.split("[\r\n]", r) self.assertEqual(4, len(lines)) self.assertRegex(lines[0], r'^\[myprog1, \d+, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}, .*WARNING.*\]: msg-warning') @@ -153,17 +153,17 @@ def test_dict_filter_others_action_False(self): set_program_name('myprog1') - log(DEBUG0, 'msg-debug0') - log(INFORMATIONAL, 'msg-informational') - log(NOTICE, 'msg-notice') - log(WARNING, 'msg-warning') - log(FAILURE, 'msg-failure') - log(CRITICAL, 'msg-critical') - log(ALERT, 'msg-alert') + log(DEBUG0, "msg-debug0") + log(INFORMATIONAL, "msg-informational") + log(NOTICE, "msg-notice") + log(WARNING, "msg-warning") + log(FAILURE, "msg-failure") + log(CRITICAL, "msg-critical") + log(ALERT, "msg-alert") r = fake_stderr.getvalue().rstrip() - lines = re.split('[\r\n]', r) + lines = re.split("[\r\n]", r) self.assertEqual(3, len(lines)) self.assertRegex(lines[0], r'^\[myprog1, \d+, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}, .*WARNING.*\]: msg-warning') @@ -195,17 +195,17 @@ def test_dict_filter_others_action_True(self): set_program_name('myprog1') - log(DEBUG0, 'msg-debug0') - log(INFORMATIONAL, 'msg-informational') - log(NOTICE, 'msg-notice') - log(WARNING, 'msg-warning') - log(FAILURE, 'msg-failure') - log(CRITICAL, 'msg-critical') - log(ALERT, 'msg-alert') + log(DEBUG0, "msg-debug0") + log(INFORMATIONAL, "msg-informational") + log(NOTICE, "msg-notice") + log(WARNING, "msg-warning") + log(FAILURE, "msg-failure") + log(CRITICAL, "msg-critical") + log(ALERT, "msg-alert") r = fake_stderr.getvalue().rstrip() - lines = re.split('[\r\n]', r) + lines = re.split("[\r\n]", r) self.assertEqual(6, len(lines)) self.assertRegex(lines[0], r'^\[myprog1, \d+, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}, .*DEBUG0.*\]: msg-debug0') diff --git a/tests/test_parse_severity.py b/tests/test_parse_severity.py index ce64330..ebd9213 100644 --- a/tests/test_parse_severity.py +++ b/tests/test_parse_severity.py @@ -19,13 +19,12 @@ INTEGER_LEVELS = [ -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, ] NAMED_LEVELS = { - - "Warn" : WARNING, - "Warning" : WARNING, - "WARN" : WARNING, - "WARNING" : WARNING, - "warn" : WARNING, - "warning" : WARNING, + 'Warn' : WARNING, + 'Warning' : WARNING, + 'WARN' : WARNING, + 'WARNING' : WARNING, + 'warn' : WARNING, + 'warning' : WARNING, } diff --git a/tests/test_trace.py b/tests/test_trace.py index 94eb922..9cce851 100755 --- a/tests/test_trace.py +++ b/tests/test_trace.py @@ -63,8 +63,8 @@ def test__trace__WITH_TRACING_DISABLED(self): r = f1() - self.assertEqual('f1-result', r) - self.assertEqual('', file.getvalue()) + self.assertEqual("f1-result", r) + self.assertEqual("", file.getvalue()) finally: file = None diff --git a/tests/test_warn.py b/tests/test_warn.py index e73fd33..01d272f 100755 --- a/tests/test_warn.py +++ b/tests/test_warn.py @@ -48,8 +48,7 @@ def test__warn__WITH_PROGRAM_NAME_AND_LOGGING_DISABLED(self): logging_enabled = True if enable_logging(False) else False try: - - warn('warning-1') + warn("warning-1") finally: enable_logging(logging_enabled) @@ -68,8 +67,7 @@ def test__warn__WITH_PROGRAM_NAME_AND_LOGGING_ENSABLED(self): logging_enabled = True if enable_logging(True) else False try: - - warn('warning-2') + warn("warning-2") finally: enable_logging(logging_enabled) @@ -90,8 +88,7 @@ def test__warn__WITH_file_PARAM_AND_LOGGING_DISABLED(self): logging_enabled = True if enable_logging(False) else False try: - - warn('warning-3', file=file) + warn("warning-3", file=file) finally: enable_logging(logging_enabled) @@ -110,10 +107,8 @@ def test__warn__WITH_file_PARAM_AND_LOGGING_ENABLED(self): logging_enabled = True if enable_logging(True) else False try: - - warn('warning-4', file=file) + warn("warning-4", file=file) finally: - enable_logging(logging_enabled) result = file.getvalue() @@ -133,8 +128,7 @@ def test__warn__WITH_file_cr_PARAM_AND_file_dl_PARAM_AND_LOGGING_ENABLED(self): logging_enabled = True if enable_logging(True) else False try: - - warn('warning-4', file_cr=file_cr, file_dl=file_dl) + warn("warning-4", file_cr=file_cr, file_dl=file_dl) finally: enable_logging(logging_enabled) @@ -159,8 +153,7 @@ def test__warn__WITH_file_PARAM_AND_file_dl_PARAM_AND_LOGGING_ENABLED(self): logging_enabled = True if enable_logging(True) else False try: - - warn('warning-4', file=file, file_dl=file_dl) + warn("warning-4", file=file, file_dl=file_dl) finally: enable_logging(logging_enabled) @@ -185,8 +178,8 @@ def test__warn__WITH_MULTIPLE_LINES_WITH_PROGRAM_NAME_AND_LOGGING_DISABLED(self) try: warn( - 'warning-5a', - 'warning-5b', + "warning-5a", + "warning-5b", ) finally: From 7a904d21a6cf2c6ffd3f5d994ac2bb2bac4d4caf Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:34:40 +1000 Subject: [PATCH 02/16] chore: fixed top-level *-imports --- diagnosticism/__init__.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/diagnosticism/__init__.py b/diagnosticism/__init__.py index 482ab93..955f943 100644 --- a/diagnosticism/__init__.py +++ b/diagnosticism/__init__.py @@ -28,8 +28,33 @@ log, set_log_filter, ) -from .program_name import * -from .severity import * +from .program_name import ( + get_program_name, + set_program_name, +) +from .severity import ( + ALERT, + BENCHMARK, + CRITICAL, + DEBUG0, + DEBUG1, + DEBUG2, + DEBUG3, + DEBUG4, + DEBUG5, + FAIL, + FAILURE, + INFO, + INFORMATIONAL, + NOTICE, + TRACE, + UNSPECIFIED, + VIOLATION, + WARN, + WARNING, + parse_verbosity, + severity_to_string, +) from .tracing import ( dbg, dbgfl, From 36d5eb61d0d99279f88d0a29884b71d6f7308c8f Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:37:24 +1000 Subject: [PATCH 03/16] fix --- diagnosticism/internal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagnosticism/internal/__init__.py b/diagnosticism/internal/__init__.py index bb314d0..b9c6dd1 100644 --- a/diagnosticism/internal/__init__.py +++ b/diagnosticism/internal/__init__.py @@ -166,7 +166,7 @@ def _str2bool(s, default_value=None): return False - s = s.trim().lower() + s = s.strip().lower() if s is not None else None if s in TRUE_STRINGS_lower: From db1b82ed006db7cc00068aed0d622c34ed69d993 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:40:42 +1000 Subject: [PATCH 04/16] doc --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 526a2ea..4f41478 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,9 @@ ## 0.15.3 - 26th June 2026 -* tidying; +* replaced top-level star-imports with explicit imports; +* fixed `_str2bool()` defect; +* general tidying; ## 0.15.2 - 27th August 2025 From 9f7c4866b1f2faf9354d3e24f4bcdf66bbc59de0 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:49:25 +1000 Subject: [PATCH 05/16] chore: added top-level `__all__` documenting the public API --- CHANGES.md | 1 + diagnosticism/__init__.py | 57 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4f41478..98c321b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 0.15.3 - 26th June 2026 * replaced top-level star-imports with explicit imports; +* added top-level `__all__` documenting the public API; * fixed `_str2bool()` defect; * general tidying; diff --git a/diagnosticism/__init__.py b/diagnosticism/__init__.py index 955f943..1755607 100644 --- a/diagnosticism/__init__.py +++ b/diagnosticism/__init__.py @@ -16,6 +16,7 @@ from .contingent_reporting import ( abort, report, + set_default_trailing_prompt, ) from .doomgram import ( DOOMGram, @@ -76,5 +77,61 @@ from .warning import warn +__all__ = [ + '__version__', + + 'ALERT', + 'BENCHMARK', + 'CRITICAL', + 'DEBUG0', + 'DEBUG1', + 'DEBUG2', + 'DEBUG3', + 'DEBUG4', + 'DEBUG5', + 'DOOMGram', + 'DOOMScope', + 'FAIL', + 'FAILURE', + 'INFO', + 'INFORMATIONAL', + 'NOTICE', + 'TRACE', + 'UNSPECIFIED', + 'VIOLATION', + 'WARN', + 'WARNING', + 'abort', + 'dbg', + 'dbgfl', + 'enable_logging', + 'enable_tracing', + 'file', + 'fileline', + 'filelinefunc', + 'func', + 'get_program_name', + 'is_logging_enabled', + 'is_severity_logged', + 'is_tracing_enabled', + 'line', + 'log', + 'parse_verbosity', + 'report', + 'set_default_trailing_prompt', + 'set_log_filter', + 'set_program_name', + 'severity_to_string', + 'trace', + 'warn', +] + +if sys.version_info[:2] >= (3, 9): + __all__.extend([ + 'asynctracefunc', + 'tracefunc', + ]) + + # ############################## end of file ############################# # From 0e4854726b2937de60777a9d5870dabc0069fed8 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 16:53:25 +1000 Subject: [PATCH 06/16] chore: removed `tests` and `examples` from installable package --- CHANGES.md | 3 ++- MANIFEST.in | 3 +++ __init__.py | 4 +++- setup.py | 6 ++---- 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 MANIFEST.in diff --git a/CHANGES.md b/CHANGES.md index 98c321b..293b67b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,9 @@ ## 0.15.3 - 26th June 2026 -* replaced top-level star-imports with explicit imports; * added top-level `__all__` documenting the public API; +* replaced top-level star-imports with explicit imports; +* removed `tests` and `examples` from installable package; * fixed `_str2bool()` defect; * general tidying; diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0dc986e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE README.md CHANGES.md EXAMPLES.md TODO.md +recursive-include examples *.py +recursive-include tests *.py diff --git a/__init__.py b/__init__.py index db720d3..f04f6a1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,4 @@ -# This file present to ensure that unittest's discover operates recursively +# This file present to ensure that unittest's discover operates recursively. +# +# It is not exported from the package. diff --git a/setup.py b/setup.py index d4d5879..35290db 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,10 @@ license='BSD-3-Clause', long_description=open('README.md').read(), long_description_content_type="text/markdown", - packages=[ - 'diagnosticism', - 'diagnosticism.internal', + packages=setuptools.find_packages(exclude=[ 'examples', 'tests', - ], + ]), url='https://github.com/synesissoftware/diagnosticism.Python', ) From 3659e735d5637694b6dc44b4d14e4dfbbc187145 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:02:42 +1000 Subject: [PATCH 07/16] chore: updated/added build scripts --- build_dist.sh | 10 ++++++++-- build_dist_uv.sh | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100755 build_dist_uv.sh diff --git a/build_dist.sh b/build_dist.sh index 138b9e5..77496c2 100755 --- a/build_dist.sh +++ b/build_dist.sh @@ -1,4 +1,10 @@ -#!/bin/bash +#! /bin/bash -python3 setup.py sdist +set -e +cd "$(dirname "$0")" + +rm -rf build/ dist/ diagnosticism.egg-info/ + +python3 -m build +python3 -m twine check dist/* diff --git a/build_dist_uv.sh b/build_dist_uv.sh new file mode 100755 index 0000000..01b7edd --- /dev/null +++ b/build_dist_uv.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +set -e + +cd "$(dirname "$0")" + +if [ ! -d .venv ]; then + uv venv +fi + +rm -rf build/ dist/ diagnosticism.egg-info/ + +uv pip install build twine +uv run python -m build +uv run twine check dist/* From fe44dd9ad44334500aa699771eb414223f4746d0 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:38:11 +1000 Subject: [PATCH 08/16] chore: restoring broken Python 2.7 compatibility --- .github/workflows/python-package.yml | 24 +++++++++++++++-- CHANGES.md | 1 + README.md | 13 +++++++++ diagnosticism/doomgram.py | 35 ++++++++++++------------ diagnosticism/internal/__init__.py | 20 ++++++++++++++ diagnosticism/tracing.py | 6 ++--- examples/doomgram.py | 3 +++ tests/__init__.py | 19 +++++++++++++ tests/test_doomgram.py | 40 ++++++++++++++-------------- 9 files changed, 119 insertions(+), 42 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 98ab029..0c43e25 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ "master", "dev", "gha" ] + branches: [ "master", "dev", "gha", "tidying" ] pull_request: - branches: [ "master", "dev", "gha" ] + branches: [ "master", "dev", "gha", "tidying" ] jobs: build: @@ -41,3 +41,23 @@ jobs: - name: Test with pytest run: | pytest + + build-py27: + + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python 2.7 + uses: actions/setup-python@v5 + with: + python-version: '2.7' + - name: Install dependencies + run: | + python -m pip install --upgrade "pip<21" setuptools mock + pip install -e . + - name: Compile library modules + run: | + python -m compileall diagnosticism + - name: Test with unittest + run: | + python -m unittest discover -s tests diff --git a/CHANGES.md b/CHANGES.md index 293b67b..1d6d0bf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * added top-level `__all__` documenting the public API; * replaced top-level star-imports with explicit imports; +* restoring broken Python 2.7 compatibility; * removed `tests` and `examples` from installable package; * fixed `_str2bool()` defect; * general tidying; diff --git a/README.md b/README.md index 931f34f..b1a4ecf 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Diagnosticism library, for Python - [Introduction](#introduction) - [Installation \& usage](#installation--usage) + - [Python version compatibility](#python-version-compatibility) - [Components](#components) - [Classes](#classes) - [`DOOMGram`](#doomgram) @@ -77,6 +78,18 @@ d.log(sev.INFO, "hello") ``` +### Python version compatibility + +**Diagnosticism.Python** is intended to run on **Python 2.7** and **Python +3.8+**. GitHub Actions currently exercises **Python 3.8–3.13** only. + +| Requirement | Applies to | +| ----------- | ---------- | +| Python **2.7** or **3.8+** | Core contingent reporting, logging, tracing, and severity APIs | +| Python **3.9+** | `@tracefunc` and `@asynctracefunc` decorators | + +The public API surface is listed in `diagnosticism.__all__`. + ## Components diff --git a/diagnosticism/doomgram.py b/diagnosticism/doomgram.py index fda9574..a3987fc 100644 --- a/diagnosticism/doomgram.py +++ b/diagnosticism/doomgram.py @@ -36,8 +36,11 @@ # ######################################################################## # +from .internal import ( + _perf_counter_ns, +) + import math -import time class DOOMGram: @@ -224,33 +227,31 @@ def push_event_time_ns(self, time_in_ns): - if time_in_ns >= 100_000_000: + if time_in_ns >= 100000000: # 100,000,000 - if time_in_ns >= 10_000_000_000: + if time_in_ns >= 10000000000: # 10,000,000,000 - if time_in_ns >= 100_000_000_000: + if time_in_ns >= 100000000000: # 100,000,000,000 self._num_events_ge_100s += 1 else: self._num_events_in_10s += 1 - else: - if time_in_ns >= 1_000_000_000: + if time_in_ns >= 1000000000: # 1,000,000,000 self._num_events_in_1s += 1 else: self._num_events_in_100ms += 1 - else: - if time_in_ns >= 10_000: + if time_in_ns >= 10000: # 10,000 - if time_in_ns >= 1_000_000: + if time_in_ns >= 1000000: # 1,000,000 - if time_in_ns >= 10_000_000: + if time_in_ns >= 10000000: # 10,000,000 self._num_events_in_10ms += 1 else: @@ -259,7 +260,7 @@ def push_event_time_ns(self, time_in_ns): else: - if time_in_ns >= 100_000: + if time_in_ns >= 100000: # 100,000 self._num_events_in_100us += 1 else: @@ -269,7 +270,7 @@ def push_event_time_ns(self, time_in_ns): if time_in_ns >= 100: - if time_in_ns >= 1_000: + if time_in_ns >= 1000: # 1,000 self._num_events_in_1us += 1 else: @@ -289,21 +290,21 @@ def push_event_time_us(self, time_in_us): Pushes an event with the given number of microseconds. """ - self.push_event_time_ns(time_in_us * 1_000) + self.push_event_time_ns(time_in_us * 1000) # 1,000 def push_event_time_ms(self, time_in_ms): """ Pushes an event with the given number of milliseconds. """ - self.push_event_time_ns(time_in_ms * 1_000_000) + self.push_event_time_ns(time_in_ms * 1000000) # 1,000,000 def push_event_time_s(self, time_in_s): """ Pushes an event with the given number of seconds. """ - self.push_event_time_ns(time_in_s * 1_000_000_000) + self.push_event_time_ns(time_in_s * 1000000000) # 1,000,000,000 def to_strip(self, **kwargs): """ @@ -426,13 +427,13 @@ def __init__(self, dg): def __enter__(self): - self._before = time.perf_counter_ns() + self._before = _perf_counter_ns() return self._dg def __exit__(self, x_type, x_val, x_tb): - after = time.perf_counter_ns() + after = _perf_counter_ns() self._dg.push_event_time_ns(after - self._before) diff --git a/diagnosticism/internal/__init__.py b/diagnosticism/internal/__init__.py index b9c6dd1..0e23ad6 100644 --- a/diagnosticism/internal/__init__.py +++ b/diagnosticism/internal/__init__.py @@ -2,6 +2,7 @@ import os import platform import sys +import time TRUE_STRINGS = [ @@ -237,5 +238,24 @@ def _is_python_3_9_or_later(): return sys.version_info[:2] >= (3, 9) +if hasattr(time, 'perf_counter_ns'): + + def _perf_counter_ns(): + """ + Return a performance counter in nanoseconds. + """ + + return time.perf_counter_ns() + +else: + + def _perf_counter_ns(): + """ + Return a performance counter in nanoseconds. + """ + + return int(time.time() * 1000000000) # 1,000,000,000 + + # ############################## end of file ############################# # diff --git a/diagnosticism/tracing.py b/diagnosticism/tracing.py index 0342ba9..0b913a0 100644 --- a/diagnosticism/tracing.py +++ b/diagnosticism/tracing.py @@ -426,19 +426,19 @@ def wrapper(*args, **kwargs): if params_l[-1][1].kind == inspect.Parameter.VAR_KEYWORD: paramK = params_l.pop() - partK = f"{paramK[0]}(dict)={kwargs}" + partK = "%s(dict)=%s" % (paramK[0], kwargs) if len(params_l) != 0: if params_l[-1][1].kind == inspect.Parameter.VAR_POSITIONAL: paramA = params_l.pop() - partA = f"{paramA[0]}(tuple)={args_n[len(params_l):]}" + partA = "%s(tuple)=%s" % (paramA[0], args_n[len(params_l):]) if len(params_l) != 0: namesN = [p[0] for p in params_l] - partN = ', '.join([f"{k}({v.__class__.__name__})={v}" for (k, v) in dict(zip(namesN, args_n)).items()]) + partN = ', '.join(["%s(%s)=%s" % (k, v.__class__.__name__, v) for (k, v) in dict(zip(namesN, args_n)).items()]) fname = func.__name__ diff --git a/examples/doomgram.py b/examples/doomgram.py index af5f771..b0aedee 100755 --- a/examples/doomgram.py +++ b/examples/doomgram.py @@ -1,5 +1,8 @@ #! /usr/bin/env python3 +# NOTE: This example requires Python 3+ (uses DOOMScope / perf_counter_ns and +# PEP 515 numeric literals). + from diagnosticism import ( DOOMGram, DOOMScope, diff --git a/tests/__init__.py b/tests/__init__.py index 2f5dee3..8b38624 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,20 @@ # This file present to deal with warnings from packager + +import sys + +if sys.version_info[0] < 3: + + import unittest + + try: + + import mock + + sys.modules['unittest.mock'] = mock + except ImportError: + + pass + + if not hasattr(unittest.TestCase, 'assertRegex'): + + unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches diff --git a/tests/test_doomgram.py b/tests/test_doomgram.py index 4b464e3..27b585f 100755 --- a/tests/test_doomgram.py +++ b/tests/test_doomgram.py @@ -115,7 +115,7 @@ def test_uniform_spread_timings_1(self): self.assertEqual(789123456789, dg.total_event_time_ns()) self.assertEqual(9, dg.min_event_time_ns()) - self.assertEqual(700_000_000_000, dg.max_event_time_ns()) + self.assertEqual(700000000000, dg.max_event_time_ns()) # 700,000,000,000 self.assertEqual(1, dg.num_events_in_1ns()) self.assertEqual(1, dg.num_events_in_10ns()) @@ -154,7 +154,7 @@ def test_uniform_spread_timings_2(self): self.assertEqual(789123456789, dg.total_event_time_ns()) self.assertEqual(9, dg.min_event_time_ns()) - self.assertEqual(700_000_000_000, dg.max_event_time_ns()) + self.assertEqual(700000000000, dg.max_event_time_ns()) # 700,000,000,000 self.assertEqual(1, dg.num_events_in_1ns()) self.assertEqual(1, dg.num_events_in_10ns()) @@ -193,7 +193,7 @@ def test_uniform_spread_timings_3(self): self.assertEqual(789123456789, dg.total_event_time_ns()) self.assertEqual(9, dg.min_event_time_ns()) - self.assertEqual(700_000_000_000, dg.max_event_time_ns()) + self.assertEqual(700000000000, dg.max_event_time_ns()) # 700,000,000,000 self.assertEqual(1, dg.num_events_in_1ns()) self.assertEqual(1, dg.num_events_in_10ns()) @@ -228,8 +228,8 @@ def test_uniform_spread_timings_4(self): self.assertEqual(9, dg.event_count()) self.assertEqual(789123456000, dg.total_event_time_ns()) - self.assertEqual(6_000, dg.min_event_time_ns()) - self.assertEqual(700_000_000_000, dg.max_event_time_ns()) + self.assertEqual(6000, dg.min_event_time_ns()) # 6,000 + self.assertEqual(700000000000, dg.max_event_time_ns()) # 700,000,000,000 self.assertEqual(0, dg.num_events_in_1ns()) self.assertEqual(0, dg.num_events_in_10ns()) @@ -264,7 +264,7 @@ def test_several_distinct_timings(self): self.assertEqual(314248103033, dg.total_event_time_ns()) self.assertEqual(10, dg.min_event_time_ns()) - self.assertEqual(309_000_000_000, dg.max_event_time_ns()) + self.assertEqual(309000000000, dg.max_event_time_ns()) # 309,000,000,000 self.assertEqual(0, dg.num_events_in_1ns()) self.assertEqual(2, dg.num_events_in_10ns()) @@ -286,22 +286,22 @@ def test_several_intersecting_timings(self): dg = DOOMGram() - dg.push_event_time_ns( 11) - dg.push_event_time_ns( 19) - dg.push_event_time_ns( 19) - dg.push_event_time_us( 7) - dg.push_event_time_us( 7) - dg.push_event_time_us( 89) - dg.push_event_time_ms( 248) - dg.push_event_time_ms(4_321) - dg.push_event_time_s( 5) - dg.push_event_time_s( 309) + dg.push_event_time_ns( 11) + dg.push_event_time_ns( 19) + dg.push_event_time_ns( 19) + dg.push_event_time_us( 7) + dg.push_event_time_us( 7) + dg.push_event_time_us( 89) + dg.push_event_time_ms( 248) + dg.push_event_time_ms(4321) # 4,321 + dg.push_event_time_s( 5) + dg.push_event_time_s( 309) self.assertEqual(10, dg.event_count()) self.assertEqual(318569103049, dg.total_event_time_ns()) self.assertEqual(11, dg.min_event_time_ns()) - self.assertEqual(309_000_000_000, dg.max_event_time_ns()) + self.assertEqual(309000000000, dg.max_event_time_ns()) # 309,000,000,000 self.assertEqual(0, dg.num_events_in_1ns()) self.assertEqual(3, dg.num_events_in_10ns()) @@ -335,12 +335,12 @@ def test_many_cumulative_timings(self): self.assertEqual(10101010000, dg.total_event_time_ns()) self.assertEqual(1, dg.min_event_time_ns()) - self.assertEqual(1_000_000_000, dg.max_event_time_ns()) + self.assertEqual(1000000000, dg.max_event_time_ns()) # 1,000,000,000 - self.assertEqual(10_000, dg.num_events_in_1ns()) + self.assertEqual(10000, dg.num_events_in_1ns()) # 10,000 self.assertEqual(0, dg.num_events_in_10ns()) self.assertEqual(0, dg.num_events_in_100ns()) - self.assertEqual(1_000, dg.num_events_in_1us()) + self.assertEqual(1000, dg.num_events_in_1us()) # 1,000 self.assertEqual(0, dg.num_events_in_10us()) self.assertEqual(0, dg.num_events_in_100us()) self.assertEqual(100, dg.num_events_in_1ms()) From e0afc06137d383b7fed70fd5a73c2fe11903624e Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:47:22 +1000 Subject: [PATCH 09/16] fix --- .github/workflows/python-package.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0c43e25..27374e8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -44,13 +44,11 @@ jobs: build-py27: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: + image: python:2.7-slim steps: - uses: actions/checkout@v4 - - name: Set up Python 2.7 - uses: actions/setup-python@v5 - with: - python-version: '2.7' - name: Install dependencies run: | python -m pip install --upgrade "pip<21" setuptools mock From 70c660b60b1ff6bed67a021a1171625f0f33a656 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:55:51 +1000 Subject: [PATCH 10/16] chore: adding missing test file --- tests/test_package.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/test_package.py diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..41d0445 --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,12 @@ +#! /usr/bin/env python3 + +import unittest + +import diagnosticism + + +class Package_tester(unittest.TestCase): + def test_all_names_are_defined(self): + + for name in diagnosticism.__all__: + self.assertTrue(hasattr(diagnosticism, name), name) From 95fd68c7d608d0a76acd4e4db1d55631ae1b5f62 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:57:03 +1000 Subject: [PATCH 11/16] chore: Python 2.x compatibility --- diagnosticism/contingent_reporting.py | 2 +- diagnosticism/severity.py | 2 +- diagnosticism/tracing.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/diagnosticism/contingent_reporting.py b/diagnosticism/contingent_reporting.py index b92a59e..373365b 100644 --- a/diagnosticism/contingent_reporting.py +++ b/diagnosticism/contingent_reporting.py @@ -181,7 +181,7 @@ def _do_report( def conrep( message, - **kwargs, + **kwargs ): """ DEPRECATED: use `report()`. diff --git a/diagnosticism/severity.py b/diagnosticism/severity.py index 5bcf847..1563631 100644 --- a/diagnosticism/severity.py +++ b/diagnosticism/severity.py @@ -212,7 +212,7 @@ def _parse_verbosity( def parse_verbosity( v, - **kwargs, + **kwargs ): """ Attempts to parse the verbosity from `v`. diff --git a/diagnosticism/tracing.py b/diagnosticism/tracing.py index 0b913a0..4a045a4 100644 --- a/diagnosticism/tracing.py +++ b/diagnosticism/tracing.py @@ -42,7 +42,7 @@ def _dbg( fr, show_fileline, *args, - **kwargs, + **kwargs ): try: @@ -95,7 +95,7 @@ def _dbg( def _flf( depth=1, - **kwargs, + **kwargs ): assert 1 == depth @@ -144,7 +144,7 @@ def _log_s( def dbgfl( *args, - **kwargs, + **kwargs ): """ Traces arguments and keyword-arguments with file + line. @@ -171,7 +171,7 @@ def dbgfl( def dbg( *args, - **kwargs, + **kwargs ): """ Traces arguments and keyword-arguments. @@ -224,7 +224,7 @@ def enable_tracing(*args): def file( - **kwargs, + **kwargs ): """ Obtains the file of the caller. @@ -238,7 +238,7 @@ def file( def func( - **kwargs, + **kwargs ): """ Obtains the function of the caller. @@ -252,7 +252,7 @@ def func( def line( - **kwargs, + **kwargs ): """ Obtains the line of the caller. @@ -266,7 +266,7 @@ def line( def fileline( - **kwargs, + **kwargs ): """ Obtains a string representing the file and line of the caller. @@ -285,7 +285,7 @@ def fileline( def filelinefunc( - **kwargs, + **kwargs ): """ Obtains a string representing the file, line, and function of the From 65a12fc7aa0bc876281aab7cbee3502147dafc09 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:57:56 +1000 Subject: [PATCH 12/16] chore: Python 2.x compatibility --- diagnosticism/warning.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/diagnosticism/warning.py b/diagnosticism/warning.py index c8d2160..b291ddd 100644 --- a/diagnosticism/warning.py +++ b/diagnosticism/warning.py @@ -45,9 +45,7 @@ def _warn( def warn( *message_lines, - file=None, - file_cr=None, - file_dl=None, + **kwargs ): """ Analogue of Ruby's `Kernel#warn()` @@ -77,6 +75,10 @@ def warn( None """ + file = kwargs.get('file', None) + file_cr = kwargs.get('file_cr', None) + file_dl = kwargs.get('file_dl', None) + if file_cr is None: file_cr = file From 38317441c35566ddfa1edfe32cc89464b716e408 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 17:58:56 +1000 Subject: [PATCH 13/16] chore: Python 2.x compatibility --- diagnosticism/tracing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diagnosticism/tracing.py b/diagnosticism/tracing.py index 4a045a4..b85a354 100644 --- a/diagnosticism/tracing.py +++ b/diagnosticism/tracing.py @@ -165,7 +165,7 @@ def dbgfl( fr, True, *args, - **kwargs, + **kwargs ) @@ -192,7 +192,7 @@ def dbg( fr, False, *args, - **kwargs, + **kwargs ) From 6637b48f5c66bde8e2be00dcc26c06b30a6cc2d7 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 26 Jun 2026 18:04:37 +1000 Subject: [PATCH 14/16] chore: Python 2.x compatibility --- .github/workflows/python-package.yml | 2 +- tests/__init__.py | 4 ++- tests/run_unittest.py | 39 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/run_unittest.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 27374e8..4a9550c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -58,4 +58,4 @@ jobs: python -m compileall diagnosticism - name: Test with unittest run: | - python -m unittest discover -s tests + python tests/run_unittest.py diff --git a/tests/__init__.py b/tests/__init__.py index 8b38624..6c47654 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,7 +13,9 @@ sys.modules['unittest.mock'] = mock except ImportError: - pass + raise ImportError( + "the 'mock' package is required to run tests on Python 2.7", + ) if not hasattr(unittest.TestCase, 'assertRegex'): diff --git a/tests/run_unittest.py b/tests/run_unittest.py new file mode 100644 index 0000000..68445fb --- /dev/null +++ b/tests/run_unittest.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +unittest entry point. + +Imports the `tests` package first so tests/__init__.py can install +Python 2.7 compatibility shims before test modules load. +""" + +import os +import sys +import unittest + + +def main(): + + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + if root not in sys.path: + + sys.path.insert(0, root) + + import tests # noqa: F401 + + start_dir = os.path.join(root, 'tests') + loader = unittest.TestLoader() + suite = loader.discover( + start_dir, + pattern='test_*.py', + top_level_dir=root, + ) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + sys.exit(0 if result.wasSuccessful() else 1) + + +if __name__ == '__main__': + + main() From 4289734cee74af7a52da631807b4541673bf90a4 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 27 Jun 2026 07:40:21 +1000 Subject: [PATCH 15/16] chore: Python 2.x compatibility --- .github/workflows/python-package.yml | 12 ++++++++++-- CHANGES.md | 3 ++- README.md | 2 +- setup.py | 3 ++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4a9550c..737bb88 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,17 @@ name: Python package on: push: - branches: [ "master", "dev", "gha", "tidying" ] + branches: + - master + - dev + - gha + - tidying pull_request: - branches: [ "master", "dev", "gha", "tidying" ] + branches: + - master + - dev + - gha + - tidying jobs: build: diff --git a/CHANGES.md b/CHANGES.md index 1d6d0bf..5791791 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,13 @@ # **Diagnosticism.Python** Changes -## 0.15.3 - 26th June 2026 +## 0.15.3 - 27th June 2026 * added top-level `__all__` documenting the public API; * replaced top-level star-imports with explicit imports; * restoring broken Python 2.7 compatibility; * removed `tests` and `examples` from installable package; * fixed `_str2bool()` defect; +* added `python_requires` for Python 2.7 and Python 3.8+; * general tidying; diff --git a/README.md b/README.md index b1a4ecf..a529ad1 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ d.log(sev.INFO, "hello") ### Python version compatibility **Diagnosticism.Python** is intended to run on **Python 2.7** and **Python -3.8+**. GitHub Actions currently exercises **Python 3.8–3.13** only. +3.8+**. GitHub Actions exercises **Python 2.7** and **Python 3.8–3.13**. | Requirement | Applies to | | ----------- | ---------- | diff --git a/setup.py b/setup.py index 35290db..b10bcbc 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ name='diagnosticism', version='0.15.3', + python_requires='>=2.7,<3.0 || >=3.8', author='Matt Wilson', author_email='matthew@synesis.com.au', @@ -15,7 +16,7 @@ 'Natural Language :: English', "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From ff11a3598d8b85cbd3fd041dafaedda155d21ee5 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 27 Jun 2026 08:56:10 +1000 Subject: [PATCH 16/16] fix --- .github/workflows/python-package.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 737bb88..c5e9d20 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -60,10 +60,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade "pip<21" setuptools mock - pip install -e . - name: Compile library modules run: | python -m compileall diagnosticism - name: Test with unittest run: | python tests/run_unittest.py + env: + PYTHONPATH: ${{ github.workspace }}