Skip to content

Commit c5deb70

Browse files
authored
Merge pull request #33 from data-science-extensions/updates
Add new `@class_property` decorator
2 parents e7b202b + 95b0500 commit c5deb70

4 files changed

Lines changed: 351 additions & 22 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ uv.lock
33
poetry.lock
44
Pipfile
55
Pipfile.lock
6+
.vscode/
67

78
# Byte-compiled / optimized / DLL files
89
__pycache__/

Makefile

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ poetry-install-all:
5757

5858
#* UV
5959
.PHONY: uv
60+
uv-shell:
61+
bash -c "source .venv/bin/activate && exec bash"
6062
install-uv:
6163
curl -LsSf https://astral.sh/uv/install.sh | sh
6264
uv --version
@@ -95,41 +97,41 @@ install-all: uv-install-all
9597
#* Linting
9698
.PHONY: linting
9799
run-black:
98-
uv run black --config pyproject.toml ./
100+
uv run --link-mode=copy black --config pyproject.toml ./
99101
run-isort:
100-
uv run isort --settings-file pyproject.toml ./
102+
uv run --link-mode=copy isort --settings-file pyproject.toml ./
101103
lint: run-black run-isort
102104

103105

104106
#* Checking
105107
.PHONY: checking
106108
check-black:
107-
uv run black --diff --check --config pyproject.toml ./
109+
uv run --link-mode=copy black --diff --check --config pyproject.toml ./
108110
check-mypy:
109-
uv run mypy --install-types --config-file pyproject.toml src/$(PACKAGE_NAME)
111+
uv run --link-mode=copy mypy --install-types --config-file pyproject.toml src/$(PACKAGE_NAME)
110112
check-isort:
111-
uv run isort --settings-file pyproject.toml ./
113+
uv run --link-mode=copy isort --settings-file pyproject.toml ./
112114
check-codespell:
113-
uv run codespell --toml pyproject.toml src/ *.py
115+
uv run --link-mode=copy codespell --toml pyproject.toml src/ *.py
114116
check-pylint:
115-
uv run pylint --rcfile=pyproject.toml src/$(PACKAGE_NAME)
117+
uv run --link-mode=copy pylint --rcfile=pyproject.toml src/$(PACKAGE_NAME)
116118
check-pytest:
117-
uv run pytest --config-file pyproject.toml
119+
uv run --link-mode=copy pytest --config-file pyproject.toml
118120
check-pycln:
119-
uv run pycln --config="pyproject.toml" src/$(PACKAGE_NAME)
121+
uv run --link-mode=copy pycln --config="pyproject.toml" src/$(PACKAGE_NAME)
120122
check-build:
121123
uv build --out-dir=dist
122124
if [ -d "dist" ]; then rm --recursive dist; fi
123125
check-mkdocs:
124-
uv run mkdocs build --site-dir="temp"
126+
uv run --link-mode=copy mkdocs build --site-dir="temp"
125127
if [ -d "temp" ]; then rm --recursive temp; fi
126128
check: check-black check-mypy check-pycln check-isort check-codespell check-pylint check-mkdocs check-build check-pytest
127129

128130

129131
#* Testing
130132
.PHONY: pytest
131133
pytest:
132-
uv run pytest --config-file pyproject.toml
134+
uv run --link-mode=copy pytest --config-file pyproject.toml
133135
copy-coverage-report:
134136
cp --recursive --update "./cov-report/html/." "./docs/code/coverage/"
135137
commit-coverage-report:
@@ -183,25 +185,25 @@ deploy-package: uv-publish
183185
#* Docs
184186
.PHONY: docs
185187
docs-serve-static:
186-
uv run mkdocs serve
188+
uv run --link-mode=copy mkdocs serve
187189
docs-serve-versioned:
188-
uv run mike serve --branch=docs-site
190+
uv run --link-mode=copy mike serve --branch=docs-site
189191
docs-build-static:
190-
uv run mkdocs build --clean
192+
uv run --link-mode=copy mkdocs build --clean
191193
docs-build-versioned:
192194
git config --global --list
193195
git config --local --list
194196
git remote -v
195-
uv run mike --debug deploy --update-aliases --branch=docs-site --push $(VERSION) latest
197+
uv run --link-mode=copy mike --debug deploy --update-aliases --branch=docs-site --push $(VERSION) latest
196198
update-git-docs:
197199
git add .
198200
git commit -m "Build docs [skip ci]"
199201
git push --force --no-verify --push-option ci.skip
200202
docs-check-versions:
201-
uv run mike --debug list --branch=docs-site
203+
uv run --link-mode=copy mike --debug list --branch=docs-site
202204
docs-delete-version:
203-
uv run mike --debug delete --branch=docs-site $(VERSION)
205+
uv run --link-mode=copy mike --debug delete --branch=docs-site $(VERSION)
204206
docs-set-default:
205-
uv run mike --debug set-default --branch=docs-site --push latest
207+
uv run --link-mode=copy mike --debug set-default --branch=docs-site --push latest
206208
build-static-docs: docs-build-static update-git-docs
207209
build-versioned-docs: docs-build-versioned docs-set-default

src/tests/test_classes.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@
1414
from random import Random
1515
from unittest import TestCase
1616

17+
# ## Python Third Party Imports ----
18+
from pytest import raises
19+
1720
# ## Local First Party Imports ----
18-
from toolbox_python.classes import get_full_class_name
21+
from toolbox_python.classes import ( # cached_class_property,
22+
class_property,
23+
get_full_class_name,
24+
)
1925
from toolbox_python.defaults import Defaults
2026

2127

2228
# ---------------------------------------------------------------------------- #
2329
# #
24-
# Test Suite ####
30+
# get_full_class_name() ####
2531
# #
2632
# ---------------------------------------------------------------------------- #
2733

@@ -48,3 +54,138 @@ def test_get_full_class_name_3(self) -> None:
4854
_output: str = get_full_class_name(_input)
4955
_expected = "random.Random"
5056
assert _output == _expected
57+
58+
59+
# ---------------------------------------------------------------------------- #
60+
# #
61+
# class_property() ####
62+
# #
63+
# ---------------------------------------------------------------------------- #
64+
65+
66+
class MyClass:
67+
_class_value: str = "original"
68+
69+
def __init__(self, instance_value: str = "instance") -> None:
70+
self._instance_value: str = instance_value
71+
72+
@class_property
73+
def class_value(cls) -> str:
74+
return cls._class_value
75+
76+
# @class_value.getter
77+
# def class_value_getter(cls) -> str:
78+
# return cls._class_value
79+
80+
@class_property
81+
def class_value_modified(cls) -> str:
82+
return cls._class_value + " modified"
83+
84+
@class_property()
85+
def class_value_with_parens(cls) -> str:
86+
return cls._class_value + " with parens"
87+
88+
@class_property(doc="This is a class property with a docstring.")
89+
def class_value_with_doc(cls) -> str:
90+
"""This is a class property with a docstring."""
91+
return cls._class_value + " with doc"
92+
93+
@class_property
94+
@classmethod
95+
def class_value_method(cls) -> str:
96+
"""This is a class method."""
97+
return cls._class_value + " with method"
98+
99+
@property
100+
def instance_value(self) -> str:
101+
return self._instance_value
102+
103+
104+
class TestClassProperty(TestCase):
105+
106+
def setUp(self) -> None:
107+
MyClass._class_value = "original"
108+
109+
def test_class_property_class_property(self) -> None:
110+
assert MyClass.class_value == "original"
111+
112+
def test_class_property_class_property_modified(self) -> None:
113+
MyClass._class_value = "modified"
114+
assert MyClass.class_value == "modified"
115+
116+
def test_class_property_instance_property(self) -> None:
117+
assert MyClass().instance_value == "instance"
118+
119+
def test_class_property_properties(self) -> None:
120+
new_class = MyClass()
121+
assert new_class.class_value == "original"
122+
assert new_class.instance_value == "instance"
123+
124+
# def test_class_property_getter(self) -> None:
125+
# assert MyClass.class_value_getter == MyClass.class_value
126+
127+
def test_class_property_modified_properties(self) -> None:
128+
129+
# Test the instance property for an instance of MyClass
130+
new_class = MyClass("instance value")
131+
assert new_class.instance_value == "instance value"
132+
133+
# Test the modified value of an instance property for the instantiated class
134+
new_class._instance_value = "new instance value"
135+
assert new_class.instance_value == "new instance value"
136+
137+
# Test the class property for an instance of MyClass
138+
# This should return the original class value, not the modified one
139+
new_class._class_value = "new class value"
140+
assert new_class.class_value == "original"
141+
142+
def test_class_property_with_parens(self) -> None:
143+
assert MyClass.class_value_with_parens == "original with parens"
144+
MyClass._class_value = "modified"
145+
assert MyClass.class_value_with_parens == "modified with parens"
146+
147+
def test_class_property_with_doc(self) -> None:
148+
assert MyClass.class_value_with_doc == "original with doc"
149+
MyClass.class_value_with_doc.__doc__ == (
150+
"This is a class property with a docstring."
151+
)
152+
153+
def test_class_property_errors(self) -> None:
154+
with raises(AttributeError):
155+
MyClass().missing_property
156+
with raises(AttributeError):
157+
MyClass.missing_property
158+
with raises(AttributeError):
159+
MyClass().missing_class_property
160+
with raises(AttributeError):
161+
MyClass.missing_class_property
162+
163+
def test_class_property_setter(self) -> None:
164+
165+
with raises(NotImplementedError):
166+
167+
class MyClassWithSetter:
168+
_class_value: str = "original"
169+
170+
@class_property
171+
def class_value(cls) -> str:
172+
return cls._class_value
173+
174+
@class_value.setter
175+
def class_value(cls, value: str) -> None:
176+
cls._class_value = value
177+
178+
def test_class_property_deleter(self) -> None:
179+
180+
with raises(NotImplementedError):
181+
182+
class MyClassWithDeleter:
183+
_class_value: str = "original"
184+
185+
@class_property
186+
def class_value(cls) -> str:
187+
return cls._class_value
188+
189+
@class_value.deleter
190+
def class_value(cls) -> None:
191+
del cls._class_value

0 commit comments

Comments
 (0)