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

Use tuple[object, ...] and dict[str, object] as upper bounds for ParamSpec.args and ParamSpec.kwargs #12668

Merged
merged 11 commits into from
Apr 29, 2022

Conversation

AlexWaygood
Copy link
Member

Fixes #12386

Description

Mypy currently thinks that a variable annotated with P.args is not iterable, and thinks that a variable annotated with P.kwargs does not have a .pop() method:

from typing import Callable
from typing_extensions import ParamSpec

P = ParamSpec('P')

def func(callback: Callable[P, str]) -> Callable[P, str]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> str:
        for a in args:  # E: "P.args" has no attribute "__iter__" (not iterable)
            pass
        b = 'foo' in args    # E: "P.args" has no attribute "__iter__" (not iterable)
        print(args.count(42))  # E: "P.args" has no attribute "count"
        print(len(args))  # E: Argument 1 to "len" has incompatible type "P.args"; expected "Sized"
        for c, d in kwargs.items():  # E: "P.kwargs" has no attribute "items"
            reveal_type(c)  # N: Revealed type is "Any"
        kwargs.pop('bar')  # E: "P.kwargs" has no attribute "pop"
        return 'baz'
    return inner

If we use tuple[Any, ...] as the upper bound of ParamSpec.args, and dict[str, Any] as the upper bound of ParamSpec.kwargs, all these false-positive errors go away.

Test Plan

Existing tests have been updated, and a test case has been added.

To avoid repetition among the ParamSpec test cases, I've added a new builtins fixture, paramspec.pyi, which basically combines tuple.pyi and dict.pyi.

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 24, 2022

Cc. @A5rocks as the ParamSpec expert :)

@github-actions

This comment has been minimized.

@AlexWaygood AlexWaygood force-pushed the paramspec-args-kwargs branch from 6d8c3d4 to 5ff2309 Compare April 24, 2022 19:37
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Collaborator

@cdce8p cdce8p 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 @AlexWaygood! It addresses one of the major issues left with the ParamSpec implementation, the other one being support for TypeAliases (#11855). The changes itself look simple enough.

Would be awesome if the PR could be included in 0.950, but it might be a bit too late for that 🤔

@@ -83,7 +83,7 @@ def changes_return_type_to_str(x: Callable[P, int]) -> Callable[P, str]: ...
def returns_int(a: str, b: bool) -> int: ...

reveal_type(changes_return_type_to_str(returns_int)) # N: Revealed type is "def (a: builtins.str, b: builtins.bool) -> builtins.str"
[builtins fixtures/tuple.pyi]
[builtins fixtures/paramspec.pyi]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just wondering, any particular reason not to use the new fixture for all ParamSpec tests? I noticed that a few where missing. Especially between L90 - L550.

Copy link
Member Author

@AlexWaygood AlexWaygood Apr 24, 2022

Choose a reason for hiding this comment

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

I used the new fixture in all the tests that broke with the changes made in this PR ;) They mostly broke because builtins.dict is undefined in tuple.pyi, but we need it for a lot of ParamSpec-related tests if ParamSpec.kwargs is bound to dict[str, Any].

There's no particular reason not to use them in all ParamSpec tests -- but I wanted to keep the diff as small as possible, and the remaining tests don't need all the classes included in paramspec.pyi. (I think it's best to use as minimal a fixture as possible, in most situations, to avoid slowing down the test suite.)

@cdce8p cdce8p mentioned this pull request Apr 24, 2022
@JukkaL
Copy link
Collaborator

JukkaL commented Apr 26, 2022

I wonder if the fallback types should be tuple[object, ...] and Mapping[str, object] instead? This is how TypedDict fallbacks behave, and a regular TypeVar has object as the fallback, not Any. The Any types allow breaking runtime type safety without mypy complaining about it. On the other hand, maybe flexibility is more desirable than stricter type checking.

Since Pyre has the reference implementation for PEP 612, one option would be to check how it behaves and implement the same behavior (if it makes sense for mypy).

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 26, 2022

I wonder if the fallback types should be tuple[object, ...] and Mapping[str, object] instead?

Mapping[str, object] might be problematic, since kwargs is a mutable dict at runtime, and users evidently expect to be able to perform mutable-mapping operations on kwargs without mypy complaining (see #12386). And I think dict[str, object] might cause problems due to invariance. (Is that correct? Not sure about that, actually.)

Since Pyre has the reference implementation for PEP 612, one option would be to check how it behaves and implement the same behavior (if it makes sense for mypy).

I've no idea what Pyre does, I'll look into it!

@AlexWaygood
Copy link
Member Author

I've no idea what Pyre does, I'll look into it!

Okay, it looks like Pyre does indeed use tuple[object, ...] for P.args: https://github.com/facebook/pyre-check/blob/3f7c757d05058177f00d25b982bc532b964fb4ab/source/analysis/test/integration/tupleTest.ml#L756

@AlexWaygood
Copy link
Member Author

Hmm, according to Pyre playground, however, Pyre doesn't acknowledge that P.args are iterable or that .items() can be called on P.kwargs.

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Apr 26, 2022

I feel like P.args and P.kwargs should probably either both use Any or both use object. I think users will find it confusing if type-checking is much more strict for P.args than it is for P.kwargs.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@AlexWaygood
Copy link
Member Author

Okay, object seems to work fine, so let's go with that for now.

@AlexWaygood AlexWaygood changed the title Use tuple[Any, ...] and dict[str, Any] as upper bounds for ParamSpec.args and ParamSpec.kwargs Use tuple[object, ...] and dict[str, object] as upper bounds for ParamSpec.args and ParamSpec.kwargs Apr 26, 2022
@AlexWaygood AlexWaygood requested a review from JukkaL April 26, 2022 20:52
Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

Looks good -- using object is consistent with how we do things elsewhere. There is a style issue that would be nice to address -- I think that it just involves moving things around a bit.

mypy/types.py Outdated
prefix: Optional['Parameters'] = None
self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int, *,
upper_bound: Optional[Type] = None,
named_type_func: Optional[Callable[..., 'Instance']] = None, line: int = -1,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently we at least mostly avoid calling back to the type checker in mypy.types or mypy.nodes (even through a callback), and I'd prefer to continue to maintain this principle, as a style issue. Also, it's better if constructors of essentially data objects such as types don't perform any non-trivial logic, such as calling get_fallback.

What about defining a helper function in mypy.semanal_shared that takes some of these arguments (named_type_func etc.) and calculates the fallback? get_fallback would be removed from here and would part of this helper function. You could name it calculate_param_spec_fallback, for example, similar to calculate_tuple_fallback that is already defined there.

Also, can you add argument types for the callback?

Copy link
Member Author

@AlexWaygood AlexWaygood Apr 28, 2022

Choose a reason for hiding this comment

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

I moved the code into semanal_shared -- although it could actually possibly be moved into typeanal, since it seems paramspec args and paramspec kwargs are only ever constructed in typeanal at the moment. (But perhaps that might change in the future -- what do you think?)

Moving the code into typeanal would mean that we wouldn't have to use a callback protocol at all, the functions could just call self.named_type() directly. But the functions might become more tightly coupled with the logic in typeanal.py.

@AlexWaygood AlexWaygood requested a review from JukkaL April 28, 2022 05:47
@github-actions
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

core (https://github.com/home-assistant/core)
+ homeassistant/components/sonos/helpers.py:65: error: Unused "type: ignore" comment

Copy link
Collaborator

@JukkaL JukkaL 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 the updates! Looks good now.

@JukkaL JukkaL merged commit c6cf7cd into python:master Apr 29, 2022
@AlexWaygood AlexWaygood deleted the paramspec-args-kwargs branch April 29, 2022 15:52
@AlexWaygood
Copy link
Member Author

@JukkaL thanks for the reviews!

DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Aug 12, 2022
DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Aug 12, 2022
DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Aug 12, 2022
DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Sep 28, 2022
DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Sep 30, 2022
DMRobertson pushed a commit to matrix-org/synapse that referenced this pull request Sep 30, 2022
* Update mypy and mypy-zope

* Unignore assigning to LogRecord attributes

Presumably python/typeshed#8064 makes this ok

Cherry-picked from #13521

* Remove unused ignores due to mypy ParamSpec fixes

python/mypy#12668

Cherry-picked from #13521

* Remove additional unused ignores

* Fix new mypy complaints related to `assertGreater`

Presumably due to python/typeshed#8077

* Changelog

* Reword changelog

Co-authored-by: Patrick Cloke <clokep@users.noreply.github.com>

Co-authored-by: Patrick Cloke <clokep@users.noreply.github.com>
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.

Paramspec cannot be used as tuple/dict
3 participants