Normalize the import namespace — app/ is currently imported under two names #371
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web#371
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Prerequisite for the
uv/pyproject.tomlmigration (#343). Split out becauseit is a defect fix, not a build change, and it should be reviewable on its
own.
Pure-Python change. Touches no Dockerfile, no compose file, no migration.
The defect
app/is a package (app/__init__.pyexists, empty), but it is also onsys.pathas a root — so its submodules get imported under two different namesin the same process:
Verified:
Same file on disk, two entries in
sys.modules, two sets of module-level state.How both roots end up on the path
compose.prod.yml:13setsPYTHONPATH: /whileWORKDIRis/appapp/is auto-added; PyCharm's default "add content roots to PYTHONPATH" adds the repo root.pylintrc:13—init-hook='import sys; sys.path[0:0] = [".", "app"]'That
.pylintrcline is why this has never been reported: lint is configured tomake both forms resolve.
Live consequences
app/db/__init__.py:24createsengineat modulescope. Nine modules do
from db import engine;app/jobs/sync_team_records.py:4does
from app.db import engine. That job runs against its own engine and itsown connection pool.
Config.get_config()runs twice — twoload_dotenv()calls, two fullos.environreads, two unrelatedConfigdataclasses.app/models/model_helpers.py:4usesfrom app.espn_nfl import ...;create_picks.py,update_game.pyandsync_team_records.pyusefrom espn_nfl import ....isinstanceacross the boundary silently returnsFalse. Not exercisedtoday, but it is a live trap for anyone adding a type check.
app/main.py:190runsuvicorn.run("app.main:app", reload=True)while the initial script import wasbare
main, so the dev loop imports the tree under both names on every reload.Which name wins:
app.-qualifiedThe package form is already the de-facto canonical one — it is the bare form
that is the intruder:
alembic/env.py:10—import app.models(migrations already depend on it)app/main.py:190—uvicorn.run("app.main:app", ...)It is also the only form that survives becoming an installed package in #343.
Direction of travel: 51 bare imports gain an
app.prefix. The 8 alreadyusing
app.stay as they are. Intra-package relative imports (from .base import ...,from .model_helpers import ...) are correct and unchanged.Scope
1. Prefix the 51 first-party imports
app/dependency.pyapp/jobs/award_notify_discord.pyapp/jobs/award_update_all.pyapp/jobs/create_picks.pyapp/jobs/nag_players.pyapp/jobs/scheduler.pyapp/jobs/sync_team_records.pyapp/jobs/update_all_scores.pyapp/jobs/update_game.pyapp/jobs/update_player_records.pyapp/main.pyapp/models/award_helpers.pyapp/routers/admin.pyapp/routers/auth.pyapp/routers/mail.pyAffected top-level names:
config,db,dependency,espn_nfl,jobs,models,routers.2. Make pylint enforce it
.pylintrc:13:This is the regression guard as well as the cleanup: with
appoff the path,any future bare first-party import fails lint as
import-errorinstead ofsilently creating a duplicate module.
3. Make template/static paths independent of the working directory
Three sites resolve relative to CWD, which is why the app only boots from
app/and raisesRuntimeError: Directory 'static' does not existfromanywhere else:
app/main.pyStaticFiles(directory="static")app/main.pyJinja2Templates(directory="templates")app/routers/admin.pyJinja2Templates(directory="templates")Use the pattern already established at
app/routers/mail.py:23:Not strictly required to fix the double import, but included here because the
container layout work in #370 depends on the app being launchable from a
directory other than
app/, and because CWD-dependence is the same class offragility.
Explicitly out of scope
PYTHONPATH: /andWORKDIR /appcanstay; once no code uses the bare form, the redundant path entry is inert. It
gets removed in #343 when the project becomes an installed package.
pyproject.toml. That is #343.app/itself — no files move, no__init__.pycontents change.
Acceptance criteria
app.prefix.pylintrcno longer injectsappontosys.pathpylint $(git ls-files '*.py')reports 10.00/10/,/login,/pingfrom the repo root as wellas from
app/alembic currentstill resolves (env.py'simport app.modelsunaffected)sync_team_recordsruns against the shared engineVerification
The duplication check, which must flip to all-
True:Then a grep that must return nothing:
Repo has no test suite, so verification is lint plus a manual boot and an
exercise of the scheduler jobs.