1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
4
5"""Astroid hooks for six module."""
6
7from textwrap import dedent
8
9from astroid import nodes
10from astroid.brain.helpers import register_module_extender
11from astroid.builder import AstroidBuilder
12from astroid.exceptions import (
13 AstroidBuildingError,
14 AttributeInferenceError,
15 InferenceError,
16)
17from astroid.manager import AstroidManager
18
19SIX_ADD_METACLASS = "six.add_metaclass"
20SIX_WITH_METACLASS = "six.with_metaclass"
21
22
23def default_predicate(line):
24 return line.strip()
25
26
27def _indent(text, prefix, predicate=default_predicate) -> str:
28 """Adds 'prefix' to the beginning of selected lines in 'text'.
29
30 If 'predicate' is provided, 'prefix' will only be added to the lines
31 where 'predicate(line)' is True. If 'predicate' is not provided,
32 it will default to adding 'prefix' to all non-empty lines that do not
33 consist solely of whitespace characters.
34 """
35
36 def prefixed_lines():
37 for line in text.splitlines(True):
38 yield prefix + line if predicate(line) else line
39
40 return "".join(prefixed_lines())
41
42
43_IMPORTS = """
44import _io
45cStringIO = _io.StringIO
46filter = filter
47from itertools import filterfalse
48input = input
49from sys import intern
50map = map
51range = range
52from importlib import reload
53reload_module = lambda module: reload(module)
54from functools import reduce
55from shlex import quote as shlex_quote
56from io import StringIO
57from collections import UserDict, UserList, UserString
58xrange = range
59zip = zip
60from itertools import zip_longest
61import builtins
62import configparser
63import copyreg
64import _dummy_thread
65import http.cookiejar as http_cookiejar
66import http.cookies as http_cookies
67import html.entities as html_entities
68import html.parser as html_parser
69import http.client as http_client
70import http.server as http_server
71BaseHTTPServer = CGIHTTPServer = SimpleHTTPServer = http.server
72import pickle as cPickle
73import queue
74import reprlib
75import socketserver
76import _thread
77import winreg
78import xmlrpc.server as xmlrpc_server
79import xmlrpc.client as xmlrpc_client
80import urllib.robotparser as urllib_robotparser
81import email.mime.multipart as email_mime_multipart
82import email.mime.nonmultipart as email_mime_nonmultipart
83import email.mime.text as email_mime_text
84import email.mime.base as email_mime_base
85import urllib.parse as urllib_parse
86import urllib.error as urllib_error
87import tkinter
88import tkinter.dialog as tkinter_dialog
89import tkinter.filedialog as tkinter_filedialog
90import tkinter.scrolledtext as tkinter_scrolledtext
91import tkinter.simpledialog as tkinder_simpledialog
92import tkinter.tix as tkinter_tix
93import tkinter.ttk as tkinter_ttk
94import tkinter.constants as tkinter_constants
95import tkinter.dnd as tkinter_dnd
96import tkinter.colorchooser as tkinter_colorchooser
97import tkinter.commondialog as tkinter_commondialog
98import tkinter.filedialog as tkinter_tkfiledialog
99import tkinter.font as tkinter_font
100import tkinter.messagebox as tkinter_messagebox
101import urllib
102import urllib.request as urllib_request
103import urllib.robotparser as urllib_robotparser
104import urllib.parse as urllib_parse
105import urllib.error as urllib_error
106"""
107
108
109def six_moves_transform():
110 code = dedent("""
111 class Moves(object):
112 {}
113 moves = Moves()
114 """).format(_indent(_IMPORTS, " "))
115 module = AstroidBuilder(AstroidManager()).string_build(code)
116 module.name = "six.moves"
117 return module
118
119
120def _six_fail_hook(modname):
121 """Fix six.moves imports due to the dynamic nature of this
122 class.
123
124 Construct a pseudo-module which contains all the necessary imports
125 for six
126
127 :param modname: Name of failed module
128 :type modname: str
129
130 :return: An astroid module
131 :rtype: nodes.Module
132 """
133
134 attribute_of = modname != "six.moves" and modname.startswith("six.moves")
135 if modname != "six.moves" and not attribute_of:
136 raise AstroidBuildingError(modname=modname)
137 module = AstroidBuilder(AstroidManager()).string_build(_IMPORTS)
138 module.name = "six.moves"
139 if attribute_of:
140 # Facilitate import of submodules in Moves
141 start_index = len(module.name)
142 attribute = modname[start_index:].lstrip(".").replace(".", "_")
143 try:
144 import_attr = module.getattr(attribute)[0]
145 except AttributeInferenceError as exc:
146 raise AstroidBuildingError(modname=modname) from exc
147 if isinstance(import_attr, nodes.Import):
148 submodule = AstroidManager().ast_from_module_name(import_attr.names[0][0])
149 return submodule
150 # Let dummy submodule imports pass through
151 # This will cause an Uninferable result, which is okay
152 return module
153
154
155def _looks_like_decorated_with_six_add_metaclass(node) -> bool:
156 if not node.decorators:
157 return False
158
159 for decorator in node.decorators.nodes:
160 if not isinstance(decorator, nodes.Call):
161 continue
162 if decorator.func.as_string() == SIX_ADD_METACLASS:
163 return True
164 return False
165
166
167def transform_six_add_metaclass(node): # pylint: disable=inconsistent-return-statements
168 """Check if the given class node is decorated with *six.add_metaclass*.
169
170 If so, inject its argument as the metaclass of the underlying class.
171 """
172 if not node.decorators:
173 return
174
175 for decorator in node.decorators.nodes:
176 if not isinstance(decorator, nodes.Call):
177 continue
178
179 try:
180 func = next(decorator.func.infer())
181 except (InferenceError, StopIteration):
182 continue
183 if (
184 isinstance(func, (nodes.FunctionDef, nodes.ClassDef))
185 and func.qname() == SIX_ADD_METACLASS
186 and decorator.args
187 ):
188 metaclass = decorator.args[0]
189 node._metaclass = metaclass
190 return node
191 return
192
193
194def _looks_like_nested_from_six_with_metaclass(node) -> bool:
195 if len(node.bases) != 1:
196 return False
197 base = node.bases[0]
198 if not isinstance(base, nodes.Call):
199 return False
200 try:
201 if hasattr(base.func, "expr"):
202 # format when explicit 'six.with_metaclass' is used
203 mod = base.func.expr.name
204 func = base.func.attrname
205 func = f"{mod}.{func}"
206 else:
207 # format when 'with_metaclass' is used directly (local import from six)
208 # check reference module to avoid 'with_metaclass' name clashes
209 mod = base.parent.parent
210 import_from = mod.locals["with_metaclass"][0]
211 func = f"{import_from.modname}.{base.func.name}"
212 except (AttributeError, KeyError, IndexError):
213 return False
214 return func == SIX_WITH_METACLASS
215
216
217def transform_six_with_metaclass(node):
218 """Check if the given class node is defined with *six.with_metaclass*.
219
220 If so, inject its argument as the metaclass of the underlying class.
221 """
222 call = node.bases[0]
223 node._metaclass = call.args[0]
224 return node
225
226
227def register(manager: AstroidManager) -> None:
228 register_module_extender(manager, "six", six_moves_transform)
229 register_module_extender(
230 manager, "requests.packages.urllib3.packages.six", six_moves_transform
231 )
232 manager.register_failed_import_hook(_six_fail_hook)
233 manager.register_transform(
234 nodes.ClassDef,
235 transform_six_add_metaclass,
236 _looks_like_decorated_with_six_add_metaclass,
237 )
238 manager.register_transform(
239 nodes.ClassDef,
240 transform_six_with_metaclass,
241 _looks_like_nested_from_six_with_metaclass,
242 )