punit 1.5.3__tar.gz → 1.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {punit-1.5.3/src/pUnit.egg-info → punit-1.7.3}/PKG-INFO +1 -1
  2. {punit-1.5.3 → punit-1.7.3}/pyproject.toml +1 -1
  3. {punit-1.5.3 → punit-1.7.3/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/SOURCES.txt +6 -0
  5. {punit-1.5.3 → punit-1.7.3}/src/punit/TestResult.py +24 -4
  6. {punit-1.5.3 → punit-1.7.3}/src/punit/__init__.py +6 -2
  7. {punit-1.5.3 → punit-1.7.3}/src/punit/__main__.py +39 -28
  8. {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/numeric.py +4 -2
  9. {punit-1.5.3 → punit-1.7.3}/src/punit/cli.py +34 -13
  10. {punit-1.5.3 → punit-1.7.3}/src/punit/facts/Fact.py +15 -5
  11. {punit-1.5.3 → punit-1.7.3}/src/punit/facts/FactManager.py +3 -1
  12. {punit-1.5.3 → punit-1.7.3}/src/punit/filters/Filter.py +13 -2
  13. {punit-1.5.3 → punit-1.7.3}/src/punit/filters/FilterManager.py +3 -1
  14. {punit-1.5.3 → punit-1.7.3}/src/punit/metadata/CallableMetadata.py +2 -1
  15. {punit-1.5.3 → punit-1.7.3}/src/punit/reports/JUnitReportGenerator.py +2 -2
  16. punit-1.7.3/src/punit/runner.py +242 -0
  17. punit-1.7.3/src/punit/setups/Setup.py +100 -0
  18. punit-1.7.3/src/punit/setups/SetupManager.py +69 -0
  19. punit-1.7.3/src/punit/setups/__init__.py +11 -0
  20. punit-1.7.3/src/punit/teardowns/Teardown.py +100 -0
  21. punit-1.7.3/src/punit/teardowns/TeardownManager.py +69 -0
  22. punit-1.7.3/src/punit/teardowns/__init__.py +11 -0
  23. {punit-1.5.3 → punit-1.7.3}/src/punit/theories/Theory.py +32 -5
  24. {punit-1.5.3 → punit-1.7.3}/src/punit/theories/TheoryManager.py +3 -1
  25. {punit-1.5.3 → punit-1.7.3}/src/punit/traits/TraitManager.py +3 -2
  26. punit-1.5.3/src/punit/runner.py +0 -124
  27. {punit-1.5.3 → punit-1.7.3}/LICENSE +0 -0
  28. {punit-1.5.3 → punit-1.7.3}/README.md +0 -0
  29. {punit-1.5.3 → punit-1.7.3}/setup.cfg +0 -0
  30. {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/dependency_links.txt +0 -0
  31. {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/entry_points.txt +0 -0
  32. {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/requires.txt +0 -0
  33. {punit-1.5.3 → punit-1.7.3}/src/pUnit.egg-info/top_level.txt +0 -0
  34. {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/__init__.py +0 -0
  35. {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/collections.py +0 -0
  36. {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/exceptions.py +0 -0
  37. {punit-1.5.3 → punit-1.7.3}/src/punit/assertions/strings.py +0 -0
  38. {punit-1.5.3 → punit-1.7.3}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  39. {punit-1.5.3 → punit-1.7.3}/src/punit/discovery/__init__.py +0 -0
  40. {punit-1.5.3 → punit-1.7.3}/src/punit/facts/__init__.py +0 -0
  41. {punit-1.5.3 → punit-1.7.3}/src/punit/filters/__init__.py +0 -0
  42. {punit-1.5.3 → punit-1.7.3}/src/punit/metadata/__init__.py +0 -0
  43. {punit-1.5.3 → punit-1.7.3}/src/punit/py.typed +0 -0
  44. {punit-1.5.3 → punit-1.7.3}/src/punit/reports/HtmlReportGenerator.py +0 -0
  45. {punit-1.5.3 → punit-1.7.3}/src/punit/reports/JsonReportGenerator.py +0 -0
  46. {punit-1.5.3 → punit-1.7.3}/src/punit/reports/__init__.py +0 -0
  47. {punit-1.5.3 → punit-1.7.3}/src/punit/theories/__init__.py +0 -0
  48. {punit-1.5.3 → punit-1.7.3}/src/punit/traits/Trait.py +0 -0
  49. {punit-1.5.3 → punit-1.7.3}/src/punit/traits/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.5.3
3
+ Version: 1.7.3
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pUnit"
3
- version = "1.5.3"
3
+ version = "1.7.3"
4
4
  description = "A modernized unit-test framework for Python."
5
5
  keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest"]
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.5.3
3
+ Version: 1.7.3
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -32,6 +32,12 @@ src/punit/reports/HtmlReportGenerator.py
32
32
  src/punit/reports/JUnitReportGenerator.py
33
33
  src/punit/reports/JsonReportGenerator.py
34
34
  src/punit/reports/__init__.py
35
+ src/punit/setups/Setup.py
36
+ src/punit/setups/SetupManager.py
37
+ src/punit/setups/__init__.py
38
+ src/punit/teardowns/Teardown.py
39
+ src/punit/teardowns/TeardownManager.py
40
+ src/punit/teardowns/__init__.py
35
41
  src/punit/theories/Theory.py
36
42
  src/punit/theories/TheoryManager.py
37
43
  src/punit/theories/__init__.py
@@ -27,6 +27,22 @@ class TextIOCapture:
27
27
 
28
28
 
29
29
  class TestResult:
30
+ """
31
+ Accumulates the outcome of a single test execution.
32
+
33
+ Example
34
+ -------
35
+
36
+ Create and populate a result manually:
37
+
38
+ .. code-block:: python
39
+
40
+ result = TestResult()
41
+ result.module_name = 'tests.example'
42
+ result.test_name = 'test_something'
43
+ result.is_success = True
44
+
45
+ """
30
46
 
31
47
  __class_name: Optional[str]
32
48
  __exception: Exception | None
@@ -59,7 +75,11 @@ class TestResult:
59
75
 
60
76
  @property
61
77
  def class_name(self) -> Optional[str]:
62
- return self.__class_name if self.__class_name is not None and len(self.__class_name) > 0 else None
78
+ return (
79
+ self.__class_name
80
+ if self.__class_name is not None and len(self.__class_name) > 0
81
+ else None
82
+ )
63
83
 
64
84
  @class_name.setter
65
85
  def class_name(self, value: Optional[str]) -> None:
@@ -171,15 +191,15 @@ class TestResult:
171
191
  elif took >= 0.000000001:
172
192
  return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
173
193
  else:
174
- return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
194
+ return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'ns'
175
195
 
176
- def captureOutput(self, quiet: bool = False) -> None:
196
+ def capture_output(self, quiet: bool = False) -> None:
177
197
  self.__stdout_capture = TextIOCapture(sys.stdout, quiet)
178
198
  self.__stderr_capture = TextIOCapture(sys.stderr, quiet)
179
199
  sys.stdout = self.__stdout_capture
180
200
  sys.stderr = self.__stderr_capture
181
201
 
182
- def releaseOutput(self) -> None:
202
+ def release_output(self) -> None:
183
203
  if self.__stdout_capture is not None and self.__stdout_capture.target is not None:
184
204
  sys.stdout = self.__stdout_capture.target
185
205
  if self.__stderr_capture is not None and self.__stderr_capture.target is not None:
@@ -3,6 +3,8 @@
3
3
 
4
4
  from .assertions import collections, exceptions, numeric, strings
5
5
  from .facts import fact
6
+ from .setups import Setup, setup
7
+ from .teardowns import Teardown, teardown
6
8
  from .theories import theory, inlinedata
7
9
  from .traits import trait
8
10
 
@@ -10,8 +12,8 @@ from .assertions.exceptions import raises
10
12
  from .assertions.numeric import approx
11
13
 
12
14
 
13
- __version__ = '1.5.3'
14
- __commit__ = '34475af'
15
+ __version__ = '1.7.3'
16
+ __commit__ = 'a403690'
15
17
  __all__ = [
16
18
  '__version__', '__commit__',
17
19
  'assertions',
@@ -21,6 +23,8 @@ __all__ = [
21
23
  'strings',
22
24
  'fact',
23
25
  'metadata',
26
+ 'setup', 'Setup',
27
+ 'teardown', 'Teardown',
24
28
  'theory', 'inlinedata',
25
29
  'trait'
26
30
  ]
@@ -11,40 +11,42 @@ from .cli import CommandLineInterface
11
11
  from .discovery import TestModuleDiscovery
12
12
  from .reports import HtmlReportGenerator, JUnitReportGenerator, JsonReportGenerator
13
13
  from .runner import TestRunner
14
+ from .setups.SetupManager import SetupManager
15
+ from .teardowns.TeardownManager import TeardownManager
14
16
 
15
17
 
16
18
  async def async_main() -> None:
17
- ts = time.time()
19
+ start_time = time.time()
18
20
  cli = CommandLineInterface.parse()
19
21
  if cli.help: # pragma: no cover
20
- cli.printHelp()
22
+ cli.print_help()
21
23
  elif cli.verbose and not cli.quiet: # pragma: no cover
22
- cli.printSummary()
24
+ cli.print_summary()
23
25
  elif not cli.quiet: # pragma: no cover
24
- cli.printVersion()
26
+ cli.print_version()
25
27
  os.chdir(cli.workdir)
26
28
  if cli.no_pathfix is not True:
27
- pathbase = str(Path.cwd())
28
- srcbase = os.path.join(pathbase, 'src')
29
- if os.path.exists(srcbase) and srcbase not in sys.path:
30
- sys.path.append(srcbase)
31
- if pathbase not in sys.path:
32
- sys.path.append(pathbase)
33
- testModuleDiscovery = TestModuleDiscovery(
29
+ cwd_path = str(Path.cwd())
30
+ src_path = os.path.join(cwd_path, 'src')
31
+ if os.path.exists(src_path) and src_path not in sys.path:
32
+ sys.path.append(src_path)
33
+ if cwd_path not in sys.path:
34
+ sys.path.append(cwd_path)
35
+ test_module_discovery = TestModuleDiscovery(
34
36
  os.path.join(cli.workdir, cli.test_package_name),
35
37
  cli.includePatterns,
36
38
  cli.excludePatterns,
37
39
  cli)
38
- testModuleDiscovery.discover()
39
- testRunner = TestRunner(cli.test_package_name, testModuleDiscovery.filenames, cli)
40
- results = await testRunner.run()
41
- totalTime = time.time() - ts
42
- failureCount = 0
40
+ test_module_discovery.discover()
41
+ test_runner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
42
+ results = await test_runner.run()
43
+ total_time = time.time() - start_time
44
+ failure_count = 0
43
45
  for result in results:
44
46
  if not result.is_success:
45
- failureCount += 1 # pragma: no cover
47
+ failure_count += 1 # pragma: no cover
46
48
  if not cli.quiet:
47
- print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
49
+ print(f'Total: {len(results)}, Failures: {failure_count}, Took: {total_time:.3f}s')
48
50
  if cli.reportFormat is not None: # pragma: no cover
49
51
  report: str = ''
50
52
  match cli.reportFormat:
@@ -61,16 +63,25 @@ async def async_main() -> None:
61
63
  with open(cli.outputFilename, 'wb') as file:
62
64
  file.write(report.encode())
63
65
  print(f'\n("{cli.reportFormat}" report written to: {cli.outputFilename})')
64
- if failureCount > 0:
65
- sys.stdout.flush()
66
- sys.stderr.flush()
67
- try:
68
- os.fsync(sys.stdout.fileno())
69
- os.fsync(sys.stderr.fileno())
70
- except (AttributeError, ValueError, OSError):
71
- # Pass if the stream doesn't support fsync (e.g. some virtualized environments)
72
- pass
73
- if cli.no_exitcode is not True: # pragma: no cover
66
+
67
+ # not everyone runs python unbuffered as they should, so force a flush
68
+ sys.stdout.flush()
69
+ sys.stderr.flush()
70
+ try:
71
+ os.fsync(sys.stdout.fileno())
72
+ os.fsync(sys.stderr.fileno())
73
+ except (AttributeError, ValueError, OSError):
74
+ pass
75
+
76
+ if cli.no_exitcode is not True:
77
+ if failure_count:
78
+ # test failures trigger exit code 119 (for automation gating)
79
+ sys.exit(119)
80
+ if TeardownManager.instance().teardown_error_count > 0:
81
+ # teardown errors also trigger exit code 119 (for automation gating)
82
+ sys.exit(119)
83
+ if SetupManager.instance().setup_error_count > 0:
84
+ # setup errors also trigger exit code 119 (for automation gating)
74
85
  sys.exit(119)
75
86
 
76
87
 
@@ -1,6 +1,8 @@
1
1
  # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  from decimal import Decimal
5
7
  import math
6
8
  from typing import cast
@@ -397,7 +399,7 @@ class ApproxRangeComparator:
397
399
  self._abs_tol = abs_tol
398
400
  self._exclusive = False
399
401
 
400
- def inclusive(self, flag: bool = True) -> 'ApproxRangeComparator':
402
+ def inclusive(self, flag: bool = True) -> ApproxRangeComparator:
401
403
  """
402
404
  Return a new ``ApproxRangeComparator`` with toggled inclusivity.
403
405
 
@@ -617,7 +619,7 @@ class approx:
617
619
  """Return a ``StrictLessThanComparator`` for strictly ``< expected`` (one-sided tolerance below)."""
618
620
  return StrictLessThanComparator(self._expected, self._rel_tol, self._abs_tol)
619
621
 
620
- def in_range(self, min_val: float, max_val: float) -> 'ApproxRangeComparator':
622
+ def in_range(self, min_val: float, max_val: float) -> ApproxRangeComparator:
621
623
  """Return a comparator for ``[min_val, max_val]`` range checks with directional tolerance."""
622
624
  return ApproxRangeComparator(min_val, max_val, self._rel_tol, self._abs_tol)
623
625
 
@@ -1,6 +1,8 @@
1
1
  # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import os
5
7
  import sys
6
8
  from typing import Optional
@@ -10,6 +12,20 @@ from .traits import Trait
10
12
 
11
13
 
12
14
  class CommandLineInterface:
15
+ """
16
+ Command-line argument parser and display helper for pUnit.
17
+
18
+ Example
19
+ -------
20
+
21
+ Parse CLI arguments from ``sys.argv``:
22
+
23
+ .. code-block:: python
24
+
25
+ cli = CommandLineInterface.parse()
26
+ print(cli.test_package_name)
27
+
28
+ """
13
29
 
14
30
  __aliases: dict[str, str]
15
31
  __excludePatterns: list[str]
@@ -46,7 +62,13 @@ class CommandLineInterface:
46
62
  self.__workdir = os.path.curdir
47
63
  self.__verbose = False
48
64
 
49
- def __parse(self, argv: list[str]) -> 'CommandLineInterface':
65
+ def __parse(self, argv: list[str]) -> CommandLineInterface:
66
+ """
67
+ Parse command-line arguments using a finite-state machine.
68
+
69
+ :param argv: List of argument strings (typically ``sys.argv``).
70
+ :returns: ``self`` for method chaining.
71
+ """
50
72
  aliasName: str | None = None
51
73
  extractFilter: bool = False
52
74
  extractExcludePattern: bool = False
@@ -158,7 +180,7 @@ class CommandLineInterface:
158
180
  FilterManager.instance().add(arg)
159
181
 
160
182
  @property
161
- def aliases(self) -> dict:
183
+ def aliases(self) -> dict[str, str]:
162
184
  return self.__aliases
163
185
 
164
186
  @property
@@ -211,16 +233,15 @@ class CommandLineInterface:
211
233
 
212
234
  @property
213
235
  def no_exitcode(self) -> bool:
214
- return self.__no_exitcode is True
236
+ return self.__no_exitcode
215
237
 
216
238
  @property
217
239
  def no_pathfix(self) -> bool:
218
- return self.__no_pathfix is True
240
+ return self.__no_pathfix
219
241
 
220
- def printHelp(self) -> None:
221
- if True: # pragma: no cover
222
- self.printVersion()
223
- print("""
242
+ def print_help(self) -> None:
243
+ self.print_version()
244
+ print("""
224
245
  Usage: python3 -m punit [-h|--help]
225
246
  [-q|--quiet] [-v|--verbose]
226
247
  [-z|--failfast]
@@ -279,10 +300,10 @@ Options:
279
300
  write to FILENAME. In this case `--report` does not
280
301
  suppress any program output.
281
302
  """)
282
- os._exit(0)
303
+ os._exit(0)
283
304
 
284
- def printSummary(self) -> None:
285
- self.printVersion()
305
+ def print_summary(self) -> None:
306
+ self.print_version()
286
307
  print(f'Working Directory:\n\t{self.__workdir}')
287
308
  print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
288
309
  if len(self.__includePatterns) > 0:
@@ -295,7 +316,7 @@ Options:
295
316
  print(f'\t{pattern}')
296
317
  FilterManager.instance().print()
297
318
 
298
- def printVersion(self) -> None:
319
+ def print_version(self) -> None:
299
320
  print(f'pUnit {__version__} ({__commit__})')
300
321
 
301
322
  def validate(self) -> None:
@@ -314,7 +335,7 @@ Options:
314
335
  self.__excludePatterns.append('/__*__')
315
336
 
316
337
  @staticmethod
317
- def parse(argv: list[str] = sys.argv) -> 'CommandLineInterface':
338
+ def parse(argv: list[str] = sys.argv) -> CommandLineInterface:
318
339
  result = CommandLineInterface().__parse(argv)
319
340
  result.validate()
320
341
  return result
@@ -3,7 +3,7 @@
3
3
 
4
4
  import inspect
5
5
  from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType, ModuleType
6
- from typing import Callable, Coroutine, Union, cast
6
+ from typing import Any, Callable, Coroutine, Union, cast
7
7
 
8
8
  from ..metadata import CallableMetadata
9
9
 
@@ -24,13 +24,14 @@ class Fact:
24
24
  def target(self) -> Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]:
25
25
  return self.__target
26
26
 
27
- async def execute(self, module: ModuleType) -> None:
27
+ async def execute(self, module: ModuleType) -> Any | None:
28
+ class_instance: Any | None = None
28
29
  coro: Coroutine | None = None
29
30
  if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
30
31
  if isinstance(self.__target, staticmethod):
31
32
  coro = self.__target()
32
33
  else:
33
- qnparts = self.__target.__qualname__.split('.')
34
+ qnparts = [p for p in self.__target.__qualname__.split('.') if p != '<locals>']
34
35
  qntarget = module
35
36
  for qnpart in qnparts[0:-1]:
36
37
  qntarget = getattr(qntarget, qnpart)
@@ -38,17 +39,26 @@ class Fact:
38
39
  coro = self.__target.__func__(qntarget)
39
40
  else:
40
41
  # every test execution gets a new instance of class
41
- coro = self.__target(cast(Callable, qntarget)())
42
+ class_instance = cast(Any, cast(Callable, qntarget)())
43
+ coro = self.__target(class_instance)
42
44
  else:
43
45
  coro = self.__target()
44
46
  if inspect.iscoroutine(coro):
45
47
  await coro
48
+ return class_instance
46
49
 
47
50
 
48
51
  def fact(target: Callable) -> Callable:
49
52
  from .FactManager import FactManager
50
- if (not inspect.isfunction(target)) and (not isinstance(target, classmethod)) and (not isinstance(target, staticmethod)):
53
+ unwrapped = inspect.unwrap(target)
54
+ if not isinstance(unwrapped, (FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType)):
51
55
  raise Exception('@fact can only be applied to functions and methods.')
56
+ if hasattr(unwrapped, '__punit_decorator'):
57
+ raise Exception(
58
+ f'@fact and {getattr(unwrapped, "__punit_decorator")} cannot decorate the same function. '
59
+ f'Function "{unwrapped.__name__}" has already been decorated.'
60
+ )
61
+ setattr(unwrapped, '__punit_decorator', '@fact')
52
62
  fact: Fact = Fact(target)
53
63
  FactManager.instance().put(fact)
54
64
  return target
@@ -2,6 +2,8 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
  ##
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  from typing import Optional
6
8
 
7
9
  from ..filters.FilterManager import FilterManager
@@ -23,7 +25,7 @@ class FactManager:
23
25
  self.__modules = {}
24
26
 
25
27
  @staticmethod
26
- def instance() -> 'FactManager':
28
+ def instance() -> FactManager:
27
29
  if FactManager.__instance is None:
28
30
  FactManager.__instance = FactManager()
29
31
  return FactManager.__instance
@@ -5,17 +5,28 @@ import re
5
5
 
6
6
 
7
7
  class Filter:
8
+ """
9
+ Wraps a glob-style filter pattern and compiles it to a case-insensitive regex.
10
+
11
+ Patterns use ``*`` for multi-character wildcards and ``?`` for single characters.
12
+ Prefix with ``!`` to mark the filter as an exclusion rule.
13
+ """
8
14
 
9
15
  __isExclude: bool
10
16
  __pattern: str
11
- __re: re.Pattern
17
+ __re: re.Pattern[str]
12
18
 
13
19
  def __init__(self, pattern: str) -> None:
14
20
  self.__isExclude = pattern.startswith('!')
15
21
  self.__pattern = pattern
16
22
  self.__re = self.__toRegex(pattern)
17
23
 
18
- def __toRegex(self, pattern: str) -> re.Pattern:
24
+ def __toRegex(self, pattern: str) -> re.Pattern[str]:
25
+ """Convert a glob-style pattern to a compiled regex.
26
+
27
+ Supports ``*`` (multi-char wildcard), ``?`` (single-char wildcard),
28
+ and ``!`` prefix (exclusion). Matching is case-insensitive.
29
+ """
19
30
  if self.__isExclude:
20
31
  pattern = pattern[1:]
21
32
  pattern = re.escape(pattern)\
@@ -1,6 +1,8 @@
1
1
  # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import os
5
7
  import select
6
8
  import sys
@@ -22,7 +24,7 @@ class FilterManager:
22
24
  return self.__filters
23
25
 
24
26
  @staticmethod
25
- def instance() -> 'FilterManager':
27
+ def instance() -> FilterManager:
26
28
  instance = FilterManager.__instance
27
29
  if instance is None:
28
30
  instance = FilterManager()
@@ -14,7 +14,8 @@ class CallableMetadata:
14
14
 
15
15
  def __init__(self, callable: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]) -> None:
16
16
  self.__callable = callable
17
- self.__class_name = '.'.join(callable.__qualname__.replace(f'{callable.__module__}.', '').split('.')[0:-1])
17
+ parts = callable.__qualname__.replace(f'{callable.__module__}.', '').split('.')[0:-1]
18
+ self.__class_name = '.'.join(p for p in parts if p != '<locals>')
18
19
  self.__module_name = callable.__module__
19
20
  self.__name = callable.__name__
20
21
 
@@ -65,7 +65,7 @@ class JUnitTestSuite:
65
65
  result: int = 0
66
66
  if self.testCases is not None:
67
67
  for testCase in self.testCases:
68
- if testCase.disabled == True:
68
+ if testCase.disabled is True:
69
69
  result += 1
70
70
  return result
71
71
 
@@ -187,7 +187,7 @@ class JUnitReportGenerator:
187
187
  ele.text = testCase.stderr
188
188
  testSuitesEle.attrib['disabled'] = str(totalDisabledCount)
189
189
  testSuitesEle.attrib['errors'] = str(totalErrorCount)
190
- testSuitesEle.attrib['failures'] = str(totalErrorCount)
190
+ testSuitesEle.attrib['failures'] = str(totalFailureCount)
191
191
  testSuitesEle.attrib['tests'] = str(totalTestCount)
192
192
  testSuitesEle.attrib['time'] = f'{totalTime:.6f}'.rstrip('0').rstrip('.')
193
193
  et.indent(testSuitesEle, space=" ")