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

Add copy.replace for 3.13 #12262

Merged
merged 24 commits into from
Jul 23, 2024
Merged

Conversation

max-muoto
Copy link
Contributor

Add copy.replace which is new in 3.13: https://docs.python.org/3.13/library/copy.html#copy.replace

@@ -11,6 +12,10 @@ PyStringMap: Any
def deepcopy(x: _T, memo: dict[int, Any] | None = None, _nil: Any = []) -> _T: ...
def copy(x: _T) -> _T: ...

if sys.version_info >= (3, 13):
def replace(obj: _T, /, **changes: Any) -> _T: ...
__all__ += ["replace"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See us mostly using += in most places with __all__, guessing this is for better static analysis support, or something like that?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is fine, it's so we don't have to repeat the whole list for every version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I meant versus using append.

Copy link
Member

Choose a reason for hiding this comment

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

Except it actually isn't in __all__: https://github.com/python/cpython/blob/89bf33732fd53fbb954aa088cfb34f13264746ad/Lib/copy.py#L59 . So we shouldn't add it in the stub either.

This may be an oversight, so consider opening an issue over on CPython for adding replace to copy.__all__. But as long as it's not there at runtime, we shouldn't add it in the stub either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My bad, let me fix that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@max-muoto max-muoto marked this pull request as draft July 3, 2024 00:30

This comment has been minimized.

stdlib/copy.pyi Outdated
@@ -11,6 +16,10 @@ PyStringMap: Any
def deepcopy(x: _T, memo: dict[int, Any] | None = None, _nil: Any = []) -> _T: ...
def copy(x: _T) -> _T: ...

if sys.version_info >= (3, 13):
def replace(obj: _SR, /, **kwargs: Any) -> _SR: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Higher-kinded types could be cool here, but obviously no way to get great kwargs type-checking. Even in that case you can't use P.kwargs without P.args.

@max-muoto
Copy link
Contributor Author

Some classes are missing __replace__ dunders but will tackle that separately.

@max-muoto max-muoto marked this pull request as ready for review July 3, 2024 00:44

This comment has been minimized.

2 similar comments

This comment has been minimized.

This comment has been minimized.

@max-muoto
Copy link
Contributor Author

max-muoto commented Jul 3, 2024

Interestingly, Pyright is fine with this example, and MpPy is not (causing the test failure):

from __future__ import annotations
from typing import Self, Protocol, ClassVar
import copy


class ReplaceableClass:
    def __init__(self, val: int) -> None:
        self.val = val

    def __replace__(self, val: int) -> Self:
        cpy = copy.copy(self)
        cpy.val = val
        return cpy


from collections.abc import Callable


class _SupportsReplace(Protocol):
    __replace__: Callable[..., Self]



def foo(sr: _SupportsReplace) -> None:
    ...

foo(ReplaceableClass(23))

Would you consider this a MyPy bug @JelleZijlstra?

@max-muoto
Copy link
Contributor Author

max-muoto commented Jul 3, 2024

Interestingly, Pyright is fine with this example, and MpPy is not (causing the test failure):

from __future__ import annotations
from typing import Self, Protocol, ClassVar
import copy


class ReplaceableClass:
    def __init__(self, val: int) -> None:
        self.val = val

    def __replace__(self, val: int) -> Self:
        cpy = copy.copy(self)
        cpy.val = val
        return cpy


from collections.abc import Callable


class _SupportsReplace(Protocol):
    __replace__: Callable[..., Self]



def foo(sr: _SupportsReplace) -> None:
    ...

foo(ReplaceableClass(23))

Would you consider this a MyPy bug @JelleZijlstra?

Using paramspec is a workaround that works for both: 58bb691

This comment has been minimized.

1 similar comment

This comment has been minimized.

@erictraut
Copy link
Contributor

I think there's a problem with the way you defined the _SupportsReplace protocol. You're defining __replace__ as an instance variable.

I recommend changing it to:

class _SupportsReplace(Protocol):
    def __replace__(self, *args: Any, **kwargs: Any) -> Self:
        ...

Mypy works fine if you define it this way.

@max-muoto
Copy link
Contributor Author

max-muoto commented Jul 3, 2024

I think there's a problem with the way you defined the _SupportsReplace protocol. You're defining __replace__ as an instance variable.

I recommend changing it to:

class _SupportsReplace(Protocol):
    def __replace__(self, *args: Any, **kwargs: Any) -> Self:
        ...

Mypy works fine if you define it this way.

Thanks, I think I thought originally this wouldn't work since it doesn't work if you leave out *args with Pyright.

Fixed: 860f674

This comment has been minimized.

@max-muoto
Copy link
Contributor Author

Added replace to __all__ based on python/cpython@7c66906

@JelleZijlstra
Copy link
Member

CI will fail for now since it's not in __all__ in the last beta. We can either wait for the next beta or merge this without __all__ for now (which would be my preference).

This comment has been minimized.

_SR = TypeVar("_SR", bound=_SupportsReplace)

class _SupportsReplace(Protocol):
# In reality doesn't support args, but there's no other great way to express this.
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically speaking, it does support positionals, but those will be passed as keywords. What it does not support however is positional-only arguments.

On the other hand, you can just force people to use keyword-only arguments so that it matches https://docs.python.org/3.13/library/copy.html#object.__replace__. Or at least, make self a positional-only argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Yes, callables with *args: Any, **kwargs: Any are treated specially in the spec.

@picnixz
Copy link
Contributor

picnixz commented Jul 4, 2024

Some classes are missing __replace__ dunders but will tackle that separately.

We just added __replace__ on AST nodes (it's not in 3.13) in case you're interested in adding it for 3.14 only.

@max-muoto
Copy link
Contributor Author

Some classes are missing __replace__ dunders but will tackle that separately.

We just added __replace__ on AST nodes (it's not in 3.13) in case you're interested in adding it for 3.14 only.

My guess is we'll get around this once we start running stubtest on 3.14.

_SR = TypeVar("_SR", bound=_SupportsReplace)

class _SupportsReplace(Protocol):
# In reality doesn't support args, but there's no other great way to express this.
Copy link
Member

Choose a reason for hiding this comment

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

Yes, callables with *args: Any, **kwargs: Any are treated specially in the spec.

This comment has been minimized.

@max-muoto
Copy link
Contributor Author

max-muoto commented Jul 23, 2024

@JelleZijlstra It looks like we're running against beta 4 now, do we want to merge this to resolve the CI failure?

This comment has been minimized.

Copy link
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

@JelleZijlstra
Copy link
Member

Sure. Seems like there's some other unrelated new stubtest failures too.

@JelleZijlstra JelleZijlstra merged commit 2909053 into python:main Jul 23, 2024
60 of 63 checks passed
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.

None yet

4 participants