diff --git a/docs/configuration/github_action.md b/docs/configuration/github_action.md index c7a8dc971..343a27252 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 56239d6f2..38de30402 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 000000000..3ed8b52ef --- /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 25ce01747..1b0e68324 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 cafcfcaaa..d82d1aa74 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 48fa50850..cee710d69 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 e9a2977b3..9dfaf70f0 100644 --- a/isort/core.py +++ b/isort/core.py @@ -7,7 +7,7 @@ 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 21d064630..cf43ac0f2 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -33,12 +33,14 @@ "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 f6fd6f51a..d0f9facd1 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 @@ def is_supported_filetype(self, file_name: str) -> bool: 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 @@ def _check_folder_gitignore(self, folder: str) -> Optional[Path]: 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 @@ def is_skipped(self, file_path: Path) -> bool: 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 eb4c24832..8904d0eda 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 a34ccebd3..b6bb54015 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 ec7c16add..77710be9e 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 4b86f7ec5..833a69de9 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"