diff --git a/routemaster/cli.py b/routemaster/cli.py index 5446d87c..c1c61928 100644 --- a/routemaster/cli.py +++ b/routemaster/cli.py @@ -97,7 +97,7 @@ def serve(ctx, bind, debug, workers): # pragma: no cover cron_thread.stop() -def _validate_config(app: App): +def _validate_config(app: App) -> None: try: validate_config(app, app.config) except ValidationError as e: diff --git a/routemaster/config/model.py b/routemaster/config/model.py index d1a70a60..37b32cbf 100644 --- a/routemaster/config/model.py +++ b/routemaster/config/model.py @@ -160,7 +160,7 @@ class FeedConfig(NamedTuple): class Webhook(NamedTuple): """Configuration for webdook requests.""" - match: Pattern + match: Pattern[str] headers: Dict[str, str] diff --git a/routemaster/context.py b/routemaster/context.py index 602c530b..ef1a141a 100644 --- a/routemaster/context.py +++ b/routemaster/context.py @@ -1,10 +1,27 @@ """Context definition for exit condition programs.""" import datetime -from typing import Any, Dict, Iterable, Optional, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Callable, + Iterable, + Optional, + Sequence, + ContextManager, +) -from routemaster.feeds import Feed from routemaster.utils import get_path +if TYPE_CHECKING: + import requests # noqa + from routemaster.feeds import Feed, FeedResponseLogger # noqa + +FeedLoggingContext = Callable[ + [str], + ContextManager[Callable[['requests.Response'], None]], +] + class Context(object): """Execution context for exit condition programs.""" @@ -15,10 +32,10 @@ def __init__( label: str, metadata: Dict[str, Any], now: datetime.datetime, - feeds: Dict[str, Feed], + feeds: Dict[str, 'Feed'], accessed_variables: Iterable[str], current_history_entry: Optional[Any], - feed_logging_context, + feed_logging_context: FeedLoggingContext, ) -> None: """Create an execution context.""" if now.tzinfo is None: @@ -80,8 +97,8 @@ def _pre_warm_feeds( self, label: str, accessed_variables: Iterable[str], - logging_context, - ): + logging_context: FeedLoggingContext, + ) -> None: for accessed_variable in accessed_variables: parts = accessed_variable.split('.') diff --git a/routemaster/cron.py b/routemaster/cron.py index 855fc3f3..25b409f4 100644 --- a/routemaster/cron.py +++ b/routemaster/cron.py @@ -55,7 +55,7 @@ def process_job( # Bound when scheduling a specific job for a state fn: LabelStateProcessor, label_provider: LabelProvider, -): +) -> None: """Process a single instance of a single cron job.""" def _iter_labels_until_terminating(state_machine, state): diff --git a/routemaster/feeds.py b/routemaster/feeds.py index d8dc2bf1..797716de 100644 --- a/routemaster/feeds.py +++ b/routemaster/feeds.py @@ -6,9 +6,12 @@ from dataclasses import InitVar, dataclass from routemaster.utils import get_path, template_url +from routemaster.config import StateMachine +FeedResponseLogger = Callable[[requests.Response], None] -def feeds_for_state_machine(state_machine) -> Dict[str, 'Feed']: + +def feeds_for_state_machine(state_machine: StateMachine) -> Dict[str, 'Feed']: """Get a mapping of feed prefixes to unfetched feeds.""" return { x.name: Feed(x.url, state_machine.name) # type: ignore @@ -42,7 +45,7 @@ class Feed: def prefetch( self, label: str, - log_response: Callable[[requests.Response], None] = lambda x: None, + log_response: FeedResponseLogger = lambda x: None, ) -> None: """Trigger the fetching of a feed's data.""" if self.data is not None: diff --git a/routemaster/logging/base.py b/routemaster/logging/base.py index d474c355..c5b82f56 100644 --- a/routemaster/logging/base.py +++ b/routemaster/logging/base.py @@ -1,12 +1,13 @@ """Base class for logging plugins.""" import contextlib +from typing import Any class BaseLogger: """Base class for logging plugins.""" - def __init__(self, config, *args, **kwargs) -> None: + def __init__(self, config: Any, **kwargs: str) -> None: self.config = config def init_flask(self, flask_app): diff --git a/routemaster/logging/plugins.py b/routemaster/logging/plugins.py index 76afc62e..20aa68a5 100644 --- a/routemaster/logging/plugins.py +++ b/routemaster/logging/plugins.py @@ -1,5 +1,6 @@ """Plugin loading and configuration.""" import importlib +from typing import List from routemaster.config import Config, LoggingPluginConfig from routemaster.logging.base import BaseLogger @@ -9,7 +10,7 @@ class PluginConfigurationException(Exception): """Raised to signal an invalid plugin that was loaded.""" -def register_loggers(config: Config): +def register_loggers(config: Config) -> List[BaseLogger]: """ Iterate through all plugins in the config file and instatiate them. """ diff --git a/routemaster/logging/python_logger.py b/routemaster/logging/python_logger.py index ad658385..abdb3f75 100644 --- a/routemaster/logging/python_logger.py +++ b/routemaster/logging/python_logger.py @@ -3,15 +3,19 @@ import time import logging import contextlib +from typing import TYPE_CHECKING from routemaster.logging.base import BaseLogger +if TYPE_CHECKING: + from routemaster.config import Config # noqa + class PythonLogger(BaseLogger): """Routemaster logging interface for Python's logging library.""" - def __init__(self, *args, log_level: str) -> None: - super().__init__(*args) + def __init__(self, config: 'Config', log_level: str) -> None: + super().__init__(config) logging.basicConfig( format=( diff --git a/routemaster/logging/split_logger.py b/routemaster/logging/split_logger.py index 8021619c..9c83f694 100644 --- a/routemaster/logging/split_logger.py +++ b/routemaster/logging/split_logger.py @@ -2,16 +2,19 @@ import functools import contextlib -from typing import List +from typing import TYPE_CHECKING, List from routemaster.logging.base import BaseLogger +if TYPE_CHECKING: + from routemaster.config import Config # noqa + class SplitLogger(BaseLogger): """Proxies logging calls to all loggers in a list.""" - def __init__(self, *args, loggers: List[BaseLogger]) -> None: - super().__init__(*args) + def __init__(self, config: 'Config', loggers: List[BaseLogger]) -> None: + super().__init__(config) self.loggers = loggers diff --git a/routemaster/logging/tests/test_loggers.py b/routemaster/logging/tests/test_loggers.py index 7933c71c..c5adc0dc 100644 --- a/routemaster/logging/tests/test_loggers.py +++ b/routemaster/logging/tests/test_loggers.py @@ -18,7 +18,7 @@ ]}), (SplitLogger, {'loggers': [ PythonLogger(None, log_level='WARN'), - BaseLogger(None, {}), + BaseLogger(None), ]}), ] diff --git a/routemaster/middleware.py b/routemaster/middleware.py index b2efef49..a65f3435 100644 --- a/routemaster/middleware.py +++ b/routemaster/middleware.py @@ -11,7 +11,7 @@ ACTIVE_MIDDLEWARES = [] -def middleware(fn: WSGIMiddleware): +def middleware(fn: WSGIMiddleware) -> WSGIMiddleware: """Decorator: add `fn` to ACTIVE_MIDDLEWARES.""" ACTIVE_MIDDLEWARES.append(fn) return fn diff --git a/routemaster/state_machine/actions.py b/routemaster/state_machine/actions.py index 01add392..858ca7c2 100644 --- a/routemaster/state_machine/actions.py +++ b/routemaster/state_machine/actions.py @@ -98,7 +98,10 @@ def process_action( return True -def _calculate_idempotency_token(label: LabelRef, latest_history) -> str: +def _calculate_idempotency_token( + label: LabelRef, + latest_history: History, +) -> str: """ We want to make sure that an action is only performed once. diff --git a/routemaster/state_machine/api.py b/routemaster/state_machine/api.py index 258439b2..9e3feea6 100644 --- a/routemaster/state_machine/api.py +++ b/routemaster/state_machine/api.py @@ -151,7 +151,7 @@ def _process_transitions_for_metadata_update( label: LabelRef, state_machine: StateMachine, state_pending_update: State, -): +) -> None: with app.session.begin_nested(): lock_label(app, label) current_state = get_current_state(app, label, state_machine) @@ -223,7 +223,7 @@ def process_cron( app: App, state_machine: StateMachine, state: State, -): +) -> None: """ Cron event entrypoint. """ diff --git a/routemaster/tests/test_layering.py b/routemaster/tests/test_layering.py index 01d10e70..c99b0e51 100644 --- a/routemaster/tests/test_layering.py +++ b/routemaster/tests/test_layering.py @@ -27,7 +27,6 @@ ('exit_conditions', 'utils'), ('context', 'utils'), - ('context', 'feeds'), ('config', 'exit_conditions'), ('config', 'context'), @@ -57,6 +56,7 @@ ('state_machine', 'webhooks'), ('feeds', 'utils'), + ('feeds', 'config'), ('webhooks', 'config'), @@ -135,10 +135,34 @@ def test_layers(): code = compile(contents, module_name, 'exec') last_import_source = None + last_load_name = None + skip_to_offset = None - top_level_import = False + for offset, instruction in enumerate(dis.get_instructions(code)): + + # Skip over checking code that is within a block like this: + # if TYPE_CHECKING: + # import... + if skip_to_offset == offset: + skip_to_offset = None + elif skip_to_offset is not None: + continue + + if instruction.opname == 'LOAD_NAME': + last_load_name = instruction.argval + continue + + elif ( + instruction.opname == 'POP_JUMP_IF_FALSE' and + last_load_name == 'TYPE_CHECKING' + ): + skip_to_offset = instruction.argval + continue + + last_load_name = None + + # Now do the actual import checking - for instruction in dis.get_instructions(code): if instruction.opname == 'IMPORT_NAME': import_target = instruction.argval diff --git a/routemaster/validation.py b/routemaster/validation.py index 379be7ad..2fd92958 100644 --- a/routemaster/validation.py +++ b/routemaster/validation.py @@ -14,13 +14,13 @@ class ValidationError(Exception): pass -def validate_config(app: App, config: Config): +def validate_config(app: App, config: Config) -> None: """Validate that a given config satisfies invariants.""" for state_machine in config.state_machines.values(): _validate_state_machine(app, state_machine) -def _validate_state_machine(app: App, state_machine: StateMachine): +def _validate_state_machine(app: App, state_machine: StateMachine) -> None: """Validate that a given state machine is internally consistent.""" with app.new_session(): _validate_route_start_to_end(state_machine) diff --git a/setup.cfg b/setup.cfg index 82beb800..ab08bdc3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,8 @@ no-accept-encodings=true [mypy] ignore_missing_imports=true strict_optional=true +disallow_any_generics=true +disallow_incomplete_defs=true [coverage:run] branch=True