compact-code 0.1.0__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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ .venv/
5
+ .pytest_cache/
6
+ .compact-code.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emmanuel Freund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: compact-code
3
+ Version: 0.1.0
4
+ Summary: Dense Python for AI agents, readable view for humans. Same logic, ~30-55% fewer tokens.
5
+ Author-email: Emmanuel Freund <freund.emmanuel@gmail.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: agents,ast,claude,llm,minify,tokens
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Code Generators
14
+ Requires-Python: >=3.9
15
+ Provides-Extra: exact
16
+ Requires-Dist: tiktoken>=0.5; extra == 'exact'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # compact-code
20
+
21
+ **Dense Python for AI agents, readable view for humans. Same logic, fewer tokens.**
22
+
23
+ AI coding agents (Claude Code, Cursor, Codex...) read your code far more often than they write it. Every `Read`, every context refill re-pays the token weight of your code. But your code is written for humans: comments, docstrings, type annotations, blank lines, 4-space indentation — none of which the agent needs to understand the logic.
24
+
25
+ `compact-code` removes all of it, **provably without changing the program** (the AST is preserved), and gives you back a readable view on demand. Think *minified JS + source maps, applied to the AI workflow*.
26
+
27
+ ```
28
+ pip install compact-code
29
+ cd your-project
30
+ compact-code compact # 3 seconds, AST-safe, reversible via git
31
+ pytest # your tests are the safety net
32
+ compact-code stats # see what you now save on every read
33
+ ```
34
+
35
+ ## What it does
36
+
37
+ - removes comments, docstrings and type annotations (keeps functional class-field annotations: dataclasses, NamedTuple, pydantic)
38
+ - strips blank lines, re-indents to 1 space
39
+ - never touches string contents, public names, parameters or logic
40
+ - verifies AST equivalence on every file before writing — if the check fails, the file is skipped
41
+
42
+ ## Measured results
43
+
44
+ Benchmarked with twin-codebase experiments (identical prompts, blind agents, real test suites as judges — full methodology in the repo):
45
+
46
+ | Scenario | Session-token savings |
47
+ |---|---|
48
+ | Surgical one-file fix | ~0% (agent barely reads code) |
49
+ | Maintenance on a fully compacted codebase | **-31%** |
50
+ | Codebase exploration / audit (replicated n=3) | **-56%** |
51
+
52
+ Quality: across 16 paired sessions, agents on compacted code passed **exactly the same test suites and evals** as agents on normal code. Zero measured loss.
53
+
54
+ Codebase density gain: -19% to -41% tokens depending on the project (less if docstrings must be kept).
55
+
56
+ ## Commands
57
+
58
+ | Command | What it does |
59
+ |---|---|
60
+ | `compact-code compact [paths]` | compact `**/*.py` in place (skips `.venv`, `.git`, etc.) |
61
+ | `compact-code compact --check` | dry run: report savings without writing |
62
+ | `compact-code compact --keep-docstrings` | keep docstrings (use when they are functional: CLI help text, doctests, flit) |
63
+ | `compact-code expand <file>` | print a readable 4-space-indented view (`--write` to rewrite in place) |
64
+ | `compact-code stats` | tokens and $ saved per full codebase read |
65
+
66
+ Install `compact-code[exact]` for exact token counts (tiktoken).
67
+
68
+ ## The honest fine print
69
+
70
+ - The gain is proportional to how much code your agent actually **reads**. Exploration-heavy and read-heavy sessions save the most; one-file quick fixes save nothing.
71
+ - Some docstrings are functional (click help text, doctests, flit metadata). Always run your test suite after compacting; on failure, restore via git and re-run with `--keep-docstrings`.
72
+ - Token counts use tiktoken `o200k_base` as a proxy for your model's tokenizer; ratios are indicative.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,58 @@
1
+ # compact-code
2
+
3
+ **Dense Python for AI agents, readable view for humans. Same logic, fewer tokens.**
4
+
5
+ AI coding agents (Claude Code, Cursor, Codex...) read your code far more often than they write it. Every `Read`, every context refill re-pays the token weight of your code. But your code is written for humans: comments, docstrings, type annotations, blank lines, 4-space indentation — none of which the agent needs to understand the logic.
6
+
7
+ `compact-code` removes all of it, **provably without changing the program** (the AST is preserved), and gives you back a readable view on demand. Think *minified JS + source maps, applied to the AI workflow*.
8
+
9
+ ```
10
+ pip install compact-code
11
+ cd your-project
12
+ compact-code compact # 3 seconds, AST-safe, reversible via git
13
+ pytest # your tests are the safety net
14
+ compact-code stats # see what you now save on every read
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ - removes comments, docstrings and type annotations (keeps functional class-field annotations: dataclasses, NamedTuple, pydantic)
20
+ - strips blank lines, re-indents to 1 space
21
+ - never touches string contents, public names, parameters or logic
22
+ - verifies AST equivalence on every file before writing — if the check fails, the file is skipped
23
+
24
+ ## Measured results
25
+
26
+ Benchmarked with twin-codebase experiments (identical prompts, blind agents, real test suites as judges — full methodology in the repo):
27
+
28
+ | Scenario | Session-token savings |
29
+ |---|---|
30
+ | Surgical one-file fix | ~0% (agent barely reads code) |
31
+ | Maintenance on a fully compacted codebase | **-31%** |
32
+ | Codebase exploration / audit (replicated n=3) | **-56%** |
33
+
34
+ Quality: across 16 paired sessions, agents on compacted code passed **exactly the same test suites and evals** as agents on normal code. Zero measured loss.
35
+
36
+ Codebase density gain: -19% to -41% tokens depending on the project (less if docstrings must be kept).
37
+
38
+ ## Commands
39
+
40
+ | Command | What it does |
41
+ |---|---|
42
+ | `compact-code compact [paths]` | compact `**/*.py` in place (skips `.venv`, `.git`, etc.) |
43
+ | `compact-code compact --check` | dry run: report savings without writing |
44
+ | `compact-code compact --keep-docstrings` | keep docstrings (use when they are functional: CLI help text, doctests, flit) |
45
+ | `compact-code expand <file>` | print a readable 4-space-indented view (`--write` to rewrite in place) |
46
+ | `compact-code stats` | tokens and $ saved per full codebase read |
47
+
48
+ Install `compact-code[exact]` for exact token counts (tiktoken).
49
+
50
+ ## The honest fine print
51
+
52
+ - The gain is proportional to how much code your agent actually **reads**. Exploration-heavy and read-heavy sessions save the most; one-file quick fixes save nothing.
53
+ - Some docstrings are functional (click help text, doctests, flit metadata). Always run your test suite after compacting; on failure, restore via git and re-run with `--keep-docstrings`.
54
+ - Token counts use tiktoken `o200k_base` as a proxy for your model's tokenizer; ratios are indicative.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "compact-code"
7
+ version = "0.1.0"
8
+ description = "Dense Python for AI agents, readable view for humans. Same logic, ~30-55% fewer tokens."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Emmanuel Freund", email = "freund.emmanuel@gmail.com" }]
13
+ keywords = ["llm", "tokens", "claude", "agents", "ast", "minify"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Code Generators",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ exact = ["tiktoken>=0.5"]
24
+
25
+ [project.scripts]
26
+ compact-code = "compact_code.cli:main"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/compact_code"]
@@ -0,0 +1,7 @@
1
+ """compact-code: dense Python for AI agents, readable view for humans."""
2
+
3
+ from .compactor import compact_source, verify_equivalence
4
+ from .expand import expand_source
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["compact_source", "verify_equivalence", "expand_source", "__version__"]
@@ -0,0 +1,153 @@
1
+ """compact-code CLI: compact / expand / stats."""
2
+
3
+ import argparse
4
+ import glob
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from . import compactor, expand, tokens
10
+
11
+ MANIFEST = ".compact-code.json"
12
+ EXCLUDED_DIRS = {
13
+ ".git", ".venv", "venv", "__pycache__", "site-packages",
14
+ ".tox", ".mypy_cache", ".pytest_cache", "node_modules", ".eggs",
15
+ }
16
+ DEFAULT_PRICE_PER_MTOK = 3.0 # USD, input tokens
17
+
18
+
19
+ def _iter_files(patterns):
20
+ if not patterns:
21
+ patterns = ["**/*.py"]
22
+ seen = set()
23
+ for pattern in patterns:
24
+ if os.path.isdir(pattern):
25
+ pattern = os.path.join(pattern, "**", "*.py")
26
+ for path in glob.glob(pattern, recursive=True):
27
+ path = os.path.normpath(path)
28
+ parts = set(path.replace("\\", "/").split("/"))
29
+ if parts & EXCLUDED_DIRS or path in seen or not path.endswith(".py"):
30
+ continue
31
+ seen.add(path)
32
+ yield path
33
+
34
+
35
+ def _load_manifest():
36
+ if os.path.exists(MANIFEST):
37
+ with open(MANIFEST, encoding="utf-8") as f:
38
+ return json.load(f)
39
+ return {"files": {}}
40
+
41
+
42
+ def _save_manifest(manifest):
43
+ with open(MANIFEST, "w", encoding="utf-8", newline="\n") as f:
44
+ json.dump(manifest, f, indent=1)
45
+
46
+
47
+ def cmd_compact(args):
48
+ manifest = _load_manifest()
49
+ changed = skipped = before_total = after_total = 0
50
+ for path in _iter_files(args.paths):
51
+ source = open(path, encoding="utf-8").read()
52
+ keep_module_doc = os.path.basename(path) == "__init__.py"
53
+ try:
54
+ out = compactor.compact_source(
55
+ source,
56
+ keep_docstrings=args.keep_docstrings,
57
+ keep_module_docstring=keep_module_doc,
58
+ )
59
+ except SyntaxError as e:
60
+ print(f"SKIP {path}: {e}")
61
+ skipped += 1
62
+ continue
63
+ if not compactor.verify_equivalence(source, out):
64
+ print(f"SKIP {path}: equivalence check failed (please report this bug)")
65
+ skipped += 1
66
+ continue
67
+ before, after = tokens.count(source), tokens.count(out)
68
+ before_total += before
69
+ after_total += after
70
+ if out != source:
71
+ if not args.check:
72
+ with open(path, "w", encoding="utf-8", newline="\n") as f:
73
+ f.write(out)
74
+ manifest["files"][path.replace("\\", "/")] = {
75
+ "before": before, "after": after,
76
+ }
77
+ changed += 1
78
+ if not args.check and changed:
79
+ _save_manifest(manifest)
80
+ saved = before_total - after_total
81
+ pct = saved / before_total * 100 if before_total else 0
82
+ verb = "would be compacted" if args.check else "compacted"
83
+ approx = "" if tokens.is_exact() else " (approx., install tiktoken for exact counts)"
84
+ print(f"{changed} files {verb}, {skipped} skipped")
85
+ print(f"{before_total:,} -> {after_total:,} tokens ({pct:.1f}% smaller){approx}")
86
+ if not args.check and changed:
87
+ print("Now run your test suite. If anything fails, restore (git) and retry "
88
+ "with --keep-docstrings (some docstrings are functional: CLI help, doctests).")
89
+ return 0
90
+
91
+
92
+ def cmd_expand(args):
93
+ source = open(args.file, encoding="utf-8").read()
94
+ out = expand.expand_source(source)
95
+ if args.write:
96
+ with open(args.file, "w", encoding="utf-8", newline="\n") as f:
97
+ f.write(out)
98
+ print(f"{args.file} expanded in place")
99
+ else:
100
+ sys.stdout.write(out)
101
+ return 0
102
+
103
+
104
+ def cmd_stats(args):
105
+ manifest = _load_manifest()
106
+ files = manifest["files"]
107
+ if not files:
108
+ print("No compaction recorded here yet. Run `compact-code compact` first.")
109
+ return 1
110
+ before = sum(f["before"] for f in files.values())
111
+ after = sum(f["after"] for f in files.values())
112
+ saved = before - after
113
+ pct = saved / before * 100 if before else 0
114
+ dollars = saved / 1_000_000 * args.price
115
+ print(f"Codebase: {before:,} -> {after:,} tokens ({pct:.1f}% smaller, {len(files)} files)")
116
+ print(f"Every full read of this codebase by your agent now saves "
117
+ f"{saved:,} tokens (~${dollars:.2f} at ${args.price}/Mtok input).")
118
+ print("Sessions that read a lot of code (exploration, onboarding, refactors) "
119
+ "benefit the most; surgical one-file fixes benefit little.")
120
+ return 0
121
+
122
+
123
+ def main(argv=None):
124
+ parser = argparse.ArgumentParser(
125
+ prog="compact-code",
126
+ description="Dense Python for AI agents, readable view for humans. "
127
+ "Same logic, fewer tokens.",
128
+ )
129
+ sub = parser.add_subparsers(dest="command", required=True)
130
+
131
+ p = sub.add_parser("compact", help="compact Python files in place (AST-safe)")
132
+ p.add_argument("paths", nargs="*", help="files, dirs or globs (default: **/*.py)")
133
+ p.add_argument("--keep-docstrings", action="store_true",
134
+ help="keep docstrings (use when they are functional: CLI help, doctests)")
135
+ p.add_argument("--check", action="store_true", help="dry run, report savings only")
136
+ p.set_defaults(func=cmd_compact)
137
+
138
+ p = sub.add_parser("expand", help="print a readable view of a compacted file")
139
+ p.add_argument("file")
140
+ p.add_argument("--write", action="store_true", help="rewrite the file expanded")
141
+ p.set_defaults(func=cmd_expand)
142
+
143
+ p = sub.add_parser("stats", help="show tokens (and $) saved in this project")
144
+ p.add_argument("--price", type=float, default=DEFAULT_PRICE_PER_MTOK,
145
+ help="USD per million input tokens (default: 3.0)")
146
+ p.set_defaults(func=cmd_stats)
147
+
148
+ args = parser.parse_args(argv)
149
+ return args.func(args)
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())
@@ -0,0 +1,118 @@
1
+ """Deterministic AST-based Python compactor.
2
+
3
+ Removes comments, docstrings and type annotations (except functional
4
+ class-field annotations), strips blank lines, and re-indents to 1 space.
5
+ The program's AST is preserved: same logic, fewer tokens.
6
+ """
7
+
8
+ import ast
9
+ import io
10
+ import tokenize
11
+
12
+
13
+ class _Transformer(ast.NodeTransformer):
14
+ def __init__(self, keep_docstrings=False):
15
+ self.keep_docstrings = keep_docstrings
16
+
17
+ def _strip_doc(self, node):
18
+ body = node.body
19
+ if (
20
+ not self.keep_docstrings
21
+ and not getattr(node, "_keep_doc", False)
22
+ and body
23
+ and isinstance(body[0], ast.Expr)
24
+ and isinstance(body[0].value, ast.Constant)
25
+ and isinstance(body[0].value.value, str)
26
+ ):
27
+ body = body[1:] or [ast.Pass()]
28
+ node.body = [self.visit(stmt) for stmt in body]
29
+ return node
30
+
31
+ def visit_Module(self, node):
32
+ return self._strip_doc(node)
33
+
34
+ def visit_ClassDef(self, node):
35
+ return self._strip_doc(node)
36
+
37
+ def visit_FunctionDef(self, node):
38
+ return self._function(node)
39
+
40
+ def visit_AsyncFunctionDef(self, node):
41
+ return self._function(node)
42
+
43
+ def _function(self, node):
44
+ for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
45
+ arg.annotation = None
46
+ if node.args.vararg:
47
+ node.args.vararg.annotation = None
48
+ if node.args.kwarg:
49
+ node.args.kwarg.annotation = None
50
+ node.returns = None
51
+ return self._strip_doc(node)
52
+
53
+ def visit_AnnAssign(self, node):
54
+ self.generic_visit(node)
55
+ # Bare class-field annotations are functional (dataclasses, NamedTuple,
56
+ # TypedDict, pydantic): keep them.
57
+ if node.value is None:
58
+ return node
59
+ if getattr(node, "_in_class", False):
60
+ return node
61
+ return ast.Assign(targets=[node.target], value=node.value)
62
+
63
+
64
+ def _mark_class_fields(tree):
65
+ for node in ast.walk(tree):
66
+ if isinstance(node, ast.ClassDef):
67
+ for stmt in node.body:
68
+ if isinstance(stmt, ast.AnnAssign):
69
+ stmt._in_class = True
70
+
71
+
72
+ def _protected_lines(source):
73
+ """Line numbers interior to multi-line string tokens: their content must
74
+ not be re-indented or blank-stripped."""
75
+ protected = set()
76
+ for tok in tokenize.generate_tokens(io.StringIO(source).readline):
77
+ if tok.type == tokenize.STRING and tok.end[0] > tok.start[0]:
78
+ protected.update(range(tok.start[0] + 1, tok.end[0] + 1))
79
+ return protected
80
+
81
+
82
+ def compact_source(source, keep_docstrings=False, keep_module_docstring=False):
83
+ """Return the compacted version of *source*.
84
+
85
+ Raises SyntaxError if *source* is not valid Python.
86
+ """
87
+ tree = ast.parse(source)
88
+ _mark_class_fields(tree)
89
+ if keep_module_docstring:
90
+ tree._keep_doc = True
91
+ tree = _Transformer(keep_docstrings).visit(tree)
92
+ ast.fix_missing_locations(tree)
93
+ out = ast.unparse(tree)
94
+ protected = _protected_lines(out)
95
+ lines = []
96
+ for lineno, line in enumerate(out.split("\n"), 1):
97
+ if lineno in protected:
98
+ lines.append(line)
99
+ continue
100
+ if not line.strip():
101
+ continue
102
+ indent = len(line) - len(line.lstrip())
103
+ lines.append(" " * (indent // 4) + line.lstrip())
104
+ return "\n".join(lines) + "\n"
105
+
106
+
107
+ def _normalized_dump(source):
108
+ """AST dump with docstrings/annotations stripped — the equivalence
109
+ invariant shared by original and compacted code."""
110
+ tree = ast.parse(source)
111
+ _mark_class_fields(tree)
112
+ tree = _Transformer(keep_docstrings=False).visit(tree)
113
+ return ast.dump(tree)
114
+
115
+
116
+ def verify_equivalence(original, compacted):
117
+ """True if *compacted* preserves the (normalized) AST of *original*."""
118
+ return _normalized_dump(original) == _normalized_dump(compacted)
@@ -0,0 +1,38 @@
1
+ """Deterministic readable view of compacted code: 4-space indentation and
2
+ blank lines between definitions. Instant, no LLM."""
3
+
4
+ import io
5
+ import tokenize
6
+
7
+
8
+ def _protected_lines(source):
9
+ protected = set()
10
+ for tok in tokenize.generate_tokens(io.StringIO(source).readline):
11
+ if tok.type == tokenize.STRING and tok.end[0] > tok.start[0]:
12
+ protected.update(range(tok.start[0] + 1, tok.end[0] + 1))
13
+ return protected
14
+
15
+
16
+ def expand_source(source):
17
+ """Return a human-readable view of *source* (does not modify logic)."""
18
+ protected = _protected_lines(source)
19
+ out = []
20
+ previous_indent = 0
21
+ for lineno, line in enumerate(source.split("\n"), 1):
22
+ if lineno in protected:
23
+ out.append(line)
24
+ continue
25
+ stripped = line.lstrip()
26
+ indent = len(line) - len(stripped)
27
+ if stripped.startswith(("def ", "async def ", "class ", "@")) and out:
28
+ # blank line before a definition, two before a top-level one
29
+ blanks = 2 if indent == 0 else 1
30
+ if not stripped.startswith("@") or previous_indent != indent:
31
+ out.extend([""] * blanks)
32
+ out.append(" " * indent + stripped)
33
+ if stripped:
34
+ previous_indent = indent
35
+ text = "\n".join(out)
36
+ while "\n\n\n\n" in text:
37
+ text = text.replace("\n\n\n\n", "\n\n\n")
38
+ return text if text.endswith("\n") else text + "\n"
@@ -0,0 +1,29 @@
1
+ """Token counting: tiktoken (o200k_base) if available, chars/4 heuristic otherwise."""
2
+
3
+ _encoder = None
4
+ _exact = None
5
+
6
+
7
+ def _setup():
8
+ global _encoder, _exact
9
+ if _exact is not None:
10
+ return
11
+ try:
12
+ import tiktoken
13
+
14
+ _encoder = tiktoken.get_encoding("o200k_base")
15
+ _exact = True
16
+ except Exception:
17
+ _exact = False
18
+
19
+
20
+ def count(text):
21
+ _setup()
22
+ if _exact:
23
+ return len(_encoder.encode(text))
24
+ return len(text) // 4
25
+
26
+
27
+ def is_exact():
28
+ _setup()
29
+ return _exact
@@ -0,0 +1,77 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ from compact_code import cli
9
+
10
+ SRC = "def f(x: int) -> int:\n \"\"\"Doubles.\"\"\"\n # comment\n return x * 2\n"
11
+
12
+
13
+ @pytest.fixture
14
+ def project(tmp_path, monkeypatch):
15
+ (tmp_path / "pkg").mkdir()
16
+ (tmp_path / "pkg" / "mod.py").write_text(SRC, encoding="utf-8")
17
+ (tmp_path / "pkg" / "__init__.py").write_text('"""Pkg."""\n', encoding="utf-8")
18
+ (tmp_path / ".venv" / "lib").mkdir(parents=True)
19
+ (tmp_path / ".venv" / "lib" / "junk.py").write_text(SRC, encoding="utf-8")
20
+ monkeypatch.chdir(tmp_path)
21
+ return tmp_path
22
+
23
+
24
+ def test_compact_check_does_not_write(project):
25
+ assert cli.main(["compact", "--check"]) == 0
26
+ assert (project / "pkg" / "mod.py").read_text(encoding="utf-8") == SRC
27
+ assert not (project / cli.MANIFEST).exists()
28
+
29
+
30
+ def test_compact_writes_and_records_manifest(project):
31
+ assert cli.main(["compact"]) == 0
32
+ out = (project / "pkg" / "mod.py").read_text(encoding="utf-8")
33
+ assert "comment" not in out and "Doubles" not in out
34
+ manifest = json.loads((project / cli.MANIFEST).read_text(encoding="utf-8"))
35
+ assert any(p.endswith("mod.py") for p in manifest["files"])
36
+
37
+
38
+ def test_compact_skips_excluded_dirs(project):
39
+ cli.main(["compact"])
40
+ junk = (project / ".venv" / "lib" / "junk.py").read_text(encoding="utf-8")
41
+ assert junk == SRC
42
+
43
+
44
+ def test_init_module_docstring_kept(project):
45
+ cli.main(["compact"])
46
+ init = (project / "pkg" / "__init__.py").read_text(encoding="utf-8")
47
+ assert "Pkg." in init
48
+
49
+
50
+ def test_stats_after_compact(project, capsys):
51
+ cli.main(["compact"])
52
+ capsys.readouterr()
53
+ assert cli.main(["stats"]) == 0
54
+ out = capsys.readouterr().out
55
+ assert "saves" in out and "$" in out
56
+
57
+
58
+ def test_stats_without_manifest(project, capsys):
59
+ assert cli.main(["stats"]) == 1
60
+
61
+
62
+ def test_expand_stdout(project, capsys):
63
+ cli.main(["compact"])
64
+ capsys.readouterr()
65
+ assert cli.main(["expand", os.path.join("pkg", "mod.py")]) == 0
66
+ out = capsys.readouterr().out
67
+ assert " return x * 2" in out
68
+
69
+
70
+ def test_entry_point_runs():
71
+ result = subprocess.run(
72
+ [sys.executable, "-m", "compact_code.cli", "--help"],
73
+ capture_output=True, text=True,
74
+ env={**os.environ, "PYTHONPATH": os.path.join(os.path.dirname(__file__), "..", "src")},
75
+ )
76
+ assert result.returncode == 0
77
+ assert "compact" in result.stdout