├── tests ├── __init__.py └── seed_isort_config_test.py ├── setup.py ├── requirements-dev.txt ├── .gitignore ├── .pre-commit-hooks.yaml ├── tox.ini ├── azure-pipelines.yml ├── LICENSE ├── .pre-commit-config.yaml ├── setup.cfg ├── README.md └── seed_isort_config.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup() 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | covdefaults 2 | coverage 3 | pytest 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | /.coverage 4 | /.mypy_cache 5 | /.tox 6 | /venv* 7 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: seed-isort-config 2 | name: seed isort known_third_party 3 | description: Statically populate the `known_third_party` `isort` setting. 4 | entry: seed-isort-config 5 | language: python 6 | always_run: true 7 | pass_filenames: false 8 | minimum_pre_commit_version: 0.14.0 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,pypy3,pre-commit 3 | 4 | [testenv] 5 | deps = -rrequirements-dev.txt 6 | commands = 7 | coverage erase 8 | coverage run -m pytest {posargs:tests} 9 | coverage report --fail-under 100 10 | 11 | [testenv:pre-commit] 12 | skip_install = true 13 | deps = pre-commit 14 | commands = pre-commit run --all-files --show-diff-on-failure 15 | 16 | [pep8] 17 | ignore = E265,E501,W504 18 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: [master, test-me-*] 4 | tags: 5 | include: ['*'] 6 | 7 | resources: 8 | repositories: 9 | - repository: asottile 10 | type: github 11 | endpoint: github 12 | name: asottile/azure-pipeline-templates 13 | ref: refs/tags/v1.0.0 14 | 15 | jobs: 16 | - template: job--pre-commit.yml@asottile 17 | - template: job--python-tox.yml@asottile 18 | parameters: 19 | toxenvs: [pypy3, py36, py37, py38] 20 | os: linux 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-docstring-first 8 | - id: check-yaml 9 | - id: debug-statements 10 | - id: double-quote-string-fixer 11 | - id: name-tests-test 12 | - id: requirements-txt-fixer 13 | - repo: https://gitlab.com/pycqa/flake8 14 | rev: 3.8.0 15 | hooks: 16 | - id: flake8 17 | - repo: https://github.com/pre-commit/mirrors-autopep8 18 | rev: v1.5.2 19 | hooks: 20 | - id: autopep8 21 | - repo: https://github.com/asottile/reorder_python_imports 22 | rev: v2.3.0 23 | hooks: 24 | - id: reorder-python-imports 25 | args: [--py3-plus] 26 | - repo: https://github.com/asottile/pyupgrade 27 | rev: v2.4.1 28 | hooks: 29 | - id: pyupgrade 30 | args: [--py36-plus] 31 | - repo: https://github.com/asottile/add-trailing-comma 32 | rev: v2.0.1 33 | hooks: 34 | - id: add-trailing-comma 35 | args: [--py36-plus] 36 | - repo: https://github.com/asottile/setup-cfg-fmt 37 | rev: v1.9.0 38 | hooks: 39 | - id: setup-cfg-fmt 40 | - repo: https://github.com/pre-commit/mirrors-mypy 41 | rev: v0.770 42 | hooks: 43 | - id: mypy 44 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = seed_isort_config 3 | version = 2.2.0 4 | description = Statically populate the `known_third_party` `isort` setting. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/asottile/seed-isort-config 8 | author = Anthony Sottile 9 | author_email = asottile@umich.edu 10 | license = MIT 11 | license_file = LICENSE 12 | classifiers = 13 | License :: OSI Approved :: MIT License 14 | Programming Language :: Python :: 3 15 | Programming Language :: Python :: 3 :: Only 16 | Programming Language :: Python :: 3.6 17 | Programming Language :: Python :: 3.7 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: Implementation :: CPython 20 | Programming Language :: Python :: Implementation :: PyPy 21 | 22 | [options] 23 | py_modules = seed_isort_config 24 | install_requires = 25 | aspy.refactor_imports 26 | python_requires = >=3.6.1 27 | 28 | [options.entry_points] 29 | console_scripts = 30 | seed-isort-config=seed_isort_config:main 31 | 32 | [bdist_wheel] 33 | universal = True 34 | 35 | [coverage:run] 36 | plugins = covdefaults 37 | 38 | [mypy] 39 | check_untyped_defs = true 40 | disallow_any_generics = true 41 | disallow_incomplete_defs = true 42 | disallow_untyped_defs = true 43 | no_implicit_optional = true 44 | 45 | [mypy-testing.*] 46 | disallow_untyped_defs = false 47 | 48 | [mypy-tests.*] 49 | disallow_untyped_defs = false 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | this is no longer needed as of `isort>=5` 4 | 5 | ___ 6 | 7 | [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.seed-isort-config?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=20&branchName=master) 8 | [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/20/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=20&branchName=master) 9 | 10 | seed-isort-config 11 | ================= 12 | 13 | Statically populate the `known_third_party` `isort` setting. 14 | 15 | [`isort`][isort] when run in isolation is not the best at determining what 16 | dependencies are third party. 17 | 18 | [`aspy.refactor_imports`][aspy.refactor_imports] is fortunately much better at 19 | this static analysis. 20 | 21 | Why not just use [`reorder-python-imports`][reorder_python_imports]? Well, it 22 | lacks a few features provided by `isort` (intentionally). 23 | 24 | What this script does is seeds the `known_third_party` isort configuration 25 | automatically. 26 | 27 | ## install 28 | 29 | `pip install seed-isort-config` 30 | 31 | ## usage 32 | 33 | `seed-isort-config` provides a single executable by the same name. Run it 34 | inside a `git` repository. 35 | 36 | To specify custom application roots (such as with the `src` pattern) pass a 37 | colon-separated `--application-directories` parameter. 38 | 39 | Files may be excluded from the process using the `--exclude` flag. 40 | This argument takes a python regular expression. 41 | 42 | For a full list of arguments, see `seed-isort-config --help`. 43 | 44 | ## getting started 45 | 46 | `seed-isort-config` looks for an existing `known_third_party` setting in an 47 | isort configuration file. It will modify that if it exists, otherwise it'll 48 | create a brand new `.isort.cfg` file. 49 | 50 | The easiest way to get started is to just add a blank `known_third_party =` 51 | section to your isort configuration (or `known_third_party = []` if you are 52 | using `pyproject.toml`). 53 | 54 | ## usage with pre-commit 55 | 56 | This works especially well when integrated with [`pre-commit`][pre-commit]. 57 | 58 | 59 | ```yaml 60 | - repo: https://github.com/asottile/seed-isort-config 61 | rev: v2.2.0 62 | hooks: 63 | - id: seed-isort-config 64 | - repo: https://github.com/timothycrosley/isort 65 | rev: 4.3.21 # pick the isort version you'd like to use from https://github.com/timothycrosley/isort/releases 66 | hooks: 67 | - id: isort 68 | ``` 69 | 70 | In this configuration, `seed-isort-config` will adjust the `known_third_party` 71 | section of the `isort` configuration before `isort` runs! 72 | 73 | Note that `seed-isort-config` doesn't act like a normal pre-commit linter so 74 | file exclusion must be configured through `args: [--exclude=...]` instead. 75 | For example: `args: [--exclude=tests/.*\.py]`. 76 | 77 | [isort]: https://github.com/timothycrosley/isort 78 | [aspy.refactor_imports]: https://github.com/asottile/aspy.refactor_imports 79 | [reorder_python_imports]: https://github.com/asottile/reorder_python_imports 80 | [pre-commit]: https://github.com/pre-commit/pre-commit 81 | -------------------------------------------------------------------------------- /seed_isort_config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ast 3 | import os.path 4 | import re 5 | import subprocess 6 | from typing import Optional 7 | from typing import Sequence 8 | from typing import Set 9 | 10 | from aspy.refactor_imports.classify import classify_import 11 | from aspy.refactor_imports.classify import ImportType 12 | 13 | 14 | ENV_BLACKLIST = frozenset(( 15 | 'GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS', 'GIT_NOGLOB_PATHSPECS', 16 | )) 17 | SUPPORTED_CONF_FILES = ( 18 | '.editorconfig', '.isort.cfg', 'setup.cfg', 'tox.ini', 'pyproject.toml', 19 | ) 20 | THIRD_PARTY_RE = re.compile( 21 | r'^([ \t]*)known_third_party([ \t]*)=([ \t]*)(?:.*?)?(\r?)$', re.M, 22 | ) 23 | KNOWN_OTHER_RE = re.compile( 24 | r'^[ \t]*known_((?!third_party)\w+)[ \t]*=[ \t]*(.*)$', re.M, 25 | ) 26 | 27 | 28 | class Visitor(ast.NodeVisitor): 29 | def __init__(self, appdirs: Sequence[str] = ('.',)) -> None: 30 | self.appdirs = appdirs 31 | self.third_party: Set[str] = set() 32 | 33 | def _maybe_append_name(self, name: str) -> None: 34 | name, _, _ = name.partition('.') 35 | imp_type = classify_import(name, self.appdirs) 36 | if imp_type == ImportType.THIRD_PARTY: 37 | self.third_party.add(name) 38 | 39 | def visit_Import(self, node: ast.Import) -> None: 40 | if node.col_offset == 0: 41 | for name in node.names: 42 | self._maybe_append_name(name.name) 43 | 44 | def visit_ImportFrom(self, node: ast.ImportFrom) -> None: 45 | if node.col_offset == 0: 46 | if not node.level: 47 | assert node.module is not None # true for node.level == 0 48 | self._maybe_append_name(node.module) 49 | 50 | 51 | def third_party_imports( 52 | filenames: Sequence[str], 53 | appdirs: Sequence[str] = ('.',), 54 | ) -> Set[str]: 55 | visitor = Visitor(appdirs) 56 | for filename in filenames: 57 | if not os.path.exists(filename): 58 | continue 59 | with open(filename, 'rb') as f: 60 | visitor.visit(ast.parse(f.read(), filename=filename)) 61 | return visitor.third_party 62 | 63 | 64 | def ini_load(imports: str) -> Sequence[str]: 65 | return imports.strip().split(',') 66 | 67 | 68 | def ini_dump(imports: Sequence[str]) -> str: 69 | return ','.join(imports) 70 | 71 | 72 | def toml_load(imports: str) -> Sequence[str]: 73 | return ast.literal_eval(imports) 74 | 75 | 76 | def toml_dump(imports: Sequence[str]) -> str: 77 | return '[{}]'.format(', '.join(f'"{i}"' for i in imports)) 78 | 79 | 80 | def main(argv: Optional[Sequence[str]] = None) -> int: 81 | parser = argparse.ArgumentParser() 82 | parser.add_argument('--extra', action='append', default=[]) 83 | parser.add_argument('--exclude', default='^$') 84 | parser.add_argument( 85 | '--application-directories', default='.', 86 | help=( 87 | 'Colon separated directories that are considered top-level ' 88 | 'application directories. Defaults to `%(default)s`' 89 | ), 90 | ) 91 | parser.add_argument( 92 | '--settings-path', default='.', 93 | help=( 94 | 'Directory containing isort config file. ' 95 | 'Defaults to `%(default)s`' 96 | ), 97 | ) 98 | args = parser.parse_args(argv) 99 | 100 | cmd = ('git', 'ls-files', '--', '*.py') 101 | env = {k: v for k, v in os.environ.items() if k not in ENV_BLACKLIST} 102 | try: 103 | out = subprocess.check_output(cmd, env=env).decode('UTF-8') 104 | except OSError: 105 | raise OSError('Cannot find git. Make sure it is in your PATH') 106 | filenames = out.splitlines() + args.extra 107 | 108 | exclude = re.compile(args.exclude) 109 | filenames = [f for f in filenames if not exclude.search(f)] 110 | 111 | appdirs = args.application_directories.split(':') 112 | third_party = third_party_imports(filenames, appdirs) 113 | 114 | for filename in SUPPORTED_CONF_FILES: 115 | filename = os.path.join(args.settings_path, filename) 116 | if not os.path.exists(filename): 117 | continue 118 | 119 | if filename.endswith('.toml'): 120 | load = toml_load 121 | dump = toml_dump 122 | else: 123 | load = ini_load 124 | dump = ini_dump 125 | 126 | with open(filename, encoding='UTF-8', newline='') as f: 127 | contents = f.read() 128 | 129 | for match in KNOWN_OTHER_RE.finditer(contents): 130 | third_party -= set(load(match.group(2))) 131 | 132 | if THIRD_PARTY_RE.search(contents): 133 | third_party_s = dump(sorted(third_party)) 134 | replacement = fr'\1known_third_party\2=\3{third_party_s}\4' 135 | new_contents = THIRD_PARTY_RE.sub(replacement, contents) 136 | if new_contents == contents: 137 | return 0 138 | else: 139 | with open(filename, 'w', encoding='UTF-8', newline='') as f: 140 | f.write(new_contents) 141 | print(f'{filename} updated.') 142 | return 1 143 | else: 144 | filename = os.path.join(args.settings_path, '.isort.cfg') 145 | third_party_s = ','.join(sorted(third_party)) 146 | if os.path.exists(filename): 147 | prefix = 'Updating' 148 | mode = 'a' 149 | contents = f'known_third_party = {third_party_s}\n' 150 | else: 151 | prefix = 'Creating' 152 | mode = 'w' 153 | contents = f'[settings]\nknown_third_party = {third_party_s}\n' 154 | 155 | print( 156 | f'{prefix} an .isort.cfg with a known_third_party setting. ' 157 | f'Feel free to move the setting to a different config file in ' 158 | f'one of {", ".join(SUPPORTED_CONF_FILES)}.\n\n' 159 | f'This setting should be committed.', 160 | ) 161 | 162 | try: 163 | os.makedirs(args.settings_path) 164 | except OSError: 165 | if not os.path.isdir(args.settings_path): 166 | raise 167 | 168 | with open(filename, mode, encoding='UTF-8') as isort_cfg: 169 | isort_cfg.write(contents) 170 | return 1 171 | 172 | 173 | if __name__ == '__main__': 174 | exit(main()) 175 | -------------------------------------------------------------------------------- /tests/seed_isort_config_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from unittest import mock 4 | 5 | import pytest 6 | 7 | from seed_isort_config import KNOWN_OTHER_RE 8 | from seed_isort_config import main 9 | from seed_isort_config import third_party_imports 10 | from seed_isort_config import THIRD_PARTY_RE 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ('s', 'expected_groups'), 15 | ( 16 | ('[isort]\nknown_third_party=\n', ('', '', '', '')), 17 | ('[isort]\nknown_third_party = foo\n', ('', ' ', ' ', '')), 18 | ('[isort]\nknown_third_party\t=\tfoo\n', ('', '\t', '\t', '')), 19 | ('[isort]\nknown_third_party =\nknown_wat=wat\n', ('', ' ', '', '')), 20 | ('[isort]\r\nknown_third_party=\r\n', ('', '', '', '\r')), 21 | ('[isort]\r\nknown_third_party = foo\r\n', ('', ' ', ' ', '\r')), 22 | ('[isort]\r\nknown_third_party\t=\tfoo\r\n', ('', '\t', '\t', '\r')), 23 | ( 24 | '[isort]\r\nknown_third_party =\r\nknown_wat=wat\r\n', 25 | ('', ' ', '', '\r'), 26 | ), 27 | ( 28 | '[tool.isort]\r\n known_third_party =\r\nknown_wat=wat\r\n', 29 | (' ', ' ', '', '\r'), 30 | ), 31 | ( 32 | '[tool.isort]\r\n\tknown_third_party =\r\nknown_wat=wat\r\n', 33 | ('\t', ' ', '', '\r'), 34 | ), 35 | ), 36 | ) 37 | def test_known_third_party_re(s, expected_groups): 38 | match = THIRD_PARTY_RE.search(s) 39 | assert match 40 | assert match.groups() == expected_groups 41 | 42 | 43 | @pytest.mark.parametrize( 44 | ('s', 'expected_groups'), 45 | ( 46 | ('[isort]\nknown_other=\n', ('other', '')), 47 | ('[isort]\nknown_other = foo\n', ('other', 'foo')), 48 | ('[isort]\nknown_other\t=\tfoo\n', ('other', 'foo')), 49 | ('[isort]\nknown_other =\nknown_third_party=wat\n', ('other', '')), 50 | ('[tool.isort]\n known_other=\n', ('other', '')), 51 | ('[tool.isort]\n\tknown_other=\n', ('other', '')), 52 | ), 53 | ) 54 | def test_known_other_re(s, expected_groups): 55 | match = KNOWN_OTHER_RE.search(s) 56 | assert match 57 | assert match.groups() == expected_groups 58 | 59 | 60 | def test_list_third_party_imports(tmpdir): 61 | with tmpdir.as_cwd(): 62 | tmpdir.join('f.py').write('import cfgv\n') 63 | tmpdir.join('g.py').write('import os, pre_commit, f\n') 64 | tmpdir.join('h.py').write('from tokenize_rt import ESCAPED_NL\n') 65 | assert third_party_imports(()) == set() 66 | assert third_party_imports(('f.py',)) == {'cfgv'} 67 | assert third_party_imports(('f.py', 'g.py')) == {'pre_commit', 'cfgv'} 68 | assert third_party_imports(('h.py',)) == {'tokenize_rt'} 69 | 70 | 71 | def test_third_party_imports_pkg(tmpdir): 72 | with tmpdir.as_cwd(): 73 | pkgdir = tmpdir.join('pkg').ensure_dir() 74 | pkgdir.join('__init__.py').ensure() 75 | pkgdir.join('i.py').write('x = 1\n') 76 | pkgdir.join('j.py').write('from .i import x\n') 77 | assert third_party_imports(('pkg/i.py', 'pkg/j.py')) == set() 78 | 79 | 80 | def test_third_party_imports_not_top_level(tmpdir): 81 | with tmpdir.as_cwd(): 82 | tmpdir.join('f.py').write( 83 | 'import cfgv\n' 84 | 'try:\n' 85 | ' from x import y\n' 86 | 'except ImportError:\n' 87 | ' from z import y\n' 88 | 'try:\n' 89 | ' import x\n' 90 | 'except ImportError:\n' 91 | ' import y\n', 92 | ) 93 | assert third_party_imports(('f.py',)) == {'cfgv'} 94 | 95 | 96 | def _make_git(): 97 | subprocess.check_call(('git', 'init', '.')) 98 | subprocess.check_call(('git', 'add', '.')) 99 | 100 | 101 | def test_integration_isort_cfg(tmpdir): 102 | with tmpdir.as_cwd(): 103 | tmpdir.join('.isort.cfg').write('[settings]\nknown_third_party=\n') 104 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 105 | tmpdir.join('g.py').write('import f\nimport os\n') 106 | _make_git() 107 | 108 | assert main(()) == 1 109 | 110 | expected = '[settings]\nknown_third_party=cfgv,pre_commit\n' 111 | assert tmpdir.join('.isort.cfg').read() == expected 112 | 113 | 114 | def test_integration_known_packages(tmpdir): 115 | with tmpdir.as_cwd(): 116 | cfg = tmpdir.join('.isort.cfg') 117 | cfg.write('[settings]\nknown_django=django\nknown_third_party=\n') 118 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 119 | tmpdir.join('g.py').write('import f\nimport os\nimport django\n') 120 | _make_git() 121 | 122 | assert main(()) == 1 123 | 124 | expected = ( 125 | '[settings]\n' 126 | 'known_django=django\n' 127 | 'known_third_party=cfgv,pre_commit\n' 128 | ) 129 | assert cfg.read() == expected 130 | 131 | 132 | def test_integration_known_packages_pyproject_toml(tmpdir): 133 | with tmpdir.as_cwd(): 134 | cfg = tmpdir.join('pyproject.toml') 135 | cfg.write( 136 | '[tool.isort]\nknown_django=["django"]\nknown_third_party=[]\n', 137 | ) 138 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 139 | tmpdir.join('g.py').write('import f\nimport os\nimport django\n') 140 | _make_git() 141 | 142 | assert main(()) == 1 143 | 144 | expected = ( 145 | '[tool.isort]\n' 146 | 'known_django=["django"]\n' 147 | 'known_third_party=["cfgv", "pre_commit"]\n' 148 | ) 149 | assert cfg.read() == expected 150 | 151 | 152 | def test_integration_editorconfig(tmpdir): 153 | with tmpdir.as_cwd(): 154 | tmpdir.join('.editorconfig').write('[*.py]\nknown_third_party=cfgv\n') 155 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 156 | _make_git() 157 | 158 | assert main(()) == 1 159 | 160 | expected = '[*.py]\nknown_third_party=cfgv,pre_commit\n' 161 | assert tmpdir.join('.editorconfig').read() == expected 162 | 163 | 164 | @pytest.mark.parametrize('filename', ('setup.cfg', 'tox.ini')) 165 | def test_integration_non_isort_cfg(filename, tmpdir): 166 | with tmpdir.as_cwd(): 167 | tmpdir.join(filename).write('[isort]\nknown_third_party = cfgv\n') 168 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 169 | _make_git() 170 | 171 | assert main(()) == 1 172 | 173 | expected = '[isort]\nknown_third_party = cfgv,pre_commit\n' 174 | assert tmpdir.join(filename).read() == expected 175 | 176 | 177 | def test_integration_pyproject_toml(tmpdir): 178 | with tmpdir.as_cwd(): 179 | cfg = tmpdir.join('pyproject.toml') 180 | cfg.write('[tool.isort]\nknown_third_party = ["cfgv"]\n') 181 | tmpdir.join('f.py').write('import pre_commit\nimport cfgv\n') 182 | _make_git() 183 | 184 | assert main(()) == 1 185 | 186 | expected = '[tool.isort]\nknown_third_party = ["cfgv", "pre_commit"]\n' 187 | assert cfg.read() == expected 188 | 189 | 190 | def test_integration_multiple_config_files_exist(tmpdir): 191 | with tmpdir.as_cwd(): 192 | tmpdir.join('setup.cfg').write('[bdist_wheel]\nuniversal = 1\n') 193 | tmpdir.join('tox.ini').write('[isort]\nknown_third_party=\n') 194 | tmpdir.join('f.py').write('import cfgv') 195 | _make_git() 196 | 197 | assert main(()) == 1 198 | 199 | expected = '[isort]\nknown_third_party=cfgv\n' 200 | assert tmpdir.join('tox.ini').read() == expected 201 | 202 | 203 | def test_integration_extra_file(tmpdir): 204 | with tmpdir.as_cwd(): 205 | tmpdir.join('.isort.cfg').write('[settings]\nknown_third_party=\n') 206 | tmpdir.join('exe').write('import cfgv\n') 207 | tmpdir.join('f.py').write('import pre_commit\n') 208 | _make_git() 209 | 210 | assert main(()) == 1 211 | 212 | expected = '[settings]\nknown_third_party=pre_commit\n' 213 | assert tmpdir.join('.isort.cfg').read() == expected 214 | 215 | assert main(('--extra', 'exe')) 216 | 217 | expected = '[settings]\nknown_third_party=cfgv,pre_commit\n' 218 | assert tmpdir.join('.isort.cfg').read() == expected 219 | 220 | 221 | @pytest.mark.parametrize( 222 | ('initial_filesystem', 'expected_filesystem'), 223 | ( 224 | ( 225 | (), 226 | (('.isort.cfg', '[settings]\nknown_third_party = cfgv\n'),), 227 | ), 228 | ( 229 | (('.isort.cfg', '[settings]\ncombine_as_imports = true\n'),), 230 | ( 231 | ( 232 | '.isort.cfg', 233 | '[settings]\n' 234 | 'combine_as_imports = true\n' 235 | 'known_third_party = cfgv\n', 236 | ), 237 | ), 238 | ), 239 | ( 240 | (('setup.cfg', '[bdist_wheel]\nuniversal = True\n'),), 241 | (('.isort.cfg', '[settings]\nknown_third_party = cfgv\n'),), 242 | ), 243 | ), 244 | ) 245 | def test_integration_no_section( 246 | tmpdir, 247 | initial_filesystem, 248 | expected_filesystem, 249 | ): 250 | with tmpdir.as_cwd(): 251 | tmpdir.join('f.py').write('import cfgv') 252 | for filename, initial in initial_filesystem: 253 | tmpdir.join(filename).write(initial) 254 | _make_git() 255 | 256 | assert main(()) == 1 257 | 258 | for filename, expected in expected_filesystem: 259 | assert tmpdir.join(filename).read() == expected 260 | 261 | 262 | def test_integration_src_layout(tmpdir): 263 | with tmpdir.as_cwd(): 264 | src = tmpdir.join('src').ensure_dir() 265 | src.join('f.py').write('import cfgv') 266 | src.join('g.py').write('import f') 267 | _make_git() 268 | 269 | assert main(('--application-directories', 'src')) == 1 270 | 271 | expected = '[settings]\nknown_third_party = cfgv\n' 272 | assert tmpdir.join('.isort.cfg').read() == expected 273 | 274 | 275 | def test_integration_settings_path(tmpdir): 276 | with tmpdir.as_cwd(): 277 | src = tmpdir.join('src').ensure_dir() 278 | src.join('f.py').write('import cfgv') 279 | _make_git() 280 | 281 | assert main(('--settings-path', 'cfg')) == 1 282 | 283 | expected = '[settings]\nknown_third_party = cfgv\n' 284 | assert tmpdir.join('cfg/.isort.cfg').read() == expected 285 | assert not tmpdir.join('.isort.cfg').exists() 286 | 287 | 288 | def test_integration_git_literal_pathspecs_1(tmpdir): 289 | """an emacs plugin, magit calls pre-commit in this way, see #5""" 290 | with mock.patch.dict(os.environ, {'GIT_LITERAL_PATHSPECS': '1'}): 291 | test_integration_isort_cfg(tmpdir) 292 | 293 | 294 | def test_integration_git_noglob_pathspecs(tmpdir): 295 | """see #47""" 296 | with mock.patch.dict(os.environ, {'GIT_NOGLOB_PATHSPECS': '1'}): 297 | test_integration_isort_cfg(tmpdir) 298 | 299 | 300 | def test_exclude(tmpdir): 301 | with tmpdir.as_cwd(): 302 | tmpdir.join('f.py').write('import cfgv\n') 303 | tmpdir.join('g.py').write('syntax error') 304 | _make_git() 305 | 306 | assert main(('--exclude', '^g.py$')) == 1 307 | 308 | expected = '[settings]\nknown_third_party = cfgv\n' 309 | assert tmpdir.join('.isort.cfg').read() == expected 310 | 311 | 312 | def test_returns_zero_no_changes(tmpdir): 313 | with tmpdir.as_cwd(): 314 | cfg = tmpdir.join('.isort.cfg') 315 | cfg.write('[settings]\nknown_third_party=cfgv\n') 316 | tmpdir.join('f.py').write('import cfgv\n') 317 | _make_git() 318 | 319 | assert main(()) == 0 320 | 321 | assert cfg.read() == '[settings]\nknown_third_party=cfgv\n' 322 | 323 | 324 | def test_returns_zero_no_changes_pyproject_toml(tmpdir): 325 | with tmpdir.as_cwd(): 326 | cfg = tmpdir.join('pyproject.toml') 327 | cfg.write('[settings]\nknown_third_party=["cfgv"]\n') 328 | tmpdir.join('f.py').write('import cfgv\n') 329 | _make_git() 330 | 331 | assert main(()) == 0 332 | 333 | assert cfg.read() == '[settings]\nknown_third_party=["cfgv"]\n' 334 | 335 | 336 | def test_removing_file_after_git_add(tmpdir): 337 | """regression test for issue #37""" 338 | with tmpdir.as_cwd(): 339 | tmpdir.join('.isort.cfg').write('[settings]\nknown_third_party=\n') 340 | tmpdir.join('f.py').write('import pre_commit\n') 341 | tmpdir.join('g.py').write('import cfgv\n') 342 | _make_git() 343 | 344 | tmpdir.join('g.py').remove() 345 | 346 | assert main(()) == 1 347 | 348 | expected = '[settings]\nknown_third_party=pre_commit\n' 349 | assert tmpdir.join('.isort.cfg').read() == expected 350 | 351 | 352 | def test_missing_git_from_path(tmpdir): 353 | """expect user-friendly error message for a missing git""" 354 | with mock.patch.object(subprocess, 'check_output', side_effect=OSError): 355 | with tmpdir.as_cwd(): 356 | _make_git() 357 | with pytest.raises(OSError) as excinfo: 358 | main(()) 359 | msg, = excinfo.value.args 360 | assert msg == 'Cannot find git. Make sure it is in your PATH' 361 | 362 | 363 | def test_newlines_preserved(tmpdir): 364 | with tmpdir.as_cwd(): 365 | cfg = tmpdir.join('.isort.cfg') 366 | cfg.write_binary(b'[settings]\nknown_third_party=\r\n') 367 | tmpdir.join('g.py').write('import cfgv\n') 368 | _make_git() 369 | 370 | assert main(()) == 1 371 | 372 | expected = b'[settings]\nknown_third_party=cfgv\r\n' 373 | assert cfg.read_binary() == expected 374 | 375 | 376 | def test_output_file_changed(tmpdir, capsys): 377 | with tmpdir.as_cwd(): 378 | cfg = tmpdir.join('.isort.cfg') 379 | cfg.write_binary(b'[settings]\nknown_third_party=\r\n') 380 | tmpdir.join('g.py').write('import cfgv\n') 381 | _make_git() 382 | 383 | assert main(()) == 1 384 | 385 | assert './.isort.cfg updated.\n' == capsys.readouterr().out 386 | 387 | expected = b'[settings]\nknown_third_party=cfgv\r\n' 388 | assert cfg.read_binary() == expected 389 | 390 | 391 | def test_indentation_preserved(tmpdir): 392 | with tmpdir.as_cwd(): 393 | cfg = tmpdir.join('pyproject.toml') 394 | cfg.write_binary(b'[tool.isort]\n\tknown_third_party=\r\n') 395 | tmpdir.join('g.py').write('import cfgv\n') 396 | _make_git() 397 | 398 | assert main(()) == 1 399 | 400 | expected = b'[tool.isort]\n\tknown_third_party=["cfgv"]\r\n' 401 | assert cfg.read_binary() == expected 402 | --------------------------------------------------------------------------------