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 omits namedtuple fields for subclasses. #9901

Closed
domdfcoding opened this issue Jan 12, 2021 · 1 comment · Fixed by #14680
Closed

Stubgen omits namedtuple fields for subclasses. #9901

domdfcoding opened this issue Jan 12, 2021 · 1 comment · Fixed by #14680
Labels

Comments

@domdfcoding
Copy link
Contributor

Bug Report

To Reproduce

Consider the common pattern of subclassing the output of the namedtuple() function to add extra methods:

from collections import namedtuple

class EmployeeRecord(namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')):
    
	def is_junior():
		return self.age < 18
  1. Save the above code to a file.
  2. Run stubgen on it.

Expected Behavior

I would have expected the stubs generated by stubgen to include the namedtuple's fields (name, age, etc.), typed as Any if no type can be inferred.
I expected the output to look something like:

from typing import NamedTuple

class EmployeeRecord(NamedTuple):
	name: Any
	age: Any
	title: Any
	department: Any
	paygrade: Any
	def is_junior(): ...

Actual Behavior

However, the output is instead:

class EmployeeRecord:
	def is_junior(): ...

EmployeeRecord doesn't subclass from NamedTuple and doesn't have the expected attributes.

Your Environment

  • Mypy version used: 0.790 and 0.800+dev.e9edcb957f341dcf7a14bb7b7acfd4186fb1248b
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: Python 3.8.5
  • Operating system and version: Ubuntu 20.04
@domdfcoding domdfcoding added the bug mypy got something wrong label Jan 12, 2021
@Avasam
Copy link
Contributor

Avasam commented Oct 12, 2022

I'll expand on this namedtuple vs NamedTuple:
mypy could (should?) also generate stubs using NamedTuple, similar to how pyright --createstub does it:
stubgen -p cv2.utils

Current behaviour:

from collections import namedtuple

NativeMethodPatchedResult = namedtuple('NativeMethodPatchedResult', ['py', 'native'])

Should be:

from _typeshed import Incomplete
from typing import NamedTuple

class NativeMethodPatchedResult(NamedTuple):
    py: Incomplete
    native: Incomplete

hamdanal added a commit to hamdanal/mypy that referenced this issue Feb 11, 2023
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>
JelleZijlstra pushed a commit that referenced this issue May 6, 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).

<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants