1+ import copy
2+ import gc
13import os
4+ import pickle
5+ import re
6+ import textwrap
27import time
38import unittest
9+ from test .support import script_helper
410
511
612class StructSeqTest (unittest .TestCase ):
@@ -37,7 +43,7 @@ def test_repr(self):
3743 # os.stat() gives a complicated struct sequence.
3844 st = os .stat (__file__ )
3945 rep = repr (st )
40- self .assertTrue (rep . startswith ( "os.stat_result" ) )
46+ self .assertStartsWith (rep , "os.stat_result" )
4147 self .assertIn ("st_mode=" , rep )
4248 self .assertIn ("st_ino=" , rep )
4349 self .assertIn ("st_dev=" , rep )
@@ -81,6 +87,7 @@ def test_fields(self):
8187 self .assertEqual (t .n_unnamed_fields , 0 )
8288 self .assertEqual (t .n_fields , time ._STRUCT_TM_ITEMS )
8389
90+ @unittest .expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument dict
8491 def test_constructor (self ):
8592 t = time .struct_time
8693
@@ -89,10 +96,72 @@ def test_constructor(self):
8996 self .assertRaises (TypeError , t , "123" )
9097 self .assertRaises (TypeError , t , "123" , dict = {})
9198 self .assertRaises (TypeError , t , "123456789" , dict = None )
99+ self .assertRaises (TypeError , t , seq = "123456789" , dict = {})
100+
101+ self .assertEqual (t ("123456789" ), tuple ("123456789" ))
102+ self .assertEqual (t ("123456789" , {}), tuple ("123456789" ))
103+ self .assertEqual (t ("123456789" , dict = {}), tuple ("123456789" ))
104+ self .assertEqual (t (sequence = "123456789" , dict = {}), tuple ("123456789" ))
105+
106+ self .assertEqual (t ("1234567890" ), tuple ("123456789" ))
107+ self .assertEqual (t ("1234567890" ).tm_zone , "0" )
108+ self .assertEqual (t ("123456789" , {"tm_zone" : "some zone" }), tuple ("123456789" ))
109+ self .assertEqual (t ("123456789" , {"tm_zone" : "some zone" }).tm_zone , "some zone" )
92110
93111 s = "123456789"
94112 self .assertEqual ("" .join (t (s )), s )
95113
114+ @unittest .expectedFailure # TODO: RUSTPYTHON; Wrong error message
115+ def test_constructor_with_duplicate_fields (self ):
116+ t = time .struct_time
117+
118+ error_message = re .escape ("got duplicate or unexpected field name(s)" )
119+ with self .assertRaisesRegex (TypeError , error_message ):
120+ t ("1234567890" , dict = {"tm_zone" : "some zone" })
121+ with self .assertRaisesRegex (TypeError , error_message ):
122+ t ("1234567890" , dict = {"tm_zone" : "some zone" , "tm_mon" : 1 })
123+ with self .assertRaisesRegex (TypeError , error_message ):
124+ t ("1234567890" , dict = {"error" : 0 , "tm_zone" : "some zone" })
125+ with self .assertRaisesRegex (TypeError , error_message ):
126+ t ("1234567890" , dict = {"error" : 0 , "tm_zone" : "some zone" , "tm_mon" : 1 })
127+
128+ @unittest .expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
129+ def test_constructor_with_duplicate_unnamed_fields (self ):
130+ assert os .stat_result .n_unnamed_fields > 0
131+ n_visible_fields = os .stat_result .n_sequence_fields
132+
133+ r = os .stat_result (range (n_visible_fields ), {'st_atime' : - 1.0 })
134+ self .assertEqual (r .st_atime , - 1.0 )
135+ self .assertEqual (r , tuple (range (n_visible_fields )))
136+
137+ r = os .stat_result ((* range (n_visible_fields ), - 1.0 ))
138+ self .assertEqual (r .st_atime , - 1.0 )
139+ self .assertEqual (r , tuple (range (n_visible_fields )))
140+
141+ with self .assertRaisesRegex (TypeError ,
142+ re .escape ("got duplicate or unexpected field name(s)" )):
143+ os .stat_result ((* range (n_visible_fields ), - 1.0 ), {'st_atime' : - 1.0 })
144+
145+ @unittest .expectedFailure # TODO: RUSTPYTHON; Wrong error message
146+ def test_constructor_with_unknown_fields (self ):
147+ t = time .struct_time
148+
149+ error_message = re .escape ("got duplicate or unexpected field name(s)" )
150+ with self .assertRaisesRegex (TypeError , error_message ):
151+ t ("123456789" , dict = {"tm_year" : 0 })
152+ with self .assertRaisesRegex (TypeError , error_message ):
153+ t ("123456789" , dict = {"tm_year" : 0 , "tm_mon" : 1 })
154+ with self .assertRaisesRegex (TypeError , error_message ):
155+ t ("123456789" , dict = {"tm_zone" : "some zone" , "tm_mon" : 1 })
156+ with self .assertRaisesRegex (TypeError , error_message ):
157+ t ("123456789" , dict = {"tm_zone" : "some zone" , "error" : 0 })
158+ with self .assertRaisesRegex (TypeError , error_message ):
159+ t ("123456789" , dict = {"error" : 0 , "tm_zone" : "some zone" , "tm_mon" : 1 })
160+ with self .assertRaisesRegex (TypeError , error_message ):
161+ t ("123456789" , dict = {"error" : 0 })
162+ with self .assertRaisesRegex (TypeError , error_message ):
163+ t ("123456789" , dict = {"tm_zone" : "some zone" , "error" : 0 })
164+
96165 def test_eviltuple (self ):
97166 class Exc (Exception ):
98167 pass
@@ -106,9 +175,80 @@ def __len__(self):
106175
107176 self .assertRaises (Exc , time .struct_time , C ())
108177
109- def test_reduce (self ):
178+ def test_pickling (self ):
110179 t = time .gmtime ()
111- x = t .__reduce__ ()
180+ for proto in range (pickle .HIGHEST_PROTOCOL + 1 ):
181+ p = pickle .dumps (t , proto )
182+ t2 = pickle .loads (p )
183+ self .assertEqual (t2 .__class__ , t .__class__ )
184+ self .assertEqual (t2 , t )
185+ self .assertEqual (t2 .tm_year , t .tm_year )
186+ self .assertEqual (t2 .tm_zone , t .tm_zone )
187+
188+ @unittest .expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
189+ def test_pickling_with_unnamed_fields (self ):
190+ assert os .stat_result .n_unnamed_fields > 0
191+
192+ r = os .stat_result (range (os .stat_result .n_sequence_fields ),
193+ {'st_atime' : 1.0 , 'st_atime_ns' : 2.0 })
194+ for proto in range (pickle .HIGHEST_PROTOCOL + 1 ):
195+ p = pickle .dumps (r , proto )
196+ r2 = pickle .loads (p )
197+ self .assertEqual (r2 .__class__ , r .__class__ )
198+ self .assertEqual (r2 , r )
199+ self .assertEqual (r2 .st_mode , r .st_mode )
200+ self .assertEqual (r2 .st_atime , r .st_atime )
201+ self .assertEqual (r2 .st_atime_ns , r .st_atime_ns )
202+
203+ def test_copying (self ):
204+ n_fields = time .struct_time .n_fields
205+ t = time .struct_time ([[i ] for i in range (n_fields )])
206+
207+ t2 = copy .copy (t )
208+ self .assertEqual (t2 .__class__ , t .__class__ )
209+ self .assertEqual (t2 , t )
210+ self .assertEqual (t2 .tm_year , t .tm_year )
211+ self .assertEqual (t2 .tm_zone , t .tm_zone )
212+ self .assertIs (t2 [0 ], t [0 ])
213+ self .assertIs (t2 .tm_year , t .tm_year )
214+
215+ t3 = copy .deepcopy (t )
216+ self .assertEqual (t3 .__class__ , t .__class__ )
217+ self .assertEqual (t3 , t )
218+ self .assertEqual (t3 .tm_year , t .tm_year )
219+ self .assertEqual (t3 .tm_zone , t .tm_zone )
220+ self .assertIsNot (t3 [0 ], t [0 ])
221+ self .assertIsNot (t3 .tm_year , t .tm_year )
222+
223+ @unittest .expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
224+ def test_copying_with_unnamed_fields (self ):
225+ assert os .stat_result .n_unnamed_fields > 0
226+
227+ n_sequence_fields = os .stat_result .n_sequence_fields
228+ r = os .stat_result ([[i ] for i in range (n_sequence_fields )],
229+ {'st_atime' : [1.0 ], 'st_atime_ns' : [2.0 ]})
230+
231+ r2 = copy .copy (r )
232+ self .assertEqual (r2 .__class__ , r .__class__ )
233+ self .assertEqual (r2 , r )
234+ self .assertEqual (r2 .st_mode , r .st_mode )
235+ self .assertEqual (r2 .st_atime , r .st_atime )
236+ self .assertEqual (r2 .st_atime_ns , r .st_atime_ns )
237+ self .assertIs (r2 [0 ], r [0 ])
238+ self .assertIs (r2 .st_mode , r .st_mode )
239+ self .assertIs (r2 .st_atime , r .st_atime )
240+ self .assertIs (r2 .st_atime_ns , r .st_atime_ns )
241+
242+ r3 = copy .deepcopy (r )
243+ self .assertEqual (r3 .__class__ , r .__class__ )
244+ self .assertEqual (r3 , r )
245+ self .assertEqual (r3 .st_mode , r .st_mode )
246+ self .assertEqual (r3 .st_atime , r .st_atime )
247+ self .assertEqual (r3 .st_atime_ns , r .st_atime_ns )
248+ self .assertIsNot (r3 [0 ], r [0 ])
249+ self .assertIsNot (r3 .st_mode , r .st_mode )
250+ self .assertIsNot (r3 .st_atime , r .st_atime )
251+ self .assertIsNot (r3 .st_atime_ns , r .st_atime_ns )
112252
113253 def test_extended_getslice (self ):
114254 # Test extended slicing by comparing with list slicing.
@@ -133,6 +273,104 @@ def test_match_args_with_unnamed_fields(self):
133273 self .assertEqual (os .stat_result .n_unnamed_fields , 3 )
134274 self .assertEqual (os .stat_result .__match_args__ , expected_args )
135275
276+ def test_copy_replace_all_fields_visible (self ):
277+ assert os .times_result .n_unnamed_fields == 0
278+ assert os .times_result .n_sequence_fields == os .times_result .n_fields
279+
280+ t = os .times ()
281+
282+ # visible fields
283+ self .assertEqual (copy .replace (t ), t )
284+ self .assertIsInstance (copy .replace (t ), os .times_result )
285+ self .assertEqual (copy .replace (t , user = 1.5 ), (1.5 , * t [1 :]))
286+ self .assertEqual (copy .replace (t , system = 2.5 ), (t [0 ], 2.5 , * t [2 :]))
287+ self .assertEqual (copy .replace (t , user = 1.5 , system = 2.5 ), (1.5 , 2.5 , * t [2 :]))
288+
289+ # unknown fields
290+ with self .assertRaisesRegex (TypeError , 'unexpected field name' ):
291+ copy .replace (t , error = - 1 )
292+ with self .assertRaisesRegex (TypeError , 'unexpected field name' ):
293+ copy .replace (t , user = 1 , error = - 1 )
294+
295+ @unittest .expectedFailure # TODO: RUSTPYTHON; Wrong error message
296+ def test_copy_replace_with_invisible_fields (self ):
297+ assert time .struct_time .n_unnamed_fields == 0
298+ assert time .struct_time .n_sequence_fields < time .struct_time .n_fields
299+
300+ t = time .gmtime (0 )
301+
302+ # visible fields
303+ t2 = copy .replace (t )
304+ self .assertEqual (t2 , (1970 , 1 , 1 , 0 , 0 , 0 , 3 , 1 , 0 ))
305+ self .assertIsInstance (t2 , time .struct_time )
306+ t3 = copy .replace (t , tm_year = 2000 )
307+ self .assertEqual (t3 , (2000 , 1 , 1 , 0 , 0 , 0 , 3 , 1 , 0 ))
308+ self .assertEqual (t3 .tm_year , 2000 )
309+ t4 = copy .replace (t , tm_mon = 2 )
310+ self .assertEqual (t4 , (1970 , 2 , 1 , 0 , 0 , 0 , 3 , 1 , 0 ))
311+ self .assertEqual (t4 .tm_mon , 2 )
312+ t5 = copy .replace (t , tm_year = 2000 , tm_mon = 2 )
313+ self .assertEqual (t5 , (2000 , 2 , 1 , 0 , 0 , 0 , 3 , 1 , 0 ))
314+ self .assertEqual (t5 .tm_year , 2000 )
315+ self .assertEqual (t5 .tm_mon , 2 )
316+
317+ # named invisible fields
318+ self .assertHasAttr (t , 'tm_zone' )
319+ with self .assertRaisesRegex (AttributeError , 'readonly attribute' ):
320+ t .tm_zone = 'some other zone'
321+ self .assertEqual (t2 .tm_zone , t .tm_zone )
322+ self .assertEqual (t3 .tm_zone , t .tm_zone )
323+ self .assertEqual (t4 .tm_zone , t .tm_zone )
324+ t6 = copy .replace (t , tm_zone = 'some other zone' )
325+ self .assertEqual (t , t6 )
326+ self .assertEqual (t6 .tm_zone , 'some other zone' )
327+ t7 = copy .replace (t , tm_year = 2000 , tm_zone = 'some other zone' )
328+ self .assertEqual (t7 , (2000 , 1 , 1 , 0 , 0 , 0 , 3 , 1 , 0 ))
329+ self .assertEqual (t7 .tm_year , 2000 )
330+ self .assertEqual (t7 .tm_zone , 'some other zone' )
331+
332+ # unknown fields
333+ with self .assertRaisesRegex (TypeError , 'unexpected field name' ):
334+ copy .replace (t , error = 2 )
335+ with self .assertRaisesRegex (TypeError , 'unexpected field name' ):
336+ copy .replace (t , tm_year = 2000 , error = 2 )
337+ with self .assertRaisesRegex (TypeError , 'unexpected field name' ):
338+ copy .replace (t , tm_zone = 'some other zone' , error = 2 )
339+
340+ def test_copy_replace_with_unnamed_fields (self ):
341+ assert os .stat_result .n_unnamed_fields > 0
342+
343+ r = os .stat_result (range (os .stat_result .n_sequence_fields ))
344+
345+ error_message = re .escape ('__replace__() is not supported' )
346+ with self .assertRaisesRegex (TypeError , error_message ):
347+ copy .replace (r )
348+ with self .assertRaisesRegex (TypeError , error_message ):
349+ copy .replace (r , st_mode = 1 )
350+ with self .assertRaisesRegex (TypeError , error_message ):
351+ copy .replace (r , error = 2 )
352+ with self .assertRaisesRegex (TypeError , error_message ):
353+ copy .replace (r , st_mode = 1 , error = 2 )
354+
355+ def test_reference_cycle (self ):
356+ # gh-122527: Check that a structseq that's part of a reference cycle
357+ # with its own type doesn't crash. Previously, if the type's dictionary
358+ # was cleared first, the structseq instance would crash in the
359+ # destructor.
360+ script_helper .assert_python_ok ("-c" , textwrap .dedent (r"""
361+ import time
362+ t = time.gmtime()
363+ type(t).refcyle = t
364+ """ ))
365+
366+ def test_replace_gc_tracked (self ):
367+ # Verify that __replace__ results are properly GC-tracked
368+ time_struct = time .gmtime (0 )
369+ lst = []
370+ replaced_struct = time_struct .__replace__ (tm_year = lst )
371+ lst .append (replaced_struct )
372+
373+ self .assertTrue (gc .is_tracked (replaced_struct ))
136374
137375if __name__ == "__main__" :
138376 unittest .main ()
0 commit comments