@@ -115,13 +115,62 @@ def attr_is_not_inherited(type_, attr):
115115
116116
117117def extra_info (obj ):
118+ import sys
119+
120+ class _OrderedSet :
121+ """Set repr with sorted elements, so hash order does not change it."""
122+
123+ def __init__ (self , items , braces ):
124+ self .items = items
125+ self .braces = braces
126+
127+ def __repr__ (self ):
128+ if not self .items :
129+ return "set()" if self .braces else "frozenset()"
130+ body = ", " .join (sorted (repr (item ) for item in self .items ))
131+ if self .braces :
132+ return "{" + body + "}"
133+ return "frozenset({" + body + "})"
134+
135+ def canon_default (param ):
136+ default = param .default
137+ # The interpreter path, version tuple, and install scheme differ per process.
138+ if isinstance (default , str ) and default == sys .executable :
139+ return "<sys.executable>"
140+ if default is sys .version_info :
141+ return "<sys.version_info>"
142+ if param .name == "scheme" and isinstance (default , str ):
143+ import sysconfig
144+
145+ if default == sysconfig .get_default_scheme ():
146+ return "<sysconfig.get_default_scheme()>"
147+ if isinstance (default , frozenset ):
148+ return _OrderedSet (default , False )
149+ if isinstance (default , set ):
150+ return _OrderedSet (default , True )
151+ return default
152+
153+ def stable_signature (target ):
154+ sig = inspect .signature (target )
155+ params = []
156+ changed = False
157+ for param in sig .parameters .values ():
158+ if param .default is not inspect .Parameter .empty :
159+ default = canon_default (param )
160+ if default is not param .default :
161+ param = param .replace (default = default )
162+ changed = True
163+ params .append (param )
164+ if changed :
165+ sig = sig .replace (parameters = params )
166+ # remove function memory addresses
167+ return re .sub (r" at 0x[0-9A-Fa-f]+" , " at 0xdeadbeef" , str (sig ))
168+
118169 if callable (obj ):
119170 doc = inspect .getdoc (obj )
120171 try :
121- sig = str (inspect .signature (obj ))
122- # remove function memory addresses
123172 return {
124- "sig" : re . sub ( " at 0x[0-9A-Fa-f]+" , " at 0xdeadbeef" , sig ),
173+ "sig" : stable_signature ( obj ),
125174 "doc" : doc ,
126175 }
127176 except Exception as e :
@@ -322,13 +371,81 @@ def dir_of_mod_or_error(module_name, keep_other=True):
322371 return result
323372
324373
374+ def _is_decimal_fallback_item (item ):
375+ """Items that belong to the pure decimal module, not an imported helper."""
376+ if inspect .ismodule (item ):
377+ return False
378+ declared = getattr (item , "__module__" , None )
379+ if declared in {"decimal" , "_pydecimal" }:
380+ return True
381+ return declared is None and inspect .getmodule (item ) is None
382+
383+
384+ def _expand_class_members (dir_result , module ):
385+ for item_name in list (dir_result ):
386+ if "." in item_name :
387+ continue
388+ item = getattr (module , item_name )
389+ if (
390+ isinstance (item , type )
391+ and item not in BUILTIN_TYPES
392+ and getattr (item , "__name__" , None ) == item_name
393+ ):
394+ for attr , info in own_attrs (item ):
395+ dir_result [f"{ item_name } .{ attr } " ] = info
396+
397+
398+ def _pure_decimal_baseline (wrapper ):
399+ """Signatures and docs from `_pydecimal`, with the wrapper module docstring."""
400+ import _pydecimal
401+
402+ result = {}
403+ for item_name in sorted (set (dir (_pydecimal ))):
404+ if item_name == "__builtins__" :
405+ continue
406+ if item_name == "__doc__" :
407+ result [item_name ] = {"sig" : None , "doc" : inspect .getdoc (wrapper )}
408+ continue
409+ item = getattr (_pydecimal , item_name )
410+ if not _is_decimal_fallback_item (item ):
411+ continue
412+ result [item_name ] = extra_info (item )
413+ _expand_class_members (result , _pydecimal )
414+ return result
415+
416+
417+ def _use_pure_zoneinfo (dir_result ):
418+ """Replace the accelerated ZoneInfo with the pure-Python class."""
419+ import zoneinfo ._zoneinfo as pure_zoneinfo
420+
421+ cls = pure_zoneinfo .ZoneInfo
422+ name = cls .__name__
423+ for key in list (dir_result ):
424+ if key == name or key .startswith (f"{ name } ." ):
425+ del dir_result [key ]
426+ dir_result [name ] = extra_info (cls )
427+ for attr , info in own_attrs (cls ):
428+ dir_result [f"{ name } .{ attr } " ] = info
429+
430+
325431def gen_modules ():
326432 # check name because modules listed have side effects on import,
327433 # e.g. printing something or opening a webpage
328434 modules = {}
329435 for mod_name in sorted (scan_modules (), key = name_sort_key ):
330436 if mod_name in IGNORED_MODULES :
331437 continue
438+ # decimal's accelerated types are not the implementation under test.
439+ if mod_name == "decimal" :
440+ module = import_module (mod_name )
441+ if isinstance (module , Exception ):
442+ print (
443+ f"!!! { mod_name } skipped because { type (module ).__name__ } : { str (module )} " ,
444+ file = sys .stderr ,
445+ )
446+ continue
447+ modules [mod_name ] = _pure_decimal_baseline (module )
448+ continue
332449 # when generating CPython list, ignore items defined by other modules
333450 dir_result = dir_of_mod_or_error (mod_name , keep_other = False )
334451 if isinstance (dir_result , Exception ):
@@ -348,6 +465,8 @@ def gen_modules():
348465 ):
349466 for attr , info in own_attrs (item ):
350467 dir_result [f"{ item_name } .{ attr } " ] = info
468+ if mod_name == "zoneinfo" :
469+ _use_pure_zoneinfo (dir_result )
351470 modules [mod_name ] = dir_result
352471 return modules
353472
0 commit comments