basedmypy icon indicating copy to clipboard operation
basedmypy copied to clipboard

Support extension stubs / type augmentation

Open KotlinIsland opened this issue 4 years ago • 3 comments

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

KotlinIsland avatar Oct 22 '21 02:10 KotlinIsland

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.

Zeckie avatar May 02 '22 12:05 Zeckie

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

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 SomeThirdPartyClass class doesn't contain anything except foo.

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

DetachHead avatar May 03 '22 00:05 DetachHead

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 SomeThirdPartyClass class doesn't contain anything except foo.

What's the use case for needing to delete everything from a class when doing an override?

KotlinIsland avatar May 03 '22 00:05 KotlinIsland