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

Interfaces for the copy standard library #29

Merged
merged 3 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1244,11 +1244,53 @@ Interfaces for emulating buffer types using the [buffer protocol][BP].

[BP]: https://docs.python.org/3/reference/datamodel.html#python-buffer-protocol

### `copy`

For the [`copy`][CP] standard library, `optype` provides the following
interfaces:

<table>
<tr>
<th align="center">operator</th>
<th colspan="2" align="center">operand</th>
</tr>
<tr>
<td>expression</td>
<td>method(s)</td>
<th>type</th>
</tr>
<tr>
<td><code>copy.copy(_)</code></td>
<td><code>__copy__</code></td>
<td><code>CanCopy[T]</code></td>
</tr>
<tr>
<td><code>copy.deepcopy(_, memo={})</code></td>
<td><code>__deepcopy__</code></td>
<td><code>CanDeepcopy[T]</code></td>
</tr>
<tr>
<td><code>copy.replace(_, **changes: V)</code> (Python 3.13+)</td>
<td><code>__replace__</code></td>
<td><code>CanReplace[T, V]</code></td>
</tr>
</table>

And for convenience, there are the runtime-checkable aliases for all three
interfaces, with `T` bound to `Self`. These are roughly equivalent to:

```python
type CanCopySelf = CanCopy[CanCopySelf]
type CanDeepcopySelf = CanDeepcopy[CanDeepcopySelf]
type CanReplaceSelf[V] = CanReplace[CanReplaceSelf[V], V]
```

[CP]: https://docs.python.org/3.13/library/copy.html

## Future plans

- Support for Python versions before 3.12 (#19).
- More standard library protocols, e.g.
- `copy` (#20)
- `dataclasses` (#21)
- `pickle` (#22).
- `typing.NamedTuple` (#23)
Expand Down
12 changes: 12 additions & 0 deletions optype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
'CanCeil',
'CanComplex',
'CanContains',
'CanCopy',
'CanCopySelf',
'CanDeepcopy',
'CanDeepcopySelf',
'CanDelattr',
'CanDelete',
'CanDelitem',
Expand Down Expand Up @@ -86,6 +90,8 @@
'CanRTruediv',
'CanRXor',
'CanReleaseBuffer',
'CanReplace',
'CanReplaceSelf',
'CanRepr',
'CanReversed',
'CanRound',
Expand Down Expand Up @@ -305,6 +311,10 @@
CanCeil,
CanComplex,
CanContains,
CanCopy,
CanCopySelf,
CanDeepcopy,
CanDeepcopySelf,
CanDelattr,
CanDelete,
CanDelitem,
Expand Down Expand Up @@ -375,6 +385,8 @@
CanRTruediv,
CanRXor,
CanReleaseBuffer,
CanReplace,
CanReplaceSelf,
CanRepr,
CanReversed,
CanRound,
Expand Down
42 changes: 42 additions & 0 deletions optype/_can.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,45 @@ def __aexit__[E: BaseException](

@runtime_checkable
class CanAsyncWith[V, R](CanAEnter[V], CanAExit[R], Protocol): ...


# `copy` stdlib
# https://docs.python.org/3.13/library/copy.html


@runtime_checkable
class CanCopy[T](Protocol):
"""Support for creating shallow copies through `copy.copy`."""
def __copy__(self) -> T: ...


@runtime_checkable
class CanDeepcopy[T](Protocol):
"""Support for creating deep copies through `copy.deepcopy`."""
def __deepcopy__(self, memo: dict[int, Any], /) -> T: ...


@runtime_checkable
class CanReplace[T, V](Protocol):
"""Support for `copy.replace` in Python 3.13+."""
def __replace__(self, /, **changes: V) -> T: ...


@runtime_checkable
class CanCopySelf(CanCopy['CanCopySelf'], Protocol):
"""Variant of `CanCopy` that returns `Self` (as it should)."""
@override
def __copy__(self) -> Self: ...


class CanDeepcopySelf(CanDeepcopy['CanDeepcopySelf'], Protocol):
"""Variant of `CanDeepcopy` that returns `Self` (as it should)."""
@override
def __deepcopy__(self, memo: dict[int, Any], /) -> Self: ...


@runtime_checkable
class CanReplaceSelf[V](CanReplace['CanReplaceSelf[Any]', V], Protocol):
"""Variant of `CanReplace` that returns `Self`."""
@override
def __replace__(self, /, **changes: V) -> Self: ...
17 changes: 11 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,6 @@ extend-ignore = [
"SLF001", # private-member-access
]

[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"

[tool.ruff.lint.flake8-type-checking]
strict = true

[tool.ruff.lint.isort]
case-sensitive = true
combine-as-imports = true
Expand All @@ -212,6 +206,17 @@ known-first-party = ["optype"]
lines-between-types = 0
lines-after-imports = 2

[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"

[tool.ruff.lint.flake8-type-checking]
strict = true

[tool.ruff.lint.pylint]
allow-dunder-method-names = [
"__replace__", # used by `copy.replace` in Python 3.13+
]

[tool.ruff.format]
# keep in sync with .editorconfig
line-ending = "lf"
Expand Down
17 changes: 15 additions & 2 deletions tests/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,25 @@ def test_name_matches_dunder(cls: type):
members = get_protocol_members(cls)
assert members

member_count = len(members)
own_members: frozenset[str]
parents = list(filter(is_protocol, cls.mro()[1:]))
# parents = [is_protocol(parent) for parent in cls.mro()[1:]]
if parents:
overridden = {
member for member in members
if callable(f := getattr(cls, member))
and getattr(f, '__override__', False)
}
own_members = members - overridden
else:
own_members = members

member_count = len(members)
own_member_count = len(own_members)

# this test should probably be split up...

if member_count > 1:
if member_count > min(1, own_member_count):
# ensure #parent protocols == #members (including inherited)
assert len(parents) == member_count

Expand Down