punit 1.5.2__tar.gz → 1.5.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 (46) hide show
  1. {punit-1.5.2/src/pUnit.egg-info → punit-1.5.3}/PKG-INFO +1 -1
  2. {punit-1.5.2 → punit-1.5.3}/pyproject.toml +1 -1
  3. {punit-1.5.2 → punit-1.5.3/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. punit-1.5.3/src/punit/TestResult.py +186 -0
  5. {punit-1.5.2 → punit-1.5.3}/src/punit/__init__.py +2 -2
  6. {punit-1.5.2 → punit-1.5.3}/src/punit/__main__.py +3 -3
  7. {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/numeric.py +1 -1
  8. {punit-1.5.2 → punit-1.5.3}/src/punit/cli.py +8 -8
  9. {punit-1.5.2 → punit-1.5.3}/src/punit/discovery/TestModuleDiscovery.py +13 -14
  10. {punit-1.5.2 → punit-1.5.3}/src/punit/facts/Fact.py +8 -9
  11. {punit-1.5.2 → punit-1.5.3}/src/punit/facts/FactManager.py +17 -17
  12. {punit-1.5.2 → punit-1.5.3}/src/punit/filters/Filter.py +7 -7
  13. {punit-1.5.2 → punit-1.5.3}/src/punit/filters/FilterManager.py +7 -7
  14. punit-1.5.3/src/punit/metadata/CallableMetadata.py +38 -0
  15. {punit-1.5.2 → punit-1.5.3}/src/punit/reports/HtmlReportGenerator.py +26 -26
  16. {punit-1.5.2 → punit-1.5.3}/src/punit/reports/JUnitReportGenerator.py +49 -48
  17. punit-1.5.3/src/punit/reports/JsonReportGenerator.py +40 -0
  18. punit-1.5.3/src/punit/runner.py +124 -0
  19. {punit-1.5.2 → punit-1.5.3}/src/punit/theories/Theory.py +10 -10
  20. {punit-1.5.2 → punit-1.5.3}/src/punit/theories/TheoryManager.py +20 -20
  21. {punit-1.5.2 → punit-1.5.3}/src/punit/traits/Trait.py +7 -7
  22. {punit-1.5.2 → punit-1.5.3}/src/punit/traits/TraitManager.py +9 -9
  23. punit-1.5.2/src/punit/TestResult.py +0 -188
  24. punit-1.5.2/src/punit/metadata/CallableMetadata.py +0 -38
  25. punit-1.5.2/src/punit/reports/JsonReportGenerator.py +0 -41
  26. punit-1.5.2/src/punit/runner.py +0 -121
  27. {punit-1.5.2 → punit-1.5.3}/LICENSE +0 -0
  28. {punit-1.5.2 → punit-1.5.3}/README.md +0 -0
  29. {punit-1.5.2 → punit-1.5.3}/setup.cfg +0 -0
  30. {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/SOURCES.txt +0 -0
  31. {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/dependency_links.txt +0 -0
  32. {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/entry_points.txt +0 -0
  33. {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/requires.txt +0 -0
  34. {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/top_level.txt +0 -0
  35. {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/__init__.py +0 -0
  36. {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/collections.py +0 -0
  37. {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/exceptions.py +0 -0
  38. {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/strings.py +0 -0
  39. {punit-1.5.2 → punit-1.5.3}/src/punit/discovery/__init__.py +0 -0
  40. {punit-1.5.2 → punit-1.5.3}/src/punit/facts/__init__.py +0 -0
  41. {punit-1.5.2 → punit-1.5.3}/src/punit/filters/__init__.py +0 -0
  42. {punit-1.5.2 → punit-1.5.3}/src/punit/metadata/__init__.py +0 -0
  43. {punit-1.5.2 → punit-1.5.3}/src/punit/py.typed +0 -0
  44. {punit-1.5.2 → punit-1.5.3}/src/punit/reports/__init__.py +0 -0
  45. {punit-1.5.2 → punit-1.5.3}/src/punit/theories/__init__.py +0 -0
  46. {punit-1.5.2 → punit-1.5.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.2
3
+ Version: 1.5.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.2"
3
+ version = "1.5.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.2
3
+ Version: 1.5.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
@@ -0,0 +1,186 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import sys
5
+ from typing import Any, TextIO, Optional, cast
6
+
7
+
8
+ class TextIOCapture:
9
+
10
+ __quiet: bool
11
+ output: str | None = None
12
+ target: TextIO
13
+
14
+ def __init__(self, target: TextIO, quiet: bool = False) -> None:
15
+ self.__quiet = quiet
16
+ self.output = None
17
+ self.target = target
18
+
19
+ def write(self, text: str) -> int:
20
+ if self.output is None:
21
+ self.output = text
22
+ else:
23
+ self.output += text
24
+ if not self.__quiet:
25
+ self.target.write(text)
26
+ return len(text)
27
+
28
+
29
+ class TestResult:
30
+
31
+ __class_name: Optional[str]
32
+ __exception: Exception | None
33
+ __file_name: str | None
34
+ __host_name: str | None
35
+ __is_success: bool | None
36
+ __module_name: str | None
37
+ __package_name: str | None
38
+ __properties: dict[str, Any]
39
+ __start_time: float | None
40
+ __stderr_capture: TextIOCapture | None
41
+ __stdout_capture: TextIOCapture | None
42
+ __stop_time: float | None
43
+ __test_name: str | None
44
+
45
+ def __init__(self) -> None:
46
+ self.__class_name = None
47
+ self.__exception = None
48
+ self.__file_name = None
49
+ self.__host_name = None
50
+ self.__is_success = None
51
+ self.__module_name = None
52
+ self.__package_name = None
53
+ self.__properties = dict[str, Any]()
54
+ self.__start_time = None
55
+ self.__stderr_capture = None
56
+ self.__stdout_capture = None
57
+ self.__stop_time = None
58
+ self.__test_name = None
59
+
60
+ @property
61
+ 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
63
+
64
+ @class_name.setter
65
+ def class_name(self, value: Optional[str]) -> None:
66
+ self.__class_name = value
67
+
68
+ @property
69
+ def exception(self) -> Exception | None:
70
+ return self.__exception
71
+
72
+ @exception.setter
73
+ def exception(self, value: Exception) -> None:
74
+ self.__exception = value
75
+
76
+ @property
77
+ def file_name(self) -> str | None:
78
+ return self.__file_name
79
+
80
+ @file_name.setter
81
+ def file_name(self, value: str) -> None:
82
+ self.__file_name = value
83
+
84
+ @property
85
+ def host_name(self) -> str | None:
86
+ return self.__host_name
87
+
88
+ @host_name.setter
89
+ def host_name(self, value: str) -> None:
90
+ self.__host_name = value
91
+
92
+ @property
93
+ def is_success(self) -> bool:
94
+ return False if self.__is_success is None else self.__is_success
95
+
96
+ @is_success.setter
97
+ def is_success(self, value: bool) -> None:
98
+ self.__is_success = value
99
+
100
+ @property
101
+ def module_name(self) -> str:
102
+ return cast(str, self.__module_name)
103
+
104
+ @module_name.setter
105
+ def module_name(self, value: str) -> None:
106
+ self.__module_name = value
107
+
108
+ @property
109
+ def package_name(self) -> str | None:
110
+ return self.__package_name
111
+
112
+ @package_name.setter
113
+ def package_name(self, value: str) -> None:
114
+ self.__package_name = value
115
+
116
+ @property
117
+ def properties(self) -> dict[str, Any]:
118
+ return self.__properties
119
+
120
+ @properties.setter
121
+ def properties(self, value: dict[str, str]) -> None:
122
+ self.__properties = value
123
+
124
+ @property
125
+ def start_time(self) -> float | None:
126
+ return self.__start_time
127
+
128
+ @start_time.setter
129
+ def start_time(self, value: float) -> None:
130
+ self.__start_time = value
131
+
132
+ @property
133
+ def stderr(self) -> str | None:
134
+ return None if self.__stderr_capture is None else self.__stderr_capture.output
135
+
136
+ @property
137
+ def stdout(self) -> str | None:
138
+ return None if self.__stdout_capture is None else self.__stdout_capture.output
139
+
140
+ @property
141
+ def stop_time(self) -> float | None:
142
+ return self.__stop_time
143
+
144
+ @stop_time.setter
145
+ def stop_time(self, value: float) -> None:
146
+ self.__stop_time = value
147
+
148
+ @property
149
+ def test_name(self) -> str | None:
150
+ return self.__test_name
151
+
152
+ @test_name.setter
153
+ def test_name(self, value: str) -> None:
154
+ self.__test_name = value
155
+
156
+ @property
157
+ def took(self) -> float | None:
158
+ return None if self.__stop_time is None or self.__start_time is None else self.__stop_time - self.__start_time
159
+
160
+ @property
161
+ def tookPretty(self) -> str:
162
+ took = self.took
163
+ if took is None:
164
+ return 'N/A'
165
+ elif took >= 1:
166
+ return f'{took:.1f}'.rstrip('0').rstrip('.') + 's'
167
+ elif took >= 0.001:
168
+ return f'{(took*1000):.0f}ms'
169
+ elif took >= 0.000001:
170
+ return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
171
+ elif took >= 0.000000001:
172
+ return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
173
+ else:
174
+ return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
175
+
176
+ def captureOutput(self, quiet: bool = False) -> None:
177
+ self.__stdout_capture = TextIOCapture(sys.stdout, quiet)
178
+ self.__stderr_capture = TextIOCapture(sys.stderr, quiet)
179
+ sys.stdout = self.__stdout_capture
180
+ sys.stderr = self.__stderr_capture
181
+
182
+ def releaseOutput(self) -> None:
183
+ if self.__stdout_capture is not None and self.__stdout_capture.target is not None:
184
+ sys.stdout = self.__stdout_capture.target
185
+ if self.__stderr_capture is not None and self.__stderr_capture.target is not None:
186
+ sys.stderr = self.__stderr_capture.target
@@ -10,8 +10,8 @@ from .assertions.exceptions import raises
10
10
  from .assertions.numeric import approx
11
11
 
12
12
 
13
- __version__ = '1.5.2'
14
- __commit__ = '19ca1ae'
13
+ __version__ = '1.5.3'
14
+ __commit__ = '34475af'
15
15
  __all__ = [
16
16
  '__version__', '__commit__',
17
17
  'assertions',
@@ -31,17 +31,17 @@ async def async_main() -> None:
31
31
  if pathbase not in sys.path:
32
32
  sys.path.append(pathbase)
33
33
  testModuleDiscovery = TestModuleDiscovery(
34
- os.path.join(cli.workdir, cli.testPackageName),
34
+ os.path.join(cli.workdir, cli.test_package_name),
35
35
  cli.includePatterns,
36
36
  cli.excludePatterns,
37
37
  cli)
38
38
  testModuleDiscovery.discover()
39
- testRunner = TestRunner(cli.testPackageName, testModuleDiscovery.filenames, cli)
39
+ testRunner = TestRunner(cli.test_package_name, testModuleDiscovery.filenames, cli)
40
40
  results = await testRunner.run()
41
41
  totalTime = time.time() - ts
42
42
  failureCount = 0
43
43
  for result in results:
44
- if not result.isSuccess:
44
+ if not result.is_success:
45
45
  failureCount += 1 # pragma: no cover
46
46
  if not cli.quiet:
47
47
  print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
@@ -79,7 +79,7 @@ def isclose(
79
79
  a_c = a if isinstance(a, complex) else complex(a, 0)
80
80
  b_c = b if isinstance(b, complex) else complex(b, 0)
81
81
  return math.isclose(a_c.real, b_c.real, rel_tol=rel_tol, abs_tol=abs_tol) and \
82
- math.isclose(a_c.imag, b_c.imag, rel_tol=rel_tol, abs_tol=abs_tol)
82
+ math.isclose(a_c.imag, b_c.imag, rel_tol=rel_tol, abs_tol=abs_tol)
83
83
 
84
84
  # Both real: delegate to stdlib (safe float cast for int/float)
85
85
  return math.isclose(float(a), float(b), rel_tol=rel_tol, abs_tol=abs_tol)
@@ -24,7 +24,7 @@ class CommandLineInterface:
24
24
  __outputFilename: Optional[str]
25
25
  __quiet: bool
26
26
  __reportFormat: Optional[str]
27
- __testPackageName: str | None
27
+ __test_package_name: str | None
28
28
  __verbose: bool
29
29
  __workdir: str | None
30
30
 
@@ -42,7 +42,7 @@ class CommandLineInterface:
42
42
  self.__outputFilename = None
43
43
  self.__quiet = False
44
44
  self.__reportFormat = None
45
- self.__testPackageName = 'tests'
45
+ self.__test_package_name = 'tests'
46
46
  self.__workdir = os.path.curdir
47
47
  self.__verbose = False
48
48
 
@@ -92,8 +92,8 @@ class CommandLineInterface:
92
92
  elif self.__workdir is None:
93
93
  self.__workdir = arg
94
94
  continue
95
- elif self.__testPackageName is None:
96
- self.__testPackageName = arg
95
+ elif self.__test_package_name is None:
96
+ self.__test_package_name = arg
97
97
  continue
98
98
  elif extractReportFormat:
99
99
  extractReportFormat = False
@@ -122,7 +122,7 @@ class CommandLineInterface:
122
122
  case '-z' | '--failfast':
123
123
  self.__failfast = True
124
124
  case '-p' | '--test-package':
125
- self.__testPackageName = None
125
+ self.__test_package_name = None
126
126
  case '-i' | '--include':
127
127
  extractIncludePattern = True
128
128
  case '-q' | '--quiet':
@@ -198,8 +198,8 @@ class CommandLineInterface:
198
198
  return self.__reportFormat
199
199
 
200
200
  @property
201
- def testPackageName(self) -> str:
202
- return 'tests' if self.__testPackageName is None else self.__testPackageName
201
+ def test_package_name(self) -> str:
202
+ return 'tests' if self.__test_package_name is None else self.__test_package_name
203
203
 
204
204
  @property
205
205
  def verbose(self) -> bool:
@@ -307,7 +307,7 @@ Options:
307
307
  os._exit(2)
308
308
  self.__workdir = os.path.abspath(self.__workdir)
309
309
  if not self.__no_default_patterns:
310
- # if no other patterns specified, default to including all files found in the directory matching `testPackageName`
310
+ # if no other patterns specified, default to including all files found in the directory matching `test_package_name`
311
311
  if len(self.__includePatterns) == 0:
312
312
  self.__includePatterns.append('*.py')
313
313
  # always exclude dunder files
@@ -12,15 +12,15 @@ from ..traits import Trait
12
12
 
13
13
  class TestModuleDiscovery:
14
14
 
15
- __cli:CommandLineInterface
16
- __excludePatterns:list[re.Pattern]
17
- __excludeTraits:list[Trait]
18
- __filenames:list[str]
19
- __includePatterns:list[re.Pattern]
20
- __includeTraits:list[Trait]
21
- __workdir:str
15
+ __cli: CommandLineInterface
16
+ __excludePatterns: list[re.Pattern]
17
+ __excludeTraits: list[Trait]
18
+ __filenames: list[str]
19
+ __includePatterns: list[re.Pattern]
20
+ __includeTraits: list[Trait]
21
+ __workdir: str
22
22
 
23
- def __init__(self, workdir:str, includePatterns:list[str], excludePatterns:list[str], cli:CommandLineInterface) -> None:
23
+ def __init__(self, workdir: str, includePatterns: list[str], excludePatterns: list[str], cli: CommandLineInterface) -> None:
24
24
  self.__cli = cli
25
25
  self.__excludePatterns = []
26
26
  if excludePatterns is not None:
@@ -41,27 +41,26 @@ class TestModuleDiscovery:
41
41
  re.IGNORECASE))
42
42
  self.__workdir = workdir
43
43
 
44
-
45
- def __convertPatternToRegex(self, pattern:str) -> str:
44
+ def __convertPatternToRegex(self, pattern: str) -> str:
46
45
  result = re.escape(pattern)\
47
46
  .replace('\\\\', '/')\
48
47
  .replace('\\*', r'.*')\
49
48
  .replace('?', '.')
50
49
  return result
51
50
 
52
- def __testAnyInclude(self, input:str) -> bool:
51
+ def __testAnyInclude(self, input: str) -> bool:
53
52
  for pat in self.__includePatterns:
54
53
  if len(pat.findall(input)) > 0:
55
54
  return True
56
55
  return False
57
56
 
58
- def __testAnyExclude(self, input:str) -> bool:
57
+ def __testAnyExclude(self, input: str) -> bool:
59
58
  for pat in self.__excludePatterns:
60
59
  if len(pat.findall(input)) > 0:
61
60
  return True
62
61
  return False
63
62
 
64
- def __walkDirectory(self, path:str) -> list[str]:
63
+ def __walkDirectory(self, path: str) -> list[str]:
65
64
  filenames = []
66
65
  if os.path.isdir(path):
67
66
  for dname, dlist, flist in os.walk(path, topdown=True):
@@ -97,7 +96,7 @@ class TestModuleDiscovery:
97
96
  @property
98
97
  def filenames(self) -> list[str]:
99
98
  return self.__filenames
100
-
99
+
101
100
  def discover(self) -> list[str]:
102
101
  FactManager.instance().excludeTraits = self.__excludeTraits
103
102
  FactManager.instance().includeTraits = self.__includeTraits
@@ -3,16 +3,16 @@
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 Callable, Coroutine, Union, cast
7
7
 
8
8
  from ..metadata import CallableMetadata
9
9
 
10
10
 
11
11
  class Fact:
12
12
 
13
- __target:Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]
13
+ __target: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]
14
14
 
15
- def __init__(self, target:Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]):
15
+ def __init__(self, target: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]):
16
16
  self.__metadata = CallableMetadata(target)
17
17
  self.__target = target
18
18
 
@@ -24,9 +24,8 @@ class Fact:
24
24
  def target(self) -> Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]:
25
25
  return self.__target
26
26
 
27
-
28
- async def execute(self, module:ModuleType) -> None:
29
- coro:Coroutine|None = None
27
+ async def execute(self, module: ModuleType) -> None:
28
+ coro: Coroutine | None = None
30
29
  if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
31
30
  if isinstance(self.__target, staticmethod):
32
31
  coro = self.__target()
@@ -39,17 +38,17 @@ class Fact:
39
38
  coro = self.__target.__func__(qntarget)
40
39
  else:
41
40
  # every test execution gets a new instance of class
42
- coro = self.__target(cast(Callable,qntarget)())
41
+ coro = self.__target(cast(Callable, qntarget)())
43
42
  else:
44
43
  coro = self.__target()
45
44
  if inspect.iscoroutine(coro):
46
45
  await coro
47
46
 
48
47
 
49
- def fact(target:Callable) -> Callable:
48
+ def fact(target: Callable) -> Callable:
50
49
  from .FactManager import FactManager
51
50
  if (not inspect.isfunction(target)) and (not isinstance(target, classmethod)) and (not isinstance(target, staticmethod)):
52
51
  raise Exception('@fact can only be applied to functions and methods.')
53
- fact:Fact = Fact(target)
52
+ fact: Fact = Fact(target)
54
53
  FactManager.instance().put(fact)
55
54
  return target
@@ -12,14 +12,14 @@ from .Fact import Fact
12
12
 
13
13
  class FactManager:
14
14
 
15
- __excludeTraits:list[Trait]
16
- __instance:Optional['FactManager'] = None
17
- __includeTraits:list[Trait]
18
- __modules:dict[str, list[Fact]]
19
-
15
+ __excludeTraits: list[Trait]
16
+ __instance: Optional['FactManager'] = None
17
+ __includeTraits: list[Trait]
18
+ __modules: dict[str, list[Fact]]
19
+
20
20
  def __init__(self) -> None:
21
21
  if FactManager.__instance is not None:
22
- raise Exception('Cannot create more than one instance of FactManager') # pragma: no cover
22
+ raise Exception('Cannot create more than one instance of FactManager') # pragma: no cover
23
23
  self.__modules = {}
24
24
 
25
25
  @staticmethod
@@ -27,24 +27,24 @@ class FactManager:
27
27
  if FactManager.__instance is None:
28
28
  FactManager.__instance = FactManager()
29
29
  return FactManager.__instance
30
-
30
+
31
31
  @property
32
32
  def excludeTraits(self) -> list[Trait]:
33
33
  return [] if self.__excludeTraits is None else self.__excludeTraits
34
34
 
35
35
  @excludeTraits.setter
36
- def excludeTraits(self, value:list[Trait]) -> None:
36
+ def excludeTraits(self, value: list[Trait]) -> None:
37
37
  self.__excludeTraits = value
38
38
 
39
39
  @property
40
40
  def includeTraits(self) -> list[Trait]:
41
41
  return [] if self.__includeTraits is None else self.__includeTraits
42
-
42
+
43
43
  @includeTraits.setter
44
- def includeTraits(self, value:list[Trait]) -> None:
44
+ def includeTraits(self, value: list[Trait]) -> None:
45
45
  self.__includeTraits = value
46
46
 
47
- def __excludeByTraits(self, fact:Fact) -> bool:
47
+ def __excludeByTraits(self, fact: Fact) -> bool:
48
48
  traits = TraitManager.instance().get(fact.target)
49
49
  if self.excludeTraits is not None and len(self.excludeTraits) > 0:
50
50
  for trait in self.excludeTraits:
@@ -59,18 +59,18 @@ class FactManager:
59
59
  return True
60
60
  return False
61
61
 
62
- def get(self, moduleName:str) -> list[Fact]:
63
- l = self.__modules.get(moduleName)
62
+ def get(self, module_name: str) -> list[Fact]:
63
+ l = self.__modules.get(module_name)
64
64
  if l is None:
65
65
  l = []
66
- self.__modules[moduleName] = l
66
+ self.__modules[module_name] = l
67
67
  return l
68
68
 
69
- def put(self, fact:Fact) -> None:
69
+ def put(self, fact: Fact) -> None:
70
70
  filters = FilterManager.instance().filters
71
- matches_filter:bool = False
71
+ matches_filter: bool = False
72
72
  for filter in filters:
73
- if filter.re.fullmatch(fact.metadata.filterName) is not None:
73
+ if filter.re.fullmatch(fact.metadata.filter_name) is not None:
74
74
  matches_filter = not filter.isExclude
75
75
  break
76
76
  if matches_filter:
@@ -6,16 +6,16 @@ import re
6
6
 
7
7
  class Filter:
8
8
 
9
- __isExclude:bool
10
- __pattern:str
11
- __re:re.Pattern
9
+ __isExclude: bool
10
+ __pattern: str
11
+ __re: re.Pattern
12
12
 
13
- def __init__(self, pattern:str) -> None:
13
+ def __init__(self, pattern: str) -> None:
14
14
  self.__isExclude = pattern.startswith('!')
15
15
  self.__pattern = pattern
16
16
  self.__re = self.__toRegex(pattern)
17
17
 
18
- def __toRegex(self, pattern:str) -> re.Pattern:
18
+ def __toRegex(self, pattern: str) -> re.Pattern:
19
19
  if self.__isExclude:
20
20
  pattern = pattern[1:]
21
21
  pattern = re.escape(pattern)\
@@ -27,11 +27,11 @@ class Filter:
27
27
  @property
28
28
  def isExclude(self) -> bool:
29
29
  return self.__isExclude
30
-
30
+
31
31
  @property
32
32
  def pattern(self) -> str:
33
33
  return self.__pattern
34
-
34
+
35
35
  @property
36
36
  def re(self) -> re.Pattern:
37
37
  return self.__re
@@ -11,8 +11,8 @@ from .Filter import Filter
11
11
 
12
12
  class FilterManager:
13
13
 
14
- __instance:Optional['FilterManager']|None = None
15
- __filters:list[Filter]
14
+ __instance: Optional['FilterManager'] | None = None
15
+ __filters: list[Filter]
16
16
 
17
17
  def __init__(self) -> None:
18
18
  self.__filters = list[Filter]()
@@ -29,18 +29,18 @@ class FilterManager:
29
29
  FilterManager.__instance = instance
30
30
  return instance
31
31
 
32
- def add(self, pattern:str) -> None:
32
+ def add(self, pattern: str) -> None:
33
33
  self.__filters.append(Filter(pattern))
34
34
 
35
- def remove(self, pattern:str) -> None:
35
+ def remove(self, pattern: str) -> None:
36
36
  for filter in [e for e in self.__filters]:
37
37
  if filter.pattern == pattern:
38
38
  self.__filters.remove(filter)
39
39
  break
40
40
 
41
- def load(self, filepath:str) -> None:
41
+ def load(self, filepath: str) -> None:
42
42
  # treat as a filepath containing one or more filter patterns
43
- lines:list[str] = []
43
+ lines: list[str] = []
44
44
  if filepath == 'stdin':
45
45
  ready, _, _ = select.select([sys.stdin], [], [], 5)
46
46
  if not ready:
@@ -57,7 +57,7 @@ class FilterManager:
57
57
  with open(filepath, 'rb') as f:
58
58
  lines = f.read().decode().splitlines()
59
59
  for line in lines:
60
- line = line.split('#')[0].strip() # strip comments and prefix/postfix whitespace
60
+ line = line.split('#')[0].strip() # strip comments and prefix/postfix whitespace
61
61
  if len(line) == 0:
62
62
  # comments and empty lines
63
63
  continue
@@ -0,0 +1,38 @@
1
+ # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType, ModuleType
5
+ from typing import Callable, Optional, Union, cast
6
+
7
+
8
+ class CallableMetadata:
9
+
10
+ __callable: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]
11
+ __class_name: str
12
+ __module_name: str
13
+ __name: str
14
+
15
+ def __init__(self, callable: Union[FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType, Callable]) -> None:
16
+ self.__callable = callable
17
+ self.__class_name = '.'.join(callable.__qualname__.replace(f'{callable.__module__}.', '').split('.')[0:-1])
18
+ self.__module_name = callable.__module__
19
+ self.__name = callable.__name__
20
+
21
+ @property
22
+ def class_name(self) -> Optional[str]:
23
+ return self.__class_name
24
+
25
+ @property
26
+ def filter_name(self) -> str:
27
+ """
28
+ The name used for pattern matched "Test Filtering".
29
+ """
30
+ return f'{".".join(self.__module_name.split(".")[1:])}/{"" if self.__class_name is None or len(self.__class_name) == 0 else f"{self.__class_name}/"}{self.__name}'
31
+
32
+ @property
33
+ def module_name(self) -> str:
34
+ return self.__module_name
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ return self.__name