-
Notifications
You must be signed in to change notification settings - Fork 5
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
Strict typing and py.typed #18
Changes from 2 commits
3fcabf1
2585162
b8b0578
0c326f3
26e40f2
208a16e
ee9dfbf
c3fa5a2
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 |
---|---|---|
@@ -0,0 +1 @@ | ||
Complete annotations and add ``py.typed`` marker -- by :user:`Avasam` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,47 @@ | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
import os | ||
import pathlib | ||
import re | ||
import shlex | ||
import sys | ||
from collections.abc import Container, MutableSequence, Sequence | ||
from typing import TYPE_CHECKING, TypeVar, overload | ||
|
||
import toml | ||
from jaraco.context import suppress | ||
from jaraco.functools import apply | ||
from pytest import Config, Parser | ||
|
||
if sys.version_info > (3, 12): | ||
if sys.version_info >= (3, 12): | ||
from importlib import resources | ||
else: | ||
import importlib_resources as resources | ||
|
||
if sys.version_info >= (3, 9): | ||
from importlib.abc import Traversable | ||
else: | ||
from pathlib import Path as Traversable | ||
|
||
if TYPE_CHECKING: | ||
from _typeshed import SupportsRead | ||
from typing_extensions import Never | ||
|
||
import toml | ||
from jaraco.context import suppress | ||
from jaraco.functools import apply | ||
|
||
_T = TypeVar("_T") | ||
|
||
consume = tuple | ||
consume = tuple # type: ignore[type-arg] # Generic doesn't matter; keep it callable | ||
""" | ||
Consume an iterable | ||
""" | ||
|
||
|
||
def none_as_empty(ob): | ||
@overload | ||
def none_as_empty(ob: None) -> dict[Never, Never]: ... | ||
@overload | ||
def none_as_empty(ob: _T) -> _T: ... | ||
def none_as_empty(ob: _T | None) -> _T | dict[Never, Never]: | ||
""" | ||
>>> none_as_empty({}) | ||
{} | ||
|
@@ -33,25 +53,31 @@ def none_as_empty(ob): | |
return ob or {} | ||
|
||
|
||
def read_plugins_stream(stream): | ||
def read_plugins_stream( | ||
stream: str | bytes | pathlib.PurePath | SupportsRead[str], | ||
) -> dict[str, dict[str, str]]: | ||
defn = toml.load(stream) | ||
return defn["tool"]["pytest-enabler"] | ||
return defn["tool"]["pytest-enabler"] # type: ignore[no-any-return] | ||
|
||
|
||
@apply(none_as_empty) | ||
@suppress(Exception) | ||
def read_plugins(path): | ||
def read_plugins(path: Traversable) -> dict[str, dict[str, str]]: | ||
with path.open(encoding='utf-8') as stream: | ||
return read_plugins_stream(stream) | ||
|
||
|
||
def pytest_load_initial_conftests(early_config, parser, args): | ||
def pytest_load_initial_conftests( | ||
early_config: Config, | ||
parser: Parser | None, | ||
args: MutableSequence[str], | ||
) -> None: | ||
plugins = { | ||
**read_plugins(resources.files().joinpath('default.toml')), | ||
**read_plugins(pathlib.Path('pyproject.toml')), | ||
} | ||
|
||
def _has_plugin(name): | ||
def _has_plugin(name: str) -> bool: | ||
pm = early_config.pluginmanager | ||
return pm.has_plugin(name) or pm.has_plugin('pytest_' + name) | ||
|
||
|
@@ -61,7 +87,7 @@ def _has_plugin(name): | |
_pytest_cov_check(enabled, early_config, parser, args) | ||
|
||
|
||
def _remove_deps(): | ||
def _remove_deps() -> None: | ||
""" | ||
Coverage will not detect function definitions as being covered | ||
if the functions are defined before coverage is invoked. As | ||
|
@@ -82,7 +108,12 @@ def _remove_deps(): | |
consume(map(sys.modules.__delitem__, to_delete)) | ||
|
||
|
||
def _pytest_cov_check(plugins, early_config, parser, args): # pragma: nocover | ||
def _pytest_cov_check( | ||
plugins: Container[str], | ||
early_config: Config, | ||
parser: Parser | None, | ||
args: Sequence[str | os.PathLike[str]], | ||
) -> None: # pragma: nocover | ||
""" | ||
pytest_cov runs its command-line checks so early that no hooks | ||
can intervene. By now, the hook that installs the plugin has | ||
|
@@ -99,6 +130,8 @@ def _pytest_cov_check(plugins, early_config, parser, args): # pragma: nocover | |
_remove_deps() | ||
# important: parse all known args to ensure pytest-cov can configure | ||
# itself based on other plugins like pytest-xdist (see #1). | ||
if parser is None: | ||
raise ValueError("parser cannot be None if cov in plugins") | ||
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. This new code block injects itself between the comment and the comment's logic. I'd like to avoid adding unnecessary logic and unreachable code to satisfy the type checker. I'd rather see a
Since it's a goal not to change the logic when adding type declarations (except where legitimate bugs are detected), let's go with the 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.
The current change only changes the error message and type (from 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. A 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.
Good catch. I was working from the assumption that the message in the Here's what I have in mind: diff --git a/pytest_enabler/__init__.py b/pytest_enabler/__init__.py
index bc8f10f..010a214 100644
--- a/pytest_enabler/__init__.py
+++ b/pytest_enabler/__init__.py
@@ -7,7 +7,7 @@ import re
import shlex
import sys
from collections.abc import Container, MutableSequence, Sequence
-from typing import TYPE_CHECKING, TypeVar, overload
+from typing import TYPE_CHECKING, TypeVar, cast, overload
import toml
from jaraco.context import suppress
@@ -84,7 +84,14 @@ def pytest_load_initial_conftests(
enabled = {key: plugins[key] for key in plugins if _has_plugin(key)}
for plugin in enabled.values():
args.extend(shlex.split(plugin.get('addopts', "")))
- _pytest_cov_check(enabled, early_config, parser, args)
+ _pytest_cov_check(
+ enabled,
+ early_config,
+ # parser is only used when known not to be None
+ # based on `enabled` and `early_config`.
+ cast(Parser, parser),
+ args,
+ )
def _remove_deps() -> None:
@@ -111,7 +118,7 @@ def _remove_deps() -> None:
def _pytest_cov_check(
plugins: Container[str],
early_config: Config,
- parser: Parser | None,
+ parser: Parser,
args: Sequence[str | os.PathLike[str]],
) -> None: # pragma: nocover
"""
@@ -130,8 +137,6 @@ def _pytest_cov_check(
_remove_deps()
# important: parse all known args to ensure pytest-cov can configure
# itself based on other plugins like pytest-xdist (see #1).
- if parser is None:
- raise ValueError("parser cannot be None if cov in plugins")
parser.parse_known_and_unknown_args(args, early_config.known_args_namespace)
with contextlib.suppress(ImportError): (8657ea1) I realize it's a little convoluted to have the caller do the cast, but I like that the type system is protecting against the unwanted type. What I'd really like, if it weren't so messy, is a wrapper for
I'd also be okay with this approach. I'll let you decide which of the three options you like best. |
||
parser.parse_known_and_unknown_args(args, early_config.known_args_namespace) | ||
|
||
with contextlib.suppress(ImportError): | ||
|
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.
Very nice. Thanks for doing it this way. I'll add this line to the skeleton as well for simpler diffs.
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.
If you add it to skeleton, you might want to write in a way where https://mypy.readthedocs.io/en/stable/error_code_list.html#require-annotation-if-variable-type-is-unclear-var-annotated won't trigger when it isn't being re-assigned and the type cannot be inferred.
or
(or disable
var-annotated
in mypy.ini fordocs/conf.py
)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.
Oh! Nice recommendation. Good thing our instincts align. I added jaraco/skeleton@0c326f3 before I saw this. I hope I got the type right. I'm testing it now.