Skip to content

Commit 2232954

Browse files
authoredOct 1, 2021
Merge pull request #1817 from Parnassius/issue/1733
Add an option to set a footer for every import section
2 parents 995e016 + 146e01b commit 2232954

File tree

7 files changed

+208
-2
lines changed

7 files changed

+208
-2
lines changed
 

‎docs/configuration/options.md

+9
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

‎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/output.py

+11
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

‎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

+19
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,6 +174,7 @@ 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
@@ -297,6 +299,7 @@ def __init__(
297299
):
298300
self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None
299301
self._section_comments: Optional[Tuple[str, ...]] = None
302+
self._section_comments_end: Optional[Tuple[str, ...]] = None
300303
self._skips: Optional[FrozenSet[str]] = None
301304
self._skip_globs: Optional[FrozenSet[str]] = None
302305
self._sorting_function: Optional[Callable[..., List[str]]] = None
@@ -307,6 +310,7 @@ def __init__(
307310
config_vars["py_version"] = config_vars["py_version"].replace("py", "")
308311
config_vars.pop("_known_patterns")
309312
config_vars.pop("_section_comments")
313+
config_vars.pop("_section_comments_end")
310314
config_vars.pop("_skips")
311315
config_vars.pop("_skip_globs")
312316
config_vars.pop("_sorting_function")
@@ -381,6 +385,7 @@ def __init__(
381385

382386
known_other = {}
383387
import_headings = {}
388+
import_footers = {}
384389
for key, value in tuple(combined_config.items()):
385390
# Collect all known sections beyond those that have direct entries
386391
if key.startswith(KNOWN_PREFIX) and key not in (
@@ -417,6 +422,8 @@ def __init__(
417422
)
418423
if key.startswith(IMPORT_HEADING_PREFIX):
419424
import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value)
425+
if key.startswith(IMPORT_FOOTER_PREFIX):
426+
import_footers[key[len(IMPORT_FOOTER_PREFIX) :].lower()] = str(value)
420427

421428
# Coerce all provided config values into their correct type
422429
default_value = _DEFAULT_SETTINGS.get(key, None)
@@ -495,6 +502,10 @@ def __init__(
495502
for import_heading_key in import_headings:
496503
combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}")
497504
combined_config["import_headings"] = import_headings
505+
if import_footers:
506+
for import_footer_key in import_footers:
507+
combined_config.pop(f"{IMPORT_FOOTER_PREFIX}{import_footer_key}")
508+
combined_config["import_footers"] = import_footers
498509

499510
unsupported_config_errors = {}
500511
for option in set(combined_config.keys()).difference(
@@ -653,6 +664,14 @@ def section_comments(self) -> Tuple[str, ...]:
653664
self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values())
654665
return self._section_comments
655666

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

‎tests/integration/test_setting_combinations.py

+4
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ def _raise(*a):
553553
),
554554
default_section="THIRDPARTY",
555555
import_headings={},
556+
import_footers={},
556557
balanced_wrapping=False,
557558
use_parentheses=True,
558559
order_by_type=True,
@@ -859,6 +860,7 @@ def _raise(*a):
859860
"single_line_exclusions": (),
860861
"default_section": "THIRDPARTY",
861862
"import_headings": {},
863+
"import_footers": {},
862864
"balanced_wrapping": False,
863865
"use_parentheses": False,
864866
"order_by_type": True,
@@ -1414,6 +1416,7 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None
14141416
),
14151417
default_section="THIRDPARTY",
14161418
import_headings={},
1419+
import_footers={},
14171420
balanced_wrapping=False,
14181421
use_parentheses=True,
14191422
order_by_type=True,
@@ -1720,6 +1723,7 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None
17201723
"single_line_exclusions": (),
17211724
"default_section": "THIRDPARTY",
17221725
"import_headings": {},
1726+
"import_footers": {},
17231727
"balanced_wrapping": False,
17241728
"use_parentheses": False,
17251729
"order_by_type": True,

‎tests/unit/test_isort.py

+157
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 = (

0 commit comments

Comments
 (0)
Please sign in to comment.