nanodjango Documentation

repository·main·Indexed 21 days ago

https://github.com/radiac/nanodjango

nanodjango allows developers to write entire Django applications, including models, views, and admin registrations, within a single Python file. It is designed for rapid prototyping and small-scale services, featuring built-in Django Ninja API support and a conversion tool to transform single-file apps into full, standard Django project structures.

Tokens
24.9K
Snippets
119
Records
142
Agent score
71%

What's inside nanodjango

  1. Use `Resolver` to manage symbol imports in plugins

    main

    When writing a plugin that moves code to a new file, use the Resolver to ensure all necessary imports and dependencies are included.

    1. Initialize a Resolver with the target module name (e.g., '.api').
    2. When you find a symbol to move, use resolver.add(name, references) to register the symbol and its dependencies.
    3. Use resolver.gen_src() to generate the necessary import statements and boilerplate code.
    4. Use converter.write_file() to write the generated header and the collected code body.
    from nanodjango.convert.plugin import Resolver
    
    # Inside a hook implementation
    resolver = Resolver(converter, ".api")
    # ... find symbols ...
    resolver.add(name, references)
    # ... later ...
    converter.write_file(
        converter.app_path / "api.py",
        resolver.gen_src(),
        "\n".join(code_lines)
    )
  2. Define and use Django models

    main

    Models are defined using standard django.db.models. Crucially, they must be defined in the same file as your Django instance and must appear after the app = Django() line.

    To apply migrations, use the nanodjango run command, which automatically creates and applies migrations for your script.

    from django.db import models
    from nanodjango import Django
    
    app = Django()
    
    class CountLog(models.Model):
        timestamp = models.DateTimeField(auto_now_add=True)

    To run the script and apply migrations:

    nanodjango run counter.py
  3. Writing a plugin with `nanodjango.hookimpl`

    main

    Nanodjango allows you to extend its behavior using hooks. Plugins are defined by decorating functions with nanodjango.hookimpl. These hooks allow you to customize the conversion process, integrate third-party packages, or inject custom logic during the Django setup or the code conversion lifecycle.

    from nanodjango import hookimpl
    
    @hookimpl
    def your_hook_name(context):
        # Your plugin logic here
        pass
  4. How the deferred import system works

    main

    In a single-file nanodjango application, you often need to define Django components (like models or views) at the module level. However, Django requires full configuration before these components can be imported.

    nanodjango.defer solves this circular dependency by intercepting import statements within a context manager. It records the intended imports without executing them immediately, allowing you to define your application structure before Django is fully initialized. The actual imports are executed when defer.apply() is called (which happens automatically during Django().__init__).

    from nanodjango import Django
    from nanodjango.defer import defer
    
    app = Django()
    
    # Use the defer context manager to record imports
    with defer:
        from django.db import models
        from django.contrib.auth.models import User
    
    # The imports are applied automatically when app is initialized,
    # or you can call it manually:
    # defer.apply()
  5. Access Django Ninja features via app.ninja

    main

    To avoid import order issues, use the app.ninja attribute to access Django Ninja features (like Schema or File) instead of importing ninja directly. This allows you to use Ninja's types and utilities while maintaining compatibility with nanodjango's single-file configuration pattern.

    class Item(app.ninja.Schema):
        foo: str
        bar: float
    
    @app.api.post('/do_something')
    def do_something(
        request,
        item: Item,
        file: app.ninja.UploadedFile = app.ninja.File(..)
    ):
        ...
  6. Understand Pyodide playground limitations

    main

    The playground runs on Pyodide (Python via WebAssembly in the browser), which imposes the following constraints on package installation:

    • Pure-Python or Pre-compiled Wheels Only: Packages must have a pure-Python wheel available. If a package contains C extensions, it will only work if Pyodide provides a pre-compiled build (e.g., numpy, pandas, pillow, and cryptography are supported). Check the Pyodide packages index for compatibility.
    • No Raw Network Sockets: The browser sandbox prevents raw TCP/UDP connections. Packages requiring network sockets will fail. You must use methods that leverage the browser's fetch API, such as urllib.request (which Pyodide patches) or pyodide.http.
    • No System-level Dependencies: You cannot install packages that rely on external shared libraries or system binaries.
  7. Understand how the Playground database works

    main

    The SQLite database used by your script is stored entirely within your browser's local storage.

    Key characteristics:

    • Privacy: The database is never sent to the server.
    • Isolation: Databases are not shared when you share a script. Every visitor who runs your script starts with their own fresh, empty database instance.
  8. Modify settings using callbacks

    main

    Instead of replacing a setting entirely, you can pass a callable (like a lambda) to modify existing nanodjango default settings. The callable will receive the current value as an argument, and its return value will be used as the new setting. This is ideal for prepending/appending to lists like MIDDLEWARE or INSTALLED_APPS.

    Note: This behavior only applies to settings that already exist in nanodjango's defaults. For new settings, the callable is stored as-is (e.g., for WHITENOISE_ADD_HEADERS_FUNCTION).

    app = Django(
        MIDDLEWARE=lambda m: [MyPreMiddleware] + m + [MyPostMiddleware],
        INSTALLED_APPS=lambda apps: [a for a in apps if "admin" not in a],
    )
  9. Handle view return values after conversion

    main

    During conversion, nanodjango may add an @ensure_http_response decorator to views that lack type hints. This decorator automatically converts str return values into HttpResponse objects at runtime.

    To avoid this decorator and ensure cleaner code in your new Django project, add a type hint for the return value (e.g., -> HttpResponse or a subclass like -> HttpResponseRedirect).

    @app.route("/author/")
    def redirect(request) -> HttpResponseRedirect:
        return HttpResponseRedirect("https://radiac.net/")
  10. Define templates for nanodjango apps

    main

    You can define templates for your application using two methods:

    1. File-based: Place template files in a templates/ directory located next to your script.
    2. Dictionary-based: Assign template content directly to the app.templates dictionary. This uses Django's locmem template loader, allowing templates to be extended, included, and mixed with file-based templates.

    Note on precedence: If a template path is defined both as a file and in the app.templates dictionary, the version in the dictionary takes precedence.

    It is recommended to define dictionary-based templates at the bottom of your script to keep them separate from your logic.

    # Assigning by key
    app.templates["base.html"] = """<!doctype html>..."""
    app.templates["myview/hello.html"] = "{% block content %}Hello{% endblock %}"
    
    # Assigning by dict
    app.templates = {
      "base.html": """<!doctype html>...""",
      "myview/hello.html": """{% extends 'base.html' %}...""",
    }
  11. Run nanodjango using WSGI or ASGI servers

    main

    You can pass the app = Django() instance directly to standard WSGI or ASGI servers. The app object automatically detects if it should use WSGI or ASGI based on whether async views or endpoints are present.

    WSGI (Standard):

    gunicorn -w 4 counter:app

    ASGI (Async):

    uvicorn counter:app

    Explicit Handlers: If you want to override the automatic detection, specify the handler explicitly:

    gunicorn counter:app.wsgi
    uvicorn counter:app.asgi
  12. Package a nanodjango app into a Python package

    main

    To integrate a nanodjango site into a larger project or publish it as a standalone PyPI package, you can wrap your script in a Python package. This allows you to launch the app using the package name (e.g., python -m myproject) instead of running a single file.

    Recommended directory structure:

    myproject/
      __init__.py
      __main__.py
      app.py

    In this setup, app.py contains your nanodjango logic, and __main__.py serves as the entry point.