hasgeek / coaster

Common patterns for Flask apps
http://coaster.readthedocs.org/
BSD 3-Clause "New" or "Revised" License
69 stars 13 forks source link

Type hinting for registry members #435

Open jace opened 8 months ago

jace commented 8 months ago

Registry members are added during module import, and the member signatures are affected by the kwarg, property and cached_property flags. This makes registries incompatible with static type checking. The member bodies can themselves by type checked, but calls to them via the registry will be opaque.

There may be a workaround using type stubs, by generating a stub file using runtime introspection:

# model.py
from typing import TYPE_CHECKING
from . import model_registry_stubs  # Stub files will be f'{filename}_registry_stubs.pyi'

class MyModel(RegistryMixin):  # Or BaseMixin or other subclass
    if TYPE_CHECKING:
        # Stub class names are f'{cls.__name__}_{registry._name}'
        forms: ClassVar[model_registry_stubs.MyModel_forms]
        views: ClassVar[model_registry_stubs.MyModel_views]
        features: ClassVar[model_registry_stubs.MyModel_features]

This boilerplate does not appear to be avoidable unless we auto-generate the model's stub files as well — which is useful for SQLAlchemy backrefs and Funnel's related @reopen decorator — but that is considerably more work as it requires extending Mypy's stubgen to perform these additional manipulations.

A helper function can accept a module, scan it for models containing registries, and return a generated stub file for its registries. Each registry generates two classes, the aforementioned f'{model.__name__}{registry._name} and a second suffixing Wrapper (or another safe set of characters). The __get__ method of the first is typed to return the second (replacing the actual single InstanceRegistry). Both contain all members, but with differing signatures.

For each member, the following manipulations are needed:

  1. [ ] Use inspect.signature to get resolved type hints. It is expected that this will not raise an error due to weird type definitions (including types defined as available only when type checking).
  2. [ ] Resolve imports for non-builtins. Add import dotted.path.to.module to imports, and change the type reference to use the full path. This will hopefully avoid overlaps.
  3. [ ] Insert a fake self first parameter, as the registry is a class and the members pretend to be methods. It can be called __registry_self__ to avoid conflicts with real parameters named self. In the main registry, this becomes the inserted definition with no further processing.
  4. [ ] For the instance wrapper definition, remove the implicit parameter (first positional or named kwarg parameter)
  5. [ ] If the method is defined as a property, add the @property decorator.
  6. [ ] If the method is defined as a cached property, add import for functools and decorate with functools.cached_property

Caveat: the registry member function can't be a generic that specifies its return type based on its input type, as the input type will be erased in the stub.

The functionality of this stub extractor can be wrapped in a Flask CLI command that will write the stub files to disk.