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

Fix 3903: untyped defs on derived classes #7548

Closed
wants to merge 10 commits into from

Conversation

gantsevdenis
Copy link
Contributor

@gantsevdenis gantsevdenis commented Sep 21, 2019

fix #3903
Looked simple enough, but not sure about the part:

if not defn.info:
           return None

If removed, produces AssertionErrors

@gantsevdenis gantsevdenis changed the title Add new option, added test Fix 3903: untyped defs on derived classes Sep 21, 2019
Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! I have few general comments:

  • I think this should be independent of --check-untyped-defs. One might want to have the latter not enables and only check the methods that have a typed definition in a superclass.
  • I think the benefit should be not only checking the bodies using "inherited" types, but also external access should be typed, i.e. Sub.meth(...) calls should be checked using the signature of Super.meth. For this reason I think the copying should happen earlier, during semantic analysis.
  • What should we do if the signature is impossible to copy? In particular what if signatures are not identical, but still Liskov-compatible?
  • What if the method in a subclass annotates only some arguments? Should this trigger an error, should we copy the rest, should we just assume the missing ones are Any?
  • You need to make your implementation works correctly in incremental and daemon modes (the latter one might require some tricky changes to type dependency tracking).
  • You need to be sure this works well with generic classes, where you wound need to not just copy the argument/return types, but map them correctly taking into account type variable binding along MRO.

@ilevkivskyi
Copy link
Member

I think most of the questions are answered in #3903 (comment) (there are also couple additional points).

@gantsevdenis
Copy link
Contributor Author

gantsevdenis commented Oct 8, 2019

Thank you for guiding!
I have followings question points:

  • I added support for generics, but not sure if something exists already which could be used, instead of my own substitute.. function? I couldn't find out how to use some of existing TransformVisitors
  • I had to do a cast, because I couldn't solve typing problem..
  • I am not sure to understand the difference between incremental mode, and daemon mode, but I ve tried using feature with daemon, and could not encounter any dependency tracking issues (I used a very simple setup though, only 2 modules, both monitored by daemon, one import each other, one of them defines aclass Base, and the other imports it: the typing issues in the second module were caught ) Ok I see a problem indeed in incremental mode. Will work on it

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although you said:

Looked simple enough

This is relatively tricky feature. I left few large-scale comments. This looks still not ready fo a detailed review.

@@ -442,7 +442,9 @@ def add_invertible_flag(flag: str,
add_invertible_flag('--disallow-untyped-decorators', default=False, strict_flag=True,
help="Disallow decorating typed functions with untyped decorators",
group=untyped_group)

add_invertible_flag('--inherit-signatures', default=False, strict_flag=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be a strict flag, because --disallow-untyped-defs is already part of --strict IIRC.

@@ -160,6 +160,45 @@
Tag = int


def substitute_unbounds(t: UnboundType, names_to_types: Mapping[str, Instance]) -> ProperType:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for misleading suggestion, doing the copy during semantic analyzis is still bad. I was thinking about superclass being in a different module that is type-checked before the module with subclass, but it obviously doesn't work always.

Also this function is just one big hack. I was thinking about something along the lines of mypy.checker.TypeChecker.bind_and_map_method().

For the latter to work well I see a possible strategy:

  1. Add two flags (or maybe better one three state flag) to functions indicating whether this function can be "improved", i.e. it has some untyped arguments, while a supertype has those typed, and whether the "copying" has already been performed.
  2. The first flag should be set during semantic analysis, this should be thought carefully to work well in import cycles.
  3. During type checking, whenever we encounter a reference to a method Sub.meth for which we know it can be "improved", but wasn't yet, we trigger a deferral.
  4. Finally, we perform the copying while analyzing the method itself (in a place where you did initially, but with a more sophisticated logic like currently)
  5. Be sure that aststrip.py resets the flags, and deps.py generates dependencies so that subclass method callsites are all rechecked when the signature in supertype changes (this may be already the case, at least partially).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gantsevdenis I can't find your comment that I have got by e-mail, but here is the response. Self-types is something like this:

T = TypeVar('T')
class Base:
    def copy(self: T) -> T: ...

class Sub(Base): ...
x = Sub().copy()  # inferred type of 'x' is 'Sub'

Then class B will be transformed to:
class B(A[int,str]):
def foo(self) -> str:pass
def bar(self,*args)->Callable[[str],int]:pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use PEP 8 everywhere including in docstring and in tests! (You can however skip empty lines in tests whenever it makes sense).

assert isinstance(parent, FuncDef), "got %s" % type(parent)
pnb_args, cnb_args = len(parent.arg_kinds), len(func.arg_kinds)
if cnb_args < pnb_args:
self.fail("Signature of \"%s\" incompatible with supertype \"%s\"" %
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the message generating logic should live messages.py. Also I think we should emit a more specific error message, explaining that we can't copy the signature, and mentioning what went wrong (not enough arguments, etc).

Also in Python you can use 'single quotes to avoid "escaping" double quotes' (also below).

def return_VT(self, *args,**kwargs): return 5
def return_VT_wrong(self,*args,**kwargs): return "" # E: Incompatible return value type (got "str", expected "int")
def return_Tuple(self,*args,**kwargs): return ("", 5) # E: Incompatible return value type (got "Tuple[str, int]", expected "Tuple[int, str]")
[builtins fixtures/dict.pyi]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several important scenarios are still missing, for example:

  • Methods with self-types
  • Generic inheritance where subclass has new type variables with non-trivial mapping to old ones
  • Chained inheritance where C inherits B that inherits A
  • Multiple inheritance
  • Incremental mode tests, see check-incremental.test
  • Daemon mode tests, see fine-grained.test
  • Import cycles
  • Tests that inherited signature is used on external and internal access (including situations with deferrals)

@ilevkivskyi
Copy link
Member

Also please note there are some merge conflicts.

@@ -276,6 +276,10 @@ definitions or calls.
It will assume all arguments have type ``Any`` and always infer ``Any``
as the return type.

``--inherit-signatures``
This allows to use base class function signature in derived classes, without explicitly

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight typo - "This allows the use of..." instead of "This allows to use..."

@rmorshea
Copy link

rmorshea commented Nov 11, 2019

This is awesome work and would go a long way in resolving all my use cases for python/typing#270. Thanks for this!

Edit: this doesn't actually cover python/typing#270 but much of the logic here would probably be shared with any implementation of the features suggested therein.

@robertwb
Copy link

I would love to see this as well, would eliminate a lot of DRY violations that are currently required for typing.

@msullivan
Copy link
Collaborator

I'm going to close this because it has been open with changes requested for quite some time. Anyone should feel free to pick it back up, though!

@msullivan msullivan closed this Oct 18, 2020
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

Successfully merging this pull request may close these issues.

untyped defs on derived classes
5 participants