swizzle
1import builtins 2import sys as _sys 3import types 4from collections.abc import Iterable 5from dataclasses import fields as dataclass_fields 6from dataclasses import is_dataclass 7from enum import Enum, EnumMeta 8from functools import wraps 9from importlib.metadata import version as get_version 10from keyword import iskeyword as _iskeyword 11from operator import itemgetter as _itemgetter 12 13from .trie import Trie 14from .utils import ( 15 get_getattr_methods, 16 get_setattr_method, 17 is_valid_sep, 18 split_attr_name, 19) 20 21try: 22 from _collections import _tuplegetter 23except ImportError: 24 _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc) 25 26 27try: 28 from importlib.metadata import PackageNotFoundError 29 from importlib.metadata import version as _version 30 31 __version__ = _version("swizzle") 32except PackageNotFoundError: 33 try: 34 from setuptools_scm import get_version 35 36 __version__ = get_version(root=".", relative_to=__file__) 37 except Exception: 38 __version__ = "0.0.0-dev" 39 40__all__ = [ 41 "swizzledtuple", 42 "t", 43 "AttrSource", 44 "swizzle", 45 "swizzle_attributes_retriever", 46] 47 48_type = builtins.type 49MISSING = object() 50 51 52class AttrSource(str, Enum): 53 """Enum for specifying how to retrieve attributes from a class.""" 54 55 SLOTS = "slots" 56 FIELDS = "fields" 57 58 59def swizzledtuple( 60 typename, 61 field_names, 62 arrange_names=None, 63 *, 64 rename=False, 65 defaults=None, 66 module=None, 67 sep=None, 68): 69 """ 70 Creates a custom named tuple class with *swizzled attributes*, allowing for rearranged field names 71 and flexible attribute access patterns. 72 73 This function generates a subclass of Python's built-in `tuple`, similar to `collections.namedtuple`, 74 but with additional features: 75 76 - Field names can be rearranged using `arrange_names`. 77 - Attribute access can be customized using a separator string (`sep`). 78 - Invalid field names can be automatically renamed (`rename=True`). 79 - Supports custom default values, modules, and attribute formatting. 80 81 Args: 82 typename (str): Name of the new named tuple type. 83 field_names (Sequence[str] | str): List of field names, or a single string that will be split. 84 rename (bool, optional): If True, invalid field names are replaced with positional names. 85 Defaults to False. 86 defaults (Sequence, optional): Default values for fields. Defaults to None. 87 module (str, optional): Module name where the tuple is defined. Defaults to the caller's module. 88 arrange_names (Sequence[str] | str, optional): Optional ordering of fields for the final structure. 89 Can include duplicates. 90 sep (str, optional): Separator string used to construct compound attribute names. 91 If `sep = '_'` provided, attributes like `v.x_y` become accessible. Defaults to None. 92 Returns: 93 Type: A new subclass of `tuple` with named fields and custom swizzle behavior. 94 95 Example: 96 ```python 97 Vector = swizzledtuple("Vector", "x y z", arrange_names="y z x x") 98 v = Vector(1, 2, 3) 99 100 print(v) # Vector(y=2, z=3, x=1, x=1) 101 print(v.yzx) # Vector(y=2, z=3, x=1) 102 print(v.yzx.xxzyzz) # Vector(x=1, x=1, z=3, y=2, z=3, z=3) 103 ``` 104 """ 105 106 if isinstance(field_names, str): 107 field_names = field_names.replace(",", " ").split() 108 field_names = list(map(str, field_names)) 109 if arrange_names is not None: 110 if isinstance(arrange_names, str): 111 arrange_names = arrange_names.replace(",", " ").split() 112 arrange_names = list(map(str, arrange_names)) 113 assert set(arrange_names) == set(field_names), ( 114 "Arrangement must contain all field names" 115 ) 116 else: 117 arrange_names = field_names.copy() 118 119 typename = _sys.intern(str(typename)) 120 121 _dir = dir(tuple) + [ 122 "__match_args__", 123 "__module__", 124 "__slots__", 125 "_asdict", 126 "_field_defaults", 127 "_fields", 128 "_make", 129 "_replace", 130 ] 131 if rename: 132 seen = set() 133 name_newname = {} 134 for index, name in enumerate(field_names): 135 if ( 136 not name.isidentifier() 137 or _iskeyword(name) 138 or name in _dir 139 or name in seen 140 ): 141 field_names[index] = f"_{index}" 142 name_newname[name] = field_names[index] 143 seen.add(name) 144 for index, name in enumerate(arrange_names): 145 arrange_names[index] = name_newname[name] 146 147 for name in [typename] + field_names: 148 if type(name) is not str: 149 raise TypeError("Type names and field names must be strings") 150 if not name.isidentifier(): 151 raise ValueError( 152 f"Type names and field names must be valid identifiers: {name!r}" 153 ) 154 if _iskeyword(name): 155 raise ValueError( 156 f"Type names and field names cannot be a keyword: {name!r}" 157 ) 158 seen = set() 159 for name in field_names: 160 if name in _dir: 161 raise ValueError( 162 "Field names cannot be an attribute name which would shadow the namedtuple methods or attributes" 163 f"{name!r}" 164 ) 165 if name in seen: 166 raise ValueError(f"Encountered duplicate field name: {name!r}") 167 seen.add(name) 168 169 arrange_indices = [field_names.index(name) for name in arrange_names] 170 171 def tuple_new(cls, iterable): 172 new = [] 173 _iterable = list(iterable) 174 for index in arrange_indices: 175 new.append(_iterable[index]) 176 return tuple.__new__(cls, new) 177 178 field_defaults = {} 179 if defaults is not None: 180 defaults = tuple(defaults) 181 if len(defaults) > len(field_names): 182 raise TypeError("Got more default values than field names") 183 field_defaults = dict( 184 reversed(list(zip(reversed(field_names), reversed(defaults)))) 185 ) 186 187 field_names = tuple(map(_sys.intern, field_names)) 188 arrange_names = tuple(map(_sys.intern, arrange_names)) 189 num_fields = len(field_names) 190 num_arrange_fields = len(arrange_names) 191 arg_list = ", ".join(field_names) 192 if num_fields == 1: 193 arg_list += "," 194 repr_fmt = "(" + ", ".join(f"{name}=%r" for name in arrange_names) + ")" 195 _dict, _tuple, _len, _zip = dict, tuple, len, zip 196 197 namespace = { 198 "_tuple_new": tuple_new, 199 "__builtins__": {}, 200 "__name__": f"swizzledtuple_{typename}", 201 } 202 code = f"lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))" 203 __new__ = eval(code, namespace) 204 __new__.__name__ = "__new__" 205 __new__.__doc__ = f"Create new instance of {typename}({arg_list})" 206 if defaults is not None: 207 __new__.__defaults__ = defaults 208 209 @classmethod 210 def _make(cls, iterable): 211 result = tuple_new(cls, iterable) 212 if _len(result) != num_arrange_fields: 213 raise ValueError( 214 f"Expected {num_arrange_fields} arguments, got {len(result)}" 215 ) 216 return result 217 218 _make.__func__.__doc__ = f"Make a new {typename} object from a sequence or iterable" 219 220 def _replace(self, /, **kwds): 221 def generator(): 222 for name in field_names: 223 if name in kwds: 224 yield kwds.pop(name) 225 else: 226 yield getattr(self, name) 227 228 result = self._make(iter(generator())) 229 if kwds: 230 raise ValueError(f"Got unexpected field names: {list(kwds)!r}") 231 return result 232 233 _replace.__doc__ = ( 234 f"Return a new {typename} object replacing specified fields with new values" 235 ) 236 237 def __repr__(self): 238 "Return a nicely formatted representation string" 239 return self.__class__.__name__ + repr_fmt % self 240 241 def _asdict(self): 242 "Return a new dict which maps field names to their values." 243 return _dict(_zip(arrange_names, self)) 244 245 def __getnewargs__(self): 246 "Return self as a plain tuple. Used by copy and pickle." 247 return _tuple(self) 248 249 @swizzle_attributes_retriever(sep=sep, type=swizzledtuple, only_attrs=field_names) 250 def __getattribute__(self, attr_name): 251 return super(_tuple, self).__getattribute__(attr_name) 252 253 def __getitem__(self, index): 254 if not isinstance(index, slice): 255 return _tuple.__getitem__(self, index) 256 257 selected_indices = arrange_indices[index] 258 selected_values = _tuple.__getitem__(self, index) 259 260 seen = set() 261 filtered = [ 262 (i, v, field_names[i]) 263 for i, v in zip(selected_indices, selected_values) 264 if not (i in seen or seen.add(i)) 265 ] 266 267 if filtered: 268 _, filtered_values, filtered_names = zip(*filtered) 269 else: 270 filtered_values, filtered_names = (), () 271 272 return swizzledtuple( 273 typename, 274 filtered_names, 275 rename=rename, 276 defaults=filtered_values, 277 module=module, 278 arrange_names=arrange_names[index], 279 sep=sep, 280 )() 281 282 for method in ( 283 __new__, 284 _make.__func__, 285 _replace, 286 __repr__, 287 _asdict, 288 __getnewargs__, 289 __getattribute__, 290 __getitem__, 291 ): 292 method.__qualname__ = f"{typename}.{method.__name__}" 293 294 class_namespace = { 295 "__doc__": f"{typename}({arg_list})", 296 "__slots__": (), 297 "_fields": field_names, 298 "_field_defaults": field_defaults, 299 "__new__": __new__, 300 "_make": _make, 301 "_replace": _replace, 302 "__repr__": __repr__, 303 "_asdict": _asdict, 304 "__getnewargs__": __getnewargs__, 305 "__getattribute__": __getattribute__, 306 "__getitem__": __getitem__, 307 } 308 seen = set() 309 for index, name in enumerate(arrange_names): 310 if name in seen: 311 continue 312 doc = _sys.intern(f"Alias for field number {index}") 313 class_namespace[name] = _tuplegetter(index, doc) 314 seen.add(name) 315 316 result = type(typename, (tuple,), class_namespace) 317 318 if module is None: 319 try: 320 module = _sys._getframemodulename(1) or "__main__" 321 except AttributeError: 322 try: 323 module = _sys._getframe(1).f_globals.get("__name__", "__main__") 324 except (AttributeError, ValueError): 325 pass 326 if module is not None: 327 result.__module__ = module 328 329 return result 330 331 332def swizzle_attributes_retriever( 333 getattr_funcs=None, 334 sep=None, 335 type=swizzledtuple, 336 only_attrs=None, 337 *, 338 setter=None, 339): 340 if sep is not None and not is_valid_sep(sep): 341 raise ValueError(f"Invalid value for sep: {sep!r}.") 342 343 if sep is None: 344 sep = "" 345 346 sep_len = len(sep) 347 348 split = None 349 trie = None 350 if isinstance(only_attrs, int): 351 split = only_attrs 352 only_attrs = None 353 elif only_attrs: 354 only_attrs = set(only_attrs) 355 if sep and not any(sep in attr for attr in only_attrs): 356 split = "by_sep" 357 elif len(set(len(attr) for attr in only_attrs)) == 1: 358 split = len(next(iter(only_attrs))) 359 if not split: 360 trie = Trie(only_attrs, sep) 361 362 def _swizzle_attributes_retriever(getattr_funcs): 363 if not isinstance(getattr_funcs, list): 364 getattr_funcs = [getattr_funcs] 365 366 def get_attribute(obj, attr_name): 367 for func in getattr_funcs: 368 try: 369 return func(obj, attr_name) 370 except AttributeError: 371 continue 372 return MISSING 373 374 def retrieve_attributes(obj, attr_name): 375 # Attempt to find an exact attribute match 376 attribute = get_attribute(obj, attr_name) 377 if attribute is not MISSING: 378 return [attr_name], [attribute] 379 380 matched_attributes = [] 381 arranged_names = [] 382 # If a sep is provided, split the name accordingly 383 if split is not None: 384 attr_parts = split_attr_name(attr_name, split, sep) 385 arranged_names = attr_parts 386 for part in attr_parts: 387 if only_attrs and part not in only_attrs: 388 raise AttributeError( 389 f"Attribute {part} is not part of an allowed field for swizzling" 390 ) 391 attribute = get_attribute(obj, part) 392 if attribute is not MISSING: 393 matched_attributes.append(attribute) 394 else: 395 raise AttributeError(f"No matching attribute found for {part}") 396 elif trie: 397 for i, name in enumerate(trie.split_longest_prefix(attr_name)): 398 attribute = get_attribute(obj, name) 399 if attribute is not MISSING: 400 arranged_names.append(name) 401 matched_attributes.append(attribute) 402 else: 403 raise AttributeError(f"No matching attribute found for {name}") 404 else: 405 # No sep provided, attempt to match substrings 406 i = 0 407 attr_len = len(attr_name) 408 409 while i < attr_len: 410 match_found = False 411 for j in range(attr_len, i, -1): 412 substring = attr_name[i:j] 413 attribute = get_attribute(obj, substring) 414 if attribute is not MISSING: 415 matched_attributes.append(attribute) 416 arranged_names.append(substring) 417 418 next_pos = j 419 if sep_len and next_pos < attr_len: 420 if not attr_name.startswith(sep, next_pos): 421 raise AttributeError( 422 f"Expected separator '{sep}' at pos {next_pos} in " 423 f"'{attr_name}', found '{attr_name[next_pos : next_pos + sep_len]}'" 424 ) 425 next_pos += sep_len 426 if next_pos == attr_len: 427 raise AttributeError( 428 f"Seperator can not be at the end of the string: {attr_name}" 429 ) 430 431 i = next_pos 432 match_found = True 433 break 434 if not match_found: 435 raise AttributeError( 436 f"No matching attribute found for substring: {attr_name[i:]}" 437 ) 438 return arranged_names, matched_attributes 439 440 @wraps(getattr_funcs[-1]) 441 def get_attributes(obj, attr_name): 442 arranged_names, matched_attributes = retrieve_attributes(obj, attr_name) 443 if len(matched_attributes) == 1: 444 return matched_attributes[0] 445 if type == swizzledtuple: 446 seen = set() 447 field_names, field_values = zip( 448 *[ 449 (name, matched_attributes[i]) 450 for i, name in enumerate(arranged_names) 451 if name not in seen and not seen.add(name) 452 ] 453 ) 454 455 name = "swizzledtuple" 456 if hasattr(obj, "__name__"): 457 name = obj.__name__ 458 elif hasattr(obj, "__class__"): 459 if hasattr(obj.__class__, "__name__"): 460 name = obj.__class__.__name__ 461 result = type( 462 name, 463 field_names, 464 arrange_names=arranged_names, 465 sep=sep, 466 ) 467 result = result(*field_values) 468 return result 469 470 return type(matched_attributes) 471 472 def set_attributes(obj, attr_name, value): 473 try: 474 arranged_names, _ = retrieve_attributes(obj, attr_name) 475 except AttributeError: 476 return setter(obj, attr_name, value) 477 478 if not isinstance(value, Iterable): 479 raise ValueError( 480 f"Expected an iterable value for swizzle attribute assignment, got {_type(value)}" 481 ) 482 if len(arranged_names) != len(value): 483 raise ValueError( 484 f"Expected {len(arranged_names)} values for swizzle attribute assignment, got {len(value)}" 485 ) 486 kv = {} 487 for k, v in zip(arranged_names, value): 488 _v = kv.get(k, MISSING) 489 if _v is MISSING: 490 kv[k] = v 491 elif _v is not v: 492 raise ValueError( 493 f"Tries to assign different values to attribute {k} in one go but only one is allowed" 494 ) 495 for k, v in kv.items(): 496 setter(obj, k, v) 497 498 if setter is not None: 499 return get_attributes, wraps(setter)(set_attributes) 500 return get_attributes 501 502 if getattr_funcs is not None: 503 return _swizzle_attributes_retriever(getattr_funcs) 504 else: 505 return _swizzle_attributes_retriever 506 507 508def swizzle( 509 cls=None, 510 meta=False, 511 sep=None, 512 type=swizzledtuple, 513 only_attrs=None, 514 setter=False, 515): 516 """ 517 Decorator that adds attribute swizzling capabilities to a class. 518 519 When accessing an attribute, normal lookup is attempted first. If that fails, 520 the attribute name is interpreted as a sequence of existing attribute names to 521 be "swizzled." For example, if an object `p` has attributes `x` and `y`, then 522 `p.x` behaves normally, but `p.yx` triggers swizzling logic and returns `(p.y, p.x)`. 523 524 Args: 525 cls (type, optional): Class to decorate. If `None`, returns a decorator function 526 for later use. Defaults to `None`. 527 meta (bool, optional): If `True`, applies swizzling to the class’s metaclass, 528 enabling swizzling of class-level attributes. Defaults to `False`. 529 sep (str, optional): Separator used between attribute names (e.g., `'_'` in `obj.x_y`). 530 If `None`, attributes are concatenated directly. Defaults to `None`. 531 type (type, optional): Type used for the returned collection of swizzled attributes. 532 Defaults to `swizzledtuple` (a tuple subclass with swizzling behavior). Can be 533 set to `tuple` or any compatible type. 534 only_attrs (iterable of str, int, or AttrSource, optional): Specifies allowed attributes 535 for swizzling: 536 - Iterable of strings: allowlist of attribute names. 537 - Integer: restricts to attribute names of that length. 538 - `AttrSource.SLOTS`: uses attributes from the class’s `__slots__`. 539 - `None`: all attributes allowed. Defaults to `None`. 540 setter (bool, optional): Enables assignment to swizzled attributes (e.g., `obj.xy = 1, 2`). 541 Strongly recommended to define `__slots__` when enabled to avoid accidental new attributes. 542 Defaults to `False`. 543 Returns: 544 type or callable: If `cls` is provided, returns the decorated class. Otherwise, returns 545 a decorator function to apply later. 546 547 Example: 548 ```python 549 @swizzle 550 class Point: 551 def __init__(self, x, y): 552 self.x = x 553 self.y = y 554 555 p = Point(1, 2) 556 print(p.yx) # Output: (2, 1) 557 ``` 558 """ 559 560 def preserve_metadata( 561 target, 562 source, 563 keys=("__name__", "__qualname__", "__doc__", "__module__", "__annotations__"), 564 ): 565 for key in keys: 566 if hasattr(source, key): 567 try: 568 setattr(target, key, getattr(source, key)) 569 except (TypeError, AttributeError): 570 pass # some attributes may be read-only 571 572 def class_decorator(cls): 573 # Collect attribute retrieval functions from the class 574 nonlocal only_attrs 575 if isinstance(only_attrs, str): 576 if only_attrs == AttrSource.SLOTS: 577 only_attrs = cls.__slots__ 578 if not only_attrs: 579 raise ValueError( 580 f"cls.__slots__ cannot be empty for only_attrs = {AttrSource.SLOTS}" 581 ) 582 elif only_attrs == AttrSource.FIELDS: 583 if hasattr(cls, "_fields"): 584 only_attrs = cls._fields 585 elif is_dataclass(cls): 586 only_attrs = [f.name for f in dataclass_fields(cls)] 587 else: 588 raise AttributeError( 589 f"No fields _fields or dataclass fields found in {cls} for only_attrs = {AttrSource.FIELDS}" 590 ) 591 if not only_attrs: 592 raise ValueError( 593 f"Fields can not be empty for only_attrs = {AttrSource.FIELDS}" 594 ) 595 596 getattr_methods = get_getattr_methods(cls) 597 598 if setter: 599 setattr_method = get_setattr_method(cls) 600 new_getter, new_setter = swizzle_attributes_retriever( 601 getattr_methods, 602 sep, 603 type, 604 only_attrs, 605 setter=setattr_method, 606 ) 607 setattr(cls, getattr_methods[-1].__name__, new_getter) 608 setattr(cls, setattr_method.__name__, new_setter) 609 else: 610 new_getter = swizzle_attributes_retriever( 611 getattr_methods, sep, type, only_attrs, setter=None 612 ) 613 setattr(cls, getattr_methods[-1].__name__, new_getter) 614 615 # Handle meta-class swizzling if requested 616 if meta: 617 meta_cls = _type(cls) 618 619 class SwizzledMetaType(meta_cls): 620 pass 621 622 if meta_cls == EnumMeta: 623 624 def cfem_dummy(*args, **kwargs): 625 pass 626 627 cfem = SwizzledMetaType._check_for_existing_members_ 628 SwizzledMetaType._check_for_existing_members_ = cfem_dummy 629 630 class SwizzledClass(cls, metaclass=SwizzledMetaType): 631 pass 632 633 if meta_cls == EnumMeta: 634 SwizzledMetaType._check_for_existing_members_ = cfem 635 636 # Preserve metadata on swizzled meta and class 637 preserve_metadata(SwizzledMetaType, meta_cls) 638 preserve_metadata(SwizzledClass, cls) 639 640 meta_cls = SwizzledMetaType 641 cls = SwizzledClass 642 643 meta_funcs = get_getattr_methods(meta_cls) 644 if setter: 645 setattr_method = get_setattr_method(meta_cls) 646 new_getter, new_setter = swizzle_attributes_retriever( 647 meta_funcs, 648 sep, 649 type, 650 only_attrs, 651 setter=setattr_method, 652 ) 653 setattr(meta_cls, meta_funcs[-1].__name__, new_getter) 654 setattr(meta_cls, setattr_method.__name__, new_setter) 655 else: 656 new_getter = swizzle_attributes_retriever( 657 meta_funcs, sep, type, only_attrs, setter=None 658 ) 659 setattr(meta_cls, meta_funcs[-1].__name__, new_getter) 660 return cls 661 662 if cls is None: 663 return class_decorator 664 else: 665 return class_decorator(cls) 666 667 668t = swizzledtuple 669# c = swizzledclass 670 671 672class Swizzle(types.ModuleType): 673 def __init__(self): 674 types.ModuleType.__init__(self, __name__) 675 self.__dict__.update(_sys.modules[__name__].__dict__) 676 677 def __call__( 678 self, 679 cls=None, 680 meta=False, 681 sep=None, 682 type=swizzledtuple, 683 only_attrs=None, 684 setter=False, 685 ): 686 return swizzle(cls, meta, sep, type, only_attrs, setter) 687 688 689_sys.modules[__name__] = Swizzle()
60def swizzledtuple( 61 typename, 62 field_names, 63 arrange_names=None, 64 *, 65 rename=False, 66 defaults=None, 67 module=None, 68 sep=None, 69): 70 """ 71 Creates a custom named tuple class with *swizzled attributes*, allowing for rearranged field names 72 and flexible attribute access patterns. 73 74 This function generates a subclass of Python's built-in `tuple`, similar to `collections.namedtuple`, 75 but with additional features: 76 77 - Field names can be rearranged using `arrange_names`. 78 - Attribute access can be customized using a separator string (`sep`). 79 - Invalid field names can be automatically renamed (`rename=True`). 80 - Supports custom default values, modules, and attribute formatting. 81 82 Args: 83 typename (str): Name of the new named tuple type. 84 field_names (Sequence[str] | str): List of field names, or a single string that will be split. 85 rename (bool, optional): If True, invalid field names are replaced with positional names. 86 Defaults to False. 87 defaults (Sequence, optional): Default values for fields. Defaults to None. 88 module (str, optional): Module name where the tuple is defined. Defaults to the caller's module. 89 arrange_names (Sequence[str] | str, optional): Optional ordering of fields for the final structure. 90 Can include duplicates. 91 sep (str, optional): Separator string used to construct compound attribute names. 92 If `sep = '_'` provided, attributes like `v.x_y` become accessible. Defaults to None. 93 Returns: 94 Type: A new subclass of `tuple` with named fields and custom swizzle behavior. 95 96 Example: 97 ```python 98 Vector = swizzledtuple("Vector", "x y z", arrange_names="y z x x") 99 v = Vector(1, 2, 3) 100 101 print(v) # Vector(y=2, z=3, x=1, x=1) 102 print(v.yzx) # Vector(y=2, z=3, x=1) 103 print(v.yzx.xxzyzz) # Vector(x=1, x=1, z=3, y=2, z=3, z=3) 104 ``` 105 """ 106 107 if isinstance(field_names, str): 108 field_names = field_names.replace(",", " ").split() 109 field_names = list(map(str, field_names)) 110 if arrange_names is not None: 111 if isinstance(arrange_names, str): 112 arrange_names = arrange_names.replace(",", " ").split() 113 arrange_names = list(map(str, arrange_names)) 114 assert set(arrange_names) == set(field_names), ( 115 "Arrangement must contain all field names" 116 ) 117 else: 118 arrange_names = field_names.copy() 119 120 typename = _sys.intern(str(typename)) 121 122 _dir = dir(tuple) + [ 123 "__match_args__", 124 "__module__", 125 "__slots__", 126 "_asdict", 127 "_field_defaults", 128 "_fields", 129 "_make", 130 "_replace", 131 ] 132 if rename: 133 seen = set() 134 name_newname = {} 135 for index, name in enumerate(field_names): 136 if ( 137 not name.isidentifier() 138 or _iskeyword(name) 139 or name in _dir 140 or name in seen 141 ): 142 field_names[index] = f"_{index}" 143 name_newname[name] = field_names[index] 144 seen.add(name) 145 for index, name in enumerate(arrange_names): 146 arrange_names[index] = name_newname[name] 147 148 for name in [typename] + field_names: 149 if type(name) is not str: 150 raise TypeError("Type names and field names must be strings") 151 if not name.isidentifier(): 152 raise ValueError( 153 f"Type names and field names must be valid identifiers: {name!r}" 154 ) 155 if _iskeyword(name): 156 raise ValueError( 157 f"Type names and field names cannot be a keyword: {name!r}" 158 ) 159 seen = set() 160 for name in field_names: 161 if name in _dir: 162 raise ValueError( 163 "Field names cannot be an attribute name which would shadow the namedtuple methods or attributes" 164 f"{name!r}" 165 ) 166 if name in seen: 167 raise ValueError(f"Encountered duplicate field name: {name!r}") 168 seen.add(name) 169 170 arrange_indices = [field_names.index(name) for name in arrange_names] 171 172 def tuple_new(cls, iterable): 173 new = [] 174 _iterable = list(iterable) 175 for index in arrange_indices: 176 new.append(_iterable[index]) 177 return tuple.__new__(cls, new) 178 179 field_defaults = {} 180 if defaults is not None: 181 defaults = tuple(defaults) 182 if len(defaults) > len(field_names): 183 raise TypeError("Got more default values than field names") 184 field_defaults = dict( 185 reversed(list(zip(reversed(field_names), reversed(defaults)))) 186 ) 187 188 field_names = tuple(map(_sys.intern, field_names)) 189 arrange_names = tuple(map(_sys.intern, arrange_names)) 190 num_fields = len(field_names) 191 num_arrange_fields = len(arrange_names) 192 arg_list = ", ".join(field_names) 193 if num_fields == 1: 194 arg_list += "," 195 repr_fmt = "(" + ", ".join(f"{name}=%r" for name in arrange_names) + ")" 196 _dict, _tuple, _len, _zip = dict, tuple, len, zip 197 198 namespace = { 199 "_tuple_new": tuple_new, 200 "__builtins__": {}, 201 "__name__": f"swizzledtuple_{typename}", 202 } 203 code = f"lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))" 204 __new__ = eval(code, namespace) 205 __new__.__name__ = "__new__" 206 __new__.__doc__ = f"Create new instance of {typename}({arg_list})" 207 if defaults is not None: 208 __new__.__defaults__ = defaults 209 210 @classmethod 211 def _make(cls, iterable): 212 result = tuple_new(cls, iterable) 213 if _len(result) != num_arrange_fields: 214 raise ValueError( 215 f"Expected {num_arrange_fields} arguments, got {len(result)}" 216 ) 217 return result 218 219 _make.__func__.__doc__ = f"Make a new {typename} object from a sequence or iterable" 220 221 def _replace(self, /, **kwds): 222 def generator(): 223 for name in field_names: 224 if name in kwds: 225 yield kwds.pop(name) 226 else: 227 yield getattr(self, name) 228 229 result = self._make(iter(generator())) 230 if kwds: 231 raise ValueError(f"Got unexpected field names: {list(kwds)!r}") 232 return result 233 234 _replace.__doc__ = ( 235 f"Return a new {typename} object replacing specified fields with new values" 236 ) 237 238 def __repr__(self): 239 "Return a nicely formatted representation string" 240 return self.__class__.__name__ + repr_fmt % self 241 242 def _asdict(self): 243 "Return a new dict which maps field names to their values." 244 return _dict(_zip(arrange_names, self)) 245 246 def __getnewargs__(self): 247 "Return self as a plain tuple. Used by copy and pickle." 248 return _tuple(self) 249 250 @swizzle_attributes_retriever(sep=sep, type=swizzledtuple, only_attrs=field_names) 251 def __getattribute__(self, attr_name): 252 return super(_tuple, self).__getattribute__(attr_name) 253 254 def __getitem__(self, index): 255 if not isinstance(index, slice): 256 return _tuple.__getitem__(self, index) 257 258 selected_indices = arrange_indices[index] 259 selected_values = _tuple.__getitem__(self, index) 260 261 seen = set() 262 filtered = [ 263 (i, v, field_names[i]) 264 for i, v in zip(selected_indices, selected_values) 265 if not (i in seen or seen.add(i)) 266 ] 267 268 if filtered: 269 _, filtered_values, filtered_names = zip(*filtered) 270 else: 271 filtered_values, filtered_names = (), () 272 273 return swizzledtuple( 274 typename, 275 filtered_names, 276 rename=rename, 277 defaults=filtered_values, 278 module=module, 279 arrange_names=arrange_names[index], 280 sep=sep, 281 )() 282 283 for method in ( 284 __new__, 285 _make.__func__, 286 _replace, 287 __repr__, 288 _asdict, 289 __getnewargs__, 290 __getattribute__, 291 __getitem__, 292 ): 293 method.__qualname__ = f"{typename}.{method.__name__}" 294 295 class_namespace = { 296 "__doc__": f"{typename}({arg_list})", 297 "__slots__": (), 298 "_fields": field_names, 299 "_field_defaults": field_defaults, 300 "__new__": __new__, 301 "_make": _make, 302 "_replace": _replace, 303 "__repr__": __repr__, 304 "_asdict": _asdict, 305 "__getnewargs__": __getnewargs__, 306 "__getattribute__": __getattribute__, 307 "__getitem__": __getitem__, 308 } 309 seen = set() 310 for index, name in enumerate(arrange_names): 311 if name in seen: 312 continue 313 doc = _sys.intern(f"Alias for field number {index}") 314 class_namespace[name] = _tuplegetter(index, doc) 315 seen.add(name) 316 317 result = type(typename, (tuple,), class_namespace) 318 319 if module is None: 320 try: 321 module = _sys._getframemodulename(1) or "__main__" 322 except AttributeError: 323 try: 324 module = _sys._getframe(1).f_globals.get("__name__", "__main__") 325 except (AttributeError, ValueError): 326 pass 327 if module is not None: 328 result.__module__ = module 329 330 return result
Creates a custom named tuple class with swizzled attributes, allowing for rearranged field names and flexible attribute access patterns.
This function generates a subclass of Python's built-in tuple
, similar to collections.namedtuple
,
but with additional features:
- Field names can be rearranged using
arrange_names
. - Attribute access can be customized using a separator string (
sep
). - Invalid field names can be automatically renamed (
rename=True
). - Supports custom default values, modules, and attribute formatting.
Arguments:
- typename (str): Name of the new named tuple type.
- field_names (Sequence[str] | str): List of field names, or a single string that will be split.
- rename (bool, optional): If True, invalid field names are replaced with positional names. Defaults to False.
- defaults (Sequence, optional): Default values for fields. Defaults to None.
- module (str, optional): Module name where the tuple is defined. Defaults to the caller's module.
- arrange_names (Sequence[str] | str, optional): Optional ordering of fields for the final structure. Can include duplicates.
- sep (str, optional): Separator string used to construct compound attribute names.
If
sep = '_'
provided, attributes likev.x_y
become accessible. Defaults to None.
Returns:
Type: A new subclass of
tuple
with named fields and custom swizzle behavior.
Example:
Vector = swizzledtuple("Vector", "x y z", arrange_names="y z x x") v = Vector(1, 2, 3) print(v) # Vector(y=2, z=3, x=1, x=1) print(v.yzx) # Vector(y=2, z=3, x=1) print(v.yzx.xxzyzz) # Vector(x=1, x=1, z=3, y=2, z=3, z=3)
60def swizzledtuple( 61 typename, 62 field_names, 63 arrange_names=None, 64 *, 65 rename=False, 66 defaults=None, 67 module=None, 68 sep=None, 69): 70 """ 71 Creates a custom named tuple class with *swizzled attributes*, allowing for rearranged field names 72 and flexible attribute access patterns. 73 74 This function generates a subclass of Python's built-in `tuple`, similar to `collections.namedtuple`, 75 but with additional features: 76 77 - Field names can be rearranged using `arrange_names`. 78 - Attribute access can be customized using a separator string (`sep`). 79 - Invalid field names can be automatically renamed (`rename=True`). 80 - Supports custom default values, modules, and attribute formatting. 81 82 Args: 83 typename (str): Name of the new named tuple type. 84 field_names (Sequence[str] | str): List of field names, or a single string that will be split. 85 rename (bool, optional): If True, invalid field names are replaced with positional names. 86 Defaults to False. 87 defaults (Sequence, optional): Default values for fields. Defaults to None. 88 module (str, optional): Module name where the tuple is defined. Defaults to the caller's module. 89 arrange_names (Sequence[str] | str, optional): Optional ordering of fields for the final structure. 90 Can include duplicates. 91 sep (str, optional): Separator string used to construct compound attribute names. 92 If `sep = '_'` provided, attributes like `v.x_y` become accessible. Defaults to None. 93 Returns: 94 Type: A new subclass of `tuple` with named fields and custom swizzle behavior. 95 96 Example: 97 ```python 98 Vector = swizzledtuple("Vector", "x y z", arrange_names="y z x x") 99 v = Vector(1, 2, 3) 100 101 print(v) # Vector(y=2, z=3, x=1, x=1) 102 print(v.yzx) # Vector(y=2, z=3, x=1) 103 print(v.yzx.xxzyzz) # Vector(x=1, x=1, z=3, y=2, z=3, z=3) 104 ``` 105 """ 106 107 if isinstance(field_names, str): 108 field_names = field_names.replace(",", " ").split() 109 field_names = list(map(str, field_names)) 110 if arrange_names is not None: 111 if isinstance(arrange_names, str): 112 arrange_names = arrange_names.replace(",", " ").split() 113 arrange_names = list(map(str, arrange_names)) 114 assert set(arrange_names) == set(field_names), ( 115 "Arrangement must contain all field names" 116 ) 117 else: 118 arrange_names = field_names.copy() 119 120 typename = _sys.intern(str(typename)) 121 122 _dir = dir(tuple) + [ 123 "__match_args__", 124 "__module__", 125 "__slots__", 126 "_asdict", 127 "_field_defaults", 128 "_fields", 129 "_make", 130 "_replace", 131 ] 132 if rename: 133 seen = set() 134 name_newname = {} 135 for index, name in enumerate(field_names): 136 if ( 137 not name.isidentifier() 138 or _iskeyword(name) 139 or name in _dir 140 or name in seen 141 ): 142 field_names[index] = f"_{index}" 143 name_newname[name] = field_names[index] 144 seen.add(name) 145 for index, name in enumerate(arrange_names): 146 arrange_names[index] = name_newname[name] 147 148 for name in [typename] + field_names: 149 if type(name) is not str: 150 raise TypeError("Type names and field names must be strings") 151 if not name.isidentifier(): 152 raise ValueError( 153 f"Type names and field names must be valid identifiers: {name!r}" 154 ) 155 if _iskeyword(name): 156 raise ValueError( 157 f"Type names and field names cannot be a keyword: {name!r}" 158 ) 159 seen = set() 160 for name in field_names: 161 if name in _dir: 162 raise ValueError( 163 "Field names cannot be an attribute name which would shadow the namedtuple methods or attributes" 164 f"{name!r}" 165 ) 166 if name in seen: 167 raise ValueError(f"Encountered duplicate field name: {name!r}") 168 seen.add(name) 169 170 arrange_indices = [field_names.index(name) for name in arrange_names] 171 172 def tuple_new(cls, iterable): 173 new = [] 174 _iterable = list(iterable) 175 for index in arrange_indices: 176 new.append(_iterable[index]) 177 return tuple.__new__(cls, new) 178 179 field_defaults = {} 180 if defaults is not None: 181 defaults = tuple(defaults) 182 if len(defaults) > len(field_names): 183 raise TypeError("Got more default values than field names") 184 field_defaults = dict( 185 reversed(list(zip(reversed(field_names), reversed(defaults)))) 186 ) 187 188 field_names = tuple(map(_sys.intern, field_names)) 189 arrange_names = tuple(map(_sys.intern, arrange_names)) 190 num_fields = len(field_names) 191 num_arrange_fields = len(arrange_names) 192 arg_list = ", ".join(field_names) 193 if num_fields == 1: 194 arg_list += "," 195 repr_fmt = "(" + ", ".join(f"{name}=%r" for name in arrange_names) + ")" 196 _dict, _tuple, _len, _zip = dict, tuple, len, zip 197 198 namespace = { 199 "_tuple_new": tuple_new, 200 "__builtins__": {}, 201 "__name__": f"swizzledtuple_{typename}", 202 } 203 code = f"lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))" 204 __new__ = eval(code, namespace) 205 __new__.__name__ = "__new__" 206 __new__.__doc__ = f"Create new instance of {typename}({arg_list})" 207 if defaults is not None: 208 __new__.__defaults__ = defaults 209 210 @classmethod 211 def _make(cls, iterable): 212 result = tuple_new(cls, iterable) 213 if _len(result) != num_arrange_fields: 214 raise ValueError( 215 f"Expected {num_arrange_fields} arguments, got {len(result)}" 216 ) 217 return result 218 219 _make.__func__.__doc__ = f"Make a new {typename} object from a sequence or iterable" 220 221 def _replace(self, /, **kwds): 222 def generator(): 223 for name in field_names: 224 if name in kwds: 225 yield kwds.pop(name) 226 else: 227 yield getattr(self, name) 228 229 result = self._make(iter(generator())) 230 if kwds: 231 raise ValueError(f"Got unexpected field names: {list(kwds)!r}") 232 return result 233 234 _replace.__doc__ = ( 235 f"Return a new {typename} object replacing specified fields with new values" 236 ) 237 238 def __repr__(self): 239 "Return a nicely formatted representation string" 240 return self.__class__.__name__ + repr_fmt % self 241 242 def _asdict(self): 243 "Return a new dict which maps field names to their values." 244 return _dict(_zip(arrange_names, self)) 245 246 def __getnewargs__(self): 247 "Return self as a plain tuple. Used by copy and pickle." 248 return _tuple(self) 249 250 @swizzle_attributes_retriever(sep=sep, type=swizzledtuple, only_attrs=field_names) 251 def __getattribute__(self, attr_name): 252 return super(_tuple, self).__getattribute__(attr_name) 253 254 def __getitem__(self, index): 255 if not isinstance(index, slice): 256 return _tuple.__getitem__(self, index) 257 258 selected_indices = arrange_indices[index] 259 selected_values = _tuple.__getitem__(self, index) 260 261 seen = set() 262 filtered = [ 263 (i, v, field_names[i]) 264 for i, v in zip(selected_indices, selected_values) 265 if not (i in seen or seen.add(i)) 266 ] 267 268 if filtered: 269 _, filtered_values, filtered_names = zip(*filtered) 270 else: 271 filtered_values, filtered_names = (), () 272 273 return swizzledtuple( 274 typename, 275 filtered_names, 276 rename=rename, 277 defaults=filtered_values, 278 module=module, 279 arrange_names=arrange_names[index], 280 sep=sep, 281 )() 282 283 for method in ( 284 __new__, 285 _make.__func__, 286 _replace, 287 __repr__, 288 _asdict, 289 __getnewargs__, 290 __getattribute__, 291 __getitem__, 292 ): 293 method.__qualname__ = f"{typename}.{method.__name__}" 294 295 class_namespace = { 296 "__doc__": f"{typename}({arg_list})", 297 "__slots__": (), 298 "_fields": field_names, 299 "_field_defaults": field_defaults, 300 "__new__": __new__, 301 "_make": _make, 302 "_replace": _replace, 303 "__repr__": __repr__, 304 "_asdict": _asdict, 305 "__getnewargs__": __getnewargs__, 306 "__getattribute__": __getattribute__, 307 "__getitem__": __getitem__, 308 } 309 seen = set() 310 for index, name in enumerate(arrange_names): 311 if name in seen: 312 continue 313 doc = _sys.intern(f"Alias for field number {index}") 314 class_namespace[name] = _tuplegetter(index, doc) 315 seen.add(name) 316 317 result = type(typename, (tuple,), class_namespace) 318 319 if module is None: 320 try: 321 module = _sys._getframemodulename(1) or "__main__" 322 except AttributeError: 323 try: 324 module = _sys._getframe(1).f_globals.get("__name__", "__main__") 325 except (AttributeError, ValueError): 326 pass 327 if module is not None: 328 result.__module__ = module 329 330 return result
Creates a custom named tuple class with swizzled attributes, allowing for rearranged field names and flexible attribute access patterns.
This function generates a subclass of Python's built-in tuple
, similar to collections.namedtuple
,
but with additional features:
- Field names can be rearranged using
arrange_names
. - Attribute access can be customized using a separator string (
sep
). - Invalid field names can be automatically renamed (
rename=True
). - Supports custom default values, modules, and attribute formatting.
Arguments:
- typename (str): Name of the new named tuple type.
- field_names (Sequence[str] | str): List of field names, or a single string that will be split.
- rename (bool, optional): If True, invalid field names are replaced with positional names. Defaults to False.
- defaults (Sequence, optional): Default values for fields. Defaults to None.
- module (str, optional): Module name where the tuple is defined. Defaults to the caller's module.
- arrange_names (Sequence[str] | str, optional): Optional ordering of fields for the final structure. Can include duplicates.
- sep (str, optional): Separator string used to construct compound attribute names.
If
sep = '_'
provided, attributes likev.x_y
become accessible. Defaults to None.
Returns:
Type: A new subclass of
tuple
with named fields and custom swizzle behavior.
Example:
Vector = swizzledtuple("Vector", "x y z", arrange_names="y z x x") v = Vector(1, 2, 3) print(v) # Vector(y=2, z=3, x=1, x=1) print(v.yzx) # Vector(y=2, z=3, x=1) print(v.yzx.xxzyzz) # Vector(x=1, x=1, z=3, y=2, z=3, z=3)
53class AttrSource(str, Enum): 54 """Enum for specifying how to retrieve attributes from a class.""" 55 56 SLOTS = "slots" 57 FIELDS = "fields"
Enum for specifying how to retrieve attributes from a class.
509def swizzle( 510 cls=None, 511 meta=False, 512 sep=None, 513 type=swizzledtuple, 514 only_attrs=None, 515 setter=False, 516): 517 """ 518 Decorator that adds attribute swizzling capabilities to a class. 519 520 When accessing an attribute, normal lookup is attempted first. If that fails, 521 the attribute name is interpreted as a sequence of existing attribute names to 522 be "swizzled." For example, if an object `p` has attributes `x` and `y`, then 523 `p.x` behaves normally, but `p.yx` triggers swizzling logic and returns `(p.y, p.x)`. 524 525 Args: 526 cls (type, optional): Class to decorate. If `None`, returns a decorator function 527 for later use. Defaults to `None`. 528 meta (bool, optional): If `True`, applies swizzling to the class’s metaclass, 529 enabling swizzling of class-level attributes. Defaults to `False`. 530 sep (str, optional): Separator used between attribute names (e.g., `'_'` in `obj.x_y`). 531 If `None`, attributes are concatenated directly. Defaults to `None`. 532 type (type, optional): Type used for the returned collection of swizzled attributes. 533 Defaults to `swizzledtuple` (a tuple subclass with swizzling behavior). Can be 534 set to `tuple` or any compatible type. 535 only_attrs (iterable of str, int, or AttrSource, optional): Specifies allowed attributes 536 for swizzling: 537 - Iterable of strings: allowlist of attribute names. 538 - Integer: restricts to attribute names of that length. 539 - `AttrSource.SLOTS`: uses attributes from the class’s `__slots__`. 540 - `None`: all attributes allowed. Defaults to `None`. 541 setter (bool, optional): Enables assignment to swizzled attributes (e.g., `obj.xy = 1, 2`). 542 Strongly recommended to define `__slots__` when enabled to avoid accidental new attributes. 543 Defaults to `False`. 544 Returns: 545 type or callable: If `cls` is provided, returns the decorated class. Otherwise, returns 546 a decorator function to apply later. 547 548 Example: 549 ```python 550 @swizzle 551 class Point: 552 def __init__(self, x, y): 553 self.x = x 554 self.y = y 555 556 p = Point(1, 2) 557 print(p.yx) # Output: (2, 1) 558 ``` 559 """ 560 561 def preserve_metadata( 562 target, 563 source, 564 keys=("__name__", "__qualname__", "__doc__", "__module__", "__annotations__"), 565 ): 566 for key in keys: 567 if hasattr(source, key): 568 try: 569 setattr(target, key, getattr(source, key)) 570 except (TypeError, AttributeError): 571 pass # some attributes may be read-only 572 573 def class_decorator(cls): 574 # Collect attribute retrieval functions from the class 575 nonlocal only_attrs 576 if isinstance(only_attrs, str): 577 if only_attrs == AttrSource.SLOTS: 578 only_attrs = cls.__slots__ 579 if not only_attrs: 580 raise ValueError( 581 f"cls.__slots__ cannot be empty for only_attrs = {AttrSource.SLOTS}" 582 ) 583 elif only_attrs == AttrSource.FIELDS: 584 if hasattr(cls, "_fields"): 585 only_attrs = cls._fields 586 elif is_dataclass(cls): 587 only_attrs = [f.name for f in dataclass_fields(cls)] 588 else: 589 raise AttributeError( 590 f"No fields _fields or dataclass fields found in {cls} for only_attrs = {AttrSource.FIELDS}" 591 ) 592 if not only_attrs: 593 raise ValueError( 594 f"Fields can not be empty for only_attrs = {AttrSource.FIELDS}" 595 ) 596 597 getattr_methods = get_getattr_methods(cls) 598 599 if setter: 600 setattr_method = get_setattr_method(cls) 601 new_getter, new_setter = swizzle_attributes_retriever( 602 getattr_methods, 603 sep, 604 type, 605 only_attrs, 606 setter=setattr_method, 607 ) 608 setattr(cls, getattr_methods[-1].__name__, new_getter) 609 setattr(cls, setattr_method.__name__, new_setter) 610 else: 611 new_getter = swizzle_attributes_retriever( 612 getattr_methods, sep, type, only_attrs, setter=None 613 ) 614 setattr(cls, getattr_methods[-1].__name__, new_getter) 615 616 # Handle meta-class swizzling if requested 617 if meta: 618 meta_cls = _type(cls) 619 620 class SwizzledMetaType(meta_cls): 621 pass 622 623 if meta_cls == EnumMeta: 624 625 def cfem_dummy(*args, **kwargs): 626 pass 627 628 cfem = SwizzledMetaType._check_for_existing_members_ 629 SwizzledMetaType._check_for_existing_members_ = cfem_dummy 630 631 class SwizzledClass(cls, metaclass=SwizzledMetaType): 632 pass 633 634 if meta_cls == EnumMeta: 635 SwizzledMetaType._check_for_existing_members_ = cfem 636 637 # Preserve metadata on swizzled meta and class 638 preserve_metadata(SwizzledMetaType, meta_cls) 639 preserve_metadata(SwizzledClass, cls) 640 641 meta_cls = SwizzledMetaType 642 cls = SwizzledClass 643 644 meta_funcs = get_getattr_methods(meta_cls) 645 if setter: 646 setattr_method = get_setattr_method(meta_cls) 647 new_getter, new_setter = swizzle_attributes_retriever( 648 meta_funcs, 649 sep, 650 type, 651 only_attrs, 652 setter=setattr_method, 653 ) 654 setattr(meta_cls, meta_funcs[-1].__name__, new_getter) 655 setattr(meta_cls, setattr_method.__name__, new_setter) 656 else: 657 new_getter = swizzle_attributes_retriever( 658 meta_funcs, sep, type, only_attrs, setter=None 659 ) 660 setattr(meta_cls, meta_funcs[-1].__name__, new_getter) 661 return cls 662 663 if cls is None: 664 return class_decorator 665 else: 666 return class_decorator(cls)
Decorator that adds attribute swizzling capabilities to a class.
When accessing an attribute, normal lookup is attempted first. If that fails,
the attribute name is interpreted as a sequence of existing attribute names to
be "swizzled." For example, if an object p
has attributes x
and y
, then
p.x
behaves normally, but p.yx
triggers swizzling logic and returns (p.y, p.x)
.
Arguments:
- cls (type, optional): Class to decorate. If
None
, returns a decorator function for later use. Defaults toNone
. - meta (bool, optional): If
True
, applies swizzling to the class’s metaclass, enabling swizzling of class-level attributes. Defaults toFalse
. - sep (str, optional): Separator used between attribute names (e.g.,
'_'
inobj.x_y
). IfNone
, attributes are concatenated directly. Defaults toNone
. - type (type, optional): Type used for the returned collection of swizzled attributes.
Defaults to
swizzledtuple
(a tuple subclass with swizzling behavior). Can be set totuple
or any compatible type. - only_attrs (iterable of str, int, or AttrSource, optional): Specifies allowed attributes
for swizzling:
- Iterable of strings: allowlist of attribute names.
- Integer: restricts to attribute names of that length.
AttrSource.SLOTS
: uses attributes from the class’s__slots__
.None
: all attributes allowed. Defaults toNone
.
- setter (bool, optional): Enables assignment to swizzled attributes (e.g.,
obj.xy = 1, 2
). Strongly recommended to define__slots__
when enabled to avoid accidental new attributes. Defaults toFalse
.
Returns:
type or callable: If
cls
is provided, returns the decorated class. Otherwise, returns a decorator function to apply later.
Example:
@swizzle class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(1, 2) print(p.yx) # Output: (2, 1)
333def swizzle_attributes_retriever( 334 getattr_funcs=None, 335 sep=None, 336 type=swizzledtuple, 337 only_attrs=None, 338 *, 339 setter=None, 340): 341 if sep is not None and not is_valid_sep(sep): 342 raise ValueError(f"Invalid value for sep: {sep!r}.") 343 344 if sep is None: 345 sep = "" 346 347 sep_len = len(sep) 348 349 split = None 350 trie = None 351 if isinstance(only_attrs, int): 352 split = only_attrs 353 only_attrs = None 354 elif only_attrs: 355 only_attrs = set(only_attrs) 356 if sep and not any(sep in attr for attr in only_attrs): 357 split = "by_sep" 358 elif len(set(len(attr) for attr in only_attrs)) == 1: 359 split = len(next(iter(only_attrs))) 360 if not split: 361 trie = Trie(only_attrs, sep) 362 363 def _swizzle_attributes_retriever(getattr_funcs): 364 if not isinstance(getattr_funcs, list): 365 getattr_funcs = [getattr_funcs] 366 367 def get_attribute(obj, attr_name): 368 for func in getattr_funcs: 369 try: 370 return func(obj, attr_name) 371 except AttributeError: 372 continue 373 return MISSING 374 375 def retrieve_attributes(obj, attr_name): 376 # Attempt to find an exact attribute match 377 attribute = get_attribute(obj, attr_name) 378 if attribute is not MISSING: 379 return [attr_name], [attribute] 380 381 matched_attributes = [] 382 arranged_names = [] 383 # If a sep is provided, split the name accordingly 384 if split is not None: 385 attr_parts = split_attr_name(attr_name, split, sep) 386 arranged_names = attr_parts 387 for part in attr_parts: 388 if only_attrs and part not in only_attrs: 389 raise AttributeError( 390 f"Attribute {part} is not part of an allowed field for swizzling" 391 ) 392 attribute = get_attribute(obj, part) 393 if attribute is not MISSING: 394 matched_attributes.append(attribute) 395 else: 396 raise AttributeError(f"No matching attribute found for {part}") 397 elif trie: 398 for i, name in enumerate(trie.split_longest_prefix(attr_name)): 399 attribute = get_attribute(obj, name) 400 if attribute is not MISSING: 401 arranged_names.append(name) 402 matched_attributes.append(attribute) 403 else: 404 raise AttributeError(f"No matching attribute found for {name}") 405 else: 406 # No sep provided, attempt to match substrings 407 i = 0 408 attr_len = len(attr_name) 409 410 while i < attr_len: 411 match_found = False 412 for j in range(attr_len, i, -1): 413 substring = attr_name[i:j] 414 attribute = get_attribute(obj, substring) 415 if attribute is not MISSING: 416 matched_attributes.append(attribute) 417 arranged_names.append(substring) 418 419 next_pos = j 420 if sep_len and next_pos < attr_len: 421 if not attr_name.startswith(sep, next_pos): 422 raise AttributeError( 423 f"Expected separator '{sep}' at pos {next_pos} in " 424 f"'{attr_name}', found '{attr_name[next_pos : next_pos + sep_len]}'" 425 ) 426 next_pos += sep_len 427 if next_pos == attr_len: 428 raise AttributeError( 429 f"Seperator can not be at the end of the string: {attr_name}" 430 ) 431 432 i = next_pos 433 match_found = True 434 break 435 if not match_found: 436 raise AttributeError( 437 f"No matching attribute found for substring: {attr_name[i:]}" 438 ) 439 return arranged_names, matched_attributes 440 441 @wraps(getattr_funcs[-1]) 442 def get_attributes(obj, attr_name): 443 arranged_names, matched_attributes = retrieve_attributes(obj, attr_name) 444 if len(matched_attributes) == 1: 445 return matched_attributes[0] 446 if type == swizzledtuple: 447 seen = set() 448 field_names, field_values = zip( 449 *[ 450 (name, matched_attributes[i]) 451 for i, name in enumerate(arranged_names) 452 if name not in seen and not seen.add(name) 453 ] 454 ) 455 456 name = "swizzledtuple" 457 if hasattr(obj, "__name__"): 458 name = obj.__name__ 459 elif hasattr(obj, "__class__"): 460 if hasattr(obj.__class__, "__name__"): 461 name = obj.__class__.__name__ 462 result = type( 463 name, 464 field_names, 465 arrange_names=arranged_names, 466 sep=sep, 467 ) 468 result = result(*field_values) 469 return result 470 471 return type(matched_attributes) 472 473 def set_attributes(obj, attr_name, value): 474 try: 475 arranged_names, _ = retrieve_attributes(obj, attr_name) 476 except AttributeError: 477 return setter(obj, attr_name, value) 478 479 if not isinstance(value, Iterable): 480 raise ValueError( 481 f"Expected an iterable value for swizzle attribute assignment, got {_type(value)}" 482 ) 483 if len(arranged_names) != len(value): 484 raise ValueError( 485 f"Expected {len(arranged_names)} values for swizzle attribute assignment, got {len(value)}" 486 ) 487 kv = {} 488 for k, v in zip(arranged_names, value): 489 _v = kv.get(k, MISSING) 490 if _v is MISSING: 491 kv[k] = v 492 elif _v is not v: 493 raise ValueError( 494 f"Tries to assign different values to attribute {k} in one go but only one is allowed" 495 ) 496 for k, v in kv.items(): 497 setter(obj, k, v) 498 499 if setter is not None: 500 return get_attributes, wraps(setter)(set_attributes) 501 return get_attributes 502 503 if getattr_funcs is not None: 504 return _swizzle_attributes_retriever(getattr_funcs) 505 else: 506 return _swizzle_attributes_retriever