ovld 0.3.9__py3-none-any.whl → 0.4.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.
ovld/dependent.py ADDED
@@ -0,0 +1,275 @@
1
+ import inspect
2
+ import re
3
+ from dataclasses import dataclass
4
+ from itertools import count
5
+ from typing import TYPE_CHECKING, Any, Mapping, TypeVar, Union
6
+
7
+ from .types import (
8
+ Intersection,
9
+ Order,
10
+ TypeRelationship,
11
+ normalize_type,
12
+ subclasscheck,
13
+ typeorder,
14
+ )
15
+
16
+ _current = count()
17
+
18
+
19
+ @dataclass
20
+ class CodeGen:
21
+ template: str
22
+ substitutions: dict
23
+
24
+ def mangle(self):
25
+ renamings = {
26
+ k: f"{{{k}__{next(_current)}}}" for k in self.substitutions
27
+ }
28
+ renamings["arg"] = "{arg}"
29
+ new_subs = {
30
+ newk[1:-1]: self.substitutions[k]
31
+ for k, newk in renamings.items()
32
+ if k in self.substitutions
33
+ }
34
+ return CodeGen(
35
+ template=self.template.format(**renamings),
36
+ substitutions=new_subs,
37
+ )
38
+
39
+
40
+ def combine(master_template, args):
41
+ fmts = []
42
+ subs = {}
43
+ for cg in args:
44
+ mangled = cg.mangle()
45
+ fmts.append(mangled.template)
46
+ subs.update(mangled.substitutions)
47
+ return CodeGen(master_template.format(*fmts), subs)
48
+
49
+
50
+ class DependentType:
51
+ exclusive_type = False
52
+ keyable_type = False
53
+
54
+ def __init__(self, bound):
55
+ self.bound = bound
56
+
57
+ def __class_getitem__(cls, item):
58
+ items = (item,) if not isinstance(item, tuple) else item
59
+ return cls(*items)
60
+
61
+ def with_bound(self, new_bound): # pragma: no cover
62
+ return type(self)(new_bound)
63
+
64
+ def check(self, value): # pragma: no cover
65
+ raise NotImplementedError()
66
+
67
+ def codegen(self):
68
+ return CodeGen("{this}.check({arg})", {"this": self})
69
+
70
+ def __typeorder__(self, other):
71
+ if isinstance(other, DependentType):
72
+ order = typeorder(self.bound, other.bound)
73
+ if order is Order.SAME:
74
+ # It isn't fully deterministic which of these will be called
75
+ # because of set ordering between the types we compare
76
+ if self < other: # pragma: no cover
77
+ return TypeRelationship(Order.LESS, matches=False)
78
+ elif other < self: # pragma: no cover
79
+ return TypeRelationship(Order.MORE, matches=False)
80
+ else:
81
+ return TypeRelationship(Order.NONE, matches=False)
82
+ else: # pragma: no cover
83
+ return TypeRelationship(order, matches=False)
84
+ elif (matches := subclasscheck(other, self.bound)) or subclasscheck(
85
+ self.bound, other
86
+ ):
87
+ return TypeRelationship(Order.LESS, matches=matches)
88
+ else:
89
+ return TypeRelationship(Order.NONE, matches=False)
90
+
91
+ def __lt__(self, other):
92
+ return False
93
+
94
+ def __or__(self, other):
95
+ if not isinstance(other, DependentType):
96
+ return NotImplemented
97
+ return Or(self, other)
98
+
99
+ def __and__(self, other):
100
+ if not isinstance(other, DependentType):
101
+ return NotImplemented
102
+ return And(self, other)
103
+
104
+
105
+ class ParametrizedDependentType(DependentType):
106
+ def __init__(self, *parameters, bound=None):
107
+ super().__init__(
108
+ self.default_bound(*parameters) if bound is None else bound
109
+ )
110
+ self.parameters = parameters
111
+
112
+ @property
113
+ def parameter(self):
114
+ return self.parameters[0]
115
+
116
+ def default_bound(self, *parameters):
117
+ return None
118
+
119
+ def with_bound(self, new_bound):
120
+ return type(self)(*self.parameters, bound=new_bound)
121
+
122
+ def __eq__(self, other):
123
+ return (
124
+ type(self) is type(other)
125
+ and self.parameters == other.parameters
126
+ and self.bound == other.bound
127
+ )
128
+
129
+ def __hash__(self):
130
+ return hash(self.parameters) ^ hash(self.bound)
131
+
132
+ def __str__(self):
133
+ params = ", ".join(map(repr, self.parameters))
134
+ return f"{type(self).__name__}({params})"
135
+
136
+ __repr__ = __str__
137
+
138
+
139
+ class FuncDependentType(ParametrizedDependentType):
140
+ def default_bound(self, *_):
141
+ fn = type(self).func
142
+ return normalize_type(
143
+ list(inspect.signature(fn).parameters.values())[0].annotation, fn
144
+ )
145
+
146
+ def __lt__(self, other):
147
+ if len(self.parameters) != len(other.parameters):
148
+ return False
149
+ p1g = sum(
150
+ p1 is Any and p2 is not Any
151
+ for p1, p2 in zip(self.parameters, other.parameters)
152
+ )
153
+ p2g = sum(
154
+ p2 is Any and p1 is not Any
155
+ for p1, p2 in zip(self.parameters, other.parameters)
156
+ )
157
+ return p2g and not p1g
158
+
159
+ def check(self, value):
160
+ return type(self).func(value, *self.parameters)
161
+
162
+
163
+ def dependent_check(fn):
164
+ t = type(fn.__name__, (FuncDependentType,), {"func": fn})
165
+ if len(inspect.signature(fn).parameters) == 1:
166
+ t = t()
167
+ return t
168
+
169
+
170
+ class Equals(ParametrizedDependentType):
171
+ keyable_type = True
172
+
173
+ def default_bound(self, *parameters):
174
+ return type(parameters[0])
175
+
176
+ def check(self, value):
177
+ return value in self.parameters
178
+
179
+ @classmethod
180
+ def keygen(cls):
181
+ return "{arg}"
182
+
183
+ def get_keys(self):
184
+ return [self.parameter]
185
+
186
+ def codegen(self):
187
+ if len(self.parameters) == 1:
188
+ return CodeGen("({arg} == {p})", {"p": self.parameter})
189
+ else:
190
+ return CodeGen("({arg} in {ps})", {"ps": self.parameters})
191
+
192
+
193
+ @dependent_check
194
+ def HasKeys(value: Mapping, *keys):
195
+ return all(k in value for k in keys)
196
+
197
+
198
+ @dependent_check
199
+ def StartsWith(value: str, prefix):
200
+ return value.startswith(prefix)
201
+
202
+
203
+ @dependent_check
204
+ def EndsWith(value: str, suffix):
205
+ return value.endswith(suffix)
206
+
207
+
208
+ @dependent_check
209
+ def Regexp(value: str, regexp):
210
+ return bool(re.search(pattern=regexp, string=value))
211
+
212
+
213
+ class Dependent:
214
+ def __class_getitem__(cls, item):
215
+ bound, dt = item
216
+ if not isinstance(dt, DependentType):
217
+ dt = dependent_check(dt)
218
+ return dt.with_bound(bound)
219
+
220
+
221
+ class Or(DependentType):
222
+ def __init__(self, *types, bound=None):
223
+ self.types = types
224
+ super().__init__(bound or self.default_bound())
225
+
226
+ def default_bound(self):
227
+ return Union[tuple([t.bound for t in self.types])]
228
+
229
+ def codegen(self):
230
+ template = " or ".join("{}" for t in self.types)
231
+ return combine(template, [t.codegen() for t in self.types])
232
+
233
+ def check(self, value):
234
+ return any(t.check(value) for t in self.types)
235
+
236
+
237
+ class And(DependentType):
238
+ def __init__(self, *types, bound=None):
239
+ self.types = types
240
+ super().__init__(bound or self.default_bound())
241
+
242
+ def default_bound(self):
243
+ bounds = frozenset(t.bound for t in self.types)
244
+ return Intersection[tuple(bounds)]
245
+
246
+ def codegen(self):
247
+ template = " and ".join("{}" for t in self.types)
248
+ return combine(template, [t.codegen() for t in self.types])
249
+
250
+ def check(self, value):
251
+ return all(t.check(value) for t in self.types)
252
+
253
+ def __str__(self):
254
+ return " & ".join(map(repr, self.types))
255
+
256
+ __repr__ = __str__
257
+
258
+
259
+ if TYPE_CHECKING: # pragma: no cover
260
+ from typing import Annotated, TypeAlias
261
+
262
+ T = TypeVar("T")
263
+ A = TypeVar("A")
264
+ Dependent: TypeAlias = Annotated[T, A]
265
+
266
+
267
+ __all__ = [
268
+ "Dependent",
269
+ "DependentType",
270
+ "Equals",
271
+ "HasKeys",
272
+ "StartsWith",
273
+ "EndsWith",
274
+ "dependent_check",
275
+ ]
ovld/mro.py CHANGED
@@ -1,171 +1,182 @@
1
- # Code lifted from Python's functools module (written for
2
- # functools.singledispatch)
3
- # https://github.com/python/cpython/blob/3.8/Lib/functools.py
4
-
5
- # We do not cover the functions because they are part of Python's stdlib and we
6
- # assume they have already been properly tested.
7
-
8
-
9
- def _issubclass(c1, c2):
10
- c1 = getattr(c1, "__proxy_for__", c1)
11
- c2 = getattr(c2, "__proxy_for__", c2)
12
- if hasattr(c1, "__origin__") or hasattr(c2, "__origin__"):
13
- o1 = getattr(c1, "__origin__", c1)
14
- o2 = getattr(c2, "__origin__", c2)
15
- if issubclass(o1, o2):
16
- if o2 is c2: # pragma: no cover
17
- return True
18
- else:
19
- args1 = getattr(c1, "__args__", ())
20
- args2 = getattr(c2, "__args__", ())
21
- if len(args1) != len(args2):
22
- return False
23
- return all(_issubclass(a1, a2) for a1, a2 in zip(args1, args2))
1
+ import typing
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from graphlib import TopologicalSorter
5
+
6
+
7
+ class Order(Enum):
8
+ LESS = -1
9
+ MORE = 1
10
+ SAME = 0
11
+ NONE = None
12
+
13
+ def opposite(self):
14
+ if self is Order.LESS:
15
+ return Order.MORE
16
+ elif self is Order.MORE:
17
+ return Order.LESS
24
18
  else:
25
- return False
26
- else:
27
- return issubclass(c1, c2)
19
+ return self
28
20
 
29
21
 
30
- def _mro(cls):
31
- if hasattr(cls, "__mro__"):
32
- return set(cls.__mro__)
33
- elif hasattr(cls, "__origin__"):
34
- return set(cls.__origin__.__mro__)
35
- else: # pragma: no cover
36
- return {cls}
22
+ @dataclass
23
+ class TypeRelationship:
24
+ order: Order
25
+ matches: bool = None
37
26
 
38
27
 
39
- def _subclasses(cls):
40
- scf = getattr(cls, "__subclasses__", ())
41
- return scf and scf()
28
+ def typeorder(t1, t2):
29
+ """Order relation between two types.
42
30
 
31
+ Returns a member of the Order enum.
43
32
 
44
- def _c3_merge(sequences): # pragma: no cover
45
- """Merges MROs in *sequences* to a single MRO using the C3 algorithm.
46
- Adapted from http://www.python.org/download/releases/2.3/mro/.
33
+ * typeorder(t1, t2) is Order.LESS if t2 is more general than t1
34
+ * typeorder(t1, t2) is Order.SAME if t1 is equal to t2
35
+ * typeorder(t1, t2) is Order.MORE if t1 is more general than t2
36
+ * typeorder(t1, t2) is Order.NONE if they cannot be compared
47
37
  """
48
- result = []
49
- while True:
50
- sequences = [s for s in sequences if s] # purge empty sequences
51
- if not sequences:
52
- return result
53
- for s1 in sequences: # find merge candidates among seq heads
54
- candidate = s1[0]
55
- for s2 in sequences:
56
- if candidate in s2[1:]:
57
- candidate = None
58
- break # reject the current head, it appears later
59
- else:
60
- break
61
- if candidate is None:
62
- raise RuntimeError("Inconsistent hierarchy")
63
- result.append(candidate)
64
- # remove the chosen candidate
65
- for seq in sequences:
66
- if seq[0] == candidate:
67
- del seq[0]
68
-
69
-
70
- def _c3_mro(cls, abcs=None, abscollect=None): # pragma: no cover
71
- """Computes the method resolution order using extended C3 linearization.
72
- If no *abcs* are given, the algorithm works exactly like the built-in C3
73
- linearization used for method resolution.
74
- If given, *abcs* is a list of abstract base classes that should be inserted
75
- into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
76
- result. The algorithm inserts ABCs where their functionality is introduced,
77
- i.e. issubclass(cls, abc) returns True for the class itself but returns
78
- False for all its direct base classes. Implicit ABCs for a given class
79
- (either registered or inferred from the presence of a special method like
80
- __len__) are inserted directly after the last ABC explicitly listed in the
81
- MRO of said class. If two implicit ABCs end up next to each other in the
82
- resulting MRO, their ordering depends on the order of types in *abcs*.
83
- """
84
- bases = getattr(cls, "__bases__", ())
85
- if hasattr(cls, "__origin__"):
86
- bases = [cls.__origin__, *bases]
87
- for i, base in enumerate(reversed(bases)):
88
- if hasattr(base, "__abstractmethods__"):
89
- boundary = len(bases) - i
90
- break # Bases up to the last explicit ABC are considered first.
38
+ if t1 == t2:
39
+ return Order.SAME
40
+
41
+ t1 = getattr(t1, "__proxy_for__", t1)
42
+ t2 = getattr(t2, "__proxy_for__", t2)
43
+
44
+ if (
45
+ hasattr(t1, "__typeorder__")
46
+ and (result := t1.__typeorder__(t2)) is not NotImplemented
47
+ ):
48
+ return result.order
49
+ elif (
50
+ hasattr(t2, "__typeorder__")
51
+ and (result := t2.__typeorder__(t1)) is not NotImplemented
52
+ ):
53
+ return result.order.opposite()
54
+
55
+ o1 = getattr(t1, "__origin__", None)
56
+ o2 = getattr(t2, "__origin__", None)
57
+
58
+ if o2 is typing.Union:
59
+ compare = [
60
+ x for t in t2.__args__ if (x := typeorder(t1, t)) is not Order.NONE
61
+ ]
62
+ if not compare:
63
+ return Order.NONE
64
+ elif any(x is Order.LESS or x is Order.SAME for x in compare):
65
+ return Order.LESS
66
+ else:
67
+ return Order.MORE
68
+
69
+ if o1 is typing.Union:
70
+ return typeorder(t2, t1).opposite()
71
+
72
+ if o2 and not o1:
73
+ return typeorder(t2, t1).opposite()
74
+
75
+ if o1:
76
+ if not o2: # or getattr(t2, "__args__", None) is None:
77
+ order = typeorder(o1, t2)
78
+ if order is order.SAME:
79
+ order = order.LESS
80
+ return order
81
+
82
+ if (order := typeorder(o1, o2)) is not Order.SAME:
83
+ return order
84
+
85
+ args1 = getattr(t1, "__args__", ())
86
+ args2 = getattr(t2, "__args__", ())
87
+
88
+ if args1 and not args2:
89
+ return Order.LESS
90
+ if args2 and not args1:
91
+ return Order.MORE
92
+ if len(args1) != len(args2):
93
+ return Order.NONE
94
+
95
+ ords = [typeorder(a1, a2) for a1, a2 in zip(args1, args2)]
96
+ if Order.MORE in ords and Order.LESS in ords:
97
+ return Order.NONE
98
+ elif Order.NONE in ords:
99
+ return Order.NONE
100
+ elif Order.MORE in ords:
101
+ return Order.MORE
102
+ elif Order.LESS in ords:
103
+ return Order.LESS
104
+ else: # pragma: no cover
105
+ # Not sure when t1 != t2 and that happens
106
+ return Order.SAME
107
+
108
+ sx = issubclass(t1, t2)
109
+ sy = issubclass(t2, t1)
110
+ if sx and sy: # pragma: no cover
111
+ # Not sure when t1 != t2 and that happens
112
+ return Order.SAME
113
+ elif sx:
114
+ return Order.LESS
115
+ elif sy:
116
+ return Order.MORE
91
117
  else:
92
- boundary = 0
93
- abcs = list(abcs) if abcs else []
94
- explicit_bases = list(bases[:boundary])
95
- abstract_bases = []
96
- other_bases = list(bases[boundary:])
97
- for base in abcs:
98
- if _issubclass(cls, base) and not any(
99
- _issubclass(b, base) for b in bases
100
- ):
101
- # If *cls* is the class that introduces behaviour described by
102
- # an ABC *base*, insert said ABC to its MRO.
103
- abstract_bases.append(base)
104
- for base in abstract_bases:
105
- abcs.remove(base)
106
- abscollect.update(abstract_bases)
107
- explicit_c3_mros = [
108
- _c3_mro(base, abcs=abcs, abscollect=abscollect)
109
- for base in explicit_bases
110
- ]
111
- abstract_c3_mros = [
112
- _c3_mro(base, abcs=abcs, abscollect=abscollect)
113
- for base in abstract_bases
114
- ]
115
- other_c3_mros = [
116
- _c3_mro(base, abcs=abcs, abscollect=abscollect) for base in other_bases
117
- ]
118
- return _c3_merge(
119
- [[cls]]
120
- + explicit_c3_mros
121
- + abstract_c3_mros
122
- + other_c3_mros
123
- + [explicit_bases]
124
- + [abstract_bases]
125
- + [other_bases]
126
- )
127
-
128
-
129
- def compose_mro(cls, types, abscollect): # pragma: no cover
130
- """Calculates the method resolution order for a given class *cls*.
131
- Includes relevant abstract base classes (with their respective bases) from
132
- the *types* iterable. Uses a modified C3 linearization algorithm.
133
- """
134
- bases = _mro(cls)
118
+ return Order.NONE
119
+
120
+
121
+ def subclasscheck(t1, t2):
122
+ """Check whether t1 is a "subclass" of t2."""
123
+ if t1 == t2:
124
+ return True
135
125
 
136
- # Remove entries which are already present in the __mro__ or unrelated.
137
- def is_related(typ):
138
- return typ not in bases and _issubclass(cls, typ)
126
+ t1 = getattr(t1, "__proxy_for__", t1)
127
+ t2 = getattr(t2, "__proxy_for__", t2)
139
128
 
140
- types = [n for n in types if is_related(n)]
129
+ if (
130
+ hasattr(t2, "__typeorder__")
131
+ and (result := t2.__typeorder__(t1)) is not NotImplemented
132
+ ):
133
+ return result.matches
141
134
 
142
- # Remove entries which are strict bases of other entries (they will end up
143
- # in the MRO anyway.
144
- def is_strict_base(typ):
145
- for other in types:
146
- if typ != other and typ in _mro(other):
135
+ o1 = getattr(t1, "__origin__", None)
136
+ o2 = getattr(t2, "__origin__", None)
137
+
138
+ if o2 is typing.Union:
139
+ return any(subclasscheck(t1, t) for t in t2.__args__)
140
+ elif o1 is typing.Union:
141
+ return all(subclasscheck(t, t2) for t in t1.__args__)
142
+
143
+ if o1 or o2:
144
+ o1 = o1 or t1
145
+ o2 = o2 or t2
146
+ if issubclass(o1, o2):
147
+ if o2 is t2: # pragma: no cover
147
148
  return True
148
- return False
149
-
150
- types = [n for n in types if not is_strict_base(n)]
151
- # Subclasses of the ABCs in *types* which are also implemented by
152
- # *cls* can be used to stabilize ABC ordering.
153
- type_set = set(types)
154
- mro = []
155
- for typ in types:
156
- if typ is object:
157
- continue
158
- found = []
159
- for sub in _subclasses(typ):
160
- if sub not in bases and _issubclass(cls, sub):
161
- found.append([s for s in sub.__mro__ if s in type_set])
162
- if not found:
163
- mro.append(typ)
164
- continue
165
- # Favor subclasses with the biggest number of useful bases
166
- found.sort(key=len, reverse=True)
167
- for sub in found:
168
- for subcls in sub:
169
- if subcls not in mro:
170
- mro.append(subcls)
171
- return _c3_mro(cls, abcs=mro, abscollect=abscollect)
149
+ else:
150
+ args1 = getattr(t1, "__args__", ())
151
+ args2 = getattr(t2, "__args__", ())
152
+ if len(args1) != len(args2):
153
+ return False
154
+ return all(
155
+ subclasscheck(a1, a2) for a1, a2 in zip(args1, args2)
156
+ )
157
+ else:
158
+ return False
159
+ else:
160
+ return issubclass(t1, t2)
161
+
162
+
163
+ def sort_types(cls, avail):
164
+ # We filter everything except subclasses and dependent types that *might* cover
165
+ # the object represented by cls.
166
+ avail = [t for t in avail if subclasscheck(cls, t)]
167
+ deps = {t: set() for t in avail}
168
+ for i, t1 in enumerate(avail):
169
+ for t2 in avail[i + 1 :]:
170
+ # NOTE: this is going to scale poorly when there's a hundred Literal in the pool
171
+ order = typeorder(t1, t2)
172
+ if order is Order.LESS:
173
+ deps[t2].add(t1)
174
+ elif order is Order.MORE:
175
+ deps[t1].add(t2)
176
+ sorter = TopologicalSorter(deps)
177
+ sorter.prepare()
178
+ while sorter.is_active():
179
+ nodes = sorter.get_ready()
180
+ yield nodes
181
+ for n in nodes:
182
+ sorter.done(n)