Phase 4: plugin scaffolding (flask plugin new) with canary tests
Lowers the barrier for sister sites to build their own plugins. Generated output satisfies the framework contract out of the box. CLI command (shopdb/plugins/cli.py): - `flask plugin new <name> --description "..."` generates a plugin skeleton under plugins/<name>/. Validates the name against CONTRIBUTING.md rules (lowercase letters/digits only, no underscores or hyphens, not in the reserved list) and refuses to overwrite existing plugins unless --overwrite is passed. - Output prints the next steps (install, migrate, test). Scaffolder (shopdb/plugins/scaffolder.py): - validate_name: enforces the naming rules - pascal_case: lowercase-to-PascalCase for class names - scaffold_plugin: copies templates with string.Template substitution. Three placeholders: $name, $Name, $description. Files with `model.py` in the path get renamed to <name>.py. Templates (shopdb/plugins/templates/): - manifest.json.tmpl: name, version 0.1.0, description, core_version range >=0.1.0,<1.0.0 (broad enough to survive minor framework bumps) - plugin.py.tmpl: <Name>Plugin class extending BasePlugin with all required hooks implemented (meta from manifest, get_blueprint returning the bp, get_models returning the example model). Includes on_install hook that seeds the AssetType row. - models/__init__.py.tmpl + models/model.py.tmpl: Asset extension table keyed by assetid with one example field. TODO comment marks it as a placeholder. - api/__init__.py.tmpl + api/routes.py.tmpl: Blueprint with list and detail endpoints using the framework's pagination + response helpers. - schemas/__init__.py.tmpl: marshmallow schema stub. - tests/__init__.py.tmpl + tests/test_plugin.py.tmpl: smoke tests asserting plugin loads, get_blueprint returns Blueprint, get_models returns at least one model. - README.md.tmpl: one-pager for plugin authors with common edits and next-step references. Canary tests (tests/test_plugin_scaffold.py): - 14 tests asserting the scaffold output passes contract checks. - Validates name rules (lowercase, reserved, hyphens, digits, etc.) - Verifies all expected files generated, manifest fields present. - Loads the generated plugin via PluginLoader (spec_from_file_location bypasses the real `plugins` package shadowing). - Asserts subclasses BasePlugin, get_blueprint returns Blueprint, get_models returns model with __tablename__. - Module-scoped fixture; cleans up sys.modules + SQLAlchemy metadata on teardown to avoid cross-test contamination. Quickstart docs (docs/PLUGIN-QUICKSTART.md): - 30-minute walkthrough: scaffold -> edit model -> add routes -> install -> verify -> add hooks. Cross-links to PLUGIN-HOOKS.md and the ADRs. Includes common-errors table. Test count: 87 -> 101 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,46 @@ def plugin_info(name: str):
|
||||
click.echo("")
|
||||
|
||||
|
||||
@plugin_cli.command('new')
|
||||
@click.argument('name')
|
||||
@click.option('--description', default='', help='One-sentence plugin description')
|
||||
@click.option('--overwrite', is_flag=True, help='Overwrite existing plugin directory')
|
||||
@with_appcontext
|
||||
def new_plugin(name: str, description: str, overwrite: bool):
|
||||
"""Scaffold a new plugin from the bundled templates.
|
||||
|
||||
Usage: flask plugin new cameras --description "Tracks shop-floor cameras"
|
||||
"""
|
||||
from pathlib import Path
|
||||
from .scaffolder import scaffold_plugin, ScaffoldError
|
||||
|
||||
plugins_dir = Path(current_app.root_path).parent / 'plugins'
|
||||
|
||||
if not description:
|
||||
description = f'{name.capitalize()} plugin (TODO: replace this description)'
|
||||
|
||||
try:
|
||||
target = scaffold_plugin(
|
||||
name=name,
|
||||
description=description,
|
||||
plugins_dir=plugins_dir,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
except ScaffoldError as e:
|
||||
click.echo(click.style(f'Scaffold failed: {e}', fg='red'))
|
||||
raise SystemExit(1)
|
||||
|
||||
click.echo(click.style(f'Created plugin at {target}', fg='green'))
|
||||
click.echo('')
|
||||
click.echo('Next steps:')
|
||||
click.echo(f' 1. Edit plugins/{name}/models/{name}.py with your domain fields')
|
||||
click.echo(f' 2. Edit plugins/{name}/api/routes.py with your endpoints')
|
||||
click.echo(f' 3. Run: flask plugin install {name}')
|
||||
click.echo(f' 4. Run: flask db migrate -m "Add {name} plugin"')
|
||||
click.echo(f' 5. Run: flask db upgrade')
|
||||
click.echo(f' 6. Run: pytest plugins/{name}/tests/')
|
||||
|
||||
|
||||
@plugin_cli.command('migrate')
|
||||
@click.argument('name')
|
||||
@click.option('--revision', default='head', help='Target revision')
|
||||
|
||||
Reference in New Issue
Block a user