tether-references 0.1.0__tar.gz

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,3 @@
1
+
2
+ Tether © 2026 by Jeff McAdams is licensed under CC BY-SA 4.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/
3
+
@@ -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,76 @@
1
+ # Tether
2
+ Tether is a small python library for managing bidirectionally related object references.
3
+
4
+ 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".
5
+
6
+ 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.
7
+
8
+ 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.
9
+
10
+ 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.
11
+
12
+ >>> from tether import create_relation
13
+ >>>
14
+ >>> class Car:
15
+ ... single='car'
16
+ ... plural='cars'
17
+ ...
18
+ >>> class Engine:
19
+ ... single='engine'
20
+ ... plural='engines'
21
+ ...
22
+ >>> rel_car_engine = create_relation(name='car_engine', a_type=Car, a_cardinality='one', b_type=Engine, b_cardinality='one')
23
+ >>> car1 = Car()
24
+ >>> engine1 = Engine()
25
+
26
+ >>> print(car1.engine)
27
+ None
28
+ >>> print(engine1.car)
29
+ None
30
+
31
+ >>> car1.engine = engine1
32
+
33
+ >>> print(car1.engine)
34
+ <__main__.Engine object at 0x7cc87a398980>
35
+ >>> print(engine1.car)
36
+ <__main__.Car object at 0x7cc87a398830>
37
+ >>>
38
+
39
+ Let's change the Engine to EVMotor, so it makes sense to have a cardinality of "many":
40
+
41
+ >>> from tether import create_relation
42
+ >>> class Car:
43
+ ... single='car'
44
+ ... plural='cars'
45
+ ...
46
+ >>> class EVMotor:
47
+ ... single='evmotor'
48
+ ... plural='evmotors'
49
+ ...
50
+ >>> rel_car_evmotor = create_relation(name='car_evmotor', a_type=Car, a_cardinality='one', b_type=EVMotor, b_cardinality='many')
51
+ >>>
52
+ >>> car1 = Car()
53
+ >>> evmotor1 = EVMotor()
54
+ >>> evmotor2 = EVMotor()
55
+ >>>
56
+ >>> print(car1.evmotors)
57
+ set()
58
+ >>> print(evmotor1.car)
59
+ None
60
+ >>> print(evmotor2.car)
61
+ None
62
+
63
+ >>> evmotor1.car = car1
64
+
65
+ >>> print(car1.evmotors)
66
+ {<__main__.EVMotor object at 0x7d60c059c980>}
67
+ >>> car1.evmotors.add(evmotor2)
68
+ >>> print(car1.evmotors)
69
+ {<__main__.EVMotor object at 0x7d60c059c980>, <__main__.EVMotor object at 0x7d60c06e6ad0>
70
+ >>> evmotor2.car
71
+ <__main__.Car object at 0x7d60c059c830>
72
+
73
+ 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.
74
+
75
+ ## Motivation
76
+ See [Motivation.md](Motivation.md)
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "tether-references"
3
+ version = "0.1.0"
4
+ description = "Tether creates bidirectionally entangled object references"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Jeff McAdams", email = "jeffmca@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = []
11
+ classifiers = [
12
+ "Development Status :: 2 - Pre-Alpha"
13
+ ]
14
+ license = "CC-BY-SA-4.0"
15
+ license-files = ["LICENSE"]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.22,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=9.0.2",
24
+ ]
25
+
26
+ [[tool.uv.index]]
27
+ name = "testpypi"
28
+ url = "https://test.pypi.org/simple/"
29
+ publish-url = "https://test.pypi.org/legacy/"
30
+ explicit = true
@@ -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