Skip to content

Commit febf054

Browse files
committedOct 2, 2021
Merge branch 'main' of https://github.com/PyCQA/isort into multiple_configs
2 parents 1072501 + 80c213b commit febf054

File tree

9 files changed

+246
-2
lines changed

9 files changed

+246
-2
lines changed
 

‎docs/configuration/options.md

+21
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,15 @@ Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY',
431431
**Python & Config File Name:** import_headings
432432
**CLI Flags:** **Not Supported**
433433

434+
## Import Footers
435+
436+
**No Description**
437+
438+
**Type:** Dict
439+
**Default:** `{}`
440+
**Python & Config File Name:** import_footers
441+
**CLI Flags:** **Not Supported**
442+
434443
## Balanced Wrapping
435444

436445
Balances wrapping to produce the most consistent line length possible
@@ -482,6 +491,18 @@ Ensures the output doesn't save if the resulting file contains syntax errors.
482491
- --ac
483492
- --atomic
484493

494+
## Lines Before Imports
495+
496+
How many lines to add before an import section
497+
498+
**Type:** Int
499+
**Default:** `-1`
500+
**Python & Config File Name:** lines_before_imports
501+
**CLI Flags:**
502+
503+
- --lbi
504+
- --lines-before-imports
505+
485506
## Lines After Imports
486507

487508
**No Description**

‎isort/core.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,10 @@ def process(
249249
else:
250250
code_sorting_section += line
251251
line = ""
252-
elif stripped_line in config.section_comments:
252+
elif (
253+
stripped_line in config.section_comments
254+
or stripped_line in config.section_comments_end
255+
):
253256
if import_section and not contains_imports:
254257
output_stream.write(import_section)
255258
import_section = line
@@ -460,6 +463,7 @@ def _indented_config(config: Config, indent: str) -> Config:
460463
wrap_length=max(config.wrap_length - len(indent), 0),
461464
lines_after_imports=1,
462465
import_headings=config.import_headings if config.indented_import_headings else {},
466+
import_footers=config.import_footers if config.indented_import_headings else {},
463467
)
464468

465469

‎isort/main.py

+3
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,9 @@ def _build_arg_parser() -> argparse.ArgumentParser:
491491
dest="indent",
492492
type=str,
493493
)
494+
output_group.add_argument(
495+
"--lbi", "--lines-before-imports", dest="lines_before_imports", type=int
496+
)
494497
output_group.add_argument(
495498
"--lai", "--lines-after-imports", dest="lines_after_imports", type=int
496499
)

‎isort/output.py

+14
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ def sorted_imports(
138138
if section_comment not in parsed.lines_without_imports[0:1]: # pragma: no branch
139139
section_output.insert(0, section_comment)
140140

141+
section_footer = config.import_footers.get(section_name.lower(), "")
142+
if section_footer and section_footer not in seen_headings:
143+
if config.dedup_headings:
144+
seen_headings.add(section_footer)
145+
section_comment_end = f"# {section_footer}"
146+
if (
147+
section_comment_end not in parsed.lines_without_imports[-1:]
148+
): # pragma: no branch
149+
section_output.append("") # Empty line for black compatibility
150+
section_output.append(section_comment_end)
151+
141152
if pending_lines_before or not no_lines_before:
142153
output += [""] * config.lines_between_sections
143154

@@ -206,6 +217,9 @@ def sorted_imports(
206217
else:
207218
formatted_output[imports_tail:0] = [""]
208219

220+
if config.lines_before_imports != -1:
221+
formatted_output[:0] = ["" for line in range(config.lines_before_imports)]
222+
209223
if parsed.place_imports:
210224
new_out_lines = []
211225
for index, line in enumerate(formatted_output):

‎isort/parse.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
187187
line, in_quote=in_quote, index=index, section_comments=config.section_comments
188188
)
189189

190-
if line in config.section_comments and not skipping_line:
190+
if (
191+
line in config.section_comments or line in config.section_comments_end
192+
) and not skipping_line:
191193
if import_index == -1: # pragma: no branch
192194
import_index = index - 1
193195
continue

‎isort/settings.py

+20
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
FALLBACK_CONFIG_SECTIONS: Tuple[str, ...] = ("isort", "tool:isort", "tool.isort")
103103

104104
IMPORT_HEADING_PREFIX = "import_heading_"
105+
IMPORT_FOOTER_PREFIX = "import_footer_"
105106
KNOWN_PREFIX = "known_"
106107
KNOWN_SECTION_MAPPING: Dict[str, str] = {
107108
STDLIB: "STANDARD_LIBRARY",
@@ -173,10 +174,12 @@ class _Config:
173174
single_line_exclusions: Tuple[str, ...] = ()
174175
default_section: str = THIRDPARTY
175176
import_headings: Dict[str, str] = field(default_factory=dict)
177+
import_footers: Dict[str, str] = field(default_factory=dict)
176178
balanced_wrapping: bool = False
177179
use_parentheses: bool = False
178180
order_by_type: bool = True
179181
atomic: bool = False
182+
lines_before_imports: int = -1
180183
lines_after_imports: int = -1
181184
lines_between_sections: int = 1
182185
lines_between_types: int = 0
@@ -297,6 +300,7 @@ def __init__(
297300
):
298301
self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None
299302
self._section_comments: Optional[Tuple[str, ...]] = None
303+
self._section_comments_end: Optional[Tuple[str, ...]] = None
300304
self._skips: Optional[FrozenSet[str]] = None
301305
self._skip_globs: Optional[FrozenSet[str]] = None
302306
self._sorting_function: Optional[Callable[..., List[str]]] = None
@@ -307,6 +311,7 @@ def __init__(
307311
config_vars["py_version"] = config_vars["py_version"].replace("py", "")
308312
config_vars.pop("_known_patterns")
309313
config_vars.pop("_section_comments")
314+
config_vars.pop("_section_comments_end")
310315
config_vars.pop("_skips")
311316
config_vars.pop("_skip_globs")
312317
config_vars.pop("_sorting_function")
@@ -381,6 +386,7 @@ def __init__(
381386

382387
known_other = {}
383388
import_headings = {}
389+
import_footers = {}
384390
for key, value in tuple(combined_config.items()):
385391
# Collect all known sections beyond those that have direct entries
386392
if key.startswith(KNOWN_PREFIX) and key not in (
@@ -417,6 +423,8 @@ def __init__(
417423
)
418424
if key.startswith(IMPORT_HEADING_PREFIX):
419425
import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value)
426+
if key.startswith(IMPORT_FOOTER_PREFIX):
427+
import_footers[key[len(IMPORT_FOOTER_PREFIX) :].lower()] = str(value)
420428

421429
# Coerce all provided config values into their correct type
422430
default_value = _DEFAULT_SETTINGS.get(key, None)
@@ -495,6 +503,10 @@ def __init__(
495503
for import_heading_key in import_headings:
496504
combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}")
497505
combined_config["import_headings"] = import_headings
506+
if import_footers:
507+
for import_footer_key in import_footers:
508+
combined_config.pop(f"{IMPORT_FOOTER_PREFIX}{import_footer_key}")
509+
combined_config["import_footers"] = import_footers
498510

499511
unsupported_config_errors = {}
500512
for option in set(combined_config.keys()).difference(
@@ -653,6 +665,14 @@ def section_comments(self) -> Tuple[str, ...]:
653665
self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values())
654666
return self._section_comments
655667

668+
@property
669+
def section_comments_end(self) -> Tuple[str, ...]:
670+
if self._section_comments_end is not None:
671+
return self._section_comments_end
672+
673+
self._section_comments_end = tuple(f"# {footer}" for footer in self.import_footers.values())
674+
return self._section_comments_end
675+
656676
@property
657677
def skips(self) -> FrozenSet[str]:
658678
if self._skips is not None:

‎tests/integration/test_hypothesmith.py

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co
4040
"sections",
4141
"known_future_library",
4242
"forced_separate",
43+
"lines_before_imports",
4344
"lines_after_imports",
4445
"lines_between_sections",
4546
"lines_between_types",

‎tests/integration/test_setting_combinations.py

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def configs() -> st.SearchStrategy[isort.Config]:
2929
"known_local_folder",
3030
"extra_standard_library",
3131
"forced_separate",
32+
"lines_before_imports",
3233
"lines_after_imports",
3334
"add_imports",
3435
"lines_between_sections",
@@ -553,10 +554,12 @@ def _raise(*a):
553554
),
554555
default_section="THIRDPARTY",
555556
import_headings={},
557+
import_footers={},
556558
balanced_wrapping=False,
557559
use_parentheses=True,
558560
order_by_type=True,
559561
atomic=False,
562+
lines_before_imports=-1,
560563
lines_after_imports=-1,
561564
lines_between_sections=1,
562565
lines_between_types=0,
@@ -859,10 +862,12 @@ def _raise(*a):
859862
"single_line_exclusions": (),
860863
"default_section": "THIRDPARTY",
861864
"import_headings": {},
865+
"import_footers": {},
862866
"balanced_wrapping": False,
863867
"use_parentheses": False,
864868
"order_by_type": True,
865869
"atomic": False,
870+
"lines_before_imports": -1,
866871
"lines_after_imports": -1,
867872
"lines_between_sections": 1,
868873
"lines_between_types": 0,
@@ -1414,10 +1419,12 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None
14141419
),
14151420
default_section="THIRDPARTY",
14161421
import_headings={},
1422+
import_footers={},
14171423
balanced_wrapping=False,
14181424
use_parentheses=True,
14191425
order_by_type=True,
14201426
atomic=False,
1427+
lines_before_imports=-1,
14211428
lines_after_imports=-1,
14221429
lines_between_sections=1,
14231430
lines_between_types=0,
@@ -1720,10 +1727,12 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None
17201727
"single_line_exclusions": (),
17211728
"default_section": "THIRDPARTY",
17221729
"import_headings": {},
1730+
"import_footers": {},
17231731
"balanced_wrapping": False,
17241732
"use_parentheses": False,
17251733
"order_by_type": True,
17261734
"atomic": False,
1735+
"lines_before_imports": -1,
17271736
"lines_after_imports": -1,
17281737
"lines_between_sections": 1,
17291738
"lines_between_types": 0,

‎tests/unit/test_isort.py

+170
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,163 @@ def test_titled_imports() -> None:
13561356
)
13571357

13581358

1359+
def test_footered_imports() -> None:
1360+
"""Tests setting both custom titles and footers to import sections."""
1361+
test_input = (
1362+
"import sys\n"
1363+
"import unicodedata\n"
1364+
"import statistics\n"
1365+
"import os\n"
1366+
"import myproject.test\n"
1367+
"import django.settings"
1368+
)
1369+
test_output = isort.code(
1370+
code=test_input,
1371+
known_first_party=["myproject"],
1372+
import_footer_stdlib="Standard Library End",
1373+
import_footer_firstparty="My Stuff End",
1374+
)
1375+
assert test_output == (
1376+
"import os\n"
1377+
"import statistics\n"
1378+
"import sys\n"
1379+
"import unicodedata\n"
1380+
"\n"
1381+
"# Standard Library End\n"
1382+
"\n"
1383+
"import django.settings\n"
1384+
"\n"
1385+
"import myproject.test\n"
1386+
"\n"
1387+
"# My Stuff End\n"
1388+
)
1389+
test_second_run = isort.code(
1390+
code=test_output,
1391+
known_first_party=["myproject"],
1392+
import_footer_stdlib="Standard Library End",
1393+
import_footer_firstparty="My Stuff End",
1394+
)
1395+
assert test_second_run == test_output
1396+
1397+
test_input_lines_down = (
1398+
"# comment 1\n"
1399+
"import django.settings\n"
1400+
"\n"
1401+
"import sys\n"
1402+
"import unicodedata\n"
1403+
"import statistics\n"
1404+
"import os\n"
1405+
"import myproject.test\n"
1406+
"\n"
1407+
"# Standard Library End\n"
1408+
)
1409+
test_output_lines_down = isort.code(
1410+
code=test_input_lines_down,
1411+
known_first_party=["myproject"],
1412+
import_footer_stdlib="Standard Library End",
1413+
import_footer_firstparty="My Stuff End",
1414+
)
1415+
assert test_output_lines_down == (
1416+
"# comment 1\n"
1417+
"import os\n"
1418+
"import statistics\n"
1419+
"import sys\n"
1420+
"import unicodedata\n"
1421+
"\n"
1422+
"# Standard Library End\n"
1423+
"\n"
1424+
"import django.settings\n"
1425+
"\n"
1426+
"import myproject.test\n"
1427+
"\n"
1428+
"# My Stuff End\n"
1429+
)
1430+
1431+
1432+
def test_titled_and_footered_imports() -> None:
1433+
"""Tests setting custom footers to import sections."""
1434+
test_input = (
1435+
"import sys\n"
1436+
"import unicodedata\n"
1437+
"import statistics\n"
1438+
"import os\n"
1439+
"import myproject.test\n"
1440+
"import django.settings"
1441+
)
1442+
test_output = isort.code(
1443+
code=test_input,
1444+
known_first_party=["myproject"],
1445+
import_heading_stdlib="Standard Library",
1446+
import_heading_firstparty="My Stuff",
1447+
import_footer_stdlib="Standard Library End",
1448+
import_footer_firstparty="My Stuff End",
1449+
)
1450+
assert test_output == (
1451+
"# Standard Library\n"
1452+
"import os\n"
1453+
"import statistics\n"
1454+
"import sys\n"
1455+
"import unicodedata\n"
1456+
"\n"
1457+
"# Standard Library End\n"
1458+
"\n"
1459+
"import django.settings\n"
1460+
"\n"
1461+
"# My Stuff\n"
1462+
"import myproject.test\n"
1463+
"\n"
1464+
"# My Stuff End\n"
1465+
)
1466+
test_second_run = isort.code(
1467+
code=test_output,
1468+
known_first_party=["myproject"],
1469+
import_heading_stdlib="Standard Library",
1470+
import_heading_firstparty="My Stuff",
1471+
import_footer_stdlib="Standard Library End",
1472+
import_footer_firstparty="My Stuff End",
1473+
)
1474+
assert test_second_run == test_output
1475+
1476+
test_input_lines_down = (
1477+
"# comment 1\n"
1478+
"import django.settings\n"
1479+
"\n"
1480+
"# Standard Library\n"
1481+
"import sys\n"
1482+
"import unicodedata\n"
1483+
"import statistics\n"
1484+
"import os\n"
1485+
"import myproject.test\n"
1486+
"\n"
1487+
"# Standard Library End\n"
1488+
)
1489+
test_output_lines_down = isort.code(
1490+
code=test_input_lines_down,
1491+
known_first_party=["myproject"],
1492+
import_heading_stdlib="Standard Library",
1493+
import_heading_firstparty="My Stuff",
1494+
import_footer_stdlib="Standard Library End",
1495+
import_footer_firstparty="My Stuff End",
1496+
)
1497+
assert test_output_lines_down == (
1498+
"# comment 1\n"
1499+
"# Standard Library\n"
1500+
"import os\n"
1501+
"import statistics\n"
1502+
"import sys\n"
1503+
"import unicodedata\n"
1504+
"\n"
1505+
"# Standard Library End\n"
1506+
"\n"
1507+
"import django.settings\n"
1508+
"\n"
1509+
"# My Stuff\n"
1510+
"import myproject.test\n"
1511+
"\n"
1512+
"# My Stuff End\n"
1513+
)
1514+
1515+
13591516
def test_balanced_wrapping() -> None:
13601517
"""Tests balanced wrapping mode, where the length of individual lines maintain width."""
13611518
test_input = (
@@ -1473,6 +1630,19 @@ def test_order_by_type() -> None:
14731630
)
14741631

14751632

1633+
def test_custom_lines_before_import_section() -> None:
1634+
"""Test the case where the number of lines to output after imports has been explicitly set."""
1635+
test_input = "from a import b\nfrom c import d\nfoo = 'bar'\n"
1636+
1637+
# default case is no line added before the import
1638+
assert isort.code(test_input) == ("from a import b\nfrom c import d\n\nfoo = 'bar'\n")
1639+
1640+
# test again with a custom number of lines before the import section
1641+
assert isort.code(test_input, lines_before_imports=2) == (
1642+
"\n\nfrom a import b\nfrom c import d\n\nfoo = 'bar'\n"
1643+
)
1644+
1645+
14761646
def test_custom_lines_after_import_section() -> None:
14771647
"""Test the case where the number of lines to output after imports has been explicitly set."""
14781648
test_input = "from a import b\nfoo = 'bar'\n"

0 commit comments

Comments
 (0)
Please sign in to comment.