punit 1.12.2__tar.gz → 1.12.4__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 (60) hide show
  1. {punit-1.12.2/src/pUnit.egg-info → punit-1.12.4}/PKG-INFO +30 -4
  2. {punit-1.12.2 → punit-1.12.4}/README.md +28 -2
  3. {punit-1.12.2 → punit-1.12.4}/pyproject.toml +2 -2
  4. {punit-1.12.2 → punit-1.12.4/src/pUnit.egg-info}/PKG-INFO +30 -4
  5. {punit-1.12.2 → punit-1.12.4}/src/pUnit.egg-info/SOURCES.txt +1 -2
  6. {punit-1.12.2 → punit-1.12.4}/src/punit/__init__.py +4 -3
  7. {punit-1.12.2 → punit-1.12.4}/src/punit/cli.py +1 -1
  8. punit-1.12.2/src/punit/parallel.py → punit-1.12.4/src/punit/parallelism.py +230 -9
  9. {punit-1.12.2 → punit-1.12.4}/src/punit/runner.py +163 -6
  10. punit-1.12.2/src/punit/sequential.py +0 -63
  11. {punit-1.12.2 → punit-1.12.4}/LICENSE +0 -0
  12. {punit-1.12.2 → punit-1.12.4}/setup.cfg +0 -0
  13. {punit-1.12.2 → punit-1.12.4}/src/pUnit.egg-info/dependency_links.txt +0 -0
  14. {punit-1.12.2 → punit-1.12.4}/src/pUnit.egg-info/entry_points.txt +0 -0
  15. {punit-1.12.2 → punit-1.12.4}/src/pUnit.egg-info/requires.txt +0 -0
  16. {punit-1.12.2 → punit-1.12.4}/src/pUnit.egg-info/top_level.txt +0 -0
  17. {punit-1.12.2 → punit-1.12.4}/src/punit/SKILL.md +0 -0
  18. {punit-1.12.2 → punit-1.12.4}/src/punit/TestResult.py +0 -0
  19. {punit-1.12.2 → punit-1.12.4}/src/punit/TextIOCapture.py +0 -0
  20. {punit-1.12.2 → punit-1.12.4}/src/punit/__main__.py +0 -0
  21. {punit-1.12.2 → punit-1.12.4}/src/punit/assertions/__init__.py +0 -0
  22. {punit-1.12.2 → punit-1.12.4}/src/punit/assertions/collections.py +0 -0
  23. {punit-1.12.2 → punit-1.12.4}/src/punit/assertions/exceptions.py +0 -0
  24. {punit-1.12.2 → punit-1.12.4}/src/punit/assertions/numeric.py +0 -0
  25. {punit-1.12.2 → punit-1.12.4}/src/punit/assertions/strings.py +0 -0
  26. {punit-1.12.2 → punit-1.12.4}/src/punit/conditions/__init__.py +0 -0
  27. {punit-1.12.2 → punit-1.12.4}/src/punit/conditions/skip.py +0 -0
  28. {punit-1.12.2 → punit-1.12.4}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  29. {punit-1.12.2 → punit-1.12.4}/src/punit/discovery/__init__.py +0 -0
  30. {punit-1.12.2 → punit-1.12.4}/src/punit/facts/Fact.py +0 -0
  31. {punit-1.12.2 → punit-1.12.4}/src/punit/facts/FactManager.py +0 -0
  32. {punit-1.12.2 → punit-1.12.4}/src/punit/facts/__init__.py +0 -0
  33. {punit-1.12.2 → punit-1.12.4}/src/punit/filters/Filter.py +0 -0
  34. {punit-1.12.2 → punit-1.12.4}/src/punit/filters/FilterManager.py +0 -0
  35. {punit-1.12.2 → punit-1.12.4}/src/punit/filters/__init__.py +0 -0
  36. {punit-1.12.2 → punit-1.12.4}/src/punit/metadata/CallableMetadata.py +0 -0
  37. {punit-1.12.2 → punit-1.12.4}/src/punit/metadata/__init__.py +0 -0
  38. {punit-1.12.2 → punit-1.12.4}/src/punit/mocks/__init__.py +0 -0
  39. {punit-1.12.2 → punit-1.12.4}/src/punit/mocks/matcher.py +0 -0
  40. {punit-1.12.2 → punit-1.12.4}/src/punit/mocks/mock.py +0 -0
  41. {punit-1.12.2 → punit-1.12.4}/src/punit/mocks/patch.py +0 -0
  42. {punit-1.12.2 → punit-1.12.4}/src/punit/py.typed +0 -0
  43. {punit-1.12.2 → punit-1.12.4}/src/punit/reports/HtmlReportGenerator.py +0 -0
  44. {punit-1.12.2 → punit-1.12.4}/src/punit/reports/JUnitReportGenerator.py +0 -0
  45. {punit-1.12.2 → punit-1.12.4}/src/punit/reports/JsonReportGenerator.py +0 -0
  46. {punit-1.12.2 → punit-1.12.4}/src/punit/reports/__init__.py +0 -0
  47. {punit-1.12.2 → punit-1.12.4}/src/punit/results/__init__.py +0 -0
  48. {punit-1.12.2 → punit-1.12.4}/src/punit/results/fails.py +0 -0
  49. {punit-1.12.2 → punit-1.12.4}/src/punit/setups/Setup.py +0 -0
  50. {punit-1.12.2 → punit-1.12.4}/src/punit/setups/SetupManager.py +0 -0
  51. {punit-1.12.2 → punit-1.12.4}/src/punit/setups/__init__.py +0 -0
  52. {punit-1.12.2 → punit-1.12.4}/src/punit/teardowns/Teardown.py +0 -0
  53. {punit-1.12.2 → punit-1.12.4}/src/punit/teardowns/TeardownManager.py +0 -0
  54. {punit-1.12.2 → punit-1.12.4}/src/punit/teardowns/__init__.py +0 -0
  55. {punit-1.12.2 → punit-1.12.4}/src/punit/theories/Theory.py +0 -0
  56. {punit-1.12.2 → punit-1.12.4}/src/punit/theories/TheoryManager.py +0 -0
  57. {punit-1.12.2 → punit-1.12.4}/src/punit/theories/__init__.py +0 -0
  58. {punit-1.12.2 → punit-1.12.4}/src/punit/traits/Trait.py +0 -0
  59. {punit-1.12.2 → punit-1.12.4}/src/punit/traits/TraitManager.py +0 -0
  60. {punit-1.12.2 → punit-1.12.4}/src/punit/traits/__init__.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.12.2
3
+ Version: 1.12.4
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Documentation, https://punit.readthedocs.io/
8
8
  Project-URL: Homepage, https://github.com/wilson0x4d/punit
9
9
  Project-URL: Repository, https://github.com/wilson0x4d/punit.git
10
- Keywords: test,unittest,unit-test,xUnit,nUnit,pytest
10
+ Keywords: test,unittest,unit-test,xUnit,nUnit,pytest,mock,patch,parallelism,vscode,vscodium
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -25,6 +25,26 @@ Dynamic: license-file
25
25
 
26
26
  This README is only a high-level introduction to **pUnit**. For more detailed documentation, please view the official docs at [https://pUnit.readthedocs.io](https://pUnit.readthedocs.io).
27
27
 
28
+ ## Features
29
+
30
+ - **IDE Support**; Run/Debug tests and review results with full error detail from within vscode using the [x4d.punit-testadapter](https://marketplace.visualstudio.com/items?itemName=x4d.punit-testadapter) extension ([also available for VSCodium via Open VSX](https://open-vsx.org/extension/x4d/punit-testadapter))
31
+ - **Fact/Theory Based TEsting**; Separate `@fact` and `@theory` decorators distinguish invariant tests from variant (parameterized) tests, allowing for more concise test permutation syntaxes.
32
+ - **Parallel Test Execution**; Optionally run tests concurrently with multiple worker threads, each with its own async event loop. Enable via `--parallel` CLI flag or `@parallel` decorator. Mix parallel and `@sequential`-marked tests in the same file.
33
+ - **Dual-Scope Setup/Teardown**; Module-scoped and class-scoped setup/teardown as bare decorated functions, no fixture plumbing or injection required. Configure with `@setup` and `@teardown`.
34
+ - **Per-Test Output Capture**; Captures each test's stdout/stderr output in isolation, included in the test results and all report formats.
35
+ - **Traits**; Categorical name/value test metadata queryable at discovery time via CLI flags. Unlike traditional marks, traits are filterable during discovery, not at decoration, and can also be used to filter in the IDE.
36
+ - **Expected Failures with Inversion**; Invert test results for known bugs with human-readable reason tracking. Mark with `@fails(reason)`, useful for TDD workflows.
37
+ - **Mock Objects**; Single-class mocking with fluent API (`.returns()`, `.side_effect()`), origin conformance, delegate/spy support, nested child mocks, and iterator protocol. Located in `punit.mocks`.
38
+ - **Module Patching**; Decorator and context-manager patching for arbitrary Python paths with automatic restoration, with support for complex dotted attribute paths.
39
+ - **Argument Matchers**; Lightweight and extensible matchers (`is_any`, `contains`, `is_gt`, `is_lt`, `in_range`, `neg`) for flexible mock call assertions.
40
+ - **Conditionals**; Skip tests using boolean, callback, or unconditional logic via `@skip()`. Evaluates at execution time, allowing for complex state mutation/evaluation in test suites.
41
+ - **Multi-Format Reports**; Built-in HTML, jUnit XML, and JSON reports which include captured output, precise timing, and full error detail. Enable with `--report` and combine with `--output` to write to a file, great for CI/CD integrations.
42
+ - **Multi-Level Test Filtering**; Unified test pipeline that combines filename globbing, case-insensitive regex on test names, trait queries, and `--filter @FILEPATH` filters (loaded from ``FILEPATH``), `--filter @stdin` allows piping lists of filters in via stdin.
43
+ - **Direct File Execution**; Run individual `.py` files without a `tests/` package or `__init__.py`. Pass file paths directly to the CLI.
44
+ - **Assertion Helpers for Exceptions**; Type-safe exception assertion via generic syntax `raises[MyException](action)`. Supports strict (`exact=True`) and subclass matching.
45
+ - **Assertion Helpers for Collections**; Assert sequences, lists, sets, dicts, and tuples using `are_same`, `has_length` and `is_none_or_empty` for comparing elements, length bounds, and emptiness, or ordering.
46
+ - **Assertion Helpers for Numericals**; Directional comparators using `> approx(threshold)` with tolerance below and `< approx(threshold)` with tolerance above, also supports fluent syntaxes for more complex numerical requirements such as invariant boundary control, mixtures, etc.
47
+
28
48
  ## Command-Line Usage
29
49
 
30
50
  Running pUnit with no arguments will perform test auto-discovery and execution:
@@ -99,7 +119,7 @@ Options:
99
119
  Default: '*.py'
100
120
  -e, --exclude PATTERN
101
121
  Exclude test files matching PATTERN, overriding --include
102
- Default: '__*__' (dunders)
122
+ Default: '__*__' (dunder files)
103
123
  -f, --filter PATTERN|@FILEPATH
104
124
  Only execute tests matching PATTERN
105
125
  Default: '*'
@@ -109,7 +129,7 @@ Options:
109
129
  Execute tests with the specified trait, negated by prefixing with '!'.
110
130
  If VALUE is specified, matches tests with the trait having specified value.
111
131
  If VALUE is not specified, matches any test with the trait having any value.
112
- Default: No filtering based on traits.
132
+ Default: No filtering based on traits.
113
133
  -w, --working-directory PATH
114
134
  Working directory (defaults to start directory)
115
135
  -n, --no-default-patterns
@@ -127,6 +147,12 @@ Options:
127
147
  If `--report` is used, instead of writing to stdout
128
148
  write to FILENAME. In this case `--report` does not
129
149
  suppress any program output.
150
+ FILE
151
+ One or more .py files to run directly.
152
+ When any FILE is specified, only those files are tested;
153
+ -p/--test-package, --include, and --exclude patterns on
154
+ discovered files are ignored (test filtering via -f/-t
155
+ still applies).
130
156
  ```
131
157
 
132
158
  ## Writing Tests
@@ -2,6 +2,26 @@
2
2
 
3
3
  This README is only a high-level introduction to **pUnit**. For more detailed documentation, please view the official docs at [https://pUnit.readthedocs.io](https://pUnit.readthedocs.io).
4
4
 
5
+ ## Features
6
+
7
+ - **IDE Support**; Run/Debug tests and review results with full error detail from within vscode using the [x4d.punit-testadapter](https://marketplace.visualstudio.com/items?itemName=x4d.punit-testadapter) extension ([also available for VSCodium via Open VSX](https://open-vsx.org/extension/x4d/punit-testadapter))
8
+ - **Fact/Theory Based TEsting**; Separate `@fact` and `@theory` decorators distinguish invariant tests from variant (parameterized) tests, allowing for more concise test permutation syntaxes.
9
+ - **Parallel Test Execution**; Optionally run tests concurrently with multiple worker threads, each with its own async event loop. Enable via `--parallel` CLI flag or `@parallel` decorator. Mix parallel and `@sequential`-marked tests in the same file.
10
+ - **Dual-Scope Setup/Teardown**; Module-scoped and class-scoped setup/teardown as bare decorated functions, no fixture plumbing or injection required. Configure with `@setup` and `@teardown`.
11
+ - **Per-Test Output Capture**; Captures each test's stdout/stderr output in isolation, included in the test results and all report formats.
12
+ - **Traits**; Categorical name/value test metadata queryable at discovery time via CLI flags. Unlike traditional marks, traits are filterable during discovery, not at decoration, and can also be used to filter in the IDE.
13
+ - **Expected Failures with Inversion**; Invert test results for known bugs with human-readable reason tracking. Mark with `@fails(reason)`, useful for TDD workflows.
14
+ - **Mock Objects**; Single-class mocking with fluent API (`.returns()`, `.side_effect()`), origin conformance, delegate/spy support, nested child mocks, and iterator protocol. Located in `punit.mocks`.
15
+ - **Module Patching**; Decorator and context-manager patching for arbitrary Python paths with automatic restoration, with support for complex dotted attribute paths.
16
+ - **Argument Matchers**; Lightweight and extensible matchers (`is_any`, `contains`, `is_gt`, `is_lt`, `in_range`, `neg`) for flexible mock call assertions.
17
+ - **Conditionals**; Skip tests using boolean, callback, or unconditional logic via `@skip()`. Evaluates at execution time, allowing for complex state mutation/evaluation in test suites.
18
+ - **Multi-Format Reports**; Built-in HTML, jUnit XML, and JSON reports which include captured output, precise timing, and full error detail. Enable with `--report` and combine with `--output` to write to a file, great for CI/CD integrations.
19
+ - **Multi-Level Test Filtering**; Unified test pipeline that combines filename globbing, case-insensitive regex on test names, trait queries, and `--filter @FILEPATH` filters (loaded from ``FILEPATH``), `--filter @stdin` allows piping lists of filters in via stdin.
20
+ - **Direct File Execution**; Run individual `.py` files without a `tests/` package or `__init__.py`. Pass file paths directly to the CLI.
21
+ - **Assertion Helpers for Exceptions**; Type-safe exception assertion via generic syntax `raises[MyException](action)`. Supports strict (`exact=True`) and subclass matching.
22
+ - **Assertion Helpers for Collections**; Assert sequences, lists, sets, dicts, and tuples using `are_same`, `has_length` and `is_none_or_empty` for comparing elements, length bounds, and emptiness, or ordering.
23
+ - **Assertion Helpers for Numericals**; Directional comparators using `> approx(threshold)` with tolerance below and `< approx(threshold)` with tolerance above, also supports fluent syntaxes for more complex numerical requirements such as invariant boundary control, mixtures, etc.
24
+
5
25
  ## Command-Line Usage
6
26
 
7
27
  Running pUnit with no arguments will perform test auto-discovery and execution:
@@ -76,7 +96,7 @@ Options:
76
96
  Default: '*.py'
77
97
  -e, --exclude PATTERN
78
98
  Exclude test files matching PATTERN, overriding --include
79
- Default: '__*__' (dunders)
99
+ Default: '__*__' (dunder files)
80
100
  -f, --filter PATTERN|@FILEPATH
81
101
  Only execute tests matching PATTERN
82
102
  Default: '*'
@@ -86,7 +106,7 @@ Options:
86
106
  Execute tests with the specified trait, negated by prefixing with '!'.
87
107
  If VALUE is specified, matches tests with the trait having specified value.
88
108
  If VALUE is not specified, matches any test with the trait having any value.
89
- Default: No filtering based on traits.
109
+ Default: No filtering based on traits.
90
110
  -w, --working-directory PATH
91
111
  Working directory (defaults to start directory)
92
112
  -n, --no-default-patterns
@@ -104,6 +124,12 @@ Options:
104
124
  If `--report` is used, instead of writing to stdout
105
125
  write to FILENAME. In this case `--report` does not
106
126
  suppress any program output.
127
+ FILE
128
+ One or more .py files to run directly.
129
+ When any FILE is specified, only those files are tested;
130
+ -p/--test-package, --include, and --exclude patterns on
131
+ discovered files are ignored (test filtering via -f/-t
132
+ still applies).
107
133
  ```
108
134
 
109
135
  ## Writing Tests
@@ -1,8 +1,8 @@
1
1
  [project]
2
2
  name = "pUnit"
3
- version = "1.12.2"
3
+ version = "1.12.4"
4
4
  description = "A modernized unit-test framework for Python."
5
- keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest"]
5
+ keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest", "mock", "patch", "parallelism", "vscode", "vscodium"]
6
6
  authors = [
7
7
  { name="Shaun Wilson", email="mrshaunwilson@msn.com" }
8
8
  ]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.12.2
3
+ Version: 1.12.4
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Documentation, https://punit.readthedocs.io/
8
8
  Project-URL: Homepage, https://github.com/wilson0x4d/punit
9
9
  Project-URL: Repository, https://github.com/wilson0x4d/punit.git
10
- Keywords: test,unittest,unit-test,xUnit,nUnit,pytest
10
+ Keywords: test,unittest,unit-test,xUnit,nUnit,pytest,mock,patch,parallelism,vscode,vscodium
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -25,6 +25,26 @@ Dynamic: license-file
25
25
 
26
26
  This README is only a high-level introduction to **pUnit**. For more detailed documentation, please view the official docs at [https://pUnit.readthedocs.io](https://pUnit.readthedocs.io).
27
27
 
28
+ ## Features
29
+
30
+ - **IDE Support**; Run/Debug tests and review results with full error detail from within vscode using the [x4d.punit-testadapter](https://marketplace.visualstudio.com/items?itemName=x4d.punit-testadapter) extension ([also available for VSCodium via Open VSX](https://open-vsx.org/extension/x4d/punit-testadapter))
31
+ - **Fact/Theory Based TEsting**; Separate `@fact` and `@theory` decorators distinguish invariant tests from variant (parameterized) tests, allowing for more concise test permutation syntaxes.
32
+ - **Parallel Test Execution**; Optionally run tests concurrently with multiple worker threads, each with its own async event loop. Enable via `--parallel` CLI flag or `@parallel` decorator. Mix parallel and `@sequential`-marked tests in the same file.
33
+ - **Dual-Scope Setup/Teardown**; Module-scoped and class-scoped setup/teardown as bare decorated functions, no fixture plumbing or injection required. Configure with `@setup` and `@teardown`.
34
+ - **Per-Test Output Capture**; Captures each test's stdout/stderr output in isolation, included in the test results and all report formats.
35
+ - **Traits**; Categorical name/value test metadata queryable at discovery time via CLI flags. Unlike traditional marks, traits are filterable during discovery, not at decoration, and can also be used to filter in the IDE.
36
+ - **Expected Failures with Inversion**; Invert test results for known bugs with human-readable reason tracking. Mark with `@fails(reason)`, useful for TDD workflows.
37
+ - **Mock Objects**; Single-class mocking with fluent API (`.returns()`, `.side_effect()`), origin conformance, delegate/spy support, nested child mocks, and iterator protocol. Located in `punit.mocks`.
38
+ - **Module Patching**; Decorator and context-manager patching for arbitrary Python paths with automatic restoration, with support for complex dotted attribute paths.
39
+ - **Argument Matchers**; Lightweight and extensible matchers (`is_any`, `contains`, `is_gt`, `is_lt`, `in_range`, `neg`) for flexible mock call assertions.
40
+ - **Conditionals**; Skip tests using boolean, callback, or unconditional logic via `@skip()`. Evaluates at execution time, allowing for complex state mutation/evaluation in test suites.
41
+ - **Multi-Format Reports**; Built-in HTML, jUnit XML, and JSON reports which include captured output, precise timing, and full error detail. Enable with `--report` and combine with `--output` to write to a file, great for CI/CD integrations.
42
+ - **Multi-Level Test Filtering**; Unified test pipeline that combines filename globbing, case-insensitive regex on test names, trait queries, and `--filter @FILEPATH` filters (loaded from ``FILEPATH``), `--filter @stdin` allows piping lists of filters in via stdin.
43
+ - **Direct File Execution**; Run individual `.py` files without a `tests/` package or `__init__.py`. Pass file paths directly to the CLI.
44
+ - **Assertion Helpers for Exceptions**; Type-safe exception assertion via generic syntax `raises[MyException](action)`. Supports strict (`exact=True`) and subclass matching.
45
+ - **Assertion Helpers for Collections**; Assert sequences, lists, sets, dicts, and tuples using `are_same`, `has_length` and `is_none_or_empty` for comparing elements, length bounds, and emptiness, or ordering.
46
+ - **Assertion Helpers for Numericals**; Directional comparators using `> approx(threshold)` with tolerance below and `< approx(threshold)` with tolerance above, also supports fluent syntaxes for more complex numerical requirements such as invariant boundary control, mixtures, etc.
47
+
28
48
  ## Command-Line Usage
29
49
 
30
50
  Running pUnit with no arguments will perform test auto-discovery and execution:
@@ -99,7 +119,7 @@ Options:
99
119
  Default: '*.py'
100
120
  -e, --exclude PATTERN
101
121
  Exclude test files matching PATTERN, overriding --include
102
- Default: '__*__' (dunders)
122
+ Default: '__*__' (dunder files)
103
123
  -f, --filter PATTERN|@FILEPATH
104
124
  Only execute tests matching PATTERN
105
125
  Default: '*'
@@ -109,7 +129,7 @@ Options:
109
129
  Execute tests with the specified trait, negated by prefixing with '!'.
110
130
  If VALUE is specified, matches tests with the trait having specified value.
111
131
  If VALUE is not specified, matches any test with the trait having any value.
112
- Default: No filtering based on traits.
132
+ Default: No filtering based on traits.
113
133
  -w, --working-directory PATH
114
134
  Working directory (defaults to start directory)
115
135
  -n, --no-default-patterns
@@ -127,6 +147,12 @@ Options:
127
147
  If `--report` is used, instead of writing to stdout
128
148
  write to FILENAME. In this case `--report` does not
129
149
  suppress any program output.
150
+ FILE
151
+ One or more .py files to run directly.
152
+ When any FILE is specified, only those files are tested;
153
+ -p/--test-package, --include, and --exclude patterns on
154
+ discovered files are ignored (test filtering via -f/-t
155
+ still applies).
130
156
  ```
131
157
 
132
158
  ## Writing Tests
@@ -13,10 +13,9 @@ src/punit/TextIOCapture.py
13
13
  src/punit/__init__.py
14
14
  src/punit/__main__.py
15
15
  src/punit/cli.py
16
- src/punit/parallel.py
16
+ src/punit/parallelism.py
17
17
  src/punit/py.typed
18
18
  src/punit/runner.py
19
- src/punit/sequential.py
20
19
  src/punit/assertions/__init__.py
21
20
  src/punit/assertions/collections.py
22
21
  src/punit/assertions/exceptions.py
@@ -14,12 +14,12 @@ from .conditions import skip
14
14
  from .teardowns import Teardown, teardown
15
15
  from .theories import theory, inlinedata
16
16
  from .traits import trait
17
- from .sequential import sequential
17
+ from .parallelism import parallel, sequential
18
18
 
19
19
  from . import mocks
20
20
 
21
- __version__ = '1.12.2'
22
- __commit__ = '3afa951'
21
+ __version__ = '1.12.4'
22
+ __commit__ = '43fecfd'
23
23
  __all__ = [
24
24
  '__version__', '__commit__',
25
25
  'assertions',
@@ -36,4 +36,5 @@ __all__ = [
36
36
  'fails',
37
37
  'skip',
38
38
  'sequential',
39
+ 'parallel',
39
40
  ]
@@ -205,7 +205,7 @@ class CommandLineInterface:
205
205
  return self.__aliases
206
206
 
207
207
  @property
208
- def parallel(self) -> int | None:
208
+ def parallelism(self) -> int | None:
209
209
  return self.__parallelism
210
210
 
211
211
  @property
@@ -2,14 +2,90 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
4
  """
5
- Parallel test execution infrastructure for pUnit.
5
+ Parallelism for pUnit.
6
6
 
7
- Provides a pool of worker threads, each with its own asyncio event loop,
8
- for executing tests in parallel.
7
+ pUnit provides three mechanisms for controlling test execution parallelism:
8
+ the **``--parallel [THREADS]``** CLI flag, the **``@parallel``** decorator,
9
+ and the **``@sequential``** decorator.
10
+
11
+ CLI flag
12
+ --------
13
+ The ``--parallel [THREADS]`` option enables multi-threaded parallel execution.
14
+ Each worker thread runs its own asyncio event loop for isolation.
15
+
16
+ * ``--parallel`` (bare flag, no number) runs tests using
17
+ ``cpu_count // 2`` workers.
18
+ * ``--parallel N`` runs tests using *N* workers.
19
+ * No flag means tests run sequentially, one after another.
20
+
21
+ When parallel mode is active the runner processes each test file in four
22
+ batches: parallel facts, parallel theories, sequential facts, then
23
+ sequential theories.
24
+
25
+ @parallel decorator
26
+ -------------------
27
+ Marking a fact or theory with ``@parallel`` triggers **auto-enable** mode.
28
+ When pUnit scans the test package and finds at least one ``@parallel``
29
+ decorated test (and no ``--parallel`` flag was given on the CLI):
30
+
31
+ * Parallel execution is enabled automatically with
32
+ ``cpu_count // 2`` workers.
33
+ * **Only** ``@parallel``-marked tests run in the concurrent batch.
34
+ * All other tests are treated as sequential (as if they carried ``@sequential``).
35
+
36
+ Applying ``@parallel`` to a **class** marks every method of that class.
37
+
38
+ ``@parallel`` is a no-op when ``--parallel`` is specified on the CLI; the
39
+ CLI flag takes full priority.
40
+
41
+ @sequential decorator
42
+ ---------------------
43
+ Marking a fact or theory with ``@sequential`` forces it to run as a
44
+ sequential test **after** all parralel tests for that file have
45
+ completed. This lets you co-run certain tests one-at-a-time inside
46
+ an otherwise parallel test suite (for example, tests that share mutable
47
+ state).
48
+
49
+ When no ``@parallel`` decorator is present and ``--parallel`` is not used
50
+ on the CLI, all tests run sequentially (the default).
51
+
52
+ Examples
53
+ --------
54
+
55
+ Enable parallel mode for the entire test package via CLI::
56
+
57
+ punit tests/ --parallel 4
58
+
59
+ Run only selected tests in parallel -- no CLI flag needed::
60
+
61
+ from punit import fact, parallel
62
+
63
+ @fact
64
+ @parallel
65
+ def test_network_io():
66
+ ...
67
+
68
+ @fact
69
+ def test_database_setup():
70
+ # Runs sequentially alongside the parallel tests
71
+ ...
72
+
73
+ Mix parallel and sequential tests inside a parallel suite::
74
+
75
+ from punit import fact, parallel, sequential
76
+
77
+ @parallel
78
+ class SlowTests:
79
+ @fact
80
+ def test_heavy_computation(self):
81
+ ...
82
+
83
+ @fact
84
+ @sequential
85
+ def test_shared_file_handle(self):
86
+ # Runs one-at-a-time after all other SlowTests finish
87
+ ...
9
88
 
10
- Each test is wrapped in a coroutine that creates a ``TestResult``, runs
11
- setup -> test -> teardown, and yields the populated result. The main thread
12
- dispatches these coroutines to worker threads and collects the results.
13
89
  """
14
90
 
15
91
  from __future__ import annotations
@@ -20,7 +96,7 @@ import queue
20
96
  import threading
21
97
  import time
22
98
  from types import ModuleType
23
- from typing import Any
99
+ from typing import Any, Callable
24
100
 
25
101
  from .TestResult import TestResult
26
102
 
@@ -239,7 +315,7 @@ async def _execute_fact(
239
315
 
240
316
  async def _execute_theory(
241
317
  theory: Any,
242
- data: tuple,
318
+ data: tuple[Any, ...],
243
319
  module: ModuleType,
244
320
  module_report_name: str,
245
321
  filename: str,
@@ -330,6 +406,151 @@ async def _execute_theory(
330
406
  return result
331
407
 
332
408
 
409
+ def sequential(target: Callable[..., Any] | type) -> Callable[..., Any]:
410
+ """Mark a test function, method, or class for **sequential** execution.
411
+
412
+ When pUnit is started with ``--parallel THREADS`` (where *THREADS* > 1),
413
+ tests decorated without ``@sequential`` run in parallel up to *N* at
414
+ any given point. ``@sequential`` tests are **not** added to the
415
+ concurrent dispatch queue -- after all peers at the same scope finish
416
+ concurrently, the sequential tests run one after another in their
417
+ definition order.
418
+
419
+ When applied to a **class**, all methods of the class are automatically
420
+ marked as sequential.
421
+
422
+ The decorator returns the original ``target`` unchanged; it installs the
423
+ ``__punit_sequential`` marker attribute on the unwrapped function (or the
424
+ class object itself for class targets).
425
+
426
+ Parameters
427
+ ----------
428
+ target : Callable | type
429
+ The test function, method, or class to mark.
430
+
431
+ Returns
432
+ -------
433
+ Callable
434
+ The original, undecorated target.
435
+
436
+ Example
437
+ -------
438
+
439
+ .. code-block:: python
440
+
441
+ from punit import fact, sequential
442
+
443
+ @fact
444
+ @sequential
445
+ def my_test():
446
+ assert True
447
+
448
+ class MyTestCase:
449
+ @fact
450
+ @sequential
451
+ def test_one(self):
452
+ assert True
453
+
454
+ # Apply @sequential to an entire class -- all methods run sequentially
455
+ @sequential
456
+ class SequentialTestCase:
457
+ @fact
458
+ def test_one(self):
459
+ assert True
460
+
461
+ @fact
462
+ def test_two(self):
463
+ assert True
464
+
465
+ """
466
+ import inspect
467
+
468
+ if isinstance(target, type):
469
+ setattr(target, "__punit_sequential", True)
470
+ for name, method in inspect.getmembers(target, predicate=inspect.isfunction):
471
+ setattr(method, "__punit_sequential", True)
472
+ return target
473
+
474
+ unwrapped = inspect.unwrap(target)
475
+ setattr(unwrapped, "__punit_sequential", True)
476
+ return target
477
+
478
+
479
+ def parallel(target: Callable[..., Any]) -> Callable[..., Any]:
480
+ """Mark a test function, method, or class for **parallel** execution.
481
+
482
+ When pUnit detects any ``@parallel``-decorated tests in a test package at
483
+ runtime, it automatically triggers (multi-threaded) parallel execution for
484
+ the entire test run -- no ``--parallel`` CLI flag is required.
485
+
486
+ When ``@parallel`` triggers auto-enable:
487
+
488
+ * Only tests marked with ``@parallel`` run in the parallel batch
489
+ * Tests not marked with ``@parallel`` are treated as sequential
490
+ (as if decorated with ``@sequential``)
491
+ * The thread pool is initialized with ``cpu_count // 2`` workers
492
+ * This auto-enable is superseded by ``--parallel`` on the CLI
493
+
494
+ When ``--parallel`` is specified on the CLI, this decorator has no effect;
495
+ the existing ``--parallel`` behavior takes priority and all
496
+ non-``@sequential`` tests run in parallel as before.
497
+
498
+ When applied to a class, all methods of the class are automatically marked
499
+ as parallel.
500
+
501
+ The decorator returns the original ``target`` unchanged; it installs the
502
+ ``__punit_parallel`` marker attribute on the unwrapped function (or the
503
+ class object itself for class targets).
504
+
505
+ Parameters
506
+ ----------
507
+ target : Callable | type
508
+ The test function, method, or class to mark.
509
+
510
+ Returns
511
+ -------
512
+ Callable
513
+ The original, undecorated target.
514
+
515
+ Example
516
+ -------
517
+
518
+ .. code-block:: python
519
+
520
+ from punit import fact, parallel
521
+
522
+ @fact
523
+ @parallel
524
+ def my_parallel_test():
525
+ assert True
526
+
527
+ class ParallelTestCase:
528
+ @fact
529
+ @parallel
530
+ def test_one(self):
531
+ assert True
532
+
533
+ @fact
534
+ def test_sequential(self):
535
+ # Runs sequentially (not in parallel batch)
536
+ assert True
537
+
538
+ """
539
+ import inspect
540
+
541
+ if isinstance(target, type):
542
+ setattr(target, "__punit_parallel", True)
543
+ for name, method in inspect.getmembers(target, predicate=inspect.isfunction):
544
+ setattr(method, "__punit_parallel", True)
545
+ return target
546
+
547
+ unwrapped = inspect.unwrap(target)
548
+ setattr(unwrapped, "__punit_parallel", True)
549
+ return target
550
+
551
+
333
552
  __all__ = [
334
- 'ThreadPool'
553
+ 'ThreadPool',
554
+ 'parallel',
555
+ 'sequential'
335
556
  ]
@@ -13,7 +13,7 @@ from typing import Any, Callable, Optional
13
13
 
14
14
  from .TextIOCapture import setup_global_text_io, _PERSISTENT_STDOUT, _PERSISTENT_STDERR
15
15
  from .cli import CommandLineInterface
16
- from .parallel import ThreadPool, _execute_fact, _execute_theory
16
+ from .parallelism import ThreadPool, _execute_fact, _execute_theory
17
17
  from .facts.FactManager import FactManager
18
18
  from .setups.SetupManager import SetupManager
19
19
  from .teardowns.TeardownManager import TeardownManager
@@ -43,19 +43,25 @@ def _is_sequential(target: Any) -> bool:
43
43
  return getattr(unwrapped, '__punit_sequential', None) is True
44
44
 
45
45
 
46
+ def _is_parallel(target: Any) -> bool:
47
+ """Return True if *target* is marked with ``@parallel``."""
48
+ unwrapped = inspect.unwrap(target)
49
+ return getattr(unwrapped, '__punit_parallel', None) is True
50
+
51
+
46
52
  def _get_parallelism(cli: 'CommandLineInterface') -> int:
47
53
  """Resolve the parallelism level from CLI.
48
54
 
49
- Returns ``-1`` when parallel mode is disabled (use sequential flow).
55
+ Returns ``-1`` when parallelism is disabled (use sequential flow).
50
56
  Returns the worker count when enabled; ``0`` maps to ``cpu_count // 2``.
51
57
  """
52
- mode = cli.parallel
53
- if mode is None:
58
+ parallelism = cli.parallelism
59
+ if parallelism is None:
54
60
  return -1
55
- if mode <= 0:
61
+ if parallelism <= 0:
56
62
  cores = os.cpu_count() or 1
57
63
  return max(1, cores // 2)
58
- return mode
64
+ return parallelism
59
65
 
60
66
 
61
67
  class TestRunner:
@@ -167,6 +173,22 @@ class TestRunner:
167
173
  if self.__cli.verbose and (not test_result.is_success) and test_result.exception is not None:
168
174
  print(f'Test File:\n {test_result.file_name}\nError:\n {test_result.exception}\n Traceback:\n{"".join(traceback.format_tb(test_result.exception.__traceback__))}')
169
175
 
176
+ def __detect_parallel_tests(self) -> bool:
177
+ """Return True if any registered fact or theory is marked with ``@parallel``."""
178
+ fm = FactManager.instance()
179
+ modules = getattr(fm, '_FactManager__modules', {})
180
+ for facts in modules.values():
181
+ for fact in facts:
182
+ if _is_parallel(fact.target):
183
+ return True
184
+ tm = TheoryManager.instance()
185
+ modules = getattr(tm, '_TheoryManager__modules', {})
186
+ for theories in modules.values():
187
+ for theory in theories:
188
+ if _is_parallel(theory.target):
189
+ return True
190
+ return False
191
+
170
192
  async def __run_facts(self, host_name: str, test_module: ModuleType, filename: str, module_report_name: str, results: list[TestResult]) -> None:
171
193
  """Run all facts for a test module and return their results."""
172
194
  facts = FactManager.instance().get(test_module.__name__)
@@ -288,6 +310,14 @@ class TestRunner:
288
310
  if parallelism > 0:
289
311
  return await self._run_parallel(results, test_package_path)
290
312
 
313
+ # Auto-enable parallel mode when @parallel-decorated tests are detected
314
+ has_parallel_tests = self.__detect_parallel_tests()
315
+ if has_parallel_tests:
316
+ cores = os.cpu_count() or 1
317
+ parallelism = max(1, cores // 2)
318
+ await self._run_parallel_parallel_only(results, test_package_path, parallelism)
319
+ return results
320
+
291
321
  for filename in self.__filenames:
292
322
  ts = time.time()
293
323
  if not self.__test_package_name:
@@ -333,6 +363,82 @@ class TestRunner:
333
363
  # Parallel execution
334
364
  # ------------------------------------------------------------------
335
365
 
366
+ async def _run_parallel_parallel_only(
367
+ self,
368
+ results: list[TestResult],
369
+ test_package_path: str,
370
+ parallelism: int,
371
+ ) -> None:
372
+ """Execute ``@parallel``-decorated tests in a worker pool, rest sequentially.
373
+
374
+ This is used when ``@parallel`` decorators are detected at runtime and
375
+ no ``--parallel`` CLI flag was provided: only tests marked with
376
+ ``@parallel`` go into the parallel batch, everything else runs one-by-one.
377
+ """
378
+ try:
379
+ pool = ThreadPool(parallelism)
380
+ with pool:
381
+ module_report_name = ''
382
+ for filename in self.__filenames:
383
+ try:
384
+ if not self.__test_package_name:
385
+ module_import_name = filename
386
+ else:
387
+ module_import_name = filename.replace(test_package_path, '').replace('/', '.')
388
+ if module_import_name.endswith('.py'):
389
+ module_import_name = module_import_name[:-3]
390
+ module_report_name = module_import_name.lstrip('.')
391
+ if not self.__test_package_name:
392
+ test_module = importlib.import_module(module_import_name)
393
+ else:
394
+ test_module = importlib.import_module(module_import_name, self.__test_package_name)
395
+ host_name = socket.gethostname()
396
+
397
+ await self._run_parallel_facts_parallel_only(host_name, test_module, filename, module_report_name, results, pool)
398
+ if self.__cli.failfast and any(e.is_success is False for e in results):
399
+ break
400
+
401
+ await self._run_parallel_theories_parallel_only(host_name, test_module, filename, module_report_name, results, pool)
402
+ if self.__cli.failfast and any(e.is_success is False for e in results):
403
+ break
404
+
405
+ facts = FactManager.instance().get(test_module.__name__)
406
+ for fact in facts:
407
+ if _is_parallel(fact.target):
408
+ continue
409
+ test_result = await _execute_fact(fact, test_module, module_report_name, filename, host_name, self.__test_package_name)
410
+ results.append(test_result)
411
+ self.print_test_result(test_result)
412
+ if self.__cli.failfast and not test_result.is_success:
413
+ return
414
+
415
+ theories = TheoryManager.instance().get(test_module.__name__)
416
+ for theory in theories:
417
+ if _is_parallel(theory.target):
418
+ continue
419
+ for data in theory.datas:
420
+ test_result = await _execute_theory(theory, data, test_module, module_report_name, filename, host_name, self.__test_package_name)
421
+ results.append(test_result)
422
+ self.print_test_result(test_result)
423
+ if self.__cli.failfast and not test_result.is_success:
424
+ return
425
+
426
+ except Exception as ex:
427
+ result = TestResult()
428
+ result.class_name = '*'
429
+ result.module_name = module_report_name
430
+ result.test_name = '*'
431
+ result.start_time = time.time()
432
+ result.stop_time = time.time()
433
+ result.is_success = False
434
+ result.exception = ex
435
+ results.append(result)
436
+ self.print_test_result(result)
437
+ if self.__cli.failfast:
438
+ break
439
+ except Exception:
440
+ pass
441
+
336
442
  async def _run_parallel(self, results: list[TestResult], test_package_path: str) -> list[TestResult]:
337
443
  """Execute all tests with a worker pool (each worker has its own event loop)."""
338
444
  parallelism = _get_parallelism(self.__cli)
@@ -474,3 +580,54 @@ class TestRunner:
474
580
  self.print_test_result(test_result)
475
581
  if self.__cli.failfast and not test_result.is_success:
476
582
  return
583
+
584
+ async def _run_parallel_facts_parallel_only(
585
+ self,
586
+ host_name: str,
587
+ test_module: ModuleType,
588
+ filename: str,
589
+ module_report_name: str,
590
+ results: list[TestResult],
591
+ pool: ThreadPool
592
+ ) -> None:
593
+ """Run @parallel facts in parallel (auto-enable mode)."""
594
+ facts = FactManager.instance().get(test_module.__name__)
595
+ coros: list[Any] = []
596
+ for fact in facts:
597
+ if not _is_parallel(fact.target):
598
+ continue
599
+ coro = _execute_fact(fact, test_module, module_report_name, filename, host_name, self.__test_package_name)
600
+ coros.append(coro)
601
+ if coros:
602
+ test_results = await asyncio.gather(*coros)
603
+ for test_result in test_results:
604
+ results.append(test_result)
605
+ self.print_test_result(test_result)
606
+ if self.__cli.failfast and not test_result.is_success:
607
+ return
608
+
609
+ async def _run_parallel_theories_parallel_only(
610
+ self,
611
+ host_name: str,
612
+ test_module: ModuleType,
613
+ filename: str,
614
+ module_report_name: str,
615
+ results: list[TestResult],
616
+ pool: ThreadPool
617
+ ) -> None:
618
+ """Run @parallel theory data-points in parallel (auto-enable mode)."""
619
+ theories = TheoryManager.instance().get(test_module.__name__)
620
+ coros: list[Any] = []
621
+ for theory in theories:
622
+ if not _is_parallel(theory.target):
623
+ continue
624
+ for data in theory.datas:
625
+ coro = _execute_theory(theory, data, test_module, module_report_name, filename, host_name, self.__test_package_name)
626
+ coros.append(coro)
627
+ if coros:
628
+ test_results = await asyncio.gather(*coros)
629
+ for test_result in test_results:
630
+ results.append(test_result)
631
+ self.print_test_result(test_result)
632
+ if self.__cli.failfast and not test_result.is_success:
633
+ return
@@ -1,63 +0,0 @@
1
- # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
- # SPDX-License-Identifier: MIT
3
-
4
- """
5
- ``@sequential`` decorator for pUnit.
6
-
7
- When running tests with ``--parallel``, any test decorated with
8
- ``@sequential`` is deferred to a sequential pass that runs after all
9
- parallel peers at the same scope have finished.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from typing import Any, Callable
15
-
16
-
17
- def sequential(target: Callable[..., Any]) -> Callable[..., Any]:
18
- """Mark a test function or method for **sequential** (sequential) execution.
19
-
20
- When pUnit is started with ``--parralel THREADS`` (where *THREADS* > 1),
21
- tests decorated without ``@sequential`` run in parallel up to *N* at
22
- any given point. ``@sequential`` tests are **not** added to the
23
- concurrent dispatch queue -- after all peers at the same scope finish
24
- concurrently, the sequential tests run one after another in their
25
- definition order.
26
-
27
- The decorator returns the original ``target`` unchanged; it installs the
28
- ``__punit_sequential`` marker attribute on the unwrapped function.
29
-
30
- Parameters
31
- ----------
32
- target : Callable
33
- The test function or method to mark.
34
-
35
- Returns
36
- -------
37
- Callable
38
- The original, undecorated target.
39
-
40
- Example
41
- -------
42
-
43
- .. code-block:: python
44
-
45
- from punit import fact, sequential
46
-
47
- @fact
48
- @sequential
49
- def my_test():
50
- assert True
51
-
52
- class MyTestCase:
53
- @fact
54
- @sequential
55
- def test_one(self):
56
- assert True
57
-
58
- """
59
- import inspect
60
-
61
- unwrapped = inspect.unwrap(target)
62
- setattr(unwrapped, "__punit_sequential", True)
63
- return target
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes