Skip to content
Draft
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
5 changes: 2 additions & 3 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
include *.txt *.rst *.cfg *.py *.ini *.toml *.yaml
exclude .installed.cfg
recursive-include reg *.py
recursive-include reg *.py py.typed
recursive-include tests *.py
recursive-include doc *.rst Makefile *.py *.bat
include .coveragerc
recursive-include doc *.rst Makefile *.py *.bat
47 changes: 44 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,18 @@ test = ["pytest >= 8", "pytest-env", "sphinx"]
docs = ["sphinx"]
coverage = ["pytest-cov"]
lint = ["black", "flake8", "flake8-pyproject"]
# NOTE: sphinx 9.1 introduced PEP-695 syntax into the code-base, so we
# can't type check in pre-3.12 mode this way, which we need to,
# as long as we still support these versions.
mypy = ["mypy", "pytest", "sphinx < 9.1.0"]
pyright = ["pyright", "pytest", "sphinx < 9.1.0"]

[tool.setuptools.packages]
find = {}

[tool.setuptools.package-data]
reg = ["py.typed"]

[tool.setuptools.dynamic]
readme = {file = ["README.rst", "CHANGES.txt"]}

Expand All @@ -51,17 +59,26 @@ addopts = ["-vv"]
env = ["RUN_ENV=test"]

[tool.coverage.run]
omit = ["reg/tests/*"]
omit = ["reg/tests/*", "reg/types.py"]
source = ["reg"]

[tool.coverage.report]
show_missing = true

[tool.flake8]
show-source = true
ignore = ["E203", "E731", "W503"]
ignore = ["E203", "E301", "E501", "E704", "E731", "W503"]
max-line-length = 88

[tool.mypy]
python_version = "3.10"
strict = true
warn_unreachable = true

[[tool.mypy.overrides]]
module = "reg.tests.fixtures.*"
disallow_untyped_defs = false

[tool.tox]
requires = ["tox>=4"]
env_list = [
Expand All @@ -75,11 +92,13 @@ env_list = [
"pre-commit",
"docs",
"perf",
"mypy",
"pyright"
]
skip_missing_interpreters = true

[tool.tox.gh.python]
"3.10" = ["py310", "perf"]
"3.10" = ["py310", "mypy", "pyright", "perf"]
"3.11" = ["py311"]
"3.12" = ["py312"]
"3.13" = ["py313"]
Expand Down Expand Up @@ -119,3 +138,25 @@ extras = []
commands = [
["python", "{toxinidir}/tox_perf.py"],
]

[tool.tox.env.mypy]
base_python = ["python3"]
extras = ["mypy"]
commands = [
["mypy", "-p", "reg", "--python-version", "3.10"],
["mypy", "-p", "reg", "--python-version", "3.11"],
["mypy", "-p", "reg", "--python-version", "3.12"],
["mypy", "-p", "reg", "--python-version", "3.13"],
["mypy", "-p", "reg", "--python-version", "3.14"],
]

[tool.tox.env.pyright]
base_python = ["python3"]
extras = ["pyright"]
commands = [
["pyright", "reg", "--pythonversion", "3.10"],
["pyright", "reg", "--pythonversion", "3.11"],
["pyright", "reg", "--pythonversion", "3.12"],
["pyright", "reg", "--pythonversion", "3.13"],
["pyright", "reg", "--pythonversion", "3.14"],
]
21 changes: 20 additions & 1 deletion reg/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# flake8: noqa
from .dispatch import dispatch, Dispatch, LookupEntry
from .context import (
dispatch_method,
Expand All @@ -17,3 +16,23 @@
match_class,
)
from .cache import DictCachingKeyLookup, LruCachingKeyLookup

__all__ = (
"ClassIndex",
"DictCachingKeyLookup",
"Dispatch",
"DispatchMethod",
"KeyIndex",
"LookupEntry",
"LruCachingKeyLookup",
"Predicate",
"RegistrationError",
"arginfo",
"clean_dispatch_methods",
"dispatch",
"dispatch_method",
"match_class",
"match_instance",
"match_key",
"methodify",
)
40 changes: 31 additions & 9 deletions reg/arginfo.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
from __future__ import annotations

import inspect
import sys

from typing import TYPE_CHECKING, Any, cast

if TYPE_CHECKING:
from collections.abc import Callable
from .types import ArgInfo

if sys.version_info < (3, 14):

def get_signature(callable): # pragma: no cover
def get_signature(
callable: Callable[..., Any],
) -> inspect.Signature: # pragma: no cover
"""A compatibility wrapper for `inspect.signature`."""
return inspect.signature(callable)

else:
from annotationlib import Format # pragma: no cover

def get_signature(callable): # pragma: no cover
def get_signature(
callable: Callable[..., Any],
) -> inspect.Signature: # pragma: no cover
"""A compatibility wrapper for `inspect.signature`."""
return inspect.signature(callable, annotation_format=Format.FORWARDREF)


def arginfo(callable):
# NOTE: This no-op decorator lets type checkers know about the extra
# attributes we add to the arginfo callable
def _coerce_to_arginfo(
f: Callable[[Callable[..., Any]], inspect.FullArgSpec | None],
) -> ArgInfo:
return cast("ArgInfo", f)


@_coerce_to_arginfo
def arginfo(callable: Callable[..., Any]) -> inspect.FullArgSpec | None:
"""Get information about the arguments of a callable.

Returns a :class:`inspect.FullArgSpec` object as for
Expand Down Expand Up @@ -43,10 +64,11 @@ def arginfo(callable):
except KeyError:
# Try to get __call__ function from the cache.
try:
return arginfo._cache[callable.__call__]
return arginfo._cache[callable.__call__] # type: ignore
except (AttributeError, KeyError):
pass

cache_key: Callable[..., Any]
if inspect.isfunction(callable):
cache_key = callable
elif inspect.ismethod(callable):
Expand All @@ -63,7 +85,7 @@ def arginfo(callable):
# Since arbitrary callable objects may not be hashable
# we instead retrieve their call method, which should be
try:
cache_key = callable.__call__
cache_key = callable.__call__ # type: ignore
except AttributeError:
return None

Expand Down Expand Up @@ -111,22 +133,22 @@ def arginfo(callable):
return result


def is_cached(callable):
def is_cached(callable: Callable[..., Any]) -> bool:
if callable in arginfo._cache:
return True
return callable.__call__ in arginfo._cache
return callable.__call__ in arginfo._cache # type: ignore


arginfo._cache = {}
arginfo.is_cached = is_cached


def fake_empty_init():
def fake_empty_init() -> None:
pass # pragma: nocoverage


class Dummy:
pass


WRAPPER_DESCRIPTOR = Dummy.__init__
WRAPPER_DESCRIPTOR: object = Dummy.__init__
85 changes: 67 additions & 18 deletions reg/cache.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
from repoze.lru import lru_cache
from __future__ import annotations

from repoze.lru import lru_cache # type: ignore
from typing import TYPE_CHECKING, Any, Generic

class Cache(dict):
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing_extensions import TypeVar
from .types import KeyLookup

_ValueT = TypeVar("_ValueT", default=Callable[..., Any])
else:
from typing import TypeVar

_ValueT = TypeVar("_ValueT")

_KT = TypeVar("_KT")
_VT = TypeVar("_VT")


class Cache(dict[_KT, _VT]):
"""A dict to cache a function."""

def __init__(self, func):
def __init__(self, func: Callable[[_KT], _VT]) -> None:
self.func = func

def __missing__(self, key):
def __missing__(self, key: _KT) -> _VT:
self[key] = result = self.func(key)
return result


class DictCachingKeyLookup:
class DictCachingKeyLookup(Generic[_ValueT]):
"""A key lookup that caches.

Implements the read-only API of :class:`reg.PredicateRegistry` using
Expand All @@ -28,14 +45,32 @@ class DictCachingKeyLookup:

"""

def __init__(self, key_lookup):
def __init__(self, key_lookup: KeyLookup[_ValueT]) -> None:
self.key_lookup = key_lookup
self.component = Cache(key_lookup.component).__getitem__
self.fallback = Cache(key_lookup.fallback).__getitem__
self.all = Cache(lambda key: list(key_lookup.all(key))).__getitem__
self.component = Cache(key_lookup.component).__getitem__ # type: ignore
self.fallback = Cache(key_lookup.fallback).__getitem__ # type: ignore

def _all(key: Sequence[Any]) -> list[_ValueT]:
return list(key_lookup.all(key))

self.all = Cache(_all).__getitem__ # type: ignore

if TYPE_CHECKING:
# NOTE: For pyright's sake we declare these callable instance attributes
# as methods, even though they're not, since pyright does not seem
# to be able to match protocols against them. mypy can deal with
# it just fine
def component(self, key: Sequence[Any], /) -> _ValueT | None:
raise NotImplementedError

def fallback(self, key: Sequence[Any], /) -> _ValueT | None:
raise NotImplementedError

def all(self, key: Sequence[Any], /) -> list[_ValueT]:
raise NotImplementedError


class LruCachingKeyLookup:
class LruCachingKeyLookup(Generic[_ValueT]):
"""A key lookup that caches.

Implements the read-only API of :class:`reg.PredicateRegistry`, using
Expand All @@ -57,12 +92,26 @@ class LruCachingKeyLookup:

def __init__(
self,
key_lookup,
component_cache_size,
all_cache_size,
fallback_cache_size,
):
key_lookup: KeyLookup[_ValueT],
component_cache_size: int,
all_cache_size: int,
fallback_cache_size: int,
) -> None:
self.key_lookup = key_lookup
self.component = lru_cache(component_cache_size)(key_lookup.component)
self.fallback = lru_cache(fallback_cache_size)(key_lookup.fallback)
self.all = lru_cache(all_cache_size)(lambda key: list(key_lookup.all(key)))
self.component = lru_cache(component_cache_size)(key_lookup.component) # type: ignore
self.fallback = lru_cache(fallback_cache_size)(key_lookup.fallback) # type: ignore
self.all = lru_cache(all_cache_size)(lambda key: list(key_lookup.all(key))) # type: ignore

if TYPE_CHECKING:
# NOTE: For pyright's sake we declare these callable instance attributes
# as methods, even though they're not, since pyright does not seem
# to be able to match protocols against them. mypy can deal with
# it just fine
def component(self, key: Sequence[Any], /) -> _ValueT | None:
raise NotImplementedError

def fallback(self, key: Sequence[Any], /) -> _ValueT | None:
raise NotImplementedError

def all(self, key: Sequence[Any], /) -> list[_ValueT]:
raise NotImplementedError
Loading
Loading