ruff/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047.py
Brent Westbrook bb6fb4686d
[pyupgrade] Add rules to use PEP 695 generics in classes and functions (UP046, UP047) (#15565)
## Summary

This PR extends our [PEP 695](https://peps.python.org/pep-0695) handling
from the type aliases handled by `UP040` to generic function and class
parameters, as suggested in the latter two examples from #4617:

```python
# Input
T = TypeVar("T", bound=float)
class A(Generic[T]):
    ...

def f(t: T):
    ...

# Output
class A[T: float]:
    ...

def f[T: float](t: T):
    ...
```

I first implemented this as part of `UP040`, but based on a brief
discussion during a very helpful pairing session with @AlexWaygood, I
opted to split them into rules separate from `UP040` and then also
separate from each other. From a quick look, and based on [this
issue](https://github.com/asottile/pyupgrade/issues/836), I'm pretty
sure neither of these rules is currently in pyupgrade, so I just took
the next available codes, `UP046` and `UP047`.

The last main TODO, noted in the rule file and in the fixture, is to
handle generic method parameters not included in the class itself, `S`
in this case:

```python
T = TypeVar("T")
S = TypeVar("S")

class Foo(Generic[T]):
    def bar(self, x: T, y: S) -> S: ...
```

but Alex mentioned that that might be okay to leave for a follow-up PR.

I also left a TODO about handling multiple subclasses instead of bailing
out when more than one is present. I'm not sure how common that would
be, but I can still handle it here, or follow up on that too.

I think this is unrelated to the PR, but when I ran `cargo dev
generate-all`, it removed the rule code `PLW0101` from
`ruff.schema.json`. It seemed unrelated, so I left that out, but I
wanted to mention it just in case.

## Test Plan

New test fixture, `cargo nextest run`

Closes #4617, closes #12542

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-22 11:35:21 -05:00

59 lines
1.3 KiB
Python

from collections.abc import Callable
from typing import Any, AnyStr, ParamSpec, TypeVar, TypeVarTuple
from somewhere import Something
S = TypeVar("S", str, bytes) # constrained type variable
T = TypeVar("T", bound=float)
Ts = TypeVarTuple("Ts")
P = ParamSpec("P")
def f(t: T) -> T:
return t
def g(ts: tuple[*Ts]) -> tuple[*Ts]:
return ts
def h(
p: Callable[P, T],
# Comment in the middle of a parameter list should be preserved
another_param,
and_another,
) -> Callable[P, T]:
return p
def i(s: S) -> S:
return s
# NOTE this case is the reason the fix is marked unsafe. If we can't confirm
# that one of the type parameters (`Something` in this case) is a TypeVar,
# which we can't do across module boundaries, we will not convert it to a
# generic type parameter. This leads to code that mixes old-style standalone
# TypeVars with the new-style generic syntax and will be rejected by type
# checkers
def broken_fix(okay: T, bad: Something) -> tuple[T, Something]:
return (okay, bad)
def any_str_param(s: AnyStr) -> AnyStr:
return s
# these cases are not handled
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
def default_var(v: V) -> V:
return v
def outer():
def inner(t: T) -> T:
return t