Skip to content

Replace per-script Django bootstrap with a cli_dispatch stub#4144

Open
lunkwill42 wants to merge 3 commits into
masterfrom
refactor/cli-dispatch-stub
Open

Replace per-script Django bootstrap with a cli_dispatch stub#4144
lunkwill42 wants to merge 3 commits into
masterfrom
refactor/cli-dispatch-stub

Conversation

@lunkwill42

Copy link
Copy Markdown
Member

Scope and purpose

NAV predates Django; its command line tools are standalone scripts that use Django's ORM without being Django management commands (effectively, NAV tries to insulate end users from Django). Until now, each of these ~30 scripts called bootstrap_django(__file__) as a bare statement sandwiched between two import blocks, so that Django would be configured before the rest of the script's imports touched the ORM. That's a hack: it blocks any future import-sorting enforcement, and reads as one.

This PR introduces nav.cli_dispatch, a single console-script entry point that every Django-dependent command in pyproject.toml now points to instead of its own module. It bootstraps Django, then looks up and calls the real implementation via a new [project.entry-points."nav.cli_commands"] table, based on which command name the process was invoked as (available via sys.argv[0], which pip/setuptools-generated console scripts always set to the installed command name). Because bootstrapping now happens before the implementing module is ever imported, none of these ~30 scripts need to work around Django not being configured yet - their own top-level imports go back to being an ordinary, freely-sortable block.

An alternative attempt at this used a decorator (needs_django) applied to each script's own main() instead. That required real per-file workarounds wherever Django-dependent names were used outside main() itself: global buckets for names shared across several functions, None-sentinel default arguments for values that used to be computed from Django models at function-definition time, from __future__ import annotations plus TYPE_CHECKING guards for type hints, and relocating navoidverify's platform-specific epoll reactor install. None of that is needed here, since the whole module is only ever imported after Django is already configured.

No user-facing behavior change. To observe the effect: any of the affected commands (e.g. navdf, navuser list, navoidverify) should work exactly as before when run by their installed name; pytest tests/integration/bin_test.py exercises all of them plus a couple of edge cases (naventity, navuser, smsd) that previously invoked their .py files directly instead of via the installed command - see the second commit for why that had to change too.

This could be a first stepping stone to finally introduce ruff-backed automatic import sorting across this codebase.

Contributor Checklist

  • Added a changelog fragment for towncrier (purely internal restructuring, no user-facing change - will add the nonews label instead)
  • Added/amended tests for new/changed code
  • Added/changed documentation (bootstrap_django() itself is unchanged and still correctly used as-is by doc/conf.py and doc/exts/alerttypes.py, which aren't part of this migration)
  • Linted/formatted the code with ruff
  • Wrote the commit message so that the first line continues the sentence "If applied, this commit will ..."
  • Based this pull request on the correct upstream branch (master)
  • Created new issues if this PR does not fix the issue completely (no further work is needed)
  • Described how to interact with NAV in order for a reviewer to observe the effects of this change first-hand
  • If this results in changes in the UI: added screenshots (no UI change)
  • Added the boilerplate header to the new nav/cli_dispatch.py file

NAV predates Django, and many of its command line tools are standalone
scripts that use Django's ORM without being management commands, so
they need Django bootstrapped before anything of theirs that touches
the ORM gets imported - the same problem manage.py solves for
management commands. Until now this was done with a bare
bootstrap_django(__file__) call sandwiched between two import blocks
in each script, which blocks any future import-sorting enforcement.

nav.cli_dispatch.main() bootstraps Django, then looks up and calls the
real implementation via the new "nav.cli_commands" entry-point group,
based on which command name the process was invoked as (available via
sys.argv[0], which pip/setuptools-generated console scripts always set
to the installed command name). Every Django-dependent command's
[project.scripts] entry now points here instead of at its own module,
with the real target moved to [project.entry-points."nav.cli_commands"]
so the dispatcher can still find it - it can't read this back from
[project.scripts] itself, since that now just says
"nav.cli_dispatch:main" for these commands.

Because bootstrapping happens before the implementing module is ever
imported, that module's own top-level imports never need to work
around Django not being configured yet - unlike a decorator applied to
that module's own main(), which still requires anything the module
uses at definition time (default argument values, decorators, class
bodies) to already be Django-safe.

The bin scripts themselves aren't touched by this commit yet: they
still call bootstrap_django() on their own, which is now a redundant
but harmless no-op (bootstrap_django() guards against being run more
than once).
Several adaptations were needed for the test suite to correctly
exercise scripts now routed through nav.cli_dispatch:

- _nav_scripts_map() resolved a dispatched script's testarg-scanning
  target from [project.scripts], which now just says
  "nav.cli_dispatch:main" for these commands. It now prefers
  [project.entry-points."nav.cli_commands"], where the real
  implementing module lives.
- test_script_runs spawned each script by inheriting the pytest
  process's full environment. Under a tox testenv, DJANGO_SETTINGS_MODULE
  is already exported there, so this never actually exercised
  bootstrap_django()'s own "var absent, set the default" branch.
- naventity, navuser and smsd are dispatched via nav.cli_dispatch, so
  they must be invoked by their installed command name for dispatch to
  find the right target - a few tests invoked them by raw file path
  instead, which bypasses cli_dispatch (and installed-command
  discovery) entirely.
All of these are now dispatched via nav.cli_dispatch (see
pyproject.toml's "nav.cli_commands" entry points), which bootstraps
Django before any of these modules is ever imported. Calling
bootstrap_django() here too was harmless (it's a no-op past the first
call) but pointless, and kept these scripts' own imports from being
ordinary, freely-sortable import blocks.
@lunkwill42 lunkwill42 self-assigned this Jul 18, 2026
@lunkwill42
lunkwill42 requested a review from a team July 18, 2026 07:31
@lunkwill42 lunkwill42 added the nonews No news fragment is necessary for this PR (e.g. refactoring, cleanups, workflow/development changes) label Jul 18, 2026
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.32%. Comparing base (3827cf7) to head (8f247d7).

Files with missing lines Patch % Lines
python/nav/cli_dispatch.py 83.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4144      +/-   ##
==========================================
- Coverage   65.37%   65.32%   -0.06%     
==========================================
  Files         629      630       +1     
  Lines       47206    47158      -48     
==========================================
- Hits        30863    30808      -55     
- Misses      16343    16350       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

Test results

    5 files      5 suites   7m 56s ⏱️
3 355 tests 3 355 ✅ 0 💤 0 ❌
6 634 runs  6 634 ✅ 0 💤 0 ❌

Results for commit 8f247d7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nonews No news fragment is necessary for this PR (e.g. refactoring, cleanups, workflow/development changes)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant