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

stubgen: Fix call-based namedtuple omitted from class bases #14680

Merged
merged 9 commits into from
May 6, 2023

Conversation

hamdanal
Copy link
Collaborator

@hamdanal hamdanal commented Feb 11, 2023

Fixes #9901
Fixes #13662

Fix inheriting from a call-based collections.namedtuple / typing.NamedTuple definition that was omitted from the generated stub.

This automatically adds support for the call-based NamedTuple in general not only in class bases (Closes #13788).

An example before and after Input:
import collections
import typing
from collections import namedtuple
from typing import NamedTuple

CollectionsCall = namedtuple("CollectionsCall", ["x", "y"])

class CollectionsClass(namedtuple("CollectionsClass", ["x", "y"])):
    def f(self, a):
        pass

class CollectionsDotClass(collections.namedtuple("CollectionsClass", ["x", "y"])):
    def f(self, a):
        pass

TypingCall = NamedTuple("TypingCall", [("x", int | None), ("y", int)])

class TypingClass(NamedTuple):
    x: int | None
    y: str

    def f(self, a):
        pass

class TypingClassWeird(NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])):
    z: float | None

    def f(self, a):
        pass

class TypingDotClassWeird(typing.NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])):
    def f(self, a):
        pass

Output diff (before and after):

diff --git a/before.pyi b/after.pyi
index c88530e2c..95ef843b4 100644
--- a/before.pyi
+++ b/after.pyi
@@ -1,26 +1,29 @@
+import typing
 from _typeshed import Incomplete
 from typing_extensions import NamedTuple

 class CollectionsCall(NamedTuple):
     x: Incomplete
     y: Incomplete

-class CollectionsClass:
+class CollectionsClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])):
     def f(self, a) -> None: ...

-class CollectionsDotClass:
+class CollectionsDotClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])):
     def f(self, a) -> None: ...

-TypingCall: Incomplete
+class TypingCall(NamedTuple):
+    x: int | None
+    y: int

 class TypingClass(NamedTuple):
     x: int | None
     y: str
     def f(self, a) -> None: ...

-class TypingClassWeird:
+class TypingClassWeird(NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])):
     z: float | None
     def f(self, a) -> None: ...

-class TypingDotClassWeird:
+class TypingDotClassWeird(typing.NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])):
     def f(self, a) -> None: ...

Fixes python#9901
Fixes python#13662

Fix inheriting from a call-based `collections.namedtuple` /
`typing.NamedTuple` definition that was omitted from the generated stub.

This automatically adds support for the call-based `NamedTuple` in
general not only as a based class (Closes python#13788).

<details>
<summary>An example before and after</summary>
Input:
```python
import collections
import typing
from collections import namedtuple
from typing import NamedTuple

CollectionsCall = namedtuple("CollectionsCall", ["x", "y"])

class CollectionsClass(namedtuple("CollectionsClass", ["x", "y"])):
    def f(self, a):
        pass

class CollectionsDotClass(collections.namedtuple("CollectionsClass", ["x", "y"])):
    def f(self, a):
        pass

TypingCall = NamedTuple("TypingCall", [("x", int | None), ("y", int)])

class TypingClass(NamedTuple):
    x: int | None
    y: str

    def f(self, a):
        pass

class TypingClassWeird(NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])):
    z: float | None

    def f(self, a):
        pass

class TypingDotClassWeird(typing.NamedTuple("TypingClassWeird", [("x", int | None), ("y", str)])):
    def f(self, a):
        pass
```

Output diff (before and after):
```diff
diff --git a/before.pyi b/after.pyi
index c88530e2c..95ef843b4 100644
--- a/before.pyi
+++ b/after.pyi
@@ -1,26 +1,29 @@
+import typing
 from _typeshed import Incomplete
 from typing_extensions import NamedTuple

 class CollectionsCall(NamedTuple):
     x: Incomplete
     y: Incomplete

-class CollectionsClass:
+class CollectionsClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])):
     def f(self, a) -> None: ...

-class CollectionsDotClass:
+class CollectionsDotClass(NamedTuple('CollectionsClass', [('x', Incomplete), ('y', Incomplete)])):
     def f(self, a) -> None: ...

-TypingCall: Incomplete
+class TypingCall(NamedTuple):
+    x: int | None
+    y: int

 class TypingClass(NamedTuple):
     x: int | None
     y: str
     def f(self, a) -> None: ...

-class TypingClassWeird:
+class TypingClassWeird(NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])):
     z: float | None
     def f(self, a) -> None: ...

-class TypingDotClassWeird:
+class TypingDotClassWeird(typing.NamedTuple('TypingClassWeird', [('x', int | None), ('y', str)])):
     def f(self, a) -> None: ...
```
</details>
mypy/stubgen.py Outdated Show resolved Hide resolved
mypy/stubgen.py Outdated Show resolved Hide resolved
mypy/stubgen.py Show resolved Hide resolved
mypy/stubgen.py Show resolved Hide resolved
mypy/stubgen.py Outdated Show resolved Hide resolved
@hamdanal
Copy link
Collaborator Author

I made the requested changes.
I also deleted the NamedTuplePrinter class that I added in an earlier commit and moved the logic to the existing AliasPrinter. This makes the code a bit cleaner.

@ikonst
Copy link
Contributor

ikonst commented Mar 10, 2023

Curious how this change didn't invalidate a test

-and f"{callee.expr.name}.{callee.name}" in "collections.namedtuple"
+and f"{callee.expr.name}.{callee.name}" == "collections.namedtuple"

@hamdanal
Copy link
Collaborator Author

Curious how this change didn't invalidate a test

-and f"{callee.expr.name}.{callee.name}" in "collections.namedtuple"
+and f"{callee.expr.name}.{callee.name}" == "collections.namedtuple"

I think because "collections.namedtuple" in "collections.namedtuple" is True

mypy/stubgen.py Outdated Show resolved Hide resolved
@ikonst
Copy link
Contributor

ikonst commented Mar 10, 2023

I think because "collections.namedtuple" in "collections.namedtuple" is True

🤦 🤣

@ikonst
Copy link
Contributor

ikonst commented Mar 10, 2023

(note that I'm not a maintainer, just a fellow contributor)

from _typeshed import Incomplete
from typing import NamedTuple

class X(NamedTuple('X', [('a', Incomplete), ('b', Incomplete)])): ...
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, in typeshed we prefer to avoid call-based namedtuples wherever possible, so we would usually do something like this for this kind of thing:

class _XBase(NamedTuple):
    a: Incomplete
    b: Incomplete

class X(_XBase): ...

I'm guessing it might be hard to achieve that from stubgen, though?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree, call-based syntax isn't desirable. It is however tricky to get the conversion to a stub-only class definition right. In fact I tried briefly when I started with the PR then decided it is safer and much simpler to keep the call-based definition.

Note that even though this syntax is ugly/undesirable, it type-checks perfectly fine by both mypy and pyright and this PR still fixes the issue where stubgen generated wrong stubs without any warning. It also makes a posterior step of manual conversion to a class definition much simpler as the information is now there in the stub instead of having to grep the python source for all namedtuple bases in class definitions.

Having said that, I don't mind implementing this, whether in this PR or in a follow up one. I do like to get the opinion of a stubgen maintainer/expert before working on it though as it will require some work. If the answer is do it, I have a couple of questions regarding this step that I can ask then.

Copy link
Collaborator Author

@hamdanal hamdanal May 6, 2023

Choose a reason for hiding this comment

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

@AlexWaygood are you comfortable enough this change will be welcome so that I can work on it or should we ping someone?

Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine to generate the stubs as in this PR. If you fix the merge conflict, I can review this PR.

# Incomplete as field types
fields_str = ", ".join(f"({f!r}, {t})" for f, t in nt_fields)
else:
# Invalid namedtuple() call, cannot determine fields
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you generate NamedTuple(name, [Incomplete]) as the base in this case. That seems wrong, as it's an invalid type. I think we should make Incomplete the base in this case.

Copy link
Member

Choose a reason for hiding this comment

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

Also, is this code path tested?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call. I fixed it. The tests now cover the invalid namedtuple call code path.

@JelleZijlstra JelleZijlstra self-assigned this May 6, 2023
@JelleZijlstra JelleZijlstra merged commit 171e6f8 into python:master May 6, 2023
@hamdanal hamdanal deleted the stubgen-namedtuple branch May 6, 2023 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG: stubgen from class does not inheritd namedtuple Stubgen omits namedtuple fields for subclasses.
4 participants