Skip to content

Commit

Permalink
refactor: unsupport old arg names, use sep, fixed missing error on sep
Browse files Browse the repository at this point in the history
  • Loading branch information
janthmueller committed Oct 22, 2024
1 parent 57c45d8 commit 074a6d1
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 57 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Swizzle
[![PyPI Latest Release](https://img.shields.io/pypi/v/swizzle.svg)](https://pypi.org/project/swizzle/)
[![PyPI Latest Release](https://img.shields.io/pypi/v/swizzle.svg)](https://pypi.org/project/swizzle/)
[![Pepy Total Downlods](https://img.shields.io/pepy/dt/swizzle)](https://pepy.tech/project/swizzle)
[![GitHub License](https://img.shields.io/github/license/janthmueller/swizzle)](https://github.com/janthmueller/swizzle/blob/main/LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/janthmueller/swizzle.svg)](https://github.com/janthmueller/swizzle/stargazers)
Expand Down Expand Up @@ -84,7 +84,7 @@ Setting the `meta` argument to `True` in the swizzle decorator extends the `geta


### Sequential matching
Attributes are matched from left to right, starting with the longest substring match. This behavior can be controlled by the `seperator` argument in the swizzle decorator.
Attributes are matched from left to right, starting with the longest substring match. This behavior can be controlled by the `sep` argument in the swizzle decorator.
```python
import swizzle

Expand Down
65 changes: 25 additions & 40 deletions swizzle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Copyright (c) 2024 Jan T. Müller <[email protected]>

import warnings
from functools import wraps
import types
import builtins
Expand All @@ -13,19 +12,20 @@
except ImportError:
_tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)

_type = builtins.type

__version__ = "2.1.1"
__version__ = "2.2.0"

MISSING = object()

def swizzledtuple(typename, field_names, *, rename=False, defaults=None, module=None, arrange_names = None, seperator = None):
def swizzledtuple(typename, field_names, *, rename=False, defaults=None, module=None, arrange_names = None, sep = None):
"""
Create a custom named tuple class with swizzled attributes, allowing for rearranged field names
and customized attribute access.
This function generates a new subclass of `tuple` with named fields, similar to Python's
`collections.namedtuple`. However, it extends the functionality by allowing field names to be
rearranged, and attributes to be accessed with a customizable separator. The function also
rearranged, and attributes to be accessed with a customizable sep. The function also
provides additional safeguards for field naming and attribute access.
Args:
Expand All @@ -41,9 +41,9 @@ def swizzledtuple(typename, field_names, *, rename=False, defaults=None, module=
in which fields should be arranged in the resulting named tuple. This allows for fields
to be rearranged and, unlike standard `namedtuple`, can include duplicates. Defaults
to the order given in `field_names`.
separator (str, optional): A separator string that customizes the structure of attribute
access. If provided, this separator allows attributes to be accessed by combining field
names with the separator in between them. Defaults to no separator.
sep (str, optional): A separator string that customizes the structure of attribute
access. If provided, this sep allows attributes to be accessed by combining field
names with the sep in between them. Defaults to no sep.
Returns:
type: A new subclass of `tuple` with named fields and customized attribute access.
Expand All @@ -55,8 +55,8 @@ def swizzledtuple(typename, field_names, *, rename=False, defaults=None, module=
duplicates, which is not possible in a standard `namedtuple`.
- The generated named tuple class includes methods like `_make`, `_replace`, `__repr__`,
`_asdict`, and `__getnewargs__`, partially customized to handle the rearranged field order.
- The `separator` argument enables a custom structure for attribute access, allowing for
combined attribute names based on the provided separator. If no separator is provided,
- The `sep` argument enables a custom structure for attribute access, allowing for
combined attribute names based on the provided sep. If no sep is provided,
standard attribute access is used.
Example:
Expand Down Expand Up @@ -192,7 +192,7 @@ def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return _tuple(self)

@swizzle_attributes_retriever(separator=seperator, type = swizzledtuple)
@swizzle_attributes_retriever(sep=sep, type = swizzledtuple)
def __getattribute__(self, attr_name):
return super(_tuple, self).__getattribute__(attr_name)

Expand Down Expand Up @@ -245,18 +245,12 @@ def __getattribute__(self, attr_name):

return result

# deprecated name
def swizzlednamedtuple(typename, field_names, *, rename=False, defaults=None, module=None, arrange_names = None, seperator = None):
warnings.warn("swizzlednamedtuple is deprecated, use swizzledtuple instead", DeprecationWarning, stacklevel=2)
return swizzledtuple(typename, field_names, rename=rename, defaults=defaults, module=module, arrange_names=arrange_names, seperator=seperator)


# Helper function to split a string based on a separator
def split_string(string, separator):
if separator == '':
# Helper function to split a string based on a sep
def split_string(string, sep):
if sep == '':
return list(string)
else:
return string.split(separator)
return string.split(sep)

# Helper function to collect attribute retrieval functions from a class or meta-class
def collect_attribute_functions(cls):
Expand All @@ -271,7 +265,7 @@ def collect_attribute_functions(cls):

# Function to combine multiple attribute retrieval functions

def swizzle_attributes_retriever(attribute_funcs=None, separator=None, type = tuple):
def swizzle_attributes_retriever(attribute_funcs=None, sep=None, type = swizzledtuple):
def _swizzle_attributes_retriever(attribute_funcs):
if not isinstance(attribute_funcs, list):
attribute_funcs = [attribute_funcs]
Expand All @@ -293,16 +287,18 @@ def retrieve_swizzled_attributes(obj, attr_name):

matched_attributes = []
arranged_names = []
# If a separator is provided, split the name accordingly
if separator is not None:
attr_parts = split_string(attr_name, separator)
# If a sep is provided, split the name accordingly
if sep is not None:
attr_parts = split_string(attr_name, sep)
arranged_names = attr_parts
for part in attr_parts:
attribute = retrieve_attribute(obj, part)
if attribute is not MISSING:
matched_attributes.append(attribute)
else:
raise AttributeError(f"No matching attribute found for part: {part}")
else:
# No separator provided, attempt to match substrings
# No sep provided, attempt to match substrings
i = 0
while i < len(attr_name):
match_found = False
Expand All @@ -318,7 +314,7 @@ def retrieve_swizzled_attributes(obj, attr_name):
if not match_found:
raise AttributeError(f"No matching attribute found for substring: {attr_name[i:]}")

if type == swizzledtuple or type == swizzlednamedtuple:
if type == swizzledtuple:
field_names = set(arranged_names)
field_values = [retrieve_attribute(obj, name) for name in field_names]
name = "swizzledtuple"
Expand All @@ -342,23 +338,14 @@ def retrieve_swizzled_attributes(obj, attr_name):
return _swizzle_attributes_retriever

# Decorator function to enable swizzling for a class
def swizzle(cls=None, meta=False, separator=None, type = tuple, **kwargs):

if 'use_meta' in kwargs:
warnings.warn("The 'use_meta' argument is deprecated and will be removed in a future version. Use 'meta' instead.", DeprecationWarning, stacklevel=2)
meta = kwargs.pop('use_meta')
if '_type' in kwargs:
warnings.warn("The '_type' argument is deprecated and will be removed in a future version. Use 'type' instead.", DeprecationWarning, stacklevel=2)
type = kwargs.pop('_type')

_type = builtins.type
def swizzle(cls=None, meta=False, sep=None, type = tuple):

def class_decorator(cls):
# Collect attribute retrieval functions from the class
attribute_funcs = collect_attribute_functions(cls)

# Apply the swizzling to the class's attribute retrieval
setattr(cls, attribute_funcs[-1].__name__, swizzle_attributes_retriever(attribute_funcs, separator, type))
setattr(cls, attribute_funcs[-1].__name__, swizzle_attributes_retriever(attribute_funcs, sep, type))

# Handle meta-class swizzling if requested
if meta:
Expand All @@ -368,11 +355,9 @@ class SwizzledMetaType(meta_cls):
pass
meta_cls = SwizzledMetaType
cls = meta_cls(cls.__name__, cls.__bases__, dict(cls.__dict__))
meta_cls = SwizzledMetaType
cls = meta_cls(cls.__name__, cls.__bases__, dict(cls.__dict__))

meta_funcs = collect_attribute_functions(meta_cls)
setattr(meta_cls, meta_funcs[-1].__name__, swizzle_attributes_retriever(meta_funcs, separator, type))
setattr(meta_cls, meta_funcs[-1].__name__, swizzle_attributes_retriever(meta_funcs, sep, type))

return cls

Expand Down
45 changes: 31 additions & 14 deletions tests/attribute_swizzle_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ def __init__(self, x, y, z):
self.z = z

class TestVectorSwizzling(unittest.TestCase):

def setUp(self):
self.vector = Vector(1, 2, 3)

def test_swizzle_yzx(self):
self.assertEqual(self.vector.yzx, (2, 3, 1))

Expand All @@ -39,13 +39,13 @@ class XYZ:
z: int

class TestXYZDataclassSwizzling(unittest.TestCase):

def setUp(self):
self.xyz = XYZ(1, 2, 3)

def test_swizzle_yzx(self):
self.assertEqual(self.xyz.yzx, (2, 3, 1))

def test_invalid_swizzle(self):
with self.assertRaises(AttributeError):
_ = self.xyz.nonexistent_attribute
Expand All @@ -59,7 +59,7 @@ class XYZEnumMeta(IntEnum):
Z = 3

class TestXYZEnumMetaSwizzling(unittest.TestCase):

def test_swizzle_meta(self):
self.assertEqual(XYZEnumMeta.YXZ, (XYZEnumMeta.Y, XYZEnumMeta.X, XYZEnumMeta.Z))

Expand All @@ -72,13 +72,13 @@ class XYZNamedTuple(NamedTuple):
z: int

class TestXYZNamedTupleSwizzling(unittest.TestCase):

def setUp(self):
self.xyz = XYZNamedTuple(1, 2, 3)

def test_swizzle_yzx(self):
self.assertEqual(self.xyz.yzx, (2, 3, 1))

def test_invalid_swizzle(self):
with self.assertRaises(AttributeError):
_ = self.xyz.nonexistent_attribute
Expand All @@ -96,33 +96,50 @@ class Test:
xyz = 7

class TestSequentialAttributeMatching(unittest.TestCase):

def test_attribute_values(self):
self.assertEqual(Test.xz, 6)
self.assertEqual(Test.yz, 5)

def test_composite_swizzle(self):
self.assertEqual(Test.xyyz, (4, 5))
self.assertEqual(Test.xyzx, (7, 1))

### 6. **Invalid Swizzle Requests Tests**

class TestInvalidSwizzleRequests(unittest.TestCase):

def test_vector_invalid_swizzle(self):
vector = Vector(1, 2, 3)
with self.assertRaises(AttributeError):
_ = vector.nonexistent_attribute

def test_xyz_invalid_swizzle(self):
xyz = XYZ(1, 2, 3)
with self.assertRaises(AttributeError):
_ = xyz.nonexistent_attribute

def test_xyz_namedtuple_invalid_swizzle(self):
xyz = XYZNamedTuple(1, 2, 3)
with self.assertRaises(AttributeError):
_ = xyz.nonexistent_attribute

class TestSeparatorSwizzling(unittest.TestCase):

def setUp(self):
@swizzle(sep='')
class Test:
def __init__(self):
self.a = 0
self.test_list = Test()

def test_swizzle_list(self):
self.assertEqual(self.test_list.a, 0)
self.assertEqual(self.test_list.aa, (0, 0))

def test_swizzle_list_raise(self):
with self.assertRaises(AttributeError):
_ = self.test_list.aabb

if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion tests/swizzledtuple_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_make_method(self):
self.assertEqual(p, (1, 2, 3))

def test_integration(self):
MyTuple = swizzledtuple('MyTuple', 'x y z', arrange_names='z x y z', rename=True, defaults=(0,0,2), seperator='')
MyTuple = swizzledtuple('MyTuple', 'x y z', arrange_names='z x y z', rename=True, defaults=(0,0,2), sep='')
t = MyTuple(1)
self.assertEqual(t, (2, 1, 0, 2))
self.assertEqual(repr(t), 'MyTuple(z=2, x=1, y=0, z=2)')
Expand Down

0 comments on commit 074a6d1

Please sign in to comment.