From cf196cf550baac9be4ab8dacc236734e690ab35a Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 19:52:09 +0000 Subject: [PATCH 01/12] Add GitHub Actions workflow for publishing Python package --- .github/workflows/publish-python-package.yml | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/publish-python-package.yml diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml new file mode 100644 index 0000000..1fe5c2d --- /dev/null +++ b/.github/workflows/publish-python-package.yml @@ -0,0 +1,46 @@ +name: Publish Python Package + +on: + push: + branches: + - main + +jobs: + build_and_publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + + - name: Check for existing dist directory + run: | + if [ -d "dist" ]; then + echo "Error: 'dist' directory already exists." >&2 + exit 1 + fi + + - name: Build distribution + run: | + python setup.py sdist bdist_wheel + + - name: Check distribution + run: | + twine check dist/* + + - name: Upload distribution to PyPI + run: | + twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI }} From 0cfd4629c201697098d12de712ca26f309cdef63 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 20:20:49 +0000 Subject: [PATCH 02/12] Add support for module-level swizzling and swizzle for method args --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 043db94..a10223a 100644 --- a/README.md +++ b/README.md @@ -102,3 +102,6 @@ print(Test.xyyz) # Output: (4, 5) print(Test.xyzx) # Output: (7, 1) ``` +## To Do +- [ ] Add support for module-level swizzling +- [ ] Swizzle for method args (swizzle+partial) From 496e990a7a4163d47056ddea9a5688d5a34a6858 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 21:52:25 +0000 Subject: [PATCH 03/12] Fix swizzle typo in XYZ class --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a10223a..e49b115 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ class XYZ(IntEnum): Z = 3 # Test the swizzle -print(XYZ.yxz) # Output: (, , ) +print(XYZ.YXZ) # Output: (, , ) ``` Setting the `meta` argument to `True` in the swizzle decorator extends the `getattr` behavior of the metaclass, enabling attribute swizzling directly on the class itself. From 14be3160cd06205ef20b1dfe112ee4d6b0d6f83f Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 21:52:54 +0000 Subject: [PATCH 04/12] Update Python version in workflow; Update branches --- .github/workflows/publish-python-package.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index 1fe5c2d..edfb718 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: build_and_publish: @@ -16,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.x' - name: Install dependencies run: | @@ -44,3 +47,4 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI }} + \ No newline at end of file From 30e7ae978506f699025d8f59c1de5e7cb118c563 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 21:53:06 +0000 Subject: [PATCH 05/12] Add unit test workflow and attribute swizzle tests --- .github/workflows/unittests.yml | 42 +++++++++++ tests/attribute_swizzle_test.py | 128 ++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 .github/workflows/unittests.yml create mode 100644 tests/attribute_swizzle_test.py diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml new file mode 100644 index 0000000..bb0887d --- /dev/null +++ b/.github/workflows/unittests.yml @@ -0,0 +1,42 @@ +# .github/workflows/python-tests.yml +name: Unit Tests + +on: + push: + branches: + - '*' # Trigger on push to all branches + +jobs: + test: + runs-on: ubuntu-latest # The environment to run the job in + + steps: + - name: Checkout code + uses: actions/checkout@v3 # Checkout the repository code + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' # Specify the Python version to use + + - name: Install dependencies + run: | + if [ -f requirements.txt ]; then + echo "requirements.txt found, installing dependencies..." + python -m pip install --upgrade pip + pip install -r requirements.txt + else + echo "requirements.txt not found, skipping dependency installation." + fi + + - name: Run tests + run: | + mkdir -p test-results # Create directory for test results + python -m unittest discover -s tests -p "*test.py" | tee test-results/test_output.log + # Here, we're redirecting output to a log file in the test-results directory + + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: unittest-results + path: test-results/test_output.log diff --git a/tests/attribute_swizzle_test.py b/tests/attribute_swizzle_test.py new file mode 100644 index 0000000..d25d3fe --- /dev/null +++ b/tests/attribute_swizzle_test.py @@ -0,0 +1,128 @@ +import unittest +import sys +import os +from dataclasses import dataclass +from enum import IntEnum +from typing import NamedTuple + +# Assuming swizzle is a decorator in your project +sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/..")) +import swizzle + +# Vector class definition +@swizzle +class Vector: + def __init__(self, x, y, z): + self.x = x + self.y = y + 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)) + + def test_invalid_swizzle(self): + with self.assertRaises(AttributeError): + _ = self.vector.nonexistent_attribute + +### 2. **XYZ Dataclass Swizzling Tests** + +@dataclass +@swizzle +class XYZ: + x: int + y: int + 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 + +### 3. **XYZ IntEnum Meta Swizzling Tests** + +@swizzle(meta=True) +class XYZEnumMeta(IntEnum): + X = 1 + Y = 2 + Z = 3 + +class TestXYZEnumMetaSwizzling(unittest.TestCase): + + def test_swizzle_meta(self): + self.assertEqual(XYZEnumMeta.YXZ, (XYZEnumMeta.Y, XYZEnumMeta.X, XYZEnumMeta.Z)) + +### 4. **XYZ NamedTuple Swizzling Tests** + +@swizzle +class XYZNamedTuple(NamedTuple): + x: int + y: int + 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 + +### 5. **Sequential Attribute Matching Tests** + +@swizzle(meta=True) +class Test: + x = 1 + y = 2 + z = 3 + xy = 4 + yz = 5 + xz = 6 + 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 + +if __name__ == "__main__": + unittest.main() From 6545dfac3eb0290f489e1d064ca0a383dc3acb29 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 21:55:42 +0000 Subject: [PATCH 06/12] Add GitHub Actions extension to devcontainer.json --- .devcontainer/devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2303a82..167edd1 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,7 +11,8 @@ "ms-python.vscode-pylance", "ms-toolsai.jupyter", "GitHub.copilot", - "visualstudioexptteam.vscodeintellicode" + "visualstudioexptteam.vscodeintellicode", + "github.vscode-github-actions" ] } } From ef3fa7bab04e3fa17b51d4b5090569f086e1825e Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 21:56:20 +0000 Subject: [PATCH 07/12] Update Python version to 3.x --- .github/workflows/unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index bb0887d..5c1f9b5 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' # Specify the Python version to use + python-version: '3.x' # Specify the Python version to use - name: Install dependencies run: | From 6f99253e823122d9e52f75e0d75fb7367bf1bcf3 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 22:09:43 +0000 Subject: [PATCH 08/12] Updated .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9d68e29..43e3c46 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ build dist *.egg-info/ *.ipynb -*temp* -.pypirc \ No newline at end of file +*temp* \ No newline at end of file From d245fc683f1bc4c49c2beaa5ab7415631c76cbfa Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 22:09:52 +0000 Subject: [PATCH 09/12] Update version number to 0.1.4 --- swizzle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swizzle/__init__.py b/swizzle/__init__.py index c0e1a14..d5d6a7f 100755 --- a/swizzle/__init__.py +++ b/swizzle/__init__.py @@ -2,7 +2,7 @@ import sys import types -__version__ = "0.1.3" +__version__ = "0.1.4" def _split(s, sep): if sep == '': From 3bd0081f7ea280b83034ac8b15c57100c9a260a5 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 23:14:29 +0000 Subject: [PATCH 10/12] Update publish-python-package.yml to trigger workflow on version tags --- .github/workflows/publish-python-package.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index edfb718..fbac743 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -4,9 +4,8 @@ on: push: branches: - main - pull_request: - branches: - - main + tags: + - 'v*.*.*' jobs: build_and_publish: From 408dea158592e2470b83802555fbf87cd0ed3280 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Aug 2024 23:19:19 +0000 Subject: [PATCH 11/12] Update workflow to publish tag-versioned Python package --- .github/workflows/publish-python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index fbac743..f9ee7cd 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -1,4 +1,4 @@ -name: Publish Python Package +name: Publish Tag-Versioned Python Package on: push: From 53cec929436d4be48cfecbe8b635defe5b1336a6 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 2 Aug 2024 21:53:12 +0000 Subject: [PATCH 12/12] test --- .github/workflows/publish-python-package.yml | 96 +++---- .github/workflows/unittests.yml | 84 +++--- .gitignore | 12 +- README.md | 214 ++++++++-------- setup.py | 86 +++---- swizzle/__init__.py | 158 ++++++------ tests/attribute_swizzle_test.py | 256 +++++++++---------- 7 files changed, 453 insertions(+), 453 deletions(-) diff --git a/.github/workflows/publish-python-package.yml b/.github/workflows/publish-python-package.yml index f9ee7cd..5af1586 100644 --- a/.github/workflows/publish-python-package.yml +++ b/.github/workflows/publish-python-package.yml @@ -1,49 +1,49 @@ -name: Publish Tag-Versioned Python Package - -on: - push: - branches: - - main - tags: - - 'v*.*.*' - -jobs: - build_and_publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - - name: Check for existing dist directory - run: | - if [ -d "dist" ]; then - echo "Error: 'dist' directory already exists." >&2 - exit 1 - fi - - - name: Build distribution - run: | - python setup.py sdist bdist_wheel - - - name: Check distribution - run: | - twine check dist/* - - - name: Upload distribution to PyPI - run: | - twine upload dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI }} +name: Publish Tag-Versioned Python Package + +on: + push: + branches: + - main + tags: + - 'v*.*.*' + +jobs: + build_and_publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + + - name: Check for existing dist directory + run: | + if [ -d "dist" ]; then + echo "Error: 'dist' directory already exists." >&2 + exit 1 + fi + + - name: Build distribution + run: | + python setup.py sdist bdist_wheel + + - name: Check distribution + run: | + twine check dist/* + + - name: Upload distribution to PyPI + run: | + twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI }} \ No newline at end of file diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 5c1f9b5..27eae0c 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -1,42 +1,42 @@ -# .github/workflows/python-tests.yml -name: Unit Tests - -on: - push: - branches: - - '*' # Trigger on push to all branches - -jobs: - test: - runs-on: ubuntu-latest # The environment to run the job in - - steps: - - name: Checkout code - uses: actions/checkout@v3 # Checkout the repository code - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' # Specify the Python version to use - - - name: Install dependencies - run: | - if [ -f requirements.txt ]; then - echo "requirements.txt found, installing dependencies..." - python -m pip install --upgrade pip - pip install -r requirements.txt - else - echo "requirements.txt not found, skipping dependency installation." - fi - - - name: Run tests - run: | - mkdir -p test-results # Create directory for test results - python -m unittest discover -s tests -p "*test.py" | tee test-results/test_output.log - # Here, we're redirecting output to a log file in the test-results directory - - - name: Upload test results - uses: actions/upload-artifact@v3 - with: - name: unittest-results - path: test-results/test_output.log +# .github/workflows/python-tests.yml +name: Unit Tests + +on: + push: + branches: + - '*' # Trigger on push to all branches + +jobs: + test: + runs-on: ubuntu-latest # The environment to run the job in + + steps: + - name: Checkout code + uses: actions/checkout@v3 # Checkout the repository code + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version to use + + - name: Install dependencies + run: | + if [ -f requirements.txt ]; then + echo "requirements.txt found, installing dependencies..." + python -m pip install --upgrade pip + pip install -r requirements.txt + else + echo "requirements.txt not found, skipping dependency installation." + fi + + - name: Run tests + run: | + mkdir -p test-results # Create directory for test results + python -m unittest discover -s tests -p "*test.py" | tee test-results/test_output.log + # Here, we're redirecting output to a log file in the test-results directory + + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: unittest-results + path: test-results/test_output.log diff --git a/.gitignore b/.gitignore index 43e3c46..c3a50c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -.vscode/ -__pycache__/ -build -dist -*.egg-info/ -*.ipynb +.vscode/ +__pycache__/ +build +dist +*.egg-info/ +*.ipynb *temp* \ No newline at end of file diff --git a/README.md b/README.md index e49b115..cf1fbdd 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,107 @@ -# Swizzle Decorator - -The **Swizzle Decorator** for Python enhances attribute lookup methods (`__getattr__` or `__getattribute__`) to facilitate dynamic and flexible retrieval of multiple attributes based on specified arrangements of their names. This concept is reminiscent of swizzling in computer graphics, where it allows efficient access to components of vectors or coordinates in various orders: - -```python -import swizzle - -@swizzle -class Vector: - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - -print(Vector(1, 2, 3).yzx) # Output: (2, 3, 1) -``` - -## Installation -### From PyPI -```bash -pip install swizzle -``` -### From GitHub -```bash -pip install git+https://github.com/janthmueller/swizzle.git -``` - -## Further Examples - -### Using `swizzle` with `dataclass` - -```python -import swizzle -from dataclasses import dataclass - -@swizzle -@dataclass -class XYZ: - x: int - y: int - z: int - -# Test the swizzle -xyz = XYZ(1, 2, 3) -print(xyz.yzx) # Output: (2, 3, 1) -``` - -### Using `swizzle` with `IntEnum` - -```python -import swizzle -from enum import IntEnum - -@swizzle(meta=True) -class XYZ(IntEnum): - X = 1 - Y = 2 - Z = 3 - -# Test the swizzle -print(XYZ.YXZ) # Output: (, , ) -``` -Setting the `meta` argument to `True` in the swizzle decorator extends the `getattr` behavior of the metaclass, enabling attribute swizzling directly on the class itself. - -### Using `swizzle` with `NamedTuple` - -```python -import swizzle -from typing import NamedTuple - -@swizzle -class XYZ(NamedTuple): - x: int - y: int - z: int - -# Test the swizzle -xyz = XYZ(1, 2, 3) -print(xyz.yzx) # Output: (2, 3, 1) -``` - - -### Sequential matching -Attributes are matched from left to right, starting with the longest substring match. -```python -import swizzle - -@swizzle(meta=True) -class Test: - x = 1 - y = 2 - z = 3 - xy = 4 - yz = 5 - xz = 6 - xyz = 7 - -# Test the swizzle -print(Test.xz) # Output: 6 -print(Test.yz) # Output: 5 -print(Test.xyyz) # Output: (4, 5) -print(Test.xyzx) # Output: (7, 1) -``` - -## To Do -- [ ] Add support for module-level swizzling -- [ ] Swizzle for method args (swizzle+partial) +# Swizzle Decorator + +The **Swizzle Decorator** for Python enhances attribute lookup methods (`__getattr__` or `__getattribute__`) to facilitate dynamic and flexible retrieval of multiple attributes based on specified arrangements of their names. This concept is reminiscent of swizzling in computer graphics, where it allows efficient access to components of vectors or coordinates in various orders: + +```python +import swizzle + +@swizzle +class Vector: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + +print(Vector(1, 2, 3).yzx) # Output: (2, 3, 1) +``` + +## Installation +### From PyPI +```bash +pip install swizzle +``` +### From GitHub +```bash +pip install git+https://github.com/janthmueller/swizzle.git +``` + +## Further Examples + +### Using `swizzle` with `dataclass` + +```python +import swizzle +from dataclasses import dataclass + +@swizzle +@dataclass +class XYZ: + x: int + y: int + z: int + +# Test the swizzle +xyz = XYZ(1, 2, 3) +print(xyz.yzx) # Output: (2, 3, 1) +``` + +### Using `swizzle` with `IntEnum` + +```python +import swizzle +from enum import IntEnum + +@swizzle(meta=True) +class XYZ(IntEnum): + X = 1 + Y = 2 + Z = 3 + +# Test the swizzle +print(XYZ.YXZ) # Output: (, , ) +``` +Setting the `meta` argument to `True` in the swizzle decorator extends the `getattr` behavior of the metaclass, enabling attribute swizzling directly on the class itself. + +### Using `swizzle` with `NamedTuple` + +```python +import swizzle +from typing import NamedTuple + +@swizzle +class XYZ(NamedTuple): + x: int + y: int + z: int + +# Test the swizzle +xyz = XYZ(1, 2, 3) +print(xyz.yzx) # Output: (2, 3, 1) +``` + + +### Sequential matching +Attributes are matched from left to right, starting with the longest substring match. +```python +import swizzle + +@swizzle(meta=True) +class Test: + x = 1 + y = 2 + z = 3 + xy = 4 + yz = 5 + xz = 6 + xyz = 7 + +# Test the swizzle +print(Test.xz) # Output: 6 +print(Test.yz) # Output: 5 +print(Test.xyyz) # Output: (4, 5) +print(Test.xyzx) # Output: (7, 1) +``` + +## To Do +- [ ] Add support for module-level swizzling +- [ ] Swizzle for method args (swizzle+partial) diff --git a/setup.py b/setup.py index f0ac177..45d27a9 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,43 @@ -# Copyright (c) 2023 Jan T. Müller - -import sys -import os -from setuptools import setup, find_packages -import swizzle - -if sys.version_info < (3, 6): - sys.exit("ERROR: swizzle requires Python 3.6+") - -with open("README.md", "r", encoding="utf-8") as fh: - long_description = fh.read() - -setup( - name="swizzle", - version=swizzle.__version__, - packages=find_packages(exclude=["tests"]), - author="Jan T. Müller", - author_email="mail@jantmueller.com", - description="The Swizzle Decorator enables the retrieval of multiple attributes, similar to swizzling in computer graphics.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/janthmueller/swizzle", - project_urls={ - "Documentation": "https://github.com/janthmueller/swizzle/blob/main/README.md", - "Source": "https://github.com/janthmueller/swizzle", - "Tracker": "https://github.com/janthmueller/swizzle/issues", - }, - license="MIT", - classifiers=[ - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3 :: Only", - ], - python_requires=">=3.6", -) - - -# python setup.py sdist bdist_wheel -# twine check dist/* -# twine upload dist/* -> insert token +# Copyright (c) 2023 Jan T. Müller + +import sys +import os +from setuptools import setup, find_packages +import swizzle + +if sys.version_info < (3, 6): + sys.exit("ERROR: swizzle requires Python 3.6+") + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="swizzle", + version=swizzle.__version__, + packages=find_packages(exclude=["tests"]), + author="Jan T. Müller", + author_email="mail@jantmueller.com", + description="The Swizzle Decorator enables the retrieval of multiple attributes, similar to swizzling in computer graphics.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/janthmueller/swizzle", + project_urls={ + "Documentation": "https://github.com/janthmueller/swizzle/blob/main/README.md", + "Source": "https://github.com/janthmueller/swizzle", + "Tracker": "https://github.com/janthmueller/swizzle/issues", + }, + license="MIT", + classifiers=[ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + ], + python_requires=">=3.6", +) + + +# python setup.py sdist bdist_wheel +# twine check dist/* +# twine upload dist/* -> insert token diff --git a/swizzle/__init__.py b/swizzle/__init__.py index d5d6a7f..932dd16 100755 --- a/swizzle/__init__.py +++ b/swizzle/__init__.py @@ -1,80 +1,80 @@ -from functools import wraps -import sys -import types - -__version__ = "0.1.4" - -def _split(s, sep): - if sep == '': - return list(s) - else: - return s.split(sep) - -def _swizzle(func, sep = None): - def _getattr(obj, name, default=object()): - try: - return func(obj, name) - except AttributeError: - return default - - @wraps(func) - def _swizzle_attributes(obj, name): - """Find attributes of an object that match substrings of a given name.""" - try: - return func(obj, name) - except AttributeError: - pass - if sep is not None: - names = _split(name, sep) - found_attributes = [func(obj, n) for n in names] - else: - found_attributes = [] - sentinel = object() - i = 0 - while i < len(name): - match_found = False - for j in range(len(name), i, -1): - substr = name[i:j] - attr = _getattr(obj, substr, sentinel) - if attr is not sentinel: - found_attributes.append(attr) - i = j # Move index to end of the matched substring - match_found = True - break - if not match_found: - raise AttributeError(f"No matching attribute found for substring: {name[i:]}") - return tuple(found_attributes) - return _swizzle_attributes - -def swizzle(cls=None, meta = False, sep = None): - def decorator(cls): - # Decorate the class's __getattr__ or __getattribute__ - cls_fn = cls.__getattr__ if hasattr(cls, '__getattr__') else cls.__getattribute__ - setattr(cls, cls_fn.__name__, _swizzle(cls_fn, sep)) - - # Handle the meta class - if meta: - meta_cls = type(cls) - if meta_cls == type: - class SwizzleType(meta_cls): pass - meta_cls = SwizzleType - cls = meta_cls(cls.__name__, cls.__bases__, dict(cls.__dict__)) - meta_fn = meta_cls.__getattr__ if hasattr(meta_cls, '__getattr__') else meta_cls.__getattribute__ - setattr(meta_cls, meta_fn.__name__, _swizzle(meta_fn, sep)) - return cls - - if cls is None: - return decorator - else: - return decorator(cls) - -class Swizzle(types.ModuleType): - def __init__(self): - types.ModuleType.__init__(self, __name__) - self.__dict__.update(sys.modules[__name__].__dict__) - - def __call__(self, cls=None, meta=False, sep = None): - return swizzle(cls, meta, sep) - -sys.modules[__name__] = Swizzle() +from functools import wraps +import sys +import types + +__version__ = "0.1.4" + +def _split(s, sep): + if sep == '': + return list(s) + else: + return s.split(sep) + +def _swizzle(func, sep = None): + def _getattr(obj, name, default=object()): + try: + return func(obj, name) + except AttributeError: + return default + + @wraps(func) + def _swizzle_attributes(obj, name): + """Find attributes of an object that match substrings of a given name.""" + try: + return func(obj, name) + except AttributeError: + pass + if sep is not None: + names = _split(name, sep) + found_attributes = [func(obj, n) for n in names] + else: + found_attributes = [] + sentinel = object() + i = 0 + while i < len(name): + match_found = False + for j in range(len(name), i, -1): + substr = name[i:j] + attr = _getattr(obj, substr, sentinel) + if attr is not sentinel: + found_attributes.append(attr) + i = j # Move index to end of the matched substring + match_found = True + break + if not match_found: + raise AttributeError(f"No matching attribute found for substring: {name[i:]}") + return tuple(found_attributes) + return _swizzle_attributes + +def swizzle(cls=None, meta = False, sep = None): + def decorator(cls): + # Decorate the class's __getattr__ or __getattribute__ + cls_fn = cls.__getattr__ if hasattr(cls, '__getattr__') else cls.__getattribute__ + setattr(cls, cls_fn.__name__, _swizzle(cls_fn, sep)) + + # Handle the meta class + if meta: + meta_cls = type(cls) + if meta_cls == type: + class SwizzleType(meta_cls): pass + meta_cls = SwizzleType + cls = meta_cls(cls.__name__, cls.__bases__, dict(cls.__dict__)) + meta_fn = meta_cls.__getattr__ if hasattr(meta_cls, '__getattr__') else meta_cls.__getattribute__ + setattr(meta_cls, meta_fn.__name__, _swizzle(meta_fn, sep)) + return cls + + if cls is None: + return decorator + else: + return decorator(cls) + +class Swizzle(types.ModuleType): + def __init__(self): + types.ModuleType.__init__(self, __name__) + self.__dict__.update(sys.modules[__name__].__dict__) + + def __call__(self, cls=None, meta=False, sep = None): + return swizzle(cls, meta, sep) + +sys.modules[__name__] = Swizzle() \ No newline at end of file diff --git a/tests/attribute_swizzle_test.py b/tests/attribute_swizzle_test.py index d25d3fe..93d1945 100644 --- a/tests/attribute_swizzle_test.py +++ b/tests/attribute_swizzle_test.py @@ -1,128 +1,128 @@ -import unittest -import sys -import os -from dataclasses import dataclass -from enum import IntEnum -from typing import NamedTuple - -# Assuming swizzle is a decorator in your project -sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/..")) -import swizzle - -# Vector class definition -@swizzle -class Vector: - def __init__(self, x, y, z): - self.x = x - self.y = y - 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)) - - def test_invalid_swizzle(self): - with self.assertRaises(AttributeError): - _ = self.vector.nonexistent_attribute - -### 2. **XYZ Dataclass Swizzling Tests** - -@dataclass -@swizzle -class XYZ: - x: int - y: int - 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 - -### 3. **XYZ IntEnum Meta Swizzling Tests** - -@swizzle(meta=True) -class XYZEnumMeta(IntEnum): - X = 1 - Y = 2 - Z = 3 - -class TestXYZEnumMetaSwizzling(unittest.TestCase): - - def test_swizzle_meta(self): - self.assertEqual(XYZEnumMeta.YXZ, (XYZEnumMeta.Y, XYZEnumMeta.X, XYZEnumMeta.Z)) - -### 4. **XYZ NamedTuple Swizzling Tests** - -@swizzle -class XYZNamedTuple(NamedTuple): - x: int - y: int - 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 - -### 5. **Sequential Attribute Matching Tests** - -@swizzle(meta=True) -class Test: - x = 1 - y = 2 - z = 3 - xy = 4 - yz = 5 - xz = 6 - 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 - -if __name__ == "__main__": - unittest.main() +import unittest +import sys +import os +from dataclasses import dataclass +from enum import IntEnum +from typing import NamedTuple + +# Assuming swizzle is a decorator in your project +sys.path.append(os.path.realpath(os.path.dirname(__file__) + "/..")) +import swizzle + +# Vector class definition +@swizzle +class Vector: + def __init__(self, x, y, z): + self.x = x + self.y = y + 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)) + + def test_invalid_swizzle(self): + with self.assertRaises(AttributeError): + _ = self.vector.nonexistent_attribute + +### 2. **XYZ Dataclass Swizzling Tests** + +@dataclass +@swizzle +class XYZ: + x: int + y: int + 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 + +### 3. **XYZ IntEnum Meta Swizzling Tests** + +@swizzle(meta=True) +class XYZEnumMeta(IntEnum): + X = 1 + Y = 2 + Z = 3 + +class TestXYZEnumMetaSwizzling(unittest.TestCase): + + def test_swizzle_meta(self): + self.assertEqual(XYZEnumMeta.YXZ, (XYZEnumMeta.Y, XYZEnumMeta.X, XYZEnumMeta.Z)) + +### 4. **XYZ NamedTuple Swizzling Tests** + +@swizzle +class XYZNamedTuple(NamedTuple): + x: int + y: int + 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 + +### 5. **Sequential Attribute Matching Tests** + +@swizzle(meta=True) +class Test: + x = 1 + y = 2 + z = 3 + xy = 4 + yz = 5 + xz = 6 + 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 + +if __name__ == "__main__": + unittest.main()