diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05c9d63..adcbeb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,12 +16,12 @@ repos: - id: check-json - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 24.2.0 hooks: - id: black - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.1" + rev: "v0.3.2" hooks: - id: ruff diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d969f96 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/poetry.lock b/poetry.lock index 9a0efc9..6d85bb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -427,22 +427,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.0.1" +version = "7.0.2" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -603,13 +603,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp [[package]] name = "mkdocs-material" -version = "9.5.12" +version = "9.5.13" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.12-py3-none-any.whl", hash = "sha256:d6f0c269f015e48c76291cdc79efb70f7b33bbbf42d649cfe475522ebee61b1f"}, - {file = "mkdocs_material-9.5.12.tar.gz", hash = "sha256:5f69cef6a8aaa4050b812f72b1094fda3d079b9a51cf27a247244c03ec455e97"}, + {file = "mkdocs_material-9.5.13-py3-none-any.whl", hash = "sha256:5cbe17fee4e3b4980c8420a04cc762d8dc052ef1e10532abd4fce88e5ea9ce6a"}, + {file = "mkdocs_material-9.5.13.tar.gz", hash = "sha256:d8e4caae576312a88fd2609b81cf43d233cdbe36860d67a68702b018b425bd87"}, ] [package.dependencies] @@ -643,13 +643,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -720,13 +720,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.7" +version = "10.7.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, - {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] [package.dependencies] @@ -757,13 +757,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytes [[package]] name = "pytest" -version = "8.0.2" +version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] @@ -771,11 +771,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1103,13 +1103,13 @@ files = [ [[package]] name = "tox" -version = "4.13.0" +version = "4.14.1" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ - {file = "tox-4.13.0-py3-none-any.whl", hash = "sha256:1143c7e2489c68026a55d3d4ae84c02c449f073b28e62f80e3e440a3b72a4afa"}, - {file = "tox-4.13.0.tar.gz", hash = "sha256:dd789a554c16c4b532924ba393c92fc8991323c4b3d466712bfecc8c9b9f24f7"}, + {file = "tox-4.14.1-py3-none-any.whl", hash = "sha256:b03754b6ee6dadc70f2611da82b4ed8f625fcafd247e15d1d0cb056f90a06d3b"}, + {file = "tox-4.14.1.tar.gz", hash = "sha256:f0ad758c3bbf7e237059c929d3595479363c3cdd5a06ac3e49d1dd020ffbee45"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 6d56b4f..f5f8869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,48 +82,27 @@ ignore = ["./tests/**/*"] target-version = "py38" line-length = 120 select = [ - "A", - "ARG", - "B", - "C", - "DJ", - "DTZ", - "E", - "EM", - "F", - "FBT", - "I", - "ICN", - "ISC", - "N", - "PLC", - "PLE", - "PLR", - "PLW", - "Q", - "RUF", - "S", - "T", - "TID", - "UP", - "W", - "YTT", + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DJ", # flake8-django + "E", # pycodestyle + "F", # pyflakes + "FBT", # flake8-boolean-trap + "N", # pep8-naming + "Q", # flake8-quotes + "TID", # flake8-tidy-imports + "W", # pycodestyle + "YTT", # flake8-2020 ] ignore = [ # Allow non-abstract empty methods in abstract base classes "B027", # Allow boolean positional values in function calls, like `dict.get(... True)` "FBT003", - # Ignore checks for possible passwords - "S105", - "S106", - "S107", # Ignore complexity "C901", - "PLR0911", - "PLR0912", - "PLR0913", - "PLR0915", ] unfixable = [ # Don't touch unused imports diff --git a/src/django_tailwind_cli/management/commands/tailwind.py b/src/django_tailwind_cli/management/commands/tailwind.py index d3f8842..e26be10 100644 --- a/src/django_tailwind_cli/management/commands/tailwind.py +++ b/src/django_tailwind_cli/management/commands/tailwind.py @@ -9,24 +9,23 @@ import urllib.request from multiprocessing import Process from pathlib import Path -from typing import Any, List, Union +from typing import Annotated, List, Optional, Union import certifi from django.conf import settings -from django.core.management.base import BaseCommand, CommandError, CommandParser +from django.core.management.base import CommandError from django.template.utils import get_app_template_dirs +from django_typer import TyperCommand, command, initialize +from typer import Argument, Option from django_tailwind_cli.utils import Config -class Command(BaseCommand): - """Create and manage a Tailwind CSS theme.""" - - def __init__(self, *args: Any, **kwargs: Any): - """Initialize the command.""" - - super().__init__(*args, **kwargs) +class Command(TyperCommand): + help = """Create and manage a Tailwind CSS theme.""" + @initialize() + def init(self): # Get the config from the settings and validate it. self.config = Config() try: @@ -35,222 +34,188 @@ def __init__(self, *args: Any, **kwargs: Any): msg = "Configuration error" raise CommandError(msg) from e - def add_arguments(self, parser: CommandParser) -> None: - """Add arguments to the command.""" - subparsers = parser.add_subparsers(dest="tailwind", required=True) - - subparsers.add_parser("build", help="Build a minified production ready CSS file.") - - subparsers.add_parser("watch", help="Start Tailwind CLI in watch mode during development.") - - subparsers.add_parser("list_templates", help="List the templates of your django project.") - - runserver_parser = subparsers.add_parser( - "runserver", - help="Start the Django development server and the Tailwind CLI in watch mode.", - ) - - runserver_parser.add_argument( - "--ipv6", - "-6", - action="store_true", - dest="use_ipv6", - help="Tells Django to use an IPv6 address.", - ) - runserver_parser.add_argument( - "--nothreading", - action="store_true", - dest="no_threading", - help="Tells Django to NOT use threading.", - ) - runserver_parser.add_argument( - "--nostatic", - action="store_true", - dest="no_reloader", - help="Tells Django to NOT use the auto-reloader.", - ) - runserver_parser.add_argument( - "--noreload", - action="store_true", - dest="no_reloader", - help="Tells Django to NOT use the auto-reloader.", - ) - runserver_parser.add_argument( - "--skip-checks", - action="store_true", - help="Skip system checks.", - ) - runserver_parser.add_argument( - "addrport", nargs="?", help="Optional port number, or ipaddr:port" - ) - - runserver_plus_parser = subparsers.add_parser( - "runserver_plus", - help=( - "Start the django-extensions runserver_plus development server and the " - "Tailwind CLI in watch mode." - ), - ) - runserver_plus_parser.add_argument( - "--ipv6", - "-6", - action="store_true", - dest="use_ipv6", - help="Tells Django to use an IPv6 address.", - ) - runserver_plus_parser.add_argument( - "--nothreading", - action="store_true", - dest="no_threading", - help="Do not run in multithreaded mode.", - ) - runserver_plus_parser.add_argument( - "--noreload", - action="store_true", - dest="no_reloader", - help="Tells Django to NOT use the auto-reloader.", - ) - runserver_plus_parser.add_argument( - "--pdb", - action="store_true", - help="Drop into pdb shell at the start of any view.", - ) - runserver_plus_parser.add_argument( - "--ipdb", - action="store_true", - help="Drop into ipdb shell at the start of any view.", - ) - runserver_plus_parser.add_argument( - "--pm", - action="store_true", - help="Drop into (i)pdb shell if an exception is raised in a view.", - ) - runserver_plus_parser.add_argument( - "--print-sql", - action="store_true", - help="Print SQL queries as they're executed.", - ) - runserver_plus_parser.add_argument( - "--cert-file", help="Optional SSL certificate file to use for the development server." - ) - runserver_plus_parser.add_argument( - "--cert", - help="[DEPRECATED] Optional SSL certificate file to use for the development server.", - ) - runserver_plus_parser.add_argument( - "--key-file", help="Optional SSL certificate file to use for the development server." - ) - runserver_plus_parser.add_argument( - "--reloader-interval", - help="Optional SSL certificate file to use for the development server.", - ) - runserver_plus_parser.add_argument( - "addrport", nargs="?", help="Optional port number, or ipaddr:port" - ) - - def handle(self, *_args: Any, **kwargs: Any) -> None: - """Perform the command's actions.""" - - # Get the subcommand from the kwargs. - label = kwargs.get("tailwind") - del kwargs["tailwind"] - # Before running the actual subcommand, we need to make sure that the CLI is installed and # the config file exists. self._download_cli_if_not_exists() self._create_tailwind_config_if_not_exists() - # Start the subcommand. - if label == "build": - self.build() - elif label == "watch": - self.watch() - elif label == "runserver": - kwargs["runserver_cmd"] = "runserver" - self.runserver(**kwargs) - elif label == "runserver_plus": - if importlib.util.find_spec("django_extensions") and importlib.util.find_spec( - "werkzeug" - ): - kwargs["runserver_cmd"] = "runserver_plus" - self.runserver(**kwargs) - else: - msg = ( - "Missing dependencies. Follow the instructions found on " - "https://django-tailwind-cli.andrich.me/installation/." - ) - raise CommandError(msg) - elif label == "list_templates": - self.list_templates() - - def build(self) -> None: + @command(help="Build a minified production ready CSS file.") + def build(self): + build_cmd = [ + str(self.config.get_full_cli_path()), + "--output", + str(self.config.get_full_dist_css_path()), + "--minify", + ] + if self.config.src_css is not None: + build_cmd.extend( + [ + "--input", + str(self.config.get_full_src_css_path()), + ] + ) try: - subprocess.run(self.get_build_cmd(), cwd=settings.BASE_DIR, check=True) # noqa: S603 + subprocess.run(build_cmd, cwd=settings.BASE_DIR, check=True) # noqa: S603 except KeyboardInterrupt: - self.stdout.write(self.style.ERROR("Canceled building production stylesheet.")) + self._write_error("Canceled building production stylesheet.") else: - self.stdout.write( - self.style.SUCCESS( - f"Built production stylesheet '{self.config.get_full_dist_css_path()}'." - ) + self._write_success(f"Built production stylesheet '{self.config.get_full_dist_css_path()}'.") + + @command(help="Start Tailwind CLI in watch mode during development.") + def watch(self): + watch_cmd = [ + str(self.config.get_full_cli_path()), + "--output", + str(self.config.get_full_dist_css_path()), + "--watch", + ] + if self.config.src_css is not None: + watch_cmd.extend( + [ + "--input", + str(self.config.get_full_src_css_path()), + ] ) - def watch(self) -> None: - """Start Tailwind CLI in watch mode during development.""" try: - subprocess.run(self.get_watch_cmd(), cwd=settings.BASE_DIR, check=True) # noqa: S603 + subprocess.run(watch_cmd, cwd=settings.BASE_DIR, check=True) # noqa: S603 except KeyboardInterrupt: - self.stdout.write(self.style.SUCCESS("Stopped watching for changes.")) + self._write_success("Stopped watching for changes.") - @staticmethod - def runserver(**kwargs: Any) -> None: # pragma: no cover - """Start the Django development server and the Tailwind CLI in watch mode.""" + @command(name="list_templates", help="List the templates of your django project.") + def list_templates(self): + template_files: List[str] = [] + app_template_dirs = get_app_template_dirs("templates") + for app_template_dir in app_template_dirs: + template_files += self._list_template_files(app_template_dir) - # Start the watch process in a separate process. - watch_cmd = [sys.executable, "manage.py", "tailwind", "watch"] - watch_process = Process( - target=subprocess.run, - args=(watch_cmd,), - kwargs={ - "cwd": settings.BASE_DIR, - "check": True, - }, - ) + for template_dir in settings.TEMPLATES[0]["DIRS"]: + template_files += self._list_template_files(template_dir) - # Start the runserver process in the current process. - debug_server_cmd = [sys.executable, "manage.py", kwargs["runserver_cmd"]] + self.stdout.write("\n".join(template_files)) - if addrport := kwargs.get("addrport"): + @command(help="Start the Django development server and the Tailwind CLI in watch mode.") + def runserver( + self, + addrport: Annotated[Optional[str], Argument(help="Optional port number, or ipaddr:port")] = None, + *, + use_ipv6: Annotated[bool, Option("--ipv6", "-6", help="Tells Django to use an IPv6 address.")] = False, + no_threading: Annotated[bool, Option("--nothreading", help="Tells Django to NOT use threading.")] = False, + no_static: Annotated[ + bool, Option("--nostatic", help="Tells Django to NOT automatically serve static files at STATIC_URL.") + ] = False, + no_reloader: Annotated[bool, Option("--noreload", help="Tells Django to NOT use the auto-reloader.")] = False, + skip_checks: Annotated[bool, Option("--skip-checks", help="Skip system checks.")] = False, + ): + debug_server_cmd = [sys.executable, "manage.py", "runserver"] + + if use_ipv6: + debug_server_cmd.append("--ipv6") + if no_threading: + debug_server_cmd.append("--nothreading") + if no_static: + debug_server_cmd.append("--nostatic") + if no_reloader: + debug_server_cmd.append("--noreload") + if skip_checks: + debug_server_cmd.append("--skip-checks") + if addrport: debug_server_cmd.append(addrport) - if kwargs.get("use_ipv6", False): + self._runserver(debug_server_cmd) + + @command( + name="runserver_plus", + help="Start the django-extensions runserver_plus development server and the Tailwind CLI in watch mode.", + ) + def runserver_plus( + self, + addrport: Annotated[Optional[str], Argument(help="Optional port number, or ipaddr:port")] = None, + *, + use_ipv6: Annotated[bool, Option("--ipv6", "-6", help="Tells Django to use an IPv6 address.")] = False, + no_threading: Annotated[bool, Option("--nothreading", help="Tells Django to NOT use threading.")] = False, + no_static: Annotated[ + bool, Option("--nostatic", help="Tells Django to NOT automatically serve static files at STATIC_URL.") + ] = False, + no_reloader: Annotated[bool, Option("--noreload", help="Tells Django to NOT use the auto-reloader.")] = False, + skip_checks: Annotated[bool, Option("--skip-checks", help="Skip system checks.")] = False, + pdb: Annotated[bool, Option("--pdb", help="Drop into pdb shell at the start of any view.")] = False, + ipdb: Annotated[bool, Option("--ipdb", help="Drop into ipdb shell at the start of any view.")] = False, + pm: Annotated[bool, Option("--pm", help="Drop into (i)pdb shell if an exception is raised in a view.")] = False, + print_sql: Annotated[bool, Option("--print-sql", help="Print SQL queries as they're executed.")] = False, + print_sql_location: Annotated[ + bool, Option("--print-sql-location", help="Show location in code where SQL query generated from.") + ] = False, + cert_file: Annotated[ + Optional[str], + Option( + help=( + "SSL .crt file path. If not provided path from --key-file will be selected. Either --cert-file or " + "--key-file must be provided to use SSL." + ) + ), + ] = None, + key_file: Annotated[ + Optional[str], + Option( + help=( + "SSL .key file path. If not provided path from --cert-file will be " + "selected. Either --cert-file or --key-file must be provided to use SSL." + ) + ), + ] = None, + ): + if not importlib.util.find_spec("django_extensions") and not importlib.util.find_spec("werkzeug"): + msg = ( + "Missing dependencies. Follow the instructions found on " + "https://django-tailwind-cli.andrich.me/installation/." + ) + raise CommandError(msg) + + debug_server_cmd = [sys.executable, "manage.py", "runserver_plus"] + + if use_ipv6: debug_server_cmd.append("--ipv6") - if kwargs.get("no_threading", False): + if no_threading: debug_server_cmd.append("--nothreading") - if kwargs.get("no_reloader", False): + if no_static: + debug_server_cmd.append("--nostatic") + if no_reloader: debug_server_cmd.append("--noreload") - if kwargs.get("skip_checks", False): + if skip_checks: debug_server_cmd.append("--skip-checks") - - if kwargs.get("print_sql", False): - debug_server_cmd.append("--print-sql") - if kwargs.get("pdb", False): + if pdb: debug_server_cmd.append("--pdb") - if kwargs.get("ipdb", False): + if ipdb: debug_server_cmd.append("--ipdb") - if kwargs.get("pm", False): + if pm: debug_server_cmd.append("--pm") - - if cert_file := kwargs.get("cert_file"): + if print_sql: + debug_server_cmd.append("--print-sql") + if print_sql_location: + debug_server_cmd.append("--print-sql-location") + if cert_file: debug_server_cmd.append(f"--cert-file={cert_file}") - elif cert := kwargs.get("cert"): - debug_server_cmd.append(f"--cert-file={cert}") - if key_file := kwargs.get("key_file"): + if key_file: debug_server_cmd.append(f"--key-file={key_file}") + if addrport: + debug_server_cmd.append(addrport) + + self._runserver(debug_server_cmd) - if reloader_interval := kwargs.get("reloader_interval"): - debug_server_cmd.append(f"--reloader-interval={reloader_interval}") + def _runserver(self, debug_server_cmd: List[str]) -> None: + # Start the watch process in a separate process. + watch_cmd = [sys.executable, "manage.py", "tailwind", "watch"] + watch_process = Process( + target=subprocess.run, + args=(watch_cmd,), + kwargs={ + "cwd": settings.BASE_DIR, + "check": True, + }, + ) + # Start the runserver process in the current process. debugserver_process = Process( target=subprocess.run, args=(debug_server_cmd,), @@ -269,78 +234,18 @@ def runserver(**kwargs: Any) -> None: # pragma: no cover watch_process.terminate() debugserver_process.terminate() - def list_templates(self) -> None: - template_files: List[str] = [] - app_template_dirs = get_app_template_dirs("templates") - for app_template_dir in app_template_dirs: - template_files += self.list_template_files(app_template_dir) - - for template_dir in settings.TEMPLATES[0]["DIRS"]: - template_files += self.list_template_files(template_dir) - - self.stdout.write("\n".join(template_files)) - - @staticmethod - def list_template_files(template_dir: Union[str, Path]) -> List[str]: - template_files: List[str] = [] - for d, _, filenames in os.walk(str(template_dir)): - for filename in filenames: - if filename.endswith(".html") or filename.endswith(".txt"): - template_files.append(os.path.join(d, filename)) - return template_files - - def get_build_cmd(self) -> List[str]: - """Get the command to build the CSS.""" - if self.config.src_css is None: - return [ - str(self.config.get_full_cli_path()), - "--output", - str(self.config.get_full_dist_css_path()), - "--minify", - ] - else: - return [ - str(self.config.get_full_cli_path()), - "--input", - str(self.config.get_full_src_css_path()), - "--output", - str(self.config.get_full_dist_css_path()), - "--minify", - ] - - def get_watch_cmd(self) -> List[str]: - """Get the command to watch the CSS.""" - if self.config.src_css is None: - return [ - str(self.config.get_full_cli_path()), - "--output", - str(self.config.get_full_dist_css_path()), - "--watch", - ] - else: - return [ - str(self.config.get_full_cli_path()), - "--input", - str(self.config.get_full_src_css_path()), - "--output", - str(self.config.get_full_dist_css_path()), - "--watch", - ] - def _download_cli_if_not_exists(self) -> None: dest_file = self.config.get_full_cli_path() if not dest_file.exists() and self.config.automatic_download: download_url = self.config.get_download_url() self.stdout.write(self.style.ERROR("Tailwind CSS CLI not found.")) - self.stdout.write( - self.style.WARNING(f"Downloading Tailwind CSS CLI from '{download_url}'") - ) + self.stdout.write(self.style.WARNING(f"Downloading Tailwind CSS CLI from '{download_url}'")) dest_file.parent.mkdir(parents=True, exist_ok=True) certifi_context = ssl.create_default_context(cafile=certifi.where()) - with urllib.request.urlopen( # noqa: S310 - download_url, context=certifi_context - ) as source, dest_file.open(mode="wb") as dest: + with urllib.request.urlopen(download_url, context=certifi_context) as source, dest_file.open( # noqa: S310 + mode="wb" + ) as dest: shutil.copyfileobj(source, dest) # make cli executable dest_file.chmod(0o755) @@ -352,9 +257,22 @@ def _create_tailwind_config_if_not_exists(self) -> None: if not tailwind_config_file.exists(): self.stdout.write(self.style.ERROR("Tailwind CSS config not found.")) tailwind_config_file.write_text(DEFAULT_TAILWIND_CONFIG) - self.stdout.write( - self.style.SUCCESS(f"Created Tailwind CSS config at '{tailwind_config_file}'") - ) + self.stdout.write(self.style.SUCCESS(f"Created Tailwind CSS config at '{tailwind_config_file}'")) + + @staticmethod + def _list_template_files(template_dir: Union[str, Path]) -> List[str]: + template_files: List[str] = [] + for d, _, filenames in os.walk(str(template_dir)): + for filename in filenames: + if filename.endswith(".html") or filename.endswith(".txt"): + template_files.append(os.path.join(d, filename)) + return template_files + + def _write_error(self, message: str) -> None: + self.stdout.write(self.style.ERROR(message)) + + def _write_success(self, message: str) -> None: + self.stdout.write(self.style.SUCCESS(message)) DEFAULT_TAILWIND_CONFIG = """/** @type {import('tailwindcss').Config} */ diff --git a/tests/test_management_commands.py b/tests/test_management_commands.py index e22c7c7..439769f 100644 --- a/tests/test_management_commands.py +++ b/tests/test_management_commands.py @@ -4,7 +4,6 @@ from django.core.management import CommandError, call_command from django_tailwind_cli.management.commands.tailwind import DEFAULT_TAILWIND_CONFIG -from django_tailwind_cli.management.commands.tailwind import Command as TailwindCommand @pytest.fixture(autouse=True) @@ -18,7 +17,7 @@ def configure_settings(mocker): def test_calling_unknown_subcommand(): - with pytest.raises(CommandError, match="invalid choice: 'not_a_valid_command'"): + with pytest.raises(CommandError, match="No such command 'not_a_valid_command'"): call_command("tailwind", "not_a_valid_command") @@ -111,10 +110,23 @@ def test_build_keyboard_interrupt(settings, tmp_path, mocker, capsys): assert "Canceled building production stylesheet." in captured.out -def test_get_build_cmd(settings): - assert "--input" not in TailwindCommand().get_build_cmd() +def test_build_without_input_file(settings, tmp_path, mocker): + settings.BASE_DIR = tmp_path + settings.TAILWIND_CLI_PATH = str(tmp_path) + subprocess_run = mocker.patch("subprocess.run") + call_command("tailwind", "build") + name, args, kwargs = subprocess_run.mock_calls[0] + assert "--input" not in args[0] + + +def test_build_with_input_file(settings, tmp_path, mocker): + settings.BASE_DIR = tmp_path + settings.TAILWIND_CLI_PATH = str(tmp_path) settings.TAILWIND_CLI_SRC_CSS = "css/source.css" - assert "--input" in TailwindCommand().get_build_cmd() + subprocess_run = mocker.patch("subprocess.run") + call_command("tailwind", "build") + name, args, kwargs = subprocess_run.mock_calls[0] + assert "--input" in args[0] def test_watch_subprocess_run_called(settings, tmp_path, mocker): @@ -159,10 +171,23 @@ def test_watch_keyboard_interrupt(settings, tmp_path, mocker, capsys): assert "Stopped watching for changes." in captured.out -def test_get_watch_cmd(settings): - assert "--input" not in TailwindCommand().get_watch_cmd() +def test_watch_without_input_file(settings, tmp_path, mocker): + settings.BASE_DIR = tmp_path + settings.TAILWIND_CLI_PATH = str(tmp_path) + subprocess_run = mocker.patch("subprocess.run") + call_command("tailwind", "watch") + name, args, kwargs = subprocess_run.mock_calls[0] + assert "--input" not in args[0] + + +def test_watch_with_input_file(settings, tmp_path, mocker): + settings.BASE_DIR = tmp_path + settings.TAILWIND_CLI_PATH = str(tmp_path) settings.TAILWIND_CLI_SRC_CSS = "css/source.css" - assert "--input" in TailwindCommand().get_watch_cmd() + subprocess_run = mocker.patch("subprocess.run") + call_command("tailwind", "watch") + name, args, kwargs = subprocess_run.mock_calls[0] + assert "--input" in args[0] def test_runserver(): @@ -174,7 +199,7 @@ def test_runserver_plus_with_django_extensions_installed(): def test_runserver_plus_without_django_extensions_installed(mocker): - mocker.patch.dict(sys.modules, {"django_extensions": None}) + mocker.patch.dict(sys.modules, {"django_extensions": None, "werkzeug": None}) with pytest.raises(CommandError, match="Missing dependencies."): call_command("tailwind", "runserver_plus")