You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm adding type hints to a codebase that had previously adopted a strategy of splitting a class across modules (context for why is below). A class decorator copies the attributes from a temporary class into an existing class, then returns the existing class, discarding the temporary class. The result is a code pattern that looks like this:
from .other_moduleimportImportedClassfrom .helpersimportreopen@reopen(ImportedClass)classTemporaryClass:
defnew_method(self) ->int:
returnself.existing_attribute+1assertTemporaryClassisImportedClass
Using the reopen decorator allows for cleaner, less surprising code, and the decorator adds safety checks over direct patching. The decorator is also typed to indicate it replaces the decorated value with its parameter so that the assert passes type checking. The problem is that mypy does not actually recognise the patching, so I get other errors:
In new_method it will raise an error about TemporaryClass not containing existing_attribute.
If I change the signature to def new_method(self: ImportedClass) -> int, it will raise an error for the erased type of self ImportedClass not being a superclass of TemporaryClass.
I can workaround this by using cast(ImportedClass, self), but mypy also does not recognise ImportedClass.new_method to exist, so downstream code will get flagged.
I'm aware monkey patching and static type analysis don't get along, so I'm wondering how to reorganise without littering my code with cast and # type: ignore. Can this be addressed with a mypy plugin, as happens with enums and dataclasses, or is this sort of distributed class definition too complex for mypy?
Why do this?
The reopen mechanism came up as a coping strategy for SQLAlchemy models. I have ORM models representing one-to-many (or parent-child) relationships that are split across files. SQLAlchemy has back-references that allow a ChildModel model class to create a collection on a ParentModel model class (like ParentModel.children), but no mechanism for injecting methods. If I need a parent.refresh_children() method, I could:
Define it in the ParentModel class. However, the method itself depends on the structure of ChildModel, and if I have multiple types of child models, all with their own support functions, it becomes hard to understand as related functionality is now spread across files, apart from also causing circular imports.
Define a classmethod on ChildModel with a signature like def refresh_parent(cls, parent: ParentModel). This keeps related functionality in the same file and in the same class, but feels unpythonic in usage:
Use the above approach with @reopen, keeping related functionality all in one file while also having a Pythonic API in usage.
But this approach no longer works with the introduction of type hinting, so I'm hoping for another way to organise that checks all the boxes: easy to read and maintain, nice API to use, and mypy-compatible.
Variations of this pattern have come up in the past, sometimes referring to Ruby's class extensions:
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm adding type hints to a codebase that had previously adopted a strategy of splitting a class across modules (context for why is below). A class decorator copies the attributes from a temporary class into an existing class, then returns the existing class, discarding the temporary class. The result is a code pattern that looks like this:
This code is equivalent to:
Using the
reopendecorator allows for cleaner, less surprising code, and the decorator adds safety checks over direct patching. The decorator is also typed to indicate it replaces the decorated value with its parameter so that theassertpasses type checking. The problem is that mypy does not actually recognise the patching, so I get other errors:new_methodit will raise an error aboutTemporaryClassnot containingexisting_attribute.def new_method(self: ImportedClass) -> int, it will raise an error for the erased type of selfImportedClassnot being a superclass ofTemporaryClass.cast(ImportedClass, self), but mypy also does not recogniseImportedClass.new_methodto exist, so downstream code will get flagged.I'm aware monkey patching and static type analysis don't get along, so I'm wondering how to reorganise without littering my code with
castand# type: ignore. Can this be addressed with a mypy plugin, as happens with enums and dataclasses, or is this sort of distributed class definition too complex for mypy?Why do this?
The
reopenmechanism came up as a coping strategy for SQLAlchemy models. I have ORM models representing one-to-many (or parent-child) relationships that are split across files. SQLAlchemy has back-references that allow aChildModelmodel class to create a collection on aParentModelmodel class (likeParentModel.children), but no mechanism for injecting methods. If I need aparent.refresh_children()method, I could:Define it in the
ParentModelclass. However, the method itself depends on the structure ofChildModel, and if I have multiple types of child models, all with their own support functions, it becomes hard to understand as related functionality is now spread across files, apart from also causing circular imports.Define a classmethod on
ChildModelwith a signature likedef refresh_parent(cls, parent: ParentModel). This keeps related functionality in the same file and in the same class, but feels unpythonic in usage:Use the above approach with
@reopen, keeping related functionality all in one file while also having a Pythonic API in usage.But this approach no longer works with the introduction of type hinting, so I'm hoping for another way to organise that checks all the boxes: easy to read and maintain, nice API to use, and mypy-compatible.
Variations of this pattern have come up in the past, sometimes referring to Ruby's class extensions:
Beta Was this translation helpful? Give feedback.
All reactions