Skip to content

Commit

Permalink
Merge branch 'main' into trailing_comma
Browse files Browse the repository at this point in the history
  • Loading branch information
timothycrosley committed May 11, 2022
2 parents 58685a4 + 485f8ad commit 798ea22
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/configuration/github_action.md
Expand Up @@ -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"
```
Expand Down
5 changes: 4 additions & 1 deletion docs/configuration/options.md
Expand Up @@ -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:** ` `
Expand Down
18 changes: 18 additions & 0 deletions 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
```
2 changes: 1 addition & 1 deletion example_isort_formatting_plugin/pyproject.toml
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion example_isort_sorting_plugin/pyproject.toml
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion example_shared_isort_profile/pyproject.toml
Expand Up @@ -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"
6 changes: 5 additions & 1 deletion isort/core.py
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
14 changes: 8 additions & 6 deletions isort/profiles.py
Expand Up @@ -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,
Expand Down
52 changes: 31 additions & 21 deletions isort/settings.py
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion isort/wrap.py
Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -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"
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_isort.py
Expand Up @@ -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"

0 comments on commit 798ea22

Please sign in to comment.