-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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 support to display field names in namedtuple diffs. #7553
Conversation
What do you mean? If |
@@ -423,6 +431,8 @@ def _compare_eq_cls( | |||
fields_to_check = [ | |||
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) | |||
] | |||
elif isnamedtuple(left): | |||
fields_to_check = left._fields |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved from: #7527 (comment)
FWIW, I like the "._fields" approach but I think it should fallback to regular tuple comparison if the attributes are not exhausted.
Also I think we should just change the display of fields using _fields, and continue looking up items in the tuple by index/iteration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I like the "._fields" approach but I think it should fallback to regular tuple comparison if the attributes are not exhausted.
Is this about checking the length of _fields
and the object itself in the other comment?
Also I think we should just change the display of fields using _fields, and continue looking up items in the tuple by index/iteration
Sorry, I don't get this. Is this about _fields
being potentially used for some other purpose in custom subclass of tuple and to rely on iterating the namedtuple by index?
@@ -423,6 +431,8 @@ def _compare_eq_cls( | |||
fields_to_check = [ | |||
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) | |||
] | |||
elif isnamedtuple(left): | |||
fields_to_check = left._fields |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should fall back to the remaining tuple elements
What do you mean? If
_fields
somehow doesn't match up with the tuple length? I'd rather just have an additionallen(obj._fields) == len(obj)
check or so then (handlingTypeError
iflen
isn't available).
I think it might need len(set(obj._fields)) == len(obj)
or something like this?
fields_and_items = [
(field if getattr(obj, field) is item else i), item
for i, (field, item) in enumerate(zip_longest(obj._fields, obj))
]
testing/test_assertion.py
Outdated
@@ -981,6 +982,30 @@ class SimpleDataObjectTwo: | |||
assert lines is None | |||
|
|||
|
|||
class TestAssert_reprcompare_namedtuple: | |||
def test_namedtuple(self) -> None: | |||
SimpleDataObject = namedtuple("SimpleDataObject", ["field_a", "field_b"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since collections
is already imported this could be collections.namedtuple
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collections.abc
is imported. I don't see collections being imported. Are you suggesting to just do import collections
and use collections.namedtuple
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import collections.abc
is approximately equivalent to
importlib.import_module("collections.abc")
import collections
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL, I mostly prefer it to be explicit. Even on import collections
the pre-commit removes it. So I have made the changes as suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the hook removes it because import a
and import a.b.c
are duplicate imports
Thanks so much for working on this @tirkarthi! I'd love to see this feature land. What's the best way to test this? I pulled your fork and tried it out but I couldn't get the field names to show. |
@tekumara You're welcome. I can see the changes with the example from the report #7527 . I tried cloning and did |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! LGTM, except some comments on the tests.
src/_pytest/assertion/util.py
Outdated
@@ -412,9 +418,11 @@ def _compare_eq_cls( | |||
left: Any, | |||
right: Any, | |||
verbose: int, | |||
type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]], | |||
type_fns: Tuple[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not related to this PR, but this type_fns
argument is really odd, it is always the same and can just be dropped & inlined instead.
If you'd like to do this that'd be nice (in a separate commit), but you don't have to :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Believe I didn't inline the arguments as the code would have been formatted over multiple lines.
testing/test_assertion.py
Outdated
for line in lines[2:]: | ||
assert "field_a" not in line | ||
|
||
def test_comparing_two_different_namedtuple(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test tests python itself, not pytest. What did you intend to test here?
testing/test_assertion.py
Outdated
right = SimpleDataObject(1, "c") | ||
|
||
lines = callequal(left, right) | ||
assert lines is not None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be best to just compare all of lines
(i.e. assert lines == [...]
).
@@ -981,6 +981,36 @@ class SimpleDataObjectTwo: | |||
assert lines is None | |||
|
|||
|
|||
class TestAssert_reprcompare_namedtuple: | |||
def test_namedtuple(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add some tests with objects that have _fields
, are tuples, but are not NamedTuples?
Also some tests were _fields
doesn't match __getattr__
and len(type(o)_fields) != len(o)
@graingert Good points that illustrate it can result in false positives. I have come to below definition of isnameduple as below but this will still cause problems in below test case. I feel adding too much cases in isnamedtuple will cause also slowdown the diff output since isnamedtuple. There could be namedtuple like objects as below that might cause surprising outputs. I guess the PR is moving more towards the line where its not feasible to display the output user wants with no standard way to detect namedtuples. def isnamedtuple(obj: Any) -> bool:
fields = getattr(obj, "_fields", None)
return (
isinstance(obj, tuple) # Check for tuple
and fields is not None # Check for _fields
and issequence(fields) # Check that its iterable
and len(obj) == len(fields) # Check for len of both
and all(hasattr(obj, field) for field in fields) # Check all attrs are present
) 2 elements from constructor. 2 attributes as
Current pytest output
With patch though length and attrs are present the util function might get confused with _fields being same and the arguments to constructor to be different.
|
IMHO we shouldn't care (or bikeshed) too much about cases which are very unlikely to happen. At least the Debian Code Search has no results at all for We should make sure pytest doesn't explode entirely when comparing them, but the "representation of details failed" message from above seems quite reasonable to me. |
I have seen projects using
|
IMO the current check |
@tirkarthi do you think you'll have time to work on this? If not, I can finish it. I think it would be a nice feature for the next release. |
@bluetech Sorry, I lost track of the discussion and might not be able to continue. Feel free to use the patch as needed. Thanks :) |
It doesn't serve any purpose that I am able to discern.
becc9f9
to
a83cc96
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed an updated version.
Note that this is strictly restricted to the case where the namedtuple types match, which avoids most of the pitfalls, while still catching 95% of the cases, in particular the "proto-dataclass" case.
a83cc96
to
4487b17
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jumping a bit late into the party, but thanks everyone!
Left just a small suggestion to the CHANGELOG message, other than that LGTM. 👍
4487b17
to
9a0f4e5
Compare
Thanks @bluetech and everybody for the review :) |
Here is a quick checklist that should be present in PRs.
If this change fixes an issue, please:
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
Create a new changelog file in the
changelog
folder, with a name like<ISSUE NUMBER>.<TYPE>.rst
. See changelog/README.rst for details.Add yourself to
AUTHORS
in alphabetical order.