Coverage for C:\Repos\ekr-pylint\pylint\utils\utils.py: 32%

197 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html 

2# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE 

3# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt 

4 

5from __future__ import annotations 

6 

7try: 

8 import isort.api 

9 

10 HAS_ISORT_5 = True 

11except ImportError: # isort < 5 

12 import isort 

13 

14 HAS_ISORT_5 = False 

15 

16import argparse 

17import codecs 

18import os 

19import re 

20import sys 

21import textwrap 

22import tokenize 

23import warnings 

24from collections.abc import Sequence 

25from io import BufferedReader, BytesIO 

26from typing import ( 

27 TYPE_CHECKING, 

28 Any, 

29 List, 

30 Pattern, 

31 TextIO, 

32 Tuple, 

33 TypeVar, 

34 Union, 

35 overload, 

36) 

37 

38from astroid import Module, modutils, nodes 

39 

40from pylint.constants import PY_EXTS 

41from pylint.typing import OptionDict 

42 

43if sys.version_info >= (3, 8): 

44 from typing import Literal 

45else: 

46 from typing_extensions import Literal 

47 

48if TYPE_CHECKING: 

49 from pylint.checkers.base_checker import BaseChecker 

50 from pylint.lint import PyLinter 

51 

52DEFAULT_LINE_LENGTH = 79 

53 

54# These are types used to overload get_global_option() and refer to the options type 

55GLOBAL_OPTION_BOOL = Literal[ 

56 "suggestion-mode", 

57 "analyse-fallback-blocks", 

58 "allow-global-unused-variables", 

59] 

60GLOBAL_OPTION_INT = Literal["max-line-length", "docstring-min-length"] 

61GLOBAL_OPTION_LIST = Literal["ignored-modules"] 

62GLOBAL_OPTION_PATTERN = Literal[ 

63 "no-docstring-rgx", 

64 "dummy-variables-rgx", 

65 "ignored-argument-names", 

66 "mixin-class-rgx", 

67] 

68GLOBAL_OPTION_PATTERN_LIST = Literal["exclude-too-few-public-methods", "ignore-paths"] 

69GLOBAL_OPTION_TUPLE_INT = Literal["py-version"] 

70GLOBAL_OPTION_NAMES = Union[ 

71 GLOBAL_OPTION_BOOL, 

72 GLOBAL_OPTION_INT, 

73 GLOBAL_OPTION_LIST, 

74 GLOBAL_OPTION_PATTERN, 

75 GLOBAL_OPTION_PATTERN_LIST, 

76 GLOBAL_OPTION_TUPLE_INT, 

77] 

78T_GlobalOptionReturnTypes = TypeVar( 

79 "T_GlobalOptionReturnTypes", 

80 bool, 

81 int, 

82 List[str], 

83 Pattern[str], 

84 List[Pattern[str]], 

85 Tuple[int, ...], 

86) 

87 

88 

89def normalize_text( 

90 text: str, line_len: int = DEFAULT_LINE_LENGTH, indent: str = "" 

91) -> str: 

92 """Wrap the text on the given line length.""" 

93 return "\n".join( 

94 textwrap.wrap( 

95 text, width=line_len, initial_indent=indent, subsequent_indent=indent 

96 ) 

97 ) 

98 

99 

100CMPS = ["=", "-", "+"] 

101 

102 

103# py3k has no more cmp builtin 

104def cmp(a: int | float, b: int | float) -> int: 

105 return (a > b) - (a < b) 

106 

107 

108def diff_string(old: int | float, new: int | float) -> str: 

109 """Given an old and new int value, return a string representing the 

110 difference. 

111 """ 

112 diff = abs(old - new) 

113 diff_str = f"{CMPS[cmp(old, new)]}{diff and f'{diff:.2f}' or ''}" 

114 return diff_str 

115 

116 

117def get_module_and_frameid(node: nodes.NodeNG) -> tuple[str, str]: 

118 """Return the module name and the frame id in the module.""" 

119 frame = node.frame(future=True) 

120 module, obj = "", [] 

121 while frame: 

122 if isinstance(frame, Module): 

123 module = frame.name 

124 else: 

125 obj.append(getattr(frame, "name", "<lambda>")) 

126 try: 

127 frame = frame.parent.frame(future=True) 

128 except AttributeError: 

129 break 

130 obj.reverse() 

131 return module, ".".join(obj) 

132 

133 

134def get_rst_title(title: str, character: str) -> str: 

135 """Permit to get a title formatted as ReStructuredText test (underlined with a chosen character).""" 

136 return f"{title}\n{character * len(title)}\n" 

137 

138 

139def get_rst_section( 

140 section: str | None, 

141 options: list[tuple[str, OptionDict, Any]], 

142 doc: str | None = None, 

143) -> str: 

144 """Format an option's section using as a ReStructuredText formatted output.""" 

145 result = "" 

146 if section: 

147 result += get_rst_title(section, "'") 

148 if doc: 

149 formatted_doc = normalize_text(doc) 

150 result += f"{formatted_doc}\n\n" 

151 for optname, optdict, value in options: 

152 help_opt = optdict.get("help") 

153 result += f":{optname}:\n" 

154 if help_opt: 

155 assert isinstance(help_opt, str) 

156 formatted_help = normalize_text(help_opt, indent=" ") 

157 result += f"{formatted_help}\n" 

158 if value and optname != "py-version": 

159 value = str(_format_option_value(optdict, value)) 

160 result += f"\n Default: ``{value.replace('`` ', '```` ``')}``\n" 

161 return result 

162 

163 

164def decoding_stream( 

165 stream: BufferedReader | BytesIO, 

166 encoding: str, 

167 errors: Literal["strict"] = "strict", 

168) -> codecs.StreamReader: 

169 try: 

170 reader_cls = codecs.getreader(encoding or sys.getdefaultencoding()) 

171 except LookupError: 

172 reader_cls = codecs.getreader(sys.getdefaultencoding()) 

173 return reader_cls(stream, errors) 

174 

175 

176def tokenize_module(node: nodes.Module) -> list[tokenize.TokenInfo]: 

177 with node.stream() as stream: 

178 readline = stream.readline 

179 return list(tokenize.tokenize(readline)) 

180 

181 

182def register_plugins(linter: PyLinter, directory: str) -> None: 

183 """Load all module and package in the given directory, looking for a 

184 'register' function in each one, used to register pylint checkers. 

185 """ 

186 imported = {} 

187 for filename in os.listdir(directory): 

188 base, extension = os.path.splitext(filename) 

189 if base in imported or base == "__pycache__": 

190 continue 

191 if ( 

192 extension in PY_EXTS 

193 and base != "__init__" 

194 or ( 

195 not extension 

196 and os.path.isdir(os.path.join(directory, base)) 

197 and not filename.startswith(".") 

198 ) 

199 ): 

200 try: 

201 module = modutils.load_module_from_file( 

202 os.path.join(directory, filename) 

203 ) 

204 except ValueError: 

205 # empty module name (usually Emacs auto-save files) 

206 continue 

207 except ImportError as exc: 

208 print(f"Problem importing module {filename}: {exc}", file=sys.stderr) 

209 else: 

210 if hasattr(module, "register"): 

211 module.register(linter) 

212 imported[base] = 1 

213 

214 

215@overload 

216def get_global_option( 

217 checker: BaseChecker, option: GLOBAL_OPTION_BOOL, default: bool | None = ... 

218) -> bool: 

219 ... 

220 

221 

222@overload 

223def get_global_option( 

224 checker: BaseChecker, option: GLOBAL_OPTION_INT, default: int | None = ... 

225) -> int: 

226 ... 

227 

228 

229@overload 

230def get_global_option( 

231 checker: BaseChecker, 

232 option: GLOBAL_OPTION_LIST, 

233 default: list[str] | None = ..., 

234) -> list[str]: 

235 ... 

236 

237 

238@overload 

239def get_global_option( 

240 checker: BaseChecker, 

241 option: GLOBAL_OPTION_PATTERN, 

242 default: Pattern[str] | None = ..., 

243) -> Pattern[str]: 

244 ... 

245 

246 

247@overload 

248def get_global_option( 

249 checker: BaseChecker, 

250 option: GLOBAL_OPTION_PATTERN_LIST, 

251 default: list[Pattern[str]] | None = ..., 

252) -> list[Pattern[str]]: 

253 ... 

254 

255 

256@overload 

257def get_global_option( 

258 checker: BaseChecker, 

259 option: GLOBAL_OPTION_TUPLE_INT, 

260 default: tuple[int, ...] | None = ..., 

261) -> tuple[int, ...]: 

262 ... 

263 

264 

265def get_global_option( 

266 checker: BaseChecker, 

267 option: GLOBAL_OPTION_NAMES, 

268 default: T_GlobalOptionReturnTypes | None = None, # pylint: disable=unused-argument 

269) -> T_GlobalOptionReturnTypes | None | Any: 

270 """DEPRECATED: Retrieve an option defined by the given *checker* or 

271 by all known option providers. 

272 

273 It will look in the list of all options providers 

274 until the given *option* will be found. 

275 If the option wasn't found, the *default* value will be returned. 

276 """ 

277 warnings.warn( 

278 "get_global_option has been deprecated. You can use " 

279 "checker.linter.config to get all global options instead.", 

280 DeprecationWarning, 

281 ) 

282 return getattr(checker.linter.config, option.replace("-", "_")) 

283 

284 

285def _splitstrip(string: str, sep: str = ",") -> list[str]: 

286 """Return a list of stripped string by splitting the string given as 

287 argument on `sep` (',' by default), empty strings are discarded. 

288 

289 >>> _splitstrip('a, b, c , 4,,') 

290 ['a', 'b', 'c', '4'] 

291 >>> _splitstrip('a') 

292 ['a'] 

293 >>> _splitstrip('a,\nb,\nc,') 

294 ['a', 'b', 'c'] 

295 

296 :type string: str or unicode 

297 :param string: a csv line 

298 

299 :type sep: str or unicode 

300 :param sep: field separator, default to the comma (',') 

301 

302 :rtype: str or unicode 

303 :return: the unquoted string (or the input string if it wasn't quoted) 

304 """ 

305 return [word.strip() for word in string.split(sep) if word.strip()] 

306 

307 

308def _unquote(string: str) -> str: 

309 """Remove optional quotes (simple or double) from the string. 

310 

311 :param string: an optionally quoted string 

312 :return: the unquoted string (or the input string if it wasn't quoted) 

313 """ 

314 if not string: 

315 return string 

316 if string[0] in "\"'": 

317 string = string[1:] 

318 if string[-1] in "\"'": 

319 string = string[:-1] 

320 return string 

321 

322 

323def _check_csv(value: list[str] | tuple[str] | str) -> Sequence[str]: 

324 if isinstance(value, (list, tuple)): 

325 return value 

326 return _splitstrip(value) 

327 

328 

329def _comment(string: str) -> str: 

330 """Return string as a comment.""" 

331 lines = [line.strip() for line in string.splitlines()] 

332 sep = "\n" 

333 return "# " + f"{sep}# ".join(lines) 

334 

335 

336def _format_option_value(optdict: OptionDict, value: Any) -> str: 

337 """Return the user input's value from a 'compiled' value. 

338 

339 TODO: 3.0: Remove deprecated function 

340 """ 

341 if optdict.get("type", None) == "py_version": 

342 value = ".".join(str(item) for item in value) 

343 elif isinstance(value, (list, tuple)): 

344 value = ",".join(_format_option_value(optdict, item) for item in value) 

345 elif isinstance(value, dict): 

346 value = ",".join(f"{k}:{v}" for k, v in value.items()) 

347 elif hasattr(value, "match"): # optdict.get('type') == 'regexp' 

348 # compiled regexp 

349 value = value.pattern 

350 elif optdict.get("type") == "yn": 

351 value = "yes" if value else "no" 

352 elif isinstance(value, str) and value.isspace(): 

353 value = f"'{value}'" 

354 return str(value) 

355 

356 

357def format_section( 

358 stream: TextIO, 

359 section: str, 

360 options: list[tuple[str, OptionDict, Any]], 

361 doc: str | None = None, 

362) -> None: 

363 """Format an option's section using the INI format.""" 

364 warnings.warn( 

365 "format_section has been deprecated. It will be removed in pylint 3.0.", 

366 DeprecationWarning, 

367 ) 

368 if doc: 

369 print(_comment(doc), file=stream) 

370 print(f"[{section}]", file=stream) 

371 with warnings.catch_warnings(): 

372 warnings.filterwarnings("ignore", category=DeprecationWarning) 

373 _ini_format(stream, options) 

374 

375 

376def _ini_format(stream: TextIO, options: list[tuple[str, OptionDict, Any]]) -> None: 

377 """Format options using the INI format.""" 

378 warnings.warn( 

379 "_ini_format has been deprecated. It will be removed in pylint 3.0.", 

380 DeprecationWarning, 

381 ) 

382 for optname, optdict, value in options: 

383 # Skip deprecated option 

384 if "kwargs" in optdict: 

385 assert isinstance(optdict["kwargs"], dict) 

386 if "new_names" in optdict["kwargs"]: 

387 continue 

388 value = _format_option_value(optdict, value) 

389 help_opt = optdict.get("help") 

390 if help_opt: 

391 assert isinstance(help_opt, str) 

392 help_opt = normalize_text(help_opt, indent="# ") 

393 print(file=stream) 

394 print(help_opt, file=stream) 

395 else: 

396 print(file=stream) 

397 if value in {"None", "False"}: 

398 print(f"#{optname}=", file=stream) 

399 else: 

400 value = str(value).strip() 

401 if re.match(r"^([\w-]+,)+[\w-]+$", str(value)): 

402 separator = "\n " + " " * len(optname) 

403 value = separator.join(x + "," for x in str(value).split(",")) 

404 # remove trailing ',' from last element of the list 

405 value = value[:-1] 

406 print(f"{optname}={value}", file=stream) 

407 

408 

409class IsortDriver: 

410 """A wrapper around isort API that changed between versions 4 and 5.""" 

411 

412 def __init__(self, config: argparse.Namespace) -> None: 

413 if HAS_ISORT_5: 

414 self.isort5_config = isort.api.Config( 

415 # There is no typo here. EXTRA_standard_library is 

416 # what most users want. The option has been named 

417 # KNOWN_standard_library for ages in pylint, and we 

418 # don't want to break compatibility. 

419 extra_standard_library=config.known_standard_library, 

420 known_third_party=config.known_third_party, 

421 ) 

422 else: 

423 # pylint: disable-next=no-member 

424 self.isort4_obj = isort.SortImports( 

425 file_contents="", 

426 known_standard_library=config.known_standard_library, 

427 known_third_party=config.known_third_party, 

428 ) 

429 

430 def place_module(self, package: str) -> str: 

431 if HAS_ISORT_5: 

432 return isort.api.place_module(package, self.isort5_config) 

433 return self.isort4_obj.place_module(package)