Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ classifiers = [
]
dependencies = [
"tomli>=2.0.0; python_version < '3.11'",
"click>=8.1.0",
"send2trash>=1.8.2",
]

Expand Down
2 changes: 1 addition & 1 deletion src/devklean/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from devklean.cli import main
from devklean.cli.main import main

if __name__ == "__main__":
main()
10 changes: 9 additions & 1 deletion src/devklean/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
from devklean.cli.main import main
from __future__ import annotations

from importlib import import_module

__all__ = ["main"]


def __getattr__(name: str):
if name == "main":
return import_module("devklean.cli.main").main
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
13 changes: 11 additions & 2 deletions src/devklean/cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
from devklean.cli.commands.clean import run_clean
from devklean.cli.commands.scan import run_scan
from __future__ import annotations

from importlib import import_module

__all__ = ["run_clean", "run_scan"]


def __getattr__(name: str):
if name == "run_clean":
return import_module("devklean.cli.commands.clean").run_clean
if name == "run_scan":
return import_module("devklean.cli.commands.scan").run_scan
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
8 changes: 7 additions & 1 deletion src/devklean/cli/commands/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
exceeds_threshold,
)
from devklean.config.models import AppConfig
from devklean.deletion import SafetyValidator, delete_items
from devklean.deletion.safety import SafetyValidator
from devklean.models import CleanableItem
from devklean.output.base import Renderer


def delete_items(*args, **kwargs):
from devklean.deletion import delete_items as _delete_items

return _delete_items(*args, **kwargs)


def _confirm_deletion(
renderer: Renderer,
count: int,
Expand Down
4 changes: 3 additions & 1 deletion src/devklean/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import sys

import click

from devklean.cli.dispatcher import dispatch
from devklean.cli.parser import build_parser, resolve_bare_invocation
from devklean.config import ConfigManager
Expand Down Expand Up @@ -62,5 +64,5 @@ def main() -> None:
sys.exit(exit_code)
except KeyboardInterrupt:
# 130 is the Unix convention for a SIGINT-terminated process (128 + 2).
print("\nAborted.", file=sys.stderr)
click.echo("\nAborted.", file=sys.stderr)
sys.exit(130)
24 changes: 19 additions & 5 deletions src/devklean/deletion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from __future__ import annotations

from devklean.deletion.integrity import IntegrityReport, check_integrity
from devklean.deletion.metadata import MetadataManager
from devklean.deletion.paths import get_deletion_metadata_dir
from devklean.deletion.safety import SafetyValidator, SafetyViolation
from devklean.deletion.trash import delete_items
from importlib import import_module

__all__ = [
"IntegrityReport",
Expand All @@ -15,3 +11,21 @@
"delete_items",
"get_deletion_metadata_dir",
]


def __getattr__(name: str):
if name == "IntegrityReport":
return import_module("devklean.deletion.integrity").IntegrityReport
if name == "check_integrity":
return import_module("devklean.deletion.integrity").check_integrity
if name == "MetadataManager":
return import_module("devklean.deletion.metadata").MetadataManager
if name == "get_deletion_metadata_dir":
return import_module("devklean.deletion.paths").get_deletion_metadata_dir
if name == "SafetyValidator":
return import_module("devklean.deletion.safety").SafetyValidator
if name == "SafetyViolation":
return import_module("devklean.deletion.safety").SafetyViolation
if name == "delete_items":
return import_module("devklean.deletion.trash").delete_items
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
16 changes: 13 additions & 3 deletions src/devklean/output/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
from devklean.output.base import Renderer
from devklean.output.json import JsonRenderer
from devklean.output.text import TextRenderer
from __future__ import annotations

from importlib import import_module

__all__ = ["JsonRenderer", "Renderer", "TextRenderer"]


def __getattr__(name: str):
if name == "Renderer":
return import_module("devklean.output.base").Renderer
if name == "JsonRenderer":
return import_module("devklean.output.json").JsonRenderer
if name == "TextRenderer":
return import_module("devklean.output.text").TextRenderer
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
8 changes: 5 additions & 3 deletions src/devklean/output/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import sys
from typing import TextIO

import click

from devklean.output.theme import Palette, get_theme

# Semantic symbols — fixed across every command and renderer.
Expand Down Expand Up @@ -73,7 +75,7 @@ def paint(self, text: str, role: str) -> str:

def _line(self, symbol: str, role: str, message: str) -> None:
prefix = self.paint(symbol, role)
print(f"{prefix} {message}", file=self._stream)
click.echo(f"{prefix} {message}", file=self._stream)

def success(self, message: str) -> None:
self._line(SYM_SUCCESS, "success", message)
Expand All @@ -88,7 +90,7 @@ def info(self, message: str) -> None:
self._line(SYM_INFO, "info", message)

def detail(self, message: str) -> None:
print(self.paint(message, "detail"), file=self._stream)
click.echo(self.paint(message, "detail"), file=self._stream)

def plain(self, message: str = "") -> None:
print(message, file=self._stream)
click.echo(message, file=self._stream)
8 changes: 7 additions & 1 deletion src/devklean/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
confirm_large_deletion,
exceeds_threshold,
)
from devklean.deletion import SafetyValidator, delete_items
from devklean.deletion.safety import SafetyValidator
from devklean.formatting import format_size, truncate
from devklean.models import CleanableItem
from devklean.output.base import Renderer
from devklean.output.sorting import items_by_size_desc


def delete_items(*args, **kwargs):
from devklean.deletion import delete_items as _delete_items

return _delete_items(*args, **kwargs)


def interactive_ui(stdscr, items: list[CleanableItem], dry_run: bool) -> list[int] | None:
import curses # Unix-only; imported lazily so this module loads on Windows.

Expand Down
3 changes: 3 additions & 0 deletions tests/test_windows_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import io
import os
import subprocess
import sys
from argparse import Namespace
Expand Down Expand Up @@ -52,10 +53,12 @@ def test_tui_module_imports_cold_without_curses() -> None:
crash every command on Windows) and an import-order/circular-import
regression that an in-process import would mask.
"""
env = {**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")}
result = subprocess.run(
[sys.executable, "-c", "import devklean.tui; print(devklean.tui.run_interactive.__name__)"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 0, result.stderr
assert "run_interactive" in result.stdout
Expand Down
Loading