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.
- {punit-1.5.2/src/pUnit.egg-info → punit-1.5.3}/PKG-INFO +1 -1
- {punit-1.5.2 → punit-1.5.3}/pyproject.toml +1 -1
- {punit-1.5.2 → punit-1.5.3/src/pUnit.egg-info}/PKG-INFO +1 -1
- punit-1.5.3/src/punit/TestResult.py +186 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/__init__.py +2 -2
- {punit-1.5.2 → punit-1.5.3}/src/punit/__main__.py +3 -3
- {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/numeric.py +1 -1
- {punit-1.5.2 → punit-1.5.3}/src/punit/cli.py +8 -8
- {punit-1.5.2 → punit-1.5.3}/src/punit/discovery/TestModuleDiscovery.py +13 -14
- {punit-1.5.2 → punit-1.5.3}/src/punit/facts/Fact.py +8 -9
- {punit-1.5.2 → punit-1.5.3}/src/punit/facts/FactManager.py +17 -17
- {punit-1.5.2 → punit-1.5.3}/src/punit/filters/Filter.py +7 -7
- {punit-1.5.2 → punit-1.5.3}/src/punit/filters/FilterManager.py +7 -7
- punit-1.5.3/src/punit/metadata/CallableMetadata.py +38 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/reports/HtmlReportGenerator.py +26 -26
- {punit-1.5.2 → punit-1.5.3}/src/punit/reports/JUnitReportGenerator.py +49 -48
- punit-1.5.3/src/punit/reports/JsonReportGenerator.py +40 -0
- punit-1.5.3/src/punit/runner.py +124 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/theories/Theory.py +10 -10
- {punit-1.5.2 → punit-1.5.3}/src/punit/theories/TheoryManager.py +20 -20
- {punit-1.5.2 → punit-1.5.3}/src/punit/traits/Trait.py +7 -7
- {punit-1.5.2 → punit-1.5.3}/src/punit/traits/TraitManager.py +9 -9
- punit-1.5.2/src/punit/TestResult.py +0 -188
- punit-1.5.2/src/punit/metadata/CallableMetadata.py +0 -38
- punit-1.5.2/src/punit/reports/JsonReportGenerator.py +0 -41
- punit-1.5.2/src/punit/runner.py +0 -121
- {punit-1.5.2 → punit-1.5.3}/LICENSE +0 -0
- {punit-1.5.2 → punit-1.5.3}/README.md +0 -0
- {punit-1.5.2 → punit-1.5.3}/setup.cfg +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/SOURCES.txt +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/dependency_links.txt +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/entry_points.txt +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/requires.txt +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/pUnit.egg-info/top_level.txt +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/collections.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/exceptions.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/assertions/strings.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/discovery/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/facts/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/filters/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/metadata/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/py.typed +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/reports/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/theories/__init__.py +0 -0
- {punit-1.5.2 → punit-1.5.3}/src/punit/traits/__init__.py +0 -0
|
@@ -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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
96
|
-
self.
|
|
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.
|
|
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
|
|
202
|
-
return 'tests' if self.
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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')
|
|
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,
|
|
63
|
-
l = self.__modules.get(
|
|
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[
|
|
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.
|
|
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()
|
|
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
|