From f1749540cb094f4674839a2637801b6b2a6949c7 Mon Sep 17 00:00:00 2001 From: jab Date: Mon, 19 Feb 2018 16:57:42 +1100 Subject: [PATCH] improve namedbidict - raise TypeError if base_type is not a frozenbidict subclass - rewrite the code to be clearer --- CHANGELOG.rst | 15 +++++++++++++-- bidict/_named.py | 49 +++++++++++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c41ded..728f0ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -79,8 +79,19 @@ Breaking API Changes (So it is no longer possible to create an infinite chain like ``DuplicationPolicy.RAISE.RAISE.RAISE...``.) -- Pickling bidicts on Python 2 now requires the latest version of the - pickle protocol (specified via -1), e.g. ``pickle.dumps(mybidict, -1)`` +- :func:`~bidict.namedbidict` now raises :class:`TypeError` if the provided + ``base_type`` is not a subclass of :class:`~bidict.frozenbidict`. + +- Pickling ordered bidicts now requires + at least version 2 of the pickle protocol. + If you are using Python 3, + :attr:`pickle.DEFAULT_PROTOCOL` is 3 anyway, + so this will not affect you. + However if you are using in Python 2, + :attr:`~pickle.DEFAULT_PROTOCOL` is 0, + so you must now explicitly specify the version + in your :func:`pickle.dumps` calls, + e.g. ``pickle.dumps(ob, 2)``. 0.14.2 (2017-12-06) diff --git a/bidict/_named.py b/bidict/_named.py index fea754a..8485cd9 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -9,6 +9,7 @@ import re +from ._frozen import frozenbidict from ._bidict import bidict @@ -22,36 +23,42 @@ def namedbidict(typename, keyname, valname, base_type=bidict): Analagous to :func:`collections.namedtuple`. """ + if not issubclass(base_type, frozenbidict): + raise TypeError('base_type must be a subclass of frozenbidict') + for name in typename, keyname, valname: if not _LEGALNAMERE.match(name): - raise ValueError('"%s" does not match pattern %s' % - (name, _LEGALNAMEPAT)) + raise ValueError('%r does not match pattern %s' % (name, _LEGALNAMEPAT)) - getfwd = lambda self: self.inv if self._isinv else self # pylint: disable=protected-access - getfwd.__name__ = valname + '_for' - getfwd.__doc__ = u'%s forward %s: %s → %s' % (typename, base_type.__name__, keyname, valname) + class _Named(base_type): - getinv = lambda self: self if self._isinv else self.inv # pylint: disable=protected-access - getinv.__name__ = keyname + '_for' - getinv.__doc__ = u'%s inverse %s: %s → %s' % (typename, base_type.__name__, valname, keyname) + __slots__ = () - __reduce__ = lambda self: ( - _make_empty, (typename, keyname, valname, base_type), self.__getstate__()) - __reduce__.__name__ = '__reduce__' - __reduce__.__doc__ = 'helper for pickle' + def _getfwd(self): + return self.inv if self._isinv else self # pylint: disable=protected-access - __dict__ = { - getfwd.__name__: property(getfwd), - getinv.__name__: property(getinv), - '__reduce__': __reduce__, - } - return type(typename, (base_type,), __dict__) + def _getinv(self): + return self if self._isinv else self.inv # pylint: disable=protected-access + + def __reduce__(self): + return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__()) + + + bname = base_type.__name__ + fname = valname + '_for' + iname = keyname + '_for' + names = dict(typename=typename, bname=bname, keyname=keyname, valname=valname) + fdoc = u'{typename} forward {bname}: {keyname} → {valname}'.format(**names) + idoc = u'{typename} inverse {bname}: {valname} → {keyname}'.format(**names) + setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) + setattr(_Named, iname, property(_Named._getinv, doc=idoc)) + + _Named.__name__ = typename + return _Named def _make_empty(typename, keyname, valname, base_type): - """ - Create a named bidict with the indicated arguments and return an empty instance. - + """Create a named bidict with the indicated arguments and return an empty instance. Used to make :func:`bidict.namedbidict` instances picklable. """ cls = namedbidict(typename, keyname, valname, base_type=base_type)