diff --git a/README.md b/README.md index dbb4a61..6afc311 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,13 @@ def count(request): Save that as ``counter.py``, then set up your database and run it locally with: ```sh -nanodjango run counter.py makemigrations counter -nanodjango run counter.py migrate -nanodjango run counter.py createsuperuser -nanodjango run counter.py +nanodjango start counter.py ``` It will create your database in a ``db.sqlite3`` file next to your ``counter.py``, with -the appropriate migrations in ``migrations/``. +the appropriate migrations in ``migrations/``. Alternatively you could run each of these +commands manually with the ``run`` command, eg +``nanodjango run counter.py runserver 0:8000`` Run it in production using WSGI: @@ -67,7 +66,7 @@ gunicorn -w 4 counter:app or automatically convert it to a full Django project: ```sh -nanodjango counter.py convert /path/to/project --name=myproject +nanodjango convert counter.py /path/to/project --name=myproject ``` and with a [couple of extra diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f41579..33cb7e6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,19 @@ Changelog ========= +0.6.0 - 2024-05-17 +------------------ + +Feature: + +* Add ``start`` command to create and initialise the database + +Thanks to: + +* Chris Beaven (SmileyChris) for suggesting a lower effort start (#4) +* Lincoln Loop for supporting this release + + 0.5.0 - 2024-05-14 ------------------ diff --git a/docs/get_started.rst b/docs/get_started.rst index b9cd728..6d7e60c 100644 --- a/docs/get_started.rst +++ b/docs/get_started.rst @@ -34,10 +34,16 @@ Create a new file, ``counter.py`` with the following: CountLog.objects.create() return f"

Number of page loads: {CountLog.objects.count()}

" -Now create the migrations, apply them, and run your project: +Now use the ``start`` command to create the migrations, apply them, and run your +project: .. code-block:: bash + nanodjango start counter.py + +or you could run each step manually: + +.. code-block:: bash nanodjango run counter.py makemigrations counter nanodjango run counter.py migrate nanodjango run counter.py createsuperuser diff --git a/nanodjango/__init__.py b/nanodjango/__init__.py index ec23954..4ef7708 100644 --- a/nanodjango/__init__.py +++ b/nanodjango/__init__.py @@ -1,4 +1,4 @@ from .app import Django # noqa -__version__ = "0.5.0" +__version__ = "0.6.0" diff --git a/nanodjango/app.py b/nanodjango/app.py index 2e1b89b..bd09b2a 100644 --- a/nanodjango/app.py +++ b/nanodjango/app.py @@ -10,6 +10,8 @@ from django import setup from django import urls as django_urls from django.contrib import admin +from django.contrib.auth import get_user_model +from django.db.models import Model from django.views import View from . import app_meta @@ -21,7 +23,12 @@ if TYPE_CHECKING: from pathlib import Path - from django.db.models import Model + +def exec_manage(*args): + from django.core.management import execute_from_command_line + + args = ["nanodjango"] + list(args) + execute_from_command_line(args) class Django: @@ -110,24 +117,6 @@ def _config(self, _settings): # Ready for Django's standard setup setup() - def _prepare(self): - """ - Perform any final setup for this project after it has been imported: - - * register the admin site - """ - admin_url = self.settings.ADMIN_URL - if admin_url or self.has_admin: - if admin_url is None: - admin_url = "admin/" - if not isinstance(admin_url, str) or not admin_url.endswith("/"): - raise ConfigurationError( - "settings.ADMIN_URL must be a string path ending in /" - ) - urlpatterns.append( - django_urls.path(admin_url.removeprefix("/"), admin.site.urls) - ) - def route(self, pattern: str, *, re=False, include=None): """ Decorator to add a view to the urls @@ -219,13 +208,14 @@ def wrap(model: type[Model]): # Called without arguments, @admin - call wrapped immediately return wrap(model) - def run(self, args: list[str] | tuple[str] | None = None): + def _prepare(self): """ - Run a Django management command, passing all arguments + Perform any final setup for this project after it has been imported: - Defaults to: - runserver 0:8000 + * detect if it has been run directly; if so, register it as an app + * register the admin site """ + # 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 @@ -239,18 +229,58 @@ def run(self, args: list[str] | tuple[str] | None = None): # Run directly, so register app module so Django won't try to load it again sys.modules[self.app_name] = sys.modules["__main__"] + # Register the admin site + admin_url = self.settings.ADMIN_URL + if admin_url or self.has_admin: + if admin_url is None: + admin_url = "admin/" + if not isinstance(admin_url, str) or not admin_url.endswith("/"): + raise ConfigurationError( + "settings.ADMIN_URL must be a string path ending in /" + ) + urlpatterns.append( + django_urls.path(admin_url.removeprefix("/"), admin.site.urls) + ) + + def run(self, args: list[str] | tuple[str] | None = None): + """ + Run a Django management command, passing all arguments + + Defaults to: + runserver 0:8000 + """ # 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 + if args: + exec_manage(*args) + else: + exec_manage("runserver", "0:8000") + + def start(self, host: str | None = None): + """ + Perform app setup commands and run the server + """ + # Be helpful and check sys.argv for the host + if host is None: + print(sys.argv) + if len(sys.argv) > 2: + raise UsageError("Usage: start [HOST]") + elif len(sys.argv) == 2: + host = sys.argv[1] + if not host: + host = "0:8000" - if not args: - args = ["runserver", "0:8000"] - args = ["nanodjango"] + list(args) - execute_from_command_line(args) + self._prepare() + exec_manage("makemigrations", self.app_name) + exec_manage("migrate") + User = get_user_model() + if User.objects.count() == 0: + exec_manage("createsuperuser") + exec_manage("runserver", host) def convert(self, path: Path, name: str): from .convert import Converter diff --git a/nanodjango/commands.py b/nanodjango/commands.py index 4db6578..5301fb0 100644 --- a/nanodjango/commands.py +++ b/nanodjango/commands.py @@ -68,9 +68,31 @@ def cli(): @click.argument("app", type=str, required=True, callback=load_app) @click.argument("args", type=str, required=False, nargs=-1) def run(app: Django, args: tuple[str]): + """ + Run a management command. + + If no command is specified, it will run runserver 0:8000 + """ app.run(args) +@cli.command() +@click.argument("app", type=str, required=True, callback=load_app) +@click.argument("host", type=str, required=False, default="") +def start(app: Django, host: str): + """ + Start the app on the specified IP and port + + This will perform a series of setup commands: + + makemigrations + migrate + createsuperuser + runserver HOST + """ + app.start(host) + + @cli.command() @click.argument("app", type=str, required=True, callback=load_app) @click.argument("path", type=click.Path(), required=True)