pUnit 1.5.2__tar.gz → 1.7.2__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.7.2}/PKG-INFO +1 -1
- {punit-1.5.2 → punit-1.7.2}/pyproject.toml +1 -1
- {punit-1.5.2 → punit-1.7.2/src/pUnit.egg-info}/PKG-INFO +1 -1
- {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/SOURCES.txt +6 -0
- punit-1.7.2/src/punit/TestResult.py +190 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/__init__.py +6 -2
- {punit-1.5.2 → punit-1.7.2}/src/punit/__main__.py +29 -18
- {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/numeric.py +5 -3
- {punit-1.5.2 → punit-1.7.2}/src/punit/cli.py +17 -15
- {punit-1.5.2 → punit-1.7.2}/src/punit/discovery/TestModuleDiscovery.py +13 -14
- {punit-1.5.2 → punit-1.7.2}/src/punit/facts/Fact.py +20 -11
- {punit-1.5.2 → punit-1.7.2}/src/punit/facts/FactManager.py +20 -18
- {punit-1.5.2 → punit-1.7.2}/src/punit/filters/Filter.py +7 -7
- {punit-1.5.2 → punit-1.7.2}/src/punit/filters/FilterManager.py +10 -8
- punit-1.7.2/src/punit/metadata/CallableMetadata.py +39 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/reports/HtmlReportGenerator.py +26 -26
- {punit-1.5.2 → punit-1.7.2}/src/punit/reports/JUnitReportGenerator.py +49 -48
- punit-1.7.2/src/punit/reports/JsonReportGenerator.py +40 -0
- punit-1.7.2/src/punit/runner.py +241 -0
- punit-1.7.2/src/punit/setups/Setup.py +100 -0
- punit-1.7.2/src/punit/setups/SetupManager.py +69 -0
- punit-1.7.2/src/punit/setups/__init__.py +11 -0
- punit-1.7.2/src/punit/teardowns/Teardown.py +100 -0
- punit-1.7.2/src/punit/teardowns/TeardownManager.py +69 -0
- punit-1.7.2/src/punit/teardowns/__init__.py +11 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/theories/Theory.py +23 -12
- {punit-1.5.2 → punit-1.7.2}/src/punit/theories/TheoryManager.py +23 -21
- {punit-1.5.2 → punit-1.7.2}/src/punit/traits/Trait.py +7 -7
- {punit-1.5.2 → punit-1.7.2}/src/punit/traits/TraitManager.py +12 -11
- 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.7.2}/LICENSE +0 -0
- {punit-1.5.2 → punit-1.7.2}/README.md +0 -0
- {punit-1.5.2 → punit-1.7.2}/setup.cfg +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/dependency_links.txt +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/entry_points.txt +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/requires.txt +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/top_level.txt +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/collections.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/exceptions.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/strings.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/discovery/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/facts/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/filters/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/metadata/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/py.typed +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/reports/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/theories/__init__.py +0 -0
- {punit-1.5.2 → punit-1.7.2}/src/punit/traits/__init__.py +0 -0
|
@@ -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
|
|
@@ -0,0 +1,190 @@
|
|
|
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 (
|
|
63
|
+
self.__class_name
|
|
64
|
+
if self.__class_name is not None and len(self.__class_name) > 0
|
|
65
|
+
else None
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@class_name.setter
|
|
69
|
+
def class_name(self, value: Optional[str]) -> None:
|
|
70
|
+
self.__class_name = value
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def exception(self) -> Exception | None:
|
|
74
|
+
return self.__exception
|
|
75
|
+
|
|
76
|
+
@exception.setter
|
|
77
|
+
def exception(self, value: Exception) -> None:
|
|
78
|
+
self.__exception = value
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def file_name(self) -> str | None:
|
|
82
|
+
return self.__file_name
|
|
83
|
+
|
|
84
|
+
@file_name.setter
|
|
85
|
+
def file_name(self, value: str) -> None:
|
|
86
|
+
self.__file_name = value
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def host_name(self) -> str | None:
|
|
90
|
+
return self.__host_name
|
|
91
|
+
|
|
92
|
+
@host_name.setter
|
|
93
|
+
def host_name(self, value: str) -> None:
|
|
94
|
+
self.__host_name = value
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_success(self) -> bool:
|
|
98
|
+
return False if self.__is_success is None else self.__is_success
|
|
99
|
+
|
|
100
|
+
@is_success.setter
|
|
101
|
+
def is_success(self, value: bool) -> None:
|
|
102
|
+
self.__is_success = value
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def module_name(self) -> str:
|
|
106
|
+
return cast(str, self.__module_name)
|
|
107
|
+
|
|
108
|
+
@module_name.setter
|
|
109
|
+
def module_name(self, value: str) -> None:
|
|
110
|
+
self.__module_name = value
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def package_name(self) -> str | None:
|
|
114
|
+
return self.__package_name
|
|
115
|
+
|
|
116
|
+
@package_name.setter
|
|
117
|
+
def package_name(self, value: str) -> None:
|
|
118
|
+
self.__package_name = value
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def properties(self) -> dict[str, Any]:
|
|
122
|
+
return self.__properties
|
|
123
|
+
|
|
124
|
+
@properties.setter
|
|
125
|
+
def properties(self, value: dict[str, str]) -> None:
|
|
126
|
+
self.__properties = value
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def start_time(self) -> float | None:
|
|
130
|
+
return self.__start_time
|
|
131
|
+
|
|
132
|
+
@start_time.setter
|
|
133
|
+
def start_time(self, value: float) -> None:
|
|
134
|
+
self.__start_time = value
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def stderr(self) -> str | None:
|
|
138
|
+
return None if self.__stderr_capture is None else self.__stderr_capture.output
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def stdout(self) -> str | None:
|
|
142
|
+
return None if self.__stdout_capture is None else self.__stdout_capture.output
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def stop_time(self) -> float | None:
|
|
146
|
+
return self.__stop_time
|
|
147
|
+
|
|
148
|
+
@stop_time.setter
|
|
149
|
+
def stop_time(self, value: float) -> None:
|
|
150
|
+
self.__stop_time = value
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def test_name(self) -> str | None:
|
|
154
|
+
return self.__test_name
|
|
155
|
+
|
|
156
|
+
@test_name.setter
|
|
157
|
+
def test_name(self, value: str) -> None:
|
|
158
|
+
self.__test_name = value
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def took(self) -> float | None:
|
|
162
|
+
return None if self.__stop_time is None or self.__start_time is None else self.__stop_time - self.__start_time
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def tookPretty(self) -> str:
|
|
166
|
+
took = self.took
|
|
167
|
+
if took is None:
|
|
168
|
+
return 'N/A'
|
|
169
|
+
elif took >= 1:
|
|
170
|
+
return f'{took:.1f}'.rstrip('0').rstrip('.') + 's'
|
|
171
|
+
elif took >= 0.001:
|
|
172
|
+
return f'{(took*1000):.0f}ms'
|
|
173
|
+
elif took >= 0.000001:
|
|
174
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
175
|
+
elif took >= 0.000000001:
|
|
176
|
+
return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
|
|
177
|
+
else:
|
|
178
|
+
return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
|
|
179
|
+
|
|
180
|
+
def capture_output(self, quiet: bool = False) -> None:
|
|
181
|
+
self.__stdout_capture = TextIOCapture(sys.stdout, quiet)
|
|
182
|
+
self.__stderr_capture = TextIOCapture(sys.stderr, quiet)
|
|
183
|
+
sys.stdout = self.__stdout_capture
|
|
184
|
+
sys.stderr = self.__stderr_capture
|
|
185
|
+
|
|
186
|
+
def release_output(self) -> None:
|
|
187
|
+
if self.__stdout_capture is not None and self.__stdout_capture.target is not None:
|
|
188
|
+
sys.stdout = self.__stdout_capture.target
|
|
189
|
+
if self.__stderr_capture is not None and self.__stderr_capture.target is not None:
|
|
190
|
+
sys.stderr = self.__stderr_capture.target
|
|
@@ -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.
|
|
14
|
-
__commit__ = '
|
|
15
|
+
__version__ = '1.7.2'
|
|
16
|
+
__commit__ = 'b60afab'
|
|
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,17 +11,19 @@ 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
19
|
ts = time.time()
|
|
18
20
|
cli = CommandLineInterface.parse()
|
|
19
21
|
if cli.help: # pragma: no cover
|
|
20
|
-
cli.
|
|
22
|
+
cli.print_help()
|
|
21
23
|
elif cli.verbose and not cli.quiet: # pragma: no cover
|
|
22
|
-
cli.
|
|
24
|
+
cli.print_summary()
|
|
23
25
|
elif not cli.quiet: # pragma: no cover
|
|
24
|
-
cli.
|
|
26
|
+
cli.print_version()
|
|
25
27
|
os.chdir(cli.workdir)
|
|
26
28
|
if cli.no_pathfix is not True:
|
|
27
29
|
pathbase = str(Path.cwd())
|
|
@@ -30,18 +32,18 @@ async def async_main() -> None:
|
|
|
30
32
|
sys.path.append(srcbase)
|
|
31
33
|
if pathbase not in sys.path:
|
|
32
34
|
sys.path.append(pathbase)
|
|
33
|
-
|
|
34
|
-
os.path.join(cli.workdir, cli.
|
|
35
|
+
test_module_discovery = TestModuleDiscovery(
|
|
36
|
+
os.path.join(cli.workdir, cli.test_package_name),
|
|
35
37
|
cli.includePatterns,
|
|
36
38
|
cli.excludePatterns,
|
|
37
39
|
cli)
|
|
38
|
-
|
|
39
|
-
testRunner = TestRunner(cli.
|
|
40
|
+
test_module_discovery.discover()
|
|
41
|
+
testRunner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
|
|
40
42
|
results = await testRunner.run()
|
|
41
43
|
totalTime = time.time() - ts
|
|
42
44
|
failureCount = 0
|
|
43
45
|
for result in results:
|
|
44
|
-
if not result.
|
|
46
|
+
if not result.is_success:
|
|
45
47
|
failureCount += 1 # pragma: no cover
|
|
46
48
|
if not cli.quiet:
|
|
47
49
|
print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
|
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 failureCount:
|
|
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
|
|
@@ -79,7 +81,7 @@ def isclose(
|
|
|
79
81
|
a_c = a if isinstance(a, complex) else complex(a, 0)
|
|
80
82
|
b_c = b if isinstance(b, complex) else complex(b, 0)
|
|
81
83
|
return math.isclose(a_c.real, b_c.real, rel_tol=rel_tol, abs_tol=abs_tol) and \
|
|
82
|
-
|
|
84
|
+
math.isclose(a_c.imag, b_c.imag, rel_tol=rel_tol, abs_tol=abs_tol)
|
|
83
85
|
|
|
84
86
|
# Both real: delegate to stdlib (safe float cast for int/float)
|
|
85
87
|
return math.isclose(float(a), float(b), rel_tol=rel_tol, abs_tol=abs_tol)
|
|
@@ -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) ->
|
|
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) ->
|
|
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
|
|
@@ -24,7 +26,7 @@ class CommandLineInterface:
|
|
|
24
26
|
__outputFilename: Optional[str]
|
|
25
27
|
__quiet: bool
|
|
26
28
|
__reportFormat: Optional[str]
|
|
27
|
-
|
|
29
|
+
__test_package_name: str | None
|
|
28
30
|
__verbose: bool
|
|
29
31
|
__workdir: str | None
|
|
30
32
|
|
|
@@ -42,11 +44,11 @@ class CommandLineInterface:
|
|
|
42
44
|
self.__outputFilename = None
|
|
43
45
|
self.__quiet = False
|
|
44
46
|
self.__reportFormat = None
|
|
45
|
-
self.
|
|
47
|
+
self.__test_package_name = 'tests'
|
|
46
48
|
self.__workdir = os.path.curdir
|
|
47
49
|
self.__verbose = False
|
|
48
50
|
|
|
49
|
-
def __parse(self, argv: list[str]) ->
|
|
51
|
+
def __parse(self, argv: list[str]) -> CommandLineInterface:
|
|
50
52
|
aliasName: str | None = None
|
|
51
53
|
extractFilter: bool = False
|
|
52
54
|
extractExcludePattern: bool = False
|
|
@@ -92,8 +94,8 @@ class CommandLineInterface:
|
|
|
92
94
|
elif self.__workdir is None:
|
|
93
95
|
self.__workdir = arg
|
|
94
96
|
continue
|
|
95
|
-
elif self.
|
|
96
|
-
self.
|
|
97
|
+
elif self.__test_package_name is None:
|
|
98
|
+
self.__test_package_name = arg
|
|
97
99
|
continue
|
|
98
100
|
elif extractReportFormat:
|
|
99
101
|
extractReportFormat = False
|
|
@@ -122,7 +124,7 @@ class CommandLineInterface:
|
|
|
122
124
|
case '-z' | '--failfast':
|
|
123
125
|
self.__failfast = True
|
|
124
126
|
case '-p' | '--test-package':
|
|
125
|
-
self.
|
|
127
|
+
self.__test_package_name = None
|
|
126
128
|
case '-i' | '--include':
|
|
127
129
|
extractIncludePattern = True
|
|
128
130
|
case '-q' | '--quiet':
|
|
@@ -198,8 +200,8 @@ class CommandLineInterface:
|
|
|
198
200
|
return self.__reportFormat
|
|
199
201
|
|
|
200
202
|
@property
|
|
201
|
-
def
|
|
202
|
-
return 'tests' if self.
|
|
203
|
+
def test_package_name(self) -> str:
|
|
204
|
+
return 'tests' if self.__test_package_name is None else self.__test_package_name
|
|
203
205
|
|
|
204
206
|
@property
|
|
205
207
|
def verbose(self) -> bool:
|
|
@@ -217,9 +219,9 @@ class CommandLineInterface:
|
|
|
217
219
|
def no_pathfix(self) -> bool:
|
|
218
220
|
return self.__no_pathfix is True
|
|
219
221
|
|
|
220
|
-
def
|
|
222
|
+
def print_help(self) -> None:
|
|
221
223
|
if True: # pragma: no cover
|
|
222
|
-
self.
|
|
224
|
+
self.print_version()
|
|
223
225
|
print("""
|
|
224
226
|
Usage: python3 -m punit [-h|--help]
|
|
225
227
|
[-q|--quiet] [-v|--verbose]
|
|
@@ -281,8 +283,8 @@ Options:
|
|
|
281
283
|
""")
|
|
282
284
|
os._exit(0)
|
|
283
285
|
|
|
284
|
-
def
|
|
285
|
-
self.
|
|
286
|
+
def print_summary(self) -> None:
|
|
287
|
+
self.print_version()
|
|
286
288
|
print(f'Working Directory:\n\t{self.__workdir}')
|
|
287
289
|
print(f'Fail Fast: \n\t{"Yes" if self.__failfast else "No"}')
|
|
288
290
|
if len(self.__includePatterns) > 0:
|
|
@@ -295,7 +297,7 @@ Options:
|
|
|
295
297
|
print(f'\t{pattern}')
|
|
296
298
|
FilterManager.instance().print()
|
|
297
299
|
|
|
298
|
-
def
|
|
300
|
+
def print_version(self) -> None:
|
|
299
301
|
print(f'pUnit {__version__} ({__commit__})')
|
|
300
302
|
|
|
301
303
|
def validate(self) -> None:
|
|
@@ -307,14 +309,14 @@ Options:
|
|
|
307
309
|
os._exit(2)
|
|
308
310
|
self.__workdir = os.path.abspath(self.__workdir)
|
|
309
311
|
if not self.__no_default_patterns:
|
|
310
|
-
# if no other patterns specified, default to including all files found in the directory matching `
|
|
312
|
+
# if no other patterns specified, default to including all files found in the directory matching `test_package_name`
|
|
311
313
|
if len(self.__includePatterns) == 0:
|
|
312
314
|
self.__includePatterns.append('*.py')
|
|
313
315
|
# always exclude dunder files
|
|
314
316
|
self.__excludePatterns.append('/__*__')
|
|
315
317
|
|
|
316
318
|
@staticmethod
|
|
317
|
-
def parse(argv: list[str] = sys.argv) ->
|
|
319
|
+
def parse(argv: list[str] = sys.argv) -> CommandLineInterface:
|
|
318
320
|
result = CommandLineInterface().__parse(argv)
|
|
319
321
|
result.validate()
|
|
320
322
|
return result
|
|
@@ -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 Any, 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,14 +24,14 @@ 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) -> Any | None:
|
|
28
|
+
class_instance: Any | None = None
|
|
29
|
+
coro: Coroutine | None = None
|
|
30
30
|
if hasattr(self.__target, '__qualname__') and self.__target.__qualname__.find('.') > -1:
|
|
31
31
|
if isinstance(self.__target, staticmethod):
|
|
32
32
|
coro = self.__target()
|
|
33
33
|
else:
|
|
34
|
-
qnparts = self.__target.__qualname__.split('.')
|
|
34
|
+
qnparts = [p for p in self.__target.__qualname__.split('.') if p != '<locals>']
|
|
35
35
|
qntarget = module
|
|
36
36
|
for qnpart in qnparts[0:-1]:
|
|
37
37
|
qntarget = getattr(qntarget, qnpart)
|
|
@@ -39,17 +39,26 @@ class Fact:
|
|
|
39
39
|
coro = self.__target.__func__(qntarget)
|
|
40
40
|
else:
|
|
41
41
|
# every test execution gets a new instance of class
|
|
42
|
-
|
|
42
|
+
class_instance = cast(Any, cast(Callable, qntarget)())
|
|
43
|
+
coro = self.__target(class_instance)
|
|
43
44
|
else:
|
|
44
45
|
coro = self.__target()
|
|
45
46
|
if inspect.iscoroutine(coro):
|
|
46
47
|
await coro
|
|
48
|
+
return class_instance
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
def fact(target:Callable) -> Callable:
|
|
51
|
+
def fact(target: Callable) -> Callable:
|
|
50
52
|
from .FactManager import FactManager
|
|
51
|
-
|
|
53
|
+
unwrapped = inspect.unwrap(target)
|
|
54
|
+
if not isinstance(unwrapped, (FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType)):
|
|
52
55
|
raise Exception('@fact can only be applied to functions and methods.')
|
|
53
|
-
|
|
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')
|
|
62
|
+
fact: Fact = Fact(target)
|
|
54
63
|
FactManager.instance().put(fact)
|
|
55
64
|
return target
|