testsweet 0.2.0__tar.gz → 0.2.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.
- testsweet-0.2.2/.github/workflows/lint.yml +16 -0
- testsweet-0.2.2/.github/workflows/publish.yml +71 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/.github/workflows/tests.yml +4 -9
- testsweet-0.2.2/.github/workflows/types.yml +16 -0
- testsweet-0.2.2/CHANGELOG.md +103 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/CLAUDE.md +3 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/PKG-INFO +3 -1
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/reference.md +5 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/pyproject.toml +30 -14
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/__main__.py +7 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_assertion.py +2 -2
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_condition_decorator.py +2 -1
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_config.py +3 -3
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_discover.py +10 -3
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_loaders.py +0 -4
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_report.py +12 -6
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_resolve.py +7 -8
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_tag.py +1 -1
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_targets.py +2 -2
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/cli.py +13 -0
- testsweet-0.2.2/tests/fixtures/runner/has_broken_import.py +1 -0
- testsweet-0.2.2/tests/fixtures/runner/uses_relative_import.py +8 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/outcomes.py +2 -2
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/plugins.py +4 -4
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/report.py +5 -2
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/runner.py +0 -1
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/skip.py +1 -1
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/targets.py +15 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/xfail.py +1 -1
- testsweet-0.2.2/uv.lock +231 -0
- testsweet-0.2.0/.github/workflows/publish.yml +0 -38
- testsweet-0.2.0/CHANGELOG.md +0 -54
- testsweet-0.2.0/claude/specs/2026-05-03_outcome-decorators.md +0 -383
- testsweet-0.2.0/docs/roadmap/examples/django.py +0 -48
- testsweet-0.2.0/tests/fixtures/runner/has_broken_import.py +0 -1
- testsweet-0.2.0/uv.lock +0 -357
- {testsweet-0.2.0 → testsweet-0.2.2}/.gitignore +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/.pre-commit-config.yaml +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/.python-version +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/LICENSE +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/README.md +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/contributing.md +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/catches.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/classes.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/functions.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/getting-started.md +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/docs/img/testsweet_200x200.png +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/__init__.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_catches.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_class_helpers.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_classify.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_markers.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_outcomes.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_plugins.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_runner.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_skip.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_tag_filter.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_walk.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_xfail.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/py.typed +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/__init__.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/catches.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/config.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/discover.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/__init__.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/empty.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/imported_only.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/mixed.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/mixed_local_imported.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/multiple.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/non_callable_marker.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/__init__.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/all_pass.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/assertion_diagnostics.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_calls_recorded.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_decorated_simple.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_decorated_with_cm.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_enter_only.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_enter_raises.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_exit_raises.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_method_fails.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_mixed_with_function.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_simple.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_test_context_raises.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_test_context_with_params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_inheritance.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_test_context.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_underscore_methods.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/empty.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/has_failure.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/keyboard_interrupt.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/non_assertion_error.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_skip.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_tag.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_xfail.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_empty.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_generator.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_generator.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_list.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_on_class_method.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_no_decoration.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_on_class_method.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_simple.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_with_failure.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/skip_on_class_method.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/skip_on_params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/tagged_class.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/xfail_on_params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/single.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/markers.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/params.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/resolve.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/tag.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/tag_filter.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/test_unittest_shim.py +0 -0
- {testsweet-0.2.0 → testsweet-0.2.2}/tests/walk.py +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ['v*']
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: astral-sh/setup-uv@v5
|
|
12
|
+
with:
|
|
13
|
+
enable-cache: true
|
|
14
|
+
- run: uv build
|
|
15
|
+
- uses: actions/upload-artifact@v4
|
|
16
|
+
with: { name: dist, path: dist/ }
|
|
17
|
+
|
|
18
|
+
testpypi:
|
|
19
|
+
needs: build
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
environment: testpypi
|
|
22
|
+
permissions:
|
|
23
|
+
id-token: write
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/download-artifact@v4
|
|
26
|
+
with: { name: dist, path: dist/ }
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
with:
|
|
29
|
+
repository-url: https://test.pypi.org/legacy/
|
|
30
|
+
|
|
31
|
+
pypi:
|
|
32
|
+
needs: testpypi
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with: { name: dist, path: dist/ }
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
41
|
+
|
|
42
|
+
github-release:
|
|
43
|
+
needs: pypi
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
permissions:
|
|
46
|
+
contents: write
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- name: Extract changelog entry
|
|
50
|
+
id: changelog
|
|
51
|
+
env:
|
|
52
|
+
VERSION: ${{ github.ref_name }}
|
|
53
|
+
run: |
|
|
54
|
+
python3 -c '
|
|
55
|
+
import re, os
|
|
56
|
+
version = os.environ["VERSION"].lstrip("v")
|
|
57
|
+
text = open("CHANGELOG.md").read()
|
|
58
|
+
m = re.search(
|
|
59
|
+
rf"\[{re.escape(version)}\] \([^)]+\)\n-+\n(.*?)(?=\n\[{re.escape(version)}\]: |\Z)",
|
|
60
|
+
text, re.DOTALL,
|
|
61
|
+
)
|
|
62
|
+
print(m.group(1).strip() if m else "")
|
|
63
|
+
' > /tmp/notes.txt
|
|
64
|
+
{
|
|
65
|
+
echo 'notes<<NOTES_EOF'
|
|
66
|
+
cat /tmp/notes.txt
|
|
67
|
+
echo 'NOTES_EOF'
|
|
68
|
+
} >> "$GITHUB_OUTPUT"
|
|
69
|
+
- uses: softprops/action-gh-release@v2
|
|
70
|
+
with:
|
|
71
|
+
body: ${{ steps.changelog.outputs.notes }}
|
|
@@ -6,7 +6,7 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
|
-
|
|
9
|
+
testsweet:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
strategy:
|
|
12
12
|
fail-fast: false
|
|
@@ -14,14 +14,9 @@ jobs:
|
|
|
14
14
|
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Install uv
|
|
19
|
-
uses: astral-sh/setup-uv@v5
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
18
|
with:
|
|
21
19
|
python-version: ${{ matrix.python-version }}
|
|
22
|
-
|
|
23
|
-
- name: Sync dependencies
|
|
24
|
-
run: uv sync
|
|
25
|
-
|
|
20
|
+
enable-cache: true
|
|
26
21
|
- name: Run tests
|
|
27
|
-
run: uv run
|
|
22
|
+
run: uv run testsweet
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
[0.2.2] (2026-06-27)
|
|
5
|
+
--------------------
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Fixed loading a test file by path (e.g. `testsweet path/to/tests.py`)
|
|
10
|
+
when the module uses relative imports.
|
|
11
|
+
- Fixed resolving dotted targets (e.g. `testsweet tests.foo`) when run
|
|
12
|
+
via the installed `testsweet` console script.
|
|
13
|
+
|
|
14
|
+
[0.2.2]: https://github.com/kaapstorm/testsweet/releases/tag/v0.2.2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
[0.2.1] (2026-06-27)
|
|
18
|
+
--------------------
|
|
19
|
+
|
|
20
|
+
### Improvements
|
|
21
|
+
|
|
22
|
+
- Switched type checking from mypy to ty
|
|
23
|
+
- Extended settings for linting and tightened type hints
|
|
24
|
+
- Added "authors" and "keywords" to `pyproject.toml`
|
|
25
|
+
- Tweaked CHANGELOG.md formatting
|
|
26
|
+
- Automated GitHub releases
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
[0.2.1]: https://github.com/kaapstorm/testsweet/releases/tag/v0.2.1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
[0.2.0] (2026-05-04)
|
|
33
|
+
--------------------
|
|
34
|
+
|
|
35
|
+
### Improvements
|
|
36
|
+
|
|
37
|
+
- Added `@skip`, `@xfail`, and `@tag` decorators for marking tests.
|
|
38
|
+
- Added command-line tag filtering: `-t`/`--tag` to include and
|
|
39
|
+
`-T`/`--exclude-tag` to exclude (both repeatable). A class-level
|
|
40
|
+
`@tag` propagates to every method on the class.
|
|
41
|
+
- Discovery now rejects orphan modifier decorators — a callable
|
|
42
|
+
carrying `@skip`, `@xfail`, `@tag`, or `@params` without `@test`
|
|
43
|
+
raises `ConfigurationError` rather than being silently ignored.
|
|
44
|
+
|
|
45
|
+
### Deprecations
|
|
46
|
+
|
|
47
|
+
- `@test_params` and `@test_params_lazy` were renamed to `@params`
|
|
48
|
+
and `@params_lazy` and no longer imply `@test`. Stack `@test` on
|
|
49
|
+
top of `@params(...)` (or `@params_lazy(...)`) on the functions
|
|
50
|
+
or methods you want discovered.
|
|
51
|
+
|
|
52
|
+
[0.2.0]: https://github.com/kaapstorm/testsweet/releases/tag/v0.2.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
[0.1.5] (2026-05-03)
|
|
56
|
+
--------------------
|
|
57
|
+
|
|
58
|
+
### Improvements
|
|
59
|
+
|
|
60
|
+
- Added convenience context manager method `__test_context__` for set-up and
|
|
61
|
+
tear-down to be applied to all test methods.
|
|
62
|
+
- Added the ability to run plugins. (The first plugin, testsweet-django, will
|
|
63
|
+
be available soon.)
|
|
64
|
+
|
|
65
|
+
[0.1.5]: https://github.com/kaapstorm/testsweet/releases/tag/v0.1.5
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
[0.1.4] (2026-04-30)
|
|
69
|
+
--------------------
|
|
70
|
+
|
|
71
|
+
### Improvements
|
|
72
|
+
|
|
73
|
+
- Improved output.
|
|
74
|
+
- Added `--help` command line option.
|
|
75
|
+
- Can be invoked using `testsweet`
|
|
76
|
+
|
|
77
|
+
### Documentation
|
|
78
|
+
|
|
79
|
+
- Clarified that class context managers are treated like
|
|
80
|
+
`setUpClass()`/`tearDownClass()`.
|
|
81
|
+
|
|
82
|
+
[0.1.4]: https://github.com/kaapstorm/testsweet/releases/tag/v0.1.4
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
[0.1.3] (2026-04-29)
|
|
86
|
+
--------------------
|
|
87
|
+
|
|
88
|
+
### Improvements
|
|
89
|
+
|
|
90
|
+
- Tightened typing and added py.typed marker
|
|
91
|
+
|
|
92
|
+
[0.1.3]: https://github.com/kaapstorm/testsweet/releases/tag/v0.1.3
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
[0.1.2] (2026-04-29)
|
|
96
|
+
--------------------
|
|
97
|
+
|
|
98
|
+
### Documentation
|
|
99
|
+
|
|
100
|
+
- Used absolute URLs in README.md to link to documentation on GitHub.
|
|
101
|
+
- Added CHANGELOG.md.
|
|
102
|
+
|
|
103
|
+
[0.1.2]: https://github.com/kaapstorm/testsweet/releases/tag/v0.1.2
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: testsweet
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Python testing for humans
|
|
5
5
|
Project-URL: Homepage, https://github.com/kaapstorm/testsweet
|
|
6
6
|
Project-URL: Source, https://github.com/kaapstorm/testsweet
|
|
7
7
|
Project-URL: Documentation, https://github.com/kaapstorm/testsweet/blob/main/README.md
|
|
8
8
|
Project-URL: Changelog, https://github.com/kaapstorm/testsweet/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Norman Hooper <kaapstorm@gmail.com>
|
|
9
10
|
License-Expression: Apache-2.0
|
|
10
11
|
License-File: LICENSE
|
|
12
|
+
Keywords: pytest,pythonic,tdd,test,test-discovery,test-framework,test-runner,testing,unit-testing,unittest
|
|
11
13
|
Classifier: Development Status :: 3 - Alpha
|
|
12
14
|
Classifier: Intended Audience :: Developers
|
|
13
15
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -286,6 +286,11 @@ Installing a plugin from PyPI is a trust decision equivalent to
|
|
|
286
286
|
installing any other dependency — testsweet does not sandbox or
|
|
287
287
|
allowlist plugins.
|
|
288
288
|
|
|
289
|
+
### Known plugins
|
|
290
|
+
|
|
291
|
+
* [testsweet-django](https://github.com/kaapstorm/testsweet-django/) is
|
|
292
|
+
the first plugin and example implementation.
|
|
293
|
+
|
|
289
294
|
|
|
290
295
|
Errors
|
|
291
296
|
------
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "testsweet"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Python testing for humans"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.11"
|
|
7
5
|
license = "Apache-2.0"
|
|
8
6
|
license-files = ["LICENSE"]
|
|
9
|
-
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Norman Hooper", email = "kaapstorm@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
keywords = [
|
|
11
|
+
"testing",
|
|
12
|
+
"test",
|
|
13
|
+
"test-framework",
|
|
14
|
+
"test-runner",
|
|
15
|
+
"unit-testing",
|
|
16
|
+
"test-discovery",
|
|
17
|
+
"tdd",
|
|
18
|
+
"pytest",
|
|
19
|
+
"unittest",
|
|
20
|
+
"pythonic",
|
|
21
|
+
]
|
|
10
22
|
classifiers = [
|
|
11
23
|
"Development Status :: 3 - Alpha",
|
|
12
24
|
"Intended Audience :: Developers",
|
|
@@ -17,6 +29,9 @@ classifiers = [
|
|
|
17
29
|
"Programming Language :: Python :: 3.14",
|
|
18
30
|
"Topic :: Software Development :: Testing",
|
|
19
31
|
]
|
|
32
|
+
readme = "README.md"
|
|
33
|
+
requires-python = ">=3.11"
|
|
34
|
+
dependencies = []
|
|
20
35
|
|
|
21
36
|
[project.scripts]
|
|
22
37
|
testsweet = "testsweet.__main__:cli"
|
|
@@ -36,9 +51,9 @@ packages = ["src/testsweet"]
|
|
|
36
51
|
|
|
37
52
|
[dependency-groups]
|
|
38
53
|
dev = [
|
|
39
|
-
"mypy",
|
|
40
54
|
"pre-commit",
|
|
41
55
|
"ruff",
|
|
56
|
+
"ty",
|
|
42
57
|
]
|
|
43
58
|
|
|
44
59
|
[[tool.uv.index]]
|
|
@@ -56,18 +71,19 @@ explicit = true
|
|
|
56
71
|
[tool.ruff]
|
|
57
72
|
line-length = 79
|
|
58
73
|
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "ANN"]
|
|
76
|
+
ignore = [
|
|
77
|
+
"ANN002", # missing annotation for *args
|
|
78
|
+
"ANN003", # missing annotation for **kwargs
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.flake8-annotations]
|
|
82
|
+
ignore-fully-untyped = true
|
|
83
|
+
|
|
59
84
|
[tool.ruff.format]
|
|
60
85
|
quote-style = "single"
|
|
61
86
|
|
|
62
|
-
[tool.mypy]
|
|
63
|
-
python_version = "3.11"
|
|
64
|
-
files = ["src", "tests"]
|
|
65
|
-
mypy_path = "src"
|
|
66
|
-
disallow_incomplete_defs = true
|
|
67
|
-
check_untyped_defs = true
|
|
68
|
-
warn_unused_ignores = true
|
|
69
|
-
warn_redundant_casts = true
|
|
70
|
-
|
|
71
87
|
[tool.testsweet.discovery]
|
|
72
88
|
include_paths = ["tests"]
|
|
73
89
|
exclude_paths = ["tests/fixtures"]
|
|
@@ -84,6 +84,13 @@ def main(argv: list[str]) -> int:
|
|
|
84
84
|
if (include or exclude) else None
|
|
85
85
|
)
|
|
86
86
|
with scoped_sys_path():
|
|
87
|
+
# `python -m testsweet` prepends the cwd to sys.path, but the
|
|
88
|
+
# installed console script does not (sys.path[0] is its own bin
|
|
89
|
+
# directory). Put the cwd on the path so dotted targets resolve
|
|
90
|
+
# against the project the same way under both invocations.
|
|
91
|
+
cwd = str(pathlib.Path.cwd())
|
|
92
|
+
if cwd not in sys.path:
|
|
93
|
+
sys.path.insert(0, cwd)
|
|
87
94
|
config = load_config(pathlib.Path.cwd())
|
|
88
95
|
plugins = load_plugins()
|
|
89
96
|
wrap_unit = unit_wrapper(plugins)
|
|
@@ -10,7 +10,7 @@ Failures (missing source, syntax errors, eval errors) silently yield
|
|
|
10
10
|
``None`` — the explainer is a nicety, not a correctness requirement.
|
|
11
11
|
"""
|
|
12
12
|
import ast
|
|
13
|
-
from types import TracebackType
|
|
13
|
+
from types import FrameType, TracebackType
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def assertion_source(exc: AssertionError) -> str | None:
|
|
@@ -55,7 +55,7 @@ def explain_assertion(exc: AssertionError) -> str | None:
|
|
|
55
55
|
|
|
56
56
|
def _locate_assert(
|
|
57
57
|
exc: AssertionError,
|
|
58
|
-
) -> tuple[
|
|
58
|
+
) -> tuple[FrameType, str, ast.Assert] | None:
|
|
59
59
|
tb = _innermost_tb(exc.__traceback__)
|
|
60
60
|
if tb is None:
|
|
61
61
|
return None
|
|
@@ -13,7 +13,8 @@ Condition = Union[bool, Callable[[], bool]]
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def make_condition_decorator(decorator_name, marker_cls, attr):
|
|
16
|
-
"""Build a decorator that attaches ``marker_cls(reason, condition)``
|
|
16
|
+
"""Build a decorator that attaches ``marker_cls(reason, condition)``
|
|
17
|
+
under ``attr``.
|
|
17
18
|
|
|
18
19
|
The returned decorator supports the bare form (``@dec``) and the
|
|
19
20
|
called form (``@dec(reason='…')``, ``@dec(condition=expr)``,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import pathlib
|
|
3
3
|
import tomllib
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
|
|
5
|
+
from typing import cast
|
|
6
6
|
|
|
7
7
|
_VALID_KEYS = frozenset({'include_paths', 'exclude_paths', 'test_files'})
|
|
8
8
|
|
|
@@ -71,6 +71,6 @@ def _to_string_tuple(value: object, key: str) -> tuple[str, ...]:
|
|
|
71
71
|
err = f'tool.testsweet.discovery.{key} must be a list of strings'
|
|
72
72
|
if not isinstance(value, list):
|
|
73
73
|
raise ConfigurationError(err)
|
|
74
|
-
if not
|
|
74
|
+
if any(not isinstance(item, str) for item in value):
|
|
75
75
|
raise ConfigurationError(err)
|
|
76
|
-
return tuple(value)
|
|
76
|
+
return cast(tuple[str, ...], tuple(value))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Discover ``@test``-marked callables and classes in a module."""
|
|
2
2
|
from types import ModuleType
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Protocol
|
|
4
4
|
|
|
5
5
|
from testsweet._config import ConfigurationError
|
|
6
6
|
from testsweet._markers import (
|
|
@@ -12,6 +12,13 @@ from testsweet._markers import (
|
|
|
12
12
|
from testsweet._params import PARAMS_MARKER
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class TestUnit(Protocol):
|
|
16
|
+
__name__: str
|
|
17
|
+
__qualname__: str
|
|
18
|
+
|
|
19
|
+
def __call__(self, *args, **kwargs): ...
|
|
20
|
+
|
|
21
|
+
|
|
15
22
|
_MODIFIER_DECORATORS = {
|
|
16
23
|
PARAMS_MARKER: '@params',
|
|
17
24
|
SKIP_MARKER: '@skip',
|
|
@@ -20,7 +27,7 @@ _MODIFIER_DECORATORS = {
|
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
|
|
23
|
-
def discover(module: ModuleType) -> list[
|
|
30
|
+
def discover(module: ModuleType) -> list[TestUnit]:
|
|
24
31
|
"""Return module-level callables marked as test units.
|
|
25
32
|
|
|
26
33
|
Order follows ``vars(module)`` (definition order on CPython 3.7+).
|
|
@@ -31,7 +38,7 @@ def discover(module: ModuleType) -> list[Callable]:
|
|
|
31
38
|
``@xfail``, ``@tag``) without ``@test``. Imported callables are
|
|
32
39
|
not checked — they were decorated wherever they were defined.
|
|
33
40
|
"""
|
|
34
|
-
tests
|
|
41
|
+
tests = []
|
|
35
42
|
for name, value in vars(module).items():
|
|
36
43
|
if not callable(value):
|
|
37
44
|
continue
|
|
@@ -39,10 +39,6 @@ def _exec_module_from_path(path: pathlib.Path) -> ModuleType:
|
|
|
39
39
|
return module
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def _load_path(target: str) -> ModuleType:
|
|
43
|
-
return _exec_module_from_path(pathlib.Path(target).resolve())
|
|
44
|
-
|
|
45
|
-
|
|
46
42
|
def _load_path_for_walk(path: pathlib.Path) -> ModuleType:
|
|
47
43
|
info = _dotted_name_for_path(path)
|
|
48
44
|
if info is None:
|
|
@@ -95,12 +95,18 @@ def _print_traceback_block(
|
|
|
95
95
|
|
|
96
96
|
def _outcome_key(outcome: Outcome) -> str:
|
|
97
97
|
match outcome:
|
|
98
|
-
case Passed():
|
|
99
|
-
|
|
100
|
-
case
|
|
101
|
-
|
|
102
|
-
case
|
|
103
|
-
|
|
98
|
+
case Passed():
|
|
99
|
+
return 'passed'
|
|
100
|
+
case Failed():
|
|
101
|
+
return 'failed'
|
|
102
|
+
case Errored():
|
|
103
|
+
return 'errored'
|
|
104
|
+
case Skipped():
|
|
105
|
+
return 'skipped'
|
|
106
|
+
case XFailed():
|
|
107
|
+
return 'xfailed'
|
|
108
|
+
case XPassed():
|
|
109
|
+
return 'xpassed'
|
|
104
110
|
|
|
105
111
|
|
|
106
112
|
_SUMMARY_ORDER = (
|
|
@@ -12,7 +12,7 @@ from types import ModuleType
|
|
|
12
12
|
from typing import Any, Callable, Iterator
|
|
13
13
|
|
|
14
14
|
from testsweet._class_helpers import _public_methods
|
|
15
|
-
from testsweet._discover import discover
|
|
15
|
+
from testsweet._discover import TestUnit, discover
|
|
16
16
|
from testsweet._markers import TAGS_MARKER
|
|
17
17
|
from testsweet._params import PARAMS_MARKER
|
|
18
18
|
|
|
@@ -46,14 +46,12 @@ def resolve_units(
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _expand_unit(
|
|
49
|
-
unit:
|
|
49
|
+
unit: TestUnit,
|
|
50
50
|
method_filter: set[str] | None,
|
|
51
51
|
keep: TagFilter | None,
|
|
52
52
|
) -> Iterator[tuple[str, Callable[[], Any]]]:
|
|
53
53
|
if isinstance(unit, type):
|
|
54
|
-
class_tags: frozenset[str] = getattr(
|
|
55
|
-
unit, TAGS_MARKER, frozenset(),
|
|
56
|
-
)
|
|
54
|
+
class_tags: frozenset[str] = getattr(unit, TAGS_MARKER, frozenset())
|
|
57
55
|
eligible = [
|
|
58
56
|
method_name
|
|
59
57
|
for method_name in _public_methods(unit)
|
|
@@ -78,9 +76,10 @@ def _expand_unit(
|
|
|
78
76
|
test_context = getattr(instance, '__test_context__', None)
|
|
79
77
|
for method_name in eligible:
|
|
80
78
|
bound = getattr(instance, method_name)
|
|
79
|
+
qualname: str = bound.__qualname__
|
|
81
80
|
if test_context is not None:
|
|
82
81
|
bound = _wrap_in_cm(bound, test_context)
|
|
83
|
-
yield from _expand_callable(bound,
|
|
82
|
+
yield from _expand_callable(bound, qualname)
|
|
84
83
|
else:
|
|
85
84
|
if keep is not None:
|
|
86
85
|
tags: frozenset[str] = getattr(
|
|
@@ -101,7 +100,7 @@ def _wrap_in_cm(
|
|
|
101
100
|
cm_factory: Callable[[], Any],
|
|
102
101
|
) -> Callable[..., Any]:
|
|
103
102
|
@functools.wraps(call)
|
|
104
|
-
def wrapped(*args
|
|
103
|
+
def wrapped(*args, **kwargs):
|
|
105
104
|
with cm_factory():
|
|
106
105
|
return call(*args, **kwargs)
|
|
107
106
|
return wrapped
|
|
@@ -126,7 +125,7 @@ def _expand_callable(
|
|
|
126
125
|
|
|
127
126
|
|
|
128
127
|
def _build_plan(
|
|
129
|
-
units: list[
|
|
128
|
+
units: list[TestUnit],
|
|
130
129
|
names: list[str],
|
|
131
130
|
) -> dict[str, set[str] | None]:
|
|
132
131
|
"""Map unit qualnames to method-name filters.
|
|
@@ -9,7 +9,7 @@ from types import ModuleType
|
|
|
9
9
|
|
|
10
10
|
from testsweet._classify import _resolve_dotted
|
|
11
11
|
from testsweet._config import DiscoveryConfig
|
|
12
|
-
from testsweet._loaders import
|
|
12
|
+
from testsweet._loaders import _load_path_for_walk
|
|
13
13
|
from testsweet._walk import (
|
|
14
14
|
_build_exclude_set,
|
|
15
15
|
_resolve_include_paths,
|
|
@@ -73,7 +73,7 @@ def parse_target(
|
|
|
73
73
|
excluded=excluded,
|
|
74
74
|
)
|
|
75
75
|
]
|
|
76
|
-
return [(
|
|
76
|
+
return [(_load_path_for_walk(path), None)]
|
|
77
77
|
return [_resolve_dotted(target)]
|
|
78
78
|
|
|
79
79
|
|
|
@@ -394,6 +394,19 @@ class Cli:
|
|
|
394
394
|
assert result.returncode == 0
|
|
395
395
|
assert result.stdout.startswith('Usage: testsweet')
|
|
396
396
|
|
|
397
|
+
def installed_entry_point_resolves_dotted_target(self):
|
|
398
|
+
script = pathlib.Path(sys.executable).with_name('testsweet')
|
|
399
|
+
if not script.exists():
|
|
400
|
+
return # not an installed environment; skip silently
|
|
401
|
+
result = subprocess.run(
|
|
402
|
+
[str(script), 'tests.fixtures.runner.all_pass'],
|
|
403
|
+
capture_output=True,
|
|
404
|
+
text=True,
|
|
405
|
+
cwd=_REPO_ROOT,
|
|
406
|
+
)
|
|
407
|
+
assert result.returncode == 0, result.stderr
|
|
408
|
+
assert 'passes_one ... ok' in result.stdout
|
|
409
|
+
|
|
397
410
|
@params([(('-h',),), (('--help',),)])
|
|
398
411
|
def help_flag_prints_usage_and_exits_zero(self, args):
|
|
399
412
|
result = _run_cli(*args)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import this_dependency_does_not_exist # ty: ignore[unresolved-import] # noqa: F401
|
|
@@ -30,7 +30,7 @@ class FailedOutcome:
|
|
|
30
30
|
def is_frozen(self):
|
|
31
31
|
out = Failed(AssertionError())
|
|
32
32
|
with catch_exceptions() as caught:
|
|
33
|
-
out.exc = AssertionError() #
|
|
33
|
+
out.exc = AssertionError() # ty: ignore[invalid-assignment]
|
|
34
34
|
assert len(caught) == 1
|
|
35
35
|
|
|
36
36
|
|
|
@@ -58,7 +58,7 @@ class SkippedOutcome:
|
|
|
58
58
|
def is_frozen(self):
|
|
59
59
|
s = Skipped('r')
|
|
60
60
|
with catch_exceptions() as caught:
|
|
61
|
-
s.reason = 'mutate' #
|
|
61
|
+
s.reason = 'mutate' # ty: ignore[invalid-assignment]
|
|
62
62
|
assert len(caught) == 1
|
|
63
63
|
|
|
64
64
|
|
|
@@ -143,7 +143,7 @@ class LoadPlugins:
|
|
|
143
143
|
value='fake_module',
|
|
144
144
|
load=lambda: plugin,
|
|
145
145
|
)
|
|
146
|
-
_plugins.entry_points = lambda group: [fake_ep]
|
|
146
|
+
_plugins.entry_points = lambda group: [fake_ep] # ty: ignore[invalid-assignment]
|
|
147
147
|
loaded = load_plugins()
|
|
148
148
|
assert loaded == [plugin]
|
|
149
149
|
|
|
@@ -160,7 +160,7 @@ class LoadPlugins:
|
|
|
160
160
|
value='broken_module',
|
|
161
161
|
load=lambda: bad,
|
|
162
162
|
)
|
|
163
|
-
_plugins.entry_points = lambda group: [fake_ep]
|
|
163
|
+
_plugins.entry_points = lambda group: [fake_ep] # ty: ignore[invalid-assignment]
|
|
164
164
|
with catch_exceptions() as excs:
|
|
165
165
|
load_plugins()
|
|
166
166
|
assert len(excs) == 1
|
|
@@ -169,7 +169,7 @@ class LoadPlugins:
|
|
|
169
169
|
|
|
170
170
|
def empty_entry_points_yields_empty_list(self):
|
|
171
171
|
from testsweet import _plugins
|
|
172
|
-
_plugins.entry_points = lambda group: []
|
|
172
|
+
_plugins.entry_points = lambda group: [] # ty: ignore[invalid-assignment]
|
|
173
173
|
assert load_plugins() == []
|
|
174
174
|
|
|
175
175
|
def import_failure_raises_configuration_error(self):
|
|
@@ -183,7 +183,7 @@ class LoadPlugins:
|
|
|
183
183
|
value='broken_module',
|
|
184
184
|
load=raises,
|
|
185
185
|
)
|
|
186
|
-
_plugins.entry_points = lambda group: [fake_ep]
|
|
186
|
+
_plugins.entry_points = lambda group: [fake_ep] # ty: ignore[invalid-assignment]
|
|
187
187
|
with catch_exceptions() as excs:
|
|
188
188
|
load_plugins()
|
|
189
189
|
assert len(excs) == 1
|