tether-references 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Cardinality = Literal['one', 'many']
|
|
7
|
+
#T = TypeVar('T')
|
|
8
|
+
#U = TypeVar('U')
|
|
9
|
+
#V = TypeVar('V')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_relation(self, *, relreg: RelationRegistry):
|
|
13
|
+
# naive implementation that searches through the relmgr's set of relation
|
|
14
|
+
# tuples until we find self._me. Return the related item
|
|
15
|
+
for rel in relreg.items:
|
|
16
|
+
if rel[0] == self:
|
|
17
|
+
return rel[1]
|
|
18
|
+
elif rel[1] == self:
|
|
19
|
+
return rel[0]
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def _set_relation(self, value: Any, *, relreg: RelationRegistry):
|
|
23
|
+
for rel in set(relreg.items):
|
|
24
|
+
if rel[0] == self:
|
|
25
|
+
relreg.items.remove(rel)
|
|
26
|
+
if rel[1] == self:
|
|
27
|
+
relreg.items.remove(rel)
|
|
28
|
+
me_type = type(self)
|
|
29
|
+
if relreg.a_type == me_type:
|
|
30
|
+
relreg.items.add((self, value))
|
|
31
|
+
elif relreg.b_type == me_type:
|
|
32
|
+
relreg.items.add((value, self))
|
|
33
|
+
|
|
34
|
+
def _del_relation(self, *, relreg: RelationRegistry):
|
|
35
|
+
for rel in set(relreg.items):
|
|
36
|
+
if rel[0] == self:
|
|
37
|
+
relreg.items.remove(rel)
|
|
38
|
+
elif rel[1] == self:
|
|
39
|
+
relreg.items.remove(rel)
|
|
40
|
+
|
|
41
|
+
def _get_relationset(self, *, relreg: RelationRegistry):
|
|
42
|
+
relset = RelationSet(relreg=relreg, srcitem=self)
|
|
43
|
+
return relset
|
|
44
|
+
|
|
45
|
+
class RelationSet:
|
|
46
|
+
def __init__(self, relreg: RelationRegistry, srcitem: Any) -> None:
|
|
47
|
+
self._srcitem = srcitem
|
|
48
|
+
self._relmgr = relreg
|
|
49
|
+
self._iternum = 0
|
|
50
|
+
|
|
51
|
+
def __and__(self):
|
|
52
|
+
raise NotImplementedError()
|
|
53
|
+
|
|
54
|
+
def __contains__(self, other_item: Any) -> bool:
|
|
55
|
+
if (self._srcitem, other_item) in self._relmgr.items:
|
|
56
|
+
return True
|
|
57
|
+
elif (other_item, self._srcitem) in self._relmgr.items:
|
|
58
|
+
return True
|
|
59
|
+
else:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def __eq__(self, other) -> bool:
|
|
63
|
+
for x in self:
|
|
64
|
+
if x not in other:
|
|
65
|
+
return False
|
|
66
|
+
for x in other:
|
|
67
|
+
if x not in self:
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def __iter__(self) -> RelationSet:
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __next__(self):
|
|
75
|
+
cur = 0
|
|
76
|
+
for iteritem in self._relmgr.items:
|
|
77
|
+
if cur < self._iternum:
|
|
78
|
+
cur += 1
|
|
79
|
+
continue
|
|
80
|
+
if self._srcitem is iteritem[0]:
|
|
81
|
+
self._iternum += 1
|
|
82
|
+
cur += 1
|
|
83
|
+
return iteritem[1]
|
|
84
|
+
elif self._srcitem is iteritem[1]:
|
|
85
|
+
self._iternum += 1
|
|
86
|
+
cur += 1
|
|
87
|
+
return iteritem[0]
|
|
88
|
+
raise StopIteration
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def __len__(self) -> int:
|
|
92
|
+
count = 0
|
|
93
|
+
for iteritem in self._relmgr.items:
|
|
94
|
+
if self._srcitem is iteritem[0]:
|
|
95
|
+
count += 1
|
|
96
|
+
elif self._srcitem is iteritem[1]:
|
|
97
|
+
count += 1
|
|
98
|
+
return count
|
|
99
|
+
|
|
100
|
+
def __repr__(self):
|
|
101
|
+
x = (set(self))
|
|
102
|
+
return str(x)
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
return self.__repr__()
|
|
106
|
+
|
|
107
|
+
def add(self, value):
|
|
108
|
+
if type(self._srcitem) == self._relmgr.a_type:
|
|
109
|
+
if type(value) != self._relmgr.b_type:
|
|
110
|
+
raise TypeError()
|
|
111
|
+
self._relmgr.items.add((self._srcitem, value))
|
|
112
|
+
elif type(self._srcitem) == self._relmgr.b_type:
|
|
113
|
+
if type(value) != self._relmgr.a_type:
|
|
114
|
+
raise TypeError()
|
|
115
|
+
self._relmgr.items.add((value, self._srcitem))
|
|
116
|
+
|
|
117
|
+
def clear(self):
|
|
118
|
+
raise NotImplementedError()
|
|
119
|
+
|
|
120
|
+
def copy(self):
|
|
121
|
+
raise NotImplementedError()
|
|
122
|
+
|
|
123
|
+
def difference(self):
|
|
124
|
+
raise NotImplementedError()
|
|
125
|
+
|
|
126
|
+
def difference_update(self):
|
|
127
|
+
raise NotImplementedError()
|
|
128
|
+
|
|
129
|
+
def discard(self, value):
|
|
130
|
+
if type(self._srcitem) == self._relmgr.a_type:
|
|
131
|
+
self._relmgr.items.discard((self._srcitem, value))
|
|
132
|
+
elif type(self._srcitem) == self._relmgr.b_type:
|
|
133
|
+
self._relmgr.items.discard((value, self._srcitem))
|
|
134
|
+
|
|
135
|
+
def intersection(self):
|
|
136
|
+
raise NotImplementedError()
|
|
137
|
+
|
|
138
|
+
def intersection_update(self):
|
|
139
|
+
raise NotImplementedError()
|
|
140
|
+
|
|
141
|
+
def isdisjoint(self):
|
|
142
|
+
raise NotImplementedError()
|
|
143
|
+
|
|
144
|
+
def issubset(self):
|
|
145
|
+
raise NotImplementedError()
|
|
146
|
+
|
|
147
|
+
def issuperset(self):
|
|
148
|
+
raise NotImplementedError()
|
|
149
|
+
|
|
150
|
+
def pop(self, index: int = None):
|
|
151
|
+
raise NotImplementedError()
|
|
152
|
+
|
|
153
|
+
def remove(self, value):
|
|
154
|
+
if type(self._srcitem) == self._relmgr.a_type:
|
|
155
|
+
if (self._srcitem, value) not in self._relmgr.items:
|
|
156
|
+
raise KeyError(value)
|
|
157
|
+
elif type(self._srcitem) == self._relmgr.b_type:
|
|
158
|
+
if (value, self._srcitem) not in self._relmgr.items:
|
|
159
|
+
raise KeyError(value)
|
|
160
|
+
|
|
161
|
+
if type(self._srcitem) == self._relmgr.a_type:
|
|
162
|
+
self._relmgr.items.discard((self._srcitem, value))
|
|
163
|
+
elif type(self._srcitem) == self._relmgr.b_type:
|
|
164
|
+
self._relmgr.items.discard((value, self._srcitem))
|
|
165
|
+
|
|
166
|
+
def symmetric_difference(self):
|
|
167
|
+
raise NotImplementedError()
|
|
168
|
+
|
|
169
|
+
def symmetric_difference_update(self):
|
|
170
|
+
raise NotImplementedError()
|
|
171
|
+
|
|
172
|
+
def union(self):
|
|
173
|
+
raise NotImplementedError()
|
|
174
|
+
|
|
175
|
+
def update(self):
|
|
176
|
+
raise NotImplementedError()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class RelationRegistry:
|
|
180
|
+
def __init__(self,
|
|
181
|
+
name: str,
|
|
182
|
+
a_type: Any,
|
|
183
|
+
a_cardinality: Cardinality,
|
|
184
|
+
b_type: Any,
|
|
185
|
+
b_cardinality: Cardinality,
|
|
186
|
+
dependancy: bool = False
|
|
187
|
+
) -> None:
|
|
188
|
+
self.name: str = name
|
|
189
|
+
self.items: set[tuple[Any, Any]] = set()
|
|
190
|
+
self.a_type: Any = a_type
|
|
191
|
+
self.a_cardinality: Cardinality = a_cardinality
|
|
192
|
+
self.b_type: Any = b_type
|
|
193
|
+
self.b_cardinality: Cardinality = b_cardinality
|
|
194
|
+
self.dependancy: bool = dependancy
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def create_relation(name: str,
|
|
198
|
+
a_type: Any,
|
|
199
|
+
a_cardinality: Cardinality,
|
|
200
|
+
b_type: Any,
|
|
201
|
+
b_cardinality: Cardinality,
|
|
202
|
+
a_attrname: str = '',
|
|
203
|
+
b_attrname: str = '',
|
|
204
|
+
dependancy: bool = False
|
|
205
|
+
) -> RelationRegistry:
|
|
206
|
+
relreg = RelationRegistry(name=name,
|
|
207
|
+
a_type=a_type,
|
|
208
|
+
a_cardinality=a_cardinality,
|
|
209
|
+
b_type=b_type,
|
|
210
|
+
b_cardinality=b_cardinality,
|
|
211
|
+
dependancy=dependancy
|
|
212
|
+
)
|
|
213
|
+
if a_cardinality == 'one':
|
|
214
|
+
get_part = partial(_get_relation, relreg=relreg)
|
|
215
|
+
set_part = partial(_set_relation, relreg=relreg)
|
|
216
|
+
del_part = partial(_del_relation, relreg=relreg)
|
|
217
|
+
prop = property(get_part, set_part, del_part)
|
|
218
|
+
if a_attrname == '':
|
|
219
|
+
setattr(b_type, a_type.single, prop)
|
|
220
|
+
else:
|
|
221
|
+
setattr(b_type, a_attrname, prop)
|
|
222
|
+
elif a_cardinality == 'many':
|
|
223
|
+
get_part = partial(_get_relationset, relreg=relreg)
|
|
224
|
+
prop = property(get_part)
|
|
225
|
+
if a_attrname == '':
|
|
226
|
+
setattr(b_type, a_type.plural, prop)
|
|
227
|
+
else:
|
|
228
|
+
setattr(b_type, a_attrname, prop)
|
|
229
|
+
if b_cardinality == 'one':
|
|
230
|
+
get_part = partial(_get_relation, relreg=relreg)
|
|
231
|
+
set_part = partial(_set_relation, relreg=relreg)
|
|
232
|
+
del_part = partial(_del_relation, relreg=relreg)
|
|
233
|
+
prop = property(get_part, set_part, del_part)
|
|
234
|
+
if b_attrname == '':
|
|
235
|
+
setattr(a_type, b_type.single, prop)
|
|
236
|
+
else:
|
|
237
|
+
setattr(a_type, b_attrname, prop)
|
|
238
|
+
elif b_cardinality == 'many':
|
|
239
|
+
get_part = partial(_get_relationset, relreg=relreg)
|
|
240
|
+
prop = property(get_part)
|
|
241
|
+
if b_attrname == '':
|
|
242
|
+
setattr(a_type, b_type.plural, prop)
|
|
243
|
+
else:
|
|
244
|
+
setattr(a_type, b_attrname, prop)
|
|
245
|
+
|
|
246
|
+
return relreg
|
|
247
|
+
|
|
248
|
+
#def get_relations(self, registry: RelationRegistry) -> Iterable:
|
|
249
|
+
# if type(self) == registry.a_type:
|
|
250
|
+
# key: str = 'a'
|
|
251
|
+
# value: str = 'b'
|
|
252
|
+
# cardinality: Cardinality = registry.a_cardinality
|
|
253
|
+
# else:
|
|
254
|
+
# key: str = 'b'
|
|
255
|
+
# value: str = 'a'
|
|
256
|
+
# cardinality: Cardinality = registry.b_cardinality
|
|
257
|
+
# for item in registry.items:
|
|
258
|
+
# if getattr(item, key) == self:
|
|
259
|
+
# if cardinality == 'one':
|
|
260
|
+
# return getattr(item, value)
|
|
261
|
+
# elif cardinality == 'many':
|
|
262
|
+
# yield getattr(item, value)
|
|
263
|
+
# if cardinality == 'one':
|
|
264
|
+
# return None
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tether-references
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tether creates bidirectionally entangled object references
|
|
5
|
+
Author: Jeff McAdams
|
|
6
|
+
Author-email: Jeff McAdams <jeffmca@gmail.com>
|
|
7
|
+
License-Expression: CC-BY-SA-4.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Tether
|
|
14
|
+
Tether is a small python library for managing bidirectionally related object references.
|
|
15
|
+
|
|
16
|
+
Thanks to Jeff Kala for coming up with the name, Tether, for this project. I suck at naming things and after a brief description, Jeff came up with the name, and I liked it, so this is now known as Tether. Jeff wins a mention here in the README for his naming cleverness. I'm just glad it came from another "Jeff".
|
|
17
|
+
|
|
18
|
+
By example, if you have two classes, class A and class B, when you call tether's create_relation() with appropriate arguments, class A and class B will have attributes (actually properties) injected to reference one or more objects of the other type.
|
|
19
|
+
|
|
20
|
+
The cardinality of the references can be either "one" or "many". If the cardinality of the reference is "many", the resulting attribute will be represented by a set-like object.
|
|
21
|
+
|
|
22
|
+
More concretely, using one of the classic examples for OOP, if you have a class Car, and a class Engine, assuming a one-to-one relation, when you create_relation() specifying Car and Engine, class Car will gain an attribute engine that will return the associated Engine object, and class Engine will gain an attribute car that will return the associated Car object.
|
|
23
|
+
|
|
24
|
+
>>> from tether import create_relation
|
|
25
|
+
>>>
|
|
26
|
+
>>> class Car:
|
|
27
|
+
... single='car'
|
|
28
|
+
... plural='cars'
|
|
29
|
+
...
|
|
30
|
+
>>> class Engine:
|
|
31
|
+
... single='engine'
|
|
32
|
+
... plural='engines'
|
|
33
|
+
...
|
|
34
|
+
>>> rel_car_engine = create_relation(name='car_engine', a_type=Car, a_cardinality='one', b_type=Engine, b_cardinality='one')
|
|
35
|
+
>>> car1 = Car()
|
|
36
|
+
>>> engine1 = Engine()
|
|
37
|
+
|
|
38
|
+
>>> print(car1.engine)
|
|
39
|
+
None
|
|
40
|
+
>>> print(engine1.car)
|
|
41
|
+
None
|
|
42
|
+
|
|
43
|
+
>>> car1.engine = engine1
|
|
44
|
+
|
|
45
|
+
>>> print(car1.engine)
|
|
46
|
+
<__main__.Engine object at 0x7cc87a398980>
|
|
47
|
+
>>> print(engine1.car)
|
|
48
|
+
<__main__.Car object at 0x7cc87a398830>
|
|
49
|
+
>>>
|
|
50
|
+
|
|
51
|
+
Let's change the Engine to EVMotor, so it makes sense to have a cardinality of "many":
|
|
52
|
+
|
|
53
|
+
>>> from tether import create_relation
|
|
54
|
+
>>> class Car:
|
|
55
|
+
... single='car'
|
|
56
|
+
... plural='cars'
|
|
57
|
+
...
|
|
58
|
+
>>> class EVMotor:
|
|
59
|
+
... single='evmotor'
|
|
60
|
+
... plural='evmotors'
|
|
61
|
+
...
|
|
62
|
+
>>> rel_car_evmotor = create_relation(name='car_evmotor', a_type=Car, a_cardinality='one', b_type=EVMotor, b_cardinality='many')
|
|
63
|
+
>>>
|
|
64
|
+
>>> car1 = Car()
|
|
65
|
+
>>> evmotor1 = EVMotor()
|
|
66
|
+
>>> evmotor2 = EVMotor()
|
|
67
|
+
>>>
|
|
68
|
+
>>> print(car1.evmotors)
|
|
69
|
+
set()
|
|
70
|
+
>>> print(evmotor1.car)
|
|
71
|
+
None
|
|
72
|
+
>>> print(evmotor2.car)
|
|
73
|
+
None
|
|
74
|
+
|
|
75
|
+
>>> evmotor1.car = car1
|
|
76
|
+
|
|
77
|
+
>>> print(car1.evmotors)
|
|
78
|
+
{<__main__.EVMotor object at 0x7d60c059c980>}
|
|
79
|
+
>>> car1.evmotors.add(evmotor2)
|
|
80
|
+
>>> print(car1.evmotors)
|
|
81
|
+
{<__main__.EVMotor object at 0x7d60c059c980>, <__main__.EVMotor object at 0x7d60c06e6ad0>
|
|
82
|
+
>>> evmotor2.car
|
|
83
|
+
<__main__.Car object at 0x7d60c059c830>
|
|
84
|
+
|
|
85
|
+
You can see that the "many" cardinality relation behaviors (mostly) as a set (at the time of this writing, not all set methods are implemented), but the bidirectional relationship is maintained.
|
|
86
|
+
|
|
87
|
+
## Motivation
|
|
88
|
+
See [Motivation.md](Motivation.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
tether_references/__init__.py,sha256=DEjFIynRkODPWWEikamlxwjNulDWro9KbKYqjPLzDCM,8400
|
|
2
|
+
tether_references/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tether_references-0.1.0.dist-info/licenses/LICENSE,sha256=OfcVFXi9jbGas5QNN9OAuAcIVMew5JGBIK3-FPSBp0Y,159
|
|
4
|
+
tether_references-0.1.0.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
|
|
5
|
+
tether_references-0.1.0.dist-info/METADATA,sha256=OYMlNy2B4Jro0bNZZCC9g5ttCExZ-1kvyKZSl8bEXQk,3323
|
|
6
|
+
tether_references-0.1.0.dist-info/RECORD,,
|