diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 97997cbe64f2..2e473c474526 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -189,6 +189,58 @@ def check_getitem(_mapping, **kwargs): .format(v, k, ', '.join(map(repr, mapping)))) from None +def select_matching_signature(funcs, *args, **kwargs): + """ + Select and call the function that accepts ``*args, **kwargs``. + + *funcs* is a list of functions which should not raise any exception (other + than `TypeError` if the arguments passed do not match their signature). + + `select_matching_signature` tries to call each of the functions in *funcs* + with ``*args, **kwargs`` (in the order in which they are given). Calls + that fail with a `TypeError` are silently skipped. As soon as a call + succeeds, `select_matching_signature` returns its return value. If no + function accepts ``*args, **kwargs``, then the `TypeError` raised by the + last failing call is re-raised. + + Callers should normally make sure that any ``*args, **kwargs`` can only + bind a single *func* (to avoid any ambiguity), although this is not checked + by `select_matching_signature`. + + Notes + ----- + `select_matching_signature` is intended to help implementing + signature-overloaded functions. In general, such functions should be + avoided, except for back-compatibility concerns. A typical use pattern is + :: + + def my_func(*args, **kwargs): + params = select_matching_signature( + [lambda old1, old2: locals(), lambda new: locals()], + *args, **kwargs) + if "old1" in params: + warn_deprecated(...) + old1, old2 = params.values() # note that locals() is ordered. + else: + new, = params.values() + # do things with params + + which allows *my_func* to be called either with two parameters (*old1* and + *old2*) or a single one (*new*). Note that the new signature is given + last, so that callers get a `TypeError` corresponding to the new signature + if the arguments they passed in do not match any signature. + """ + # Rather than relying on locals() ordering, one could have just used func's + # signature (``bound = inspect.signature(func).bind(*args, **kwargs); + # bound.apply_defaults(); return bound``) but that is significantly slower. + for i, func in enumerate(funcs): + try: + return func(*args, **kwargs) + except TypeError: + if i == len(funcs) - 1: + raise + + def warn_external(message, category=None): """ `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index c2becd70733a..a0364d54e083 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1994,32 +1994,23 @@ def __init__(self, *args, **kwargs): # signature deprecation since="3.5": Change to new signature after the # deprecation has expired. Also remove setting __init__.__signature__, # and remove the Notes from the docstring. - # - # We use lambdas to parse *args, **kwargs through the respective old - # and new signatures. - try: - # Old signature: - # The following raises a TypeError iif the args don't match. - w, h, coords, antialiased, shading, kwargs = ( + params = _api.select_matching_signature( + [ lambda meshWidth, meshHeight, coordinates, antialiased=True, - shading='flat', **kwargs: - (meshWidth, meshHeight, coordinates, antialiased, shading, - kwargs))(*args, **kwargs) - except TypeError as exc: - # New signature: - # If the following raises a TypeError (i.e. args don't match), - # just let it propagate. - coords, antialiased, shading, kwargs = ( + shading='flat', **kwargs: locals(), lambda coordinates, antialiased=True, shading='flat', **kwargs: - (coordinates, antialiased, shading, kwargs))(*args, **kwargs) - coords = np.asarray(coords, np.float64) - else: # The old signature matched. + locals() + ], + *args, **kwargs).values() + *old_w_h, coords, antialiased, shading, kwargs = params + if old_w_h: # The old signature matched. _api.warn_deprecated( "3.5", message="This usage of Quadmesh is deprecated: Parameters " "meshWidth and meshHeights will be removed; " "coordinates must be 2D; all parameters except " "coordinates will be keyword-only.") + w, h = old_w_h coords = np.asarray(coords, np.float64).reshape((h + 1, w + 1, 2)) kwargs.setdefault("pickradius", 0) # end of signature deprecation code