Skip to content

Comments

Add early Django configuration#93

Open
SmileyChris wants to merge 2 commits intoradiac:mainfrom
SmileyChris:early-config
Open

Add early Django configuration#93
SmileyChris wants to merge 2 commits intoradiac:mainfrom
SmileyChris:early-config

Conversation

@SmileyChris
Copy link
Contributor

Summary

This PR implements early Django configuration, addressing several long-standing issues around import order and PEP8 compliance (#34, #43, #85).

The Problem

Previously, Django wasn't configured until app = Django() was called. This meant:

  • Django imports that required configuration (like from django.contrib.auth.models import User) had to come after app = Django()
  • This violated PEP8 import ordering conventions
  • Users had to use defer to work around import order issues
  • IDE type checkers (mypy, pyright) struggled with the deferred imports

The Solution

Django is now configured during from nanodjango import Django via a two-phase approach:

Phase 1: Early extraction (AST parsing)

  • When you from nanodjango import Django, the source file is parsed via AST
  • Simple module-level settings are extracted and evaluated
  • Django is configured with these settings, allowing subsequent imports to work

Phase 2: Late update (at Django() instantiation)

  • When app = Django() is called, the module has fully executed
  • Complex settings (conditionals, function calls) are grabbed from the module namespace
  • Django settings are updated with any values that couldn't be extracted earlier

What This Enables

# PEP8-compliant import order now works!
from nanodjango import Django  # isort: skip
from django.db import models
from django.contrib.auth.models import User

# Module-level settings
DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]

# Complex/conditional settings work too
if os.environ.get("PRODUCTION"):
    ALLOWED_HOSTS = ["example.com"]
else:
    ALLOWED_HOSTS = ["*"]

app = Django()

class MyModel(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)

Key Implementation Details

  1. Lazy loading via __getattr__ (__init__.py)

    • The Django class is only imported when first accessed
    • This ensures early config runs when the user's script imports Django, not when nanodjango is first loaded (important for CLI mode)
  2. New early.py module

    • EarlyConfigurator class handles AST parsing and Django setup
    • Extracts simple assignments, annotated assignments, and augmented assignments
    • Injects BASE_DIR, Path, and os into the evaluation context
  3. CLI mode support (commands.py)

    • Deferred Django import until after user script is loaded
    • Works with both python script.py and nanodjango run script.py
  4. Late settings update (app.py)

    • Scans module namespace for uppercase variables
    • Applies any settings that changed or couldn't be extracted earlier

defer is now rarely needed

With early configuration working in all modes, nanodjango.defer is now only needed for unusual third-party packages that perform initialization at import time in ways that conflict with early configuration.

Test plan

  • All 80 existing tests pass
  • Direct mode (python script.py) works with User import
  • CLI mode (nanodjango run script.py) works with User import
  • Conditional settings work (tested with PRODUCTION env var)
  • Computed settings work (function call results)
  • Module-level settings override defaults
  • Constructor settings override module-level settings

Documentation

Updated:

  • docs/settings.rst - New module-level settings section with "How it works" explanation
  • docs/defer.rst - Note that defer is rarely needed now
  • docs/tutorial.rst - Updated model definition guidance
  • docs/troubleshooting.rst - Updated "Settings not configured" section
  • docs/changelog.rst - Added 0.14.0 entry

Closes #34, #43, #85

Configure Django during `from nanodjango import Django` instead of waiting
for `app = Django()`. This enables PEP8-compliant import ordering and
module-level settings.

Key changes:
- Add early.py for AST-based settings extraction and early Django setup
- Use lazy loading via __getattr__ to support both direct and CLI modes
- Extract simple settings early, apply complex settings at Django() time
- Update commands.py to defer Django import for CLI compatibility
- Add MIGRATION_MODULES to early config

This allows patterns like:
    from nanodjango import Django
    from django.contrib.auth.models import User  # Now works!

And module-level settings:
    DEBUG = False
    if os.environ.get("PROD"):
        ALLOWED_HOSTS = ["example.com"]

Closes radiac#34, radiac#43, radiac#85
- Skip injected settings when collecting unused definitions
- Update requires-python to >=3.10 (Django 5.2 requirement)
- Add test dependencies to pyproject.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

How to configure Auth, compains about INSTALLED_APPS

1 participant