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()
def swizzledtuple( typename, field_names, arrange_names=None, *, rename=False, defaults=None, module=None, sep=None):
 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 like v.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)
def t( typename, field_names, arrange_names=None, *, rename=False, defaults=None, module=None, sep=None):
 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 like v.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)
class AttrSource(builtins.str, enum.Enum):
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.

SLOTS = <AttrSource.SLOTS: 'slots'>
FIELDS = <AttrSource.FIELDS: 'fields'>
def swizzle( cls=None, meta=False, sep=None, type=<function swizzledtuple>, only_attrs=None, setter=False):
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 to None.
  • meta (bool, optional): If True, applies swizzling to the class’s metaclass, enabling swizzling of class-level attributes. Defaults to False.
  • sep (str, optional): Separator used between attribute names (e.g., '_' in obj.x_y). If None, attributes are concatenated directly. Defaults to None.
  • type (type, optional): Type used for the returned collection of swizzled attributes. Defaults to swizzledtuple (a tuple subclass with swizzling behavior). Can be set to tuple 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 to None.
  • 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 to False.
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)
def swizzle_attributes_retriever( getattr_funcs=None, sep=None, type=<function swizzledtuple>, only_attrs=None, *, setter=None):
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