undersort 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: undersort
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A tool to sort class methods by visibility (public, protected, private)
5
5
  Project-URL: Homepage, https://github.com/kivicode/undersort
6
6
  Project-URL: Repository, https://github.com/kivicode/undersort
@@ -68,6 +68,10 @@ order = ["public", "protected", "private"]
68
68
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
69
69
  # Default: ["instance", "class", "static"]
70
70
  method_type_order = ["instance", "class", "static"]
71
+
72
+ # Exclude files/directories matching these patterns (optional)
73
+ # Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
74
+ # exclude = ["tests/*", "migrations/*.py"]
71
75
  ```
72
76
 
73
77
  ### Method Visibility Rules
@@ -185,9 +189,15 @@ undersort --diff example.py
185
189
 
186
190
  # Combine flags
187
191
  undersort --check --diff src/
192
+
193
+ # Exclude specific files or directories
194
+ undersort --exclude "tests/*" --exclude "migrations/*.py" src/
195
+
196
+ # Multiple exclude patterns (can be combined with config file patterns)
197
+ undersort --exclude "test_*.py" --exclude "*/legacy/*" .
188
198
  ```
189
199
 
190
- **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively.
200
+ **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively. You can add custom exclusions via CLI flags or the config file.
191
201
 
192
202
  ### Pre-commit Integration
193
203
 
@@ -41,6 +41,10 @@ order = ["public", "protected", "private"]
41
41
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
42
42
  # Default: ["instance", "class", "static"]
43
43
  method_type_order = ["instance", "class", "static"]
44
+
45
+ # Exclude files/directories matching these patterns (optional)
46
+ # Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
47
+ # exclude = ["tests/*", "migrations/*.py"]
44
48
  ```
45
49
 
46
50
  ### Method Visibility Rules
@@ -158,9 +162,15 @@ undersort --diff example.py
158
162
 
159
163
  # Combine flags
160
164
  undersort --check --diff src/
165
+
166
+ # Exclude specific files or directories
167
+ undersort --exclude "tests/*" --exclude "migrations/*.py" src/
168
+
169
+ # Multiple exclude patterns (can be combined with config file patterns)
170
+ undersort --exclude "test_*.py" --exclude "*/legacy/*" .
161
171
  ```
162
172
 
163
- **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively.
173
+ **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively. You can add custom exclusions via CLI flags or the config file.
164
174
 
165
175
  ### Pre-commit Integration
166
176
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "undersort"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A tool to sort class methods by visibility (public, protected, private)"
9
9
  authors = [{ name = "KiviCode", email = "kivicode.dev@gmail.com" }]
10
10
  readme = "README.md"
@@ -50,6 +50,10 @@ order = ["public", "protected", "private"]
50
50
  # Default order if not specified: ["instance", "class", "static"]
51
51
  method_type_order = ["instance", "class", "static"]
52
52
 
53
+ # Exclude files/directories matching these patterns (optional)
54
+ # Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
55
+ # exclude = ["tests/*", "migrations/*.py"]
56
+
53
57
  [tool.ruff]
54
58
  unsafe-fixes = true
55
59
  exclude = [".venv"]
@@ -14,7 +14,7 @@ class TestConfigLoading:
14
14
  """Test that default config is returned when no pyproject.toml exists."""
15
15
  monkeypatch.chdir(tmp_path)
16
16
  config = load_config()
17
- assert config == {"order": ["public", "protected", "private"], "method_type_order": None}
17
+ assert config == {"order": ["public", "protected", "private"], "method_type_order": None, "exclude": None}
18
18
 
19
19
  def test_load_custom_order(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
20
20
  """Test loading custom order from pyproject.toml."""
@@ -27,7 +27,7 @@ order = ["private", "protected", "public"]
27
27
 
28
28
  monkeypatch.chdir(tmp_path)
29
29
  config = load_config()
30
- assert config == {"order": ["private", "protected", "public"], "method_type_order": None}
30
+ assert config == {"order": ["private", "protected", "public"], "method_type_order": None, "exclude": None}
31
31
 
32
32
  def test_invalid_order_values(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
33
33
  """Test that invalid order values fall back to default."""
@@ -40,7 +40,7 @@ order = ["public", "invalid", "private"]
40
40
 
41
41
  monkeypatch.chdir(tmp_path)
42
42
  config = load_config()
43
- assert config == {"order": ["public", "protected", "private"], "method_type_order": None}
43
+ assert config == {"order": ["public", "protected", "private"], "method_type_order": None, "exclude": None}
44
44
 
45
45
  def test_missing_order_key(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
46
46
  """Test that missing order key returns default."""
@@ -53,7 +53,7 @@ some_other_key = "value"
53
53
 
54
54
  monkeypatch.chdir(tmp_path)
55
55
  config = load_config()
56
- assert config == {"order": ["public", "protected", "private"], "method_type_order": None}
56
+ assert config == {"order": ["public", "protected", "private"], "method_type_order": None, "exclude": None}
57
57
 
58
58
  def test_missing_tool_section(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
59
59
  """Test that missing tool.undersort section returns default."""
@@ -66,7 +66,7 @@ name = "test"
66
66
 
67
67
  monkeypatch.chdir(tmp_path)
68
68
  config = load_config()
69
- assert config == {"order": ["public", "protected", "private"], "method_type_order": None}
69
+ assert config == {"order": ["public", "protected", "private"], "method_type_order": None, "exclude": None}
70
70
 
71
71
  def test_find_pyproject_in_parent(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
72
72
  """Test that pyproject.toml is found in parent directories."""
@@ -84,7 +84,7 @@ order = ["private", "public", "protected"]
84
84
  monkeypatch.chdir(subdir)
85
85
 
86
86
  config = load_config()
87
- assert config == {"order": ["private", "public", "protected"], "method_type_order": None}
87
+ assert config == {"order": ["private", "public", "protected"], "method_type_order": None, "exclude": None}
88
88
 
89
89
  def test_corrupted_toml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
90
90
  """Test that corrupted TOML file falls back to default."""
@@ -97,7 +97,7 @@ order = ["public" # Invalid TOML
97
97
 
98
98
  monkeypatch.chdir(tmp_path)
99
99
  config = load_config()
100
- assert config == {"order": ["public", "protected", "private"], "method_type_order": None}
100
+ assert config == {"order": ["public", "protected", "private"], "method_type_order": None, "exclude": None}
101
101
 
102
102
  def test_find_pyproject_toml_not_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
103
103
  """Test _find_pyproject_toml returns None when not found."""
@@ -113,3 +113,39 @@ order = ["public" # Invalid TOML
113
113
  monkeypatch.chdir(tmp_path)
114
114
  result = _find_pyproject_toml()
115
115
  assert result == pyproject_path
116
+
117
+ def test_load_exclude_patterns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
118
+ """Test loading exclude patterns from pyproject.toml."""
119
+ pyproject_content = """
120
+ [tool.undersort]
121
+ order = ["public", "protected", "private"]
122
+ exclude = ["tests/*", "migrations/*.py"]
123
+ """
124
+ pyproject_path = tmp_path / "pyproject.toml"
125
+ pyproject_path.write_text(pyproject_content)
126
+
127
+ monkeypatch.chdir(tmp_path)
128
+ config = load_config()
129
+ assert config == {
130
+ "order": ["public", "protected", "private"],
131
+ "method_type_order": None,
132
+ "exclude": ["tests/*", "migrations/*.py"],
133
+ }
134
+
135
+ def test_invalid_exclude_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
136
+ """Test that invalid exclude type falls back to None."""
137
+ pyproject_content = """
138
+ [tool.undersort]
139
+ order = ["public", "protected", "private"]
140
+ exclude = "invalid"
141
+ """
142
+ pyproject_path = tmp_path / "pyproject.toml"
143
+ pyproject_path.write_text(pyproject_content)
144
+
145
+ monkeypatch.chdir(tmp_path)
146
+ config = load_config()
147
+ assert config == {
148
+ "order": ["public", "protected", "private"],
149
+ "method_type_order": None,
150
+ "exclude": None,
151
+ }
@@ -107,3 +107,75 @@ class TestCollectPythonFiles:
107
107
  assert result[0].name == "a.py"
108
108
  assert result[1].name == "b.py"
109
109
  assert result[2].name == "c.py"
110
+
111
+ def test_exclude_patterns_simple(self, tmp_path: Path) -> None:
112
+ """Test excluding files by simple pattern."""
113
+ (tmp_path / "main.py").write_text("# main")
114
+ (tmp_path / "test.py").write_text("# test")
115
+ (tmp_path / "setup.py").write_text("# setup")
116
+
117
+ result = collect_python_files(tmp_path, recursive=False, exclude_patterns=["test.py"])
118
+
119
+ assert len(result) == 2
120
+ assert any(f.name == "main.py" for f in result)
121
+ assert any(f.name == "setup.py" for f in result)
122
+ assert not any(f.name == "test.py" for f in result)
123
+
124
+ def test_exclude_patterns_wildcard(self, tmp_path: Path) -> None:
125
+ """Test excluding files by wildcard pattern."""
126
+ (tmp_path / "main.py").write_text("# main")
127
+ (tmp_path / "test_one.py").write_text("# test")
128
+ (tmp_path / "test_two.py").write_text("# test")
129
+
130
+ result = collect_python_files(tmp_path, recursive=False, exclude_patterns=["test_*.py"])
131
+
132
+ assert len(result) == 1
133
+ assert result[0].name == "main.py"
134
+
135
+ def test_exclude_patterns_directory(self, tmp_path: Path) -> None:
136
+ """Test excluding entire directories."""
137
+ (tmp_path / "main.py").write_text("# main")
138
+ (tmp_path / "tests").mkdir()
139
+ (tmp_path / "tests" / "test_one.py").write_text("# test")
140
+ (tmp_path / "tests" / "test_two.py").write_text("# test")
141
+ (tmp_path / "src").mkdir()
142
+ (tmp_path / "src" / "app.py").write_text("# app")
143
+
144
+ result = collect_python_files(tmp_path, recursive=True, exclude_patterns=["tests/*"])
145
+
146
+ assert len(result) == 2
147
+ assert any(f.name == "main.py" for f in result)
148
+ assert any(f.name == "app.py" for f in result)
149
+ assert not any("test" in f.name for f in result)
150
+
151
+ def test_exclude_patterns_multiple(self, tmp_path: Path) -> None:
152
+ """Test excluding with multiple patterns."""
153
+ (tmp_path / "main.py").write_text("# main")
154
+ (tmp_path / "test.py").write_text("# test")
155
+ (tmp_path / "migrations").mkdir()
156
+ (tmp_path / "migrations" / "001.py").write_text("# migration")
157
+ (tmp_path / "src").mkdir()
158
+ (tmp_path / "src" / "app.py").write_text("# app")
159
+
160
+ result = collect_python_files(tmp_path, recursive=True, exclude_patterns=["test.py", "migrations/*"])
161
+
162
+ assert len(result) == 2
163
+ assert any(f.name == "main.py" for f in result)
164
+ assert any(f.name == "app.py" for f in result)
165
+ assert not any(f.name == "test.py" for f in result)
166
+ assert not any("001.py" in str(f) for f in result)
167
+
168
+ def test_exclude_patterns_nested_directories(self, tmp_path: Path) -> None:
169
+ """Test excluding nested directories."""
170
+ (tmp_path / "main.py").write_text("# main")
171
+ (tmp_path / "tests").mkdir()
172
+ (tmp_path / "tests" / "unit").mkdir()
173
+ (tmp_path / "tests" / "unit" / "test_one.py").write_text("# test")
174
+ (tmp_path / "src").mkdir()
175
+ (tmp_path / "src" / "app.py").write_text("# app")
176
+
177
+ result = collect_python_files(tmp_path, recursive=True, exclude_patterns=["tests/*"])
178
+
179
+ assert len(result) == 2
180
+ assert any(f.name == "main.py" for f in result)
181
+ assert any(f.name == "app.py" for f in result)
@@ -10,11 +10,12 @@ def load_config() -> dict[str, list[str] | None]:
10
10
  """Load configuration from pyproject.toml.
11
11
 
12
12
  Returns:
13
- Dictionary with 'order' and 'method_type_order' keys.
13
+ Dictionary with 'order', 'method_type_order', and 'exclude' keys.
14
14
  """
15
15
  default_config: dict[str, list[str] | None] = {
16
16
  "order": ["public", "protected", "private"],
17
17
  "method_type_order": None,
18
+ "exclude": None,
18
19
  }
19
20
 
20
21
  pyproject_path = _find_pyproject_toml()
@@ -50,6 +51,13 @@ def load_config() -> dict[str, list[str] | None]:
50
51
  else:
51
52
  result["method_type_order"] = method_type_order
52
53
 
54
+ if "exclude" in config:
55
+ exclude = config["exclude"]
56
+ if isinstance(exclude, list):
57
+ result["exclude"] = exclude
58
+ else:
59
+ logger.warning(f"Invalid exclude value in {pyproject_path}. Must be a list.")
60
+
53
61
  return result
54
62
 
55
63
 
@@ -1,6 +1,7 @@
1
1
  """Main entry point for undersort CLI."""
2
2
 
3
3
  import argparse
4
+ import fnmatch
4
5
  import sys
5
6
  from pathlib import Path
6
7
 
@@ -9,12 +10,13 @@ from undersort.config import load_config
9
10
  from undersort.sorter import sort_file
10
11
 
11
12
 
12
- def collect_python_files(path: Path, recursive: bool = True) -> list[Path]:
13
+ def collect_python_files(path: Path, recursive: bool = True, exclude_patterns: list[str] | None = None) -> list[Path]:
13
14
  """Collect Python files from a path (file or directory).
14
15
 
15
16
  Args:
16
17
  path: File or directory path
17
18
  recursive: If True, recursively search directories
19
+ exclude_patterns: List of glob patterns to exclude (e.g., ["tests/*", "migrations/*.py"])
18
20
 
19
21
  Returns:
20
22
  List of Python file paths
@@ -37,12 +39,47 @@ def collect_python_files(path: Path, recursive: bool = True) -> list[Path]:
37
39
  if not any(part in exclude_dirs or (part.startswith(".") and part != ".") for part in f.parts)
38
40
  ]
39
41
 
42
+ if exclude_patterns:
43
+ filtered_files = [f for f in filtered_files if not _matches_any_pattern(f, exclude_patterns)]
44
+
40
45
  return sorted(filtered_files)
41
46
 
42
47
  return []
43
48
 
44
49
 
45
- def main() -> int:
50
+ def _matches_any_pattern(file_path: Path, patterns: list[str]) -> bool:
51
+ """Check if a file matches any of the exclusion patterns.
52
+
53
+ Args:
54
+ file_path: Path to check
55
+ patterns: List of glob patterns
56
+
57
+ Returns:
58
+ True if file matches any pattern, False otherwise
59
+ """
60
+ path_str = str(file_path)
61
+
62
+ for pattern in patterns:
63
+ if fnmatch.fnmatch(path_str, pattern):
64
+ return True
65
+
66
+ if "/" not in pattern:
67
+ if fnmatch.fnmatch(file_path.name, pattern):
68
+ return True
69
+ continue
70
+
71
+ if fnmatch.fnmatch(path_str, f"*/{pattern}"):
72
+ return True
73
+
74
+ for part_idx in range(len(file_path.parts)):
75
+ subpath = str(Path(*file_path.parts[part_idx:]))
76
+ if fnmatch.fnmatch(subpath, pattern):
77
+ return True
78
+
79
+ return False
80
+
81
+
82
+ def main() -> int: # noqa: PLR0912
46
83
  """Main entry point for undersort."""
47
84
  parser = argparse.ArgumentParser(description="Sort class methods by visibility (public, protected, private)")
48
85
  parser.add_argument(
@@ -68,18 +105,30 @@ def main() -> int:
68
105
  default=True,
69
106
  help="Don't recursively search directories",
70
107
  )
108
+ parser.add_argument(
109
+ "--exclude",
110
+ action="append",
111
+ help="Exclude files/directories matching pattern (can be used multiple times)",
112
+ )
71
113
 
72
114
  args = parser.parse_args()
73
115
 
74
116
  config = load_config()
75
117
 
118
+ exclude_patterns: list[str] = []
119
+ config_exclude = config.get("exclude")
120
+ if config_exclude:
121
+ exclude_patterns.extend(config_exclude)
122
+ if args.exclude:
123
+ exclude_patterns.extend(args.exclude)
124
+
76
125
  all_files: list[Path] = []
77
126
  for path in args.paths:
78
127
  if not path.exists():
79
128
  logger.error(f"Path not found: {path}")
80
129
  continue
81
130
 
82
- python_files = collect_python_files(path, args.recursive)
131
+ python_files = collect_python_files(path, args.recursive, exclude_patterns or None)
83
132
  all_files.extend(python_files)
84
133
 
85
134
  if not all_files:
@@ -905,7 +905,7 @@ wheels = [
905
905
 
906
906
  [[package]]
907
907
  name = "undersort"
908
- version = "0.1.4"
908
+ version = "0.1.5"
909
909
  source = { editable = "." }
910
910
  dependencies = [
911
911
  { name = "libcst", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes