Repository navigation
Fix db:install review findings and rename from database:install - #324
Conversation
- Compare an already-imported name by its module path (or the loaded object for re-exports) and stop with manual steps when it is bound to a different module, instead of registering the wrong class. - Treat attribute references (orm.DatabaseProvider) as registered. - Edit a positional providers list instead of appending providers=. - Print next steps after install: uv add hint when SQLAlchemy is missing, DB_* entries from the installer's database section, the migrations directory and migration commands. - Rename database:install to db:install (DbInstallCommand). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-tmgbedu
left a comment
There was a problem hiding this comment.
Verdict: approve (no blockers, posted as COMMENT because the author account is the reviewer account)
Follow-up to #323. I read the full diff, checked each item from the #323 review, ran the bug scenarios against main's register_provider, and ran the command on scratch apps.
Blockers
None.
Item-by-item
| # | Item | Status | Evidence |
|---|---|---|---|
| 3 | Import dedupe by path / identity | Fixed | On main, from fastapi_startkit.masoniteorm.config import CacheConfig, from other import Something as CacheProvider and from .cache import CacheConfig are all kept, and the provider is registered against the wrong class. On this branch each raises RegistrationError, and db:install exits 1 with the file byte-identical (test_does_not_register_when_database_config_is_imported_from_elsewhere, test_conflicting_import_prints_manual_steps_and_leaves_bootstrap). |
| 4 | Attribute refs count as registered | Fixed | On main, [(cache.CacheProvider, X)] and [fastapi_startkit.cache.CacheProvider] get a second entry. Now unchanged. |
| 5 | Positional providers / *args / RegistrationError |
Fixed | On main, Application('.', None, [A]) became ..., [A], providers=[...] (TypeError at boot). It is now edited in place. A non-list positional value and *ARGS stop with a reason. The only caller of register_provider is InstallCommand.register_provider, which catches RegistrationError. The installer (installer/) does not call it. |
| U1 | uv add hint |
Fixed | Real run on a fresh fastapi-startkit init --database none app with no SQLAlchemy printed uv add "fastapi-startkit[database,postgres]". The extras database/sqlite/postgres/mysql all exist in pyproject.toml. |
| U2 | Next steps | Accurate | I followed them on a scratch copy of example/fastapi-app (sqlite): mkdir -p databases/migrations, the DB_* lines into .env, db:make:migration create_users_table, then db:migrate, which migrated OK. The uv run python artisan ... form matches init's own output. |
| 6 | Rename | Complete | git grep finds no database:install / DatabaseInstallCommand / database_install_command references. The ADR 041 title and text and the index row are updated. |
Non-blocking notes
**kwargsis not treated like*args(inline).Application(**KW)getsproviders=[...]appended. That is valid syntax, but it raisesTypeErrorat boot ifKWalready hasproviders.- A bootstrap with a syntax error gives a raw traceback (inline, carried over from #323).
ast.parseraisesSyntaxError, which is not converted into aRegistrationErrorwith manual steps. - Only top-level
importstatements are checked for conflicts (inline, carried over from #323). Aclass DatabaseConfig:orDatabaseConfig = ...at module level, or an import insideif TYPE_CHECKING:ortry:, is not seen. In the class case the new import is inserted and then silently shadowed by the class. is_registeredmatches anywhere inside the list (inline).[(A, settings.DatabaseProvider)](config position) counts as registered. This is contrived and only mentioned for completeness.Applicationimported under an alias (from fastapi_startkit import Application as App) stops with "Could not find an Application(...) call.". That is safe, but a hint about the alias would help.same_objectonly resolves modules that are already insys.modules. In a realartisanrun the bootstrap is imported first, so a submodule import such asfrom fastapi_startkit.masoniteorm.<submodule> import DatabaseProviderresolves correctly. Calling the pure function standalone would report a false conflict. Behaviour is fine; just be aware.- Nit:
test_db_install_command.pyhas a stray blank line between twofastapi_startkitimports (lines 8–10).
Break-it attempts (all produce valid, compiling output or a clean stop)
- These were all correct: a multi-line list with a trailing
# commentand no comma, a commented-out item after the last one,]on the last item's line, CRLF line endings, non-ASCII text before the insertion point, a multi-line parenthesised import as the last import, an import with a trailing# noqa, semicolon imports,from __future__only,[*BASE, A],Application('.', None)(getsproviders=), and a secondApplication(...)inside a function (the module-level call is edited). - A clean stop for
providers=P(variable),Application(*ARGS)and the aliasedApplication.
Real runs (scratch copies, run twice)
example/fastapi-app: the first run created the config, registered the provider and exited 0. The second run said "already exists, skipped" and "already registered", exited 0, and both files kept identical md5s.application/does not exist in this repo, so I used a freshfastapi-startkit init starter --database noneapp (bootstrap withconfig=AppConfig), with--driver postgresand SQLAlchemy missing. The first run printed theuv addhint. The second run failed atartisanimport withModuleNotFoundError: sqlalchemy, which is exactly what the hint warns about. After installing the extra deps, the rerun was idempotent (md5 unchanged).
Results
uv run pytest --ignore=tests/masoniteorm/postgres --cov --cov-report=term-missing: 3465 passed, 10 skipped. Total coverage 92.10%.install_command.pyanddb_install_command.pyare at 100%.ruff checkpasses andruff format --checkis clean on the changed files.basedpyrightonconsole/and the tests: 0 errors. The 4reportMissingTypeArgumentwarnings are in files this PR does not touch.
|
|
||
|
|
||
| def providers_argument(call: ast.Call) -> ast.List | None: | ||
| if any(isinstance(argument, ast.Starred) for argument in call.args): |
There was a problem hiding this comment.
Non-blocking: **kwargs isn't covered. Application(**KW) falls through to providers is None and gets providers=[...] appended. That raises TypeError: got multiple values for keyword argument 'providers' at boot when KW contains it. To be consistent with *args, consider also stopping on any(keyword.arg is None for keyword in call.keywords) when there is no explicit providers=.
|
|
||
| def register_provider(source: str, provider: str, entry: str, imports: Mapping[str, str]) -> str | None: | ||
| def register_provider(source: str, provider: str, entry: str, imports: Mapping[str, str]) -> str: | ||
| tree = ast.parse(source) |
There was a problem hiding this comment.
Non-blocking (carried over from #323): a bootstrap with a syntax error raises SyntaxError here and the user gets a traceback instead of the reason and manual steps. Wrapping it as RegistrationError(f"{BOOTSTRAP_PATH} has a syntax error on line {error.lineno}.") would fit the new error path.
| @@ -184,8 +203,12 @@ def append_item( | |||
|
|
|||
| def import_inserts(tree: ast.Module, lines: list[str], imports: Mapping[str, str]) -> list[Insert]: | |||
| existing = [node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))] | |||
There was a problem hiding this comment.
Non-blocking: only top-level import nodes are inspected. A module-level class DatabaseConfig: / DatabaseConfig = ..., or an import under if TYPE_CHECKING: / try:, is not seen. Reproduced: with class DatabaseConfig: pass in the bootstrap, the new from config.database import DatabaseConfig is inserted above it and then silently shadowed. This is rare in a bootstrap file, so it's fine to leave, but it is a gap in the "name bound to something else" check.
| return any( | ||
| (isinstance(node, ast.Name) and node.id == provider) | ||
| or (isinstance(node, ast.Attribute) and node.attr == provider) | ||
| for node in ast.walk(providers) |
There was a problem hiding this comment.
Nit: ast.walk matches the name anywhere in the list, including the config position of a tuple: [(A, settings.DatabaseProvider)] counts as registered. Checking only each element (or elt.elts[0] for tuples) would be stricter. This is contrived, so it's optional.
|
|
||
| from fastapi_startkit.console.database_install_command import DatabaseInstallCommand | ||
| from fastapi_startkit.console import db_install_command | ||
|
|
There was a problem hiding this comment.
Nit: stray blank line splitting the fastapi_startkit import group.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Follow-up to #323 (merged). Fixes the findings from the review and renames the command to match the
db:*convention. ADR 041 and its index row are updated in place.Bugs
fastapi_startkit.masoniteorm.config.database.DatabaseConfig, a relative import, or an alias, the command stops. It printsDatabaseConfig is already imported as <path>, not config.database.DatabaseConfig.plus the manual steps, and exits 1 without touching the file. Aliasing was considered and rejected in the ADR.orm.DatabaseProviderandfastapi_startkit.masoniteorm.DatabaseProvidernow count as already registered.providers.Application(base, env, [...])now has the entry inserted into that list. A positional or keyword value that is not a list literal, or a call with*args, stops with a clear reason.register_providernow raisesRegistrationError(reason)instead of returningNone, and the command prints that reason before the manual steps.Usability
uv add "fastapi-startkit[database,<driver>]"whenfind_spec("sqlalchemy")finds nothing, theDB_*.enventries taken from the installer'sdefine_databasesection (the same data asinit --database),mkdir -p databases/migrationswhen the directory is missing (db:make:migrationdoes not create it), and thedb:make:migration/db:migratecommands.Rename
database:installis nowdb:install. The class isDbInstallCommandinconsole/db_install_command.py, and the tests,AppProviderregistration, ADR 041 title/text anddocs/adr/AGENTS.mdrow are updated to match. No other docs or installer code referenced the old name.Tests
Each fix has a regression test. I confirmed that bugs 3–5 reproduce with the
register_provideronmain.uv run pytest --ignore=tests/masoniteorm/postgres --cov: 3464 passed, 10 skipped, total coverage 92.09%.install_command.pyanddb_install_command.pyare at 100%.ruff checkandruff format --checkpass.basedpyrighton the changed sources and tests: 0 errors, no new warnings.🤖 Generated with Claude Code