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 Class Decorator/Metaclass/Base Class plugin #4328

Merged
merged 13 commits into from
Dec 14, 2017

Conversation

euresti
Copy link
Contributor

@euresti euresti commented Dec 6, 2017

Work towards #2088

mypy/plugin.py Outdated
has_default = {} # type: Dict[str, Expression]
args = []

for name, table in info.names.items():
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just discovered this is overly broad, but you get the idea.

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.

Sorry for a long delay! This looks good for a start. You can start adding tests to show how it works with attrs. Also could you please fix lint failures and mypy self check? (see Travis logs)

"""Interface for accessing semantic analyzer functionality in plugins."""

@abstractmethod
def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
Copy link
Member

Choose a reason for hiding this comment

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

Is this the only method that would be potentially used by plugins? Do we need to add some more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My current explorations also need parse_bool and anal_type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fail could be useful too. Should I add them as needed or add them now?

Copy link
Member

Choose a reason for hiding this comment

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

Should I add them as needed or add them now?

I am not sure why we actually need this class. Can't we just pass everything? Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm following the pattern of the other plugins. Technically the object that's passed in is the full object. I believe this base class is here to limit what methods the plugins should be calling. i.e. if the method isn't listed here then the mypy checker will complain about it.

Copy link
Member

Choose a reason for hiding this comment

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

Then we probably need to think what to add here. My expectation is that many methods of SemanticAnalyzer may be useful. You can play more with your implementation for attrs and add here whatever you ever used.

Copy link
Member

Choose a reason for hiding this comment

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

I think we can keep only these methods, and then add more in your PR with the actual plugin (if necessary).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Here is why we are using an ABC instead exposing the entire semantic analyzer interface:

  • By only giving access to a subset of an interface, we declare the public part of the semantic analyzer interface. This has a few benefits related to modularity and loose coupling:
    • Everything else in the semantic analyzer can be more freely modified without breaking plugins. If we change the public interface, we may break 3rd party plugins. (Currently the plugin interface is still not officially supported so we can change things without worries, but I'd like to make the interface more stable in the future.)
    • It's less likely that plugins will rely on internal implementation details that are likely to change, or on poorly thought-out internal APIs that are hard to use correctly.
    • It's easier to reason about plugins in general, as the plugins are expected to stick to the exposed public interface instead of freely accessing mypy internals.
  • This can avoid a cyclic import dependency (maybe not right now, but it will help untangle cyclic dependencies). They slow down incremental checking.

mypy/plugin.py Outdated

for name, table in info.names.items():
if table.type:
var = Var(name.lstrip("_"), table.type)
Copy link
Member

Choose a reason for hiding this comment

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

Variable name table is a bit misleading, I would call this symbol or similar.

mypy/plugin.py Outdated
args = []

for name, table in info.names.items():
if table.type:
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be treated differently. You should look at the right hand sides for assignments in the class and take those that have attr.ib (note however, that I am not an expert in attrs so I would like to see many tests for this reviewed by an attrs maintainer).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If possible I'd like to remove all this code from the PR. The actual plugin will require possibly more thought. Is it weird to just have a commit that creates a plugin but doesn't actually use it?

Also it turns out I can't actually use a plugin unless attr.pyi exists. So I'd have to figure out how to bring in python-attrs/attrs#238

Copy link
Member

Choose a reason for hiding this comment

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

If possible I'd like to remove all this code from the PR. The actual plugin will require possibly more thought. Is it weird to just have a commit that creates a plugin but doesn't actually use it?

It is totally OK. I would say it is even preferable to split this PR in two parts:

  • new plugin API
  • actual attrs plugin (with tests)

mypy/semanal.py Outdated
hook = self.plugin.get_class_base_hook(type_info.type.fullname())
if hook:
hook(ClassDefContext(defn, self))

Copy link
Member

Choose a reason for hiding this comment

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

I would factor out this whole block into a separate method, and call it here like self.apply_plugin_hooks(defn) or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. Will do.

@euresti
Copy link
Contributor Author

euresti commented Dec 11, 2017

All right. I cleaned up the code and everything passes. I removed the actual plugin to implement in a different PR.

@euresti euresti changed the title RFC: Add Class Decorator/Metaclass/Base Class plugin Add Class Decorator/Metaclass/Base Class plugin Dec 11, 2017
mypy/semanal.py Outdated
"""Apply a plugin hook that may infer a more precise definition for a class."""
def get_fullname(expr: Expression) -> Optional[str]:
# We support @foo.bar(...) @foo.bar and @bar
# TODO: Support IndexExpressions?
Copy link
Member

Choose a reason for hiding this comment

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

IndexExpression is prohibited in decorators by the Python parser.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But not base classes which also uses this.

Copy link
Member

Choose a reason for hiding this comment

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

For base classes yes, this is important, since they can be generic.

@euresti
Copy link
Contributor Author

euresti commented Dec 12, 2017

Added correct base class support ... broke the world. Sigh.

mypy/semanal.py Outdated
sym = self.lookup_qualified(base.name, base)
if sym is None or sym.node is None:
continue
base_name = sym.node.fullname()
Copy link
Member

Choose a reason for hiding this comment

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

Why this is more complex than in other cases? I would expect you can just extract the name using get_full_name (if you would add one more case for IndexExpr).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of this:

    # Base class expressions (not semantically analyzed -- can be arbitrary expressions)
    base_type_exprs = None  # type: List[Expression]

If I ask for the expr.base.fullname of class Foo(Mapping[str, str]) I get Mapping instead of typing.Mapping

mypy/semanal.py Outdated
# We support foo.bar(...), foo.bar, and bar.
if isinstance(expr, CallExpr):
if isinstance(expr.callee, RefExpr):
return expr.callee.fullname
Copy link
Member

Choose a reason for hiding this comment

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

RefExpr can be both NameExpr and MemberExpr. Why do you have the same code for both here, while below you need special casing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I should probably just recurse.

@euresti
Copy link
Contributor Author

euresti commented Dec 13, 2017

Ok. Tests pass and IndexExpr is parsed. Let me know if this needs anything else.

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.

Thanks! This is almost ready, only some style comments.

mypy/semanal.py Outdated
def get_fullname(expr: Expression) -> Optional[str]:
if isinstance(expr, CallExpr):
return get_fullname(expr.callee)
elif isinstance(expr, (MemberExpr, NameExpr)):
Copy link
Member

Choose a reason for hiding this comment

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

Replace (MemberExpr, NameExpr) with RefExpr.

mypy/semanal.py Outdated
return expr.fullname

# If we don't have a fullname look it up.
sym = self.lookup_type_node(expr)
Copy link
Member

Choose a reason for hiding this comment

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

Add a longer comment here explaining that currently base classes are analysed in different manner than other expressions (analysed by exprtotype.py etc.) and therefore some AST nodes doesn't have names set. Maybe also add a TODO item to unify this (this is however low priority since this will require major rewriting with minor benefits).

mypy/semanal.py Outdated
sym = self.lookup_type_node(expr)
if sym:
return sym.fullname
elif isinstance(expr, IndexExpr):
Copy link
Member

Choose a reason for hiding this comment

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

Move this branch after CallExpr so the order will look more logical.

"""Interface for accessing semantic analyzer functionality in plugins."""

@abstractmethod
def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
Copy link
Member

Choose a reason for hiding this comment

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

I think we can keep only these methods, and then add more in your PR with the actual plugin (if necessary).

mypy/semanal.py Outdated
elif isinstance(expr, (MemberExpr, NameExpr)):
if expr.fullname:
return expr.fullname

Copy link
Member

Choose a reason for hiding this comment

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

I think this empty line is not needed here.

mypy/plugin.py Outdated
) -> Optional[Callable[[ClassDefContext], None]]:
return None

def get_class_metaclass_hook(self, fullname: str
Copy link
Member

Choose a reason for hiding this comment

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

I think you can call this just get_metaclass_hook.

mypy/plugin.py Outdated
) -> Optional[Callable[[ClassDefContext], None]]:
return None

def get_class_base_hook(self, fullname: str
Copy link
Contributor Author

Choose a reason for hiding this comment

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

get_base_class_hook maybe?

Copy link
Member

Choose a reason for hiding this comment

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

get_base_class_hook maybe?

As you prefer, both are fine with me.

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.

I am going to merge this soon, unless there are some objections.

@gvanrossum
Copy link
Member

gvanrossum commented Dec 14, 2017 via email

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

Since the plugins are run in the second pass of semantic analysis, I wonder how they will interact with things that happen later during semantic analysis? This might affect forward references at least. It's worth testing this at some point (and maybe writing test cases -- see below). This can happen in a later PR, but it's worth creating an issue to track this.

It's possible to write tests focused on the plugin architecture. It would be nice to have at least a few tests not specific to any real plugin. The file test-data/unit/check-custom-plugin.test has the existing test cases. This can happen in a separate PR.

Maybe rename AnalyzerPluginInterface to TypeAnalyzerPluginInterface to make it clear how it's different from the new ABC?

And sorry for the late minute review!

@ilevkivskyi
Copy link
Member

@JukkaL

And sorry for the late minute review!

No problem, thanks! I have opened #4363 to track the tests.

I wonder how they will interact with things that happen later during semantic analysis? This might affect forward references at least.

Yes, it makes to have tests specifically for forward references. I created a separate issue for this #4364

@euresti
Could you please make this renaming?

Maybe rename AnalyzerPluginInterface to TypeAnalyzerPluginInterface to make it clear how it's different from the new ABC?

@euresti
Copy link
Contributor Author

euresti commented Dec 14, 2017

Renamed!

@ilevkivskyi ilevkivskyi merged commit 29ba885 into python:master Dec 14, 2017
@euresti euresti deleted the class_decorator_plugin branch December 27, 2017 04:16
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.

4 participants