Source code for attr_utils.mypy_plugin

#!/usr/bin/env python
#
#  mypy_plugin.py
"""
Plugin for `mypy <https://github.com/python/mypy>`_ which adds support for attr_utils.

.. versionadded:: 0.4.0

.. compound::

	To use this plugin, add the following to your
	`mypy configuration file <https://mypy.readthedocs.io/en/stable/config_file.html>`_:

		.. code-block:: ini

			[mypy]
			plugins=attr_utils.mypy_plugin

	See https://mypy.readthedocs.io/en/stable/extending_mypy.html#configuring-mypy-to-use-plugins
	for more information.

.. autosummary-widths:: 7/16
	:html: 2/10

.. automodulesumm:: attr_utils.mypy_plugin
	:autosummary-sections: Classes

.. autosummary-widths:: 1/2
	:html: 4/10

.. automodulesumm:: attr_utils.mypy_plugin
	:autosummary-sections: Functions
"""
#
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#
#  Based on https://github.com/python/mypy/issues/5719
#  and https://gitter.im/python/typing?at=5e078653eac8d1511e737d8c
#
#  Also based on mypy
#  https://github.com/python/mypy
#  Copyright (c) 2015-2019 Jukka Lehtosalo and contributors
#  Licensed under the terms of the MIT license.
#

# stdlib
from typing import Any, Callable, List, MutableMapping, Optional

# 3rd party
from mypy.nodes import (  # nodep
		ARG_OPT,
		ARG_POS,
		MDEF,
		Argument,
		Block,
		ClassDef,
		FuncDef,
		PassStmt,
		SymbolTableNode,
		Var
		)
from mypy.plugin import ClassDefContext, Plugin, SemanticAnalyzerPluginInterface  # nodep
from mypy.plugins.common import add_method_to_class  # nodep
from mypy.semanal_shared import set_callable_name  # nodep
from mypy.types import AnyType, CallableType, Instance, Type, TypeOfAny, TypeType  # nodep
from mypy.typevars import fill_typevars  # nodep
from mypy.util import get_unique_redefinition_name  # nodep
from mypy.version import __version__ as mypy_version  # nodep

__all__ = ["attr_utils_serialise_serde", "AttrUtilsPlugin", "add_classmethod_to_class", "plugin"]

#: Registry mapping decorator full names to the callable that handles the methods added by the decorator.
decorator_registry: MutableMapping[str, Callable[[ClassDefContext], None]] = {}

# ref: https://github.com/python/mypy/pull/11332
_builtins = "builtins" if mypy_version > "0.930" else "__builtins__"


[docs]def attr_utils_serialise_serde(cls_def_ctx: ClassDefContext): """ Handles :func:`attr_utils.serialise.serde`. :param cls_def_ctx: """ info = cls_def_ctx.cls.info # https://gitter.im/python/typing?at=5e078653eac8d1511e737d8c str_type = cls_def_ctx.api.named_type(f"{_builtins}.str") bool_type = cls_def_ctx.api.named_type(f"{_builtins}.bool") implicit_any = AnyType(TypeOfAny.special_form) mapping = cls_def_ctx.api.lookup_fully_qualified_or_none("typing.Mapping") mutable_mapping = cls_def_ctx.api.lookup_fully_qualified_or_none("typing.MutableMapping") mapping_str_any_type = Instance(mapping.node, [str_type, implicit_any]) # type: ignore mutable_mapping_str_any_type = Instance(mutable_mapping.node, [str_type, implicit_any]) # type: ignore # # maybe_mapping_str_any = UnionType.make_union([typ, NoneType()])(mapping_str_any_type) decorated_class_instance = Instance( cls_def_ctx.api.lookup_fully_qualified_or_none(cls_def_ctx.cls.fullname).node, # type: ignore [], ) if "to_dict" not in info.names: add_method_to_class( api=cls_def_ctx.api, cls=cls_def_ctx.cls, name="to_dict", args=[Argument(Var("convert_values", bool_type), bool_type, None, ARG_OPT)], return_type=mutable_mapping_str_any_type, ) if "from_dict" not in info.names: add_classmethod_to_class( api=cls_def_ctx.api, cls=cls_def_ctx.cls, name="from_dict", args=[Argument(Var('d', mapping_str_any_type), mapping_str_any_type, None, ARG_POS)], return_type=decorated_class_instance, cls_type=TypeType(decorated_class_instance), )
# # def attr__make_attrs(cls_def_ctx: ClassDefContext): # """ # Handles :func:attr.ib`. # # :param cls_def_ctx: # """ # # info = cls_def_ctx.cls.info # # list_ = cls_def_ctx.api.lookup_fully_qualified_or_none('typing.List') # attribute = cls_def_ctx.api.lookup_fully_qualified_or_none('attr.Attribute') # # # # if "__attrs_attrs__" not in info.names: # # info.names["__attrs_attrs__"] = SymbolTableNode( # # MDEF, # # Var("__attrs_attrs__", Instance(list_.node, [attribute])), # type: ignore # # plugin_generated=True, # # ) decorator_registry["attr_utils.serialise.serde"] = attr_utils_serialise_serde # decorator_registry["attr._make.attrs"] = attr__make_attrs # decorator_registry["attr.s"] = attr__make_attrs # decorator_registry["attr.attrs"] = attr__make_attrs
[docs]class AttrUtilsPlugin(Plugin): """ Plugin for :github:repo:`mypy <python/mypy>` which adds support for ``attr_utils``. .. autoclasssumm:: AttrUtilsPlugin :autosummary-sections: ;; """
[docs] def get_class_decorator_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]: """ Allows mypy to handle decorators that add extra methods to classes. :param fullname: The full name of the decorator. """ return decorator_registry.get(fullname, None)
[docs]def add_classmethod_to_class( api: SemanticAnalyzerPluginInterface, cls: ClassDef, name: str, args: List[Argument], return_type: Type, cls_type: Optional[Type] = None, tvar_def: Optional[Any] = None, ) -> None: """ Adds a new classmethod to a class definition. :param api: :param cls: :param name: :param args: :param return_type: :param cls_type: :param tvar_def: """ info = cls.info # First remove any previously generated methods with the same name # to avoid clashes and problems in the semantic analyzer. if name in info.names: sym = info.names[name] if sym.plugin_generated and isinstance(sym.node, FuncDef): cls.defs.body.remove(sym.node) cls_type = cls_type or fill_typevars(info) function_type = api.named_type(f"{_builtins}.function") args = [Argument(Var("cls"), cls_type, None, ARG_POS)] + args arg_types, arg_names, arg_kinds = [], [], [] for arg in args: assert arg.type_annotation, "All arguments must be fully typed." arg_types.append(arg.type_annotation) arg_names.append(arg.variable.name) arg_kinds.append(arg.kind) signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type) if tvar_def: signature.variables = [tvar_def] func = FuncDef(name, args, Block([PassStmt()])) func.is_class = True func.info = info func.type = set_callable_name(signature, func) func._fullname = info.fullname + '.' + name func.line = info.line # NOTE: we would like the plugin generated node to dominate, but we still # need to keep any existing definitions so they get semantically analyzed. if name in info.names: # Get a nice unique name instead. r_name = get_unique_redefinition_name(name, info.names) info.names[r_name] = info.names[name] info.names[name] = SymbolTableNode(MDEF, func, plugin_generated=True) info.defn.defs.body.append(func)
[docs]def plugin(version: str): """ Entry point to :mod:`attr_utils.mypy_plugin`. :param version: The current mypy version. """ # ignore version argument if the plugin works with all mypy versions. return AttrUtilsPlugin