From 9255cf1c717caef10488ec4548490f551a87aae5 Mon Sep 17 00:00:00 2001 From: Ryan Hiebert Date: Sat, 21 Dec 2024 06:29:57 -0600 Subject: [PATCH] Add support to built-in django-admin command --- HISTORY.rst | 5 +++++ django_cmd.pths | 1 + django_cmd.py | 16 +++++++++++--- django_cmd_test.py | 52 +++++++++++++++++++--------------------------- pyproject.toml | 14 +++++++++++-- 5 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 django_cmd.pths diff --git a/HISTORY.rst b/HISTORY.rst index 63a3537..4529713 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,8 @@ +2.3 (2024-12-20) +++++++++++++++++ + +* Add configuration to the built-in ``django-admin`` command. + 2.2 (2024-12-19) ++++++++++++++++ diff --git a/django_cmd.pths b/django_cmd.pths new file mode 100644 index 0000000..618e948 --- /dev/null +++ b/django_cmd.pths @@ -0,0 +1 @@ +import django_cmd; print('hi'); django_cmd.patch_django() diff --git a/django_cmd.py b/django_cmd.py index a66a1ae..be8b132 100644 --- a/django_cmd.py +++ b/django_cmd.py @@ -1,6 +1,7 @@ import configparser import os import sys +from functools import wraps from pathlib import Path try: @@ -9,10 +10,10 @@ # Python < 3.11 import tomli as tomllib -from django.core.management import execute_from_command_line +import django.core.management -def main(): +def configure(): """Run Django, getting the default from a file if needed.""" settings_module = None @@ -37,4 +38,13 @@ def main(): if settings_module == os.environ["DJANGO_SETTINGS_MODULE"]: sys.path.insert(0, os.getcwd()) - execute_from_command_line(sys.argv) + +@wraps(django.core.management.ManagementUtility, updated=()) +class ConfiguredManagementUtility(django.core.management.ManagementUtility): + def execute(self): + configure() + return super().execute() + + +def patch_django(): + django.core.management.ManagementUtility = ConfiguredManagementUtility diff --git a/django_cmd_test.py b/django_cmd_test.py index dde64bd..9d892fd 100644 --- a/django_cmd_test.py +++ b/django_cmd_test.py @@ -4,7 +4,7 @@ import pytest -from django_cmd import main +from django_cmd import configure @contextmanager @@ -21,95 +21,85 @@ def restore_environ(keys): @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_passthru(monkeypatch, mocker, tmpdir): +def test_configure_passthru(monkeypatch, tmpdir): """It shouldn't change a given DJANGO_SETTINGS_MODULE.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "spam.eggs") content = "[django]\nsettings_module = ball.yarn\n" tmpdir.chdir() tmpdir.join("setup.cfg").write(content.encode("utf-8")) - main() + configure() assert os.environ.get("DJANGO_SETTINGS_MODULE") == "spam.eggs" - assert cmd.called @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_from_pyproject_toml(mocker, tmpdir): +def test_configure_from_pyproject_toml(tmpdir): """Read settings module path from toml file.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") content = '[tool.django]\nsettings_module = "ball.yarn"\n' tmpdir.chdir() tmpdir.join("pyproject.toml").write(content.encode("utf-8")) - main() + configure() assert os.environ.get("DJANGO_SETTINGS_MODULE") == "ball.yarn" - assert cmd.called @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_from_pyproject_toml_nosetting(mocker, tmpdir): +def test_configure_from_pyproject_toml_nosetting(mocker, tmpdir): """Handle if there's a tool.django section with no settings module.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") content = '[tool.django]\nsomesetting = "notrelevant"\n' tmpdir.chdir() tmpdir.join("pyproject.toml").write(content.encode("utf-8")) - main() + configure() assert "DJANGO_SETTINGS_MODULE" not in os.environ - assert cmd.called @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_from_pyproject_toml_nodjango(mocker, tmpdir): +def test_main_from_pyproject_toml_nodjango(tmpdir): """Handle if there's no tool.django section.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") content = '[project]\nname = "ball"\n' tmpdir.chdir() tmpdir.join("pyproject.toml").write(content.encode("utf-8")) - main() + configure() assert "DJANGO_SETTINGS_MODULE" not in os.environ - assert cmd.called @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_from_setup_cfg(mocker, tmpdir): +def test_configure_from_setup_cfg(tmpdir): """Read settings module path from config file.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") content = "[django]\nsettings_module = ball.yarn\n" tmpdir.chdir() tmpdir.join("setup.cfg").write(content.encode("utf-8")) - main() + configure() assert os.environ.get("DJANGO_SETTINGS_MODULE") == "ball.yarn" - assert cmd.called @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_main_no_configfile(mocker, tmpdir): +def test_configure_no_configfile(tmpdir): """Try to read settings module, but fail and still run command.""" - cmd = mocker.patch("django_cmd.execute_from_command_line") tmpdir.chdir() - main() + configure() assert "DJANGO_SETTINGS_MODULE" not in os.environ - assert cmd.called +@pytest.mark.parametrize("command", ["django", "django-admin"]) @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_new_project(tmpdir): +def test_new_project(command, tmpdir): """Should be able to use with a new project.""" tmpdir.chdir() - subprocess.run(["django", "startproject", "myproject", "."], check=True) + subprocess.run([command, "startproject", "myproject", "."], check=True) config = '[tool.django]\nsettings_module = "myproject.settings"\n' tmpdir.join("pyproject.toml").write(config.encode("utf-8")) - subprocess.run(["django", "check"], check=True) + subprocess.run([command, "check"], check=True) @pytest.mark.skipif( os.environ.get("TOX"), reason="Doesn't release the port quickly enough to run multiple times in quick succession with tox.", ) +@pytest.mark.parametrize("command", ["django-admin"]) # If django-admin works, so will django @restore_environ(["DJANGO_SETTINGS_MODULE"]) -def test_runserver(tmpdir): +def test_runserver(command, tmpdir): """Should be able to run the development server for several seconds.""" tmpdir.chdir() - subprocess.run(["django", "startproject", "myproject", "."], check=True) + subprocess.run([command, "startproject", "myproject", "."], check=True) config = '[tool.django]\nsettings_module = "myproject.settings"\n' tmpdir.join("pyproject.toml").write(config.encode("utf-8")) with pytest.raises(subprocess.TimeoutExpired): @@ -119,4 +109,4 @@ def test_runserver(tmpdir): # 2 seems to be OK, but to make it hopefully more reliable # we'll use 3 seconds. Otherwise this might not break even # if the functionality does. - subprocess.run(["django", "runserver"], check=True, timeout=3) + subprocess.run([command, "runserver"], check=True, timeout=3) diff --git a/pyproject.toml b/pyproject.toml index 02ad0ae..3bbb21e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "django-cmd" -version = "2.2" +version = "2.3" description = "Have a django command" authors = [{ name = "Ryan Hiebert", email = "ryan@ryanhiebert.com" }] license = "MIT" @@ -40,7 +40,7 @@ homepage = "https://github.com/ryanhiebert/django-cmd" repository = "https://github.com/ryanhiebert/django-cmd" [project.scripts] -django = "django_cmd:main" +django = "django.core.management:execute_from_command_line" [build-system] requires = ["hatchling"] @@ -52,3 +52,13 @@ dev = ["pytest", "pytest-mock", "coverage", "ruff", "isort"] [tool.coverage.run] branch = true source = "django_cmd" + +# [tool.hatch.build] +# artifacts = ["django_cmd.pth"] + +[tool.hatch.build.hooks.autorun] +dependencies = ["hatch-autorun"] +code = """ +import django_cmd +django_cmd.patch_django() +"""