Skip to content

Commit 723114a

Browse files
authored
Merge pull request #490 from stephenfin/typing
Add typing
2 parents 3d6efa3 + 9ffa6b3 commit 723114a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+802
-544
lines changed

.github/workflows/test.yml

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,45 @@
1+
---
12
name: Test
23

34
on: [push, pull_request]
45

56
jobs:
6-
build:
7+
lint:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v6
11+
with:
12+
fetch-depth: 0
13+
14+
- name: Set up Python 3.13
15+
uses: actions/setup-python@v6
16+
with:
17+
python-version: "3.13"
18+
19+
- name: Cache
20+
uses: actions/cache@v5
21+
with:
22+
path: ~/.cache/pip
23+
key: ${{ runner.os }}-lint-${{ hashFiles('**/setup.py') }}
24+
restore-keys: |
25+
${{ runner.os }}-lint-
26+
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip wheel
30+
python -m pip install sphinx
31+
python -m pip install ".[test,twisted,dev]"
32+
33+
- name: Ruff
34+
run: |
35+
python -m ruff check .
36+
python -m ruff format --check .
37+
38+
- name: Type Check
39+
run: |
40+
python -m mypy testtools
41+
42+
test:
743
runs-on: ubuntu-latest
844
strategy:
945
fail-fast: false
@@ -24,9 +60,7 @@ jobs:
2460
uses: actions/cache@v5
2561
with:
2662
path: ~/.cache/pip
27-
key:
28-
${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py')
29-
}}
63+
key: ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }}
3064
restore-keys: |
3165
${{ matrix.os }}-${{ matrix.python-version }}-
3266
@@ -36,24 +70,42 @@ jobs:
3670
python -m pip install sphinx
3771
python -m pip install ".[test,twisted,dev]"
3872
39-
- name: Ruff
73+
- name: Run tests
4074
run: |
41-
python -m ruff check .
75+
python -W once -m testtools.run testtools.tests.test_suite
4276
43-
- name: Format
44-
run: |
45-
python -m ruff format --check .
77+
docs:
78+
runs-on: ubuntu-latest
79+
steps:
80+
- uses: actions/checkout@v6
81+
with:
82+
fetch-depth: 0
83+
84+
- name: Set up Python 3.13
85+
uses: actions/setup-python@v6
86+
with:
87+
python-version: "3.13"
4688

47-
- name: Tests
89+
- name: Cache
90+
uses: actions/cache@v5
91+
with:
92+
path: ~/.cache/pip
93+
key: ${{ runner.os }}-docs-${{ hashFiles('**/setup.py') }}
94+
restore-keys: |
95+
${{ runner.os }}-docs-
96+
97+
- name: Install dependencies
4898
run: |
49-
python -W once -m testtools.run testtools.tests.test_suite
99+
python -m pip install --upgrade pip wheel
100+
python -m pip install sphinx
101+
python -m pip install ".[test,twisted,dev]"
50102
51103
- name: Docs
52104
run: |
53105
make clean-sphinx docs
54106
55107
success:
56-
needs: build
108+
needs: ["lint", "test", "docs"]
57109
runs-on: ubuntu-latest
58110
name: test successful
59111
steps:

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ include MANIFEST.in
44
include NEWS
55
include README.rst
66
include .gitignore
7+
include testtools/py.typed
78
recursive-include doc *.rst *.py
89
prune doc/_build

pyproject.toml

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ Homepage = "https://github.com/testing-cabal/testtools"
3535
[project.optional-dependencies]
3636
test = ["testscenarios", "testresources"]
3737
twisted = ["Twisted", "fixtures"]
38-
dev = ["ruff==0.14.9"]
38+
dev = [
39+
"ruff==0.14.9",
40+
"mypy>=1.0.0",
41+
]
3942

4043
[tool.hatch.version]
4144
source = "vcs"
@@ -54,18 +57,47 @@ __version__ = {version_tuple!r}
5457
only-include = ["testtools"]
5558

5659
[tool.mypy]
57-
warn_redundant_casts = true
58-
warn_unused_configs = true
59-
check_untyped_defs = true
60+
python_version = "3.10"
61+
show_column_numbers = true
62+
show_error_context = true
63+
strict = true
64+
# this is here temporarily due to the rapid changes in typing across
65+
# dependencies
66+
warn_unused_ignores = false
67+
# FIXME(stephenfin): We should remove this
68+
implicit_reexport = true
69+
exclude = 'doc'
6070

6171
[[tool.mypy.overrides]]
6272
module = [
63-
"fixtures.*",
6473
"testresources.*",
6574
"testscenarios.*",
6675
]
6776
ignore_missing_imports = true
6877

78+
[[tool.mypy.overrides]]
79+
module = [
80+
# FIXME(stephenfin): We would like to remove all modules from this list
81+
# except testtools.tests (we're not sadists)
82+
"testtools.assertions",
83+
"testtools.compat",
84+
"testtools.content",
85+
"testtools.content_type",
86+
"testtools.matchers.*",
87+
"testtools.monkey",
88+
"testtools.run",
89+
"testtools.runtest",
90+
"testtools.testcase",
91+
"testtools.testresult.*",
92+
"testtools.testsuite",
93+
"testtools.twistedsupport.*",
94+
"testtools.tests.*",
95+
]
96+
disallow_untyped_calls = false
97+
disallow_untyped_defs = false
98+
disallow_subclassing_any = false
99+
disallow_any_generics = false
100+
69101
[tool.ruff.lint]
70102
select = [
71103
"E",
@@ -76,8 +108,7 @@ select = [
76108
"RSE",
77109
"RUF",
78110
]
79-
ignore = [
80-
]
111+
ignore = []
81112

82113
[tool.ruff.lint.pydocstyle]
83114
convention = "google"

testtools/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@
4141
"skip",
4242
"skipIf",
4343
"skipUnless",
44-
"try_import",
4544
"unique_text_generator",
4645
"version",
4746
]
4847

49-
from testtools.helpers import try_import
5048
from testtools.matchers._impl import Matcher # noqa: F401
5149
from testtools.runtest import (
5250
MultipleExceptions,
@@ -96,7 +94,7 @@
9694
)
9795

9896

99-
def __get_git_version():
97+
def __get_git_version() -> str | None:
10098
import os
10199
import subprocess
102100

@@ -137,9 +135,10 @@ def __get_git_version():
137135
from ._version import __version__, version
138136
except ModuleNotFoundError:
139137
# package is not installed
140-
if version := __get_git_version():
138+
if v := __get_git_version():
139+
version = v
141140
# we're in a git repo
142-
__version__ = tuple([int(v) if v.isdigit() else v for v in version.split(".")])
141+
__version__ = tuple([int(x) if x.isdigit() else x for x in version.split(".")])
143142
else:
144143
# we're working with a tarball or similar
145144
version = "0.0.0"

testtools/_version.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Type stub for auto-generated _version module
2+
from typing import Union
3+
4+
__version__: tuple[Union[int, str], ...] # noqa: UP007
5+
version: str

testtools/compat.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616
import locale
1717
import os
1818
import sys
19-
import typing
19+
import types
2020
import unicodedata
21+
from io import BytesIO, StringIO # for backwards-compat
22+
from typing import Any, NoReturn
2123

22-
# Ensure retro-compatibility with older testtools releases
23-
from io import BytesIO, StringIO
2424

25-
26-
def reraise(exc_class, exc_obj, exc_tb, _marker=object()) -> typing.NoReturn:
25+
def reraise(
26+
exc_class: type[BaseException],
27+
exc_obj: BaseException,
28+
exc_tb: types.TracebackType,
29+
_marker: Any = object(),
30+
) -> NoReturn:
2731
"""Re-raise an exception received from sys.exc_info() or similar."""
2832
raise exc_obj.with_traceback(exc_tb)
2933

testtools/content.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,9 @@ def filter_stack(stack):
235235

236236
def json_content(json_data):
237237
"""Create a JSON Content object from JSON-encodeable data."""
238-
data = json.dumps(json_data)
238+
json_str = json.dumps(json_data)
239239
# The json module perversely returns native str not bytes
240-
data = data.encode("utf8")
240+
data = json_str.encode("utf8")
241241
return Content(JSON, lambda: [data])
242242

243243

testtools/helpers.py

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,15 @@
11
# Copyright (c) 2010-2012 testtools developers. See LICENSE for details.
22

3-
import sys
3+
from collections.abc import Callable
4+
from typing import Any, TypeVar
45

6+
T = TypeVar("T")
7+
K = TypeVar("K")
8+
V = TypeVar("V")
9+
R = TypeVar("R")
510

6-
def try_import(name, alternative=None, error_callback=None):
7-
"""Attempt to import a module, with a fallback.
811

9-
Attempt to import ``name``. If it fails, return ``alternative``. When
10-
supporting multiple versions of Python or optional dependencies, it is
11-
useful to be able to try to import a module.
12-
13-
:param name: The name of the object to import, e.g. ``os.path`` or
14-
``os.path.join``.
15-
:param alternative: The value to return if no module can be imported.
16-
Defaults to None.
17-
:param error_callback: If non-None, a callable that is passed the
18-
ImportError when the module cannot be loaded.
19-
"""
20-
module_segments = name.split(".")
21-
last_error = None
22-
remainder = []
23-
24-
# module_name will be what successfully imports. We cannot walk from the
25-
# __import__ result because in import loops (A imports A.B, which imports
26-
# C, which calls try_import("A.B")) A.B will not yet be set.
27-
while module_segments:
28-
module_name = ".".join(module_segments)
29-
try:
30-
__import__(module_name)
31-
except ImportError:
32-
last_error = sys.exc_info()[1]
33-
remainder.append(module_segments.pop())
34-
continue
35-
else:
36-
break
37-
else:
38-
if last_error is not None and error_callback is not None:
39-
error_callback(last_error)
40-
return alternative
41-
42-
module = sys.modules[module_name]
43-
nonexistent = object()
44-
for segment in reversed(remainder):
45-
module = getattr(module, segment, nonexistent)
46-
if module is nonexistent:
47-
if last_error is not None and error_callback is not None:
48-
error_callback(last_error)
49-
return alternative
50-
51-
return module
52-
53-
54-
def map_values(function, dictionary):
12+
def map_values(function: Callable[[V], R], dictionary: dict[K, V]) -> dict[K, R]:
5513
"""Map ``function`` across the values of ``dictionary``.
5614
5715
:return: A dict with the same keys as ``dictionary``, where the value
@@ -60,17 +18,17 @@ def map_values(function, dictionary):
6018
return {k: function(dictionary[k]) for k in dictionary}
6119

6220

63-
def filter_values(function, dictionary):
21+
def filter_values(function: Callable[[V], bool], dictionary: dict[K, V]) -> dict[K, V]:
6422
"""Filter ``dictionary`` by its values using ``function``."""
6523
return {k: v for k, v in dictionary.items() if function(v)}
6624

6725

68-
def dict_subtract(a, b):
26+
def dict_subtract(a: dict[K, V], b: dict[K, Any]) -> dict[K, V]:
6927
"""Return the part of ``a`` that's not in ``b``."""
7028
return {k: a[k] for k in set(a) - set(b)}
7129

7230

73-
def list_subtract(a, b):
31+
def list_subtract(a: list[T], b: list[T]) -> list[T]:
7432
"""Return a list ``a`` without the elements of ``b``.
7533
7634
If a particular value is in ``a`` twice and ``b`` once then the returned

0 commit comments

Comments
 (0)