Skip to content

Show class that originally defined attribute in output#6806

Closed
ucodery wants to merge 4 commits into
python:masterfrom
ucodery:issue2500
Closed

Show class that originally defined attribute in output#6806
ucodery wants to merge 4 commits into
python:masterfrom
ucodery:issue2500

Conversation

@ucodery

@ucodery ucodery commented May 8, 2019

Copy link
Copy Markdown
Contributor

If a subclass or its instance attempts to assign an incompatible type
display the class that originally defined the type.

Fixes issue #2500

ucodery added 3 commits May 6, 2019 10:44
If a subclass or its instance attempts to assign an incompatible type
display the class that originally defined the type.

@ilevkivskyi ilevkivskyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for PR! Here I left some comments, I will review the tests in the next round.

Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
lvalue_bases.append(metacls)
attribute_is_defined = (attribute_is_defined or
metacls.has_readable_member(context.name))
if attribute_is_defined and context.name not in direct_lvalue_vars:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to check for attribute_is_defined, to avoid this extra message if the type is from a __getattr__()? Please add a comment. Naively, if an attribute is not defined at all, we can't know its type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so this was to not bother with the work of traversing base classes if the attribute doesn't exist, since we presumably won't find it. It was a short circuit break.

Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated
# the type of accessing the attribute (even after the override).
rvalue_type = self.check_simple_assignment(get_type, rvalue, context)
rvalue_type = self.check_parent_member_assignment(instance_type, get_type,
attribute_type, rvalue, context)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pass both attribute_type and get_type here? This looks wrong, I think you should only pass get_type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, it seems to me, we may not show this additional info if descriptor protocol is involved, it may only add confusion, because the definition a user will see in the base class, is the descriptor type. But this is not a strong opinion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_type (i.e. lvalue_type in the next method) is used only when it falls back on check_simple_assignment and get_type is what was used previously so I didn't change that.
I had considered not doing the fallback in check_parent_member_assignment, then it wouldn't need its lvalue_type arg at all, but it would have to return Optional[Type] and all callers would have to implement the fall back. I thought this was less desirable.

Comment thread mypy/checker.py Outdated
Comment thread mypy/checker.py Outdated

@ilevkivskyi ilevkivskyi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updates! This looks better, I still have few questions/comments.

Comment thread mypy/checker.py
# the type of accessing the attribute (even after the override).
rvalue_type = self.check_simple_assignment(get_type, rvalue, context)
rvalue_type = self.check_parent_member_assignment(instance_type, get_type,
lvalue, attribute_type, rvalue)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a test case for a situation where __get__ is involved?

Comment thread mypy/checker.py

# Here we just infer the type, the result should be type-checked like a normal assignment.
# For this we use the rvalue as type context.
# For this we use the rvalue as type lvalue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mass replace context -> lvalue went wrong.

Comment thread mypy/checker.py
def check_parent_member_assignment(self, lvalue_base_type: Type,
lvalue_type: Type, lvalue: MemberExpr,
rvalue_type: Type, rvalue: Expression) -> Type:
"""Type inherited member assignment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't parse this as English sentence, could you please reformulate?

Comment thread mypy/checker.py
"""Type inherited member assignment.

This defers to check_simple_assignment if the attribute is local to the instance_type,
otherwise it will inform the user where the attribute type was originally defined.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say explicitly that it does exactly the same as check_simple_assignment(), but adds better error message if possible.

Comment thread mypy/checker.py
Return the inferred rvalue_type.
"""
if (isinstance(lvalue_base_type, Instance) and
lvalue.name not in lvalue_base_type.type.names):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you invert this check and put the last return here? This will save one indent level for the whole function body.

Comment thread mypy/checker.py
lvalue_type, rvalue, lvalue,
msg=message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT,
lvalue_name=message_registry.METACLASS_MISMATCH.format(
metaclass.type.defn.name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole part is duplicated below. Can you try factoring it out?

Comment thread mypy/checker.py
message_registry.METACLASS_MISMATCH.format(
metaclass.type.defn.name))
lvalue_base_type = metaclass
if lvalue_base_type.has_readable_member(lvalue.name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a performance optimization? If yes, I would remove it. It will not give much speed-up and the logic in this method is already hard to follow.

A()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
A(False)().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
A()()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
A(False)()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need al these test cases? What do they test?

A(True)().b = False
A(True)(None).b = True
A(True)(None).b = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "bool")
A(True)()(False).b = "" # E: Incompatible types in assignment (expression has type "str", variable has type "bool")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here.

[case testRecursiveCallable]
from typing import Optional
class A:
x = 0 # Type: int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • You don't need to use type comments in tests.
  • This is not a type comment because of a typo (uppercase T)
  • It is not needed here

@hauntsaninja

Copy link
Copy Markdown
Collaborator

Closing this PR, since it's gotten stale and there are a number of merge conflicts. But you're most welcome to pick it back up :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants