tailwhip 0.9__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.
tailwhip/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Tailwhip - Sort Tailwind CSS classes in HTML and CSS files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import metadata
6
+
7
+ from tailwhip.cli import main
8
+
9
+ __version__ = metadata.version("tailwhip")
10
+ __author__ = "Martin Mahner"
11
+ __all__ = ["__author__", "__version__", "main"]
tailwhip/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Entry point for python -m tailwhip."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tailwhip.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
tailwhip/cli.py ADDED
@@ -0,0 +1,158 @@
1
+ """CLI entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+ from importlib import metadata
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from tailwhip.configuration import (
15
+ CONSOLE_THEME,
16
+ VerbosityLevel,
17
+ config,
18
+ get_pyproject_toml_data,
19
+ update_configuration,
20
+ )
21
+ from tailwhip.files import apply_changes, find_files
22
+
23
+
24
+ def version_callback(value: bool) -> None:
25
+ """Print version and exit."""
26
+ if value:
27
+ version = metadata.version("tailwhip")
28
+ typer.echo(f"tailwhip {version}")
29
+ raise typer.Exit
30
+
31
+
32
+ app = typer.Typer(
33
+ help="Sort Tailwind CSS classes in HTML and CSS files.",
34
+ add_completion=False,
35
+ rich_markup_mode="rich",
36
+ )
37
+
38
+
39
+ def main() -> None:
40
+ """Entrypoint for the CLI."""
41
+ app()
42
+
43
+
44
+ @app.command(context_settings={"help_option_names": ["-h", "--help"]})
45
+ def run( # noqa: PLR0913
46
+ paths: Annotated[
47
+ list[Path],
48
+ typer.Argument(
49
+ help="Files or directories to process.",
50
+ metavar="PATH",
51
+ ),
52
+ ],
53
+ version: Annotated[ # noqa: ARG001
54
+ bool,
55
+ typer.Option(
56
+ "--version",
57
+ "-V",
58
+ callback=version_callback,
59
+ is_eager=True,
60
+ help="Show version and exit.",
61
+ ),
62
+ ] = False,
63
+ write_mode: Annotated[
64
+ bool,
65
+ typer.Option(
66
+ "--write",
67
+ "-w",
68
+ help="Write changes to files (default: dry-run mode).",
69
+ ),
70
+ ] = False,
71
+ quiet: Annotated[
72
+ bool,
73
+ typer.Option(
74
+ "--quiet",
75
+ "-q",
76
+ help="Suppress output except errors and warnings.",
77
+ ),
78
+ ] = False,
79
+ verbosity: Annotated[
80
+ int,
81
+ typer.Option(
82
+ "--verbose",
83
+ "-v",
84
+ count=True,
85
+ help="Increase output verbosity (-v: changes, -vv: diff, -vvv: debug).",
86
+ ),
87
+ ] = 0,
88
+ custom_configuration_file: Annotated[
89
+ Path | None,
90
+ typer.Option(
91
+ "--configuration",
92
+ "-c",
93
+ help="Load custom configuration file (overrides pyproject.toml settings).",
94
+ metavar="FILE",
95
+ ),
96
+ ] = None,
97
+ ) -> None:
98
+ """
99
+ Sort Tailwind CSS classes in HTML and CSS files.
100
+
101
+ Automatically discovers and sorts Tailwind classes according to a consistent
102
+ ordering. Supports HTML, CSS, and template files with Tailwind @apply directives.
103
+ """
104
+ # Check if the given configuration file exists
105
+ if custom_configuration_file and not custom_configuration_file.exists():
106
+ typer.echo(
107
+ f"Custom configuration file {custom_configuration_file} not found.",
108
+ err=True,
109
+ )
110
+ raise typer.Exit(1)
111
+
112
+ # Setup configuration values -------------------------------------------------------
113
+
114
+ # 1. Overwrite config defaults with pyproject.toml settings if they exist
115
+ if pyproject_data := get_pyproject_toml_data(Path.cwd()):
116
+ update_configuration(pyproject_data)
117
+
118
+ # 2. Overwrite config defaults with custom settings if they exist
119
+ if custom_configuration_file:
120
+ update_configuration(custom_configuration_file)
121
+
122
+ # 3. Overwrite config defaults with CLI options
123
+ update_configuration(
124
+ {
125
+ "write_mode": write_mode,
126
+ "verbosity": 0 if quiet else verbosity + 1,
127
+ }
128
+ )
129
+
130
+ # Setup helper tools ---------------------------------------------------------------
131
+
132
+ config.console = Console(quiet=quiet, theme=CONSOLE_THEME)
133
+
134
+ # Start Tailwhipping ---------------------------------------------------------------
135
+
136
+ start_time = time.time()
137
+ found_any, skipped, changed = apply_changes(targets=find_files(paths=paths))
138
+ duration = time.time() - start_time
139
+
140
+ if not found_any:
141
+ config.console.print("[red]Error: No files found[/red]")
142
+ sys.exit(1)
143
+
144
+ if config.verbosity == VerbosityLevel.NORMAL:
145
+ config.console.print(
146
+ "\nUse [important] -v [/important] (show unchanged files) or [important] -vv [/important] (show diff preview) for more detail."
147
+ )
148
+
149
+ if not config.write_mode:
150
+ config.console.print(
151
+ "\n:warning: Dry Run. No files were actually written. "
152
+ "Use [important] --write [/important] to write changes."
153
+ )
154
+
155
+ config.console.print(
156
+ f"⏱ Completed in [bold]{duration:.3f}s[/bold] for {changed} files. [dim]({skipped} skipped)[/dim]",
157
+ highlight=False,
158
+ )
@@ -0,0 +1,119 @@
1
+ """Configuration management for tailwhip."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from enum import IntEnum
7
+ from pathlib import Path
8
+
9
+ import dynaconf
10
+ import rich
11
+ import tomllib
12
+
13
+ # Path to default configuration file
14
+ BASE_CONFIGURATION_FILE = Path(__file__).parent / "configuration.toml"
15
+
16
+ # Console theme for rich.Console output
17
+ CONSOLE_THEME = rich.theme.Theme(
18
+ {
19
+ "important": "white on deep_pink4",
20
+ "highlight": "yellow1",
21
+ "filename": "white",
22
+ "bold": "sky_blue1",
23
+ }
24
+ )
25
+
26
+
27
+ def get_pyproject_toml_data(start_path: Path) -> Path | None:
28
+ """Search for pyproject.toml starting at the given path."""
29
+ pyproject_path = None
30
+
31
+ for directory in [start_path, *start_path.resolve().parents]:
32
+ candidate = directory / "pyproject.toml"
33
+ if candidate.exists():
34
+ pyproject_path = candidate
35
+ break
36
+
37
+ with pyproject_path.open("rb") as f:
38
+ data = tomllib.load(f)
39
+
40
+ return data.get("tool", {}).get("tailwhip")
41
+
42
+
43
+ def update_configuration(data: dict | Path) -> None:
44
+ """Update configuration with the given data."""
45
+ if isinstance(data, dict):
46
+ config.update(data, merge=False)
47
+ _recompile_constants()
48
+ return
49
+
50
+ if isinstance(data, Path):
51
+ with data.open("rb") as f:
52
+ config_data = tomllib.load(f)
53
+ config.update(config_data, merge=False)
54
+ _recompile_constants()
55
+ return
56
+
57
+ # pragma: no cover
58
+ msg = f"Invalid data type '{type(data)}' for configuration update." # pragma: no cover
59
+ raise TypeError(msg) # pragma: no cover
60
+
61
+
62
+ def _recompile_constants() -> None:
63
+ """Re-compile regular expressions pattern objects."""
64
+ config.tailwind_colors = set(config.tailwind_colors)
65
+ config.custom_colors = set(config.custom_colors)
66
+
67
+ config.UTILITY_PATTERNS = [re.compile("^" + g) for g in config.utility_groups]
68
+ config.VARIANT_PATTERNS = [re.compile(v) for v in config.variant_groups]
69
+ config.CLASS_ATTR_RE = re.compile(config.class_regex, re.IGNORECASE | re.DOTALL)
70
+ config.APPLY_RE = re.compile(config.apply_regex, re.MULTILINE)
71
+
72
+
73
+ class VerbosityLevel(IntEnum):
74
+ """Verbosity level enum."""
75
+
76
+ QUIET = 0
77
+ NORMAL = 1 # Default
78
+ VERBOSE = 2 # Show unchanged files
79
+ DIFF = 3 # Show diff of changes
80
+ DEBUG = 4
81
+
82
+
83
+ class TailwhipConfig(dynaconf.Dynaconf):
84
+ """Configuration for tailwhip."""
85
+
86
+ # Utilities created at runtime
87
+ console: rich.console.Console
88
+
89
+ # Settings provided by the base config
90
+ verbosity: VerbosityLevel
91
+ write_mode: bool
92
+ default_globs: list[str]
93
+ skip_expressions: list[str]
94
+ variant_separator: str
95
+ utility_groups: list[str]
96
+ variant_groups: list[str]
97
+ tailwind_colors: set[str]
98
+ custom_colors: set[str]
99
+ class_regex: str
100
+ apply_regex: str
101
+
102
+ # Compiled regex patterns
103
+ UTILITY_PATTERNS: list[re.Pattern]
104
+ VARIANT_PATTERNS: list[re.Pattern]
105
+ CLASS_ATTR_RE: re.Pattern
106
+ APPLY_RE: re.Pattern
107
+
108
+
109
+ config = TailwhipConfig(
110
+ settings_files=[str(BASE_CONFIGURATION_FILE)],
111
+ merge_enabled=False,
112
+ envvar_prefix="TAILWHIP",
113
+ root_path=Path.cwd(),
114
+ load_dotenv=False,
115
+ lowercase_read=True,
116
+ )
117
+
118
+ # Initialize constants on module load
119
+ _recompile_constants()