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 """
112 class Moves(object):
113 {}
114 moves = Moves()
115 """
116 ).format(_indent(_IMPORTS, " "))
117 module = AstroidBuilder(AstroidManager()).string_build(code)
118 module.name = "six.moves"
119 return module
120
121
122def _six_fail_hook(modname):
123 """Fix six.moves imports due to the dynamic nature of this
124 class.
125
126 Construct a pseudo-module which contains all the necessary imports
127 for six
128
129 :param modname: Name of failed module
130 :type modname: str
131
132 :return: An astroid module
133 :rtype: nodes.Module
134 """
135
136 attribute_of = modname != "six.moves" and modname.startswith("six.moves")
137 if modname != "six.moves" and not attribute_of:
138 raise AstroidBuildingError(modname=modname)
139 module = AstroidBuilder(AstroidManager()).string_build(_IMPORTS)
140 module.name = "six.moves"
141 if attribute_of:
142 # Facilitate import of submodules in Moves
143 start_index = len(module.name)
144 attribute = modname[start_index:].lstrip(".").replace(".", "_")
145 try:
146 import_attr = module.getattr(attribute)[0]
147 except AttributeInferenceError as exc:
148 raise AstroidBuildingError(modname=modname) from exc
149 if isinstance(import_attr, nodes.Import):
150 submodule = AstroidManager().ast_from_module_name(import_attr.names[0][0])
151 return submodule
152 # Let dummy submodule imports pass through
153 # This will cause an Uninferable result, which is okay
154 return module
155
156
157def _looks_like_decorated_with_six_add_metaclass(node) -> bool:
158 if not node.decorators:
159 return False
160
161 for decorator in node.decorators.nodes:
162 if not isinstance(decorator, nodes.Call):
163 continue
164 if decorator.func.as_string() == SIX_ADD_METACLASS:
165 return True
166 return False
167
168
169def transform_six_add_metaclass(node): # pylint: disable=inconsistent-return-statements
170 """Check if the given class node is decorated with *six.add_metaclass*.
171
172 If so, inject its argument as the metaclass of the underlying class.
173 """
174 if not node.decorators:
175 return
176
177 for decorator in node.decorators.nodes:
178 if not isinstance(decorator, nodes.Call):
179 continue
180
181 try:
182 func = next(decorator.func.infer())
183 except (InferenceError, StopIteration):
184 continue
185 if func.qname() == SIX_ADD_METACLASS and decorator.args:
186 metaclass = decorator.args[0]
187 node._metaclass = metaclass
188 return node
189 return
190
191
192def _looks_like_nested_from_six_with_metaclass(node) -> bool:
193 if len(node.bases) != 1:
194 return False
195 base = node.bases[0]
196 if not isinstance(base, nodes.Call):
197 return False
198 try:
199 if hasattr(base.func, "expr"):
200 # format when explicit 'six.with_metaclass' is used
201 mod = base.func.expr.name
202 func = base.func.attrname
203 func = f"{mod}.{func}"
204 else:
205 # format when 'with_metaclass' is used directly (local import from six)
206 # check reference module to avoid 'with_metaclass' name clashes
207 mod = base.parent.parent
208 import_from = mod.locals["with_metaclass"][0]
209 func = f"{import_from.modname}.{base.func.name}"
210 except (AttributeError, KeyError, IndexError):
211 return False
212 return func == SIX_WITH_METACLASS
213
214
215def transform_six_with_metaclass(node):
216 """Check if the given class node is defined with *six.with_metaclass*.
217
218 If so, inject its argument as the metaclass of the underlying class.
219 """
220 call = node.bases[0]
221 node._metaclass = call.args[0]
222 return node
223
224
225def register(manager: AstroidManager) -> None:
226 register_module_extender(manager, "six", six_moves_transform)
227 register_module_extender(
228 manager, "requests.packages.urllib3.packages.six", six_moves_transform
229 )
230 manager.register_failed_import_hook(_six_fail_hook)
231 manager.register_transform(
232 nodes.ClassDef,
233 transform_six_add_metaclass,
234 _looks_like_decorated_with_six_add_metaclass,
235 )
236 manager.register_transform(
237 nodes.ClassDef,
238 transform_six_with_metaclass,
239 _looks_like_nested_from_six_with_metaclass,
240 )