Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type hinting for registry members #435

Open
6 tasks
jace opened this issue Dec 18, 2023 · 0 comments
Open
6 tasks

Type hinting for registry members #435

jace opened this issue Dec 18, 2023 · 0 comments

Comments

@jace
Copy link
Member

jace commented Dec 18, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant