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 5f4c0c0a6..74042e793 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -491,17 +491,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): @@ -509,25 +514,52 @@ 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, method): + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__,) + ) + except AttributeError: + pass + + return method + def attrs(maybe_cls=None, these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, @@ -753,7 +785,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. @@ -807,7 +839,7 @@ def _make_cmp(attrs): unique_filename, ) eq = locs["__eq__"] - ne = _ne + ne = __ne__ def attrs_to_tuple(obj): """ @@ -815,7 +847,7 @@ def attrs_to_tuple(obj): """ return _attrs_to_tuple(obj, attrs) - def lt(self, other): + def __lt__(self, other): """ Automatically created by attrs. """ @@ -824,7 +856,7 @@ def lt(self, other): else: return NotImplemented - def le(self, other): + def __le__(self, other): """ Automatically created by attrs. """ @@ -833,7 +865,7 @@ def le(self, other): else: return NotImplemented - def gt(self, other): + def __gt__(self, other): """ Automatically created by attrs. """ @@ -842,7 +874,7 @@ def gt(self, other): else: return NotImplemented - def ge(self, other): + def __ge__(self, other): """ Automatically created by attrs. """ @@ -851,7 +883,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): @@ -877,7 +909,7 @@ def _make_repr(attrs, ns): if a.repr ) - def repr_(self): + def __repr__(self): """ Automatically created by attrs. """ @@ -898,7 +930,7 @@ def repr_(self): for name in attr_names ) ) - return repr_ + return __repr__ def _add_repr(cls, ns=None, attrs=None): @@ -908,8 +940,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 a77a01269..9746a0c88 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -997,3 +997,49 @@ 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__ + + 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__