1010
1111# For Python < 3.13 compatibility: copy.replace doesn't exist in older Python
1212if 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 :
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
2223class _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
@@ -51,35 +58,50 @@ def __hash__(self) -> int:
5158 ),
5259)
5360@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 ."""
5663 container = BaseContainer (container_value )
5764
65+ # Test with new inner_value returns a new container
5866 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
6069
6170 assert new_container is not container
6271 assert new_container ._inner_value == new_value # noqa: SLF001
6372 assert isinstance (new_container , BaseContainer )
6473 assert type (new_container ) is type (container ) # noqa: WPS516
6574
6675
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
7281
82+ assert new_container is not container
83+ assert isinstance (new_container , BaseContainer )
7384
74- def test_replace_invalid_attributes () -> None :
75- """Test __replace__ with invalid attributes."""
76- container = BaseContainer ('test' )
7785
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
8091
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]
83105
84106
85107@pytest .mark .skipif (
@@ -99,30 +121,56 @@ def test_replace_invalid_attributes() -> None:
99121)
100122@example (None )
101123def test_copy_replace (container_value : Any ) -> None :
102- """Test copy.replace with BaseContainer."""
124+ """Ensures copy.replace works with BaseContainer."""
103125 container = BaseContainer (container_value )
104126
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.
106131
132+ # Test with new inner_value returns a new container using copy.replace
107133 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]
109136
110137 assert new_container is not container
111138 assert new_container ._inner_value == new_value # noqa: SLF001
112139 assert isinstance (new_container , BaseContainer )
113140 assert type (new_container ) is type (container ) # noqa: WPS516
114141
115142
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]
123167
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]
126172
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