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.
Files changed (52) hide show
  1. {punit-1.5.2/src/pUnit.egg-info → punit-1.7.2}/PKG-INFO +1 -1
  2. {punit-1.5.2 → punit-1.7.2}/pyproject.toml +1 -1
  3. {punit-1.5.2 → punit-1.7.2/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/SOURCES.txt +6 -0
  5. punit-1.7.2/src/punit/TestResult.py +190 -0
  6. {punit-1.5.2 → punit-1.7.2}/src/punit/__init__.py +6 -2
  7. {punit-1.5.2 → punit-1.7.2}/src/punit/__main__.py +29 -18
  8. {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/numeric.py +5 -3
  9. {punit-1.5.2 → punit-1.7.2}/src/punit/cli.py +17 -15
  10. {punit-1.5.2 → punit-1.7.2}/src/punit/discovery/TestModuleDiscovery.py +13 -14
  11. {punit-1.5.2 → punit-1.7.2}/src/punit/facts/Fact.py +20 -11
  12. {punit-1.5.2 → punit-1.7.2}/src/punit/facts/FactManager.py +20 -18
  13. {punit-1.5.2 → punit-1.7.2}/src/punit/filters/Filter.py +7 -7
  14. {punit-1.5.2 → punit-1.7.2}/src/punit/filters/FilterManager.py +10 -8
  15. punit-1.7.2/src/punit/metadata/CallableMetadata.py +39 -0
  16. {punit-1.5.2 → punit-1.7.2}/src/punit/reports/HtmlReportGenerator.py +26 -26
  17. {punit-1.5.2 → punit-1.7.2}/src/punit/reports/JUnitReportGenerator.py +49 -48
  18. punit-1.7.2/src/punit/reports/JsonReportGenerator.py +40 -0
  19. punit-1.7.2/src/punit/runner.py +241 -0
  20. punit-1.7.2/src/punit/setups/Setup.py +100 -0
  21. punit-1.7.2/src/punit/setups/SetupManager.py +69 -0
  22. punit-1.7.2/src/punit/setups/__init__.py +11 -0
  23. punit-1.7.2/src/punit/teardowns/Teardown.py +100 -0
  24. punit-1.7.2/src/punit/teardowns/TeardownManager.py +69 -0
  25. punit-1.7.2/src/punit/teardowns/__init__.py +11 -0
  26. {punit-1.5.2 → punit-1.7.2}/src/punit/theories/Theory.py +23 -12
  27. {punit-1.5.2 → punit-1.7.2}/src/punit/theories/TheoryManager.py +23 -21
  28. {punit-1.5.2 → punit-1.7.2}/src/punit/traits/Trait.py +7 -7
  29. {punit-1.5.2 → punit-1.7.2}/src/punit/traits/TraitManager.py +12 -11
  30. punit-1.5.2/src/punit/TestResult.py +0 -188
  31. punit-1.5.2/src/punit/metadata/CallableMetadata.py +0 -38
  32. punit-1.5.2/src/punit/reports/JsonReportGenerator.py +0 -41
  33. punit-1.5.2/src/punit/runner.py +0 -121
  34. {punit-1.5.2 → punit-1.7.2}/LICENSE +0 -0
  35. {punit-1.5.2 → punit-1.7.2}/README.md +0 -0
  36. {punit-1.5.2 → punit-1.7.2}/setup.cfg +0 -0
  37. {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/dependency_links.txt +0 -0
  38. {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/entry_points.txt +0 -0
  39. {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/requires.txt +0 -0
  40. {punit-1.5.2 → punit-1.7.2}/src/pUnit.egg-info/top_level.txt +0 -0
  41. {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/__init__.py +0 -0
  42. {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/collections.py +0 -0
  43. {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/exceptions.py +0 -0
  44. {punit-1.5.2 → punit-1.7.2}/src/punit/assertions/strings.py +0 -0
  45. {punit-1.5.2 → punit-1.7.2}/src/punit/discovery/__init__.py +0 -0
  46. {punit-1.5.2 → punit-1.7.2}/src/punit/facts/__init__.py +0 -0
  47. {punit-1.5.2 → punit-1.7.2}/src/punit/filters/__init__.py +0 -0
  48. {punit-1.5.2 → punit-1.7.2}/src/punit/metadata/__init__.py +0 -0
  49. {punit-1.5.2 → punit-1.7.2}/src/punit/py.typed +0 -0
  50. {punit-1.5.2 → punit-1.7.2}/src/punit/reports/__init__.py +0 -0
  51. {punit-1.5.2 → punit-1.7.2}/src/punit/theories/__init__.py +0 -0
  52. {punit-1.5.2 → punit-1.7.2}/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.7.2
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.7.2"
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.7.2
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
@@ -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.5.2'
14
- __commit__ = '19ca1ae'
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.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
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
- testModuleDiscovery = TestModuleDiscovery(
34
- os.path.join(cli.workdir, cli.testPackageName),
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
- testModuleDiscovery.discover()
39
- testRunner = TestRunner(cli.testPackageName, testModuleDiscovery.filenames, 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.isSuccess:
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
- 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 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
- math.isclose(a_c.imag, b_c.imag, rel_tol=rel_tol, abs_tol=abs_tol)
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) -> '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
@@ -24,7 +26,7 @@ class CommandLineInterface:
24
26
  __outputFilename: Optional[str]
25
27
  __quiet: bool
26
28
  __reportFormat: Optional[str]
27
- __testPackageName: str | None
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.__testPackageName = 'tests'
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]) -> 'CommandLineInterface':
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.__testPackageName is None:
96
- self.__testPackageName = arg
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.__testPackageName = None
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 testPackageName(self) -> str:
202
- return 'tests' if self.__testPackageName is None else self.__testPackageName
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 printHelp(self) -> None:
222
+ def print_help(self) -> None:
221
223
  if True: # pragma: no cover
222
- self.printVersion()
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 printSummary(self) -> None:
285
- self.printVersion()
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 printVersion(self) -> None:
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 `testPackageName`
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) -> 'CommandLineInterface':
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, Union, cast
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
- async def execute(self, module:ModuleType) -> None:
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
- coro = self.__target(cast(Callable,qntarget)())
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
- 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)):
52
55
  raise Exception('@fact can only be applied to functions and methods.')
53
- fact:Fact = Fact(target)
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