Skip to content

Commit c515492

Browse files
Adrian AcalaAdrian Acala
Adrian Acala
authored and
Adrian Acala
committed
feat: Add __replace__ magic method to BaseContainer for Python 3.13 support
- Implemented __replace__ method in BaseContainer to support the copy.replace() function. - Updated CHANGELOG to reflect this new feature. - Enhanced test cases to ensure __replace__ works as expected, including handling of invalid attributes.
1 parent 3169b17 commit c515492

File tree

3 files changed

+26
-8
lines changed

3 files changed

+26
-8
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ See [0Ver](https://0ver.org/).
1010

1111
### Features
1212

13-
- Add `__replace__` protocol support for Python 3.13's `copy.replace()`
13+
- Add `__replace__` magic method to `BaseContainer` to support `copy.replace()` function from Python 3.13
1414

1515
## 0.25.0
1616

returns/primitives/container.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,14 @@ def __setstate__(self, state: _PickleState | Any) -> None:
6969
object.__setattr__(self, '_inner_value', state)
7070

7171
def __replace__(self, /, **changes) -> 'BaseContainer':
72-
"""Protocol for copy.replace() function (Python 3.13+)."""
72+
"""
73+
Creates a new container with replaced attribute values.
74+
75+
Implements the protocol for the `copy.replace()` function
76+
introduced in Python 3.13.
77+
78+
The only supported attribute is '_inner_value'.
79+
"""
7380
if not changes:
7481
return self
7582

tests/test_primitives/test_container/test_base_container/test_replace.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,39 @@
1010

1111
# For Python < 3.13 compatibility: copy.replace doesn't exist in older Python
1212
if TYPE_CHECKING: # pragma: no cover
13-
13+
# Defining a dummy replace function for type checking
1414
def _replace(container_instance: Any, /, **changes: Any) -> Any:
1515
"""Dummy replace function for type checking."""
1616
return container_instance
1717

18+
# Assigning it to copy.replace for type checking
1819
if not hasattr(copy, 'replace'):
1920
copy.replace = _replace # type: ignore
2021

2122

2223
class _CustomClass:
24+
"""A custom class for replace testing."""
25+
2326
__slots__ = ('inner_value',)
2427

2528
def __init__(self, inner_value: str) -> None:
29+
"""Initialize instance."""
2630
self.inner_value = inner_value
2731

2832
def __eq__(self, other: object) -> bool:
33+
"""Compare with other."""
2934
if isinstance(other, _CustomClass):
3035
return self.inner_value == other.inner_value
3136
return NotImplemented
3237

3338
def __ne__(self, other: object) -> bool:
39+
"""Not equal to other."""
3440
if isinstance(other, _CustomClass):
3541
return self.inner_value != other.inner_value
3642
return NotImplemented
3743

3844
def __hash__(self) -> int:
45+
"""Return hash of the inner value."""
3946
return hash(self.inner_value)
4047

4148

@@ -52,9 +59,10 @@ def __hash__(self) -> int:
5259
)
5360
@example(None)
5461
def test_replace(container_value: Any) -> None:
55-
"""Test __replace__ magic method."""
62+
"""Ensures __replace__ magic method works as expected."""
5663
container = BaseContainer(container_value)
5764

65+
# Test with new inner_value returns a new container
5866
new_value = 'new_value'
5967
new_container = container.__replace__(_inner_value=new_value)
6068

@@ -65,14 +73,15 @@ def test_replace(container_value: Any) -> None:
6573

6674

6775
def test_replace_no_changes() -> None:
68-
"""Test __replace__ with no changes."""
76+
"""Ensures __replace__ with no changes returns the same container."""
6977
container = BaseContainer('test')
78+
# We need to call the method directly to test this functionality
7079
result = container.__replace__() # noqa: PLC2801
7180
assert result is container
7281

7382

7483
def test_replace_invalid_attributes() -> None:
75-
"""Test __replace__ with invalid attributes."""
84+
"""Ensures __replace__ raises ValueError for invalid attributes."""
7685
container = BaseContainer('test')
7786

7887
with pytest.raises(ValueError, match='Only _inner_value can be replaced'):
@@ -99,11 +108,13 @@ def test_replace_invalid_attributes() -> None:
99108
)
100109
@example(None)
101110
def test_copy_replace(container_value: Any) -> None:
102-
"""Test copy.replace with BaseContainer."""
111+
"""Ensures copy.replace works with BaseContainer."""
103112
container = BaseContainer(container_value)
104113

114+
# Test with no changes returns the same container
105115
assert copy.replace(container) is container # type: ignore[attr-defined]
106116

117+
# Test with new inner_value returns a new container
107118
new_value = 'new_value'
108119
new_container = copy.replace(container, _inner_value=new_value) # type: ignore[attr-defined]
109120

@@ -118,7 +129,7 @@ def test_copy_replace(container_value: Any) -> None:
118129
reason='copy.replace requires Python 3.13+',
119130
)
120131
def test_copy_replace_invalid_attributes() -> None:
121-
"""Test copy.replace with invalid attributes."""
132+
"""Ensures copy.replace raises ValueError for invalid attributes."""
122133
container = BaseContainer('test')
123134

124135
with pytest.raises(ValueError, match='Only _inner_value can be replaced'):

0 commit comments

Comments
 (0)