ovld 0.3.9__py3-none-any.whl → 0.4.1__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,203 @@
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()
42
-
28
+ def _issubclass(t1, t2):
29
+ try:
30
+ return issubclass(t1, t2)
31
+ except TypeError:
32
+ try:
33
+ return isinstance(t1, t2)
34
+ except TypeError: # pragma: no cover
35
+ return False
43
36
 
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/.
47
- """
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.
91
- 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)
135
37
 
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)
38
+ def typeorder(t1, t2):
39
+ """Order relation between two types.
139
40
 
140
- types = [n for n in types if is_related(n)]
41
+ Returns a member of the Order enum.
141
42
 
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):
43
+ * typeorder(t1, t2) is Order.LESS if t2 is more general than t1
44
+ * typeorder(t1, t2) is Order.SAME if t1 is equal to t2
45
+ * typeorder(t1, t2) is Order.MORE if t1 is more general than t2
46
+ * typeorder(t1, t2) is Order.NONE if they cannot be compared
47
+ """
48
+ if t1 == t2:
49
+ return Order.SAME
50
+
51
+ t1 = getattr(t1, "__proxy_for__", t1)
52
+ t2 = getattr(t2, "__proxy_for__", t2)
53
+
54
+ if (
55
+ hasattr(t1, "__typeorder__")
56
+ and (result := t1.__typeorder__(t2)) is not NotImplemented
57
+ ):
58
+ return result.order
59
+ elif (
60
+ hasattr(t2, "__typeorder__")
61
+ and (result := t2.__typeorder__(t1)) is not NotImplemented
62
+ ):
63
+ return result.order.opposite()
64
+
65
+ o1 = getattr(t1, "__origin__", None)
66
+ o2 = getattr(t2, "__origin__", None)
67
+
68
+ if o2 is typing.Union:
69
+ if t1 is typing.Union:
70
+ return Order.MORE
71
+ compare = [
72
+ x for t in t2.__args__ if (x := typeorder(t1, t)) is not Order.NONE
73
+ ]
74
+ if not compare:
75
+ return Order.NONE
76
+ elif any(x is Order.LESS or x is Order.SAME for x in compare):
77
+ return Order.LESS
78
+ else:
79
+ return Order.MORE
80
+
81
+ if o1 is typing.Union:
82
+ return typeorder(t2, t1).opposite()
83
+
84
+ if o2 and not o1:
85
+ return typeorder(t2, t1).opposite()
86
+
87
+ if o1:
88
+ if not o2: # or getattr(t2, "__args__", None) is None:
89
+ order = typeorder(o1, t2)
90
+ if order is order.SAME:
91
+ order = order.LESS
92
+ return order
93
+
94
+ if (order := typeorder(o1, o2)) is not Order.SAME:
95
+ return order
96
+
97
+ args1 = getattr(t1, "__args__", ())
98
+ args2 = getattr(t2, "__args__", ())
99
+
100
+ if args1 and not args2:
101
+ return Order.LESS
102
+ if args2 and not args1:
103
+ return Order.MORE
104
+ if len(args1) != len(args2):
105
+ return Order.NONE
106
+
107
+ ords = [typeorder(a1, a2) for a1, a2 in zip(args1, args2)]
108
+ if Order.MORE in ords and Order.LESS in ords:
109
+ return Order.NONE
110
+ elif Order.NONE in ords:
111
+ return Order.NONE
112
+ elif Order.MORE in ords:
113
+ return Order.MORE
114
+ elif Order.LESS in ords:
115
+ return Order.LESS
116
+ else: # pragma: no cover
117
+ # Not sure when t1 != t2 and that happens
118
+ return Order.SAME
119
+
120
+ sx = _issubclass(t1, t2)
121
+ sy = _issubclass(t2, t1)
122
+ if sx and sy: # pragma: no cover
123
+ # Not sure when t1 != t2 and that happens
124
+ return Order.SAME
125
+ elif sx:
126
+ return Order.LESS
127
+ elif sy:
128
+ return Order.MORE
129
+ else:
130
+ return Order.NONE
131
+
132
+
133
+ def subclasscheck(t1, t2):
134
+ """Check whether t1 is a "subclass" of t2."""
135
+ if t1 == t2:
136
+ return True
137
+
138
+ t1 = getattr(t1, "__proxy_for__", t1)
139
+ t2 = getattr(t2, "__proxy_for__", t2)
140
+
141
+ if (
142
+ hasattr(t2, "__typeorder__")
143
+ and (result := t2.__typeorder__(t1)) is not NotImplemented
144
+ ):
145
+ return result.matches
146
+
147
+ o1 = getattr(t1, "__origin__", None)
148
+ o2 = getattr(t2, "__origin__", None)
149
+
150
+ if o2 is typing.Union:
151
+ return t1 is typing.Union or any(
152
+ subclasscheck(t1, t) for t in t2.__args__
153
+ )
154
+ elif o1 is typing.Union:
155
+ return t2 is typing.Union or all(
156
+ subclasscheck(t, t2) for t in t1.__args__
157
+ )
158
+
159
+ if not isinstance(o1, type):
160
+ o1 = None
161
+ if not isinstance(o2, type):
162
+ o2 = None
163
+
164
+ if o1 or o2:
165
+ o1 = o1 or t1
166
+ o2 = o2 or t2
167
+ if _issubclass(o1, o2):
168
+ if o2 is t2: # pragma: no cover
147
169
  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)
170
+ else:
171
+ args1 = getattr(t1, "__args__", ())
172
+ args2 = getattr(t2, "__args__", ())
173
+ if len(args1) != len(args2):
174
+ return False
175
+ return all(
176
+ subclasscheck(a1, a2) for a1, a2 in zip(args1, args2)
177
+ )
178
+ else:
179
+ return False
180
+ else:
181
+ return _issubclass(t1, t2)
182
+
183
+
184
+ def sort_types(cls, avail):
185
+ # We filter everything except subclasses and dependent types that *might* cover
186
+ # the object represented by cls.
187
+ avail = [t for t in avail if subclasscheck(cls, t)]
188
+ deps = {t: set() for t in avail}
189
+ for i, t1 in enumerate(avail):
190
+ for t2 in avail[i + 1 :]:
191
+ # NOTE: this is going to scale poorly when there's a hundred Literal in the pool
192
+ order = typeorder(t1, t2)
193
+ if order is Order.LESS:
194
+ deps[t2].add(t1)
195
+ elif order is Order.MORE:
196
+ deps[t1].add(t2)
197
+ sorter = TopologicalSorter(deps)
198
+ sorter.prepare()
199
+ while sorter.is_active():
200
+ nodes = sorter.get_ready()
201
+ yield nodes
202
+ for n in nodes:
203
+ sorter.done(n)