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 +5 -0
- codesorter/cli.py +227 -0
- codesorter/const.py +25 -0
- codesorter/py.typed +0 -0
- codesorter/sort_code.py +334 -0
- codesorter-0.1.0.dist-info/METADATA +323 -0
- codesorter-0.1.0.dist-info/RECORD +10 -0
- codesorter-0.1.0.dist-info/WHEEL +4 -0
- codesorter-0.1.0.dist-info/entry_points.txt +2 -0
- codesorter-0.1.0.dist-info/licenses/LICENSE.txt +22 -0
codesorter/__init__.py
ADDED
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
|
codesorter/sort_code.py
ADDED
|
@@ -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,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.
|