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 __dataclass_fields__ and __attrs_attrs__ to dataclasses #8578

Merged
merged 18 commits into from Aug 20, 2021
Merged

Add __dataclass_fields__ and __attrs_attrs__ to dataclasses #8578

merged 18 commits into from Aug 20, 2021

Conversation

tkukushkin
Copy link
Contributor

Fixes #6568.

def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext') -> None:
attr_name = '__attrs_attrs__'
attr_type = ctx.api.named_type('__builtins__.tuple', [
ctx.api.named_type('attr.Attribute'),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ilevkivskyi ctx.api.named_type is for builtin types, but what i should use instead of it?

Copy link
Member

Choose a reason for hiding this comment

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

It is not for builtin types, it is for types that are guaranteed to be present (i.e. loaded).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ilevkivskyi, and how to ensure that attr.Attribute is loaded? Or maybe need to use Any if it is not loaded?

Copy link
Member

Choose a reason for hiding this comment

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

They should be loaded, because the attr module must be analyzed if this plugin is called. The tests fail because they run with partial stubs, not the real ones, see for example https://github.com/python/mypy/blob/master/test-data/unit/lib-stub/attr.pyi, Attribute is not present there.

Also the latter is generic, so I would explicitly add a type argument here (even if it is Any).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They should be loaded, because the attr module must be analyzed if this plugin is called.

I have a problem with different styles of import of attr.

This example works fine:

import attr

@attr.s
class Foo:
    pass

class AttrsProtocol(Protocol):
    __attrs_attrs__: Tuple[attr.Attribute, ...]

def foo(x: AttrsProtocol) -> None:
    pass

foo(Foo())

But if I change import to:

from attr import attrs, Attribute

@attrs
class Foo:
    pass

I get exception:

  File "/Users/tkukushkin/Projects/mypy/mypy/plugins/attrs.py", line 664, in _add_attrs_magic_attribute
    ctx.api.named_type('attr.Attribute'),
  File "/Users/tkukushkin/Projects/mypy/mypy/semanal.py", line 4143, in named_type
    assert sym, "Internal error: attempted to construct unknown type"
AssertionError: Internal error: attempted to construct unknown type

Some tests fail because of this.

The tests fail because they run with partial stubs, not the real ones, see for example https://github.com/python/mypy/blob/master/test-data/unit/lib-stub/attr.pyi, Attribute is not present there.

Yes, I already added Attribute to stub, but not pushed yet.

Copy link
Member

Choose a reason for hiding this comment

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

Well, I just looked at the code of named_type() and it is horrible, it uses lookup_qualified() (which is just a normal symbol lookup following Python name resolution rules) so I wonder how it ever worked. You can try using named_type_or_none() instead, that one looks reasonable.

Another option is to actually fix named_type(), but it may be not the best task as a first contribution.

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

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

Please add few dedicated tests and make this a non-draft.

def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext') -> None:
attr_name = '__attrs_attrs__'
attr_type = ctx.api.named_type('__builtins__.tuple', [
ctx.api.named_type('attr.Attribute'),
Copy link
Member

Choose a reason for hiding this comment

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

They should be loaded, because the attr module must be analyzed if this plugin is called. The tests fail because they run with partial stubs, not the real ones, see for example https://github.com/python/mypy/blob/master/test-data/unit/lib-stub/attr.pyi, Attribute is not present there.

Also the latter is generic, so I would explicitly add a type argument here (even if it is Any).

@@ -361,6 +363,21 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None:
var._fullname = info.fullname + '.' + var.name
info.names[var.name] = SymbolTableNode(MDEF, var)

def _add_dataclass_fields_magic_attribute(self) -> None:
attr_name = '__dataclass_fields__'
dataclass_field_type = self._ctx.api.named_type('dataclasses.Field')
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here.

@tkukushkin
Copy link
Contributor Author

tkukushkin commented Mar 29, 2020

@ilevkivskyi Thanks for your suggestions. named_type_or_none works fine, but ClassDefContext.api has type SemanticAnalyzerPluginInterface which hasn't member named_type_or_none. Should I add this method to interface?

And second question: how do you recommend to fix tests testNarrowingParentWithEnumsBasic and testNarrowingParentWithIsInstanceBasic. They use fixtures/tuple.pyi and fixtures/isinstance.pyi respectively. These partial stubs don't contain dict, which I use in _add_dataclass_fields_magic_attribute. Should I create special partial stubs for this tests, or split these tests on several small tests with different objects, or extend existing stubs by simple dict definition (class dict(Generic[_KT, _VT]): pass)?

@ilevkivskyi
Copy link
Member

@tkukushkin Thanks for updates! Here are answers to your questions:

Should I add this method to interface?

I think it would make sense to add it.

Should I create special partial stubs for this tests, or split these tests on several small tests with different objects, or extend existing stubs by simple dict definition

I think creating dedicated partial stubs (fixtures) is fine.

maybe this test is not valid.

Good catch! Yeah, I think this test passes because of bad partial stubs. In real stubs object always has __eq__() so the code you linked is wrong, it should read info.names.get('__eq__') is None instead of info.get('__eq__') is None (to avoid MRO lookup and only check current class).

You can open a separate issue/PR for this.

@tkukushkin
Copy link
Contributor Author

Fixed __eq__ in #8642

@tkukushkin tkukushkin marked this pull request as ready for review October 11, 2020 12:44
@tkukushkin
Copy link
Contributor Author

@JukkaL maybe you can review this pr?

Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

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

A few small comments, and CI is currently failing.

mypy/plugin.py Outdated Show resolved Hide resolved
mypy/plugins/attrs.py Outdated Show resolved Hide resolved
Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

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

Looks good apart from the stray file

Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

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

Thanks! I'll merge once CI passes (if I don't, feel free to remind me).

@JelleZijlstra JelleZijlstra merged commit 7576f65 into python:master Aug 20, 2021
@eviltwin
Copy link

I really don't want to be "that guy" and ask when this will see a release... but do you have published release cadences? This has been merged for a month now and the last release was in June. I'm not trying to phrase this in an "omg what's the hold up?" kind of way - I totally get that other things can be blocking the release.

It's just that I happened across an issue this morning whilst consuming djangorestframework-dataclasses that would be solved by this feature, so I'm a little itchy to see it released :)

@JukkaL
Copy link
Collaborator

JukkaL commented Sep 21, 2021

@eviltwin Tracking issue for the next release: #11158

freundTech added a commit to freundTech/mypy that referenced this pull request Sep 27, 2021
@Tinche
Copy link
Contributor

Tinche commented Dec 16, 2021

@tkukushkin Thanks a lot for this, I think this work is going to be very valuable going forward and I can close my failed attempt at #10467. How difficult would it be to have attr.fields(A) actually return A.__attrs_attrs__? That API is much nicer than the dunder (although the dunder should stay for protocol checking etc).

@tkukushkin
Copy link
Contributor Author

@Tinche I spent some time and got such a proof of concept:

class AttrsFieldsPlugin(Plugin):

    def get_function_signature_hook(self, fullname: str) -> Optional[Callable[[FunctionSigContext], FunctionLike]]:
        if fullname == 'attr.fields':
            return self._attrs_fiels_hook
        return None

    def _attrs_fiels_hook(self, context: FunctionSigContext) -> FunctionLike:
        return CallableType(
            arg_types=[AnyType(TypeOfAny.explicit)],
            arg_kinds=[ArgKind.ARG_POS],
            arg_names=['cls'],
            ret_type=context.args[0][0].node.names['__attrs_attrs__'].type,
            fallback=context.api.named_type('builtins.function'),
        )

Unfortunately, I don't have time to finalize this solution (need to correct the type of the function argument and deal with the situation when not attrs class passed to the attrs.fields)

I also understand attrs.fields returns named tuple, in this solution the function returns regular tuple, and I'm not sure if there is a way to fix that.

@Tinche
Copy link
Contributor

Tinche commented Dec 17, 2021

@tkukushkin

Hello again! Thanks for looking into this.

I think fields doesn't need to be handled by the plugin at all. I've created a PR for attrs adding a protocol and making fields work out of the box: python-attrs/attrs#890

The problem isn't fields, but as you said the fact the plugin generates tuples instead of namedtuples for __attrs_attrs__. So the Mypy plugin needs to be improved to generate a namedtuple subclass for each attrs class it sees and assign that to __attrs_attrs__. I will attempt this but since I'm not super knowledgeable about Mypy would appreciate pointers ;)

In any case, thanks for getting us this far.

@tkukushkin
Copy link
Contributor Author

@Tinche I looked at your pull request – beautiful solution, didn't think of that. But yes, I don't really know how to make __attrs_attrs__ become namedtuple :-(.

lundybernard added a commit to lundybernard/batconf that referenced this pull request Jan 21, 2022
Blocking issue resolved by this update to mypy
python/mypy#6568
python/mypy#8578

we can now check that a DataClass object conforms to our ConfigProtocol
Protocol, allowing us to pass ally mypy checks.
lundybernard added a commit to lundybernard/batconf that referenced this pull request Jan 21, 2022
Blocking issue resolved by this update to mypy
python/mypy#6568
python/mypy#8578

we can now check that a DataClass object conforms to our ConfigProtocol
Protocol, allowing us to pass ally mypy checks.
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.

Recognizing wrapped classes as supporting a Protocol
7 participants