diff --git a/CHANGES.txt b/CHANGES.txt index 3806f3e..33288e0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,10 @@ CHANGES 0.15 (unreleased) ================= +- Add type hints + +- Add partial attribute to directive methods for better typing support + - Fix Flake8 errors. - Apply Black code formatter. diff --git a/MANIFEST.in b/MANIFEST.in index 42b3003..9c575dc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include *.txt *.rst *.cfg *.py *.ini *.toml *.yaml include .coveragerc exclude .installed.cfg -recursive-include dectate *.py +recursive-include dectate *.py py.typed recursive-include doc *.rst Makefile *.py *.bat recursive-include scenarios *.py *.txt diff --git a/dectate/__init__.py b/dectate/__init__.py index 033fa89..f576588 100644 --- a/dectate/__init__.py +++ b/dectate/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa from .app import App, directive from .sentinel import Sentinel, NOT_FOUND from .config import commit, Action, Composite, CodeInfo @@ -13,3 +12,26 @@ from .query import Query from .tool import query_tool, convert_dotted_name, convert_bool, query_app from .toposort import topological_sort + +__all__ = ( + "NOT_FOUND", + "Action", + "App", + "CodeInfo", + "Composite", + "ConfigError", + "ConflictError", + "DirectiveError", + "DirectiveReportError", + "Query", + "QueryError", + "Sentinel", + "TopologicalSortError", + "commit", + "convert_bool", + "convert_dotted_name", + "directive", + "query_app", + "query_tool", + "topological_sort", +) diff --git a/dectate/app.py b/dectate/app.py index 35d5d3f..773cea0 100644 --- a/dectate/app.py +++ b/dectate/app.py @@ -1,6 +1,29 @@ +from __future__ import annotations + import sys +from functools import update_wrapper +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + Generic, + ParamSpec, + TypeVar, + cast, +) from .config import Configurable, Directive, commit, create_code_info +if TYPE_CHECKING: + from collections.abc import Callable, Collection, Iterator + from typing_extensions import Self + from .config import Action, Composite, DirectiveAbbreviation + from .types import DirectiveCallable + +_T = TypeVar("_T") +_ActionT = TypeVar("_ActionT", bound="Action | Composite") +_AppT = TypeVar("_AppT", bound="App") +_P = ParamSpec("_P") + class Config: """The object that contains the configurations. @@ -9,7 +32,11 @@ class Config: class attribute of :class:`Action`. """ - pass + if TYPE_CHECKING: + # NOTE: Since Config attributes are completely dynamic + # this is the best we can do. + def __getattr__(self, name: str) -> Any: + pass class AppMeta(type): @@ -18,11 +45,15 @@ class AppMeta(type): Sets up ``config`` and ``dectate`` class attributes. """ - def __new__(cls, name, bases, d): + def __new__( + cls, name: str, bases: tuple[type[Any], ...], d: dict[str, Any] + ) -> type[App]: extends = [base.dectate for base in bases if hasattr(base, "dectate")] d["config"] = config = Config() d["dectate"] = configurable = Configurable(extends, config) result = super().__new__(cls, name, bases, d) + if TYPE_CHECKING: + assert issubclass(result, App) configurable.app_class = result return result @@ -40,7 +71,7 @@ class App(metaclass=AppMeta): logger_name = "dectate.directive" """The prefix to use for directive debug logging.""" - dectate = None + dectate: Configurable """A dectate Configurable instance is installed here. This is installed when the class object is initialized, so during @@ -51,7 +82,7 @@ class App(metaclass=AppMeta): as committed configurations. """ - config = None + config: Config """Config object that contains the configuration after commit. This is installed when the class object is initialized, so during @@ -65,7 +96,9 @@ class App(metaclass=AppMeta): """ @classmethod - def get_directive_methods(cls): + def get_directive_methods( + cls, + ) -> Iterator[tuple[str, DirectiveMethod[Self, ...]]]: for name in dir(cls): attr = getattr(cls, name) im_func = getattr(attr, "__func__", None) @@ -75,7 +108,7 @@ def get_directive_methods(cls): yield name, attr @classmethod - def commit(cls): + def commit(cls) -> Collection[type[App]]: """Commit this class and any depending on it. This is intended to be overridden by subclasses if committing @@ -90,7 +123,7 @@ def commit(cls): return [cls] @classmethod - def is_committed(cls): + def is_committed(cls) -> bool: """True if this app class was ever committed. :return: bool that is ``True`` when the app was committed before. @@ -98,7 +131,7 @@ def is_committed(cls): return cls.dectate.committed @classmethod - def clean(cls): + def clean(cls) -> None: """A method that sets or restores the state of the class. Normally Dectate only sets up configuration into the ``config`` @@ -109,7 +142,55 @@ class during configuration time. You can override this classmethod pass -def directive(action_factory): +class BoundDirectiveMethod(Generic[_AppT, _P]): + __name__: str + __qualname__: str + + def __init__( + self, + cls: type[_AppT], + func: DirectiveCallable[Concatenate[type[_AppT], _P]], + ) -> None: + self.__objclass__ = cls + self.__func__ = func + self.action_factory = func.action_factory + update_wrapper(self, func) + # remove forwarded partial, since we need to modify the params + self.__dict__ = self.__dict__.copy() + del self.__dict__["partial"] + + def partial(self, *args: Any, **kw: Any) -> DirectiveAbbreviation: + return self.__func__.partial(self.__objclass__, *args, **kw) + + def __call__(self, *args: _P.args, **kw: _P.kwargs) -> Directive: + return self.__func__(self.__objclass__, *args, **kw) + + def __repr__(self) -> str: + return f"" + + +class DirectiveMethod(Generic[_AppT, _P]): + __name__: str + __qualname__: str + + def __init__(self, func: DirectiveCallable[Concatenate[Any, _P]]): + self.__func__ = func + update_wrapper(self, func) # type: ignore[arg-type] + + def __get__( + self, instance: _AppT | None, owner: type[_AppT], / + ) -> DirectiveCallable[_P]: + return BoundDirectiveMethod(owner, self.__func__) + + +def directive( + # NOTE: Ideally this would be type[_ActionT] & Callable[_P, _ActionT] + # but until type intersections are a thing, this is the best + # trade-off, since we want to see an error if we provide incorrect + # arguments to a directive. Type checkers not catching that this + # needs to be a type instance, is the lesser evil. + action_factory: Callable[_P, _ActionT], +) -> DirectiveMethod[Any, _P]: """Create a classmethod to hook action to application class. You pass in a :class:`dectate.Action` or a @@ -133,14 +214,28 @@ class my_directive(dectate.Action): :param action_factory: an action class to use as the directive. :return: a class method that represents the directive. """ + if not isinstance(action_factory, type): + raise TypeError( + "action_factory needs to be `dectate.Action` or `dectate.Composite` subclass." + ) - def method(cls, *args, **kw): - frame = sys._getframe(1) + def method(cls: Any, *args: _P.args, **kw: _P.kwargs) -> Directive: + frame = sys._getframe(2) code_info = create_code_info(frame) return Directive(action_factory, code_info, cls, args, kw) + def partial(cls: Any, *args: Any, **kw: Any) -> DirectiveAbbreviation: + frame = sys._getframe(2) + code_info = create_code_info(frame) + directive = Directive(action_factory, code_info, cls, args, kw) + with directive as abbreviation: + return abbreviation + # sphinxext and App.get_action_classes need to recognize this - method.action_factory = action_factory - method.__doc__ = action_factory.__doc__ - method.__module__ = action_factory.__module__ - return classmethod(method) + _method = cast("DirectiveCallable[Concatenate[Any, _P]]", method) + _method.action_factory = action_factory + _method.partial = partial # type: ignore[method-assign] + _method.__doc__ = action_factory.__doc__ + _method.__module__ = action_factory.__module__ + + return DirectiveMethod(_method) diff --git a/dectate/config.py b/dectate/config.py index 4dcb7c2..168d3b5 100644 --- a/dectate/config.py +++ b/dectate/config.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import abc import logging import sys import inspect +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar from .error import ( ConflictError, ConfigError, @@ -11,6 +14,15 @@ from .toposort import topological_sort from .sentinel import NOT_FOUND +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + from types import FrameType, TracebackType + from .app import App, Config + from .sentinel import Sentinel + +_T = TypeVar("_T") +_F = TypeVar("_F", bound="Callable[..., Any]") + order_count = 0 @@ -30,9 +42,9 @@ class Configurable: and then executes each action group, which performs them. """ - app_class = None + app_class: type[App] | None = None - def __init__(self, extends, config): + def __init__(self, extends: list[Configurable], config: Config) -> None: """ :param extends: the configurables that this configurable extends. @@ -45,13 +57,13 @@ def __init__(self, extends, config): self.extends = extends self.config = config # all action classes known - self._action_classes = {} + self._action_classes: dict[type[Action | Composite], str] = {} # directives used with configurable - self._directives = [] + self._directives: list[tuple[Directive, Any]] = [] # have we ever been committed self.committed = False - def register_directive(self, directive, obj): + def register_directive(self, directive: Directive, obj: Any) -> None: """Register a directive with this configurable. Called during import time when directives are used. @@ -63,9 +75,10 @@ def register_directive(self, directive, obj): """ self._directives.append((directive, obj)) - def _fixup_directive_names(self): + def _fixup_directive_names(self) -> None: """Set up correct name for directives.""" app_class = self.app_class + assert app_class is not None for name, method in app_class.get_directive_methods(): func = method.__func__ func.__name__ = name @@ -75,7 +88,7 @@ def _fixup_directive_names(self): if hasattr(func, "__qualname__"): func.__qualname__ = type(app_class).__name__ + "." + name - def get_action_classes(self): + def get_action_classes(self) -> dict[type[Action | Composite], str]: """Get all action classes registered for this app. This includes action classes registered for its base class. @@ -84,6 +97,7 @@ def get_action_classes(self): """ result = {} app_class = self.app_class + assert app_class is not None for name, method in app_class.get_directive_methods(): result[method.__func__.action_factory] = name @@ -94,7 +108,7 @@ def get_action_classes(self): result[action_class] = name return result - def setup(self): + def setup(self) -> None: """Set up config object and action groups. This happens during the start of the commit phase. @@ -113,21 +127,23 @@ def setup(self): self.delete_config(action_class) # now we create ActionGroup objects for each action class group + self._action_groups: dict[type[Action], ActionGroup] self._action_groups = d = {} # and we track what config factories we've seen for consistency # checking - self._factories_seen = {} + self._factories_seen: dict[str, Callable[..., Any]] = {} for action_class in grouped_action_classes: self.setup_config(action_class) d[action_class] = ActionGroup( action_class, self.action_extends(action_class) ) - def setup_config(self, action_class): + def setup_config(self, action_class: type[Action]) -> None: """Set up the config objects on the ``config`` attribute. :param action_class: the action subclass to setup config for. """ + assert self.app_class is not None # sort the items in order of creation items = topological_sort(action_class.config.items(), factory_key) # this introduces all dependencies, including those only @@ -153,7 +169,7 @@ def setup_config(self, action_class): ) setattr(config, name, factory(**kw)) - def delete_config(self, action_class): + def delete_config(self, action_class: type[Action]) -> None: """Delete config objects on the ``config`` attribute. :param action_class: the action class subclass to delete config for. @@ -169,7 +185,7 @@ def delete_config(self, action_class): if hasattr(config, name): delattr(config, name) - def group_actions(self): + def group_actions(self) -> None: """Groups actions for this configurable into action groups.""" # turn directives into actions actions = [ @@ -185,7 +201,9 @@ def group_actions(self): action_class = action.__class__ d[action_class].add(action, obj) - def get_action_group(self, action_class): + def get_action_group( + self, action_class: type[Action] + ) -> ActionGroup | None: """Return ActionGroup for ``action_class`` or ``None`` if not found. :param action_class: the action class to find the action group of. @@ -193,7 +211,7 @@ def get_action_group(self, action_class): """ return self._action_groups.get(action_class, None) - def action_extends(self, action_class): + def action_extends(self, action_class: type[Action]) -> list[ActionGroup]: """Get ActionGroup for action class in ``extends``. :param action_class: the action class @@ -207,8 +225,9 @@ def action_extends(self, action_class): for configurable in self.extends ] - def execute(self): + def execute(self) -> None: """Execute actions for configurable.""" + assert self.app_class is not None self.app_class.clean() self.setup() self.group_actions() @@ -226,7 +245,9 @@ class ActionGroup: indicate another action class to group with using ``group_class``. """ - def __init__(self, action_class, extends): + def __init__( + self, action_class: type[Action], extends: list[ActionGroup] + ) -> None: """ :param action_class: the action_class that identifies this action group. @@ -234,11 +255,11 @@ def __init__(self, action_class, extends): list of action groups extended by this action group. """ self.action_class = action_class - self._actions = [] - self._action_map = {} + self._actions: list[tuple[Action, Any]] = [] + self._action_map: dict[Any, tuple[Action, Any]] = {} self.extends = extends - def add(self, action, obj): + def add(self, action: Action, obj: Any) -> None: """Add an action and the object this action is to be performed on. :param action: an :class:`Action` instance. @@ -246,7 +267,7 @@ def add(self, action, obj): """ self._actions.append((action, obj)) - def prepare(self, configurable): + def prepare(self, configurable: Configurable) -> None: """Prepare the action group for a configurable. Detect any conflicts between actions. @@ -255,7 +276,7 @@ def prepare(self, configurable): :param configurable: The :class:`Configurable` option to prepare for. """ # check for conflicts and fill action map - discriminators = {} + discriminators: dict[Any, Action] = {} self._action_map = action_map = {} for action, obj in self._actions: @@ -273,7 +294,7 @@ def prepare(self, configurable): for extend in self.extends: self.combine(extend) - def get_actions(self): + def get_actions(self) -> list[tuple[Action, Any]]: """Get all actions registered for this action group. :return: list of action instances in registration order. @@ -282,7 +303,7 @@ def get_actions(self): result.sort(key=lambda value: value[0].order or 0) return result - def combine(self, actions): + def combine(self, actions: ActionGroup) -> None: """Combine another prepared actions with this one. Those configuration actions that would conflict are taken to @@ -297,7 +318,7 @@ def combine(self, actions): to_combine.update(self._action_map) self._action_map = to_combine - def execute(self, configurable): + def execute(self, configurable: Configurable) -> None: """Perform actions for configurable. :param configurable: the :class:`Configurable` instance to execute @@ -337,7 +358,7 @@ class Action(metaclass=abc.ABCMeta): same action class or actions with the same ``action_group``. """ - config = {} + config: ClassVar[dict[str, Callable[..., Any]]] = {} """Describe configuration. A dict mapping configuration names to factory functions. The @@ -360,7 +381,7 @@ class Action(metaclass=abc.ABCMeta): :meth:`Action.after`. """ - depends = [] + depends: list[type[Action]] = [] """List of other action classes to be executed before this one. @@ -372,7 +393,7 @@ class Action(metaclass=abc.ABCMeta): Omit if you don't care about the order. """ - group_class = None + group_class: type[Action] | None = None """Action class to group with. This class attribute can be supplied with the class of another @@ -384,7 +405,7 @@ class Action(metaclass=abc.ABCMeta): By default an action only groups with others of its same class. """ - filter_name = {} + filter_name: dict[str, str] = {} """Map of names used in query filter to attribute names. If for instance you want to be able to filter the attribute @@ -399,7 +420,7 @@ class Action(metaclass=abc.ABCMeta): same as the attribute name. """ - def filter_get_value(self, name): + def filter_get_value(self, name: str) -> Any | Sentinel: """A function to get the filter value. Takes two arguments, action and name. Should return the @@ -422,7 +443,7 @@ def filter_get_value(self, name): """ return NOT_FOUND - filter_compare = {} + filter_compare: dict[str, Callable[[Any, Any], bool]] = {} """Map of names used in query filter to comparison functions. If for instance you want to be able check whether the value of @@ -436,7 +457,7 @@ def filter_get_value(self, name): The default filter compare is an equality comparison. """ - filter_convert = {} + filter_convert: dict[str, Callable[[str], Any]] = {} """Map of names to convert functions. The query tool that can be generated for a Dectate-based @@ -454,15 +475,18 @@ def filter_get_value(self, name): """ # the directive that was used gets stored on the instance - directive = None + directive: Directive | None = None + + # the order gets stored on the instance + order: int # this is here to make update_wrapper work even when an __init__ # is not provided by the subclass - def __init__(self): + def __init__(self) -> None: pass @property - def code_info(self): + def code_info(self) -> CodeInfo | None: """Info about where in the source code the action was invoked. Is an instance of :class:`CodeInfo`. @@ -474,13 +498,13 @@ def code_info(self): return None return self.directive.code_info - def _log(self, configurable, obj): + def _log(self, configurable: Configurable, obj: Any) -> None: """Log this directive for configurable given configured obj.""" if self.directive is None: return self.directive.log(configurable, obj) - def get_value_for_filter(self, name): + def get_value_for_filter(self, name: str) -> Any | Sentinel: """Get value. Takes into account ``filter_name``, ``filter_get_value`` Used by the query system. You can override it if your action @@ -494,11 +518,11 @@ def get_value_for_filter(self, name): if value is not NOT_FOUND: return value if self.filter_get_value is None: - return value + return value # type: ignore[unreachable] return self.filter_get_value(name) @classmethod - def _get_config_kw(cls, configurable): + def _get_config_kw(cls, configurable: Configurable) -> dict[str, Any]: """Get the config objects set up for this configurable into a dict. This dict can then be passed as keyword parameters (using ``**``) @@ -521,78 +545,97 @@ def _get_config_kw(cls, configurable): result[name] = getattr(config, name) return result - @abc.abstractmethod - def identifier(self, **kw): - """Returns an immutable that uniquely identifies this config. + # NOTE: This is not ideal, since it reduces the information type + # checkers have about these methods on the base class, but + # if we don't do this, we will need to either deal with + # incompatible method override errors on the subclasses, when + # sticking to the intended way to define these methods in + # subclasses or change the signature of all implementations + # in order to get rid of the error. Neither is very ergonomic, + # so we can't ask the average Joe to do that. Ideally we + # could express the constraint on **kw through unpacking a + # TypeVar, that is bound to a TypedDict, however that is not + # supported, see https://github.com/python/typing/issues/1399 + if TYPE_CHECKING: + identifier: Any + discriminators: Any + perform: Any + before: Any + after: Any + else: - Needs to be implemented by the :class:`Action` subclass. + @abc.abstractmethod + def identifier(self, **kw: Any) -> Any: + """Returns an immutable that uniquely identifies this config. - Used for overrides and conflict detection. + Needs to be implemented by the :class:`Action` subclass. - If two actions in the same group have the same identifier in - the same configurable, those two actions are in conflict and a - :class:`ConflictError` is raised during :func:`commit`. + Used for overrides and conflict detection. - If an action in an extending configurable has the same - identifier as the configurable being extended, that action - overrides the original one in the extending configurable. + If two actions in the same group have the same identifier in + the same configurable, those two actions are in conflict and a + :class:`ConflictError` is raised during :func:`commit`. - :param ``**kw``: a dictionary of configuration objects as specified - by the ``config`` class attribute. - :return: an immutable value uniquely identifying this action. - """ + If an action in an extending configurable has the same + identifier as the configurable being extended, that action + overrides the original one in the extending configurable. - def discriminators(self, **kw): - """Returns an iterable of immutables to detect conflicts. + :param ``**kw``: a dictionary of configuration objects as specified + by the ``config`` class attribute. + :return: an immutable value uniquely identifying this action. + """ - Can be implemented by the :class:`Action` subclass. + def discriminators(self, **kw: Any) -> Iterable[Any]: + """Returns an iterable of immutables to detect conflicts. - Used for additional configuration conflict detection. + Can be implemented by the :class:`Action` subclass. - :param ``**kw``: a dictionary of configuration objects as specified - by the ``config`` class attribute. - :return: an iterable of immutable values. - """ - return [] + Used for additional configuration conflict detection. - @abc.abstractmethod - def perform(self, obj, **kw): - """Do whatever configuration is needed for ``obj``. + :param ``**kw``: a dictionary of configuration objects as specified + by the ``config`` class attribute. + :return: an iterable of immutable values. + """ + return [] - Needs to be implemented by the :class:`Action` subclass. + @abc.abstractmethod + def perform(self, obj: Any, **kw: Any) -> None: + """Do whatever configuration is needed for ``obj``. - Raise a :exc:`DirectiveError` to indicate that the action - cannot be performed due to incorrect configuration. + Needs to be implemented by the :class:`Action` subclass. - :param obj: the object that the action should be performed - for. Typically a function or a class object. - :param ``**kw``: a dictionary of configuration objects as specified - by the ``config`` class attribute. - """ + Raise a :exc:`DirectiveError` to indicate that the action + cannot be performed due to incorrect configuration. - @staticmethod - def before(**kw): - """Do setup just before actions in a group are performed. + :param obj: the object that the action should be performed + for. Typically a function or a class object. + :param ``**kw``: a dictionary of configuration objects as specified + by the ``config`` class attribute. + """ - Can be implemented as a static method by the :class:`Action` - subclass. + @staticmethod + def before(**kw: Any) -> None: + """Do setup just before actions in a group are performed. - :param ``**kw``: a dictionary of configuration objects as specified - by the ``config`` class attribute. - """ - pass + Can be implemented as a static method by the :class:`Action` + subclass. - @staticmethod - def after(**kw): - """Do setup just after actions in a group are performed. + :param ``**kw``: a dictionary of configuration objects as specified + by the ``config`` class attribute. + """ + pass - Can be implemented as a static method by the :class:`Action` - subclass. + @staticmethod + def after(**kw: Any) -> None: + """Do setup just after actions in a group are performed. - :param ``**kw``: a dictionary of configuration objects as specified - by the ``config`` class attribute. - """ - pass + Can be implemented as a static method by the :class:`Action` + subclass. + + :param ``**kw``: a dictionary of configuration objects as specified + by the ``config`` class attribute. + """ + pass class Composite(metaclass=abc.ABCMeta): @@ -604,7 +647,7 @@ class Composite(metaclass=abc.ABCMeta): method and return a iterable of actions in there. """ - query_classes = [] + query_classes: list[type[Action | Composite]] = [] """A list of actual action classes that this composite can generate. This is to allow the querying of composites. If the list if empty @@ -613,7 +656,7 @@ class Composite(metaclass=abc.ABCMeta): be generated in another way they are in the same query result. """ - filter_convert = {} + filter_convert: dict[str, Callable[[str], Any]] = {} """Map of names to convert functions. The query tool that can be generated for a Dectate-based @@ -630,13 +673,16 @@ class Composite(metaclass=abc.ABCMeta): :func:`convert_dotted_name`. """ + # the directive that was used gets stored on the instance + directive: Directive | None = None + # this is here to make update_wrapper work even when an __init__ # is not provided by the subclass - def __init__(self): + def __init__(self) -> None: pass @property - def code_info(self): + def code_info(self) -> CodeInfo | None: """Info about where in the source code the action was invoked. Is an instance of :class:`CodeInfo`. @@ -649,7 +695,7 @@ def code_info(self): return self.directive.code_info @abc.abstractmethod - def actions(self, obj): + def actions(self, obj: Any) -> Iterable[tuple[Action | Composite, Any]]: """Specify a iterable of actions to perform for ``obj``. The iteratable should yield ``action, obj`` tuples, @@ -675,7 +721,14 @@ class Directive: the directive was used for the purposes of error reporting. """ - def __init__(self, action_factory, code_info, app_class, args, kw): + def __init__( + self, + action_factory: type[Action | Composite], + code_info: CodeInfo, + app_class: type[App], + args: tuple[Any, ...], + kw: dict[str, Any], + ) -> None: """ :param action_factory: function that constructs an action instance. :code_info: a :class:`CodeInfo` instance describing where this @@ -694,10 +747,10 @@ def __init__(self, action_factory, code_info, app_class, args, kw): self.argument_info = (args, kw) @property - def directive_name(self): + def directive_name(self) -> str: return self.configurable._action_classes[self.action_factory] - def action(self): + def action(self) -> Action | Composite: """Get the :class:`Action` instance represented by this directive. :return: :class:`dectate.Action` instance. @@ -711,13 +764,18 @@ def action(self): result.directive = self return result - def __enter__(self): + def __enter__(self) -> DirectiveAbbreviation: return DirectiveAbbreviation(self) - def __exit__(self, type, value, tb): + def __exit__( + self, + type: type[BaseException] | None, + value: BaseException | None, + tb: TracebackType | None, + ) -> None: pass - def __call__(self, wrapped): + def __call__(self, wrapped: _T) -> _T: """Call with function or class to decorate. The decorated object is returned unchanged. @@ -728,13 +786,14 @@ def __call__(self, wrapped): self.configurable.register_directive(self, wrapped) return wrapped - def log(self, configurable, obj): + def log(self, configurable: Configurable, obj: Any) -> None: """Log this directive. :configurable: the configurable that this directive is logged for. :obj: the function or class object to that this directive is used on. """ + assert configurable.app_class is not None directive_name = self.directive_name logger = logging.getLogger( f"{configurable.app_class.logger_name}.{directive_name}" @@ -777,10 +836,10 @@ def log(self, configurable, obj): class DirectiveAbbreviation: """An abbreviated directive to be used with the ``with`` statement.""" - def __init__(self, directive): + def __init__(self, directive: Directive) -> None: self.directive = directive - def __call__(self, *args, **kw): + def __call__(self, *args: Any, **kw: Any) -> Directive: """Combine the args and kw from the directive with supplied ones.""" frame = sys._getframe(1) code_info = create_code_info(frame) @@ -797,8 +856,19 @@ def __call__(self, *args, **kw): kw=combined_kw, ) + def __enter__(self) -> DirectiveAbbreviation: + return self + + def __exit__( + self, + type: type[BaseException] | None, + value: BaseException | None, + tb: TracebackType | None, + ) -> None: + pass -def commit(*apps): + +def commit(*apps: type[App] | Configurable) -> None: """Commit one or more app classes A commit causes the configuration actions to be performed. The @@ -823,7 +893,9 @@ def commit(*apps): configurable.execute() -def sort_configurables(configurables): +def sort_configurables( + configurables: Iterable[Configurable], +) -> list[Configurable]: """Sort configurables topologically by ``extends``. :param configurables: an iterable of configurables to sort. @@ -832,7 +904,9 @@ def sort_configurables(configurables): return topological_sort(configurables, lambda c: c.extends) -def sort_action_classes(action_classes): +def sort_action_classes( + action_classes: Iterable[type[Action]], +) -> list[type[Action]]: """Sort action classes topologically by depends. :param action_classes: iterable of :class:`Action` subclasses @@ -842,7 +916,9 @@ class objects. return topological_sort(action_classes, lambda c: c.depends) -def group_action_classes(action_classes): +def group_action_classes( + action_classes: Iterable[type[Action | Composite]], +) -> set[type[Action]]: """Group action classes by ``group_class``. :param action_classes: iterable of action classes @@ -881,7 +957,9 @@ def group_action_classes(action_classes): return result -def expand_actions(actions): +def expand_actions( + actions: Iterable[tuple[Action | Composite, Any]], +) -> Iterator[tuple[Action, Any]]: """Expand any :class:`Composite` instances into :class:`Action` instances. Expansion is recursive; composites that return composites are expanded @@ -923,34 +1001,33 @@ class CodeInfo: did the invocation. """ - def __init__(self, path, lineno, sourceline): + def __init__(self, path: str, lineno: int, sourceline: str | None) -> None: self.path = path self.lineno = lineno self.sourceline = sourceline - def filelineno(self): + def filelineno(self) -> str: return f'File "{self.path}", line {self.lineno}' -def create_code_info(frame): +def create_code_info(frame: FrameType) -> CodeInfo: """Return code information about a frame. Returns a :class:`CodeInfo` instance. """ frameinfo = inspect.getframeinfo(frame) - - try: - sourceline = frameinfo.code_context[0].strip() - except Exception: + if frameinfo.code_context: + sourceline: str | None = frameinfo.code_context[0].strip() + else: # if no source file exists, e.g., due to eval - sourceline = frameinfo.code_context + sourceline = None return CodeInfo( path=frameinfo.filename, lineno=frameinfo.lineno, sourceline=sourceline ) -def factory_key(item): +def factory_key(item: tuple[str, _F]) -> Iterable[tuple[str, _F]]: """Helper for topological sort of factories. :param item: a ``name, factory`` tuple to generate the key for. @@ -961,10 +1038,15 @@ def factory_key(item): arguments = getattr(factory, "factory_arguments", None) if arguments is None: return [] - return arguments.items() + return arguments.items() # type: ignore[no-any-return] -def get_factory_arguments(action_class, config, factory, app_class): +def get_factory_arguments( + action_class: type[Action], + config: Config, + factory: Callable[..., Any], + app_class: type[App], +) -> dict[str, Any]: """Get arguments needed to construct factory. Factories can define a ``factory_arguments`` attribute to control @@ -983,7 +1065,7 @@ def get_factory_arguments(action_class, config, factory, app_class): arguments = getattr(factory, "factory_arguments", None) app_class_arg = getattr(factory, "app_class_arg", False) - result = {} + result: dict[str, Any] = {} if app_class_arg: result["app_class"] = app_class @@ -1004,7 +1086,7 @@ def get_factory_arguments(action_class, config, factory, app_class): return result -def dotted_name(cls): +def dotted_name(cls: type) -> str: """Dotted name for a class. Example: ``my.module.MyClass``. diff --git a/dectate/error.py b/dectate/error.py index 5f08a47..2ee1196 100644 --- a/dectate/error.py +++ b/dectate/error.py @@ -1,11 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dectate.config import Action, CodeInfo + + class ConfigError(Exception): """Raised when configuration is bad.""" -def conflict_keyfunc(action): +def conflict_keyfunc(action: Action) -> tuple[str, int]: code_info = action.code_info if code_info is None: - return 0 + return ("", 0) return (code_info.path, code_info.lineno) @@ -15,7 +23,7 @@ class ConflictError(ConfigError): Describes where in the code directives are in conflict. """ - def __init__(self, actions): + def __init__(self, actions: list[Action]) -> None: actions.sort(key=conflict_keyfunc) self.actions = actions result = ["Conflict between:"] @@ -35,7 +43,7 @@ class DirectiveReportError(ConfigError): Describes where in the code the problem occurred. """ - def __init__(self, message, code_info): + def __init__(self, message: str, code_info: CodeInfo | None) -> None: result = [message] if code_info is not None: result.append(" %s" % code_info.filelineno()) @@ -56,8 +64,6 @@ class DirectiveError(ConfigError): :exc:`DirectiveReportError`. """ - pass - class TopologicalSortError(ValueError): """Raised if dependencies cannot be sorted topologically. diff --git a/dectate/py.typed b/dectate/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/dectate/query.py b/dectate/query.py index 85f442d..141969e 100644 --- a/dectate/query.py +++ b/dectate/query.py @@ -1,9 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar from .config import Composite from .error import QueryError +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Sequence + from .app import App + from .config import Action, Configurable + + +_T_co = TypeVar("_T_co", covariant=True) -class Callable: - def __call__(self, app_class): + +class Callable(Generic[_T_co]): + def __call__(self, app_class: type[App] | App) -> Iterable[_T_co]: """Execute the query against an app class. :param app_class: a :class:`App` subclass to execute the query @@ -14,9 +25,13 @@ def __call__(self, app_class): """ return self.execute(app_class.dectate) + # NOTE: forward declare required subclass method + def execute(self, configurable: Configurable) -> Iterable[_T_co]: + raise NotImplementedError + -class Base(Callable): - def filter(self, **kw): +class Base(Callable[tuple["Action", Any]]): + def filter(self, **kw: Any) -> Filter: """Filter this query by keyword arguments. The keyword arguments are matched with attributes on the @@ -36,7 +51,7 @@ def filter(self, **kw): """ return Filter(self, **kw) - def attrs(self, *names): + def attrs(self, *names: str) -> Attrs: """Extract attributes from resulting actions. The list of attribute names indicates which keys to include in @@ -49,7 +64,7 @@ def attrs(self, *names): """ return Attrs(self, names) - def obj(self): + def obj(self) -> Obj: """Get objects from results. Throws away actions in the results and return an iterable of objects. @@ -71,11 +86,14 @@ class Query(Base): are looked up on the app class before execution. """ - def __init__(self, *action_classes): + def __init__(self, *action_classes: type[Action | Composite] | str) -> None: self.action_classes = action_classes - def execute(self, configurable): + def execute( + self, configurable: Configurable + ) -> Iterator[tuple[Action, Any]]: app_class = configurable.app_class + assert app_class is not None action_classes = [] for action_class in self.action_classes: if isinstance(action_class, str): @@ -84,7 +102,9 @@ def execute(self, configurable): return query_action_classes(configurable, action_classes) -def expand_action_classes(action_classes): +def expand_action_classes( + action_classes: Iterable[type[Action | Composite]], +) -> set[type[Action]]: result = set() for action_class in action_classes: if issubclass(action_class, Composite): @@ -105,7 +125,10 @@ def expand_action_classes(action_classes): return result -def query_action_classes(configurable, action_classes): +def query_action_classes( + configurable: Configurable, + action_classes: Iterable[type[Action | Composite]], +) -> Iterator[tuple[Action, Any]]: for action_class in expand_action_classes(action_classes): action_group = configurable.get_action_group(action_class) if action_group is None: @@ -116,7 +139,9 @@ def query_action_classes(configurable, action_classes): yield from action_group.get_actions() -def get_action_class(app_class, directive_name): +def get_action_class( + app_class: type[App] | App, directive_name: str +) -> type[Action | Composite]: directive_method = getattr(app_class, directive_name, None) if directive_method is None: raise QueryError( @@ -128,19 +153,21 @@ def get_action_class(app_class, directive_name): raise QueryError( f"{directive_name!r} on {app_class!r} is not a directive" ) - return action_class + return action_class # type: ignore[no-any-return] -def compare_equality(compared, value): +def compare_equality(compared: object, value: object) -> bool: return compared == value class Filter(Base): - def __init__(self, query, **kw): + def __init__(self, query: Base, **kw: Any) -> None: self.query = query self.kw = kw - def execute(self, configurable): + def execute( + self, configurable: Configurable + ) -> Iterator[tuple[Action, Any]]: for action, obj in self.query.execute(configurable): for name, value in sorted(self.kw.items()): compared = action.get_value_for_filter(name) @@ -151,12 +178,12 @@ def execute(self, configurable): yield action, obj -class Attrs(Callable): - def __init__(self, query, names): +class Attrs(Callable[dict[str, Any]]): + def __init__(self, query: Base, names: Sequence[str]) -> None: self.query = query self.names = names - def execute(self, configurable): + def execute(self, configurable: Configurable) -> Iterator[dict[str, Any]]: for action, obj in self.query.execute(configurable): attrs = {} for name in self.names: @@ -164,10 +191,10 @@ def execute(self, configurable): yield attrs -class Obj(Callable): - def __init__(self, query): +class Obj(Callable[Any]): + def __init__(self, query: Base) -> None: self.query = query - def execute(self, configurable): + def execute(self, configurable: Configurable) -> Iterator[Any]: for action, obj in self.query.execute(configurable): yield obj diff --git a/dectate/sentinel.py b/dectate/sentinel.py index 602cc4d..5d7982e 100644 --- a/dectate/sentinel.py +++ b/dectate/sentinel.py @@ -1,8 +1,11 @@ +from __future__ import annotations + + class Sentinel: - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def __repr__(self): + def __repr__(self) -> str: return "<%s>" % self.name diff --git a/dectate/tests/test_directive.py b/dectate/tests/test_directive.py index 009ac95..07f4d77 100644 --- a/dectate/tests/test_directive.py +++ b/dectate/tests/test_directive.py @@ -1,28 +1,35 @@ +from __future__ import annotations + +import pytest + +from typing import TYPE_CHECKING, Any + from dectate.app import App, directive from dectate.config import commit, Action, Composite from dectate.error import ConflictError, ConfigError -import pytest +if TYPE_CHECKING: + from collections.abc import Callable, Generator -def test_simple(): +def test_simple() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -30,23 +37,25 @@ def f(): assert MyApp.config.my == [("hello", f)] -def test_decorator(): +def test_decorator() -> None: + # NOTE: This style is not supported by mypy, since class decorators + # currently cannot change the type of the attribute. class MyApp(App): @directive class foo(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) - @MyApp.foo("hello") - def f(): + @MyApp.foo("hello") # type: ignore[operator] + def f() -> None: pass commit(MyApp) @@ -54,24 +63,24 @@ def f(): assert MyApp.config.my == [("hello", f)] -def test_commit_method(): +def test_commit_method() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass result = MyApp.commit() @@ -80,73 +89,76 @@ def f(): assert list(result) == [MyApp] -def test_directive_name(): +def test_directive_name() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[MyDirective]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[MyDirective]) -> None: my.append(self) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass MyApp.commit() - MyApp.config.my[0].directive.directive_name == "foo" + MyApp.config.my[ + 0 + ].directive.directive_name == "foo" # pyright: ignore[reportUnusedExpression] -def test_conflict_same_directive(): +def test_conflict_same_directive() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass @MyApp.foo("hello") - def f2(): + def f2() -> None: pass with pytest.raises(ConflictError): commit(MyApp) -def test_app_inherit(): +def test_app_inherit() -> None: class Registry: - pass + message: str + obj: Any class MyDirective(Action): config = {"my": Registry} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.message = self.message my.obj = obj @@ -157,7 +169,7 @@ class SubApp(MyApp): pass @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp, SubApp) @@ -168,20 +180,21 @@ def f(): assert SubApp.config.my.obj is f -def test_app_override(): +def test_app_override() -> None: class Registry: - pass + message: str + obj: Any class MyDirective(Action): config = {"my": Registry} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.message = self.message my.obj = obj @@ -192,11 +205,11 @@ class SubApp(MyApp): pass @MyApp.foo("hello") - def f(): + def f() -> None: pass @SubApp.foo("hello") - def f2(): + def f2() -> None: pass commit(MyApp, SubApp) @@ -207,29 +220,33 @@ def f2(): assert SubApp.config.my.obj is f2 -def test_different_group_no_conflict(): +def test_different_group_no_conflict() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform( + self, obj: Callable[..., Any], foo: list[tuple[str, Any]] + ) -> None: foo.append((self.message, obj)) class BarDirective(Action): config = {"bar": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, bar): + def identifier(self, bar: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, bar): + def perform( + self, obj: Callable[..., Any], bar: list[tuple[str, Any]] + ) -> None: bar.append((self.message, obj)) class MyApp(App): @@ -237,11 +254,11 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass @MyApp.bar("hello") - def g(): + def g() -> None: pass commit(MyApp) @@ -250,30 +267,34 @@ def g(): assert MyApp.config.bar == [("hello", g)] -def test_same_group_conflict(): +def test_same_group_conflict() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform( + self, obj: Callable[..., Any], foo: list[tuple[str, Any]] + ) -> None: foo.append((self.message, obj)) class BarDirective(Action): # should now conflict group_class = FooDirective - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform( + self, obj: Callable[..., Any], foo: list[tuple[str, Any]] + ) -> None: foo.append((self.message, obj)) class MyApp(App): @@ -281,64 +302,64 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass @MyApp.bar("hello") - def g(): + def g() -> None: pass with pytest.raises(ConflictError): commit(MyApp) -def test_discriminator_conflict(): +def test_discriminator_conflict() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message, others): + def __init__(self, message: str, others: list[str]) -> None: self.message = message self.others = others - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def discriminators(self, my): + def discriminators(self, my: list[tuple[str, Any]]) -> list[str]: return self.others - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(FooDirective) @MyApp.foo("f", ["a"]) - def f(): + def f() -> None: pass @MyApp.foo("g", ["a", "b"]) - def g(): + def g() -> None: pass with pytest.raises(ConflictError): commit(MyApp) -def test_discriminator_same_group_conflict(): +def test_discriminator_same_group_conflict() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message, others): + def __init__(self, message: str, others: list[str]) -> None: self.message = message self.others = others - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def discriminators(self, my): + def discriminators(self, my: list[tuple[str, Any]]) -> list[str]: return self.others - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class BarDirective(FooDirective): @@ -349,43 +370,43 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.foo("f", ["a"]) - def f(): + def f() -> None: pass @MyApp.bar("g", ["a", "b"]) - def g(): + def g() -> None: pass with pytest.raises(ConflictError): commit(MyApp) -def test_discriminator_no_conflict(): +def test_discriminator_no_conflict() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message, others): + def __init__(self, message: str, others: list[str]) -> None: self.message = message self.others = others - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def discriminators(self, my): + def discriminators(self, my: list[tuple[str, Any]]) -> list[str]: return self.others - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(FooDirective) @MyApp.foo("f", ["a"]) - def f(): + def f() -> None: pass @MyApp.foo("g", ["b"]) - def g(): + def g() -> None: pass commit(MyApp) @@ -393,21 +414,21 @@ def g(): assert MyApp.config.my == [("f", f), ("g", g)] -def test_discriminator_different_group_no_conflict(): +def test_discriminator_different_group_no_conflict() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message, others): + def __init__(self, message: str, others: list[str]) -> None: self.message = message self.others = others - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def discriminators(self, my): + def discriminators(self, my: list[tuple[str, Any]]) -> list[str]: return self.others - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class BarDirective(FooDirective): @@ -419,11 +440,11 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.foo("f", ["a"]) - def f(): + def f() -> None: pass @MyApp.bar("g", ["a", "b"]) - def g(): + def g() -> None: pass commit(MyApp) @@ -431,17 +452,17 @@ def g(): assert MyApp.config.my == [("f", f), ("g", g)] -def test_depends(): +def test_depends() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class BarDirective(Action): @@ -449,13 +470,13 @@ class BarDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -463,11 +484,11 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.bar("a") - def g(): + def g() -> None: pass @MyApp.foo("b") - def f(): + def f() -> None: pass commit(MyApp) @@ -476,24 +497,24 @@ def f(): assert MyApp.config.my == [("b", f), ("a", g)] -def test_composite(): +def test_composite() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), obj) for message in self.messages] class MyApp(App): @@ -501,7 +522,7 @@ class MyApp(App): composite = directive(CompositeDirective) @MyApp.composite(["a", "b", "c"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -509,27 +530,27 @@ def f(): assert MyApp.config.my == [("a", f), ("b", f), ("c", f)] -def test_composite_change_object(): +def test_composite_change_object() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) - def other(): + def other() -> None: pass class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), other) for message in self.messages] class MyApp(App): @@ -537,7 +558,7 @@ class MyApp(App): composite = directive(CompositeDirective) @MyApp.composite(["a", "b", "c"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -545,24 +566,24 @@ def f(): assert MyApp.config.my == [("a", other), ("b", other), ("c", other)] -def test_composite_private_sub(): +def test_composite_private_sub() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), obj) for message in self.messages] class MyApp(App): @@ -571,7 +592,7 @@ class MyApp(App): composite = directive(CompositeDirective) @MyApp.composite(["a", "b", "c"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -579,24 +600,24 @@ def f(): assert MyApp.config.my == [("a", f), ("b", f), ("c", f)] -def test_composite_private_composite(): +def test_composite_private_composite() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), obj) for message in self.messages] class MyApp(App): @@ -604,7 +625,7 @@ class MyApp(App): _composite = directive(CompositeDirective) @MyApp.sub("a") - def f(): + def f() -> None: pass commit(MyApp) @@ -612,32 +633,32 @@ def f(): assert MyApp.config.my == [("a", f)] -def test_nested_composite(): +def test_nested_composite() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class SubCompositeDirective(Composite): - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def actions(self, obj): + def actions(self, obj: Any) -> Generator[tuple[SubDirective, Any]]: yield SubDirective(self.message + "_0"), obj yield SubDirective(self.message + "_1"), obj class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubCompositeDirective, Any]]: return [ (SubCompositeDirective(message), obj) for message in self.messages @@ -649,7 +670,7 @@ class MyApp(App): composite = directive(CompositeDirective) @MyApp.composite(["a", "b", "c"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -665,18 +686,22 @@ def f(): ] -def test_with_statement_kw(): +def test_with_statement_kw() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, model, name): + def __init__(self, model: type[Any], name: str) -> None: self.model = model self.name = name - def identifier(self, my): + def identifier( + self, my: list[tuple[type[Any], str, Any]] + ) -> tuple[type[Any], str]: return (self.model, self.name) - def perform(self, obj, my): + def perform( + self, obj: Any, my: list[tuple[type[Any], str, Any]] + ) -> None: my.append((self.model, self.name, obj)) class Dummy: @@ -685,14 +710,21 @@ class Dummy: class MyApp(App): foo = directive(FooDirective) - with MyApp.foo(model=Dummy) as foo: + # NOTE: This is another use-case that's not well supported by + # type checkers. This would require some kind of partial + # type transform, so we're allowed to omit required arguments + # For now this will require either providing a default for + # those parameters or ignoring the type error. This seems + # still better than completely erasing the signature of the + # directive. We instead provide a new helper attribute partial. + with MyApp.foo(model=Dummy) as foo: # type: ignore @foo(name="a") - def f(): + def f() -> None: pass @foo(name="b") - def g(): + def g() -> None: pass commit(MyApp) @@ -703,18 +735,22 @@ def g(): ] -def test_with_statement_args(): +def test_with_statement_args() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, model, name): + def __init__(self, model: type[Any], name: str) -> None: self.model = model self.name = name - def identifier(self, my): + def identifier( + self, my: list[tuple[type[Any], str, Any]] + ) -> tuple[type[Any], str]: return (self.model, self.name) - def perform(self, obj, my): + def perform( + self, obj: Any, my: list[tuple[type[Any], str, Any]] + ) -> None: my.append((self.model, self.name, obj)) class MyApp(App): @@ -723,14 +759,56 @@ class MyApp(App): class Dummy: pass - with MyApp.foo(Dummy) as foo: + with MyApp.foo(Dummy) as foo: # type: ignore[call-arg] @foo("a") - def f(): + def f() -> None: pass @foo("b") - def g(): + def g() -> None: + pass + + commit(MyApp) + + assert MyApp.config.my == [ + (Dummy, "a", f), + (Dummy, "b", g), + ] + + +def test_partial_with_statement_kw() -> None: + class FooDirective(Action): + config = {"my": list} + + def __init__(self, model: type[Any], name: str) -> None: + self.model = model + self.name = name + + def identifier( + self, my: list[tuple[type[Any], str, Any]] + ) -> tuple[type[Any], str]: + return (self.model, self.name) + + def perform( + self, obj: Any, my: list[tuple[type[Any], str, Any]] + ) -> None: + my.append((self.model, self.name, obj)) + + class Dummy: + pass + + class MyApp(App): + foo = directive(FooDirective) + + with MyApp.foo.partial(model=Dummy) as foo: + + @foo(name="a") + def f() -> None: + pass + + @foo(name="b") + def g() -> None: pass commit(MyApp) @@ -741,37 +819,79 @@ def g(): ] -def test_before(): +def test_partial_with_statement_args() -> None: + class FooDirective(Action): + config = {"my": list} + + def __init__(self, model: type[Any], name: str) -> None: + self.model = model + self.name = name + + def identifier( + self, my: list[tuple[type[Any], str, Any]] + ) -> tuple[type[Any], str]: + return (self.model, self.name) + + def perform( + self, obj: Any, my: list[tuple[type[Any], str, Any]] + ) -> None: + my.append((self.model, self.name, obj)) + + class MyApp(App): + foo = directive(FooDirective) + + class Dummy: + pass + + with MyApp.foo.partial(Dummy) as foo: + + @foo("a") + def f() -> None: + pass + + @foo("b") + def g() -> None: + pass + + commit(MyApp) + + assert MyApp.config.my == [ + (Dummy, "a", f), + (Dummy, "b", g), + ] + + +def test_before() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.before = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert self.before self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def before(my): + def before(my: Registry) -> None: my.before = True class MyApp(App): foo = directive(FooDirective) @MyApp.foo(name="hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -782,30 +902,30 @@ def f(): ] -def test_before_without_use(): +def test_before_without_use() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.before = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert self.before self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def before(my): + def before(my: Registry) -> None: my.before = True class MyApp(App): @@ -817,42 +937,42 @@ class MyApp(App): assert MyApp.config.my.li == [] -def test_before_group(): +def test_before_group() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.before = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert self.before self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def before(my): + def before(my: Registry) -> None: my.before = True class BarDirective(Action): group_class = FooDirective - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: pass class MyApp(App): @@ -860,11 +980,11 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.bar(name="bye") - def f(): + def f() -> None: pass @MyApp.foo(name="hello") - def g(): + def g() -> None: pass commit(MyApp) @@ -875,29 +995,29 @@ def g(): ] -def test_config_group(): +def test_config_group() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.name, obj)) class BarDirective(Action): group_class = FooDirective - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.name, obj)) class MyApp(App): @@ -905,11 +1025,11 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.bar(name="bye") - def f(): + def f() -> None: pass @MyApp.foo(name="hello") - def g(): + def g() -> None: pass commit(MyApp) @@ -920,42 +1040,42 @@ def g(): ] -def test_before_group_without_use(): +def test_before_group_without_use() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.before = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert self.before self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def before(my): + def before(my: Registry) -> None: my.before = True class BarDirective(Action): group_class = FooDirective - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): @@ -968,37 +1088,37 @@ class MyApp(App): assert MyApp.config.my.li == [] -def test_after(): +def test_after() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.after = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert not self.after self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def after(my): + def after(my: Registry) -> None: my.after = True class MyApp(App): foo = directive(FooDirective) @MyApp.foo(name="hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1009,30 +1129,30 @@ def f(): ] -def test_after_without_use(): +def test_after_without_use() -> None: class Registry: - def __init__(self): - self.li = [] + def __init__(self) -> None: + self.li: list[tuple[str, Any]] = [] self.after = False - def add(self, name, obj): + def add(self, name: str, obj: Any) -> None: assert not self.after self.li.append((name, obj)) class FooDirective(Action): config = {"my": Registry} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, my): + def identifier(self, my: Registry) -> str: return self.name - def perform(self, obj, my): + def perform(self, obj: Any, my: Registry) -> None: my.add(self.name, obj) @staticmethod - def after(my): + def after(my: Registry) -> None: my.after = True class MyApp(App): @@ -1044,17 +1164,17 @@ class MyApp(App): assert MyApp.config.my.li == [] -def test_action_loop_should_conflict(): +def test_action_loop_should_conflict() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -1063,34 +1183,34 @@ class MyApp(App): for i in range(2): @MyApp.foo("hello") - def f(): + def f() -> None: pass with pytest.raises(ConflictError): commit(MyApp) -def test_action_init_only_during_commit(): +def test_action_init_only_during_commit() -> None: init_called = [] class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: init_called.append("there") self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass assert init_called == [] @@ -1100,17 +1220,17 @@ def f(): assert init_called == ["there"] -def test_registry_should_exist_even_without_directive_use(): +def test_registry_should_exist_even_without_directive_use() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -1121,17 +1241,17 @@ class MyApp(App): assert MyApp.config.my == [] -def test_registry_should_exist_even_without_directive_use_subclass(): +def test_registry_should_exist_even_without_directive_use_subclass() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -1146,24 +1266,24 @@ class SubApp(MyApp): assert SubApp.config.my == [] -def test_rerun_commit(): +def test_rerun_commit() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1174,30 +1294,30 @@ def f(): assert MyApp.config.my == [("hello", f)] -def test_rerun_commit_add_directive(): +def test_rerun_commit_add_directive() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @MyApp.foo("bye") - def g(): + def g() -> None: pass # and again @@ -1206,17 +1326,17 @@ def g(): assert MyApp.config.my == [("hello", f), ("bye", g)] -def test_order_subclass(): +def test_order_subclass() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -1226,15 +1346,15 @@ class SubApp(MyApp): pass @SubApp.foo("c") - def h(): + def h() -> None: pass @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp, SubApp) @@ -1242,30 +1362,32 @@ def g(): assert SubApp.config.my == [("a", f), ("b", g), ("c", h)] -def test_registry_single_factory_argument(): +def test_registry_single_factory_argument() -> None: class Other: factory_arguments = {"my": list} - def __init__(self, my): + def __init__(self, my: list[tuple[str, Any]]) -> None: self.my = my class MyDirective(Action): config = {"my": list, "other": Other} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my, other): + def identifier(self, my: list[tuple[str, Any]], other: Other) -> str: return self.message - def perform(self, obj, my, other): + def perform( + self, obj: Any, my: list[tuple[str, Any]], other: Other + ) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1273,30 +1395,30 @@ def f(): assert MyApp.config.other.my == [("hello", f)] -def test_registry_factory_argument_introduces_new_registry(): +def test_registry_factory_argument_introduces_new_registry() -> None: class Other: factory_arguments = {"my": list} - def __init__(self, my): + def __init__(self, my: list[tuple[str, Any]]) -> None: self.my = my class MyDirective(Action): config = {"other": Other} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, other): + def identifier(self, other: Other) -> str: return self.message - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: other.my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1305,26 +1427,26 @@ def f(): assert MyApp.config.my is MyApp.config.other.my -def test_registry_factory_argument_introduces_new_registry_subclass(): +def test_registry_factory_argument_introduces_new_registry_subclass() -> None: class IsUsedElsewhere: poked = False class Other: factory_arguments = {"my": IsUsedElsewhere} - def __init__(self, my): + def __init__(self, my: IsUsedElsewhere) -> None: self.my = my class MyDirective(Action): config = {"other": Other} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, other): + def identifier(self, other: Other) -> str: return self.message - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: assert not other.my.poked other.my.poked = True @@ -1335,7 +1457,7 @@ class SubApp(MyApp): pass @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1346,24 +1468,32 @@ def f(): commit(SubApp) -def test_registry_multiple_factory_arguments(): +def test_registry_multiple_factory_arguments() -> None: class Other: factory_arguments = {"my": list, "my2": list} - def __init__(self, my, my2): + def __init__(self, my: list[tuple[str, Any]], my2: list[str]) -> None: self.my = my self.my2 = my2 class MyDirective(Action): config = {"my": list, "my2": list, "other": Other} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my, my2, other): + def identifier( + self, my: list[tuple[str, Any]], my2: list[str], other: Other + ) -> str: return self.message - def perform(self, obj, my, my2, other): + def perform( + self, + obj: Any, + my: list[tuple[str, Any]], + my2: list[str], + other: Other, + ) -> None: my.append((self.message, obj)) my2.append("blah") @@ -1371,7 +1501,7 @@ class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1380,23 +1510,23 @@ def f(): assert MyApp.config.other.my2 == ["blah"] -def test_registry_factory_arguments_depends(): +def test_registry_factory_arguments_depends() -> None: class Other: factory_arguments = {"my": list} - def __init__(self, my): + def __init__(self, my: list[tuple[str, Any]]) -> None: self.my = my class FooDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class BarDirective(Action): @@ -1404,13 +1534,13 @@ class BarDirective(Action): depends = [FooDirective] - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, other): + def identifier(self, other: Other) -> str: return self.name - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: pass class MyApp(App): @@ -1418,7 +1548,7 @@ class MyApp(App): bar = directive(BarDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -1426,24 +1556,43 @@ def f(): assert MyApp.config.other.my == [("hello", f)] -def test_registry_factory_arguments_depends_complex(): +def test_registry_factory_arguments_depends_complex() -> None: class Registry: pass class PredicateRegistry: factory_arguments = {"registry": Registry} - def __init__(self, registry): + def __init__(self, registry: Registry) -> None: self.registry = registry class SettingAction(Action): config = {"registry": Registry} + # NOTE: mypy will complain about SettingAction being abstract + # without these overrides, which is correct, but not + # relevant for this test. + if TYPE_CHECKING: + + def identifier(self, **kw: Any) -> Any: + pass + + def perform(self, obj: Any, **kw: Any) -> None: + pass + class PredicateAction(Action): config = {"predicate_registry": PredicateRegistry} depends = [SettingAction] + if TYPE_CHECKING: + + def identifier(self, **kw: Any) -> Any: + pass + + def perform(self, obj: Any, **kw: Any) -> None: + pass + class ViewAction(Action): config = {"registry": Registry} @@ -1459,7 +1608,7 @@ class MyApp(App): assert MyApp.config.registry is MyApp.config.predicate_registry.registry -def test_is_committed(): +def test_is_committed() -> None: class MyApp(App): pass @@ -1470,29 +1619,29 @@ class MyApp(App): assert MyApp.is_committed() -def test_registry_config_inconsistent(): +def test_registry_config_inconsistent() -> None: class FooDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class BarDirective(Action): config = {"my": dict} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: dict[str, Any]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: dict[str, Any]) -> None: my[self.message] = obj class MyApp(App): @@ -1503,29 +1652,31 @@ class MyApp(App): commit(MyApp) -def test_registry_factory_argument_inconsistent(): +def test_registry_factory_argument_inconsistent() -> None: class Other: factory_arguments = {"my": list} - def __init__(self, my): + def __init__(self, my: list[Any]) -> None: self.my = my class YetAnother: factory_arguments = {"my": dict} - def __init__(self, my): + def __init__(self, my: dict[str, Any]) -> None: self.my = my class MyDirective(Action): config = {"other": Other, "yetanother": YetAnother} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, other, yetanother): + def identifier(self, other: Other, yetanother: YetAnother) -> str: return self.message - def perform(self, obj, other, yetanother): + def perform( + self, obj: Any, other: Other, yetanother: YetAnother + ) -> None: pass class MyApp(App): @@ -1535,23 +1686,25 @@ class MyApp(App): commit(MyApp) -def test_registry_factory_argument_and_config_inconsistent(): +def test_registry_factory_argument_and_config_inconsistent() -> None: class Other: factory_arguments = {"my": dict} - def __init__(self, my): + def __init__(self, my: dict[str, Any]) -> None: self.my = my class MyDirective(Action): config = {"my": list, "other": Other} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my, other): + def identifier(self, my: list[tuple[str, Any]], other: Other) -> str: return self.message - def perform(self, obj, my, other): + def perform( + self, obj: Any, my: list[tuple[str, Any]], other: Other + ) -> None: my.append((self.message, obj)) class MyApp(App): @@ -1568,13 +1721,13 @@ class ReprDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) @@ -1582,7 +1735,7 @@ class MyAppForRepr(App): foo = directive(ReprDirective) -def test_directive_repr(): +def test_directive_repr() -> None: MyAppForRepr.commit() assert repr(MyAppForRepr.foo) == ( @@ -1591,32 +1744,36 @@ def test_directive_repr(): ) -def test_app_class_passed_into_action(): +def test_app_class_passed_into_action() -> None: class MyDirective(Action): config = {"my": list} app_class_arg = True - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, app_class, my): + def identifier( + self, app_class: type[MyApp], my: list[tuple[str, Any]] + ) -> str: return self.message - def perform(self, obj, app_class, my): + def perform( + self, obj: Any, app_class: type[MyApp], my: list[tuple[str, Any]] + ) -> None: app_class.touched.append(None) my.append((self.message, obj)) class MyApp(App): - touched = [] + touched: list[None] = [] foo = directive(MyDirective) class SubApp(MyApp): - touched = [] + touched: list[None] = [] @MyApp.foo("hello") - def f(): + def f() -> None: pass assert not MyApp.touched @@ -1633,38 +1790,43 @@ def f(): assert SubApp.touched == [None] -def test_app_class_passed_into_factory(): +def test_app_class_passed_into_factory() -> None: + # NOTE: mypy grabs the wrong version of MyApp if we don't define + # it before it's used + class BaseApp(App): + touched: bool + class Other: factory_arguments = {"my": list} app_class_arg = True - def __init__(self, my, app_class): + def __init__(self, my: list[Any], app_class: type[BaseApp]) -> None: self.my = my self.app_class = app_class - def touch(self): + def touch(self) -> None: self.app_class.touched = True class MyDirective(Action): config = {"other": Other} - def __init__(self): + def __init__(self) -> None: pass - def identifier(self, other): + def identifier(self, other: Other) -> tuple[()]: return () - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: other.touch() - class MyApp(App): + class MyApp(BaseApp): touched = False foo = directive(MyDirective) @MyApp.foo() - def f(): + def f() -> None: pass assert not MyApp.touched @@ -1674,35 +1836,40 @@ def f(): assert MyApp.touched -def test_app_class_passed_into_factory_no_factory_arguments(): +def test_app_class_passed_into_factory_no_factory_arguments() -> None: + # NOTE: mypy grabs the wrong version of MyApp if we don't define + # it before it's used + class BaseApp(App): + touched: bool + class Other: app_class_arg = True - def __init__(self, app_class): + def __init__(self, app_class: type[BaseApp]) -> None: self.app_class = app_class - def touch(self): + def touch(self) -> None: self.app_class.touched = True class MyDirective(Action): config = {"other": Other} - def __init__(self): + def __init__(self) -> None: pass - def identifier(self, other): + def identifier(self, other: Other) -> tuple[()]: return () - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: other.touch() - class MyApp(App): + class MyApp(BaseApp): touched = False foo = directive(MyDirective) @MyApp.foo() - def f(): + def f() -> None: pass assert not MyApp.touched @@ -1712,32 +1879,37 @@ def f(): assert MyApp.touched -def test_app_class_passed_into_factory_separation(): +def test_app_class_passed_into_factory_separation() -> None: + # NOTE: mypy grabs the wrong version of MyApp if we don't define + # it before it's used + class BaseApp(App): + touched: bool + class Other: factory_arguments = {"my": list} app_class_arg = True - def __init__(self, my, app_class): + def __init__(self, my: list[Any], app_class: type[BaseApp]) -> None: self.my = my self.app_class = app_class - def touch(self): + def touch(self) -> None: self.app_class.touched = True class MyDirective(Action): config = {"other": Other} - def __init__(self): + def __init__(self) -> None: pass - def identifier(self, other): + def identifier(self, other: Other) -> tuple[()]: return () - def perform(self, obj, other): + def perform(self, obj: Any, other: Other) -> None: other.touch() - class MyApp(App): + class MyApp(BaseApp): touched = False foo = directive(MyDirective) @@ -1745,14 +1917,16 @@ class SubApp(MyApp): touched = False @MyApp.foo() - def f(): + def f() -> None: pass assert not MyApp.touched commit(MyApp) - assert MyApp.touched + # NOTE: mypy narrowing will make the code below unreachable + if not TYPE_CHECKING: + assert MyApp.touched assert not SubApp.touched @@ -1761,32 +1935,32 @@ def f(): assert SubApp.touched -def test_app_class_cleanup(): +def test_app_class_cleanup() -> None: class MyDirective(Action): config = {} app_class_arg = True - def __init__(self): + def __init__(self) -> None: pass - def identifier(self, app_class): + def identifier(self, app_class: type[MyApp]) -> tuple[()]: return () - def perform(self, obj, app_class): + def perform(self, obj: Any, app_class: type[MyApp]) -> None: app_class.touched.append(None) class MyApp(App): - touched = [] + touched: list[None] = [] @classmethod - def clean(cls): + def clean(cls) -> None: cls.touched = [] foo = directive(MyDirective) @MyApp.foo() - def f(): + def f() -> None: pass assert not MyApp.touched diff --git a/dectate/tests/test_error.py b/dectate/tests/test_error.py index ca8d97e..834a713 100644 --- a/dectate/tests/test_error.py +++ b/dectate/tests/test_error.py @@ -1,6 +1,11 @@ +from __future__ import annotations + +import pytest + +from typing import Any, NoReturn + from dectate.app import App, directive from dectate.config import commit, Action, Composite - from dectate.error import ( ConflictError, ConfigError, @@ -8,25 +13,23 @@ DirectiveReportError, ) -import pytest - -def test_directive_error_in_action(): +def test_directive_error_in_action() -> None: class FooDirective(Action): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> NoReturn: raise DirectiveError("A real problem") class MyApp(App): foo = directive(FooDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass with pytest.raises(DirectiveReportError) as e: @@ -38,19 +41,19 @@ def f(): assert "/test_error.py" in value -def test_directive_error_in_composite(): +def test_directive_error_in_composite() -> None: class FooDirective(Composite): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def actions(self, obj): + def actions(self, obj: Any) -> NoReturn: raise DirectiveError("Something went wrong") class MyApp(App): foo = directive(FooDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass with pytest.raises(DirectiveReportError) as e: @@ -62,26 +65,26 @@ def f(): assert "/test_error.py" in value -def test_conflict_error(): +def test_conflict_error() -> None: class FooDirective(Action): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> NoReturn: raise DirectiveError("A real problem") class MyApp(App): foo = directive(FooDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass @MyApp.foo("hello") - def g(): + def g() -> None: pass with pytest.raises(ConflictError) as e: @@ -94,16 +97,16 @@ def g(): assert "/test_error.py" in value -def test_with_statement_error(): +def test_with_statement_error() -> None: class FooDirective(Action): - def __init__(self, model, name): + def __init__(self, model: type[Any], name: str) -> None: self.model = model self.name = name - def identifier(self): + def identifier(self) -> tuple[type[Any], str]: return (self.model, self.name) - def perform(self, obj): + def perform(self, obj: Any) -> NoReturn: raise DirectiveError("A real problem") class MyApp(App): @@ -112,14 +115,14 @@ class MyApp(App): class Dummy: pass - with MyApp.foo(model=Dummy) as foo: + with MyApp.foo(model=Dummy) as foo: # type: ignore[call-arg] @foo(name="a") - def f(): + def f() -> None: pass @foo(name="b") - def g(): + def g() -> None: pass with pytest.raises(DirectiveReportError) as e: @@ -132,24 +135,24 @@ def g(): assert "/test_error.py" in value -def test_composite_codeinfo_propagation(): +def test_composite_codeinfo_propagation() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class CompositeDirective(Composite): - def __init__(self, messages): + def __init__(self, messages: list[str]) -> None: self.messages = messages - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), obj) for message in self.messages] class MyApp(App): @@ -157,11 +160,11 @@ class MyApp(App): composite = directive(CompositeDirective) @MyApp.composite(["a"]) - def f(): + def f() -> None: pass @MyApp.composite(["a"]) - def g(): + def g() -> None: pass with pytest.raises(ConflictError) as e: @@ -173,25 +176,25 @@ def g(): assert "/test_error.py" in value -def test_type_error_not_enough_arguments(): +def test_type_error_not_enough_arguments() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) # not enough arguments - @MyApp.foo() - def f(): + @MyApp.foo() # type: ignore[call-arg] + def f() -> None: pass with pytest.raises(DirectiveReportError) as e: @@ -201,25 +204,25 @@ def f(): assert "@MyApp.foo()" in value -def test_type_error_too_many_arguments(): +def test_type_error_too_many_arguments() -> None: class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) # too many arguments - @MyApp.foo("a", "b") - def f(): + @MyApp.foo("a", "b") # type: ignore[call-arg] + def f() -> None: pass with pytest.raises(DirectiveReportError) as e: @@ -229,29 +232,29 @@ def f(): assert 'MyApp.foo("a", "b")' in value -def test_cannot_group_class_group_class(): +def test_cannot_group_class_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) class BarDirective(Action): group_class = FooDirective - def __init__(self, message): + def __init__(self, message: str) -> None: pass class QuxDirective(Action): group_class = BarDirective # should go to FooDirective instead - def __init__(self, message): + def __init__(self, message: str) -> None: pass class MyApp(App): @@ -263,17 +266,17 @@ class MyApp(App): commit(MyApp) -def test_cannot_use_config_with_group_class(): +def test_cannot_use_config_with_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) class BarDirective(Action): @@ -281,7 +284,7 @@ class BarDirective(Action): group_class = FooDirective - def __init__(self, message): + def __init__(self, message: str) -> None: pass class MyApp(App): @@ -292,23 +295,23 @@ class MyApp(App): commit(MyApp) -def test_cann_inherit_config_with_group_class(): +def test_cann_inherit_config_with_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) class BarDirective(FooDirective): group_class = FooDirective - def __init__(self, message): + def __init__(self, message: str) -> None: pass class MyApp(App): @@ -318,24 +321,24 @@ class MyApp(App): commit(MyApp) -def test_cannot_use_before_with_group_class(): +def test_cannot_use_before_with_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) class BarDirective(Action): group_class = FooDirective @staticmethod - def before(): + def before() -> None: pass class MyApp(App): @@ -346,21 +349,21 @@ class MyApp(App): commit(MyApp) -def test_can_inherit_before_with_group_class(): +def test_can_inherit_before_with_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) @staticmethod - def before(foo): + def before(foo: list[tuple[str, Any]]) -> None: pass class BarDirective(FooDirective): @@ -373,24 +376,24 @@ class MyApp(App): commit(MyApp) -def test_cannot_use_after_with_group_class(): +def test_cannot_use_after_with_group_class() -> None: class FooDirective(Action): config = {"foo": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, foo): + def identifier(self, foo: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[tuple[str, Any]]) -> None: foo.append((self.message, obj)) class BarDirective(Action): group_class = FooDirective @staticmethod - def after(): + def after() -> None: pass class MyApp(App): @@ -401,21 +404,21 @@ class MyApp(App): commit(MyApp) -def test_action_without_init(): +def test_action_without_init() -> None: class FooDirective(Action): config = {"foo": list} - def identifier(self, foo): + def identifier(self, foo: list[Any]) -> tuple[()]: return () - def perform(self, obj, foo): + def perform(self, obj: Any, foo: list[Any]) -> None: foo.append(obj) class MyApp(App): foo = directive(FooDirective) @MyApp.foo() - def f(): + def f() -> None: pass commit(MyApp) @@ -423,21 +426,21 @@ def f(): assert MyApp.config.foo == [f] -def test_composite_without_init(): +def test_composite_without_init() -> None: class SubDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class CompositeDirective(Composite): - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubDirective, Any]]: return [(SubDirective(message), obj) for message in ["a", "b"]] class MyApp(App): @@ -447,7 +450,7 @@ class MyApp(App): commit(MyApp) @MyApp.composite() - def f(): + def f() -> None: pass commit(MyApp) diff --git a/dectate/tests/test_helpers.py b/dectate/tests/test_helpers.py index 39d0949..1b5fe44 100644 --- a/dectate/tests/test_helpers.py +++ b/dectate/tests/test_helpers.py @@ -1,15 +1,17 @@ +from __future__ import annotations + import sys -from ..config import create_code_info +from ..config import CodeInfo, create_code_info -def current_code_info(): +def current_code_info() -> CodeInfo: return create_code_info(sys._getframe(1)) -def test_create_code_info(): +def test_create_code_info() -> None: x = current_code_info() assert x.path == __file__ - assert x.lineno == 10 + assert x.lineno == 12 assert x.sourceline == "x = current_code_info()" x = eval("current_code_info()") diff --git a/dectate/tests/test_logging.py b/dectate/tests/test_logging.py index 112edfc..0511be4 100644 --- a/dectate/tests/test_logging.py +++ b/dectate/tests/test_logging.py @@ -1,18 +1,21 @@ +from __future__ import annotations + import logging +from typing import Any from dectate.app import App, directive from dectate.config import Action, commit class Handler(logging.Handler): - def __init__(self, level=logging.NOTSET): + def __init__(self, level: int | str = logging.NOTSET): super().__init__(level) - self.records = [] + self.records: list[logging.LogRecord] = [] - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: self.records.append(record) -def test_intercept_logging(): +def test_intercept_logging() -> None: log = logging.getLogger("my_logger") test_handler = Handler() @@ -27,7 +30,7 @@ def test_intercept_logging(): assert test_handler.records[0].getMessage() == "This is a log message" -def test_simple_config_logging(): +def test_simple_config_logging() -> None: log = logging.getLogger("dectate.directive.foo") test_handler = Handler() @@ -38,20 +41,20 @@ def test_simple_config_logging(): class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) @@ -66,7 +69,7 @@ def f(): assert messages[0] == expected -def test_subclass_config_logging(): +def test_subclass_config_logging() -> None: log = logging.getLogger("dectate.directive.foo") test_handler = Handler() @@ -77,13 +80,13 @@ def test_subclass_config_logging(): class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -93,7 +96,7 @@ class SubApp(MyApp): pass @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp, SubApp) @@ -116,7 +119,7 @@ def f(): assert messages[1] == expected -def test_override_logger_name(): +def test_override_logger_name() -> None: log = logging.getLogger("morepath.directive.foo") test_handler = Handler() @@ -127,13 +130,13 @@ def test_override_logger_name(): class MyDirective(Action): config = {"my": list} - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message - def identifier(self, my): + def identifier(self, my: list[tuple[str, Any]]) -> str: return self.message - def perform(self, obj, my): + def perform(self, obj: Any, my: list[tuple[str, Any]]) -> None: my.append((self.message, obj)) class MyApp(App): @@ -142,7 +145,7 @@ class MyApp(App): foo = directive(MyDirective) @MyApp.foo("hello") - def f(): + def f() -> None: pass commit(MyApp) diff --git a/dectate/tests/test_query.py b/dectate/tests/test_query.py index 555ae09..46074bc 100644 --- a/dectate/tests/test_query.py +++ b/dectate/tests/test_query.py @@ -1,5 +1,9 @@ +from __future__ import annotations + import pytest +from typing import TYPE_CHECKING, Any + from dectate import ( Query, App, @@ -11,29 +15,33 @@ NOT_FOUND, ) +if TYPE_CHECKING: + from collections.abc import Generator + from dectate import Sentinel + -def test_query(): +def test_query() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -43,28 +51,28 @@ def g(): assert list(q(MyApp)) == [{"name": "a"}, {"name": "b"}] -def test_query_directive_name(): +def test_query_directive_name() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -74,29 +82,29 @@ def g(): assert list(q(MyApp)) == [{"name": "a"}, {"name": "b"}] -def test_multi_action_query(): +def test_multi_action_query() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class BarAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): @@ -104,11 +112,11 @@ class MyApp(App): bar = directive(BarAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.bar("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -121,28 +129,28 @@ def g(): ] -def test_filter(): +def test_filter() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -154,20 +162,24 @@ def g(): ] -def test_filter_multiple_fields(): +def test_filter_multiple_fields() -> None: class FooAction(Action): config = {"registry": list} filter_compare = {"model": issubclass} - def __init__(self, model, name): + def __init__(self, model: type[Any], name: str) -> None: self.model = model self.name = name - def identifier(self, registry): + def identifier( + self, registry: list[tuple[type[Any], str, Any]] + ) -> tuple[type[Any], str]: return (self.model, self.name) - def perform(self, obj, registry): + def perform( + self, obj: Any, registry: list[tuple[type[Any], str, Any]] + ) -> None: registry.append((self.model, self.name, obj)) class MyApp(App): @@ -180,19 +192,19 @@ class Beta: pass @MyApp.foo(model=Alpha, name="a") - def f(): + def f() -> None: pass @MyApp.foo(model=Alpha, name="b") - def g(): + def g() -> None: pass @MyApp.foo(model=Beta, name="a") - def h(): + def h() -> None: pass @MyApp.foo(model=Beta, name="b") - def i(): + def i() -> None: pass commit(MyApp) @@ -205,28 +217,28 @@ def i(): assert list(q.filter(model=Beta, name="b").obj()(MyApp)) == [i] -def test_filter_not_found(): +def test_filter_not_found() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -236,30 +248,30 @@ def g(): assert list(q(MyApp)) == [] -def test_filter_different_attribute_name(): +def test_filter_different_attribute_name() -> None: class FooAction(Action): config = {"registry": list} filter_name = {"name": "_name"} - def __init__(self, name): + def __init__(self, name: str) -> None: self._name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self._name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self._name, obj)) class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -269,29 +281,29 @@ def g(): assert list(q(MyApp)) == [{"name": "a"}] -def test_filter_get_value(): +def test_filter_get_value() -> None: class FooAction(Action): - def filter_get_value(self, name): + def filter_get_value(self, name: str) -> str | Sentinel: return self.kw.get(name, NOT_FOUND) - def __init__(self, **kw): + def __init__(self, **kw: str) -> None: self.kw = kw - def identifier(self): + def identifier(self) -> tuple[tuple[str, str], ...]: return tuple(sorted(self.kw.items())) - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo(x="a", y="b") - def f(): + def f() -> None: pass @MyApp.foo(x="a", y="c") - def g(): + def g() -> None: pass commit(MyApp) @@ -305,32 +317,32 @@ def g(): assert list(q(MyApp)) == [{"x": "a", "y": "b"}] -def test_filter_name_and_get_value(): +def test_filter_name_and_get_value() -> None: class FooAction(Action): filter_name = {"name": "_name"} - def filter_get_value(self, name): + def filter_get_value(self, name: str) -> str | Sentinel: return self.kw.get(name, NOT_FOUND) - def __init__(self, name, **kw): + def __init__(self, name: str, **kw: str) -> None: self._name = name self.kw = kw - def identifier(self): + def identifier(self) -> tuple[tuple[str, str], ...]: return tuple(sorted(self.kw.items())) - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo(name="hello", x="a", y="b") - def f(): + def f() -> None: pass @MyApp.foo(name="bye", x="a", y="c") - def g(): + def g() -> None: pass commit(MyApp) @@ -340,30 +352,30 @@ def g(): assert list(q(MyApp)) == [{"x": "a", "y": "b", "name": "hello"}] -def test_filter_get_value_and_default(): +def test_filter_get_value_and_default() -> None: class FooAction(Action): - def filter_get_value(self, name): + def filter_get_value(self, name: str) -> str | Sentinel: return self.kw.get(name, NOT_FOUND) - def __init__(self, name, **kw): + def __init__(self, name: str, **kw: str) -> None: self.name = name self.kw = kw - def identifier(self): + def identifier(self) -> tuple[tuple[str, str], ...]: return tuple(sorted(self.kw.items())) - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo(name="hello", x="a", y="b") - def f(): + def f() -> None: pass @MyApp.foo(name="bye", x="a", y="c") - def g(): + def g() -> None: pass commit(MyApp) @@ -373,19 +385,23 @@ def g(): assert list(q(MyApp)) == [{"x": "a", "y": "b", "name": "hello"}] -def test_filter_class(): +def test_filter_class() -> None: class ViewAction(Action): config = {"registry": list} filter_compare = {"model": issubclass} - def __init__(self, model): + def __init__(self, model: type[Any]) -> None: self.model = model - def identifier(self, registry): + def identifier( + self, registry: list[tuple[type[Any], Any]] + ) -> type[Any]: return self.model - def perform(self, obj, registry): + def perform( + self, obj: Any, registry: list[tuple[type[Any], Any]] + ) -> None: registry.append((self.model, obj)) class MyApp(App): @@ -404,19 +420,19 @@ class Delta(Gamma): pass @MyApp.view(model=Alpha) - def f(): + def f() -> None: pass @MyApp.view(model=Beta) - def g(): + def g() -> None: pass @MyApp.view(model=Gamma) - def h(): + def h() -> None: pass @MyApp.view(model=Delta) - def i(): + def i() -> None: pass commit(MyApp) @@ -430,17 +446,17 @@ def i(): assert list(Query(ViewAction).filter(model=Delta).obj()(MyApp)) == [i] -def test_query_group_class(): +def test_query_group_class() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class BarAction(FooAction): @@ -451,11 +467,11 @@ class MyApp(App): bar = directive(BarAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.bar("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -465,17 +481,17 @@ def g(): assert list(q(MyApp)) == [{"name": "a"}, {"name": "b"}] -def test_query_on_group_class_action(): +def test_query_on_group_class_action() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class BarAction(FooAction): @@ -486,11 +502,11 @@ class MyApp(App): bar = directive(BarAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.bar("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -500,17 +516,17 @@ def g(): assert list(q(MyApp)) == [{"name": "a"}, {"name": "b"}] -def test_multi_query_on_group_class_action(): +def test_multi_query_on_group_class_action() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class BarAction(FooAction): @@ -521,11 +537,11 @@ class MyApp(App): bar = directive(BarAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.bar("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -538,17 +554,17 @@ def g(): ] -def test_inheritance(): +def test_inheritance() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): @@ -558,11 +574,11 @@ class SubApp(MyApp): pass @MyApp.foo("a") - def f(): + def f() -> None: pass @SubApp.foo("b") - def g(): + def g() -> None: pass commit(SubApp) @@ -572,26 +588,26 @@ def g(): assert list(q(SubApp)) == [{"name": "a"}, {"name": "b"}] -def test_composite_action(): +def test_composite_action() -> None: class SubAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class CompositeAction(Composite): query_classes = [SubAction] - def __init__(self, names): + def __init__(self, names: list[str]) -> None: self.names = names - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubAction, Any]]: return [(SubAction(name), obj) for name in self.names] class MyApp(App): @@ -599,7 +615,7 @@ class MyApp(App): composite = directive(CompositeAction) @MyApp.composite(["a", "b"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -609,24 +625,24 @@ def f(): assert list(q(MyApp)) == [{"name": "a"}, {"name": "b"}] -def test_composite_action_without_query_classes(): +def test_composite_action_without_query_classes() -> None: class SubAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class CompositeAction(Composite): - def __init__(self, names): + def __init__(self, names: list[str]) -> None: self.names = names - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubAction, Any]]: return [(SubAction(name), obj) for name in self.names] class MyApp(App): @@ -634,7 +650,7 @@ class MyApp(App): composite = directive(CompositeAction) @MyApp.composite(["a", "b"]) - def f(): + def f() -> None: pass commit(MyApp) @@ -645,35 +661,35 @@ def f(): list(q(MyApp)) -def test_nested_composite_action(): +def test_nested_composite_action() -> None: class SubSubAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class SubAction(Composite): query_classes = [SubSubAction] - def __init__(self, names): + def __init__(self, names: list[str]) -> None: self.names = names - def actions(self, obj): + def actions(self, obj: Any) -> list[tuple[SubSubAction, Any]]: return [(SubSubAction(name), obj) for name in self.names] class CompositeAction(Composite): query_classes = [SubAction] - def __init__(self, amount): + def __init__(self, amount: int) -> None: self.amount = amount - def actions(self, obj): + def actions(self, obj: Any) -> Generator[tuple[SubAction, Any]]: for i in range(self.amount): yield SubAction(["a%s" % i, "b%s" % i]), obj @@ -683,7 +699,7 @@ class MyApp(App): composite = directive(CompositeAction) @MyApp.composite(2) - def f(): + def f() -> None: pass commit(MyApp) @@ -698,29 +714,29 @@ def f(): ] -def test_query_action_for_other_app(): +def test_query_action_for_other_app() -> None: class FooAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class BarAction(Action): config = {"registry": list} - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self, registry): + def identifier(self, registry: list[tuple[str, Any]]) -> str: return self.name - def perform(self, obj, registry): + def perform(self, obj: Any, registry: list[tuple[str, Any]]) -> None: registry.append((self.name, obj)) class MyApp(App): @@ -730,11 +746,11 @@ class OtherApp(App): bar = directive(BarAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) diff --git a/dectate/tests/test_sentinel.py b/dectate/tests/test_sentinel.py index 0cbafc5..6a446c8 100644 --- a/dectate/tests/test_sentinel.py +++ b/dectate/tests/test_sentinel.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from ..sentinel import NOT_FOUND -def test_not_found(): +def test_not_found() -> None: assert repr(NOT_FOUND) == "" diff --git a/dectate/tests/test_tool.py b/dectate/tests/test_tool.py index cd9667e..84129fd 100644 --- a/dectate/tests/test_tool.py +++ b/dectate/tests/test_tool.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import pytest from argparse import ArgumentTypeError +from typing import Any from dectate.config import Action, commit from dectate.app import App, directive @@ -16,48 +19,48 @@ ) -def test_parse_app_class_main(): +def test_parse_app_class_main() -> None: from dectate.tests.fixtures import anapp app_class = parse_app_class("dectate.tests.fixtures.anapp.AnApp") assert app_class is anapp.AnApp -def test_parse_app_class_cannot_import(): +def test_parse_app_class_cannot_import() -> None: with pytest.raises(ArgumentTypeError): parse_app_class("dectate.tests.fixtures.nothere.AnApp") -def test_parse_app_class_not_a_class(): +def test_parse_app_class_not_a_class() -> None: with pytest.raises(ArgumentTypeError): parse_app_class("dectate.tests.fixtures.anapp.other") -def test_parse_app_class_no_app_class(): +def test_parse_app_class_no_app_class() -> None: with pytest.raises(ArgumentTypeError): parse_app_class("dectate.tests.fixtures.anapp.OtherClass") -def test_parse_directive_main(): +def test_parse_directive_main() -> None: from dectate.tests.fixtures import anapp action_class = parse_directive(anapp.AnApp, "foo") assert action_class is anapp.FooAction -def test_parse_directive_no_attribute(): +def test_parse_directive_no_attribute() -> None: from dectate.tests.fixtures import anapp assert parse_directive(anapp.AnApp, "unknown") is None -def test_parse_directive_not_a_directive(): +def test_parse_directive_not_a_directive() -> None: from dectate.tests.fixtures import anapp assert parse_directive(anapp.AnApp, "known") is None -def test_parse_filters_main(): +def test_parse_filters_main() -> None: assert parse_filters(["a=b", "c = d", "e=f ", " g=h"]) == { "a": "b", "c": "d", @@ -66,12 +69,12 @@ def test_parse_filters_main(): } -def test_parse_filters_error(): +def test_parse_filters_error() -> None: with pytest.raises(ToolError): parse_filters(["a"]) -def test_convert_filters_main(): +def test_convert_filters_main() -> None: class MyAction(Action): filter_convert = {"model": convert_dotted_name} @@ -84,7 +87,7 @@ class MyAction(Action): assert converted["model"] is OtherClass -def test_convert_filters_default(): +def test_convert_filters_default() -> None: class MyAction(Action): pass @@ -93,7 +96,7 @@ class MyAction(Action): assert converted["name"] == "foo" -def test_convert_filters_error(): +def test_convert_filters_error() -> None: class MyAction(Action): filter_convert = {"model": convert_dotted_name} @@ -103,7 +106,7 @@ class MyAction(Action): ) -def test_convert_filters_value_error(): +def test_convert_filters_value_error() -> None: class MyAction(Action): filter_convert = {"count": int} @@ -113,26 +116,26 @@ class MyAction(Action): convert_filters(MyAction, {"count": "a"}) -def test_query_tool_output(): +def test_query_tool_output() -> None: class FooAction(Action): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass commit(MyApp) @@ -145,15 +148,15 @@ def g(): assert li -def test_query_tool_output_multiple_apps(): +def test_query_tool_output_multiple_apps() -> None: class FooAction(Action): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class Base(App): @@ -169,11 +172,11 @@ class GammaApp(Base): pass @AlphaApp.foo("a") - def f(): + def f() -> None: pass @GammaApp.foo("b") - def g(): + def g() -> None: pass commit(AlphaApp, BetaApp, GammaApp) @@ -183,76 +186,77 @@ def g(): assert len(li) == 8 -def test_query_app(): +def test_query_app() -> None: class FooAction(Action): filter_convert = {"count": int} - def __init__(self, count): + def __init__(self, count: int) -> None: self.count = count - def identifier(self): + def identifier(self) -> int: return self.count - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo(1) - def f(): + def f() -> None: pass @MyApp.foo(2) - def g(): + def g() -> None: pass commit(MyApp) li = list(query_app(MyApp, "foo", count="1")) assert len(li) == 1 + assert isinstance(li[0][0], FooAction) assert li[0][0].count == 1 -def test_query_tool_uncommitted(): +def test_query_tool_uncommitted() -> None: class FooAction(Action): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def identifier(self): + def identifier(self) -> str: return self.name - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): foo = directive(FooAction) @MyApp.foo("a") - def f(): + def f() -> None: pass @MyApp.foo("b") - def g(): + def g() -> None: pass with pytest.raises(ToolError): list(query_tool_output([MyApp], "foo", {"name": "a"})) -def test_convert_bool(): +def test_convert_bool() -> None: assert convert_bool("True") assert not convert_bool("False") with pytest.raises(ValueError): convert_bool("flurb") -def test_convert_dotted_name_builtin(): +def test_convert_dotted_name_builtin() -> None: assert convert_dotted_name("builtins.int") is int assert convert_dotted_name("builtins.object") is object -def test_app_without_directive(): +def test_app_without_directive() -> None: class MyApp(App): pass @@ -262,17 +266,17 @@ class MyApp(App): assert li == [] -def test_inheritance(): +def test_inheritance() -> None: class FooAction(Action): filter_convert = {"count": int} - def __init__(self, count): + def __init__(self, count: int) -> None: self.count = count - def identifier(self): + def identifier(self) -> int: return self.count - def perform(self, obj): + def perform(self, obj: Any) -> None: pass class MyApp(App): @@ -282,11 +286,11 @@ class SubApp(MyApp): pass @MyApp.foo(1) - def f(): + def f() -> None: pass @MyApp.foo(2) - def g(): + def g() -> None: pass commit(SubApp) diff --git a/dectate/tests/test_toposort.py b/dectate/tests/test_toposort.py index 0a26eba..0f20f2a 100644 --- a/dectate/tests/test_toposort.py +++ b/dectate/tests/test_toposort.py @@ -3,7 +3,7 @@ import pytest -def test_topological_sort_on_dcg(): +def test_topological_sort_on_dcg() -> None: adjacency = { "A": ["B", "C"], "B": ["C", "D"], @@ -16,7 +16,7 @@ def test_topological_sort_on_dcg(): topological_sort(adjacency.keys(), adjacency.__getitem__) -def test_topological_sort_on_dag(): +def test_topological_sort_on_dag() -> None: adjacency = { "A": ["B", "C"], "B": ["C", "D"], diff --git a/dectate/tool.py b/dectate/tool.py index c38c7d8..8c73838 100644 --- a/dectate/tool.py +++ b/dectate/tool.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import argparse import inspect +from typing import TYPE_CHECKING, Any from .query import Query, get_action_class from .error import QueryError from .app import App +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from .config import Action, Composite + from .query import Filter + class ToolError(Exception): pass -def query_tool(app_classes): +def query_tool(app_classes: Iterable[type[App]]) -> None: """Command-line query tool for dectate. Uses command-line arguments to do the query and prints the results. @@ -39,12 +47,12 @@ def query_tool(app_classes): ) parser.add_argument("directive", help="Name of the directive.") - args, filters = parser.parse_known_args() + args, raw_filters = parser.parse_known_args() if args.app: app_classes = args.app - filters = parse_filters(filters) + filters = parse_filters(raw_filters) try: lines = list(query_tool_output(app_classes, args.directive, filters)) @@ -55,7 +63,9 @@ def query_tool(app_classes): print(line) -def query_tool_output(app_classes, directive, filters): +def query_tool_output( + app_classes: Iterable[type[App]], directive: str, filters: dict[str, str] +) -> Iterator[str]: for app_class in app_classes: if not app_class.is_committed(): raise ToolError("App %r was not committed." % app_class) @@ -75,7 +85,9 @@ def query_tool_output(app_classes, directive, filters): yield "" -def query_app(app_class, directive, **filters): +def query_app( + app_class: type[App], directive: str, **filters: Any +) -> Iterable[tuple[Action, Any]]: """Query a single app with raw filters. This function is especially useful for writing unit tests that @@ -89,20 +101,22 @@ def query_app(app_class, directive, **filters): action_class = parse_directive(app_class, directive) if action_class is not None: filter_kw = convert_filters(action_class, filters) - query = Query(action_class).filter(**filter_kw) + query: Query | Filter = Query(action_class).filter(**filter_kw) else: query = Query() # empty query return query(app_class) -def parse_directive(app_class, directive_name): +def parse_directive( + app_class: type[App], directive_name: str +) -> type[Action | Composite] | None: try: return get_action_class(app_class, directive_name) except QueryError: return None -def parse_app_class(s): +def parse_app_class(s: str) -> type[App]: try: app_class = resolve_dotted_name(s) except ImportError: @@ -116,11 +130,11 @@ def parse_app_class(s): return app_class -def convert_default(s): +def convert_default(s: str) -> str: return s -def convert_dotted_name(s): +def convert_dotted_name(s: str) -> Any: """Convert input string to an object in a module. Takes a dotted name: ``pkg.module.attr`` gets ``attr`` @@ -139,7 +153,7 @@ def convert_dotted_name(s): raise ToolError("Cannot resolve dotted name: %s" % s) -def convert_bool(s): +def convert_bool(s: str) -> bool: """Convert input string to boolean. Input string must either be ``True`` or ``False``. @@ -152,7 +166,7 @@ def convert_bool(s): raise ValueError("Cannot convert bool: %r" % s) -def parse_filters(entries): +def parse_filters(entries: Iterable[str]) -> dict[str, str]: result = {} for entry in entries: try: @@ -164,7 +178,9 @@ def parse_filters(entries): return result -def convert_filters(action_class, filters): +def convert_filters( + action_class: type[Action | Composite], filters: dict[str, str] +) -> dict[str, Any]: filter_convert = action_class.filter_convert result = {} @@ -179,22 +195,25 @@ def convert_filters(action_class, filters): return result -def resolve_dotted_name(name, module=None): +def resolve_dotted_name(name: str, module: str | None = None) -> Any: """Adapted from zope.dottedname""" - name = name.split(".") - if not name[0]: + name_parts = name.split(".") + if not name_parts[0]: if module is None: raise ValueError("relative name without base module") - module = module.split(".") - name.pop(0) - while not name[0]: - module.pop() - name.pop(0) - name = module + name - - used = name.pop(0) + module_parts = module.split(".") + name_parts.pop(0) + if TYPE_CHECKING: + # NOTE: Undo mypy narrowing to Literal[""] for name_parts[0] + name_parts = name_parts + while not name_parts[0]: + module_parts.pop() + name_parts.pop(0) + name_parts = module_parts + name_parts + + used = name_parts.pop(0) found = __import__(used) - for n in name: + for n in name_parts: used += "." + n try: found = getattr(found, n) diff --git a/dectate/toposort.py b/dectate/toposort.py index 604fc70..f4b9fcb 100644 --- a/dectate/toposort.py +++ b/dectate/toposort.py @@ -1,29 +1,39 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar from .error import TopologicalSortError +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + +_T = TypeVar("_T") + -def topological_sort(l, get_depends): # noqa: E741 +def topological_sort( + l: Iterable[_T], get_depends: Callable[[_T], Iterable[_T]] # noqa: E741 +) -> list[_T]: """`Topological sort`_ .. _`Topological sort`: https://en.wikipedia.org/wiki/Topological_sorting - Given a list of items that depend on each other, sort so that + Given an iterable of items that depend on each other, sort so that dependencies come before the dependent items. Dependency graph must be a DAG_. .. _DAG: https://en.wikipedia.org/wiki/Directed_acyclic_graph - :param l: a list of items to sort + :param l: an iterable of items to sort :param get_depends: a function that given an item gives other items that this item depends on. This item will be sorted after the items it depends on. - :return: the list sorted topologically. + :return: a list of the given items sorted topologically. """ result = [] marked = set() temporary_marked = set() - def visit(n): + def visit(n: _T) -> None: if n in marked: return if n in temporary_marked: diff --git a/dectate/types.py b/dectate/types.py new file mode 100644 index 0000000..0278193 --- /dev/null +++ b/dectate/types.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, TypeVar + +if TYPE_CHECKING: + from .app import App + from .config import Action, Composite, Directive, DirectiveAbbreviation + + +_AppT = TypeVar("_AppT", bound="App") +_P = ParamSpec("_P") + + +class DirectiveCallable(Protocol[_P]): + __name__: str + __qualname__: str + action_factory: type[Action | Composite] + + def partial(self, *args: Any, **kwargs: Any) -> DirectiveAbbreviation: + raise NotImplementedError + + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Directive: + raise NotImplementedError diff --git a/doc/usage.rst b/doc/usage.rst index 8c27454..2deca4d 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -969,7 +969,7 @@ You can instead write: class SuccinctWithApp(WithApp): pass - with SuccinctWithApp.foo('a') as foo: + with SuccinctWithApp.foo.partial('a') as foo: @foo('x') def f(): pass diff --git a/pyproject.toml b/pyproject.toml index 2eb2a06..22423fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,10 +37,15 @@ test = ["pytest >= 8", "pytest-env"] coverage = ["pytest-cov"] lint = ["black", "flake8", "flake8-pyproject"] docs = ["sphinx"] +mypy = ["mypy", "pytest"] +pyright = ["pyright", "pytest"] [tool.setuptools.packages] find = {} +[tool.setuptools.package-data] +dectate = ["py.typed"] + [tool.setuptools.dynamic] readme = {file = ["README.rst", "CHANGES.txt"]} @@ -51,7 +56,7 @@ addopts = ["-vv"] env = ["RUN_ENV=test"] [tool.coverage.run] -omit = ["dectate/tests/*"] +omit = ["dectate/tests/*", "dectate/types.py"] source = ["dectate"] [tool.coverage.report] @@ -59,9 +64,28 @@ show_missing = true [tool.flake8] show-source = true -ignore = ["E203", "W503"] +ignore = ["E203", "E301", "E501", "E704", "W503", "W504"] max-line-length = 80 +[tool.mypy] +python_version = "3.10" +strict = true +warn_unreachable = true + +[[tool.mypy.overrides]] +module = "dectate.sphinxext.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "dectate.tests.fixtures.*" +ignore_errors = true + +[tool.pyright] +exclude = [ + "**/sphinxext.py", + "**/tests/fixtures/anapp.py", +] + [tool.black] line-length = 80 target-version = ['py310', 'py311', 'py312', 'py313', 'py314'] @@ -90,11 +114,13 @@ env_list = [ "coverage", "pre-commit", "docs", + "mypy", + "pyright", ] skip_missing_interpreters = true [tool.tox.gh.python] -"3.10" = ["py310"] +"3.10" = ["py310", "mypy", "pyright"] "3.11" = ["py311"] "3.12" = ["py312"] "3.13" = ["py313"] @@ -127,3 +153,25 @@ extras = ["docs"] commands = [ ["sphinx-build", "-b", "doctest", "doc", "{env_tmp_dir}"], ] + +[tool.tox.env.mypy] +base_python = ["python3"] +extras = ["mypy"] +commands = [ + ["mypy", "-p", "dectate", "--python-version", "3.10"], + ["mypy", "-p", "dectate", "--python-version", "3.11"], + ["mypy", "-p", "dectate", "--python-version", "3.12"], + ["mypy", "-p", "dectate", "--python-version", "3.13"], + ["mypy", "-p", "dectate", "--python-version", "3.14"], +] + +[tool.tox.env.pyright] +base_python = ["python3"] +extras = ["pyright"] +commands = [ + ["pyright", "dectate", "--pythonversion", "3.10"], + ["pyright", "dectate", "--pythonversion", "3.11"], + ["pyright", "dectate", "--pythonversion", "3.12"], + ["pyright", "dectate", "--pythonversion", "3.13"], + ["pyright", "dectate", "--pythonversion", "3.14"], +]