FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add basic support for recursive TypeVar defaults (PEP 696) by cdce8p · Pull Request #16878 · python/mypy · GitHub

Repository navigation

Add basic support for recursive TypeVar defaults (PEP 696) - #16878

Merged
JelleZijlstra merged 2 commits into
python:masterfrom
cdce8p:TypeVar-11-recurrsive-basic
Feb 16, 2024
Merged

JelleZijlstra merged 2 commits into
python:masterfrom
cdce8p:TypeVar-11-recurrsive-basic

Conversation

cdce8p commented Feb 6, 2024

Copy link
Copy Markdown
Collaborator

Ref: #14851

This comment has been minimized.

cdce8p requested a review from JelleZijlstra February 6, 2024 14:37
Comment thread mypy/expandtype.py
self.recursive_tvar_guard[tvar_id] = None
repl = repl.accept(self)
if isinstance(repl, TypeVarType):
repl.default = repl.default.accept(self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Shouldn't we .copy_modified() here?

cdce8p Feb 6, 2024 •
edited
Loading

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That doesn't work, unfortunately. This here is used to link nested TypeVarTypes so if it's changed / evaluated in one location the other stays in sync.

An example from the tests

T1 = TypeVar("T1", default=str)
T2 = TypeVar("T2", default=T1)

class ClassD1(Generic[T1, T2]): ...

k = ClassD1()
reveal_type(k)  # should be `ClassD1[str, str]`

For the first pass the type variables are as follows

T1`1 = str
T2`2 = T1`1 = str

The section here now replaces the default for T2 with the tbd of the default of T1:

T1`1 = str
T2`2 = <result of T1`1>

--
With .copy_modified it would be a copy of the T1'1 thus when it's evaluated it's not used for T2. The test would still return

ClassD1[str, T1`1 = str]

At least that's how I understand it 😅

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Looked at it more closely. It's basically as said above. In particular, it's called in freshend_function_type_vars. We enter this function with linked TypeVars

callee.variables[0] == callee.variables[1].default  # True

Then create new ones for each and link them again in expand_type, so that

tvs[0] == tvs[1].default  # True

# and by extension
fresh.variables[0] == fresh.variables[1].default  # True

mypy/mypy/expandtype.py

Lines 119 to 130 in 517f5ae

def freshen_function_type_vars(callee: F) -> F:
"""Substitute fresh type variables for generic function type variables."""
if isinstance(callee, CallableType):
if not callee.is_generic():
return cast(F, callee)
tvs = []
tvmap: dict[TypeVarId, Type] = {}
for v in callee.variables:
tv = v.new_unification_variable(v)
tvs.append(tv)
tvmap[v.id] = tv
fresh = expand_type(callee, tvmap).copy_modified(variables=tvs)

Comment thread mypy/semanal.py Outdated
for name, tvar_expr in declared_tvars:
tvar_expr_default = tvar_expr.default
if isinstance(tvar_expr_default, UnboundType):
# Assumption here is that the names cannot be duplicated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Unfortunately that's not a safe assumption

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Do you have an example I could try? Mypy will already complain about this here and reject the second TypeVar call.

T1 = TypeVar("T1", default=int)
T1 = TypeVar("T1", default=str)  # error
error: Cannot redefine "T1" as a type variable
error: Invalid assignment target
error: "int" not callable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I think you have to import the other TypeVar from a different module.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That results in the same error message. The second TypeVar call would just be ignored. The logic here would still as it only compares the actual TypeVar name type_var_name = fullname.rpartition(".")[2]. Not the complete fullname.

E.g. the comparison is with T1 not __main__.T1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I thought something like this might fail:

[case testTypeVarDefaultsMultipleFiles]

from typing import Generic, TypeVar
from file2 import T as T2

T = TypeVar('T')

class Gen(Generic[T, T2]):
    pass

def func(a: Gen, b: Gen[str], c: Gen[str, str]) -> None:
    reveal_type(a)  # N: Revealed type is "__main__.Gen[Any, builtins.int]"
    reveal_type(b)  # N: Revealed type is "__main__.Gen[builtins.str, builtins.int]"
    reveal_type(c)  # N: Revealed type is "__main__.Gen[builtins.str, builtins.str]"

[file file2.py]
from typing import TypeVar

T = TypeVar('T', default=int)

But this passes as expected, and I couldn't find a variation that fails. So I suppose this is fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This variation fails:

[case testTypeVarDefaultsMultipleFiles]

from typing import Generic, TypeVar
from file2 import T as T2

T = TypeVar('T', default=T2)

class Gen(Generic[T2, T]):
    pass

def func(a: Gen, b: Gen[str], c: Gen[str, float]) -> None:
    reveal_type(a)  # N: Revealed type is "__main__.Gen[builtins.int, builtins.int]"
    reveal_type(b)  # N: Revealed type is "__main__.Gen[builtins.str, builtins.str]"
    reveal_type(c)  # N: Revealed type is "__main__.Gen[builtins.str, builtins.float]"

[file file2.py]
from typing import TypeVar

T = TypeVar('T', default=int)
Expected:
  main:11: note: Revealed type is "__main__.Gen[builtins.int, builtins.int]" (diff)
  main:12: note: Revealed type is "__main__.Gen[builtins.str, builtins.str]" (diff)
  main:13: note: Revealed type is "__main__.Gen[builtins.str, builtins.float]"
Actual:
  main:11: note: Revealed type is "__main__.Gen[builtins.int, T2?]" (diff)
  main:12: note: Revealed type is "__main__.Gen[builtins.str, T2?]" (diff)
  main:13: note: Revealed type is "__main__.Gen[builtins.str, builtins.float]"

Not code I'd expect anyone to actually write, but it does seem like it should be legal.

cdce8p Feb 16, 2024 •
edited
Loading

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for the test, that helped! Tbh I could also have though about this earlier but it's actually quite simple to add a name lookup here 😅

cdce8p requested a review from JelleZijlstra February 7, 2024 06:38
cdce8p added the topic-pep-696 TypeVar defaults label Feb 8, 2024

Copy link
Copy Markdown
Contributor

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

steam.py (https://github.com/Gobot1234/steam.py)
+ steam/ext/commands/commands.py:851: error: Overloaded function implementation does not accept all possible arguments of signature 3  [misc]
+ steam/ext/commands/commands.py:851: error: Overloaded function implementation cannot produce return type of signature 3  [misc]

JelleZijlstra merged commit 2e5174c into python:master Feb 16, 2024
cdce8p deleted the TypeVar-11-recurrsive-basic branch February 16, 2024 14:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic-pep-696 TypeVar defaults

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL