codesorter 0.1.0__py3-none-any.whl

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.
codesorter/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CodeSorter is a codemod that sorts Python code in a project."""
2
+
3
+ from codesorter.cli import main
4
+
5
+ __all__ = ["main"]
codesorter/cli.py ADDED
@@ -0,0 +1,227 @@
1
+ """Command-line interface for the code sorter tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ import libcst as cst
11
+ import libcst.tool
12
+ import pathspec
13
+ from libcst.codemod import CodemodContext
14
+
15
+ from codesorter.const import DEFAULT_EXCLUDES
16
+ from codesorter.sort_code import SortCodeCommand
17
+
18
+ if TYPE_CHECKING:
19
+ from pathspec.pattern import Pattern
20
+
21
+
22
+ def _build_parser() -> argparse.ArgumentParser:
23
+ """Build the argument parser for the code sorter CLI."""
24
+ parser = argparse.ArgumentParser(
25
+ prog="codesorter",
26
+ description=(
27
+ "Sort Python code in the specified package or file. This tool analyzes "
28
+ "Python code and sorts classes and functions based on their dependencies "
29
+ "and relationships."
30
+ ),
31
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
32
+ )
33
+ parser.add_argument(
34
+ "-c",
35
+ "--check",
36
+ action="store_true",
37
+ help="Don't write files; exit non-zero if any file would be reordered.",
38
+ )
39
+ parser.add_argument(
40
+ "-j",
41
+ "--jobs",
42
+ type=int,
43
+ help="Number of jobs to use when processing files.",
44
+ )
45
+ parser.add_argument(
46
+ "-s",
47
+ "--show-successes",
48
+ action="store_true",
49
+ help="Print files successfully sorted with no warnings.",
50
+ )
51
+ parser.add_argument(
52
+ "-u",
53
+ "--unified-diff",
54
+ action="store_true",
55
+ help="Output unified diff instead of contents.",
56
+ )
57
+ parser.add_argument(
58
+ "-e",
59
+ "--exclude",
60
+ dest="extra_excludes",
61
+ action="append",
62
+ default=[],
63
+ metavar="NAME",
64
+ help="Directory name to skip when walking paths. May be passed multiple times.",
65
+ )
66
+ parser.add_argument(
67
+ "--no-default-excludes",
68
+ action="store_true",
69
+ help="Disable the built-in directory excludes (.git, .venv, __pycache__, build, dist, ...).",
70
+ )
71
+ parser.add_argument(
72
+ "--no-gitignore",
73
+ action="store_true",
74
+ help="Do not honor .gitignore files when walking paths.",
75
+ )
76
+ parser.add_argument(
77
+ "paths",
78
+ nargs="*",
79
+ help="Files or directories to sort. Defaults to the current directory.",
80
+ )
81
+ return parser
82
+
83
+
84
+ def _check_files(*, files: list[str]) -> int:
85
+ """Run the sort codemod in-process and report files that would change."""
86
+ changed: list[str] = []
87
+ failed: list[str] = []
88
+ for path in files:
89
+ try:
90
+ source = Path(path).read_text(encoding="utf-8")
91
+ except OSError as exc:
92
+ sys.stderr.write(f"{path}: read error: {exc}\n")
93
+ failed.append(path)
94
+ continue
95
+ try:
96
+ new_tree = SortCodeCommand(CodemodContext()).transform_module(cst.parse_module(source))
97
+ except Exception as exc: # noqa: BLE001
98
+ sys.stderr.write(f"{path}: transform error: {exc}\n")
99
+ failed.append(path)
100
+ continue
101
+ if new_tree.code != source:
102
+ changed.append(path)
103
+ sys.stderr.write(f"Would reorder: {path}\n")
104
+ if changed or failed:
105
+ sys.stderr.write(
106
+ f"{len(changed)} file(s) would be reordered, {len(failed)} file(s) failed.\n",
107
+ )
108
+ return 1
109
+ return 0
110
+
111
+
112
+ def _collect_files(
113
+ *,
114
+ excludes: set[str],
115
+ honor_gitignore: bool,
116
+ parser: argparse.ArgumentParser,
117
+ paths: tuple[str, ...],
118
+ ) -> list[str]:
119
+ """Expand the given paths into a sorted, de-duplicated list of .py files."""
120
+ seen: set[Path] = set()
121
+ files: list[str] = []
122
+ for raw in paths:
123
+ path = Path(raw)
124
+ if not path.exists():
125
+ parser.error(f"Path not found: {raw}")
126
+ if path.is_file():
127
+ resolved = path.resolve()
128
+ if resolved not in seen:
129
+ seen.add(resolved)
130
+ files.append(str(path))
131
+ continue
132
+
133
+ ignore_specs = _load_gitignore_specs(root=path, excludes=excludes) if honor_gitignore else []
134
+
135
+ for candidate in sorted(path.rglob("*.py")):
136
+ relative_parts = candidate.relative_to(path).parts
137
+ if any(part in excludes for part in relative_parts):
138
+ continue
139
+ if _matches_gitignore(candidate=candidate, specs=ignore_specs):
140
+ continue
141
+ resolved = candidate.resolve()
142
+ if resolved in seen:
143
+ continue
144
+ seen.add(resolved)
145
+ files.append(str(candidate))
146
+ return files
147
+
148
+
149
+ def _load_gitignore_specs(
150
+ *,
151
+ root: Path,
152
+ excludes: set[str],
153
+ ) -> list[tuple[Path, pathspec.PathSpec[Pattern]]]:
154
+ """Collect (anchor_dir, spec) for every .gitignore at or below root.
155
+
156
+ Each spec is interpreted relative to its containing directory, matching git's own
157
+ behavior with nested .gitignore files.
158
+
159
+ """
160
+ specs: list[tuple[Path, pathspec.PathSpec[Pattern]]] = []
161
+ for gitignore in root.rglob(".gitignore"):
162
+ if not gitignore.is_file():
163
+ continue
164
+ if any(part in excludes for part in gitignore.relative_to(root).parts):
165
+ continue
166
+ lines = gitignore.read_text(encoding="utf-8", errors="replace").splitlines()
167
+ spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
168
+ specs.append((gitignore.parent, spec))
169
+ return specs
170
+
171
+
172
+ def _matches_gitignore(
173
+ *,
174
+ candidate: Path,
175
+ specs: list[tuple[Path, pathspec.PathSpec[Pattern]]],
176
+ ) -> bool:
177
+ """Return True if any covering .gitignore matches the candidate file."""
178
+ for anchor, spec in specs:
179
+ try:
180
+ rel = candidate.relative_to(anchor)
181
+ except ValueError:
182
+ continue
183
+ if spec.match_file(str(rel)):
184
+ return True
185
+ return False
186
+
187
+
188
+ def main(*, argv: list[str] | None = None) -> None:
189
+ """Sort Python code in the specified package or file."""
190
+ parser = _build_parser()
191
+ args = parser.parse_args(argv)
192
+
193
+ paths: tuple[str, ...] = tuple(args.paths) or (".",)
194
+
195
+ excludes = set(args.extra_excludes)
196
+ if not args.no_default_excludes:
197
+ excludes.update(DEFAULT_EXCLUDES)
198
+
199
+ files = _collect_files(
200
+ excludes=excludes,
201
+ honor_gitignore=not args.no_gitignore,
202
+ parser=parser,
203
+ paths=paths,
204
+ )
205
+ if not files:
206
+ parser.error("No Python files to sort.")
207
+
208
+ if args.check:
209
+ sys.exit(_check_files(files=files))
210
+
211
+ cst_argv = [
212
+ "codemod",
213
+ "-x",
214
+ "codesorter.sort_code.SortCodeCommand",
215
+ *files,
216
+ ]
217
+
218
+ if args.unified_diff:
219
+ cst_argv.append("--unified-diff")
220
+
221
+ if args.show_successes:
222
+ cst_argv.append("--show-successes")
223
+
224
+ if args.jobs is not None:
225
+ cst_argv.extend(["--jobs", str(args.jobs)])
226
+
227
+ sys.exit(libcst.tool.main("codesorter", cst_argv))
codesorter/const.py ADDED
@@ -0,0 +1,25 @@
1
+ """Package constants and version metadata."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ DEFAULT_EXCLUDES: tuple[str, ...] = (
6
+ ".bzr",
7
+ ".direnv",
8
+ ".eggs",
9
+ ".git",
10
+ ".hg",
11
+ ".mypy_cache",
12
+ ".nox",
13
+ ".pytest_cache",
14
+ ".ruff_cache",
15
+ ".svn",
16
+ ".tox",
17
+ ".venv",
18
+ "__pycache__",
19
+ "__pypackages__",
20
+ "build",
21
+ "dist",
22
+ "env",
23
+ "node_modules",
24
+ "venv",
25
+ )
codesorter/py.typed ADDED
File without changes
@@ -0,0 +1,334 @@
1
+ """The SortCodeCommand libcst codemod that reorders classes, methods, and functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from collections import defaultdict
7
+ from enum import auto
8
+
9
+ import libcst as cst
10
+ from libcst import matchers as m
11
+ from libcst import metadata as md
12
+ from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand
13
+
14
+ _PROPERTY_DECORATOR_PARTS = 2
15
+ _PLAIN_DECORATOR_PARTS = 1
16
+
17
+
18
+ def _gen_unique_name(node: cst.ClassDef | cst.FunctionDef) -> str:
19
+ parts = [node.name.value]
20
+ if isinstance(node, cst.ClassDef):
21
+ items: tuple[cst.CSTNode, ...] = (*node.bases, *node.decorators, *node.keywords)
22
+ else:
23
+ items = (node,)
24
+ for item in items:
25
+ parts.extend(cst.ensure_type(name, cst.Name).value for name in m.findall(item, m.Name()))
26
+ return ".".join(parts)
27
+
28
+
29
+ class DependencyType(enum.IntEnum):
30
+ """Ordering buckets used to keep dependents after their dependencies."""
31
+
32
+ not_required = auto()
33
+ required = auto()
34
+
35
+
36
+ class FixtureType(enum.IntEnum):
37
+ """Pytest fixture scopes used as a secondary sort key for fixture methods."""
38
+
39
+ na = 0
40
+ session_fixture = auto()
41
+ package_fixture = auto()
42
+ module_fixture = auto()
43
+ class_fixture = auto()
44
+ function_fixture = auto()
45
+
46
+
47
+ class MethodType(enum.IntEnum):
48
+ """Method kinds used to sort methods within a class body."""
49
+
50
+ na = 0
51
+ abstractmethod = auto()
52
+ autouse_fixture = auto()
53
+ fixture = auto()
54
+ staticmethod = auto()
55
+ classmethod = auto()
56
+ cachedproperty = auto()
57
+ property = auto()
58
+ contextmanager = auto()
59
+ instance = auto()
60
+
61
+
62
+ class PropertyType(enum.IntEnum):
63
+ """Property accessor kinds used to group getter/setter/deleter together."""
64
+
65
+ na = 0
66
+ getter = auto()
67
+ setter = auto()
68
+ deleter = auto()
69
+
70
+
71
+ class SortingTransformer(cst.CSTTransformer):
72
+ """Apply a precomputed replacements map to swap nodes during a traversal."""
73
+
74
+ def __init__(self, replacements: dict[str, cst.ClassDef | cst.FunctionDef]) -> None:
75
+ """Store the replacements keyed by unique node name."""
76
+ self.replacements = replacements
77
+ super().__init__()
78
+
79
+ def on_leave(
80
+ self,
81
+ original_node: cst.CSTNode,
82
+ updated_node: cst.CSTNode,
83
+ ) -> cst.CSTNode:
84
+ """Swap matching class or function nodes for their replacement."""
85
+ if isinstance(original_node, (cst.ClassDef, cst.FunctionDef)):
86
+ return self.replacements.get(_gen_unique_name(original_node), updated_node)
87
+ return updated_node
88
+
89
+
90
+ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransformer):
91
+ """Reorder module- and class-level definitions based on their dependencies."""
92
+
93
+ # Add a description so that future codemodders can see what this does.
94
+ DESCRIPTION: str = "Sorts code in project"
95
+
96
+ METADATA_DEPENDENCIES = (md.ScopeProvider,)
97
+
98
+ @property
99
+ def in_class(self) -> bool:
100
+ """Return True while the visitor is inside a class body."""
101
+ return bool(self._class_depth)
102
+
103
+ @in_class.setter
104
+ def in_class(self, value: bool) -> None:
105
+ if value:
106
+ self._class_depth += 1
107
+ elif self._class_depth > 0:
108
+ self._class_depth -= 1
109
+ else:
110
+ self._class_depth = 0
111
+
112
+ def __init__(self, context: CodemodContext) -> None:
113
+ """Initialize per-run state used while collecting and sorting nodes."""
114
+ super().__init__(context)
115
+ self._counter = 0
116
+ self._class_depth = 0
117
+ self.original_nodes: dict[str, cst.CSTNode] = {}
118
+ self.dependencies: defaultdict[str, set[str]] = defaultdict(set)
119
+ self.dependents: defaultdict[str, set[str]] = defaultdict(set)
120
+
121
+ def _get_dependencies( # noqa: C901
122
+ self,
123
+ node: cst.ClassDef | cst.FunctionDef,
124
+ ) -> tuple[list[str], md.Scope]:
125
+ original = self.original_nodes.get(_gen_unique_name(node))
126
+ meta = None if original is None else self.get_metadata(md.ScopeProvider, original, None)
127
+ if meta is None:
128
+ msg = f"missing scope metadata for {node.name.value!r}"
129
+ raise ValueError(msg)
130
+ dependencies: set[str] = set()
131
+ if isinstance(meta, (md.ClassScope, md.GlobalScope)):
132
+
133
+ def _outer_scope(scope: object) -> bool:
134
+ return isinstance(scope, (md.ClassScope, md.GlobalScope)) or (
135
+ isinstance(scope, md.ClassScope) and scope.parent != meta
136
+ )
137
+
138
+ for found in self.extractall(
139
+ node,
140
+ m.SaveMatchedNode(
141
+ m.Name(
142
+ value=m.DoesNotMatch(node.name.value),
143
+ metadata=m.MatchMetadataIfTrue(md.ScopeProvider, _outer_scope),
144
+ ),
145
+ "name",
146
+ ),
147
+ ):
148
+ try:
149
+ node_name = cst.ensure_type(found["name"], cst.Name).value
150
+ is_import = isinstance(
151
+ next(iter(meta.assignments[node_name])),
152
+ md.ImportAssignment,
153
+ )
154
+ if is_import:
155
+ continue
156
+ is_global_scope = False
157
+ for access in meta[node_name]:
158
+ if isinstance(access, md.Access) and access.is_annotation:
159
+ continue
160
+ if isinstance(access.scope, md.GlobalScope):
161
+ is_global_scope = True
162
+ break
163
+ if isinstance(access.scope, md.ClassScope):
164
+ if access.scope.node == node:
165
+ continue
166
+ is_global_scope = True
167
+ break
168
+ if is_global_scope:
169
+ dependencies.add(node_name)
170
+ except StopIteration:
171
+ pass
172
+ if self.matches(node, m.ClassDef()):
173
+ for subclass in self.extractall(
174
+ node,
175
+ m.ClassDef(
176
+ bases=[
177
+ m.Arg(
178
+ value=m.SaveMatchedNode(
179
+ m.Name(
180
+ value=m.DoesNotMatch(node.name.value),
181
+ ),
182
+ "name",
183
+ )
184
+ ),
185
+ ]
186
+ ),
187
+ ):
188
+ subclass_name = cst.ensure_type(subclass["name"], cst.Name).value
189
+ for assignment in meta.globals.assignments[subclass_name]:
190
+ if isinstance(assignment, (md.BuiltinAssignment, md.ImportAssignment)):
191
+ continue
192
+ dependencies.add(subclass_name)
193
+ return list(dependencies), meta
194
+
195
+ def _get_replacements(
196
+ self,
197
+ items: list[cst.ClassDef | cst.FunctionDef],
198
+ ) -> dict[str, cst.ClassDef | cst.FunctionDef]:
199
+ return {
200
+ _gen_unique_name(old): new for old, new in zip(items, sorted(items, key=self._node_sort_key), strict=True)
201
+ }
202
+
203
+ def _node_sort_key(
204
+ self,
205
+ node: cst.ClassDef | cst.FunctionDef,
206
+ ) -> tuple[list[DependencyType], bool, MethodType, FixtureType, str, PropertyType]:
207
+ _, meta = self._get_dependencies(node)
208
+ is_class = self.matches(node, m.ClassDef())
209
+ method_type = MethodType.na
210
+ fixture_type = FixtureType.na
211
+ node_name = node.name.value
212
+ property_type = PropertyType.na
213
+ if not is_class:
214
+ method_type = MethodType.instance
215
+ for outer_decorator in node.decorators:
216
+ decorator = outer_decorator.decorator
217
+ decorator_parts = [cst.ensure_type(part, cst.Name).value for part in self.findall(decorator, m.Name())]
218
+ if len(decorator_parts) == _PROPERTY_DECORATOR_PARTS:
219
+ decorator_type, accessor = decorator_parts
220
+ if decorator_type == node.name.value:
221
+ method_type = MethodType.property
222
+ property_type = PropertyType[accessor]
223
+ if len(decorator_parts) == _PLAIN_DECORATOR_PARTS:
224
+ decorator_type = decorator_parts[0]
225
+ if decorator_type == "property":
226
+ method_type = MethodType.property
227
+ property_type = PropertyType.getter
228
+ else:
229
+ method_type = MethodType.__members__.get(decorator_type, method_type)
230
+ if self.matches(
231
+ decorator,
232
+ m.Attribute(attr=m.Name(value="fixture"), value=m.Name("pytest"))
233
+ | m.Call(func=m.Attribute(attr=m.Name(value="fixture"), value=m.Name("pytest"))),
234
+ ):
235
+ scope_match = self.extract(
236
+ decorator,
237
+ m.Call(
238
+ args=[
239
+ m.ZeroOrMore(m.DoNotCare()),
240
+ m.OneOf(
241
+ m.Arg(
242
+ keyword=m.Name(value="scope"),
243
+ value=m.SaveMatchedNode(m.SimpleString(), "scope"),
244
+ )
245
+ ),
246
+ m.ZeroOrMore(m.DoNotCare()),
247
+ ]
248
+ ),
249
+ )
250
+ autouse = self.matches(
251
+ decorator,
252
+ m.Call(
253
+ args=[
254
+ m.ZeroOrMore(m.DoNotCare()),
255
+ m.OneOf(
256
+ m.Arg(
257
+ keyword=m.Name(value="autouse"),
258
+ value=m.Name("True"),
259
+ )
260
+ ),
261
+ m.ZeroOrMore(m.DoNotCare()),
262
+ ]
263
+ ),
264
+ )
265
+ fixture_type = FixtureType.function_fixture
266
+ if scope_match:
267
+ scope_value = cst.ensure_type(scope_match["scope"], cst.SimpleString).evaluated_value
268
+ fixture_type = FixtureType[f"{scope_value}_fixture"]
269
+ method_type = MethodType.autouse_fixture if autouse else MethodType.fixture
270
+ elif isinstance(decorator, cst.Attribute) and isinstance(decorator.value, cst.Name):
271
+ method_type = MethodType.__members__.get(decorator.value.value, method_type)
272
+ return (
273
+ [
274
+ DependencyType.required
275
+ if item.name in self.dependencies[node.name.value]
276
+ else DependencyType.not_required
277
+ for item in (
278
+ meta.assignments if not is_class and isinstance(meta, md.ClassScope) else meta.globals.assignments
279
+ )
280
+ ],
281
+ not is_class if self.in_class else is_class,
282
+ method_type,
283
+ fixture_type,
284
+ node_name,
285
+ property_type,
286
+ )
287
+
288
+ def _resolve_dependents(self, node: cst.ClassDef | cst.FunctionDef) -> None:
289
+ dependencies, _ = self._get_dependencies(node)
290
+ for dependency in dependencies:
291
+ self.dependencies[node.name.value].add(dependency)
292
+ for parent_dependency in self.dependencies[dependency]:
293
+ self.dependencies[node.name.value].add(parent_dependency)
294
+
295
+ def leave_ClassDef(
296
+ self,
297
+ original_node: cst.ClassDef,
298
+ updated_node: cst.ClassDef,
299
+ ) -> cst.ClassDef:
300
+ """Sort the methods of the class body before returning the rewritten node."""
301
+ items = [item for item in updated_node.body.body if isinstance(item, (cst.ClassDef, cst.FunctionDef))]
302
+ updated_node = cst.ensure_type(
303
+ updated_node.visit(SortingTransformer(self._get_replacements(items))), cst.ClassDef
304
+ )
305
+ self.in_class = False
306
+ return updated_node
307
+
308
+ def leave_Module(
309
+ self,
310
+ original_node: cst.Module,
311
+ updated_node: cst.Module,
312
+ ) -> cst.Module:
313
+ """Sort the module-level definitions before returning the rewritten module."""
314
+ items = [item for item in updated_node.body if isinstance(item, (cst.ClassDef, cst.FunctionDef))]
315
+ updated_node = cst.ensure_type(
316
+ updated_node.visit(SortingTransformer(self._get_replacements(items))), cst.Module
317
+ )
318
+ self.original_nodes = {}
319
+ return updated_node
320
+
321
+ def visit_ClassDef(self, node: cst.ClassDef) -> bool:
322
+ """Record the class node and its dependencies before descending into it."""
323
+ unique_name = _gen_unique_name(node)
324
+ self.original_nodes[unique_name] = node
325
+ self._resolve_dependents(node)
326
+ self.in_class = True
327
+ return True
328
+
329
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
330
+ """Record the function node and skip descending into its body."""
331
+ unique_name = _gen_unique_name(node)
332
+ self.original_nodes[unique_name] = node
333
+ self._resolve_dependents(node)
334
+ return False
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.4
2
+ Name: codesorter
3
+ Version: 0.1.0
4
+ Summary: A Python codemod that sorts and organizes code in your files.
5
+ Project-URL: Change Log, https://codesorter.readthedocs.io/en/latest/package_info/change_log.html
6
+ Project-URL: Documentation, https://codesorter.readthedocs.io/
7
+ Project-URL: Issue Tracker, https://github.com/praw-dev/CodeSorter/issues
8
+ Project-URL: Source Code, https://github.com/praw-dev/CodeSorter
9
+ Author-email: Joel Payne <lilspazjoekp@gmail.com>
10
+ Maintainer-email: Joel Payne <lilspazjoekp@gmail.com>, Bryce Boe <bbzbryce@gmail.com>
11
+ License: Copyright (c) 2022, Joel Payne
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+ 2. Redistributions in binary form must reproduce the above copyright notice,
20
+ this list of conditions and the following disclaimer in the documentation
21
+ and/or other materials provided with the distribution.
22
+
23
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
+ License-File: LICENSE.txt
34
+ Keywords: codemod,codesorter,libcst,sort
35
+ Classifier: Development Status :: 4 - Beta
36
+ Classifier: Environment :: Console
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3.10
43
+ Classifier: Programming Language :: Python :: 3.11
44
+ Classifier: Programming Language :: Python :: 3.12
45
+ Classifier: Programming Language :: Python :: 3.13
46
+ Classifier: Programming Language :: Python :: 3.14
47
+ Classifier: Topic :: Software Development :: Code Generators
48
+ Classifier: Topic :: Software Development :: Quality Assurance
49
+ Classifier: Topic :: Utilities
50
+ Classifier: Typing :: Typed
51
+ Requires-Python: >=3.10
52
+ Requires-Dist: libcst<2.0.0,>=1.8.6
53
+ Requires-Dist: pathspec>=1.1.1
54
+ Description-Content-Type: text/x-rst
55
+
56
+ ############
57
+ CodeSorter
58
+ ############
59
+
60
+ CodeSorter is a LibCST codemod that automatically sorts and organizes Python code.
61
+
62
+ **********
63
+ Features
64
+ **********
65
+
66
+ - **Smart Sorting**: Automatically sorts functions, classes, and methods alphabetically
67
+ - **Decorator Awareness**: Properly handles ``@property``, ``@staticmethod``,
68
+ ``@classmethod``, and ``@pytest.fixture`` decorators
69
+ - **Hierarchical Organization**: Maintains logical grouping within classes and modules
70
+ - **Pytest Integration**: Special handling for pytest fixtures with proper scope
71
+ ordering
72
+ - **CLI Interface**: Simple command-line interface for easy integration
73
+ - **Pre-commit Hook**: Ready-to-use pre-commit hook for automated code organization
74
+
75
+ **************
76
+ Installation
77
+ **************
78
+
79
+ From PyPI:
80
+
81
+ .. code-block:: bash
82
+
83
+ # using uv and add to the lint dependency group
84
+ uv add --group lint codesorter
85
+ # or using pip
86
+ pip install codesorter
87
+
88
+ From Source:
89
+
90
+ .. code-block:: bash
91
+
92
+ git clone https://github.com/praw-dev/CodeSorter.git
93
+ cd CodeSorter
94
+ uv pip install -e .
95
+
96
+ Development Installation:
97
+
98
+ .. code-block:: bash
99
+
100
+ git clone https://github.com/praw-dev/CodeSorter.git
101
+ cd CodeSorter
102
+ uv sync
103
+
104
+ *******
105
+ Usage
106
+ *******
107
+
108
+ Command Line Interface
109
+ ======================
110
+
111
+ The simplest way to use CodeSorter is through the command-line interface:
112
+
113
+ .. code-block:: bash
114
+
115
+ # Sort a single file
116
+ codesorter my_file.py
117
+
118
+ # Sort all Python files in a directory
119
+ codesorter my_project/
120
+
121
+ # Sort with additional options
122
+ codesorter --help
123
+
124
+ Pre-commit Hook
125
+ ===============
126
+
127
+ Add CodeSorter to your pre-commit configuration to automatically sort code on every
128
+ commit:
129
+
130
+ .. code-block:: yaml
131
+
132
+ # .pre-commit-config.yaml
133
+ repos:
134
+ - repo: https://github.com/praw-dev/CodeSorter
135
+ rev: v0.0.3
136
+ hooks:
137
+ - id: codesorter
138
+
139
+ Or use the check-only variant, which fails the hook without modifying files:
140
+
141
+ .. code-block:: yaml
142
+
143
+ repos:
144
+ - repo: https://github.com/praw-dev/CodeSorter
145
+ rev: v0.0.3
146
+ hooks:
147
+ - id: codesorter-check
148
+
149
+ Programmatic Usage
150
+ ==================
151
+
152
+ You can also use CodeSorter programmatically:
153
+
154
+ .. code-block:: python
155
+
156
+ import libcst as cst
157
+ from codesorter.sort_code import SortCodeCommand
158
+ from libcst.codemod import CodemodContext
159
+
160
+ # Parse your code
161
+ code = """
162
+ def z_function():
163
+ pass
164
+
165
+ def a_function():
166
+ pass
167
+ """
168
+
169
+ # Create context and command
170
+ context = CodemodContext()
171
+ command = SortCodeCommand(context)
172
+
173
+ # Transform the code
174
+ result = command.transform_module(cst.parse_module(code))
175
+ print(result.code)
176
+
177
+ **************
178
+ How It Works
179
+ **************
180
+
181
+ CodeSorter uses LibCST (Concrete Syntax Tree) to parse and transform Python code. It
182
+ applies sophisticated sorting rules:
183
+
184
+ Function Sorting
185
+ ================
186
+
187
+ - Functions are sorted alphabetically by name
188
+ - Global functions are sorted separately from class methods
189
+
190
+ Class Method Sorting
191
+ ====================
192
+
193
+ - Methods are sorted with special consideration for decorators: - ``@property`` methods
194
+ (getters, setters, deleters) - ``@staticmethod`` methods - ``@classmethod`` methods -
195
+ Regular instance methods
196
+ - Methods within classes maintain their logical grouping
197
+
198
+ Pytest Fixture Sorting
199
+ ======================
200
+
201
+ - Fixtures are sorted by scope (session, package, module, class, function)
202
+ - Within each scope, fixtures are sorted alphabetically
203
+ - ``autouse`` fixtures are handled specially
204
+
205
+ Example Transformation
206
+ ======================
207
+
208
+ **Before:**
209
+
210
+ .. code-block:: python
211
+
212
+ class MyClass:
213
+ def z_method(self):
214
+ pass
215
+
216
+ @property
217
+ def a_property(self):
218
+ pass
219
+
220
+ @staticmethod
221
+ def b_static():
222
+ pass
223
+
224
+ **After:**
225
+
226
+ .. code-block:: python
227
+
228
+ class MyClass:
229
+ @property
230
+ def a_property(self):
231
+ pass
232
+
233
+ @staticmethod
234
+ def b_static():
235
+ pass
236
+
237
+ def z_method(self):
238
+ pass
239
+
240
+ *************
241
+ Development
242
+ *************
243
+
244
+ Setting Up Development Environment
245
+ ==================================
246
+
247
+ .. code-block:: bash
248
+
249
+ # Clone the repository
250
+ git clone https://github.com/praw-dev/CodeSorter.git
251
+ cd CodeSorter
252
+
253
+ # Install with development dependencies
254
+ uv sync
255
+
256
+ # Install pre-commit hooks
257
+ uv run pre-commit install
258
+
259
+ Running Tests
260
+ =============
261
+
262
+ .. code-block:: bash
263
+
264
+ # Run all tests
265
+ uv run pytest
266
+
267
+ # Run a specific test file
268
+ uv run pytest tests/test_sort_code.py
269
+
270
+ # Run the full tox matrix (tests, type, pre-commit)
271
+ uv run tox
272
+
273
+ Code Quality
274
+ ============
275
+
276
+ The project uses several tools to maintain code quality:
277
+
278
+ - **Ruff**: Fast linting and formatting
279
+ - **Pyright**: Type checking
280
+ - **Pre-commit**: Automated quality checks
281
+
282
+ Run all quality checks:
283
+
284
+ .. code-block:: bash
285
+
286
+ uv run pre-commit run --all-files
287
+
288
+ **************
289
+ Contributing
290
+ **************
291
+
292
+ 1. Fork the repository
293
+ 2. Create a feature branch: ``git checkout -b feature-name``
294
+ 3. Make your changes and add tests
295
+ 4. Run the test suite: ``uv run pytest``
296
+ 5. Run pre-commit hooks: ``pre-commit run --all-files``
297
+ 6. Commit your changes: ``git commit -m "Add feature"``
298
+ 7. Push to your fork: ``git push origin feature-name``
299
+ 8. Create a Pull Request
300
+
301
+ **********
302
+ Examples
303
+ **********
304
+
305
+ See the ``examples/`` directory for before and after examples of CodeSorter in action:
306
+
307
+ - ``examples/before_example.py``: Unsorted code
308
+ - ``examples/after_example.py``: Same code after sorting
309
+
310
+ *********
311
+ License
312
+ *********
313
+
314
+ This project is licensed under the MIT License - see the `LICENSE.txt
315
+ <https://github.com/praw-dev/CodeSorter/blob/main/LICENSE.txt>`_ file for details.
316
+
317
+ ***********
318
+ Changelog
319
+ ***********
320
+
321
+ See the `change log
322
+ <https://codesorter.readthedocs.io/en/latest/package_info/change_log.html>`_ for the
323
+ full list of changes.
@@ -0,0 +1,10 @@
1
+ codesorter/__init__.py,sha256=LEkD4Xio5nUxV_N7WhQ7DAFfHMCEtRmRrJkODCLRAF0,120
2
+ codesorter/cli.py,sha256=AHKOQSZBIeAWlUrUmlXWv7XB3WH6lD9Jt5WlXWzUsvQ,6845
3
+ codesorter/const.py,sha256=6jjdeCSSO_qNISocBBTNGQJDrHy68UW5PVzk2lUak3I,390
4
+ codesorter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ codesorter/sort_code.py,sha256=-HaHK8V4VvCwpC8_jRb8DJddw_MsWI4i8CyfEgYo6v8,13440
6
+ codesorter-0.1.0.dist-info/METADATA,sha256=xYYbadEngwcNCRZm0z-mXxgH1-5rkbmcw5bB-399C2w,8830
7
+ codesorter-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ codesorter-0.1.0.dist-info/entry_points.txt,sha256=K470wbtPLAVgT0JmGTuYFKdHK7zWk_rKfrY4y0LpOyQ,47
9
+ codesorter-0.1.0.dist-info/licenses/LICENSE.txt,sha256=_eHf9j8MG7lK9BIHqJYFtSEz85ITUVhydD8AYjHXYu4,1297
10
+ codesorter-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codesorter = codesorter:main
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2022, Joel Payne
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.