10
10
11
11
# For Python < 3.13 compatibility: copy.replace doesn't exist in older Python
12
12
if TYPE_CHECKING : # pragma: no cover
13
-
14
- def _replace (container_instance : Any , / , ** changes : Any ) -> Any :
13
+ # Defining a dummy replace function for type checking
14
+ def _replace (container_instance : Any , / , inner_value : Any ) -> Any :
15
15
"""Dummy replace function for type checking."""
16
16
return container_instance
17
17
18
+ # Assigning it to copy.replace for type checking
18
19
if not hasattr (copy , 'replace' ):
19
20
copy .replace = _replace # type: ignore
20
21
21
22
22
23
class _CustomClass :
24
+ """A custom class for replace testing."""
25
+
23
26
__slots__ = ('inner_value' ,)
24
27
25
28
def __init__ (self , inner_value : str ) -> None :
29
+ """Initialize instance."""
26
30
self .inner_value = inner_value
27
31
28
32
def __eq__ (self , other : object ) -> bool :
33
+ """Compare with other."""
29
34
if isinstance (other , _CustomClass ):
30
35
return self .inner_value == other .inner_value
31
36
return NotImplemented
32
37
33
38
def __ne__ (self , other : object ) -> bool :
39
+ """Not equal to other."""
34
40
if isinstance (other , _CustomClass ):
35
41
return self .inner_value != other .inner_value
36
42
return NotImplemented
37
43
38
44
def __hash__ (self ) -> int :
45
+ """Return hash of the inner value."""
39
46
return hash (self .inner_value )
40
47
41
48
@@ -51,35 +58,50 @@ def __hash__(self) -> int:
51
58
),
52
59
)
53
60
@example (None )
54
- def test_replace (container_value : Any ) -> None :
55
- """Test __replace__ magic method."""
61
+ def test_replace_method (container_value : Any ) -> None :
62
+ """Ensures __replace__ magic method works as expected ."""
56
63
container = BaseContainer (container_value )
57
64
65
+ # Test with new inner_value returns a new container
58
66
new_value = 'new_value'
59
- new_container = container .__replace__ (_inner_value = new_value )
67
+ # Test direct call to __replace__
68
+ new_container = container .__replace__ (new_value ) # noqa: PLC2801
60
69
61
70
assert new_container is not container
62
71
assert new_container ._inner_value == new_value # noqa: SLF001
63
72
assert isinstance (new_container , BaseContainer )
64
73
assert type (new_container ) is type (container ) # noqa: WPS516
65
74
66
75
67
- def test_replace_no_changes () -> None :
68
- """Test __replace__ with no changes ."""
69
- container = BaseContainer ( 'test' )
70
- result = container . __replace__ () # noqa: PLC2801
71
- assert result is container
76
+ def test_base_container_replace_direct_call ( container ) :
77
+ """Test direct call to the __replace__ method ."""
78
+ new_value = 'new_value'
79
+ # Test direct call to __replace__
80
+ new_container = container . __replace__ ( new_value ) # noqa: PLC2801
72
81
82
+ assert new_container is not container
83
+ assert isinstance (new_container , BaseContainer )
73
84
74
- def test_replace_invalid_attributes () -> None :
75
- """Test __replace__ with invalid attributes."""
76
- container = BaseContainer ('test' )
77
85
78
- with pytest .raises (ValueError , match = 'Only _inner_value can be replaced' ):
79
- container .__replace__ (invalid_attr = 'value' )
86
+ def test_base_container_replace_direct_call_invalid_args (container ):
87
+ """Test direct call with invalid arguments."""
88
+ # Direct call with no args should fail
89
+ with pytest .raises (TypeError ):
90
+ container .__replace__ () # noqa: PLC2801
80
91
81
- with pytest .raises (ValueError , match = 'Only _inner_value can be replaced' ):
82
- container .__replace__ (_inner_value = 'new' , another_attr = 'value' )
92
+ # Direct call with keyword args matching the name is allowed by Python,
93
+ # even with /.
94
+ # If uncommented, it should pass as Python allows this.
95
+ # Removing commented test case for
96
+ # `container.__replace__(inner_value='new')`
97
+
98
+ # Direct call with extra positional args should fail
99
+ with pytest .raises (TypeError ):
100
+ container .__replace__ ('new' , 'extra' ) # noqa: PLC2801
101
+
102
+ # Direct call with unexpected keyword args should fail
103
+ with pytest .raises (TypeError ):
104
+ container .__replace__ (other_kwarg = 'value' ) # type: ignore[attr-defined]
83
105
84
106
85
107
@pytest .mark .skipif (
@@ -99,30 +121,56 @@ def test_replace_invalid_attributes() -> None:
99
121
)
100
122
@example (None )
101
123
def test_copy_replace (container_value : Any ) -> None :
102
- """Test copy.replace with BaseContainer."""
124
+ """Ensures copy.replace works with BaseContainer."""
103
125
container = BaseContainer (container_value )
104
126
105
- assert copy .replace (container ) is container # type: ignore[attr-defined]
127
+ # Test with no changes is not directly possible via copy.replace with this
128
+ # __replace__ implementation.
129
+ # The copy.replace function itself handles the no-change case if the
130
+ # object supports it, but our __replace__ requires a value.
106
131
132
+ # Test with new inner_value returns a new container using copy.replace
107
133
new_value = 'new_value'
108
- new_container = copy .replace (container , _inner_value = new_value ) # type: ignore[attr-defined]
134
+ # copy.replace calls __replace__ with the new value as a positional arg
135
+ new_container = copy .replace (container , new_value ) # type: ignore[attr-defined]
109
136
110
137
assert new_container is not container
111
138
assert new_container ._inner_value == new_value # noqa: SLF001
112
139
assert isinstance (new_container , BaseContainer )
113
140
assert type (new_container ) is type (container ) # noqa: WPS516
114
141
115
142
116
- @pytest .mark .skipif (
117
- sys .version_info < (3 , 13 ),
118
- reason = 'copy.replace requires Python 3.13+' ,
119
- )
120
- def test_copy_replace_invalid_attributes () -> None :
121
- """Test copy.replace with invalid attributes."""
122
- container = BaseContainer ('test' )
143
+ def test_base_container_replace_via_copy_no_changes (container_value ):
144
+ """Test copy.replace with no actual change in value."""
145
+ container = BaseContainer (container_value )
146
+
147
+ # Test with no changes is not directly possible via copy.replace with this
148
+ # __replace__ implementation.
149
+ # The copy.replace function itself handles the no-change case if the
150
+ # object supports it, but our __replace__ requires a value.
151
+ # If copy.replace is called with the same value, it should work.
152
+ new_container = copy .replace (container , inner_value = container_value )
153
+
154
+ assert new_container is not container # A new instance should be created
155
+
156
+
157
+ def test_base_container_replace_via_copy_invalid_args (container ):
158
+ """Test copy.replace with invalid arguments."""
159
+ # copy.replace converts the keyword 'inner_value' to a positional arg
160
+ # for __replace__(self, /, inner_value), so this is valid.
161
+ # Removing commented out test case for copy.replace with inner_value kwarg
162
+
163
+ # However, passing other keyword arguments will fail because __replace__
164
+ # doesn't accept them.
165
+ with pytest .raises (TypeError ):
166
+ copy .replace (container , other_kwarg = 'value' ) # type: ignore[attr-defined]
123
167
124
- with pytest .raises (ValueError , match = 'Only _inner_value can be replaced' ):
125
- copy .replace (container , invalid_attr = 'value' ) # type: ignore[attr-defined]
168
+ # copy.replace should raise TypeError if extra positional arguments
169
+ # are passed.
170
+ with pytest .raises (TypeError ):
171
+ copy .replace (container , 'new' , 'extra' ) # type: ignore[attr-defined]
126
172
127
- with pytest .raises (ValueError , match = 'Only _inner_value can be replaced' ):
128
- copy .replace (container , _inner_value = 'new' , another_attr = 'value' ) # type: ignore[attr-defined]
173
+ # copy.replace should raise TypeError if no value is passed
174
+ # (our __replace__ requires one).
175
+ with pytest .raises (TypeError ):
176
+ copy .replace (container ) # type: ignore[attr-defined]
0 commit comments