-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Function compatibility rewrite #2521
Conversation
I am aware that typechecking typeshed fails. In the last week, we added the werkzeug library to typeshed, which is not clean according to the new rules. python/typeshed@021b162 There are two main sources of the typechecking problems:
Sidenote: this code stops complaining about EDIT: aha, it looks like the method it will find in the MRO will be the one in the base class and not the mixin. The one in the base class then is a subtype of the one in the mixin, so the subtyping rules stop complaining; it's a valid instance of base and an valid instance of mixin. Ok, but I don't think that's what the writer intended -- maybe the writer intended methods to get resolved from the mixin before the base? |
There is future work that builds on this change that expands the |
Thanks for the PR! This will take some careful study to review. Switching the signature of Are the typeshed changes related? |
This change itself shouldn't update typeshed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for implementing this -- this fixes a bunch of problematic edge cases and paves the way for more expressive callable type annotations. I wrote some minor comments. I'll need to reason through various corner cases still.
ee_var = everything | ||
ee_var = everywhere | ||
|
||
ee_var = specific_1 # The difference between Callable[[...], blah] and one with a *args: Any, **kwargs: Any is that the ... goes loosely both ways. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra []
around ...
in comment: should be Callable[..., blah]
.
ee_var = everywhere | ||
|
||
ee_var = specific_1 # The difference between Callable[[...], blah] and one with a *args: Any, **kwargs: Any is that the ... goes loosely both ways. | ||
ee_def = specific_1 # E: Incompatible types in assignment (expression has type Callable[[int, str], None], variable has type Callable[[Any, Any], None]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message is confusing (missing indication of *
and **
in the variable).
@@ -542,6 +543,13 @@ def deserialize(cls, data: JsonDict) -> 'FunctionLike': | |||
_dummy = object() # type: Any | |||
|
|||
|
|||
FormalArgument = NamedTuple('FormalArgument', [ | |||
('name', str), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Optional[...]
for types of items that can be None
(for documentation, these aren't checked yet).
return True | ||
|
||
|
||
def is_left_more_general( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this could have a more descriptive name, since this also checks things that arguably aren't about being more general, such as position. Random ideas: are_args_compatible
, is_left_corresponding_and_more_general
# used for in practice. | ||
|
||
# Every argument in R must have a corresponding argument in L, and every | ||
# required argument in L must have a corresponding arugment in R. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: arugment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The typo arugment
is still there :)
# sure left has its own infinite set of optional named arguments. | ||
if not left.is_kw_arg: | ||
return False | ||
left_names = set([name for name in left.arg_names if name is not None]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder: Use set comprehensions.
constructor, | ||
arg_name, | ||
strip_quotes(self.format(arg_type)), | ||
arg_kind in (ARG_NAMED, ARG_NAMED_OPT))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we have this default to False
and omit the third argument if it's False
? Currently the representations of arguments feel overly long to me, and this would help a bit.
And I'd like to add that some of the edge cases that this PR fixes are super subtle and required a lot of sweating the details. |
This doesn't look right:
If I add a type annotation to This doesn't generate an error, though it doesn't look okay:
I think that we need to make sure that each left argument only maps to a single required right argument. |
bc2f11a
to
16cf426
Compare
Can you fix the merge conflict? I'll try to review the latest changes later this week. |
The new algorithm correctly handles argument names. It pays attention to them when matching up arguments for function assignments, but not for overrides (because too many libraries vary the agument names between superclass and subclass). I'll include a full writeup in prose of the new algorithm in the pull request. Print callable types with named arguments differently from each other when important Fix importFunctionAndAssignFunction test to match var names
16cf426
to
d752499
Compare
Ok. I rebased, and also made a small refactor -- I'm not totally sure how I feel about it, because it makes the code more readable (and shorter!), but also introduces a slight exacerbation of a current circular dependency. I do thing the concept of a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran this against Dropbox internal repos and it found at least one thing that looked like a bug -- nice!
A few things are still causing trouble:
- Mypy now complains about
typeshed/stdlib/2/posixpath.pyi
-- the stub may need to be tweaked a bit. - Maybe we should not complain about incompatible names for positional arguments if one of the callables is dynamically typed (all arguments and return value have
Any
types). Mypy tries to not give many false positives for legacy unnanotated code, and here we now generate more errors for things which might be innocent, such as having an argument that is implicitly considered positional-only (but without the dunder prefix, since that's a recent thing) so the name isn't important. I saw conditional function definitions that are now are generating errors.
# used for in practice. | ||
|
||
# Every argument in R must have a corresponding argument in L, and every | ||
# required argument in L must have a corresponding arugment in R. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The typo arugment
is still there :)
Looks good now -- thanks for powering through! |
✨ 🍰 ✨ However, FYI, this broke the typeshed build since it uncovered some stub problems. Fixing those now:
|
Hmm! I'm sorry! For my future edification, does the typeshed build include more stringent checking than the checks of typeshed in the mypy build? I'd merged a few changes to typeshed to get that passing, but I'd like to not miss stuff in the future.
… On Dec 22, 2016, at 1:37 PM, Łukasz Langa ***@***.***> wrote:
✨ 🍰 ✨
However, FYI, this broke the typeshed build since it uncovered some stub problems. Fixing those now:
$ tests/mypy_test.py -p2
running mypy --python-version 2.7 --strict-optional # with 524 files
third_party/2/werkzeug/wrappers.pyi:194: error: Definition of "freeze" in base class "BaseResponse" is incompatible with definition in base class "ETagResponseMixin"
third_party/2/werkzeug/contrib/testtools.pyi:13: error: Definition of "freeze" in base class "BaseResponse" is incompatible with definition in base class "ETagResponseMixin"
third_party/2/werkzeug/datastructures.pyi:176: error: Definition of "__delitem__" in base class "ImmutableHeadersMixin" is incompatible with definition in base class "Headers"
third_party/2/itsdangerous.pyi:149: error: Definition of "load_payload" in base class "URLSafeSerializerMixin" is incompatible with definition in base class "Serializer"
third_party/2/itsdangerous.pyi:152: error: Definition of "load_payload" in base class "URLSafeSerializerMixin" is incompatible with definition in base class "Serializer"
third_party/2and3/boto/connection.pyi:114: error: Signature of "make_request" incompatible with supertype "AWSAuthConnection"
third_party/2and3/boto/s3/connection.pyi:71: error: Signature of "make_request" incompatible with supertype "AWSAuthConnection"
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub, or mute the thread.
|
Starting with python/mypy#2521 mypy is performing stricter function signature checks. This makes the stubs diverge from the actual implementation but makes the stubs internally consistent. Since this is an actual typing issue in the base implementation, we need to defer to the original authors to fix it.
Starting with python/mypy#2521 mypy is performing stricter function signature checks. This makes the stubs diverge from the actual implementation but makes the stubs internally consistent. Since this is an actual typing issue in the base implementation, we need to defer to the original authors to fix it.
Starting with python/mypy#2521 mypy is performing stricter function signature checks. This makes the stubs diverge from the actual implementation but makes the stubs internally consistent. Since this is an actual typing issue in the base implementation, we need to defer to the original authors to fix it.
Starting with python/mypy#2521 mypy is performing stricter function signature checks. This makes the stubs diverge from the actual implementation but makes the stubs internally consistent. Since this is an actual typing issue in the base implementation, we need to defer to the original authors to fix it.
@sixolet, typeshed runs Mypy over all stubs which the Mypy test runner currently doesn't do. Maybe it should? In the mean time, re-running typeshed tests is your best bet to be sure nothing breaks. As you can see above, I reported bugs and put workarounds in typeshed for everything except for https://github.com/boto/boto. In this case, their implementation is really seriously breaking the Liskov Substitution Principle. Consider: class AWSAuthConnection(object):
def make_request(self, method, path, headers=None, data='', host=None,
auth_path=None, sender=None, override_num_retries=None,
params=None, retry_handler=None): ...
class AWSQueryConnection(AWSAuthConnection):
def make_request(self, action, params=None, path='/', verb='GET'): ... I'm skeptical if we can expect upstream to fix their design issue in this case, because this is pretty ancient code that's still in major use. This API is just broken and the stub cannot really do anything about it. We need a way to let Mypy know to ignore this particular problem in stubs. In the mean time, I excluded |
Starting with python/mypy#2521 mypy is performing stricter function signature checks. This makes the stubs diverge from the actual implementation but makes the stubs internally consistent. Since this is an actual typing issue in the base implementation, we need to defer to the original authors to fix it. Sadly, in this case the breakage is rather fundamental and unlikely to get fixed by upstream. Consider: ``` class AWSAuthConnection(object): def make_request(self, method, path, headers=None, data='', host=None, auth_path=None, sender=None, override_num_retries=None, params=None, retry_handler=None): ... class AWSQueryConnection(AWSAuthConnection): def make_request(self, action, params=None, path='/', verb='GET'): ... ``` Hence, until we have a workaround for the error produced by Mypy, we're excluding those stubs from being tested against.
Before, the logic for determining whether a function was an
Any
-permissive subtype of another function compared their argument types in order, with some special provision for*args
. It did not handle keyword-only arguments, or**kwargs
, or argument names being different, or any other such situation.This PR introduces an algorithm for
is_callable_subtype
that pairs up corresponding arguments by both name and position, and makes sure the argument in the ostensible subtype is "more general". It also deals with what happens when an arg in one function corresponds to different args in the other function when you try to pair it up by name vs. when you try to pair it up by argument position.Note that specifically for the case of determining whether a function is a valid override, we ignore the names of positional arguments; too many people regularly pay no attention to the names of positional arguments of functions in subtypes in code they write, even if it's not a technically perfect python subtype. We do error in the case the names of positional arguments are transposed (ex. argument
foo
is the first argument in one function, but the second argument in the other), as this is likely to be an actual mistake.We pay attention to the names of positional arguments for assignment statements and argument accepting.