Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add __module__ and __qualname__ to methods, fix __name__ #316

Merged
merged 6 commits into from
Dec 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/309.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
All generated methods now have correct ``__module__``, ``__name__``, and (on Python 3) ``__qualname__`` attributes.
73 changes: 52 additions & 21 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,43 +491,75 @@ 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):
self._cls_dict["__hash__"] = None
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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -807,15 +839,15 @@ def _make_cmp(attrs):
unique_filename,
)
eq = locs["__eq__"]
ne = _ne
ne = __ne__

def attrs_to_tuple(obj):
"""
Save us some typing.
"""
return _attrs_to_tuple(obj, attrs)

def lt(self, other):
def __lt__(self, other):
"""
Automatically created by attrs.
"""
Expand All @@ -824,7 +856,7 @@ def lt(self, other):
else:
return NotImplemented

def le(self, other):
def __le__(self, other):
"""
Automatically created by attrs.
"""
Expand All @@ -833,7 +865,7 @@ def le(self, other):
else:
return NotImplemented

def gt(self, other):
def __gt__(self, other):
"""
Automatically created by attrs.
"""
Expand All @@ -842,7 +874,7 @@ def gt(self, other):
else:
return NotImplemented

def ge(self, other):
def __ge__(self, other):
"""
Automatically created by attrs.
"""
Expand All @@ -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):
Expand All @@ -877,7 +909,7 @@ def _make_repr(attrs, ns):
if a.repr
)

def repr_(self):
def __repr__(self):
"""
Automatically created by attrs.
"""
Expand All @@ -898,7 +930,7 @@ def repr_(self):
for name in attr_names
)
)
return repr_
return __repr__


def _add_repr(cls, ns=None, attrs=None):
Expand All @@ -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


Expand Down
46 changes: 46 additions & 0 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__