output.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import copy
  2. import itertools
  3. from functools import partial
  4. from typing import Any, Iterable, List, Optional, Set, Tuple, Type
  5. from isort.format import format_simplified
  6. from . import parse, sorting, wrap
  7. from .comments import add_to_line as with_comments
  8. from .identify import STATEMENT_DECLARATIONS
  9. from .settings import DEFAULT_CONFIG, Config
  10. def sorted_imports(
  11. parsed: parse.ParsedContent,
  12. config: Config = DEFAULT_CONFIG,
  13. extension: str = "py",
  14. import_type: str = "import",
  15. ) -> str:
  16. """Adds the imports back to the file.
  17. (at the index of the first import) sorted alphabetically and split between groups
  18. """
  19. if parsed.import_index == -1:
  20. return _output_as_string(parsed.lines_without_imports, parsed.line_separator)
  21. formatted_output: List[str] = parsed.lines_without_imports.copy()
  22. remove_imports = [format_simplified(removal) for removal in config.remove_imports]
  23. sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate)
  24. if config.no_sections:
  25. parsed.imports["no_sections"] = {"straight": {}, "from": {}}
  26. base_sections: Tuple[str, ...] = ()
  27. for section in sections:
  28. if section == "FUTURE":
  29. base_sections = ("FUTURE",)
  30. continue
  31. parsed.imports["no_sections"]["straight"].update(
  32. parsed.imports[section].get("straight", {})
  33. )
  34. parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {}))
  35. sections = base_sections + ("no_sections",)
  36. output: List[str] = []
  37. seen_headings: Set[str] = set()
  38. pending_lines_before = False
  39. for section in sections:
  40. straight_modules = parsed.imports[section]["straight"]
  41. if not config.only_sections:
  42. straight_modules = sorting.sort(
  43. config,
  44. straight_modules,
  45. key=lambda key: sorting.module_key(
  46. key, config, section_name=section, straight_import=True
  47. ),
  48. reverse=config.reverse_sort,
  49. )
  50. from_modules = parsed.imports[section]["from"]
  51. if not config.only_sections:
  52. from_modules = sorting.sort(
  53. config,
  54. from_modules,
  55. key=lambda key: sorting.module_key(key, config, section_name=section),
  56. reverse=config.reverse_sort,
  57. )
  58. if config.star_first:
  59. star_modules = []
  60. other_modules = []
  61. for module in from_modules:
  62. if "*" in parsed.imports[section]["from"][module]:
  63. star_modules.append(module)
  64. else:
  65. other_modules.append(module)
  66. from_modules = star_modules + other_modules
  67. straight_imports = _with_straight_imports(
  68. parsed, config, straight_modules, section, remove_imports, import_type
  69. )
  70. from_imports = _with_from_imports(
  71. parsed, config, from_modules, section, remove_imports, import_type
  72. )
  73. lines_between = [""] * (
  74. config.lines_between_types if from_modules and straight_modules else 0
  75. )
  76. if config.from_first:
  77. section_output = from_imports + lines_between + straight_imports
  78. else:
  79. section_output = straight_imports + lines_between + from_imports
  80. if config.force_sort_within_sections:
  81. # collapse comments
  82. comments_above = []
  83. new_section_output: List[str] = []
  84. for line in section_output:
  85. if not line:
  86. continue
  87. if line.startswith("#"):
  88. comments_above.append(line)
  89. elif comments_above:
  90. new_section_output.append(_LineWithComments(line, comments_above))
  91. comments_above = []
  92. else:
  93. new_section_output.append(line)
  94. # only_sections options is not imposed if force_sort_within_sections is True
  95. new_section_output = sorting.sort(
  96. config,
  97. new_section_output,
  98. key=partial(sorting.section_key, config=config),
  99. reverse=config.reverse_sort,
  100. )
  101. # uncollapse comments
  102. section_output = []
  103. for line in new_section_output:
  104. comments = getattr(line, "comments", ())
  105. if comments:
  106. section_output.extend(comments)
  107. section_output.append(str(line))
  108. section_name = section
  109. no_lines_before = section_name in config.no_lines_before
  110. if section_output:
  111. if section_name in parsed.place_imports:
  112. parsed.place_imports[section_name] = section_output
  113. continue
  114. section_title = config.import_headings.get(section_name.lower(), "")
  115. if section_title and section_title not in seen_headings:
  116. if config.dedup_headings:
  117. seen_headings.add(section_title)
  118. section_comment = f"# {section_title}"
  119. if section_comment not in parsed.lines_without_imports[0:1]: # pragma: no branch
  120. section_output.insert(0, section_comment)
  121. section_footer = config.import_footers.get(section_name.lower(), "")
  122. if section_footer and section_footer not in seen_headings:
  123. if config.dedup_headings:
  124. seen_headings.add(section_footer)
  125. section_comment_end = f"# {section_footer}"
  126. if (
  127. section_comment_end not in parsed.lines_without_imports[-1:]
  128. ): # pragma: no branch
  129. section_output.append("") # Empty line for black compatibility
  130. section_output.append(section_comment_end)
  131. if pending_lines_before or not no_lines_before:
  132. output += [""] * config.lines_between_sections
  133. output += section_output
  134. pending_lines_before = False
  135. else:
  136. pending_lines_before = pending_lines_before or not no_lines_before
  137. if config.ensure_newline_before_comments:
  138. output = _ensure_newline_before_comment(output)
  139. while output and output[-1].strip() == "":
  140. output.pop() # pragma: no cover
  141. while output and output[0].strip() == "":
  142. output.pop(0)
  143. if config.formatting_function:
  144. output = config.formatting_function(
  145. parsed.line_separator.join(output), extension, config
  146. ).splitlines()
  147. output_at = 0
  148. if parsed.import_index < parsed.original_line_count:
  149. output_at = parsed.import_index
  150. formatted_output[output_at:0] = output
  151. if output:
  152. imports_tail = output_at + len(output)
  153. while [
  154. character.strip() for character in formatted_output[imports_tail : imports_tail + 1]
  155. ] == [""]:
  156. formatted_output.pop(imports_tail)
  157. if len(formatted_output) > imports_tail:
  158. next_construct = ""
  159. tail = formatted_output[imports_tail:]
  160. for index, line in enumerate(tail): # pragma: no branch
  161. should_skip, in_quote, *_ = parse.skip_line(
  162. line,
  163. in_quote="",
  164. index=len(formatted_output),
  165. section_comments=config.section_comments,
  166. needs_import=False,
  167. )
  168. if not should_skip and line.strip():
  169. if (
  170. line.strip().startswith("#")
  171. and len(tail) > (index + 1)
  172. and tail[index + 1].strip()
  173. ):
  174. continue
  175. next_construct = line
  176. break
  177. if in_quote: # pragma: no branch
  178. next_construct = line
  179. break
  180. if config.lines_after_imports != -1:
  181. lines_after_imports = config.lines_after_imports
  182. if config.profile == "black" and extension == "pyi": # special case for black
  183. lines_after_imports = 1
  184. formatted_output[imports_tail:0] = ["" for line in range(lines_after_imports)]
  185. elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLARATIONS):
  186. formatted_output[imports_tail:0] = ["", ""]
  187. else:
  188. formatted_output[imports_tail:0] = [""]
  189. if config.lines_before_imports != -1:
  190. lines_before_imports = config.lines_before_imports
  191. if config.profile == "black" and extension == "pyi": # special case for black
  192. lines_before_imports = 1
  193. formatted_output[:0] = ["" for line in range(lines_before_imports)]
  194. if parsed.place_imports:
  195. new_out_lines = []
  196. for index, line in enumerate(formatted_output):
  197. new_out_lines.append(line)
  198. if line in parsed.import_placements:
  199. new_out_lines.extend(parsed.place_imports[parsed.import_placements[line]])
  200. if (
  201. len(formatted_output) <= (index + 1)
  202. or formatted_output[index + 1].strip() != ""
  203. ):
  204. new_out_lines.append("")
  205. formatted_output = new_out_lines
  206. return _output_as_string(formatted_output, parsed.line_separator)
  207. def _with_from_imports(
  208. parsed: parse.ParsedContent,
  209. config: Config,
  210. from_modules: Iterable[str],
  211. section: str,
  212. remove_imports: List[str],
  213. import_type: str,
  214. ) -> List[str]:
  215. output: List[str] = []
  216. for module in from_modules:
  217. if module in remove_imports:
  218. continue
  219. import_start = f"from {module} {import_type} "
  220. from_imports = list(parsed.imports[section]["from"][module])
  221. if (
  222. not config.no_inline_sort
  223. or (config.force_single_line and module not in config.single_line_exclusions)
  224. ) and not config.only_sections:
  225. from_imports = sorting.sort(
  226. config,
  227. from_imports,
  228. key=lambda key: sorting.module_key(
  229. key,
  230. config,
  231. True,
  232. config.force_alphabetical_sort_within_sections,
  233. section_name=section,
  234. ),
  235. reverse=config.reverse_sort,
  236. )
  237. if remove_imports:
  238. from_imports = [
  239. line for line in from_imports if f"{module}.{line}" not in remove_imports
  240. ]
  241. sub_modules = [f"{module}.{from_import}" for from_import in from_imports]
  242. as_imports = {
  243. from_import: [
  244. f"{from_import} as {as_module}" for as_module in parsed.as_map["from"][sub_module]
  245. ]
  246. for from_import, sub_module in zip(from_imports, sub_modules)
  247. if sub_module in parsed.as_map["from"]
  248. }
  249. if config.combine_as_imports and not ("*" in from_imports and config.combine_star):
  250. if not config.no_inline_sort:
  251. for as_import in as_imports:
  252. if not config.only_sections:
  253. as_imports[as_import] = sorting.sort(config, as_imports[as_import])
  254. for from_import in copy.copy(from_imports):
  255. if from_import in as_imports:
  256. idx = from_imports.index(from_import)
  257. if parsed.imports[section]["from"][module][from_import]:
  258. from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import)
  259. else:
  260. from_imports[idx : (idx + 1)] = as_imports.pop(from_import)
  261. only_show_as_imports = False
  262. comments = parsed.categorized_comments["from"].pop(module, ())
  263. above_comments = parsed.categorized_comments["above"]["from"].pop(module, None)
  264. while from_imports:
  265. if above_comments:
  266. output.extend(above_comments)
  267. above_comments = None
  268. if "*" in from_imports and config.combine_star:
  269. import_statement = wrap.line(
  270. with_comments(
  271. _with_star_comments(parsed, module, list(comments or ())),
  272. f"{import_start}*",
  273. removed=config.ignore_comments,
  274. comment_prefix=config.comment_prefix,
  275. ),
  276. parsed.line_separator,
  277. config,
  278. )
  279. from_imports = [
  280. from_import for from_import in from_imports if from_import in as_imports
  281. ]
  282. only_show_as_imports = True
  283. elif config.force_single_line and module not in config.single_line_exclusions:
  284. import_statement = ""
  285. while from_imports:
  286. from_import = from_imports.pop(0)
  287. single_import_line = with_comments(
  288. comments,
  289. import_start + from_import,
  290. removed=config.ignore_comments,
  291. comment_prefix=config.comment_prefix,
  292. )
  293. comment = (
  294. parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None)
  295. )
  296. if comment:
  297. single_import_line += (
  298. f"{comments and ';' or config.comment_prefix} " f"{comment}"
  299. )
  300. if from_import in as_imports:
  301. if (
  302. parsed.imports[section]["from"][module][from_import]
  303. and not only_show_as_imports
  304. ):
  305. output.append(
  306. wrap.line(single_import_line, parsed.line_separator, config)
  307. )
  308. from_comments = parsed.categorized_comments["straight"].get(
  309. f"{module}.{from_import}"
  310. )
  311. if not config.only_sections:
  312. output.extend(
  313. with_comments(
  314. from_comments,
  315. wrap.line(
  316. import_start + as_import, parsed.line_separator, config
  317. ),
  318. removed=config.ignore_comments,
  319. comment_prefix=config.comment_prefix,
  320. )
  321. for as_import in sorting.sort(config, as_imports[from_import])
  322. )
  323. else:
  324. output.extend(
  325. with_comments(
  326. from_comments,
  327. wrap.line(
  328. import_start + as_import, parsed.line_separator, config
  329. ),
  330. removed=config.ignore_comments,
  331. comment_prefix=config.comment_prefix,
  332. )
  333. for as_import in as_imports[from_import]
  334. )
  335. else:
  336. output.append(wrap.line(single_import_line, parsed.line_separator, config))
  337. comments = None
  338. else:
  339. while from_imports and from_imports[0] in as_imports:
  340. from_import = from_imports.pop(0)
  341. if not config.only_sections:
  342. as_imports[from_import] = sorting.sort(config, as_imports[from_import])
  343. from_comments = (
  344. parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or []
  345. )
  346. if (
  347. parsed.imports[section]["from"][module][from_import]
  348. and not only_show_as_imports
  349. ):
  350. specific_comment = (
  351. parsed.categorized_comments["nested"]
  352. .get(module, {})
  353. .pop(from_import, None)
  354. )
  355. if specific_comment:
  356. from_comments.append(specific_comment)
  357. output.append(
  358. wrap.line(
  359. with_comments(
  360. from_comments,
  361. import_start + from_import,
  362. removed=config.ignore_comments,
  363. comment_prefix=config.comment_prefix,
  364. ),
  365. parsed.line_separator,
  366. config,
  367. )
  368. )
  369. from_comments = []
  370. for as_import in as_imports[from_import]:
  371. specific_comment = (
  372. parsed.categorized_comments["nested"]
  373. .get(module, {})
  374. .pop(as_import, None)
  375. )
  376. if specific_comment:
  377. from_comments.append(specific_comment)
  378. output.append(
  379. wrap.line(
  380. with_comments(
  381. from_comments,
  382. import_start + as_import,
  383. removed=config.ignore_comments,
  384. comment_prefix=config.comment_prefix,
  385. ),
  386. parsed.line_separator,
  387. config,
  388. )
  389. )
  390. from_comments = []
  391. if "*" in from_imports:
  392. output.append(
  393. with_comments(
  394. _with_star_comments(parsed, module, []),
  395. f"{import_start}*",
  396. removed=config.ignore_comments,
  397. comment_prefix=config.comment_prefix,
  398. )
  399. )
  400. from_imports.remove("*")
  401. for from_import in copy.copy(from_imports):
  402. comment = (
  403. parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None)
  404. )
  405. if comment:
  406. from_imports.remove(from_import)
  407. if from_imports:
  408. use_comments = []
  409. else:
  410. use_comments = comments
  411. comments = None
  412. single_import_line = with_comments(
  413. use_comments,
  414. import_start + from_import,
  415. removed=config.ignore_comments,
  416. comment_prefix=config.comment_prefix,
  417. )
  418. single_import_line += (
  419. f"{use_comments and ';' or config.comment_prefix} " f"{comment}"
  420. )
  421. output.append(wrap.line(single_import_line, parsed.line_separator, config))
  422. from_import_section = []
  423. while from_imports and (
  424. from_imports[0] not in as_imports
  425. or (
  426. config.combine_as_imports
  427. and parsed.imports[section]["from"][module][from_import]
  428. )
  429. ):
  430. from_import_section.append(from_imports.pop(0))
  431. if config.combine_as_imports:
  432. comments = (comments or []) + list(
  433. parsed.categorized_comments["from"].pop(f"{module}.__combined_as__", ())
  434. )
  435. import_statement = with_comments(
  436. comments,
  437. import_start + (", ").join(from_import_section),
  438. removed=config.ignore_comments,
  439. comment_prefix=config.comment_prefix,
  440. )
  441. if not from_import_section:
  442. import_statement = ""
  443. do_multiline_reformat = False
  444. force_grid_wrap = config.force_grid_wrap
  445. if force_grid_wrap and len(from_import_section) >= force_grid_wrap:
  446. do_multiline_reformat = True
  447. if len(import_statement) > config.line_length and len(from_import_section) > 1:
  448. do_multiline_reformat = True
  449. # If line too long AND have imports AND we are
  450. # NOT using GRID or VERTICAL wrap modes
  451. if (
  452. len(import_statement) > config.line_length
  453. and len(from_import_section) > 0
  454. and config.multi_line_output
  455. not in (wrap.Modes.GRID, wrap.Modes.VERTICAL) # type: ignore
  456. ):
  457. do_multiline_reformat = True
  458. if config.split_on_trailing_comma and module in parsed.trailing_commas:
  459. import_statement = wrap.import_statement(
  460. import_start=import_start,
  461. from_imports=from_import_section,
  462. comments=comments,
  463. line_separator=parsed.line_separator,
  464. config=config,
  465. explode=True,
  466. )
  467. elif do_multiline_reformat:
  468. import_statement = wrap.import_statement(
  469. import_start=import_start,
  470. from_imports=from_import_section,
  471. comments=comments,
  472. line_separator=parsed.line_separator,
  473. config=config,
  474. )
  475. if config.multi_line_output == wrap.Modes.GRID: # type: ignore
  476. other_import_statement = wrap.import_statement(
  477. import_start=import_start,
  478. from_imports=from_import_section,
  479. comments=comments,
  480. line_separator=parsed.line_separator,
  481. config=config,
  482. multi_line_output=wrap.Modes.VERTICAL_GRID, # type: ignore
  483. )
  484. if (
  485. max(
  486. len(import_line)
  487. for import_line in import_statement.split(parsed.line_separator)
  488. )
  489. > config.line_length
  490. ):
  491. import_statement = other_import_statement
  492. elif len(import_statement) > config.line_length:
  493. import_statement = wrap.line(import_statement, parsed.line_separator, config)
  494. if import_statement:
  495. output.append(import_statement)
  496. return output
  497. def _with_straight_imports(
  498. parsed: parse.ParsedContent,
  499. config: Config,
  500. straight_modules: Iterable[str],
  501. section: str,
  502. remove_imports: List[str],
  503. import_type: str,
  504. ) -> List[str]:
  505. output: List[str] = []
  506. as_imports = any((module in parsed.as_map["straight"] for module in straight_modules))
  507. # combine_straight_imports only works for bare imports, 'as' imports not included
  508. if config.combine_straight_imports and not as_imports:
  509. if not straight_modules:
  510. return []
  511. above_comments: List[str] = []
  512. inline_comments: List[str] = []
  513. for module in straight_modules:
  514. if module in parsed.categorized_comments["above"]["straight"]:
  515. above_comments.extend(parsed.categorized_comments["above"]["straight"].pop(module))
  516. if module in parsed.categorized_comments["straight"]:
  517. inline_comments.extend(parsed.categorized_comments["straight"][module])
  518. combined_straight_imports = ", ".join(straight_modules)
  519. if inline_comments:
  520. combined_inline_comments = " ".join(inline_comments)
  521. else:
  522. combined_inline_comments = ""
  523. output.extend(above_comments)
  524. if combined_inline_comments:
  525. output.append(
  526. f"{import_type} {combined_straight_imports} # {combined_inline_comments}"
  527. )
  528. else:
  529. output.append(f"{import_type} {combined_straight_imports}")
  530. return output
  531. for module in straight_modules:
  532. if module in remove_imports:
  533. continue
  534. import_definition = []
  535. if module in parsed.as_map["straight"]:
  536. if parsed.imports[section]["straight"][module]:
  537. import_definition.append((f"{import_type} {module}", module))
  538. import_definition.extend(
  539. (f"{import_type} {module} as {as_import}", f"{module} as {as_import}")
  540. for as_import in parsed.as_map["straight"][module]
  541. )
  542. else:
  543. import_definition.append((f"{import_type} {module}", module))
  544. comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None)
  545. if comments_above:
  546. output.extend(comments_above)
  547. output.extend(
  548. with_comments(
  549. parsed.categorized_comments["straight"].get(imodule),
  550. idef,
  551. removed=config.ignore_comments,
  552. comment_prefix=config.comment_prefix,
  553. )
  554. for idef, imodule in import_definition
  555. )
  556. return output
  557. def _output_as_string(lines: List[str], line_separator: str) -> str:
  558. return line_separator.join(_normalize_empty_lines(lines))
  559. def _normalize_empty_lines(lines: List[str]) -> List[str]:
  560. while lines and lines[-1].strip() == "":
  561. lines.pop(-1)
  562. lines.append("")
  563. return lines
  564. class _LineWithComments(str):
  565. comments: List[str]
  566. def __new__(
  567. cls: Type["_LineWithComments"], value: Any, comments: List[str]
  568. ) -> "_LineWithComments":
  569. instance = super().__new__(cls, value)
  570. instance.comments = comments
  571. return instance
  572. def _ensure_newline_before_comment(output: List[str]) -> List[str]:
  573. new_output: List[str] = []
  574. def is_comment(line: Optional[str]) -> bool:
  575. return line.startswith("#") if line else False
  576. for line, prev_line in zip(output, [None] + output): # type: ignore
  577. if is_comment(line) and prev_line != "" and not is_comment(prev_line):
  578. new_output.append("")
  579. new_output.append(line)
  580. return new_output
  581. def _with_star_comments(parsed: parse.ParsedContent, module: str, comments: List[str]) -> List[str]:
  582. star_comment = parsed.categorized_comments["nested"].get(module, {}).pop("*", None)
  583. if star_comment:
  584. return comments + [star_comment]
  585. return comments