From 44c074d27a60f46f378b12b79d2f2f16f30b6b21 Mon Sep 17 00:00:00 2001 From: Richard Terry Date: Sun, 21 Apr 2024 08:31:50 +0100 Subject: [PATCH] Add support for calling app.run() within the script --- docs/management.rst | 64 ++++++++++++++++++++++++++++++++++++++++++ examples/scale.py | 7 +++++ nanodjango/app.py | 20 ++++++++++++- nanodjango/commands.py | 8 +++--- 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/docs/management.rst b/docs/management.rst index 9896855..2ffc157 100644 --- a/docs/management.rst +++ b/docs/management.rst @@ -30,3 +30,67 @@ nanodjango uses the filename as the app name - eg: .. code-block:: bash nanodjango counter.py run makemigrations counter + + +.. _run_script: + +Running your script directly +============================ + +You don't need to use the ``nanodjango`` command - you can call ``app.run()`` from the +bottom of your script, eg: + +.. code-block:: python + + from nanodjango import Django + app = Django() + ... + if __name__ == "__main__": + app.run() + +You can then run the script directly to launch the Django development server. This will +also automatically collect any arguments you may have passed on the command line:: + + python hello.py run runserver 0:8004 + + +Running it as a standalone script +--------------------------------- + +You can take it a step further and add a [PEP 723](https://peps.python.org/pep-0723/) +comment to the top to specify ``nanodjango`` as a dependency: + +.. code-block:: python + + # /// script + # dependencies = ["nanodjango"] + # /// + from nanodjango import Django + app = Django() + ... + if __name__ == "__main__": + app.run() + +This will allow you to pass it to ``pipx run``, to run your development server without +installing anything first: + +.. code-block:: bash + + # Create a temporary venv with ``nanodjango`` installed, then run the script + pipx run ./script.py + + # Pass some arguments + pipx run ./script.py -- runserver 0:8000 + + +Running in production +--------------------- + +The commands above are suitable for running the Django development server locally, but +are not appropriate for use in production. + +Instead, you can pass nanodjango's ``app = Django()`` to a WSGI server: + +.. code-block:: bash + + gunicorn -w 4 counter:app diff --git a/examples/scale.py b/examples/scale.py index 2bcd347..8ee3481 100644 --- a/examples/scale.py +++ b/examples/scale.py @@ -12,6 +12,9 @@ cd /path/to/site ./manage.py runserver 0:8000 """ +# /// script +# dependencies = ["nanodjango"] +# /// import os @@ -83,3 +86,7 @@ class Counts(ListView): def something(name): return os.getenv(name) + + +if __name__ == "__main__": + app.run() diff --git a/nanodjango/app.py b/nanodjango/app.py index 7af0d26..b7910b0 100644 --- a/nanodjango/app.py +++ b/nanodjango/app.py @@ -166,13 +166,31 @@ def wrap(model: type[Model]): # Called without arguments, @admin - call wrapped immediately return wrap(model) - def run(self, args: list[str]): + def run(self, args: list[str] | None = None): """ Run a Django management command, passing all arguments Defaults to: runserver 0:8000 """ + # Check if this is being called from click commands or directly + if self.app_name not in sys.modules: + # Hasn't been run through the ``nanodjango`` command + if ( + "__main__" not in sys.modules + or getattr(sys.modules["__main__"], "app") != self + ): + # Doesn't look like it was run directly either + raise UsageError("App module not initialised") + + # Run directly, so register app module so Django won't try to load it again + sys.modules[self.app_name] = sys.modules["__main__"] + + # Be helpful and check sys.argv for args. This will almost certainly be because + # it's running directly. + if args is None: + args = sys.argv[1:] + self._prepare() from django.core.management import execute_from_command_line diff --git a/nanodjango/commands.py b/nanodjango/commands.py index 32f10e9..1917e97 100644 --- a/nanodjango/commands.py +++ b/nanodjango/commands.py @@ -40,11 +40,11 @@ def run(ctx, args: tuple[str]): ) @click.pass_context def convert(ctx, path: click.Path, name: str, can_delete: bool = False): - path: Path = Path(str(path)).resolve() - if can_delete and path.exists(): - shutil.rmtree(str(path)) + target_path: Path = Path(str(path)).resolve() + if can_delete and target_path.exists(): + shutil.rmtree(str(target_path)) - ctx.obj["app"].convert(path, name) + ctx.obj["app"].convert(target_path, name) def invoke():