From 36f84204c8e55b6ac4a2cdb0ef9da2063d1945a0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 17 Dec 2017 18:35:00 +0100 Subject: [PATCH 1/3] Add __module__ and __qualname__ to methods, fix __name__ Fixes #309 --- changelog.d/309.change.rst | 1 + src/attr/_make.py | 66 ++++++++++++++++++++++++++------------ tests/test_make.py | 22 +++++++++++++ 3 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 changelog.d/309.change.rst diff --git a/changelog.d/309.change.rst b/changelog.d/309.change.rst new file mode 100644 index 000000000..6a8e4fc7a --- /dev/null +++ b/changelog.d/309.change.rst @@ -0,0 +1 @@ +All generated methods now have correct ``__module__``, ``__name__``, and (on Python 3) ``__qualname__`` attributes. diff --git a/src/attr/_make.py b/src/attr/_make.py index 2eca3d4c2..cf99d2b65 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -468,17 +468,22 @@ def slots_setstate(self, state): return cls def add_repr(self, ns): - self._cls_dict["__repr__"] = _make_repr(self._attrs, ns=ns) + self._cls_dict["__repr__"] = self._add_method_dunders( + _make_repr(self._attrs, ns=ns) + ) return self def add_str(self): - repr_ = self._cls_dict.get("__repr__") - if repr_ is None: + repr = self._cls_dict.get("__repr__") + if repr is None: raise ValueError( "__str__ can only be generated if a __repr__ exists." ) - self._cls_dict["__str__"] = repr_ + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) return self def make_unhashable(self): @@ -486,25 +491,45 @@ def make_unhashable(self): return self def add_hash(self): - self._cls_dict["__hash__"] = _make_hash(self._attrs) + self._cls_dict["__hash__"] = self._add_method_dunders( + _make_hash(self._attrs) + ) + return self def add_init(self): - self._cls_dict["__init__"] = _make_init( - self._attrs, - self._has_post_init, - self._frozen, + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._attrs, + self._has_post_init, + self._frozen, + ) ) + return self def add_cmp(self): cd = self._cls_dict cd["__eq__"], cd["__ne__"], cd["__lt__"], cd["__le__"], cd["__gt__"], \ - cd["__ge__"] = _make_cmp(self._attrs) + cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_cmp(self._attrs) + ) return self + def _add_method_dunders(self, meth): + """ + Add __module__ and __qualname__ to a method *method*. + """ + meth.__module__ = self._cls.__module__ + if not PY2: + meth.__qualname__ = ".".join( + (self._cls.__qualname__, meth.__name__,) + ) + return meth + def attrs(maybe_cls=None, these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, @@ -730,7 +755,7 @@ def _add_hash(cls, attrs): return cls -def _ne(self, other): +def __ne__(self, other): """ Check equality and either forward a NotImplemented or return the result negated. @@ -784,7 +809,7 @@ def _make_cmp(attrs): unique_filename, ) eq = locs["__eq__"] - ne = _ne + ne = __ne__ def attrs_to_tuple(obj): """ @@ -792,7 +817,7 @@ def attrs_to_tuple(obj): """ return _attrs_to_tuple(obj, attrs) - def lt(self, other): + def __lt__(self, other): """ Automatically created by attrs. """ @@ -801,7 +826,7 @@ def lt(self, other): else: return NotImplemented - def le(self, other): + def __le__(self, other): """ Automatically created by attrs. """ @@ -810,7 +835,7 @@ def le(self, other): else: return NotImplemented - def gt(self, other): + def __gt__(self, other): """ Automatically created by attrs. """ @@ -819,7 +844,7 @@ def gt(self, other): else: return NotImplemented - def ge(self, other): + def __ge__(self, other): """ Automatically created by attrs. """ @@ -828,7 +853,7 @@ def ge(self, other): else: return NotImplemented - return eq, ne, lt, le, gt, ge + return eq, ne, __lt__, __le__, __gt__, __ge__ def _add_cmp(cls, attrs=None): @@ -854,7 +879,7 @@ def _make_repr(attrs, ns): if a.repr ) - def repr_(self): + def __repr__(self): """ Automatically created by attrs. """ @@ -875,7 +900,7 @@ def repr_(self): for name in attr_names ) ) - return repr_ + return __repr__ def _add_repr(cls, ns=None, attrs=None): @@ -885,8 +910,7 @@ def _add_repr(cls, ns=None, attrs=None): if attrs is None: attrs = cls.__attrs_attrs__ - repr_ = _make_repr(attrs, ns) - cls.__repr__ = repr_ + cls.__repr__ = _make_repr(attrs, ns) return cls diff --git a/tests/test_make.py b/tests/test_make.py index 70cfaf3ec..ea8223f42 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -864,3 +864,25 @@ class C(object): .build_class() assert "ns.C(x=1)" == repr(cls(1)) + + @pytest.mark.parametrize("meth_name", [ + "__init__", "__hash__", "__repr__", "__str__", + "__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__", + ]) + def test_attaches_meta_dunders(self, meth_name): + """ + Generated methods have correct __module__, __name__, and __qualname__ + attributes. + """ + @attr.s(hash=True, str=True) + class C(object): + def organic(self): + pass + + meth = getattr(C, meth_name) + + assert meth_name == meth.__name__ + assert C.organic.__module__ == meth.__module__ + if not PY2: + organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] + assert organic_prefix + "." + meth_name == meth.__qualname__ From bcc589fb8f274d45b51ec755af5e49937a222115 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 18 Dec 2017 08:08:12 +0100 Subject: [PATCH 2/3] Better naming --- src/attr/_make.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index cf99d2b65..8b4b27c63 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -519,16 +519,16 @@ def add_cmp(self): return self - def _add_method_dunders(self, meth): + def _add_method_dunders(self, method): """ - Add __module__ and __qualname__ to a method *method*. + Add __module__ and __qualname__ to a *method*. """ - meth.__module__ = self._cls.__module__ + method.__module__ = self._cls.__module__ if not PY2: - meth.__qualname__ = ".".join( - (self._cls.__qualname__, meth.__name__,) + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__,) ) - return meth + return method def attrs(maybe_cls=None, these=None, repr_ns=None, From 4bc5bcdcf988e00d2c8ad94e65e578e4fe2b6b8c Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 18 Dec 2017 09:06:38 +0100 Subject: [PATCH 3/3] Be more defensive about the presence of the dunders --- src/attr/_make.py | 13 ++++++++++--- tests/test_make.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 8b4b27c63..641ea6a71 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -521,13 +521,20 @@ def add_cmp(self): def _add_method_dunders(self, method): """ - Add __module__ and __qualname__ to a *method*. + Add __module__ and __qualname__ to a *method* if possible. """ - method.__module__ = self._cls.__module__ - if not PY2: + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: method.__qualname__ = ".".join( (self._cls.__qualname__, method.__name__,) ) + except AttributeError: + pass + return method diff --git a/tests/test_make.py b/tests/test_make.py index ea8223f42..3ea867f1a 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -886,3 +886,27 @@ def organic(self): if not PY2: organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] assert organic_prefix + "." + meth_name == meth.__qualname__ + + def test_handles_missing_meta_on_class(self): + """ + If the class hasn't a __module__ or __qualname__, the method hasn't + either. + """ + class C(object): + pass + + b = _ClassBuilder( + C, these=None, slots=False, frozen=False, auto_attribs=False, + ) + b._cls = {} # no __module__; no __qualname__ + + def fake_meth(self): + pass + + fake_meth.__module__ = "42" + fake_meth.__qualname__ = "23" + + rv = b._add_method_dunders(fake_meth) + + assert "42" == rv.__module__ == fake_meth.__module__ + assert "23" == rv.__qualname__ == fake_meth.__qualname__