-
-
Notifications
You must be signed in to change notification settings - Fork 4
Strict type checking and re-enable mypy #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3206ddc
4b2f0ad
db2f186
0359b8d
1c81cb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
from __future__ import annotations | ||
|
||
import builtins | ||
import contextlib | ||
import errno | ||
import functools | ||
|
@@ -12,17 +13,43 @@ | |
import sys | ||
import tempfile | ||
import urllib.request | ||
from typing import Iterator | ||
from collections.abc import Callable, Generator, Iterator | ||
from types import TracebackType | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Literal, | ||
TypeVar, | ||
Union, | ||
cast, | ||
) | ||
|
||
# jaraco/backports.tarfile#1 | ||
if TYPE_CHECKING or sys.version_info >= (3, 12): | ||
import tarfile | ||
else: | ||
from backports import tarfile | ||
|
||
if TYPE_CHECKING: | ||
from _typeshed import FileDescriptorOrPath, OptExcInfo, StrPath | ||
from typing_extensions import ParamSpec, Self, TypeAlias, Unpack | ||
|
||
if sys.version_info < (3, 12): | ||
from backports import tarfile | ||
else: | ||
import tarfile | ||
_FileDescriptorOrPathT = TypeVar( | ||
"_FileDescriptorOrPathT", bound=FileDescriptorOrPath | ||
) | ||
_P = ParamSpec("_P") | ||
|
||
_UnpackableOptExcInfo: TypeAlias = tuple[ | ||
Union[type[BaseException], None], | ||
Union[BaseException, None], | ||
Union[TracebackType, None], | ||
] | ||
_R = TypeVar("_R") | ||
_T1_co = TypeVar("_T1_co", covariant=True) | ||
_T2_co = TypeVar("_T2_co", covariant=True) | ||
|
||
|
||
@contextlib.contextmanager | ||
def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: | ||
def pushd(dir: StrPath) -> Iterator[StrPath]: | ||
""" | ||
>>> tmp_path = getfixture('tmp_path') | ||
>>> with pushd(tmp_path): | ||
|
@@ -39,9 +66,7 @@ def pushd(dir: str | os.PathLike) -> Iterator[str | os.PathLike]: | |
|
||
|
||
@contextlib.contextmanager | ||
def tarball( | ||
url, target_dir: str | os.PathLike | None = None | ||
) -> Iterator[str | os.PathLike]: | ||
def tarball(url: str, target_dir: StrPath | None = None) -> Iterator[StrPath]: | ||
""" | ||
Get a URL to a tarball, download, extract, yield, then clean up. | ||
|
||
|
@@ -86,13 +111,21 @@ def tarball( | |
|
||
def strip_first_component( | ||
member: tarfile.TarInfo, | ||
path, | ||
path: object, | ||
) -> tarfile.TarInfo: | ||
_, member.name = member.name.split('/', 1) | ||
return member | ||
|
||
|
||
def _compose(*cmgrs): | ||
def _compose( | ||
*cmgrs: Unpack[ | ||
tuple[ | ||
# Flipped from compose_two because of reverse | ||
Callable[[_T1_co], contextlib.AbstractContextManager[_T2_co]], | ||
Callable[_P, contextlib.AbstractContextManager[_T1_co]], | ||
] | ||
], | ||
) -> Callable[_P, contextlib._GeneratorContextManager[_T2_co]]: | ||
""" | ||
Compose any number of dependent context managers into a single one. | ||
|
||
|
@@ -112,14 +145,21 @@ def _compose(*cmgrs): | |
... assert os.path.samefile(os.getcwd(), dir) | ||
""" | ||
|
||
def compose_two(inner, outer): | ||
def composed(*args, **kwargs): | ||
def compose_two( | ||
inner: Callable[_P, contextlib.AbstractContextManager[_T1_co]], | ||
outer: Callable[[_T1_co], contextlib.AbstractContextManager[_T2_co]], | ||
) -> Callable[_P, contextlib._GeneratorContextManager[_T2_co]]: | ||
def composed(*args: _P.args, **kwargs: _P.kwargs) -> Generator[_T2_co]: | ||
with inner(*args, **kwargs) as saved, outer(saved) as res: | ||
yield res | ||
|
||
return contextlib.contextmanager(composed) | ||
|
||
return functools.reduce(compose_two, reversed(cmgrs)) | ||
# reversed makes cmgrs no longer variadic, breaking type validation | ||
# Mypy infers compose_two as Callable[[function, function], function]. See: | ||
# - https://github.com/python/typeshed/issues/7580 | ||
# - https://github.com/python/mypy/issues/8240 | ||
return functools.reduce(compose_two, reversed(cmgrs)) # type: ignore[return-value, arg-type] | ||
|
||
|
||
tarball_cwd = _compose(pushd, tarball) | ||
|
@@ -128,7 +168,11 @@ def composed(*args, **kwargs): | |
""" | ||
|
||
|
||
def remove_readonly(func, path, exc_info): | ||
def remove_readonly( | ||
func: Callable[[_FileDescriptorOrPathT], object], | ||
path: _FileDescriptorOrPathT, | ||
exc_info: tuple[object, OSError, object], | ||
) -> None: | ||
""" | ||
Add support for removing read-only files on Windows. | ||
""" | ||
|
@@ -142,16 +186,20 @@ def remove_readonly(func, path, exc_info): | |
raise | ||
|
||
|
||
def robust_remover(): | ||
def robust_remover() -> Callable[..., None]: | ||
return ( | ||
functools.partial(shutil.rmtree, onerror=remove_readonly) | ||
functools.partial( | ||
# cast for python/mypy#18637 / python/mypy#17585 | ||
cast("Callable[..., None]", shutil.rmtree), | ||
onerror=remove_readonly, | ||
) | ||
if platform.system() == 'Windows' | ||
else shutil.rmtree | ||
) | ||
|
||
|
||
@contextlib.contextmanager | ||
def temp_dir(remover=shutil.rmtree): | ||
def temp_dir(remover: Callable[[str], object] = shutil.rmtree) -> Generator[str]: | ||
""" | ||
Create a temporary directory context. Pass a custom remover | ||
to override the removal behavior. | ||
|
@@ -173,8 +221,11 @@ def temp_dir(remover=shutil.rmtree): | |
|
||
@contextlib.contextmanager | ||
def repo_context( | ||
url, branch: str | None = None, quiet: bool = True, dest_ctx=robust_temp_dir | ||
): | ||
url: str, | ||
branch: str | None = None, | ||
quiet: bool = True, | ||
dest_ctx: Callable[[], contextlib.AbstractContextManager[str]] = robust_temp_dir, | ||
) -> Generator[str]: | ||
""" | ||
Check out the repo indicated by url. | ||
|
||
|
@@ -190,7 +241,7 @@ def repo_context( | |
exe = 'git' if 'git' in url else 'hg' | ||
with dest_ctx() as repo_dir: | ||
cmd = [exe, 'clone', url, repo_dir] | ||
cmd.extend(['--branch', branch] * bool(branch)) | ||
cmd.extend(['--branch', branch] if branch else []) | ||
stream = subprocess.DEVNULL if quiet else None | ||
subprocess.check_call(cmd, stdout=stream, stderr=stream) | ||
yield repo_dir | ||
|
@@ -230,37 +281,42 @@ class ExceptionTrap: | |
False | ||
""" | ||
|
||
exc_info = None, None, None | ||
exc_info: OptExcInfo = None, None, None | ||
|
||
def __init__(self, exceptions=(Exception,)): | ||
def __init__(self, exceptions: tuple[type[BaseException], ...] = (Exception,)): | ||
self.exceptions = exceptions | ||
|
||
def __enter__(self): | ||
def __enter__(self) -> Self: | ||
return self | ||
|
||
@property | ||
def type(self): | ||
def type(self) -> type[BaseException] | None: | ||
return self.exc_info[0] | ||
|
||
@property | ||
def value(self): | ||
def value(self) -> BaseException | None: | ||
return self.exc_info[1] | ||
|
||
@property | ||
def tb(self): | ||
def tb(self) -> TracebackType | None: | ||
return self.exc_info[2] | ||
|
||
def __exit__(self, *exc_info): | ||
type = exc_info[0] | ||
matches = type and issubclass(type, self.exceptions) | ||
def __exit__( | ||
self, | ||
*exc_info: Unpack[_UnpackableOptExcInfo], # noqa: PYI036 # We can do better than object | ||
) -> builtins.type[BaseException] | None | bool: | ||
exc_type = exc_info[0] | ||
matches = exc_type and issubclass(exc_type, self.exceptions) | ||
if matches: | ||
self.exc_info = exc_info | ||
self.exc_info = exc_info # type: ignore[assignment] | ||
return matches | ||
Comment on lines
-253
to
312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively: def __exit__(
self,
exc_type: builtins.type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> builtins.type[BaseException] | None | bool:
matches = exc_type and issubclass(exc_type, self.exceptions)
if matches:
self.exc_info = (exc_type, exc, exc_tb) # type: ignore[assignment]
return matches |
||
|
||
def __bool__(self): | ||
def __bool__(self) -> bool: | ||
return bool(self.type) | ||
|
||
def raises(self, func, *, _test=bool): | ||
def raises( | ||
self, func: Callable[_P, _R], *, _test: Callable[[ExceptionTrap], bool] = bool | ||
) -> functools._Wrapped[_P, _R, _P, bool]: | ||
""" | ||
Wrap func and replace the result with the truth | ||
value of the trap (True if an exception occurred). | ||
|
@@ -280,14 +336,14 @@ def raises(self, func, *, _test=bool): | |
""" | ||
|
||
@functools.wraps(func) | ||
def wrapper(*args, **kwargs): | ||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> bool: | ||
with ExceptionTrap(self.exceptions) as trap: | ||
func(*args, **kwargs) | ||
return _test(trap) | ||
|
||
return wrapper | ||
|
||
def passes(self, func): | ||
def passes(self, func: Callable[_P, _R]) -> functools._Wrapped[_P, _R, _P, bool]: | ||
""" | ||
Wrap func and replace the result with the truth | ||
value of the trap (True if no exception). | ||
|
@@ -341,16 +397,23 @@ class on_interrupt(contextlib.ContextDecorator): | |
... on_interrupt('ignore')(do_interrupt)() | ||
""" | ||
|
||
def __init__(self, action='error', /, code=1): | ||
def __init__( | ||
self, action: Literal['ignore', 'suppress', 'error'] = 'error', /, code: int = 1 | ||
): | ||
self.action = action | ||
self.code = code | ||
|
||
def __enter__(self): | ||
def __enter__(self) -> Self: | ||
return self | ||
|
||
def __exit__(self, exctype, excinst, exctb): | ||
def __exit__( | ||
self, | ||
exctype: type[BaseException] | None, | ||
excinst: BaseException | None, | ||
exctb: TracebackType | None, | ||
) -> None | bool: | ||
if exctype is not KeyboardInterrupt or self.action == 'ignore': | ||
return | ||
return None | ||
elif self.action == 'error': | ||
raise SystemExit(self.code) from excinst | ||
return self.action == 'suppress' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[mypy] | ||
# Is the project well-typed? | ||
strict = False | ||
strict = True | ||
|
||
# Early opt-in even when strict = False | ||
warn_unused_ignores = True | ||
|
@@ -13,3 +13,11 @@ explicit_package_bases = True | |
disable_error_code = | ||
# Disable due to many false positives | ||
overload-overlap, | ||
|
||
# jaraco/backports.tarfile#1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
[mypy-backports.*] | ||
follow_untyped_imports = True | ||
|
||
# jaraco/portend#17 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ref: jaraco/portend#17 |
||
[mypy-portend.*] | ||
ignore_missing_imports = True |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,9 +73,4 @@ type = [ | |
# local | ||
] | ||
|
||
|
||
[tool.setuptools_scm] | ||
|
||
|
||
[tool.pytest-enabler.mypy] | ||
# Disabled due to jaraco/skeleton#143 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refs: python/typeshed#7580 & python/mypy#8240