| # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html |
| # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt |
| |
| """Astroid hooks for six module.""" |
| |
| from textwrap import dedent |
| |
| from astroid import nodes |
| from astroid.brain.helpers import register_module_extender |
| from astroid.builder import AstroidBuilder |
| from astroid.exceptions import ( |
| AstroidBuildingError, |
| AttributeInferenceError, |
| InferenceError, |
| ) |
| from astroid.manager import AstroidManager |
| |
| SIX_ADD_METACLASS = "six.add_metaclass" |
| SIX_WITH_METACLASS = "six.with_metaclass" |
| |
| |
| def default_predicate(line): |
| return line.strip() |
| |
| |
| def _indent(text, prefix, predicate=default_predicate) -> str: |
| """Adds 'prefix' to the beginning of selected lines in 'text'. |
| |
| If 'predicate' is provided, 'prefix' will only be added to the lines |
| where 'predicate(line)' is True. If 'predicate' is not provided, |
| it will default to adding 'prefix' to all non-empty lines that do not |
| consist solely of whitespace characters. |
| """ |
| |
| def prefixed_lines(): |
| for line in text.splitlines(True): |
| yield prefix + line if predicate(line) else line |
| |
| return "".join(prefixed_lines()) |
| |
| |
| _IMPORTS = """ |
| import _io |
| cStringIO = _io.StringIO |
| filter = filter |
| from itertools import filterfalse |
| input = input |
| from sys import intern |
| map = map |
| range = range |
| from importlib import reload |
| reload_module = lambda module: reload(module) |
| from functools import reduce |
| from shlex import quote as shlex_quote |
| from io import StringIO |
| from collections import UserDict, UserList, UserString |
| xrange = range |
| zip = zip |
| from itertools import zip_longest |
| import builtins |
| import configparser |
| import copyreg |
| import _dummy_thread |
| import http.cookiejar as http_cookiejar |
| import http.cookies as http_cookies |
| import html.entities as html_entities |
| import html.parser as html_parser |
| import http.client as http_client |
| import http.server as http_server |
| BaseHTTPServer = CGIHTTPServer = SimpleHTTPServer = http.server |
| import pickle as cPickle |
| import queue |
| import reprlib |
| import socketserver |
| import _thread |
| import winreg |
| import xmlrpc.server as xmlrpc_server |
| import xmlrpc.client as xmlrpc_client |
| import urllib.robotparser as urllib_robotparser |
| import email.mime.multipart as email_mime_multipart |
| import email.mime.nonmultipart as email_mime_nonmultipart |
| import email.mime.text as email_mime_text |
| import email.mime.base as email_mime_base |
| import urllib.parse as urllib_parse |
| import urllib.error as urllib_error |
| import tkinter |
| import tkinter.dialog as tkinter_dialog |
| import tkinter.filedialog as tkinter_filedialog |
| import tkinter.scrolledtext as tkinter_scrolledtext |
| import tkinter.simpledialog as tkinder_simpledialog |
| import tkinter.tix as tkinter_tix |
| import tkinter.ttk as tkinter_ttk |
| import tkinter.constants as tkinter_constants |
| import tkinter.dnd as tkinter_dnd |
| import tkinter.colorchooser as tkinter_colorchooser |
| import tkinter.commondialog as tkinter_commondialog |
| import tkinter.filedialog as tkinter_tkfiledialog |
| import tkinter.font as tkinter_font |
| import tkinter.messagebox as tkinter_messagebox |
| import urllib |
| import urllib.request as urllib_request |
| import urllib.robotparser as urllib_robotparser |
| import urllib.parse as urllib_parse |
| import urllib.error as urllib_error |
| """ |
| |
| |
| def six_moves_transform(): |
| code = dedent( |
| """ |
| class Moves(object): |
| {} |
| moves = Moves() |
| """ |
| ).format(_indent(_IMPORTS, " ")) |
| module = AstroidBuilder(AstroidManager()).string_build(code) |
| module.name = "six.moves" |
| return module |
| |
| |
| def _six_fail_hook(modname): |
| """Fix six.moves imports due to the dynamic nature of this |
| class. |
| |
| Construct a pseudo-module which contains all the necessary imports |
| for six |
| |
| :param modname: Name of failed module |
| :type modname: str |
| |
| :return: An astroid module |
| :rtype: nodes.Module |
| """ |
| |
| attribute_of = modname != "six.moves" and modname.startswith("six.moves") |
| if modname != "six.moves" and not attribute_of: |
| raise AstroidBuildingError(modname=modname) |
| module = AstroidBuilder(AstroidManager()).string_build(_IMPORTS) |
| module.name = "six.moves" |
| if attribute_of: |
| # Facilitate import of submodules in Moves |
| start_index = len(module.name) |
| attribute = modname[start_index:].lstrip(".").replace(".", "_") |
| try: |
| import_attr = module.getattr(attribute)[0] |
| except AttributeInferenceError as exc: |
| raise AstroidBuildingError(modname=modname) from exc |
| if isinstance(import_attr, nodes.Import): |
| submodule = AstroidManager().ast_from_module_name(import_attr.names[0][0]) |
| return submodule |
| # Let dummy submodule imports pass through |
| # This will cause an Uninferable result, which is okay |
| return module |
| |
| |
| def _looks_like_decorated_with_six_add_metaclass(node) -> bool: |
| if not node.decorators: |
| return False |
| |
| for decorator in node.decorators.nodes: |
| if not isinstance(decorator, nodes.Call): |
| continue |
| if decorator.func.as_string() == SIX_ADD_METACLASS: |
| return True |
| return False |
| |
| |
| def transform_six_add_metaclass(node): # pylint: disable=inconsistent-return-statements |
| """Check if the given class node is decorated with *six.add_metaclass*. |
| |
| If so, inject its argument as the metaclass of the underlying class. |
| """ |
| if not node.decorators: |
| return |
| |
| for decorator in node.decorators.nodes: |
| if not isinstance(decorator, nodes.Call): |
| continue |
| |
| try: |
| func = next(decorator.func.infer()) |
| except (InferenceError, StopIteration): |
| continue |
| if ( |
| isinstance(func, (nodes.FunctionDef, nodes.ClassDef)) |
| and func.qname() == SIX_ADD_METACLASS |
| and decorator.args |
| ): |
| metaclass = decorator.args[0] |
| node._metaclass = metaclass |
| return node |
| return |
| |
| |
| def _looks_like_nested_from_six_with_metaclass(node) -> bool: |
| if len(node.bases) != 1: |
| return False |
| base = node.bases[0] |
| if not isinstance(base, nodes.Call): |
| return False |
| try: |
| if hasattr(base.func, "expr"): |
| # format when explicit 'six.with_metaclass' is used |
| mod = base.func.expr.name |
| func = base.func.attrname |
| func = f"{mod}.{func}" |
| else: |
| # format when 'with_metaclass' is used directly (local import from six) |
| # check reference module to avoid 'with_metaclass' name clashes |
| mod = base.parent.parent |
| import_from = mod.locals["with_metaclass"][0] |
| func = f"{import_from.modname}.{base.func.name}" |
| except (AttributeError, KeyError, IndexError): |
| return False |
| return func == SIX_WITH_METACLASS |
| |
| |
| def transform_six_with_metaclass(node): |
| """Check if the given class node is defined with *six.with_metaclass*. |
| |
| If so, inject its argument as the metaclass of the underlying class. |
| """ |
| call = node.bases[0] |
| node._metaclass = call.args[0] |
| return node |
| |
| |
| def register(manager: AstroidManager) -> None: |
| register_module_extender(manager, "six", six_moves_transform) |
| register_module_extender( |
| manager, "requests.packages.urllib3.packages.six", six_moves_transform |
| ) |
| manager.register_failed_import_hook(_six_fail_hook) |
| manager.register_transform( |
| nodes.ClassDef, |
| transform_six_add_metaclass, |
| _looks_like_decorated_with_six_add_metaclass, |
| ) |
| manager.register_transform( |
| nodes.ClassDef, |
| transform_six_with_metaclass, |
| _looks_like_nested_from_six_with_metaclass, |
| ) |