Skip to content

Commit e6b1137

Browse files
committed
refactor: reduce cognitive complexity in core modules
1 parent b5c1fdd commit e6b1137

3 files changed

Lines changed: 130 additions & 108 deletions

File tree

src/treemapper/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _validate_budget(budget: int | None) -> None:
3434

3535

3636
def _validate_alpha(alpha: float) -> None:
37-
if not 0 < alpha < 1:
37+
if alpha <= 0 or alpha >= 1:
3838
_exit_error(f"--alpha must be between 0 and 1 (exclusive), got {alpha}")
3939

4040

src/treemapper/treemapper.py

Lines changed: 100 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,114 @@
1+
from __future__ import annotations
2+
13
import logging
24
import sys
5+
from typing import TYPE_CHECKING, Any
36

7+
if TYPE_CHECKING:
8+
from .cli import ParsedArgs
49

5-
def main() -> None:
6-
# Force UTF-8 for stdout on Windows (default cp1252 can't handle Unicode)
10+
11+
def _configure_windows_utf8() -> None:
712
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
813
sys.stdout.reconfigure(encoding="utf-8")
9-
from .cli import parse_args
10-
from .clipboard import ClipboardError, copy_to_clipboard
14+
15+
16+
def _build_diff_tree(args: ParsedArgs) -> dict[str, Any]:
17+
from .diffctx import GitError, build_diff_context
18+
19+
assert args.diff_range is not None
20+
try:
21+
return build_diff_context(
22+
root_dir=args.root_dir,
23+
diff_range=args.diff_range,
24+
budget_tokens=args.budget,
25+
alpha=args.alpha,
26+
tau=args.tau,
27+
no_content=args.no_content,
28+
ignore_file=args.ignore_file,
29+
no_default_ignores=args.no_default_ignores,
30+
full=args.full_diff,
31+
)
32+
except GitError as e:
33+
print(f"Error: {e}", file=sys.stderr)
34+
sys.exit(1)
35+
36+
37+
def _build_standard_tree(args: ParsedArgs) -> dict[str, Any]:
1138
from .ignore import get_ignore_specs
12-
from .logger import setup_logging
13-
from .tokens import print_token_summary
1439
from .tree import TreeBuildContext, build_tree
15-
from .writer import tree_to_string, write_string_to_file
1640

41+
ctx = TreeBuildContext(
42+
base_dir=args.root_dir,
43+
combined_spec=get_ignore_specs(args.root_dir, args.ignore_file, args.no_default_ignores, args.output_file),
44+
output_file=args.output_file,
45+
max_depth=args.max_depth,
46+
no_content=args.no_content,
47+
max_file_bytes=args.max_file_bytes,
48+
)
49+
return {
50+
"name": args.root_dir.name,
51+
"type": "directory",
52+
"children": build_tree(args.root_dir, ctx),
53+
}
54+
55+
56+
def _handle_clipboard(output_content: str, args: ParsedArgs) -> bool:
57+
from .clipboard import ClipboardError, copy_to_clipboard
58+
59+
if not args.copy:
60+
return False
1761
try:
18-
args = parse_args()
19-
setup_logging(args.verbosity)
20-
21-
if args.diff_range:
22-
from .diffctx import GitError, build_diff_context
23-
24-
try:
25-
directory_tree = build_diff_context(
26-
root_dir=args.root_dir,
27-
diff_range=args.diff_range,
28-
budget_tokens=args.budget,
29-
alpha=args.alpha,
30-
tau=args.tau,
31-
no_content=args.no_content,
32-
ignore_file=args.ignore_file,
33-
no_default_ignores=args.no_default_ignores,
34-
full=args.full_diff,
35-
)
36-
except GitError as e:
37-
print(f"Error: {e}", file=sys.stderr)
38-
sys.exit(1)
39-
else:
40-
ctx = TreeBuildContext(
41-
base_dir=args.root_dir,
42-
combined_spec=get_ignore_specs(args.root_dir, args.ignore_file, args.no_default_ignores, args.output_file),
43-
output_file=args.output_file,
44-
max_depth=args.max_depth,
45-
no_content=args.no_content,
46-
max_file_bytes=args.max_file_bytes,
47-
)
48-
49-
directory_tree = {
50-
"name": args.root_dir.name,
51-
"type": "directory",
52-
"children": build_tree(args.root_dir, ctx),
53-
}
54-
55-
output_content = tree_to_string(directory_tree, args.output_format)
56-
print_token_summary(output_content)
57-
58-
clipboard_ok = False
59-
if args.copy:
60-
try:
61-
copy_to_clipboard(output_content)
62-
print("Copied to clipboard", file=sys.stderr)
63-
clipboard_ok = True
64-
except ClipboardError as e:
65-
print(f"Clipboard unavailable: {e}", file=sys.stderr)
66-
67-
if args.output_file:
68-
try:
69-
write_string_to_file(output_content, args.output_file, args.output_format)
70-
print(f"Saved to {args.output_file}", file=sys.stderr)
71-
except IsADirectoryError:
72-
print(f"Error: {args.output_file} is a directory", file=sys.stderr)
73-
sys.exit(1)
74-
except OSError:
75-
sys.exit(1)
76-
elif args.force_stdout or not args.copy or not clipboard_ok:
77-
# force_stdout: -o - was explicitly passed (forces stdout even with --copy)
78-
sys.stdout.write(output_content)
79-
logging.info(f"Directory tree written to stdout in {args.output_format} format")
62+
copy_to_clipboard(output_content)
63+
print("Copied to clipboard", file=sys.stderr)
64+
return True
65+
except ClipboardError as e:
66+
print(f"Clipboard unavailable: {e}", file=sys.stderr)
67+
return False
8068

69+
70+
def _handle_output_file(output_content: str, args: ParsedArgs) -> None:
71+
from .writer import write_string_to_file
72+
73+
if not args.output_file:
74+
return
75+
try:
76+
write_string_to_file(output_content, args.output_file, args.output_format)
77+
print(f"Saved to {args.output_file}", file=sys.stderr)
78+
except IsADirectoryError:
79+
print(f"Error: {args.output_file} is a directory", file=sys.stderr)
80+
sys.exit(1)
81+
except OSError:
82+
sys.exit(1)
83+
84+
85+
def _run() -> None:
86+
from .cli import parse_args
87+
from .logger import setup_logging
88+
from .tokens import print_token_summary
89+
from .writer import tree_to_string
90+
91+
args = parse_args()
92+
setup_logging(args.verbosity)
93+
94+
directory_tree = _build_diff_tree(args) if args.diff_range else _build_standard_tree(args)
95+
96+
output_content = tree_to_string(directory_tree, args.output_format)
97+
print_token_summary(output_content)
98+
99+
clipboard_ok = _handle_clipboard(output_content, args)
100+
_handle_output_file(output_content, args)
101+
102+
should_write_stdout = args.force_stdout or not args.copy or not clipboard_ok
103+
if not args.output_file and should_write_stdout:
104+
sys.stdout.write(output_content)
105+
logging.info(f"Directory tree written to stdout in {args.output_format} format")
106+
107+
108+
def main() -> None:
109+
_configure_windows_utf8()
110+
try:
111+
_run()
81112
except KeyboardInterrupt:
82113
print("\nInterrupted", file=sys.stderr)
83114
sys.exit(130)

tests/conftest.py

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,8 @@ def _run(args):
115115
main()
116116
return True
117117
except SystemExit as e:
118-
if e.code != 0:
119-
print(f"SystemExit caught with code: {e.code}")
120-
return False
121-
return True
122-
except Exception as e:
123-
print(f"Caught unexpected exception in run_mapper: {e}")
124-
raise
118+
# Intentional: CLI uses sys.exit() for normal termination
119+
return e.code == 0
125120

126121
return _run
127122

@@ -165,30 +160,39 @@ def run_treemapper_subprocess(args, cwd=None, **kwargs):
165160
return subprocess.run(command, cwd=cwd, **kwargs)
166161

167162

168-
# ---> НАЧАЛО: Перенесенная фикстура set_perms <---
163+
def _check_wsl_windows_path(path: Path) -> bool:
164+
if not IS_WSL:
165+
return False
166+
return "/mnt/" in str(path)
167+
168+
169+
def _restore_permissions(paths_changed: list[Path], original_perms: dict[Path, int]) -> None:
170+
logging.debug(f"Cleaning up permissions for: {paths_changed}")
171+
for path in paths_changed:
172+
if not path.exists() or path not in original_perms:
173+
continue
174+
orig = original_perms[path]
175+
if orig is None:
176+
logging.warning(f"Original permissions for {path} were None, not restoring.")
177+
continue
178+
try:
179+
os.chmod(path, orig)
180+
logging.debug(f"Restored permissions for {path}")
181+
except OSError as e:
182+
logging.warning(f"Could not restore permissions for {path}: {e}")
183+
184+
169185
@pytest.fixture
170-
def set_perms(request):
186+
def set_perms():
171187
"""Fixture to temporarily set file/directory permissions (non-Windows)."""
172-
original_perms = {}
173-
paths_changed = []
188+
original_perms: dict[Path, int] = {}
189+
paths_changed: list[Path] = []
174190

175191
def _set_perms(path: Path, perms: int):
176192
if sys.platform == "win32":
177193
pytest.skip("Permission tests skipped on Windows.")
178-
179-
# Check if running in WSL environment
180-
is_wsl = False
181-
try:
182-
with open("/proc/version") as f:
183-
if "microsoft" in f.read().lower():
184-
is_wsl = True
185-
except FileNotFoundError:
186-
pass
187-
188-
# Skip tests on Windows paths in WSL
189-
if is_wsl and "/mnt/" in str(path):
194+
if _check_wsl_windows_path(path):
190195
pytest.skip(f"Permission tests skipped on Windows-mounted paths in WSL: {path}")
191-
192196
if not path.exists():
193197
pytest.skip(f"Path does not exist, cannot set permissions: {path}")
194198
try:
@@ -201,20 +205,7 @@ def _set_perms(path: Path, perms: int):
201205

202206
yield _set_perms
203207

204-
logging.debug(f"Cleaning up permissions for: {paths_changed}")
205-
for path in paths_changed:
206-
if path.exists() and path in original_perms:
207-
try:
208-
if original_perms[path] is not None:
209-
os.chmod(path, original_perms[path])
210-
logging.debug(f"Restored permissions for {path}")
211-
else:
212-
logging.warning(f"Original permissions for {path} were None, not restoring.")
213-
except OSError as e:
214-
logging.warning(f"Could not restore permissions for {path}: {e}")
215-
216-
217-
# ---> КОНЕЦ: Перенесенная фикстура set_perms <---
208+
_restore_permissions(paths_changed, original_perms)
218209

219210

220211
# --- New fixtures for test modernization ---

0 commit comments

Comments
 (0)