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.
Files changed (118) hide show
  1. testsweet-0.2.2/.github/workflows/lint.yml +16 -0
  2. testsweet-0.2.2/.github/workflows/publish.yml +71 -0
  3. {testsweet-0.2.0 → testsweet-0.2.2}/.github/workflows/tests.yml +4 -9
  4. testsweet-0.2.2/.github/workflows/types.yml +16 -0
  5. testsweet-0.2.2/CHANGELOG.md +103 -0
  6. {testsweet-0.2.0 → testsweet-0.2.2}/CLAUDE.md +3 -0
  7. {testsweet-0.2.0 → testsweet-0.2.2}/PKG-INFO +3 -1
  8. {testsweet-0.2.0 → testsweet-0.2.2}/docs/reference.md +5 -0
  9. {testsweet-0.2.0 → testsweet-0.2.2}/pyproject.toml +30 -14
  10. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/__main__.py +7 -0
  11. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_assertion.py +2 -2
  12. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_condition_decorator.py +2 -1
  13. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_config.py +3 -3
  14. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_discover.py +10 -3
  15. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_loaders.py +0 -4
  16. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_report.py +12 -6
  17. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_resolve.py +7 -8
  18. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_tag.py +1 -1
  19. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_targets.py +2 -2
  20. {testsweet-0.2.0 → testsweet-0.2.2}/tests/cli.py +13 -0
  21. testsweet-0.2.2/tests/fixtures/runner/has_broken_import.py +1 -0
  22. testsweet-0.2.2/tests/fixtures/runner/uses_relative_import.py +8 -0
  23. {testsweet-0.2.0 → testsweet-0.2.2}/tests/outcomes.py +2 -2
  24. {testsweet-0.2.0 → testsweet-0.2.2}/tests/plugins.py +4 -4
  25. {testsweet-0.2.0 → testsweet-0.2.2}/tests/report.py +5 -2
  26. {testsweet-0.2.0 → testsweet-0.2.2}/tests/runner.py +0 -1
  27. {testsweet-0.2.0 → testsweet-0.2.2}/tests/skip.py +1 -1
  28. {testsweet-0.2.0 → testsweet-0.2.2}/tests/targets.py +15 -0
  29. {testsweet-0.2.0 → testsweet-0.2.2}/tests/xfail.py +1 -1
  30. testsweet-0.2.2/uv.lock +231 -0
  31. testsweet-0.2.0/.github/workflows/publish.yml +0 -38
  32. testsweet-0.2.0/CHANGELOG.md +0 -54
  33. testsweet-0.2.0/claude/specs/2026-05-03_outcome-decorators.md +0 -383
  34. testsweet-0.2.0/docs/roadmap/examples/django.py +0 -48
  35. testsweet-0.2.0/tests/fixtures/runner/has_broken_import.py +0 -1
  36. testsweet-0.2.0/uv.lock +0 -357
  37. {testsweet-0.2.0 → testsweet-0.2.2}/.gitignore +0 -0
  38. {testsweet-0.2.0 → testsweet-0.2.2}/.pre-commit-config.yaml +0 -0
  39. {testsweet-0.2.0 → testsweet-0.2.2}/.python-version +0 -0
  40. {testsweet-0.2.0 → testsweet-0.2.2}/LICENSE +0 -0
  41. {testsweet-0.2.0 → testsweet-0.2.2}/README.md +0 -0
  42. {testsweet-0.2.0 → testsweet-0.2.2}/docs/contributing.md +0 -0
  43. {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/catches.py +0 -0
  44. {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/classes.py +0 -0
  45. {testsweet-0.2.0 → testsweet-0.2.2}/docs/examples/functions.py +0 -0
  46. {testsweet-0.2.0 → testsweet-0.2.2}/docs/getting-started.md +0 -0
  47. {testsweet-0.2.0 → testsweet-0.2.2}/docs/img/testsweet_200x200.png +0 -0
  48. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/__init__.py +0 -0
  49. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_catches.py +0 -0
  50. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_class_helpers.py +0 -0
  51. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_classify.py +0 -0
  52. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_markers.py +0 -0
  53. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_outcomes.py +0 -0
  54. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_params.py +0 -0
  55. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_plugins.py +0 -0
  56. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_runner.py +0 -0
  57. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_skip.py +0 -0
  58. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_tag_filter.py +0 -0
  59. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_walk.py +0 -0
  60. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/_xfail.py +0 -0
  61. {testsweet-0.2.0 → testsweet-0.2.2}/src/testsweet/py.typed +0 -0
  62. {testsweet-0.2.0 → testsweet-0.2.2}/tests/__init__.py +0 -0
  63. {testsweet-0.2.0 → testsweet-0.2.2}/tests/catches.py +0 -0
  64. {testsweet-0.2.0 → testsweet-0.2.2}/tests/config.py +0 -0
  65. {testsweet-0.2.0 → testsweet-0.2.2}/tests/discover.py +0 -0
  66. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/__init__.py +0 -0
  67. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/empty.py +0 -0
  68. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/imported_only.py +0 -0
  69. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/mixed.py +0 -0
  70. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/mixed_local_imported.py +0 -0
  71. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/multiple.py +0 -0
  72. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/non_callable_marker.py +0 -0
  73. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/__init__.py +0 -0
  74. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/all_pass.py +0 -0
  75. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/assertion_diagnostics.py +0 -0
  76. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_calls_recorded.py +0 -0
  77. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_decorated_simple.py +0 -0
  78. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_decorated_with_cm.py +0 -0
  79. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_enter_only.py +0 -0
  80. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_enter_raises.py +0 -0
  81. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_exit_raises.py +0 -0
  82. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_method_fails.py +0 -0
  83. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_mixed_with_function.py +0 -0
  84. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_simple.py +0 -0
  85. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_test_context_raises.py +0 -0
  86. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_test_context_with_params.py +0 -0
  87. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_inheritance.py +0 -0
  88. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_test_context.py +0 -0
  89. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/class_with_underscore_methods.py +0 -0
  90. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/empty.py +0 -0
  91. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/has_failure.py +0 -0
  92. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/keyboard_interrupt.py +0 -0
  93. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/non_assertion_error.py +0 -0
  94. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_params.py +0 -0
  95. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_skip.py +0 -0
  96. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_tag.py +0 -0
  97. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/orphan_xfail.py +0 -0
  98. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_empty.py +0 -0
  99. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_generator.py +0 -0
  100. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_generator.py +0 -0
  101. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_list.py +0 -0
  102. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_lazy_on_class_method.py +0 -0
  103. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_no_decoration.py +0 -0
  104. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_on_class_method.py +0 -0
  105. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_simple.py +0 -0
  106. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/params_with_failure.py +0 -0
  107. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/skip_on_class_method.py +0 -0
  108. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/skip_on_params.py +0 -0
  109. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/tagged_class.py +0 -0
  110. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/runner/xfail_on_params.py +0 -0
  111. {testsweet-0.2.0 → testsweet-0.2.2}/tests/fixtures/single.py +0 -0
  112. {testsweet-0.2.0 → testsweet-0.2.2}/tests/markers.py +0 -0
  113. {testsweet-0.2.0 → testsweet-0.2.2}/tests/params.py +0 -0
  114. {testsweet-0.2.0 → testsweet-0.2.2}/tests/resolve.py +0 -0
  115. {testsweet-0.2.0 → testsweet-0.2.2}/tests/tag.py +0 -0
  116. {testsweet-0.2.0 → testsweet-0.2.2}/tests/tag_filter.py +0 -0
  117. {testsweet-0.2.0 → testsweet-0.2.2}/tests/test_unittest_shim.py +0 -0
  118. {testsweet-0.2.0 → testsweet-0.2.2}/tests/walk.py +0 -0
@@ -0,0 +1,16 @@
1
+ name: lint
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ruff:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - run: uv run ruff check
@@ -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
- test:
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 python -m testsweet
22
+ run: uv run testsweet
@@ -0,0 +1,16 @@
1
+ name: types
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ty:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - run: uv run ty check src/ tests/
@@ -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
@@ -5,6 +5,9 @@
5
5
  Start commands with `uv run ...` to run in the uv virtualenv.
6
6
 
7
7
  * Python: `uv run python ...`
8
+ * Run tests: `uv run testsweet`
9
+ * Run linter: `uv run ruff check`
10
+ * Run type checker: `uv run ty check src/ tests/`
8
11
 
9
12
  ## File locations
10
13
 
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testsweet
3
- Version: 0.2.0
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.0"
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
- dependencies = []
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[object, str, ast.Assert] | None:
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)`` under ``attr``.
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 all(isinstance(item, str) for item in value):
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 Callable
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[Callable]:
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: list[Callable] = []
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(): return 'passed'
99
- case Failed(): return 'failed'
100
- case Errored(): return 'errored'
101
- case Skipped(): return 'skipped'
102
- case XFailed(): return 'xfailed'
103
- case XPassed(): return 'xpassed'
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: Any,
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, bound.__qualname__)
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: Any, **kwargs: Any) -> Any:
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[Any],
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.
@@ -17,7 +17,7 @@ def tag(*names):
17
17
  new = frozenset(names)
18
18
 
19
19
  def decorator(func):
20
- existing = getattr(func, TAGS_MARKER, frozenset())
20
+ existing: frozenset[str] = getattr(func, TAGS_MARKER, frozenset())
21
21
  setattr(func, TAGS_MARKER, existing | new)
22
22
  return func
23
23
 
@@ -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 _load_path, _load_path_for_walk
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 [(_load_path(target), None)]
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
@@ -0,0 +1,8 @@
1
+ from testsweet import test
2
+
3
+ from .all_pass import passes_one # noqa: F401
4
+
5
+
6
+ @test
7
+ def uses_rel():
8
+ pass
@@ -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() # type: ignore[misc]
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' # type: ignore[misc]
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