Coverage for /usr/lib/python3/dist-packages/sympy/multipledispatch/dispatcher.py: 49%
184 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1from __future__ import annotations
3from warnings import warn
4import inspect
5from .conflict import ordering, ambiguities, super_signature, AmbiguityWarning
6from .utils import expand_tuples
7import itertools as itl
10class MDNotImplementedError(NotImplementedError):
11 """ A NotImplementedError for multiple dispatch """
14### Functions for on_ambiguity
16def ambiguity_warn(dispatcher, ambiguities):
17 """ Raise warning when ambiguity is detected
19 Parameters
20 ----------
21 dispatcher : Dispatcher
22 The dispatcher on which the ambiguity was detected
23 ambiguities : set
24 Set of type signature pairs that are ambiguous within this dispatcher
26 See Also:
27 Dispatcher.add
28 warning_text
29 """
30 warn(warning_text(dispatcher.name, ambiguities), AmbiguityWarning)
33class RaiseNotImplementedError:
34 """Raise ``NotImplementedError`` when called."""
36 def __init__(self, dispatcher):
37 self.dispatcher = dispatcher
39 def __call__(self, *args, **kwargs):
40 types = tuple(type(a) for a in args)
41 raise NotImplementedError(
42 "Ambiguous signature for %s: <%s>" % (
43 self.dispatcher.name, str_signature(types)
44 ))
46def ambiguity_register_error_ignore_dup(dispatcher, ambiguities):
47 """
48 If super signature for ambiguous types is duplicate types, ignore it.
49 Else, register instance of ``RaiseNotImplementedError`` for ambiguous types.
51 Parameters
52 ----------
53 dispatcher : Dispatcher
54 The dispatcher on which the ambiguity was detected
55 ambiguities : set
56 Set of type signature pairs that are ambiguous within this dispatcher
58 See Also:
59 Dispatcher.add
60 ambiguity_warn
61 """
62 for amb in ambiguities:
63 signature = tuple(super_signature(amb))
64 if len(set(signature)) == 1:
65 continue
66 dispatcher.add(
67 signature, RaiseNotImplementedError(dispatcher),
68 on_ambiguity=ambiguity_register_error_ignore_dup
69 )
71###
74_unresolved_dispatchers: set[Dispatcher] = set()
75_resolve = [True]
78def halt_ordering():
79 _resolve[0] = False
82def restart_ordering(on_ambiguity=ambiguity_warn):
83 _resolve[0] = True
84 while _unresolved_dispatchers:
85 dispatcher = _unresolved_dispatchers.pop()
86 dispatcher.reorder(on_ambiguity=on_ambiguity)
89class Dispatcher:
90 """ Dispatch methods based on type signature
92 Use ``dispatch`` to add implementations
94 Examples
95 --------
97 >>> from sympy.multipledispatch import dispatch
98 >>> @dispatch(int)
99 ... def f(x):
100 ... return x + 1
102 >>> @dispatch(float)
103 ... def f(x): # noqa: F811
104 ... return x - 1
106 >>> f(3)
107 4
108 >>> f(3.0)
109 2.0
110 """
111 __slots__ = '__name__', 'name', 'funcs', 'ordering', '_cache', 'doc'
113 def __init__(self, name, doc=None):
114 self.name = self.__name__ = name
115 self.funcs = {}
116 self._cache = {}
117 self.ordering = []
118 self.doc = doc
120 def register(self, *types, **kwargs):
121 """ Register dispatcher with new implementation
123 >>> from sympy.multipledispatch.dispatcher import Dispatcher
124 >>> f = Dispatcher('f')
125 >>> @f.register(int)
126 ... def inc(x):
127 ... return x + 1
129 >>> @f.register(float)
130 ... def dec(x):
131 ... return x - 1
133 >>> @f.register(list)
134 ... @f.register(tuple)
135 ... def reverse(x):
136 ... return x[::-1]
138 >>> f(1)
139 2
141 >>> f(1.0)
142 0.0
144 >>> f([1, 2, 3])
145 [3, 2, 1]
146 """
147 def _(func):
148 self.add(types, func, **kwargs)
149 return func
150 return _
152 @classmethod
153 def get_func_params(cls, func):
154 if hasattr(inspect, "signature"):
155 sig = inspect.signature(func)
156 return sig.parameters.values()
158 @classmethod
159 def get_func_annotations(cls, func):
160 """ Get annotations of function positional parameters
161 """
162 params = cls.get_func_params(func)
163 if params:
164 Parameter = inspect.Parameter
166 params = (param for param in params
167 if param.kind in
168 (Parameter.POSITIONAL_ONLY,
169 Parameter.POSITIONAL_OR_KEYWORD))
171 annotations = tuple(
172 param.annotation
173 for param in params)
175 if not any(ann is Parameter.empty for ann in annotations):
176 return annotations
178 def add(self, signature, func, on_ambiguity=ambiguity_warn):
179 """ Add new types/method pair to dispatcher
181 >>> from sympy.multipledispatch import Dispatcher
182 >>> D = Dispatcher('add')
183 >>> D.add((int, int), lambda x, y: x + y)
184 >>> D.add((float, float), lambda x, y: x + y)
186 >>> D(1, 2)
187 3
188 >>> D(1, 2.0)
189 Traceback (most recent call last):
190 ...
191 NotImplementedError: Could not find signature for add: <int, float>
193 When ``add`` detects a warning it calls the ``on_ambiguity`` callback
194 with a dispatcher/itself, and a set of ambiguous type signature pairs
195 as inputs. See ``ambiguity_warn`` for an example.
196 """
197 # Handle annotations
198 if not signature:
199 annotations = self.get_func_annotations(func)
200 if annotations:
201 signature = annotations
203 # Handle union types
204 if any(isinstance(typ, tuple) for typ in signature):
205 for typs in expand_tuples(signature):
206 self.add(typs, func, on_ambiguity)
207 return
209 for typ in signature:
210 if not isinstance(typ, type):
211 str_sig = ', '.join(c.__name__ if isinstance(c, type)
212 else str(c) for c in signature)
213 raise TypeError("Tried to dispatch on non-type: %s\n"
214 "In signature: <%s>\n"
215 "In function: %s" %
216 (typ, str_sig, self.name))
218 self.funcs[signature] = func
219 self.reorder(on_ambiguity=on_ambiguity)
220 self._cache.clear()
222 def reorder(self, on_ambiguity=ambiguity_warn):
223 if _resolve[0]:
224 self.ordering = ordering(self.funcs)
225 amb = ambiguities(self.funcs)
226 if amb:
227 on_ambiguity(self, amb)
228 else:
229 _unresolved_dispatchers.add(self)
231 def __call__(self, *args, **kwargs):
232 types = tuple([type(arg) for arg in args])
233 try:
234 func = self._cache[types]
235 except KeyError:
236 func = self.dispatch(*types)
237 if not func:
238 raise NotImplementedError(
239 'Could not find signature for %s: <%s>' %
240 (self.name, str_signature(types)))
241 self._cache[types] = func
242 try:
243 return func(*args, **kwargs)
245 except MDNotImplementedError:
246 funcs = self.dispatch_iter(*types)
247 next(funcs) # burn first
248 for func in funcs:
249 try:
250 return func(*args, **kwargs)
251 except MDNotImplementedError:
252 pass
253 raise NotImplementedError("Matching functions for "
254 "%s: <%s> found, but none completed successfully"
255 % (self.name, str_signature(types)))
257 def __str__(self):
258 return "<dispatched %s>" % self.name
259 __repr__ = __str__
261 def dispatch(self, *types):
262 """ Deterimine appropriate implementation for this type signature
264 This method is internal. Users should call this object as a function.
265 Implementation resolution occurs within the ``__call__`` method.
267 >>> from sympy.multipledispatch import dispatch
268 >>> @dispatch(int)
269 ... def inc(x):
270 ... return x + 1
272 >>> implementation = inc.dispatch(int)
273 >>> implementation(3)
274 4
276 >>> print(inc.dispatch(float))
277 None
279 See Also:
280 ``sympy.multipledispatch.conflict`` - module to determine resolution order
281 """
283 if types in self.funcs:
284 return self.funcs[types]
286 try:
287 return next(self.dispatch_iter(*types))
288 except StopIteration:
289 return None
291 def dispatch_iter(self, *types):
292 n = len(types)
293 for signature in self.ordering:
294 if len(signature) == n and all(map(issubclass, types, signature)):
295 result = self.funcs[signature]
296 yield result
298 def resolve(self, types):
299 """ Deterimine appropriate implementation for this type signature
301 .. deprecated:: 0.4.4
302 Use ``dispatch(*types)`` instead
303 """
304 warn("resolve() is deprecated, use dispatch(*types)",
305 DeprecationWarning)
307 return self.dispatch(*types)
309 def __getstate__(self):
310 return {'name': self.name,
311 'funcs': self.funcs}
313 def __setstate__(self, d):
314 self.name = d['name']
315 self.funcs = d['funcs']
316 self.ordering = ordering(self.funcs)
317 self._cache = {}
319 @property
320 def __doc__(self):
321 docs = ["Multiply dispatched method: %s" % self.name]
323 if self.doc:
324 docs.append(self.doc)
326 other = []
327 for sig in self.ordering[::-1]:
328 func = self.funcs[sig]
329 if func.__doc__:
330 s = 'Inputs: <%s>\n' % str_signature(sig)
331 s += '-' * len(s) + '\n'
332 s += func.__doc__.strip()
333 docs.append(s)
334 else:
335 other.append(str_signature(sig))
337 if other:
338 docs.append('Other signatures:\n ' + '\n '.join(other))
340 return '\n\n'.join(docs)
342 def _help(self, *args):
343 return self.dispatch(*map(type, args)).__doc__
345 def help(self, *args, **kwargs):
346 """ Print docstring for the function corresponding to inputs """
347 print(self._help(*args))
349 def _source(self, *args):
350 func = self.dispatch(*map(type, args))
351 if not func:
352 raise TypeError("No function found")
353 return source(func)
355 def source(self, *args, **kwargs):
356 """ Print source code for the function corresponding to inputs """
357 print(self._source(*args))
360def source(func):
361 s = 'File: %s\n\n' % inspect.getsourcefile(func)
362 s = s + inspect.getsource(func)
363 return s
366class MethodDispatcher(Dispatcher):
367 """ Dispatch methods based on type signature
369 See Also:
370 Dispatcher
371 """
373 @classmethod
374 def get_func_params(cls, func):
375 if hasattr(inspect, "signature"):
376 sig = inspect.signature(func)
377 return itl.islice(sig.parameters.values(), 1, None)
379 def __get__(self, instance, owner):
380 self.obj = instance
381 self.cls = owner
382 return self
384 def __call__(self, *args, **kwargs):
385 types = tuple([type(arg) for arg in args])
386 func = self.dispatch(*types)
387 if not func:
388 raise NotImplementedError('Could not find signature for %s: <%s>' %
389 (self.name, str_signature(types)))
390 return func(self.obj, *args, **kwargs)
393def str_signature(sig):
394 """ String representation of type signature
396 >>> from sympy.multipledispatch.dispatcher import str_signature
397 >>> str_signature((int, float))
398 'int, float'
399 """
400 return ', '.join(cls.__name__ for cls in sig)
403def warning_text(name, amb):
404 """ The text for ambiguity warnings """
405 text = "\nAmbiguities exist in dispatched function %s\n\n" % (name)
406 text += "The following signatures may result in ambiguous behavior:\n"
407 for pair in amb:
408 text += "\t" + \
409 ', '.join('[' + str_signature(s) + ']' for s in pair) + "\n"
410 text += "\n\nConsider making the following additions:\n\n"
411 text += '\n\n'.join(['@dispatch(' + str_signature(super_signature(s))
412 + ')\ndef %s(...)' % name for s in amb])
413 return text