summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Edmund Crosley <timothy.crosley@gmail.com>2022-05-11 13:42:47 -0700
committerGitHub <noreply@github.com>2022-05-11 13:42:47 -0700
commit798ea224eeee2e740882920822c74a0aa9e25458 (patch)
treec18e9416d286286afbe4b4717a0ea9b15179923d
parent58685a4735208770838b46142bc45b9ab83af837 (diff)
parent485f8adcddc174117925b59bf9d913fd33d37984 (diff)
downloadisort-798ea224eeee2e740882920822c74a0aa9e25458.tar.gz
Merge branch 'main' into trailing_comma
-rw-r--r--docs/configuration/github_action.md2
-rw-r--r--docs/configuration/options.md5
-rw-r--r--docs/howto/shared_profiles.md18
-rw-r--r--example_isort_formatting_plugin/pyproject.toml2
-rw-r--r--example_isort_sorting_plugin/pyproject.toml2
-rw-r--r--example_shared_isort_profile/pyproject.toml2
-rw-r--r--isort/core.py6
-rw-r--r--isort/profiles.py14
-rw-r--r--isort/settings.py52
-rw-r--r--isort/wrap.py2
-rw-r--r--poetry.lock23
-rw-r--r--pyproject.toml2
-rw-r--r--tests/unit/test_isort.py18
13 files changed, 109 insertions, 39 deletions
diff --git a/docs/configuration/github_action.md b/docs/configuration/github_action.md
index c7a8dc97..343a2725 100644
--- a/docs/configuration/github_action.md
+++ b/docs/configuration/github_action.md
@@ -52,7 +52,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: 3.8
- - uses: jamescurtin/isort-action@master
+ - uses: isort/isort-action@master
with:
requirementsFiles: "requirements.txt requirements-test.txt"
```
diff --git a/docs/configuration/options.md b/docs/configuration/options.md
index 56239d6f..38de3040 100644
--- a/docs/configuration/options.md
+++ b/docs/configuration/options.md
@@ -1143,7 +1143,10 @@ Inserts a blank line before a comment following an import.
## Profile
-Base profile type to use for configuration. Profiles include: black, django, pycharm, google, open_stack, plone, attrs, hug, wemake, appnexus. As well as any shared profiles.
+Base profile type to use for configuration. Profiles include: black, django,
+pycharm, google, open\_stack, plone, attrs, hug, wemake, appnexus. As well as
+any [shared
+profiles](https://pycqa.github.io/isort/docs/howto/shared_profiles.html).
**Type:** String
**Default:** ` `
diff --git a/docs/howto/shared_profiles.md b/docs/howto/shared_profiles.md
new file mode 100644
index 00000000..3ed8b52e
--- /dev/null
+++ b/docs/howto/shared_profiles.md
@@ -0,0 +1,18 @@
+# Shared Profiles
+
+As well as the [built in
+profiles](https://pycqa.github.io/isort/docs/configuration/profiles.html), you
+can define and share your own profiles.
+
+All that's required is to create a Python package that exposes an entry point to
+a dictionary exposing profile settings under `isort.profiles`. An example is
+available [within the `isort`
+repo](https://github.com/PyCQA/isort/tree/main/example_shared_isort_profile)
+
+### Example `.isort.cfg`
+
+```
+[options.entry_points]
+isort.profiles =
+ shared_profile=my_module:PROFILE
+```
diff --git a/example_isort_formatting_plugin/pyproject.toml b/example_isort_formatting_plugin/pyproject.toml
index 25ce0174..1b0e6832 100644
--- a/example_isort_formatting_plugin/pyproject.toml
+++ b/example_isort_formatting_plugin/pyproject.toml
@@ -17,4 +17,4 @@ black = ">20.08b1"
[build-system]
requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.masonry.api"
+build-backend = "poetry.core.masonry.api"
diff --git a/example_isort_sorting_plugin/pyproject.toml b/example_isort_sorting_plugin/pyproject.toml
index cafcfcaa..d82d1aa7 100644
--- a/example_isort_sorting_plugin/pyproject.toml
+++ b/example_isort_sorting_plugin/pyproject.toml
@@ -16,4 +16,4 @@ natsort = ">=7.1.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.masonry.api"
+build-backend = "poetry.core.masonry.api"
diff --git a/example_shared_isort_profile/pyproject.toml b/example_shared_isort_profile/pyproject.toml
index 48fa5085..cee710d6 100644
--- a/example_shared_isort_profile/pyproject.toml
+++ b/example_shared_isort_profile/pyproject.toml
@@ -15,4 +15,4 @@ python = ">=3.6"
[build-system]
requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.masonry.api"
+build-backend = "poetry.core.masonry.api"
diff --git a/isort/core.py b/isort/core.py
index e9a2977b..9dfaf70f 100644
--- a/isort/core.py
+++ b/isort/core.py
@@ -7,7 +7,7 @@ import isort.literal
from isort.settings import DEFAULT_CONFIG, Config
from . import output, parse
-from .exceptions import FileSkipComment
+from .exceptions import ExistingSyntaxErrors, FileSkipComment
from .format import format_natural, remove_whitespace
from .settings import FILE_SKIP_COMMENTS
@@ -303,6 +303,10 @@ def process(
else:
while ")" not in stripped_line:
line = input_stream.readline()
+
+ if not line: # end of file without closing parenthesis
+ raise ExistingSyntaxErrors("Parenthesis is not closed")
+
stripped_line = line.strip().split("#")[0]
import_statement += line
diff --git a/isort/profiles.py b/isort/profiles.py
index 21d06463..cf43ac0f 100644
--- a/isort/profiles.py
+++ b/isort/profiles.py
@@ -33,12 +33,14 @@ open_stack = {
"force_sort_within_sections": True,
"lexicographical": True,
}
-plone = {
- "force_alphabetical_sort": True,
- "force_single_line": True,
- "lines_after_imports": 2,
- "line_length": 200,
-}
+plone = black.copy()
+plone.update(
+ {
+ "force_alphabetical_sort": True,
+ "force_single_line": True,
+ "lines_after_imports": 2,
+ }
+)
attrs = {
"atomic": True,
"force_grid_wrap": 0,
diff --git a/isort/settings.py b/isort/settings.py
index f6fd6f51..d0f9facd 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -238,7 +238,7 @@ class _Config:
reverse_sort: bool = False
star_first: bool = False
import_dependencies = Dict[str, str]
- git_ignore: Dict[Path, Set[Path]] = field(default_factory=dict)
+ git_ls_files: Dict[Path, Set[str]] = field(default_factory=dict)
format_error: str = "{error}: {message}"
format_success: str = "{success}: {message}"
sort_order: str = "natural"
@@ -553,7 +553,7 @@ class Config(_Config):
else:
return bool(_SHEBANG_RE.match(line))
- def _check_folder_gitignore(self, folder: str) -> Optional[Path]:
+ def _check_folder_git_ls_files(self, folder: str) -> Optional[Path]:
env = {**os.environ, "LANG": "C.UTF-8"}
try:
topfolder_result = subprocess.check_output( # nosec # skipcq: PYL-W1510
@@ -564,26 +564,30 @@ class Config(_Config):
git_folder = Path(topfolder_result.rstrip()).resolve()
- files: List[str] = []
- # don't check symlinks; either part of the repo and would be checked
- # twice, or is external to the repo and git won't know anything about it
- for root, _dirs, git_files in os.walk(git_folder, followlinks=False):
- if ".git" in _dirs:
- _dirs.remove(".git")
- for git_file in git_files:
- files.append(os.path.join(root, git_file))
- git_options = ["-C", str(git_folder), "-c", "core.quotePath="]
- try:
- ignored = subprocess.check_output( # nosec # skipcq: PYL-W1510
- ["git", *git_options, "check-ignore", "-z", "--stdin", "--no-index"],
+ # files committed to git
+ tracked_files = (
+ subprocess.check_output( # nosec # skipcq: PYL-W1510
+ ["git", "-C", str(git_folder), "ls-files", "-z"],
encoding="utf-8",
env=env,
- input="\0".join(files),
)
- except subprocess.CalledProcessError:
- return None
+ .rstrip("\0")
+ .split("\0")
+ )
+ # files that haven't been committed yet, but aren't ignored
+ tracked_files_others = (
+ subprocess.check_output( # nosec # skipcq: PYL-W1510
+ ["git", "-C", str(git_folder), "ls-files", "-z", "--others", "--exclude-standard"],
+ encoding="utf-8",
+ env=env,
+ )
+ .rstrip("\0")
+ .split("\0")
+ )
- self.git_ignore[git_folder] = {Path(f) for f in ignored.rstrip("\0").split("\0")}
+ self.git_ls_files[git_folder] = {
+ str(git_folder / Path(f)) for f in tracked_files + tracked_files_others
+ }
return git_folder
def is_skipped(self, file_path: Path) -> bool:
@@ -625,14 +629,20 @@ class Config(_Config):
git_folder = None
file_paths = [file_path, file_path.resolve()]
- for folder in self.git_ignore:
+ for folder in self.git_ls_files:
if any(folder in path.parents for path in file_paths):
git_folder = folder
break
else:
- git_folder = self._check_folder_gitignore(str(file_path.parent))
+ git_folder = self._check_folder_git_ls_files(str(file_path.parent))
- if git_folder and any(path in self.git_ignore[git_folder] for path in file_paths):
+ # git_ls_files are good files you should parse. If you're not in the allow list, skip.
+
+ if (
+ git_folder
+ and not file_path.is_dir()
+ and str(file_path.resolve()) not in self.git_ls_files[git_folder]
+ ):
return True
return False
diff --git a/isort/wrap.py b/isort/wrap.py
index eb4c2483..8904d0ed 100644
--- a/isort/wrap.py
+++ b/isort/wrap.py
@@ -76,7 +76,7 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) ->
comment = None
if "#" in content:
line_without_comment, comment = content.split("#", 1)
- for splitter in ("import ", ".", "as "):
+ for splitter in ("import ", "cimport ", ".", "as "):
exp = r"\b" + re.escape(splitter) + r"\b"
if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith(
splitter
diff --git a/poetry.lock b/poetry.lock
index a34ccebd..b6bb5401 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -646,7 +646,7 @@ python-versions = "*"
[[package]]
name = "ipython"
-version = "7.16.2"
+version = "7.16.3"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
@@ -1771,7 +1771,7 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
[metadata]
lock-version = "1.1"
python-versions = ">=3.6.2"
-content-hash = "82f7502d9058e54bc0775bb78952f7dda996e0b64c3e62df523b733ab743ade5"
+content-hash = "d19842f9f21974ac81503f3d73c248871f7ddc2bde4fb3cc3e1a3852add2059d"
[metadata.files]
appdirs = [
@@ -2077,8 +2077,8 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipython = [
- {file = "ipython-7.16.2-py3-none-any.whl", hash = "sha256:2f644313be4fdc5c8c2a17467f2949c29423c9e283a159d1fc9bf450a1a300af"},
- {file = "ipython-7.16.2.tar.gz", hash = "sha256:613085f8acb0f35f759e32bea35fba62c651a4a2e409a0da11414618f5eec0c4"},
+ {file = "ipython-7.16.3-py3-none-any.whl", hash = "sha256:c0427ed8bc33ac481faf9d3acf7e84e0010cdaada945e0badd1e2e74cc075833"},
+ {file = "ipython-7.16.3.tar.gz", hash = "sha256:5ac47dc9af66fc2f5530c12069390877ae372ac905edca75a92a6e363b5d7caa"},
]
ipython-genutils = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@@ -2121,6 +2121,9 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
+ {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
+ {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
+ {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
@@ -2132,6 +2135,9 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
+ {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
+ {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
+ {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
@@ -2143,6 +2149,9 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
+ {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
+ {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
+ {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
@@ -2155,6 +2164,9 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
+ {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
+ {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
+ {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
@@ -2167,6 +2179,9 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
+ {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
+ {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
+ {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
diff --git a/pyproject.toml b/pyproject.toml
index ec7c16ad..77710be9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,7 +61,7 @@ flake8-bugbear = "^19.8"
black = {version = "^21.10b0", allow-prereleases = true}
coverage = {version = "^6.0b1", allow-prereleases = true}
mypy = "^0.902"
-ipython = "^7.7"
+ipython = "^7.16"
pytest = "^6.0"
pytest-cov = "^2.7"
pytest-mock = "^1.10"
diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py
index 4b86f7ec..833a69de 100644
--- a/tests/unit/test_isort.py
+++ b/tests/unit/test_isort.py
@@ -5578,3 +5578,21 @@ def test_split_on_trailing_comma() -> None:
output = isort.code(expected_output, split_on_trailing_comma=True)
assert output == expected_output
+
+
+def test_infinite_loop_in_unmatched_parenthesis() -> None:
+ test_input = "from os import ("
+
+ # ensure a syntax error is raised for unmatched parenthesis
+ with pytest.raises(ExistingSyntaxErrors):
+ isort.code(test_input)
+
+ test_input = """from os import (
+ path,
+
+ walk
+)
+"""
+
+ # ensure other cases are handled correctly
+ assert isort.code(test_input) == "from os import path, walk\n"