Lint (dm.lint)

Static checker against the dartwork-mpl anti-pattern catalog (0.4+).

dm.lint runs a Python source string through the 15-rule anti-pattern catalog shipped at asset/prompt/02-anti-patterns.yaml. It is the same engine behind the MCP lint_dartwork_mpl_code tool and the dartwork-mpl lint CLI, so editor integrations, CI, and AI assistants all see the same violations.

Quick start

import dartwork_mpl as dm

source = """
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6.7, 4.0))
plt.tight_layout()
"""

issues = dm.lint.lint(source)
for issue in issues:
    print(issue.rule_id, issue.severity, issue.message)

# Or render a human-readable report
print(dm.lint.format_report(issues))

Each rule entry has an id, severity (critical / warning / info), a short message, an optional why blurb, and a recommended fix_suggestion. The catalog lives in YAML so adding or tightening a rule is a single file edit; the lint engine reloads it on call.

Catalog highlights

  • figsize-direct — raw figsize=(w, h) tuples are forbidden; use figsize=dm.figsize("<n>cm", "<aspect>").

  • dm-subplots-removeddm.subplots / dm.figure were removed; use plt.subplots(figsize=dm.figsize(...)).

  • raw-width-number — bare numbers passed to dm.figsize are rejected because they carry no unit.

  • tight-layoutplt.tight_layout() is forbidden; use dm.simple_layout(fig).

  • width-token — deprecated 0.3 width tokens (dm.SW / MW / TW / DW).

  • oversize-width — widths beyond 17 cm break most page layouts.

  • fontsize-literal / linewidth-literal — pass numeric values via dm.fs(n) / dm.lw(n) so they track the active style.

  • raw-hex-color — prefer named palette tokens (oc., tw., dc., …) over inline hex.

  • jet-cmap — flag rainbow colormaps that misrepresent ordinal data.

The full list (always authoritative) is at asset/prompt/02-anti-patterns.yaml in the source tree, or via the MCP resource dartwork-mpl://guide/anti-patterns.

API

dartwork-mpl lint engine.

Loads the anti-pattern catalog from asset/prompt/02-anti-patterns.yaml and applies it to a Python source string. Used by the MCP lint_dartwork_mpl_code tool, the dartwork-mpl lint CLI, and CI drift tests.

The catalog is the single source of truth: code never inlines rule text. Add or change rules in the YAML file; this module loads them verbatim.

class dartwork_mpl.lint.Issue(rule_id: str, severity: str, message: str, line: int | None = None, snippet: str | None = None, column: int | None = None, fix_suggestion: str | None = None)[source]

Bases: object

A detected violation.

column is the absolute byte offset of the match in the source string (0-indexed). It is included to disambiguate multiple violations on the same line — (rule_id, line) alone collapses them and hides the second occurrence from auto-fixers.

fix_suggestion mirrors the YAML field of the same name and is surfaced inline by format_report() so AI agents can apply a fix without a second round-trip.

column: int | None = None
fix_suggestion: str | None = None
line: int | None = None
message: str
rule_id: str
severity: str
snippet: str | None = None
class dartwork_mpl.lint.Rule(id: str, severity: str, detector_kind: str, detector_value: str, message: str, why: str | None = None, fix_suggestion: str | None = None)[source]

Bases: object

A single anti-pattern definition.

detector_kind: str
detector_value: str
fix_suggestion: str | None = None
id: str
message: str
severity: str
why: str | None = None
dartwork_mpl.lint.apply_lint_fixes(code: str) tuple[str, list[Issue], list[Issue]][source]

Apply safe mechanical fixes for a curated subset of lint rules.

Performs identifier- and call-level rewrites for rules whose replacement does not depend on caller-supplied parameters (currently plt-style-use and the no-arg form of tight-layout). Each rule is applied as a whole-source regex substitution, after which the linter re-runs to compute the diff between before and after issue sets.

Parameters:

code (str) – Python source.

Returns:

(fixed_code, applied_issues, unfixed_issues)applied mirrors issues that disappear after the rewrite; unfixed is what still trips the linter (typically context-dependent rules like figsize-direct).

Return type:

tuple[str, list[Issue], list[Issue]]

dartwork_mpl.lint.format_report(issues: list[Issue]) str[source]

Render issues as a multi-line [SEV] rule-id: message report.

The full message is preserved (including any subsequent lines from a YAML | block scalar) and indented under the header line so reports stay readable in plain-text MCP/CLI output.

If a rule provides a fix_suggestion, it is emitted on its own line directly after the message as fix: <suggestion> so AI agents can lift the replacement directly without a second round-trip.

dartwork_mpl.lint.lint(code: str, *, rules: Iterable[Rule] | None = None) list[Issue][source]

Apply anti-pattern rules to a Python source string.

Note

code must be Python source, not YAML/Markdown/JSON. The rules are regex-based, so feeding non-Python content (e.g. the anti-patterns YAML itself) will produce false positives.

Parameters:
  • code (str) – Python source to scan.

  • rules (Iterable[Rule] | None, optional) – Override the rule set (e.g. for tests). Defaults to load_rules() output.

Returns:

Issues in declaration order, deduplicated by (rule_id, column) so multiple violations on the same line are reported separately.

Return type:

list[Issue]

dartwork_mpl.lint.load_rules(path: Path | None = None) list[Rule][source]

Load and parse the anti-pattern catalog.

Parameters:

path (Path | None, optional) – Override path for testing. Defaults to the bundled 02-anti-patterns.yaml.

Returns:

Parsed rule objects in declaration order.

Return type:

list[Rule]

dartwork_mpl.lint.migrate_legacy_code(code: str) str[source]

Best-effort regex rewrite from 0.3-era to 0.4 dartwork-mpl idioms.

Two passes:

  1. Safe substitutions are applied in place (dm.cm2indm.cm, plt.style.usedm.style.use).

  2. Context-dependent patterns (deprecated width tokens, the removed dm.subplots / dm.figure, raw figsize=(w,h) tuples, tight_layout() calls, and the removed dm.agent_utils / dm.xplot namespaces) get a # TODO(dm-migrate): comment inserted above the offending line so the agent can see what to change without losing the original code.

Parameters:

code (str) – 0.3-era Python source.

Returns:

Rewritten source. Always returned (never raises). Use lint() on the result to confirm no critical issues remain after the agent applies the manual hints.

Return type:

str

Notes

AST-based migration is intentionally out of scope (see docs/superpowers/specs/2026-05-01-ai-readiness-0.5-roadmap.md, “Out of Scope”). Inputs that don’t match any pattern are returned unchanged.