Skip to content

Commit

Permalink
Add support for calling app.run() within the script
Browse files Browse the repository at this point in the history
  • Loading branch information
radiac committed Apr 21, 2024
1 parent faa3aec commit 44c074d
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 5 deletions.
64 changes: 64 additions & 0 deletions docs/management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions examples/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
cd /path/to/site
./manage.py runserver 0:8000
"""
# /// script
# dependencies = ["nanodjango"]
# ///

import os

Expand Down Expand Up @@ -83,3 +86,7 @@ class Counts(ListView):

def something(name):
return os.getenv(name)


if __name__ == "__main__":
app.run()
20 changes: 19 additions & 1 deletion nanodjango/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions nanodjango/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down

0 comments on commit 44c074d

Please sign in to comment.