_named.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 Joshua Bronson. All Rights Reserved.
  3. #
  4. # This Source Code Form is subject to the terms of the Mozilla Public
  5. # License, v. 2.0. If a copy of the MPL was not distributed with this
  6. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7. """Provides :func:`bidict.namedbidict`."""
  8. import re
  9. from ._abc import BidirectionalMapping
  10. from ._bidict import bidict
  11. _VALID_NAME = re.compile('^[A-z][A-z0-9_]*$')
  12. def namedbidict(typename, keyname, valname, base_type=bidict):
  13. r"""Create a new subclass of *base_type* with custom accessors.
  14. Analagous to :func:`collections.namedtuple`.
  15. The new class's ``__name__`` will be set to *typename*.
  16. Instances of it will provide access to their
  17. :attr:`inverse <BidirectionalMapping.inv>`\s
  18. via the custom *keyname*\_for property,
  19. and access to themselves
  20. via the custom *valname*\_for property.
  21. *See also* the :ref:`namedbidict usage documentation
  22. <other-bidict-types:\:func\:\`~bidict.namedbidict\`>`
  23. :raises ValueError: if any of the *typename*, *keyname*, or *valname*
  24. strings does not match ``%s``, or if *keyname == valname*.
  25. :raises TypeError: if *base_type* is not a subclass of
  26. :class:`BidirectionalMapping`.
  27. (This function requires slightly more of *base_type*,
  28. e.g. the availability of an ``_isinv`` attribute,
  29. but all the :ref:`concrete bidict types
  30. <other-bidict-types:Bidict Types Diagram>`
  31. that the :mod:`bidict` module provides can be passed in.
  32. Check out the code if you actually need to pass in something else.)
  33. """
  34. # Re the `base_type` docs above:
  35. # The additional requirements (providing _isinv and __getstate__) do not belong in the
  36. # BidirectionalMapping interface, and it's overkill to create additional interface(s) for this.
  37. # On the other hand, it's overkill to require that base_type be a subclass of BidictBase, since
  38. # that's too specific. The BidirectionalMapping check along with the docs above should suffice.
  39. if not issubclass(base_type, BidirectionalMapping):
  40. raise TypeError(base_type)
  41. names = (typename, keyname, valname)
  42. if not all(map(_VALID_NAME.match, names)) or keyname == valname:
  43. raise ValueError(names)
  44. class _Named(base_type):
  45. __slots__ = ()
  46. def _getfwd(self):
  47. return self.inv if self._isinv else self
  48. def _getinv(self):
  49. return self if self._isinv else self.inv
  50. def __reduce__(self):
  51. return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__())
  52. bname = base_type.__name__
  53. fname = valname + '_for'
  54. iname = keyname + '_for'
  55. names = dict(typename=typename, bname=bname, keyname=keyname, valname=valname)
  56. fdoc = u'{typename} forward {bname}: {keyname} → {valname}'.format(**names)
  57. idoc = u'{typename} inverse {bname}: {valname} → {keyname}'.format(**names)
  58. setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) # pylint: disable=protected-access
  59. setattr(_Named, iname, property(_Named._getinv, doc=idoc)) # pylint: disable=protected-access
  60. _Named.__name__ = typename
  61. return _Named
  62. namedbidict.__doc__ %= _VALID_NAME.pattern # pylint: disable=no-member
  63. def _make_empty(typename, keyname, valname, base_type):
  64. """Create a named bidict with the indicated arguments and return an empty instance.
  65. Used to make :func:`bidict.namedbidict` instances picklable.
  66. """
  67. cls = namedbidict(typename, keyname, valname, base_type=base_type)
  68. return cls()