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 +11 -0
- tailwhip/__main__.py +8 -0
- tailwhip/cli.py +158 -0
- tailwhip/configuration.py +119 -0
- tailwhip/configuration.toml +427 -0
- tailwhip/files.py +193 -0
- tailwhip/process.py +235 -0
- tailwhip/sorting.py +231 -0
- tailwhip/tests/__init__.py +1 -0
- tailwhip/tests/test_cli.py +240 -0
- tailwhip/tests/test_file_finder.py +230 -0
- tailwhip/tests/test_sorting.py +465 -0
- tailwhip/tests/test_write.py +114 -0
- tailwhip-0.9.dist-info/METADATA +179 -0
- tailwhip-0.9.dist-info/RECORD +19 -0
- tailwhip-0.9.dist-info/WHEEL +5 -0
- tailwhip-0.9.dist-info/entry_points.txt +2 -0
- tailwhip-0.9.dist-info/licenses/LICENSE +25 -0
- tailwhip-0.9.dist-info/top_level.txt +1 -0
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
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()
|