Enforce plugin contract purity: single import surface via shopdb.api
Plugins were reaching into internal core paths (shopdb.core.models.*, shopdb.extensions, shopdb.utils.*), coupling them to core's file layout and violating the ADR-001 contract. Consolidate onto one versioned surface. - shopdb.api: expand from 2 helpers to the full plugin import surface - db, cache; BaseModel, AuditMixin; core models (Asset, AssetType, AssetStatus, Vendor, Model, Communication, CommunicationType, Location, Setting, AuditLog, Application, AppVersion, OperatingSystem); response + pagination helpers; employee_connection. Documented in PLUGIN-HOOKS.md. - Migrate all 22 plugin source files to import only from shopdb.api (plus shopdb.plugins.base for the ABC). - Drop the printers plugin's legacy MachineType dependency: remove _ensure_legacy_machine_types and the seed_supplies machinetypeid lookup (Model.machinetypeid is nullable; printers carry type via PrinterType). - Guard test test_plugins_only_import_contract_surface scans plugin source and fails on any core import outside shopdb.api / shopdb.plugins.base. - Scaffold templates updated so generated plugins are contract-pure. - Bump __contract_version__ 0.2.0 -> 0.3.0 (additive surface expansion; manifests pin <1.0.0 so they still satisfy). 145 tests pass, naming/style green, app factory boots all 6 plugins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ plugin's plugin.py / manifest.json.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -143,3 +144,38 @@ def test_baseplugin_does_not_have_event_handlers_hook():
|
||||
def test_baseplugin_has_collector_schema_hook():
|
||||
"""The collector schema hook is on the contract surface."""
|
||||
assert hasattr(BasePlugin, 'get_collector_schema')
|
||||
|
||||
|
||||
# Imports a plugin may make from the core. shopdb.api is the contract surface;
|
||||
# shopdb.plugins.base is the plugin ABC. Anything else (shopdb.core.*,
|
||||
# shopdb.extensions, shopdb.utils.*) is a contract violation per ADR-001.
|
||||
ALLOWED_CORE_IMPORTS = ('shopdb.api', 'shopdb.plugins.base')
|
||||
|
||||
_PLUGIN_IMPORT_RE = re.compile(
|
||||
r'^\s*(?:from (shopdb[\w.]*) import|import (shopdb[\w.]*))', re.MULTILINE
|
||||
)
|
||||
|
||||
|
||||
def _plugin_source_files():
|
||||
root = Path(__file__).resolve().parent.parent / 'plugins'
|
||||
return [p for p in root.rglob('*.py') if '__pycache__' not in p.parts]
|
||||
|
||||
|
||||
def test_plugins_only_import_contract_surface():
|
||||
"""Plugins must import core code only via shopdb.api / shopdb.plugins.base."""
|
||||
violations = []
|
||||
for path in _plugin_source_files():
|
||||
text = path.read_text()
|
||||
for match in _PLUGIN_IMPORT_RE.finditer(text):
|
||||
module = match.group(1) or match.group(2)
|
||||
if not module.startswith('shopdb'):
|
||||
continue
|
||||
if any(module == a or module.startswith(a + '.')
|
||||
for a in ALLOWED_CORE_IMPORTS):
|
||||
continue
|
||||
line = text[:match.start()].count('\n') + 1
|
||||
violations.append(f'{path.name}:{line} imports {module}')
|
||||
assert not violations, (
|
||||
'Plugins must import core only via shopdb.api or shopdb.plugins.base. '
|
||||
'Violations:\n' + '\n'.join(violations)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user