Skip to content
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 #3

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,14 @@
)

nitpick_ignore += [
# jaraco/nspektr#3
('py:class', 'Requirement'),
('py:class', 'Marker'),
('py:class', 'importlib.metadata.EntryPoint'),
('py:class', 'itertools.chain'),
('py:class', 'metadata.EntryPoint'),
('py:class', 'itertools.filterfalse'),
('py:class', 'Requirement'),
('py:class', 'nspektr._T'),
('py:class', 'importlib.metadata.Distribution'),
]
2 changes: 1 addition & 1 deletion mypy.ini
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
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Complete annotations and add ``py.typed`` marker -- by :user:`Avasam`
54 changes: 35 additions & 19 deletions nspektr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import itertools
import functools
from __future__ import annotations

import contextlib
import functools
import itertools
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, Iterator, TypeVar

from packaging.requirements import Requirement
from packaging.version import Version
from more_itertools import always_iterable
from jaraco.context import suppress
from jaraco.functools import apply
from more_itertools import always_iterable
from packaging.markers import Marker
from packaging.requirements import Requirement
from packaging.version import Version

from ._compat import metadata, repair_extras

if TYPE_CHECKING:
from typing_extensions import Literal, Self

_T = TypeVar("_T")


def resolve(req: Requirement) -> metadata.Distribution:
"""
Expand All @@ -25,13 +35,13 @@ def resolve(req: Requirement) -> metadata.Distribution:
dist = metadata.distribution(req.name)
if not req.specifier.contains(Version(dist.version), prereleases=True):
raise metadata.PackageNotFoundError(str(req))
dist.extras = req.extras # type: ignore
dist.extras = req.extras # type: ignore[attr-defined] # Adding extras as if this was an EntryPoint
return dist


@apply(bool)
@suppress(metadata.PackageNotFoundError)
def is_satisfied(req: Requirement):
def is_satisfied(req: Requirement) -> metadata.Distribution:
return resolve(req)


Expand All @@ -40,21 +50,23 @@ def is_satisfied(req: Requirement):

class NullMarker:
@classmethod
def wrap(cls, req: Requirement):
def wrap(cls, req: Requirement) -> Marker | Self:
return req.marker or cls()

def evaluate(self, *args, **kwargs):
def evaluate(self, *args: object, **kwargs: object) -> Literal[True]:
return True


def find_direct_dependencies(dist, extras=None):
def find_direct_dependencies(
dist: metadata.Distribution, extras: str | Iterable[str | None] | None = None
) -> itertools.chain[Requirement]:
"""
Find direct, declared dependencies for dist.
"""
simple = (
req
for req in map(Requirement, always_iterable(dist.requires))
if NullMarker.wrap(req).evaluate(dict(extra=None))
if NullMarker.wrap(req).evaluate(dict(extra=""))
)
extra_deps = (
req
Expand All @@ -65,7 +77,7 @@ def find_direct_dependencies(dist, extras=None):
return itertools.chain(simple, extra_deps)


def traverse(items, visit):
def traverse(items: Iterator[_T], visit: Callable[[_T], Iterable[_T]]) -> Iterator[_T]:
"""
Given an iterable of items, traverse the items.

Expand All @@ -81,13 +93,15 @@ def traverse(items, visit):
items = itertools.chain(items, visit(item))


def find_req_dependencies(req):
def find_req_dependencies(req: Requirement) -> Iterator[Requirement]:
with contextlib.suppress(metadata.PackageNotFoundError):
dist = resolve(req)
yield from find_direct_dependencies(dist)


def find_dependencies(dist, extras=None):
def find_dependencies(
dist: metadata.Distribution, extras: str | Iterable[str | None] | None = None
) -> Iterator[Requirement]:
"""
Find all reachable dependencies for dist.

Expand All @@ -104,7 +118,9 @@ def find_dependencies(dist, extras=None):
True
"""

def visit(req, seen=set()):
def visit(
req: Requirement, seen: set[Requirement] = set()
) -> tuple[()] | Iterator[Requirement]:
if req in seen:
return ()
seen.add(req)
Expand All @@ -114,18 +130,18 @@ def visit(req, seen=set()):


class Unresolved(Exception):
def __iter__(self):
def __iter__(self) -> Iterator[Requirement]:
return iter(self.args[0])


def missing(ep):
def missing(ep: metadata.EntryPoint) -> itertools.filterfalse[Requirement]:
"""
Generate the unresolved dependencies (if any) of ep.
"""
return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras)))
return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras))) # type: ignore[arg-type] # FIXME


def check(ep):
def check(ep: metadata.EntryPoint) -> None:
"""
>>> ep, = metadata.entry_points(group='console_scripts', name='pytest')
>>> check(ep)
Expand Down
23 changes: 14 additions & 9 deletions nspektr/_compat.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import contextlib
import sys
from __future__ import annotations

import sys
from collections.abc import Iterable
from re import Match

if sys.version_info >= (3, 10):
import importlib.metadata as metadata
else:
import importlib_metadata as metadata # type: ignore # noqa: F401
import importlib.metadata as _metadata
else: # pragma: no cover #jaraco/skeleton#130
import importlib_metadata as _metadata # noqa: F401

metadata = _metadata # Explicit re-export


def repair_extras(extras):
def repair_extras(extras: list[str] | Iterable[Match[str]]) -> list[str]:
"""
Repair extras that appear as match objects.

python/importlib_metadata#369 revealed a flaw in the EntryPoint
implementation. This function wraps the extras to ensure
they are proper strings even on older implementations.
"""
with contextlib.suppress(AttributeError):
return list(item.group(0) for item in extras)
return extras
try:
return list(item.group(0) for item in extras) # type: ignore[union-attr] # Explicitly repairing this error
except AttributeError:
return extras # type: ignore[return-value] # On a single failure we assume it's all strings
Empty file added nspektr/py.typed
Empty file.
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,3 @@ type = [


[tool.setuptools_scm]


[tool.pytest-enabler.mypy]
# Disabled due to jaraco/skeleton#143
Loading