Skip to content

feat(printer-models): PrinterModel Protocol + ModelRegistry for plugin discovery#48

Merged
strausmann merged 3 commits into
mainfrom
feat/printer-model-plugin
May 11, 2026
Merged

feat(printer-models): PrinterModel Protocol + ModelRegistry for plugin discovery#48
strausmann merged 3 commits into
mainfrom
feat/printer-model-plugin

Conversation

@strausmann
Copy link
Copy Markdown
Owner

Summary

Phase 2 Task 2.7 from the plan. Establishes the plugin contract every Brother (and future) printer-model series must implement, plus a class-level registry that resolves a discovered printer to its driver via PJL or SNMP fingerprint.

This PR adds the shared infrastructure ONLY — no PT or QL series plugin yet. Those come in Tasks 2.1 / 2.2.

What's in this PR

backend/app/printer_models/base.py

  • @runtime_checkable Protocol PrinterModel with the per-model contract:
    • Attributes: model_id, pjl_signatures, snmp_model_oid_value_substr, dpi, print_head_pins
    • Methods: async query_status(host, port, timeout_s) -> StatusBlock, width_to_pixels(tape_spec) -> int, build_print_job(image, tape_spec, auto_cut, high_resolution) -> bytes
  • Imports TapeSpec from app.models.tape and StatusBlock from app.services.status_block.

backend/app/printer_models/registry.py

  • ModelRegistry with class-level _models: ClassVar[list[PrinterModel]] = []
  • register(model) appends — duplicates not prevented (documented).
  • all() returns a copy so callers can iterate / sort without mutating the registry.
  • find_by_pjl(pjl_string) / find_by_snmp_oid_value(oid_value) substring-match; raise ModelNotFoundError with the offending input on miss.
  • Both lookup methods document first-match / registration-order semantics.

backend/tests/unit/printer_models/test_registry.py + conftest.py

  • 5 tests: register-and-find by PJL, by SNMP, both unknown-input raises, all() returns a copy.
  • conftest.py has an autouse fixture _clear_model_registry that resets _models before each test — replaces five manual monkeypatch.setattr calls and makes new tests automatically isolated.

What's NOT in this PR

  • PT-series plugin (Task 2.1)
  • QL-series plugin (Task 2.2)
  • Auto-discovery / entry-point machinery (out of plan scope — explicit import registers each plugin)
  • isinstance(model, PrinterModel) enforcement in register() (the docstring on base.py now correctly notes that callers/utilities may use the @runtime_checkable decorator if they want a guard)

Test plan

  • pytest -q → 48 passed (43 from before + 5 new).
  • pytest tests/unit/printer_models/ -v → 5/5.
  • ruff format --check . clean.
  • ruff check . clean.
  • mypy app/ (strict) clean.

Review history (subagent-driven)

  1. Implementer 7e6ca4e — initial commit.
  2. Spec compliance: ✅ — all attributes, methods, defaults, tests match the plan.
  3. Code quality: APPROVED_WITH_NITS — flagged docstring/code contradiction on isinstance, repetitive monkeypatch.setattr, and ordering-semantics ambiguity.
  4. Fix commit 33a4369 — corrected docstrings, added autouse fixture, documented order semantics.

Linked plan

docs/superpowers/plans/2026-05-11-label-printer-hub.md Task 2.7 (Phase 2 — Plugin Foundation).

strausmann and others added 2 commits May 11, 2026 07:53
…n discovery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…test isolation

- Fix base.py module docstring: @runtime_checkable does not mean the
  registry itself runs isinstance() checks; it is available for plugin
  authors and guard utilities if they choose to use it.
- Rephrase register() docstring: clearer wording, no misleading
  "idempotent" language.
- Add registration-order / first-match semantics note to both
  find_by_pjl() and find_by_snmp_oid_value() docstrings.
- Extract the repeated monkeypatch.setattr(ModelRegistry, "_models", [])
  into an autouse fixture in a new conftest.py; remove it from all five
  test functions and drop the now-unused monkeypatch parameter from each
  signature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 11, 2026 07:58
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes the foundational infrastructure for the printer-model plugin system. It introduces a standardized contract for printer drivers and a registry mechanism to resolve discovered hardware to their respective implementations, facilitating future integration of specific printer series.

Highlights

  • PrinterModel Protocol: Defined a new @runtime_checkable Protocol to standardize the interface for printer-model drivers, including status querying and print job generation.
  • ModelRegistry: Implemented a centralized registry to manage printer-model plugins, supporting lookup by PJL signatures or SNMP OID values.
  • Test Infrastructure: Added a dedicated test suite with an autouse fixture for automatic registry isolation, ensuring clean state between test runs.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Establishes the backend’s printer-model plugin foundation by introducing a PrinterModel protocol contract and a simple in-process registry to resolve model plugins via PJL/SNMP fingerprints, with unit tests ensuring basic registry behavior and test isolation.

Changes:

  • Added PrinterModel (@runtime_checkable Protocol) defining the model-plugin contract.
  • Added ModelRegistry + ModelNotFoundError for registration and fingerprint-based lookup (PJL / SNMP).
  • Added unit tests for registry behavior, plus an autouse fixture to reset registry state between tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
backend/app/printer_models/base.py Defines the PrinterModel protocol used as the plugin contract.
backend/app/printer_models/registry.py Adds a class-level plugin registry with PJL/SNMP lookup helpers and a not-found exception.
backend/tests/unit/printer_models/conftest.py Adds an autouse fixture to reset the registry for test isolation.
backend/tests/unit/printer_models/test_registry.py Adds unit tests covering register/find behavior, not-found cases, and all() copy semantics.

Comment on lines +26 to +30
model_id: str # canonical, e.g. "PT-P750W"
pjl_signatures: list[str] # PJL MDL substrings this plugin handles
snmp_model_oid_value_substr: str # substring of the SNMP OID 2435.2.3.9.1.1.7.0 value
dpi: tuple[int, int] # (width_dpi, height_dpi)
print_head_pins: int # physical pin count
Comment on lines +31 to +38
def test_registry_unknown_pjl_raises() -> None:
with pytest.raises(ModelNotFoundError):
ModelRegistry.find_by_pjl("MDL:UnknownModel;")


def test_registry_unknown_snmp_raises() -> None:
with pytest.raises(ModelNotFoundError):
ModelRegistry.find_by_snmp_oid_value("Unknown printer")
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request establishes a plugin-based architecture for printer models by introducing a PrinterModel protocol and a ModelRegistry for resolving models via PJL or SNMP signatures. Feedback focuses on hardening the registry lookup logic to prevent accidental matches with empty signature strings and ensuring that test mocks fully implement the protocol to comply with type-safety requirements.

"""
for model in cls._models:
for sig in model.pjl_signatures:
if sig in pjl_string:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The substring match sig in pjl_string will return True for any input if sig is an empty string. This could lead to incorrect model resolution if a plugin is misconfigured with an empty signature. Adding a truthiness check ensures that only valid, non-empty signatures are matched.

Suggested change
if sig in pjl_string:
if sig and sig in pjl_string:

order determines priority if multiple plugins match.
"""
for model in cls._models:
if model.snmp_model_oid_value_substr in oid_value:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the PJL lookup, an empty snmp_model_oid_value_substr would cause this method to match any OID value. Adding a check for a non-empty substring prevents accidental matches on misconfigured plugins.

Suggested change
if model.snmp_model_oid_value_substr in oid_value:
if model.snmp_model_oid_value_substr and model.snmp_model_oid_value_substr in oid_value:

Comment on lines +9 to +14
class FakePtModel:
model_id = "PT-P750W"
pjl_signatures: ClassVar[list[str]] = ["MDL:PT-P750W"]
snmp_model_oid_value_substr = "PT-P750W"
dpi = (180, 180)
print_head_pins = 128
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

FakePtModel does not implement the PrinterModel protocol (missing query_status, width_to_pixels, and build_print_job). While the registry currently only uses attributes for lookup, this incomplete implementation will fail mypy --strict checks and isinstance(fake, PrinterModel) runtime checks, making the tests fragile and unrepresentative of real plugins.

Suggested change
class FakePtModel:
model_id = "PT-P750W"
pjl_signatures: ClassVar[list[str]] = ["MDL:PT-P750W"]
snmp_model_oid_value_substr = "PT-P750W"
dpi = (180, 180)
print_head_pins = 128
class FakePtModel:
model_id = "PT-P750W"
pjl_signatures: ClassVar[list[str]] = ["MDL:PT-P750W"]
snmp_model_oid_value_substr = "PT-P750W"
dpi = (180, 180)
print_head_pins = 128
async def query_status(self, *args, **kwargs): ...
def width_to_pixels(self, *args, **kwargs): ...
def build_print_job(self, *args, **kwargs): ...

…n test invariants

- register() now raises ValueError at registration time when a plugin
  supplies an empty PJL signature or an empty SNMP OID substring;
  empty substrings match every input and would silently shadow other plugins
- FakePtModel completed with all three Protocol methods (query_status,
  width_to_pixels, build_print_job) so isinstance() checks against
  @runtime_checkable PrinterModel are correct
- Unknown-input error assertions tightened with match= to verify the
  offending string appears in the exception message
- Two new tests: test_register_rejects_empty_pjl_signature,
  test_register_rejects_empty_snmp_substring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@strausmann strausmann merged commit 2ae0e09 into main May 11, 2026
9 checks passed
github-actions Bot pushed a commit that referenced this pull request May 12, 2026
## 0.3.0 (2026-05-12)

* feat(config): pydantic-settings module with env-driven runtime configuration (#45) ([878e9e0](878e9e0)), closes [#45](#45)
* feat(integrations): AppLookupService aggregator — Phase 3 complete (#53) ([222bef4](222bef4)), closes [#53](#53)
* feat(integrations): Grocy + Spoolman lookup clients with shared NotFoundError base (#52) ([b1c9c3c](b1c9c3c)), closes [#52](#52)
* feat(integrations): LabelData schema + Snipe-IT lookup client (#51) ([3bc180f](3bc180f)), closes [#51](#51)
* feat(label-renderer): Template schema + Pillow/qrcode renderer for 1-bit label bitmaps (#54) ([fb77028](fb77028)), closes [#54](#54)
* feat(printer-models): Brother PT-Series TapeRegistry with TZe and heat-shrink specs (#47) ([7526019](7526019)), closes [#47](#47)
* feat(printer-models): Job lifecycle FSM with explicit state machine (#49) ([1a8c40e](1a8c40e)), closes [#49](#49)
* feat(printer-models): PrinterModel Protocol + ModelRegistry for plugin discovery (#48) ([2ae0e09](2ae0e09)), closes [#48](#48)
* feat(printer-models): PrintQueue worker with pause/resume/cancel/retry (#50) ([dfdf6fe](dfdf6fe)), closes [#50](#50)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants