basedmypy
basedmypy copied to clipboard
Support extension stubs / type augmentation
Overriding existing types and monkey patching sounds based.
@extension[str]
class StrExtension:
foo: int
"asdf".foo + 1
While often frowned upon(just fix it at the source), it could be also be useful for fixing up third-party packages that have incomplete / incorrect stubs:
Official stub:
class SomeThirdPartyClass:
def foo(self, a, b, c): ...
vendored stub:
@extension[SomeThirdPartyClass]
class _SomeThirdPartyClass:
def foo(self, a: int, b: str, c: bool) -> Foo: ...
There would need to be some thought as to how this could be applied to module level declarations, perhaps steal module from typescript?
An option to specify overriding vs augmenting would be needed.
Also, should it be a decorator, or part of the class bases?
@extension[str]
class StrExtension: ...
class StrExtension(Extension[str]): ...
Alternative
An alternative to this approach could be a magic module path like __extensions__/__overrides__. Everything in this folder would mirror the module space(eg: __extensions__/SomeThirdPartyPackage/SomeThirdPartyModule.py). Anything in this directory would be applied on top of the base types.
Additional points
sitecustomize.py could be a useful place to do things
Monkey patching seems to usually refer to making changes at runtime (eg. https://stackoverflow.com/questions/5626193/what-is-monkey-patching), presumably that isn't what you mean here.
In those examples, why are the names different (in your example, it looks like class StrExtension is specifying a new class / stub for a class that extends str, rather than just supplying or fixing typing information for an existing class)?
Why not have something like:
@StubExtension
class str:
foo: int
and
@StubExtension
class SomeThirdPartyClass:
def foo(self, a: int, b: str, c: bool) -> Foo: ...
Maybe it could also specify scope, so something like:
@StubExtension(scope=class)
class SomeThirdPartyClass:
def foo(self, a: int, b: str, c: bool) -> Foo: ...
would mean that SomeThirdPartyClass class doesn't contain anything except foo.
In those examples, why are the names different (in your example, it looks like
class StrExtensionis specifying a new class / stub for a class that extends str, rather than just supplying or fixing typing information for an existing class)?Why not have something like:
@StubExtension class str: foo: int
this could cause potential conflicts when importing several extensions on the same class. see how Dart does it:
Also, extensions have names, which can be helpful if an API conflict arises.
Maybe it could also specify scope, so something like:
@StubExtension(scope=class) class SomeThirdPartyClass: def foo(self, a: int, b: str, c: bool) -> Foo: ...would mean that
SomeThirdPartyClassclass doesn't contain anything exceptfoo.
wouldn't this break LSP if you extend a class to remove methods? perhaps a use case could be to fix incorrect stubs, however there are some potential issues in supporting that which are discussed in https://github.com/microsoft/TypeScript/issues/36146#issuecomment-573925535 https://github.com/microsoft/TypeScript/issues/36146#issuecomment-1018010023
Monkey patching seems to usually refer to making changes at runtime, presumably that isn't what you mean here.
This feature is to statically type monkey patched members, so that is what I meant.
from forbiddenfruit import curse
@extension[str]
class StrExtension:
def foo(self): ...
def foo(self: str): ...
curse(str, "foo", foo)
"asdf".foo()
In those examples, why are the names different
how Dart does it
Swift does it different to Dart, just referring to the original name. I think the no name way would be really clunky in Python, exacerbated by the fact that there is no dedicated syntax:
no name:
from third_party.x.y import Z
@extension
class Z:
a: int
explicit name:
from third_party.x.y import Z
@extension[Z]
class ZExtension:
a: int
This would create all sorts of lint issues regarding redefinition and unused imports etc. Also would also make reflection harder.
would mean that
SomeThirdPartyClassclass doesn't contain anything exceptfoo.
What's the use case for needing to delete everything from a class when doing an override?