diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bbcc9dd4..39a9110b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,12 +22,12 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + pip install pytest pytest-mock pip install pyyaml==6.0 pip install -e .[test] - name: Run tests run: | - pytest -vv tests/ + pytest -vv coverage: name: "Coverage" @@ -41,12 +41,12 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-cov coveralls + pip install pytest pytest-cov pytest-mock coveralls pip install pyyaml==6.0 pip install -e .[test] - name: Run tests run: | - pytest --cov=ssort -v tests/ + pytest --cov=ssort -v - name: Upload coverage report to coveralls run: | coveralls --service=github @@ -102,7 +102,7 @@ jobs: pip install -e . - name: Run ssort run: | - ssort --check --diff src/ tests/ + ssort --check --diff . pyflakes: name: "PyFlakes" diff --git a/pyproject.toml b/pyproject.toml index 5ab348ed..d05790ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,11 @@ exclude = "test_data/samples/*" ignore_missing_imports = true module = "pathspec" +[tool.pytest.ini_options] +testpaths = [ + "tests" +] + [tool.setuptools] include-package-data = false license-files = [ @@ -75,3 +80,10 @@ attr = "ssort.__version__" [tool.setuptools.packages.find] where = ["src"] + +[tool.ssort] +extend_skip = [ + ".pytest_cache", + "__pycache__", + "test_data" +] diff --git a/src/ssort/_config.py b/src/ssort/_config.py new file mode 100644 index 00000000..f9921a04 --- /dev/null +++ b/src/ssort/_config.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import sys +from dataclasses import dataclass, field +from pathlib import Path + +if sys.version_info >= (3, 11): + from tomllib import load +else: + from tomli import load + + +__all__ = ["get_config_from_root"] + + +DEFAULT_SKIP = frozenset( + { + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + } +) + + +def iter_valid_python_files_recursive(folder, *, is_invalid): + for child in folder.iterdir(): + if is_invalid(child): + continue + + elif child.is_file() and child.suffix == ".py": + yield child + + elif child.is_dir(): + yield from iter_valid_python_files_recursive( + child, is_invalid=is_invalid + ) + + +@dataclass(frozen=True) +class Config: + skip: frozenset | list = DEFAULT_SKIP + skip_glob: list = field(default_factory=list) + extend_skip: list = field(default_factory=list) + + def is_invalid(self, path): + if path.name in (set(self.skip) | set(self.extend_skip)): + return True + + for pat in self.skip_glob: + if path.is_file() and path.match(pat): + return True + + return False + + def iterate_files_matching_patterns(self, pattern): + for pat in pattern: + path = Path(pat).resolve() + + if path.is_file() and path.suffix == ".py": + yield path + + elif path.is_dir(): + yield from iter_valid_python_files_recursive( + path, is_invalid=self.is_invalid + ) + + +def parse_pyproject_toml(path): + with open(path, "rb") as fh: + pyproject_toml = load(fh) + + return pyproject_toml.get("tool", {}).get("ssort", {}) + + +def get_config_from_root(root): + path_pyproject_toml = root / "pyproject.toml" + + if path_pyproject_toml.exists(): + config_dict = parse_pyproject_toml(path_pyproject_toml) + else: + config_dict = {} + + return Config(**config_dict) diff --git a/src/ssort/_files.py b/src/ssort/_files.py index aca21ec8..13b8ce86 100644 --- a/src/ssort/_files.py +++ b/src/ssort/_files.py @@ -1,72 +1,33 @@ -from __future__ import annotations +from pathlib import Path -import os -import pathlib -from typing import Iterable +__all__ = ["find_project_root"] -import pathspec -from ssort._utils import memoize +def current_working_dir(): + return Path(".").resolve() -_EMPTY_PATH_SPEC = pathspec.PathSpec([]) +def find_project_root(patterns): + all_patterns = [current_working_dir()] -@memoize -def _is_project_root(path: pathlib.Path) -> bool: - if path == path.root or path == path.parent: - return True + if patterns: + all_patterns.extend(patterns) - if (path / ".git").is_dir(): - return True + paths = [Path(p).resolve() for p in all_patterns] + parents_and_self = [ + list(reversed(p.parents)) + ([p] if p.is_dir() else []) for p in paths + ] - return False + *_, (common_base, *_) = ( + common_parent + for same_lvl_parent in zip(*parents_and_self) + if len(common_parent := set(same_lvl_parent)) == 1 + ) + for directory in (common_base, *common_base.parents): + if (directory / ".git").exists() or ( + directory / "pyproject.toml" + ).is_file(): + return directory -@memoize -def _get_ignore_patterns(path: pathlib.Path) -> pathspec.PathSpec: - git_ignore = path / ".gitignore" - if git_ignore.is_file(): - with git_ignore.open() as f: - return pathspec.PathSpec.from_lines("gitwildmatch", f) - - return _EMPTY_PATH_SPEC - - -def is_ignored(path: str | os.PathLike) -> bool: - # Can't use pathlib.Path.resolve() here because we want to maintain - # symbolic links. - path = pathlib.Path(os.path.abspath(path)) - - for part in (path, *path.parents): - patterns = _get_ignore_patterns(part) - if patterns.match_file(path.relative_to(part)): - return True - - if _is_project_root(part): - return False - - return False - - -def find_python_files( - patterns: Iterable[str | os.PathLike[str]], -) -> Iterable[pathlib.Path]: - if not patterns: - patterns = ["."] - - paths_set = set() - for pattern in patterns: - path = pathlib.Path(pattern) - if not path.is_dir(): - subpaths = [path] - else: - subpaths = [ - subpath - for subpath in path.glob("**/*.py") - if not is_ignored(subpath) and subpath.is_file() - ] - - for subpath in sorted(subpaths): - if subpath not in paths_set: - paths_set.add(subpath) - yield subpath + return common_base diff --git a/src/ssort/_main.py b/src/ssort/_main.py index a7efa1d7..ee6cfd51 100644 --- a/src/ssort/_main.py +++ b/src/ssort/_main.py @@ -3,8 +3,9 @@ import re import sys +from ssort._config import get_config_from_root from ssort._exceptions import UnknownEncodingError -from ssort._files import find_python_files +from ssort._files import find_project_root from ssort._ssort import ssort from ssort._utils import ( detect_encoding, @@ -42,19 +43,14 @@ def main(): unsortable = 0 unchanged = 0 - for path in find_python_files(args.files): + root = find_project_root(args.files) + config = get_config_from_root(root) + + for path in config.iterate_files_matching_patterns(args.files): errors = False try: original_bytes = path.read_bytes() - except FileNotFoundError: - sys.stderr.write(f"ERROR: {escape_path(path)} does not exist\n") - unsortable += 1 - continue - except IsADirectoryError: - sys.stderr.write(f"ERROR: {escape_path(path)} is a directory\n") - unsortable += 1 - continue except PermissionError: sys.stderr.write(f"ERROR: {escape_path(path)} is not readable\n") unsortable += 1 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..4a0722fd --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,261 @@ +from pathlib import Path + +import pytest + +import ssort._config as config + + +def test_default_skip_defined(): + assert hasattr(config, "DEFAULT_SKIP") + + +class TestIterValidPythonFiles: + @pytest.fixture() + def names(self): + return [ + "apple.py", + "cat.py", + "cats.py", + "dir/bark.py", + "dir/meow.py", + "dog.py", + "cats/not_a_cat.py", + ] + + @pytest.fixture() + def skip_glob(self): + return ["dir/*", "cat*"] + + @pytest.fixture() + def is_invalid_glob(self, tmp_path): + def fun(path): + return path.relative_to(tmp_path) in [ + Path("cat.py"), + Path("cats.py"), + Path("dir/bark.py"), + Path("dir/meow.py"), + ] + + return fun + + @pytest.fixture() + def is_invalid(self, names): + def fun(name): + return name in names + + return fun + + @pytest.fixture() + def folder(self, tmp_path, names): + for name in names: + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + return tmp_path + + @pytest.fixture() + def files(self, names, folder): + return [folder / name for name in names] + + def test_is_invalid_skip_only(self, names, folder, is_invalid, files): + cfg = config.Config(skip=names) + + valid = folder / "banana.py" + valid.touch() + files.append(valid) + + valid = folder / "dir" / "banana.py" + valid.touch() + files.append(valid) + + for file in files: + assert is_invalid(file.name) == cfg.is_invalid(file) + + def test_is_invalid_extend_skip_only( + self, names, folder, is_invalid, files + ): + cfg = config.Config(skip=[], extend_skip=names) + + valid = folder / "banana.py" + valid.touch() + files.append(valid) + + valid = folder / "dir" / "banana.py" + valid.touch() + files.append(valid) + + for file in files: + assert is_invalid(file.name) == cfg.is_invalid(file) + + def test_is_invalid_skip_glob_only( + self, skip_glob, is_invalid_glob, files + ): + cfg = config.Config(skip=[], skip_glob=skip_glob) + + for file in files: + assert is_invalid_glob(file) == cfg.is_invalid(file) + assert is_invalid_glob(file.parent) == cfg.is_invalid(file.parent) + + def test_iter_valid_python_files_recursive( + self, folder, is_invalid, names + ): + ret = list( + config.iter_valid_python_files_recursive( + folder, is_invalid=is_invalid + ) + ) + + assert len(ret) == len(names) + + for name in ret: + assert str(name.relative_to(folder)) in names + + def test_iter_valid_python_files_recursive_empty(self, tmp_path): + with pytest.raises(StopIteration): + next( + config.iter_valid_python_files_recursive( + tmp_path, is_invalid=lambda x: True + ) + ) + + +class TestConfig: + def test___init___default_values(self): + cfg = config.Config() + + assert hasattr(cfg, "skip") + assert hasattr(cfg, "extend_skip") + + assert isinstance(cfg.skip, frozenset) + assert len(cfg.skip) > 0 + + assert cfg.extend_skip == [] + + def test___init___overwrite_default(self): + cfg = config.Config(skip="no", extend_skip="nono") + + assert cfg.skip == "no" + assert cfg.extend_skip == "nono" + + def test_is_invalid(self): + skip = ["hello", "banana"] + extend_skip = ["world"] + + cfg = config.Config(skip=skip, extend_skip=extend_skip) + + skip.extend(extend_skip) + + for s in skip: + assert cfg.is_invalid(Path(s)) + assert cfg.is_invalid(Path("directory") / s) + assert not cfg.is_invalid(Path(s) / "directory") + + for s in ["apple", "bananas", "bananas/worlds", "Hello"]: + assert not cfg.is_invalid(Path(s)) + + @pytest.fixture() + def mock_iter(self, mocker): + return mocker.patch( + "ssort._config.iter_valid_python_files_recursive", + return_value=[None], + ) + + def test_iterate_files_matching_patterns_existing_python_file( + self, tmp_path, mock_iter + ): + path = tmp_path / "file.py" + path.touch() + + cfg = config.Config() + ret = cfg.iterate_files_matching_patterns([path]) + + assert list(ret) == [path] + mock_iter.assert_not_called() + + def test_iterate_files_matching_patterns_missing_python_file( + self, tmp_path, mock_iter + ): + path = tmp_path / "file.py" + + cfg = config.Config() + ret = cfg.iterate_files_matching_patterns([path]) + + assert list(ret) == [] + mock_iter.assert_not_called() + + def test_iterate_files_matching_patterns_dir(self, tmp_path, mock_iter): + path0 = tmp_path / "dir0" + path1 = tmp_path / "dir1" + path0.mkdir() + path1.mkdir() + + cfg = config.Config() + ret = cfg.iterate_files_matching_patterns([path0, path1]) + + assert list(ret) == [None, None] + mock_iter.assert_any_call(path0, is_invalid=cfg.is_invalid) + mock_iter.assert_any_call(path1, is_invalid=cfg.is_invalid) + assert mock_iter.call_count == 2 + + +@pytest.fixture() +def toml(tmp_path): + toml = """ + [tool.ssort] + banana = 'banana' + extend_skip = ['extend_skip'] + skip = ['skip'] + name-with-dash = '---' + + [tool.ssort.class] + sort_order = [] + name-with-dash = '---' + """ + + path = tmp_path / "test.tml" + with open(path, "w") as fh: + fh.write(toml) + + return path + + +def test_parse_pyproject_toml(toml, mocker): + ret = config.parse_pyproject_toml(toml) + + assert ret["banana"] == "banana" + assert ret["name-with-dash"] == "---" + assert ret["skip"] == ["skip"] + + assert ret["class"]["name-with-dash"] == "---" + assert ret["class"]["sort_order"] == [] + assert ret["extend_skip"] == ["extend_skip"] + + +def test_get_config_from_root_exists(mocker, tmp_path): + return_value = {"skyscraper": "tall"} + mock_parse = mocker.patch( + "ssort._config.parse_pyproject_toml", return_value=return_value + ) + mock_config = mocker.patch("ssort._config.Config") + + path = tmp_path / "pyproject.toml" + path.touch() + assert path.exists() + + config.get_config_from_root(path.parent) + + mock_parse.assert_called_once_with(path) + mock_config.assert_called_once_with(**return_value) + + +def test_get_config_from_root_not_exists(mocker, tmp_path): + mock_parse = mocker.patch("ssort._config.parse_pyproject_toml") + mock_config = mocker.patch("ssort._config.Config") + + path = tmp_path / "pyproject.toml" + assert not path.exists() + + config.get_config_from_root(path.parent) + + mock_parse.assert_not_called() + mock_config.assert_called_once_with(**{}) diff --git a/tests/test_executable.py b/tests/test_executable.py index 0260d263..83eac71d 100644 --- a/tests/test_executable.py +++ b/tests/test_executable.py @@ -65,6 +65,11 @@ def _write_fixtures(dirpath, texts): return paths +@pytest.fixture(autouse=True) +def root(mocker, tmp_path): + mocker.patch("ssort._main.find_project_root", return_value=tmp_path) + + @pytest.fixture(params=["entrypoint", "module"]) def check(request): def _check(dirpath): @@ -357,11 +362,8 @@ def test_ssort_empty_dir(ssort, tmp_path): def test_ssort_non_existent_file(ssort, tmp_path): path = tmp_path / "file.py" - expected_msgs = [ - f"ERROR: {escape_path(path)} does not exist\n", - "1 file was not sortable\n", - ] - expected_status = 1 + expected_msgs = ["No files are present to be sorted. Nothing to do.\n"] + expected_status = 0 actual_msgs, actual_status = ssort(path) @@ -371,7 +373,7 @@ def test_ssort_non_existent_file(ssort, tmp_path): def test_ssort_no_py_extension(ssort, tmp_path): path = tmp_path / "file" path.write_bytes(_good) - expected_msgs = ["1 file was left unchanged\n"] + expected_msgs = ["No files are present to be sorted. Nothing to do.\n"] expected_status = 0 actual_msgs, actual_status = ssort(path) assert (actual_msgs, actual_status) == (expected_msgs, expected_status) diff --git a/tests/test_files.py b/tests/test_files.py index 42a0c55e..dcbef2e2 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,223 +1,89 @@ -from __future__ import annotations - -import pathlib +from pathlib import Path import pytest -from ssort._files import is_ignored - - -def test_ignore_git( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / ".git").mkdir() - (tmp_path / ".gitignore").write_text("ignored") - - assert not is_ignored("src") - assert not is_ignored("src/main.py") - - assert is_ignored("ignored") - assert is_ignored("ignored/main.py") - - assert is_ignored("src/ignored") - assert is_ignored("src/ignored/main.py") - - assert not is_ignored("../ignored") - assert not is_ignored("../ignored/main.py") - - assert is_ignored(f"../{tmp_path.name}/ignored") - assert is_ignored(f"../{tmp_path.name}/ignored/main.py") - - -def test_ignore_git_with_no_repo( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / ".gitignore").write_text("ignored") - - assert not is_ignored("src") - assert not is_ignored("src/main.py") - - assert is_ignored("ignored") - assert is_ignored("ignored/main.py") - - assert is_ignored("src/ignored") - assert is_ignored("src/ignored/main.py") - - assert not is_ignored("../ignored") - assert not is_ignored("../ignored/main.py") - - assert is_ignored(f"../{tmp_path.name}/ignored") - assert is_ignored(f"../{tmp_path.name}/ignored/main.py") - - -def test_ignore_git_in_subdirectory( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / ".git").mkdir() - (tmp_path / ".gitignore").write_text("parent") - - (tmp_path / "sub").mkdir() - (tmp_path / "sub" / ".gitignore").write_text("child") - - assert not is_ignored("src") - assert not is_ignored("src/main.py") - assert not is_ignored("sub/src") - assert not is_ignored("sub/src/main.py") - - assert is_ignored("parent") - assert is_ignored("parent/main.py") - assert is_ignored("sub/parent") - assert is_ignored("sub/parent/main.py") - - assert is_ignored("src/parent") - assert is_ignored("src/parent/main.py") - assert is_ignored("sub/src/parent") - assert is_ignored("sub/src/parent/main.py") - - assert not is_ignored("../parent") - assert not is_ignored("../parent/main.py") - assert not is_ignored("../sub/parent") - assert not is_ignored("../sub/parent/main.py") - - assert is_ignored(f"../{tmp_path.name}/parent") - assert is_ignored(f"../{tmp_path.name}/parent/main.py") - assert is_ignored(f"../{tmp_path.name}/sub/parent") - assert is_ignored(f"../{tmp_path.name}/sub/parent/main.py") - - assert not is_ignored("child") - assert not is_ignored("child/main.py") - assert is_ignored("sub/child") - assert is_ignored("sub/child/main.py") - - assert not is_ignored("src/child") - assert not is_ignored("src/child/main.py") - assert is_ignored("sub/src/child") - assert is_ignored("sub/src/child/main.py") - - assert not is_ignored("sub/../child") - assert not is_ignored("sub/../child/main.py") - - assert is_ignored(f"../{tmp_path.name}/sub/child") - assert is_ignored(f"../{tmp_path.name}/sub/child/main.py") - - -def test_ignore_git_in_working_subdirectory( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - (tmp_path / ".git").mkdir() - (tmp_path / ".gitignore").write_text("ignored") - - (tmp_path / "sub").mkdir() - monkeypatch.chdir(tmp_path / "sub") - - assert not is_ignored("src") - assert not is_ignored("src/main.py") - - assert is_ignored("ignored") - assert is_ignored("ignored/main.py") - - assert is_ignored("src/ignored") - assert is_ignored("src/ignored/main.py") - - assert is_ignored("../ignored") - assert is_ignored("../ignored/main.py") - - assert is_ignored("../sub/ignored") - assert is_ignored("../sub/ignored/main.py") - - assert not is_ignored("../../ignored") - assert not is_ignored("../../ignored/main.py") - - -def test_ignore_git_in_working_parent_directory( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / "sub").mkdir() - (tmp_path / "sub" / ".git").mkdir() - (tmp_path / "sub" / ".gitignore").write_text("ignored") - - assert not is_ignored("ignored") - assert not is_ignored("ignored/main.py") - - assert is_ignored("sub/ignored") - assert is_ignored("sub/ignored/main.py") - - assert is_ignored("sub/src/ignored") - assert is_ignored("sub/src/ignored/main.py") - - assert not is_ignored("sub/../ignored") - assert not is_ignored("sub/../ignored/main.py") - - -def test_ignore_git_subdirectory_pattern( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / ".git").mkdir() - (tmp_path / ".gitignore").write_text("sub/ignored") - - (tmp_path / "sub").mkdir() - - assert not is_ignored("sub") - assert not is_ignored("sub/main.py") - - assert is_ignored("sub/ignored") - assert is_ignored("sub/ignored/main.py") - - -def test_ignore_git_symlink_recursive( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / ".git").mkdir() - (tmp_path / ".gitignore").write_text("ignored") - - (tmp_path / "dir").mkdir() - (tmp_path / "dir" / "link").symlink_to(tmp_path / "dir") - - assert not is_ignored("dir") - assert not is_ignored("dir/link") - assert not is_ignored("dir/link/link") - - assert is_ignored("dir/ignored") - assert is_ignored("dir/link/ignored") - assert is_ignored("dir/link/link/ignored") - - -def test_ignore_git_symlink_outside_repo( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / "repo" / ".git").mkdir(parents=True) - (tmp_path / "repo" / ".gitignore").write_text("link") - - (tmp_path / "link").mkdir() - (tmp_path / "repo" / "link").symlink_to(tmp_path / "link") - - assert not is_ignored("link") - assert not is_ignored("link/main.py") - assert is_ignored("repo/link") - assert is_ignored("repo/link/main.py") - - -def test_ignore_symlink_circular( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.chdir(tmp_path) - - (tmp_path / "link1").symlink_to(tmp_path / "link2") - (tmp_path / "link2").symlink_to(tmp_path / "link1") - - assert not is_ignored("link1") - assert not is_ignored("link2") +from ssort._files import current_working_dir, find_project_root + + +def test_current_working_dir(): + assert current_working_dir() == Path(".").resolve() + + +class TestFindProjectRoot: + @pytest.fixture( + params=[ + (".",), + (".", "dir"), + ("dir", "dirA/B"), + ("dirA/B", "dir"), + ("a/a/a", "b/b/b"), + ("dir/a", "dir/b"), + ] + ) + def subdir(self, request): + return request.param + + @pytest.fixture() + def mock_current_working_dir(self, mocker, tmp_path): + return mocker.patch( + "ssort._files.current_working_dir", return_value=tmp_path / "root" + ) + + @pytest.fixture() + def git(self, tmp_path): + root = tmp_path / "root" + (root / ".git").mkdir(parents=True) + return root + + def test_find_project_root_git( + self, subdir, git, mock_current_working_dir + ): + print(subdir) + patterns = [git / sub for sub in subdir] + + for p in patterns: + p.mkdir(parents=True, exist_ok=True) + + assert git == find_project_root(patterns) + mock_current_working_dir.assert_called_once() + + @pytest.fixture() + def pyproject(self, tmp_path): + root = tmp_path / "root" + root.mkdir() + (root / "pyproject.toml").touch() + return root + + def test_find_project_root_pyproject( + self, subdir, pyproject, mock_current_working_dir + ): + patterns = [pyproject / sub for sub in subdir] + + for p in patterns: + p.mkdir(parents=True, exist_ok=True) + + assert pyproject == find_project_root(patterns) + mock_current_working_dir.assert_called_once() + + @pytest.fixture() + def neither(self, tmp_path): + root = tmp_path / "root" + root.mkdir() + return root + + def test_find_project_root_neither( + self, subdir, neither, mocker, tmp_path + ): + mocker.patch( + "ssort._files.current_working_dir", + return_value=tmp_path / "root" / "dir", + ) + patterns = [neither / sub for sub in subdir] + + if all(s.startswith("dir/") for s in subdir): + neither = neither / "dir" + + for p in patterns: + p.mkdir(parents=True, exist_ok=True) + + assert neither == find_project_root(patterns) diff --git a/tox.ini b/tox.ini index 18f14fa8..1218d129 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,10 @@ isolated_build = true [testenv] deps = pytest + pytest-mock pyyaml==6.0 commands = - pytest -vv tests/ + pytest -vv [testenv:black] deps = @@ -25,7 +26,7 @@ commands = [testenv:ssort] commands = - ssort --check --diff src/ tests/ + ssort --check --diff . [testenv:pyflakes] deps =