Skip to content

Commit 3557688

Browse files
committed
Version 0.2.0
1 parent af1c374 commit 3557688

12 files changed

Lines changed: 261 additions & 203 deletions

.gitignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,4 @@
33
.__cache__
44
.env
55

6-
/.vscode/
7-
/.venv/
8-
9-
requirements.txt
6+
/.venv/

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"python.testing.pytestArgs": [
3+
"test"
4+
],
5+
"python.testing.unittestEnabled": false,
6+
"python.testing.pytestEnabled": true
7+
}

classmods/__init__.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
# __version__
2-
from .__version__ import __version__ as version
3-
4-
from ._remote_attrib import (
5-
RemoteAttribMixin,
6-
RemoteAttrib,
7-
RemoteAttribType,
8-
)
9-
from ._method_monitor import MethodMonitor
10-
from ._constant_attrib import ConstantAttrib
1+
from .__version__ import get_version
2+
from ._descriptors import ConstantAttrib, RemoteAttrib
113
from ._decorators import logwrap, suppress_errors
4+
from ._method_monitor import MethodMonitor
5+
126

137
__all__ = [
14-
'version',
15-
'RemoteAttribMixin',
16-
'RemoteAttrib',
17-
'RemoteAttribType',
8+
'get_version',
189
'ConstantAttrib',
19-
'MethodMonitor',
10+
'RemoteAttrib',
2011
'logwrap',
2112
'suppress_errors',
13+
'MethodMonitor',
2214
]

classmods/__version__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
__version__ = '0.1.3'
2-
__letter__ = 'i'
1+
__version__ = '0.2.0'
2+
33

44
def get_version() -> str:
5-
return f"{__letter__}'{__version__}'"
5+
return __version__
6+
67

78
__all__ = [
89
'__all__',
9-
'__letter__',
1010
]

classmods/_constant_attrib.py

Lines changed: 0 additions & 30 deletions
This file was deleted.

classmods/_descriptors.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import time
2+
from typing import Any, Dict, Optional, Callable, Self, Tuple, Type, TypeVar, Generic
3+
4+
T = TypeVar('T')
5+
6+
class ConstantAttrib(Generic[T]):
7+
"""
8+
A descriptor that enforces constant values at instance level.
9+
(Does not support class-level assignment)
10+
11+
Example:
12+
>>> class MyClass:
13+
... VALUE = ConstantAttrib[int]()
14+
...
15+
>>> obj = MyClass()
16+
>>> obj.VALUE = 42 # First assignment works
17+
>>> obj.VALUE = 10 # Raises AttributeError
18+
>>> print(obj.VALUE) # 42
19+
"""
20+
21+
def __set_name__(self, owner: Type[Any], name: str) -> None:
22+
self.name = name
23+
self.private_name = f"_{name}_constant"
24+
25+
def __get__(self, instance: Any, owner: Type[Any]) -> T:
26+
if instance is None:
27+
return self # type: ignore
28+
29+
if self.private_name not in instance.__dict__:
30+
raise AttributeError(f"Constant attribute '{self.name}' not set")
31+
32+
return instance.__dict__[self.private_name]
33+
34+
def __set__(self, instance: Any, value: T) -> None:
35+
if self.private_name in instance.__dict__:
36+
raise AttributeError(f"Cannot modify constant attribute '{self.name}'")
37+
instance.__dict__[self.private_name] = value
38+
39+
def __delete__(self, instance: Any) -> None:
40+
raise AttributeError(f"Cannot delete constant attribute '{self.name}'")
41+
42+
class RemoteAttrib(Generic[T]):
43+
"""
44+
Descriptor that acts as a remote attribute.
45+
It allows calling a method on the object to `get`, `set`, `delete`.
46+
You can modify mapped value on remote side with ease.
47+
48+
Example:
49+
>>> import requests
50+
>>>
51+
>>> class RemoteUser:
52+
... def __init__(self, user_id: int):
53+
... self.user_id = user_id
54+
>>>
55+
... def _get_name(self):
56+
... print("Fetching from API...")
57+
... return requests.get(f"https://api.example.com/user/{self.user_id}/name").json()["name"]
58+
>>>
59+
... def _set_name(self, value):
60+
... print("Sending update to API...")
61+
... requests.post(
62+
... f"https://api.example.com/user/{self.user_id}/name",
63+
... json={"name": value}
64+
... )
65+
>>>
66+
... name = RemoteAttrib[str]( # Specify true type if using type hints.
67+
... get=_get_name,
68+
... set=_set_name,
69+
... cache_timeout=10
70+
... )
71+
... user = RemoteUser(user_id=42)
72+
>>>
73+
... print(user.name) # Calls API, caches result
74+
... print(user.name) # Uses cache
75+
... time.sleep(11)
76+
... print(user.name) # Refreshes from API
77+
>>>
78+
... user.name = "Alice" # Sends update to API
79+
"""
80+
def __init__(
81+
self,
82+
get: Optional[Callable[..., Any]] = None,
83+
set: Optional[Callable[..., None]] = None,
84+
delete: Optional[Callable[..., None]] = None,
85+
cache_timeout: int | float = 0,
86+
*,
87+
get_args: Optional[Tuple[Any]] = None,
88+
get_kwargs: Optional[Dict[str, Any]] = None,
89+
set_args: Optional[Tuple[Any]] = None,
90+
set_kwargs: Optional[Dict[str, Any]] = None,
91+
delete_args: Optional[Tuple[Any]] = None,
92+
delete_kwargs: Optional[Dict[str, Any]] = None,
93+
) -> None:
94+
'''
95+
A mixin for remote attributes.
96+
97+
Args:
98+
get: A function that gets the attribute value. Defaults to None.
99+
set: A function that sets the attribute value. Defaults to None.
100+
delete: A function that deletes the attribute. Defaults to None.
101+
cache_timeout: The time in seconds to cache the attribute value. Defaults to 0.
102+
get_args: The arguments to pass to the get function. Defaults to None.
103+
get_kwargs: The keyword arguments to pass to the get function. Defaults to None.
104+
set_args: The arguments to pass to the set function. Defaults to None.
105+
set_kwargs: The keyword arguments to pass to the set function. Defaults to None.
106+
delete_args: The arguments to pass to the delete function. Defaults to None.
107+
delete_kwargs: The keyword arguments to pass to the delete function. Defaults to None.
108+
'''
109+
self._get = get
110+
self._set = set
111+
self._del = delete
112+
self._get_args = get_args or ()
113+
self._set_args = set_args or ()
114+
self._del_args = delete_args or ()
115+
self._set_kwargs = set_kwargs or {}
116+
self._get_kwargs = get_kwargs or {}
117+
self._del_kwargs = delete_kwargs or {}
118+
self._cache_timeout = cache_timeout
119+
self.name: str = '' # python will fill this
120+
121+
def __ensure_cache__(self, instance: Any) -> None:
122+
if not hasattr(instance, '_remote_attrib_cache'):
123+
instance._remote_attrib_cache = {}
124+
125+
def __set_name__(self, owner: Type[Any], name: str) -> None:
126+
self.name = name
127+
128+
def __get__(self, instance: Optional[Any], owner: Optional[Type] = None) -> Self | T:
129+
if instance is None:
130+
return self
131+
132+
self.__ensure_cache__(instance)
133+
cache_entry = instance._remote_attrib_cache.get(self.name)
134+
if cache_entry and (time.time() - cache_entry[1] <= self._cache_timeout):
135+
return cache_entry[0]
136+
137+
if self._get is None:
138+
raise AttributeError(f'No getter for attribute {self.name}.')
139+
140+
value = self._get(
141+
instance,
142+
*self._get_args,
143+
**self._get_kwargs,
144+
)
145+
146+
if self._cache_timeout > 0:
147+
instance._remote_attrib_cache[self.name] = (value, time.time())
148+
149+
return value
150+
151+
def __set__(self, instance: Any, value: Any) -> None:
152+
if self._set is None:
153+
raise AttributeError(f'No setter for attribute {self.name}.')
154+
155+
self.__ensure_cache__(instance)
156+
self._set(
157+
instance,
158+
value,
159+
*self._set_args,
160+
**self._set_kwargs,
161+
)
162+
instance._remote_attrib_cache.pop(self.name, None)
163+
164+
def __delete__(self, instance: Any) -> None:
165+
if self._del is None:
166+
raise AttributeError(
167+
f'No deleter for attribute {self.name}.')
168+
169+
self.__ensure_cache__(instance)
170+
self._del(
171+
instance,
172+
*self._del_args,
173+
**self._del_kwargs,
174+
)
175+
instance._remote_attrib_cache.pop(self.name, None)
176+

classmods/_remote_attrib.py

Lines changed: 0 additions & 120 deletions
This file was deleted.

0 commit comments

Comments
 (0)