funkshway 2026.5.2__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.
- funkshway/__init__.py +2 -0
- funkshway/core.py +73 -0
- funkshway/main.py +25 -0
- funkshway/py.typed +0 -0
- funkshway-2026.5.2.dist-info/METADATA +39 -0
- funkshway-2026.5.2.dist-info/RECORD +8 -0
- funkshway-2026.5.2.dist-info/WHEEL +4 -0
- funkshway-2026.5.2.dist-info/entry_points.txt +2 -0
funkshway/__init__.py
ADDED
funkshway/core.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import libcst as cst
|
|
2
|
+
import libcst.matchers as m
|
|
3
|
+
|
|
4
|
+
class UnrecognizedBlockError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class FunkshwayTransformer(cst.CSTTransformer):
|
|
8
|
+
"""
|
|
9
|
+
Initial transformer for funkshway.
|
|
10
|
+
Currently just performs an idempotent round-trip.
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def is_constant_assignment(node: cst.CSTNode) -> bool:
|
|
15
|
+
if m.matches(node, m.SimpleStatementLine(body=[m.Assign(targets=[m.AssignTarget(target=m.Name(value=m.MatchRegex(r"^[A-Z_][A-Z0-9_]*$")))])])):
|
|
16
|
+
return True
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
def is_main_block(node: cst.CSTNode) -> bool:
|
|
20
|
+
if m.matches(node, m.If(test=m.Comparison(left=m.Name(value="__name__"), comparisons=[m.ComparisonTarget(operator=m.Equal(), comparator=m.SimpleString(value='"__main__"'))]))):
|
|
21
|
+
return True
|
|
22
|
+
if m.matches(node, m.If(test=m.Comparison(left=m.Name(value="__name__"), comparisons=[m.ComparisonTarget(operator=m.Equal(), comparator=m.SimpleString(value="'__main__'"))]))):
|
|
23
|
+
return True
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def check_for_unrecognized_blocks(module: cst.Module):
|
|
27
|
+
"""
|
|
28
|
+
Checks if there are any blocks at the top level that are not supported.
|
|
29
|
+
"""
|
|
30
|
+
for statement in module.body:
|
|
31
|
+
# Ignore empty lines and comments at top level if they are just standalone?
|
|
32
|
+
# Actually, comments should be associated with blocks.
|
|
33
|
+
|
|
34
|
+
# Supported:
|
|
35
|
+
# 1. Imports
|
|
36
|
+
if m.matches(statement, m.SimpleStatementLine(body=[m.Import() | m.ImportFrom()])):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
# 2. Constants
|
|
40
|
+
if is_constant_assignment(statement):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# 3. Classes
|
|
44
|
+
if m.matches(statement, m.ClassDef()):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# 4. Functions
|
|
48
|
+
if m.matches(statement, m.FunctionDef()):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# 5. Main block
|
|
52
|
+
if is_main_block(statement):
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# 6. Standalone comments/docstrings might be tricky.
|
|
56
|
+
# For Phase 1, let's be strict.
|
|
57
|
+
if m.matches(statement, m.SimpleStatementLine(body=[m.Expr(value=m.SimpleString())])):
|
|
58
|
+
# Docstring
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
raise UnrecognizedBlockError(f"Unrecognized block at top level: {statement}")
|
|
62
|
+
|
|
63
|
+
def format_code(source_code: str) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Parses source code, applies transformations, and returns formatted code.
|
|
66
|
+
"""
|
|
67
|
+
module = cst.parse_module(source_code)
|
|
68
|
+
|
|
69
|
+
check_for_unrecognized_blocks(module)
|
|
70
|
+
|
|
71
|
+
# Phase 1: Just idempotent round-trip
|
|
72
|
+
transformed_module = module.visit(FunkshwayTransformer())
|
|
73
|
+
return transformed_module.code
|
funkshway/main.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from funkshway.core import format_code, UnrecognizedBlockError
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(description="funkshway: Opinionated Python code reordering")
|
|
7
|
+
parser.add_argument("file", help="Python file to format")
|
|
8
|
+
args = parser.parse_args()
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
with open(args.file, "r") as f:
|
|
12
|
+
source_code = f.read()
|
|
13
|
+
|
|
14
|
+
formatted_code = format_code(source_code)
|
|
15
|
+
print(formatted_code, end="")
|
|
16
|
+
|
|
17
|
+
except UnrecognizedBlockError as e:
|
|
18
|
+
print(f"Refusal: {e}", file=sys.stderr)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
funkshway/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: funkshway
|
|
3
|
+
Version: 2026.5.2
|
|
4
|
+
Summary: CLI to move code blocks in a Python file into an opinionated, idempotent order
|
|
5
|
+
Author-email: David Seaward <72779-lofidevops@users.noreply.gitlab.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: libcst>=1.3.1
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# funkshway 🕺
|
|
11
|
+
|
|
12
|
+
CLI to move code blocks in a Python file into an opinionated, idempotent order.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv tool install funkshway
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
funkshway path/to/file.py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Idempotent round-trip guaranteed for supported blocks.
|
|
27
|
+
Unrecognized code at top-level will cause the tool to refuse formatting.
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
git tag -a 2.0.0 -m "Version 2.0.0"
|
|
33
|
+
uv build
|
|
34
|
+
uv publish # uses tag for version number
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Colophon
|
|
38
|
+
|
|
39
|
+
Building almost exclusively with Junie using AGENTS.md, specification.toml and PLAN.md
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
funkshway/__init__.py,sha256=imkavrY7QF6kURhae7tbS664tPadwehGUmmpEuXtt6U,55
|
|
2
|
+
funkshway/core.py,sha256=utT4ngCAbNtOW0Ct_VCIlf1p-XAuO_if6l6cG4NlW2w,2593
|
|
3
|
+
funkshway/main.py,sha256=mLYcgs1FL2Q1ajPdOz7DQwo4O8rqaIHvQjSAgmv8mAE,725
|
|
4
|
+
funkshway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
funkshway-2026.5.2.dist-info/METADATA,sha256=T2KR6xQxoahK31L3zHYEplIQg2zEqLR15bvgUnlpYzY,856
|
|
6
|
+
funkshway-2026.5.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
funkshway-2026.5.2.dist-info/entry_points.txt,sha256=o2qWzHStUsUChbkvDH_tKvTmVsdTIGZoM1LdqaIn00Q,50
|
|
8
|
+
funkshway-2026.5.2.dist-info/RECORD,,
|