| Total Complexity | 88 |
| Total Lines | 410 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like structured_data.match often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | """Utilities for destructuring values using matchables and match targets. |
||
| 2 | |||
| 3 | Given a value to destructure, called ``value``: |
||
| 4 | |||
| 5 | - Construct a matchable: ``matchable = Matchable(value)`` |
||
| 6 | - The matchable is initially falsy, but it will become truthy if it is passed a |
||
| 7 | **match target** that matches ``value``: |
||
| 8 | ``assert matchable(some_pattern_that_matches)`` (Matchable returns itself |
||
| 9 | from the call, so you can put the calls in an if-elif block, and only make a |
||
| 10 | given call at most once.) |
||
| 11 | - When the matchable is truthy, it can be indexed to access bindings created by |
||
| 12 | the target. |
||
| 13 | """ |
||
| 14 | |||
| 15 | from __future__ import annotations |
||
| 16 | |||
| 17 | import collections |
||
| 18 | import functools |
||
| 19 | import inspect |
||
| 20 | import typing |
||
| 21 | |||
| 22 | from ._attribute_constructor import AttributeConstructor |
||
| 23 | from ._destructure import DESTRUCTURERS |
||
| 24 | from ._match_failure import MatchFailure |
||
| 25 | from ._not_in import not_in |
||
| 26 | from ._patterns.basic_patterns import DISCARD |
||
| 27 | from ._patterns.basic_patterns import Pattern |
||
| 28 | from ._patterns.bind import Bind |
||
| 29 | from ._patterns.mapping_match import AttrPattern |
||
| 30 | from ._patterns.mapping_match import DictPattern |
||
| 31 | from ._pep_570_when import pep_570_when |
||
| 32 | from ._stack_iter import Action |
||
| 33 | from ._stack_iter import Extend |
||
| 34 | from ._stack_iter import Yield |
||
| 35 | from ._stack_iter import stack_iter |
||
| 36 | |||
| 37 | |||
| 38 | def names(target) -> typing.List[str]: |
||
| 39 | """Return every name bound by a target.""" |
||
| 40 | return DESTRUCTURERS.names(target) |
||
| 41 | |||
| 42 | |||
| 43 | def _as_name(key): |
||
| 44 | if isinstance(key, Pattern): |
||
| 45 | return key.name |
||
| 46 | return key |
||
| 47 | |||
| 48 | |||
| 49 | def _multi_index(dct, key): |
||
| 50 | if isinstance(key, tuple): |
||
| 51 | return tuple(dct[sub_key] for sub_key in key) |
||
| 52 | if isinstance(key, dict): |
||
| 53 | return {name: dct[value] for (name, value) in key.items()} |
||
| 54 | raise KeyError(key) |
||
| 55 | |||
| 56 | |||
| 57 | class MatchDict(collections.abc.MutableMapping): |
||
| 58 | """A MutableMapping that allows for retrieval into structures. |
||
| 59 | |||
| 60 | The actual keys in the mapping must be string values. Most of the mapping |
||
| 61 | methods will only operate on or yield string keys. The exception is |
||
| 62 | subscription: the "key" in subscription can be a structure made of tuples |
||
| 63 | and dicts. For example, ``md["a", "b"] == (md["a"], md["b"])``, and |
||
| 64 | ``md[{1: "a"}] == {1: md["a"]}``. The typical use of this will be to |
||
| 65 | extract many match values at once, as in ``a, b, c == md["a", "b", "c"]``. |
||
| 66 | |||
| 67 | The behavior of most of the pre-defined MutableMapping methods is currently |
||
| 68 | neither tested nor guaranteed. |
||
| 69 | """ |
||
| 70 | |||
| 71 | def __init__(self) -> None: |
||
| 72 | self.data: typing.Dict[str, typing.Any] = {} |
||
| 73 | |||
| 74 | def __getitem__(self, key): |
||
| 75 | key = _as_name(key) |
||
| 76 | if isinstance(key, str): |
||
| 77 | return self.data[key] |
||
| 78 | return _multi_index(self, key) |
||
| 79 | |||
| 80 | def __setitem__(self, key, value): |
||
| 81 | key = _as_name(key) |
||
| 82 | if not isinstance(key, str): |
||
| 83 | raise TypeError |
||
| 84 | self.data[key] = value |
||
| 85 | |||
| 86 | def __delitem__(self, key): |
||
| 87 | del self.data[_as_name(key)] |
||
| 88 | |||
| 89 | def __iter__(self): |
||
| 90 | yield from self.data |
||
| 91 | |||
| 92 | def __len__(self): |
||
| 93 | return len(self.data) |
||
| 94 | |||
| 95 | |||
| 96 | def _stack_iteration(item) -> typing.Optional[Action]: |
||
| 97 | target, value = item |
||
| 98 | if target is DISCARD: |
||
| 99 | return None |
||
| 100 | if isinstance(target, Pattern): |
||
| 101 | return Yield(item) |
||
| 102 | destructurer = DESTRUCTURERS.get_destructurer(target) |
||
| 103 | if destructurer: |
||
| 104 | return Extend(zip(destructurer(target), destructurer(value))) |
||
| 105 | if target != value: |
||
| 106 | raise MatchFailure |
||
| 107 | return None |
||
| 108 | |||
| 109 | |||
| 110 | def _match(target, value) -> MatchDict: |
||
| 111 | local_target = target |
||
| 112 | local_value = value |
||
| 113 | match_dict = MatchDict() |
||
| 114 | for local_target, local_value in stack_iter( |
||
| 115 | (local_target, local_value), _stack_iteration |
||
| 116 | ): |
||
| 117 | not_in(container=match_dict, item=local_target.name) |
||
| 118 | match_dict[local_target.name] = local_value |
||
| 119 | return match_dict |
||
| 120 | |||
| 121 | |||
| 122 | class Matchable: |
||
| 123 | """Given a value, attempt to match against a target. |
||
| 124 | |||
| 125 | The truthiness of ``Matchable`` values varies on whether they have bindings |
||
| 126 | associated with them. They are truthy exactly when they have bindings. |
||
| 127 | |||
| 128 | ``Matchable`` values provide two basic forms of syntactic sugar. |
||
| 129 | ``m_able(target)`` is equivalent to ``m_able.match(target)``, and |
||
| 130 | ``m_able[k]`` will return ``m_able.matches[k]`` if the ``Matchable`` is |
||
| 131 | truthy, and raise a ``ValueError`` otherwise. |
||
| 132 | """ |
||
| 133 | |||
| 134 | value: typing.Any |
||
| 135 | matches: typing.Optional[MatchDict] |
||
| 136 | |||
| 137 | def __init__(self, value: typing.Any): |
||
| 138 | self.value = value |
||
| 139 | self.matches = None |
||
| 140 | |||
| 141 | def match(self, target) -> Matchable: |
||
| 142 | """Match against target, generating a set of bindings.""" |
||
| 143 | try: |
||
| 144 | self.matches = _match(target, self.value) |
||
| 145 | except MatchFailure: |
||
| 146 | self.matches = None |
||
| 147 | return self |
||
| 148 | |||
| 149 | def __call__(self, target) -> Matchable: |
||
| 150 | return self.match(target) |
||
| 151 | |||
| 152 | def __getitem__(self, key): |
||
| 153 | if self.matches is None: |
||
| 154 | raise ValueError |
||
| 155 | return self.matches[key] |
||
| 156 | |||
| 157 | def __bool__(self): |
||
| 158 | return self.matches is not None |
||
| 159 | |||
| 160 | |||
| 161 | # In lower-case for aesthetics. |
||
| 162 | pat = AttributeConstructor(Pattern) # pylint: disable=invalid-name |
||
| 163 | |||
| 164 | |||
| 165 | def _decorate(matchers, structure, func): |
||
| 166 | matchers.append((structure, func)) |
||
| 167 | return func |
||
| 168 | |||
| 169 | |||
| 170 | class Descriptor: |
||
| 171 | """Base class for decorator classes.""" |
||
| 172 | __wrapped__ = None |
||
| 173 | |||
| 174 | def __new__(cls, func, *args, **kwargs): |
||
| 175 | new = super().__new__(cls, *args, **kwargs) |
||
| 176 | new.__doc__ = None |
||
| 177 | if func is None: |
||
| 178 | return new |
||
| 179 | return functools.wraps(func)(new) |
||
| 180 | |||
| 181 | |||
| 182 | class _DocWrapper: |
||
| 183 | |||
| 184 | def __init__(self, doc=None): |
||
| 185 | self.doc = doc |
||
| 186 | |||
| 187 | @classmethod |
||
| 188 | def wrap_class(cls, klass): |
||
| 189 | klass.__doc__ = cls(klass.__doc__) |
||
| 190 | return klass |
||
| 191 | |||
| 192 | def __get__(self, instance, owner): |
||
| 193 | if instance is None: |
||
| 194 | return self.doc |
||
| 195 | return vars(instance).get("__doc__") |
||
| 196 | |||
| 197 | def __set__(self, instance, value): |
||
| 198 | vars(instance)["__doc__"] = value |
||
|
1 ignored issue
–
show
|
|||
| 199 | |||
| 200 | def __delete__(self, instance): |
||
| 201 | vars(instance).pop("__doc__", None) |
||
| 202 | |||
| 203 | |||
| 204 | @_DocWrapper.wrap_class |
||
| 205 | class Property(Descriptor): |
||
| 206 | """Decorator with value-based dispatch. Acts as a property.""" |
||
| 207 | |||
| 208 | fset = None |
||
| 209 | fdel = None |
||
| 210 | |||
| 211 | protected = False |
||
| 212 | |||
| 213 | def __new__(cls, func=None, fset=None, fdel=None, doc=None, *args, **kwargs): |
||
| 214 | del fset, fdel, doc |
||
| 215 | return super().__new__(cls, func, *args, **kwargs) |
||
| 216 | |||
| 217 | def __init__(self, func=None, fset=None, fdel=None, doc=None, *args, **kwargs): |
||
| 218 | del func |
||
| 219 | super().__init__(*args, **kwargs) |
||
| 220 | self.fset = fset |
||
| 221 | self.fdel = fdel |
||
| 222 | if doc is not None: |
||
| 223 | self.__doc__ = doc |
||
| 224 | self.get_matchers = [] |
||
| 225 | self.set_matchers = [] |
||
| 226 | self.delete_matchers = [] |
||
| 227 | self.protected = True |
||
| 228 | |||
| 229 | def __setattr__(self, name, value): |
||
| 230 | if self.protected and name != "__doc__": |
||
| 231 | raise AttributeError |
||
| 232 | super().__setattr__(name, value) |
||
| 233 | |||
| 234 | def __delattr__(self, name): |
||
| 235 | if self.protected and name != "__doc__": |
||
| 236 | raise AttributeError |
||
| 237 | super().__delattr__(name) |
||
| 238 | |||
| 239 | def getter(self, getter): |
||
| 240 | """Return a copy of self with the getter replaced.""" |
||
| 241 | return Property(getter, self.fset, self.fdel, self.__doc__) |
||
| 242 | |||
| 243 | def setter(self, setter): |
||
| 244 | """Return a copy of self with the setter replaced.""" |
||
| 245 | return Property(self.__wrapped__, setter, self.fdel, self.__doc__) |
||
| 246 | |||
| 247 | def deleter(self, deleter): |
||
| 248 | """Return a copy of self with the deleter replaced.""" |
||
| 249 | return Property(self.__wrapped__, self.fset, deleter, self.__doc__) |
||
| 250 | |||
| 251 | def __get__(self, instance, owner): |
||
| 252 | if instance is None: |
||
| 253 | return self |
||
| 254 | matchable = Matchable(instance) |
||
| 255 | for (structure, func) in self.get_matchers: |
||
| 256 | if matchable(structure): |
||
| 257 | return func(**matchable.matches) |
||
| 258 | if self.__wrapped__ is None: |
||
| 259 | raise ValueError(self) |
||
| 260 | return self.__wrapped__(instance) |
||
| 261 | |||
| 262 | def __set__(self, instance, value): |
||
| 263 | matchable = Matchable((instance, value)) |
||
| 264 | for (structure, func) in self.set_matchers: |
||
| 265 | if matchable(structure): |
||
| 266 | func(**matchable.matches) |
||
| 267 | return |
||
| 268 | if self.fset is None: |
||
| 269 | raise ValueError((instance, value)) |
||
| 270 | self.fset(instance, value) |
||
| 271 | |||
| 272 | def __delete__(self, instance): |
||
| 273 | matchable = Matchable(instance) |
||
| 274 | for (structure, func) in self.delete_matchers: |
||
| 275 | if matchable(structure): |
||
| 276 | func(**matchable.matches) |
||
| 277 | return |
||
| 278 | if self.fdel is None: |
||
| 279 | raise ValueError(instance) |
||
| 280 | self.fdel(instance) |
||
| 281 | |||
| 282 | def get_when(self, instance): |
||
| 283 | """Add a binding to the getter.""" |
||
| 284 | structure = instance |
||
| 285 | names(structure) # Raise ValueError if there are duplicates |
||
| 286 | return functools.partial(_decorate, self.get_matchers, structure) |
||
| 287 | |||
| 288 | def set_when(self, instance, value): |
||
| 289 | """Add a binding to the setter.""" |
||
| 290 | structure = (instance, value) |
||
| 291 | names(structure) # Raise ValueError if there are duplicates |
||
| 292 | return functools.partial(_decorate, self.set_matchers, structure) |
||
| 293 | |||
| 294 | def delete_when(self, instance): |
||
| 295 | """Add a binding to the deleter.""" |
||
| 296 | structure = instance |
||
| 297 | names(structure) # Raise ValueError if there are duplicates |
||
| 298 | return functools.partial(_decorate, self.delete_matchers, structure) |
||
| 299 | |||
| 300 | |||
| 301 | class Function(Descriptor): |
||
| 302 | """Decorator with value-based dispatch. Acts as a function.""" |
||
| 303 | |||
| 304 | def __init__(self, func, *args, **kwargs): |
||
| 305 | del func |
||
| 306 | super().__init__(*args, **kwargs) |
||
| 307 | self.matchers = [] |
||
| 308 | |||
| 309 | def __get__(self, instance, owner): |
||
| 310 | if instance is None: |
||
| 311 | return self |
||
| 312 | return functools.partial(self, instance) |
||
| 313 | |||
| 314 | def __call__(*args, **kwargs): |
||
| 315 | # Okay, so, this is a convoluted mess. |
||
| 316 | # First, we extract self from the beginning of the argument list |
||
| 317 | self, *args = args |
||
| 318 | # Then we figure out what signature we're giving the outside world. |
||
| 319 | signature = inspect.signature(self) |
||
| 320 | # The signature lets us regularize the call and apply any defaults |
||
| 321 | bound_arguments = signature.bind(*args, **kwargs) |
||
| 322 | bound_arguments.apply_defaults() |
||
| 323 | |||
| 324 | # Extract the *args and **kwargs, if any. |
||
| 325 | # These are never used in the matching, just passed to the underlying function |
||
| 326 | bound_args = () |
||
| 327 | bound_kwargs = {} |
||
| 328 | values = bound_arguments.arguments.copy() |
||
| 329 | for parameter in signature.parameters.values(): |
||
| 330 | if parameter.kind is inspect.Parameter.VAR_POSITIONAL: |
||
| 331 | bound_args = values.pop(parameter.name) |
||
| 332 | if parameter.kind is inspect.Parameter.VAR_KEYWORD: |
||
| 333 | bound_kwargs = values.pop(parameter.name) |
||
| 334 | |||
| 335 | matchable = Matchable(values) |
||
| 336 | for structure, func in self.matchers: |
||
| 337 | if matchable(structure): |
||
| 338 | for key, value in matchable.matches.items(): |
||
| 339 | if key in bound_kwargs: |
||
| 340 | raise TypeError |
||
| 341 | bound_kwargs[key] = value |
||
| 342 | function_sig = inspect.signature(func) |
||
| 343 | function_args = function_sig.bind(**bound_kwargs) |
||
| 344 | for parameter in function_sig.parameters.values(): |
||
| 345 | if parameter.kind is inspect.Parameter.VAR_POSITIONAL: |
||
| 346 | function_args.arguments[parameter.name] = bound_args |
||
| 347 | function_args.apply_defaults() |
||
| 348 | return func(*function_args.args, **function_args.kwargs) |
||
| 349 | raise ValueError(values) |
||
| 350 | |||
| 351 | @pep_570_when |
||
| 352 | def when(self, kwargs): |
||
| 353 | """Add a binding for this function.""" |
||
| 354 | structure = DictPattern(kwargs, exhaustive=True) |
||
| 355 | names(structure) # Raise ValueError if there are duplicates |
||
| 356 | return functools.partial(_decorate, self.matchers, structure) |
||
| 357 | |||
| 358 | |||
| 359 | # This wraps a function that, for reasons, can't be called directly by the code |
||
| 360 | # The function body should probably just be a docstring. |
||
| 361 | def function(_func=None, *, positional_until=0): |
||
| 362 | """Convert a function to dispatch by value. |
||
| 363 | |||
| 364 | The original function is not called when the dispatch function is invoked. |
||
| 365 | """ |
||
| 366 | def wrap(func): |
||
| 367 | signature = inspect.signature(func) |
||
| 368 | new_parameters = [] |
||
| 369 | for index, parameter in enumerate(signature.parameters.values()): |
||
| 370 | if parameter.kind is inspect.Parameter.POSITIONAL_ONLY: |
||
| 371 | raise ValueError("Signature already contains positional-only arguments") |
||
| 372 | if index < positional_until: |
||
| 373 | if parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD: |
||
| 374 | raise ValueError("Cannot overwrite non-POSITIONAL_OR_KEYWORD kind") |
||
| 375 | parameter = parameter.replace(kind=inspect.Parameter.POSITIONAL_ONLY) |
||
| 376 | new_parameters.append(parameter) |
||
| 377 | new_signature = signature.replace(parameters=new_parameters) |
||
| 378 | if new_signature != signature: |
||
| 379 | func.__signature__ = new_signature |
||
| 380 | return Function(func) |
||
| 381 | |||
| 382 | if _func is None: |
||
| 383 | return wrap |
||
| 384 | |||
| 385 | return wrap(_func) |
||
| 386 | |||
| 387 | |||
| 388 | def decorate_in_order(*args): |
||
| 389 | """Apply decorators in the order they're passed to the function.""" |
||
| 390 | def decorator(func): |
||
| 391 | for arg in args: |
||
| 392 | func = arg(func) |
||
| 393 | return func |
||
| 394 | |||
| 395 | return decorator |
||
| 396 | |||
| 397 | |||
| 398 | __all__ = [ |
||
| 399 | "AttrPattern", |
||
| 400 | "Bind", |
||
| 401 | "DictPattern", |
||
| 402 | "MatchDict", |
||
| 403 | "Matchable", |
||
| 404 | "Pattern", |
||
| 405 | "Property", |
||
| 406 | "decorate_in_order", |
||
| 407 | "function", |
||
| 408 | "names", |
||
| 409 | "pat", |
||
| 410 | ] |
||
| 411 |