ovld 0.3.8__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,126 +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
1
+ import typing
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from graphlib import TopologicalSorter
4
5
 
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
6
 
7
+ class Order(Enum):
8
+ LESS = -1
9
+ MORE = 1
10
+ SAME = 0
11
+ NONE = None
8
12
 
9
- def _c3_merge(sequences): # pragma: no cover
10
- """Merges MROs in *sequences* to a single MRO using the C3 algorithm.
11
- Adapted from http://www.python.org/download/releases/2.3/mro/.
12
- """
13
- result = []
14
- while True:
15
- sequences = [s for s in sequences if s] # purge empty sequences
16
- if not sequences:
17
- return result
18
- for s1 in sequences: # find merge candidates among seq heads
19
- candidate = s1[0]
20
- for s2 in sequences:
21
- if candidate in s2[1:]:
22
- candidate = None
23
- break # reject the current head, it appears later
24
- else:
25
- break
26
- if candidate is None:
27
- raise RuntimeError("Inconsistent hierarchy")
28
- result.append(candidate)
29
- # remove the chosen candidate
30
- for seq in sequences:
31
- if seq[0] == candidate:
32
- del seq[0]
33
-
34
-
35
- def _c3_mro(cls, abcs=None): # pragma: no cover
36
- """Computes the method resolution order using extended C3 linearization.
37
- If no *abcs* are given, the algorithm works exactly like the built-in C3
38
- linearization used for method resolution.
39
- If given, *abcs* is a list of abstract base classes that should be inserted
40
- into the resulting MRO. Unrelated ABCs are ignored and don't end up in the
41
- result. The algorithm inserts ABCs where their functionality is introduced,
42
- i.e. issubclass(cls, abc) returns True for the class itself but returns
43
- False for all its direct base classes. Implicit ABCs for a given class
44
- (either registered or inferred from the presence of a special method like
45
- __len__) are inserted directly after the last ABC explicitly listed in the
46
- MRO of said class. If two implicit ABCs end up next to each other in the
47
- resulting MRO, their ordering depends on the order of types in *abcs*.
13
+ def opposite(self):
14
+ if self is Order.LESS:
15
+ return Order.MORE
16
+ elif self is Order.MORE:
17
+ return Order.LESS
18
+ else:
19
+ return self
20
+
21
+
22
+ @dataclass
23
+ class TypeRelationship:
24
+ order: Order
25
+ matches: bool = None
26
+
27
+
28
+ def typeorder(t1, t2):
29
+ """Order relation between two types.
30
+
31
+ Returns a member of the Order enum.
32
+
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
48
37
  """
49
- for i, base in enumerate(reversed(cls.__bases__)):
50
- if hasattr(base, "__abstractmethods__"):
51
- boundary = len(cls.__bases__) - i
52
- 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
53
117
  else:
54
- boundary = 0
55
- abcs = list(abcs) if abcs else []
56
- explicit_bases = list(cls.__bases__[:boundary])
57
- abstract_bases = []
58
- other_bases = list(cls.__bases__[boundary:])
59
- for base in abcs:
60
- if issubclass(cls, base) and not any(
61
- issubclass(b, base) for b in cls.__bases__
62
- ):
63
- # If *cls* is the class that introduces behaviour described by
64
- # an ABC *base*, insert said ABC to its MRO.
65
- abstract_bases.append(base)
66
- for base in abstract_bases:
67
- abcs.remove(base)
68
- explicit_c3_mros = [_c3_mro(base, abcs=abcs) for base in explicit_bases]
69
- abstract_c3_mros = [_c3_mro(base, abcs=abcs) for base in abstract_bases]
70
- other_c3_mros = [_c3_mro(base, abcs=abcs) for base in other_bases]
71
- return _c3_merge(
72
- [[cls]]
73
- + explicit_c3_mros
74
- + abstract_c3_mros
75
- + other_c3_mros
76
- + [explicit_bases]
77
- + [abstract_bases]
78
- + [other_bases]
79
- )
80
-
81
-
82
- def compose_mro(cls, types): # pragma: no cover
83
- """Calculates the method resolution order for a given class *cls*.
84
- Includes relevant abstract base classes (with their respective bases) from
85
- the *types* iterable. Uses a modified C3 linearization algorithm.
86
- """
87
- bases = set(cls.__mro__)
88
-
89
- # Remove entries which are already present in the __mro__ or unrelated.
90
- def is_related(typ):
91
- return (
92
- typ not in bases
93
- and hasattr(typ, "__mro__")
94
- and issubclass(cls, typ)
95
- )
96
-
97
- types = [n for n in types if is_related(n)]
98
-
99
- # Remove entries which are strict bases of other entries (they will end up
100
- # in the MRO anyway.
101
- def is_strict_base(typ):
102
- for other in types:
103
- if typ != other and typ in other.__mro__:
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
125
+
126
+ t1 = getattr(t1, "__proxy_for__", t1)
127
+ t2 = getattr(t2, "__proxy_for__", t2)
128
+
129
+ if (
130
+ hasattr(t2, "__typeorder__")
131
+ and (result := t2.__typeorder__(t1)) is not NotImplemented
132
+ ):
133
+ return result.matches
134
+
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
104
148
  return True
105
- return False
106
-
107
- types = [n for n in types if not is_strict_base(n)]
108
- # Subclasses of the ABCs in *types* which are also implemented by
109
- # *cls* can be used to stabilize ABC ordering.
110
- type_set = set(types)
111
- mro = []
112
- for typ in types:
113
- found = []
114
- for sub in typ.__subclasses__():
115
- if sub not in bases and issubclass(cls, sub):
116
- found.append([s for s in sub.__mro__ if s in type_set])
117
- if not found:
118
- mro.append(typ)
119
- continue
120
- # Favor subclasses with the biggest number of useful bases
121
- found.sort(key=len, reverse=True)
122
- for sub in found:
123
- for subcls in sub:
124
- if subcls not in mro:
125
- mro.append(subcls)
126
- return _c3_mro(cls, abcs=mro)
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)