3

SQLAlchemy backrefs tend to cause circular imports as they get complex, so I came up with a way to "re-open" a Python class like in Ruby:

def reopen(cls):
    """
    Moves the contents of the decorated class into an existing class and returns that.

    Usage::

        from .other_module import ExistingClass

        @reopen(ExistingClass)
        class ExistingClass:
            @property
            def new_property(self):
                pass

    This is equivalent to::

        def new_property(self):
            pass

        ExistingClass.new_property = property(new_property)
    """

    def decorator(temp_cls):
        for attr, value in temp_cls.__dict__.items():
            # Skip the standard Python attributes, process the rest
            if attr not in ('__dict__', '__doc__', '__module__', '__weakref__'):
                setattr(cls, attr, value)
        return cls

    return decorator

This is a simplified version; the full implementation has more safety checks and tests.

This code works fairly well for inserting bi-directional SQLAlchemy relationships and helper methods wherever SQLAlchemy's existing relationship+backref mechanism doesn't suffice.

However:

  1. mypy raises error "name 'ExistingClass' already defined (possibly by an import)"
  2. pylint raises E0102 "class already defined"

I could ignore both errors by having an annotation on the line (# type: ignore and # skipcq), but overuse of those annotations can let bugs slip through. It'll be nice to tell mypy and pylint that this use is okay.

How can I do that?

Kiran Jonnalagadda
  • 2,606
  • 1
  • 30
  • 29
  • Does this answer your question? [How do I disable a Pylint warning?](https://stackoverflow.com/questions/4341746/how-do-i-disable-a-pylint-warning) – RMPR Oct 20 '20 at 11:50
  • I'd prefer to not do a blanket disable as it'll reduce the utility for catching actual bugs. Inline per-instance disabling is my current solution, but it feels like I'm training myself to simply ignore all errors and warnings. – Kiran Jonnalagadda Oct 20 '20 at 12:08
  • You can import classes that are only used for type annotations in a `if typing.TYPE_CHECKING:` block to prevent circular imports. – Wombatz Oct 20 '20 at 22:34
  • The circular import problem is from SQLAlchemy relationships, not type checking. – Kiran Jonnalagadda Oct 20 '20 at 22:41

1 Answers1

0

As a workaround, I've switched to this pattern:

@reopen(ExistingClass)
class __ExistingClass:
    @property
    def new_property(self):
        pass

>>> assert __ExistingClass is ExistingClass
True

Pylint and Mypy no longer complain about redefinition. However, Mypy will not recognise ExistingClass.new_property as an addition to the class, so it kicks the problem downstream to code that uses it.

My impression is that such class extension hacks, while discussed multiple times, have not gained enough traction as a coding pattern, and in Mypy's case this will need a custom plugin.

Kiran Jonnalagadda
  • 2,606
  • 1
  • 30
  • 29