docx_plus.lint¶
Audit a document for formatting defects, and describe what repairing them
would change. See the CLI page for the docx-plus lint
and docx-plus plan commands over the same engine.
Nothing here writes. lint reports; plan_fixes turns findings into an
ordered, serializable description of the repair and stops there. Designing
the fix model at a point where no code path can apply it is the whole reason
the two halves shipped separately.
A composing layer, not a capability module: like cli/ it sits above the
capability packages and reads across them, adding no OOXML knowledge of its
own. Every judgement it makes is built on
styles/'s cascade resolver and the
document sweep.
Rule kinds¶
The distinction that keeps an opinionated feature inside a lean library:
| Kind | Needs config? | The judgement |
|---|---|---|
consistency |
no | a value fights the document's own applied styles — the document supplies the target |
structural |
no | an objective defect, true regardless of house style |
policy |
yes | a value differs from a target you supplied |
No policy rule is ever enabled by default, so docx_plus never asserts a
house style of its own. It reports that forty paragraphs resolve identically
under three style ids; whether that is a problem stays your call.
Writing a rule¶
Rules register themselves at import, so a new one is a single function:
from collections.abc import Iterator
from docx_plus.lint import Issue, LintContext, rule
@rule(
id="all-caps-heading",
kind="consistency",
severity="info",
description="A heading typed in capitals rather than styled.",
tags={"headings"},
default_on=False,
)
def all_caps_heading(ctx: LintContext) -> Iterator[Issue]:
for resolved in ctx.paragraphs:
if resolved.formatting.outline_level is None:
continue
text = resolved.text
if text.isupper() and len(text) > 3:
yield Issue(
message="Heading is typed in capitals; use the style's caps property.",
location=Location(paragraph_index=resolved.index),
observed=text,
)
A rule yields Issue — only what the rule
itself knows. The engine promotes each to a
Finding by stamping on the id, kind, and
severity from the registration, so a rule cannot advertise one severity in
--list-rules and emit another.
Rules receive the whole swept document, not one paragraph at a time, because the interesting rules are comparative: "this font is an outlier", "these two styles resolve identically", "the outline skips a level". None of those can be decided from a single paragraph.
Fixes and the plan¶
A rule that knows how to repair what it found attaches a
Fix to its Issue. There is no separate
"fixable" flag to keep in step: a finding is fixable exactly when it carries
one.
from docx_plus.lint import lint, plan_fixes
plan = plan_fixes(lint(doc))
for planned in plan.fixes:
print(planned.rule, planned.safety, planned.fix.summary)
A fix is a sequence of named operations from a closed vocabulary
(FixOp), not a callable — a plan has to
survive being written to a file, reviewed, and handed to a different process
than the one that built it.
plan_fixes then decides the three things
no individual rule can, because each is a property of the set of findings:
- Order. Deletions last and back to front. Every operation names a position in the document as it was swept, so a deletion partway down invalidates every index below it.
- The content gate. A fix that removes a paragraph or a style definition
changes what the document contains, not how it looks. Those are withheld
unless you pass
allow_content=True, and reported inplan.deferredso they are visible rather than silently dropped. - Conflicts. Two rules can independently claim the same run property or
overlapping spans of the same text. Claims are per property and per
character span rather than per paragraph, so a paragraph carrying several
unrelated defects is still fixable; the earlier fix wins and the loser is
named in
plan.conflicts.
Every finding lands in exactly one of fixes, deferred, conflicts, or
unfixable, so a plan accounts for the whole audit.
Not everything has a fix¶
Eleven of the twenty rules are report-only, and deliberately. A skipped outline level can be repaired by promoting this heading or demoting the one above it, and those produce different documents; two styles that resolve identically give no reason to prefer either as the survivor; typed indentation needs a number the document does not contain. Each rule's docstring says which case it is. A plan that guessed would be the library asserting a house style, which is exactly what the rule kinds exist to prevent.
Profiles¶
The one place a house opinion may live. A profile enables and disables
rules, overrides severities, and supplies rule-specific options — today
font-outliers's max_share / max_runs and
manual-heading-formatting's max_chars; once policy rules ship, their
targets. A rule reads them through LintContext.option(rule_id, key,
default).
{
"rules": {
"double-space": {"enabled": false},
"style-drift": {"severity": "error"},
"font-outliers": {"enabled": true, "options": {"max_share": 0.02}}
}
}
Pass it as lint(doc, profile=...), or check docx-plus-lint.json into the
repository and both CLI commands will find it beside the document (or above
it). Naming a rule with --rule overrides a profile that disabled it:
configuration never gets to veto a direct question about one document.
A profile may not configure a tag — "apply this severity to whatever carries the tag today" is not a stable thing to check in — and a profile naming a rule that does not exist is an error on load rather than a setting that silently does nothing.
docx_plus.lint.engine ¶
The lint entry point: sweep the document once, run the selected rules.
lint ¶
lint(
doc: Document,
*,
select: Sequence[str] | None = None,
exclude: Sequence[str] | None = None,
include_tables: bool = True,
profile: Profile | str | Path | Mapping[str, Any] | None = None,
) -> list[Finding]
Audit doc and return what the selected rules noticed.
Pure read: nothing here mutates the document. The cascade is resolved once for the whole document and shared across every rule, so the cost is one sweep regardless of how many rules run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Document
|
The python-docx :class: |
required |
select
|
Sequence[str] | None
|
Rule ids and/or tags to run. |
None
|
exclude
|
Sequence[str] | None
|
Rule ids and/or tags to skip; applied last. |
None
|
include_tables
|
bool
|
Whether to sweep paragraphs inside table cells. |
True
|
profile
|
Profile | str | Path | Mapping[str, Any] | None
|
A :class: |
None
|
Returns:
| Type | Description |
|---|---|
list[Finding]
|
Findings sorted by severity, then document order. |
Raises:
| Type | Description |
|---|---|
UnknownRuleError
|
If a selector matches no rule id or tag. |
InvalidProfileError
|
If |
StyleCascadeError
|
If a |
Note
Only the main document body is audited. Headers, footers,
footnotes, endnotes, and comments are not swept — see
:func:~docx_plus.styles.iter_resolved_paragraphs.
Example
from docx import Document from docx_plus.lint import lint doc = Document() _ = doc.add_paragraph("Two spaces here.") for finding in lint(doc): ... print(finding.rule, "-", finding.message) double-space - Two or more consecutive spaces between words.
Source code in docx_plus/lint/engine.py
docx_plus.lint.plan ¶
Report to plan: turn findings into an ordered, inspectable list of edits.
Nothing here writes. :func:plan_fixes takes findings and returns a
description of what a repair pass would change — the ordering it would
apply the edits in, the ones it would withhold, and the pairs that cannot
both be applied. Applying a plan is a later release, and building the plan
first is deliberate: the whole fix model gets designed, serialized, and
reviewed at a point where no code path can corrupt a document.
Three things the planner owns that no individual rule can decide, because each of them is a property of the set of findings rather than of any one:
- Order. Deletions go last and run back to front. A rule that deletes paragraph 12 and a rule that edits the text of paragraph 40 both address positions in the document as swept, and the first deletion invalidates every index after it. Sorting deletions to the end and applying them in descending order keeps every other edit's position valid.
- The content gate. A fix that removes a paragraph or a style definition changes what the document contains, not how it looks. Those are withheld unless the caller asks for them, and reported separately so they are visible rather than silently dropped.
- Conflicts. Two rules can independently target the same run, the same paragraph property, or overlapping spans of the same text. Each is right on its own and they cannot both apply.
A plan is JSON-serializable end to end (:meth:FixPlan.to_dict), because a
plan that cannot be written to a file, reviewed, and handed to a different
process is not much of a plan.
FixPlan
dataclass
¶
FixPlan(
fixes: tuple[PlannedFix, ...] = (),
deferred: tuple[PlannedFix, ...] = (),
conflicts: tuple[FixConflict, ...] = (),
unfixable: tuple[Finding, ...] = (),
)
An ordered, inspectable description of what a repair pass would do.
Attributes:
| Name | Type | Description |
|---|---|---|
fixes |
tuple[PlannedFix, ...]
|
What would be applied, in application order. |
deferred |
tuple[PlannedFix, ...]
|
Fixes withheld by the content gate. Not a failure — a
caller who wants them passes |
conflicts |
tuple[FixConflict, ...]
|
Pairs that could not both apply, with the loser named. |
unfixable |
tuple[Finding, ...]
|
Findings with no known repair. Carried so a plan accounts for every finding it was given rather than quietly shortening the list. |
operations
property
¶
Every operation that would be applied, flattened into plan order.
to_dict ¶
The serializable record for the whole plan.
Source code in docx_plus/lint/plan.py
PlannedFix
dataclass
¶
One finding's fix, placed in a plan.
Position in :attr:FixPlan.fixes is the order — there is no separate
sequence number to fall out of step with it.
Attributes:
| Name | Type | Description |
|---|---|---|
finding |
Finding
|
What was reported, carried whole so a plan reads without the report alongside it. |
fix |
Fix
|
The repair. Non-optional here, unlike on the finding: a plan only ever holds findings that have one. |
operations
property
¶
The edits, in the order the rule requires them applied.
lowest_touched_index
property
¶
The largest paragraph_index any of this fix's operations names.
What the back-to-front deletion order has to sort on. Sorting on the
finding's location instead was unsound: a finding located at
paragraph 1 whose fix deletes paragraph 20, planned alongside one
located at paragraph 5 deleting paragraph 6, came out as
[delete 6, delete 20] — and after 6 goes, the old 20 sits at 19.
No shipped rule produces that shape, but plan_fixes is public
and takes arbitrary findings.
Falls back to the finding's own location for a fix whose operations
name no paragraph at all, such as delete-style.
to_dict ¶
The serializable record for this planned fix.
Source code in docx_plus/lint/plan.py
FixConflict
dataclass
¶
Two fixes that cannot both be applied.
Resolution is by plan order and nothing else: the earlier fix is kept and the later one is dropped. That is arbitrary between two equally good repairs, which is exactly why the loser is reported rather than discarded — the caller can exclude the winning rule and re-plan if they wanted the other one.
Attributes:
| Name | Type | Description |
|---|---|---|
kept |
PlannedFix
|
The fix that stays in the plan. |
dropped |
PlannedFix
|
The fix removed because of the collision. |
reason |
str
|
What the two both claimed, in the document's terms. |
to_dict ¶
The serializable record for this conflict.
Source code in docx_plus/lint/plan.py
plan_fixes ¶
Order the repairs for findings and say which of them can coexist.
A pure function of the findings: it never reads the document, so a plan can be built from a stored report. That is also its limit — it can only reason about what the findings say, which is why the fix vocabulary measures text spans against the original text rather than describing edits as transformations to replay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
findings
|
Sequence[Finding]
|
What :func: |
required |
allow_content
|
bool
|
Whether to include fixes that change what the document contains rather than only how it looks. Off by default: a formatting pass should not quietly delete a paragraph. |
False
|
Returns:
| Type | Description |
|---|---|
FixPlan
|
The plan. Nothing is applied. |
Raises:
| Type | Description |
|---|---|
InvalidFixError
|
If a fix both deletes a paragraph and does positional work elsewhere. See that error for why the plan cannot order such a fix safely. |
Example
from docx import Document from docx_plus.lint import lint, plan_fixes doc = Document() _ = doc.add_paragraph("Spaced out .") plan = plan_fixes(lint(doc)) for planned in plan.fixes: ... print(planned.rule, "-", planned.fix.summary) double-space - Collapse 1 run of spaces to a single space. space-before-punctuation - Remove the whitespace before 1 punctuation mark.
Source code in docx_plus/lint/plan.py
docx_plus.lint.models ¶
The linter's vocabulary: Finding, Location, Rule, LintContext.
The shape here is deliberately close to the sibling wordlive linter's
(../wordlive/spec-linter.md §4), so a document audited by either tool
reads the same way. The one structural difference is :class:Location:
wordlive addresses a live document by anchor id (para:7), which only
means anything to a running Word instance, so findings here carry an index
into the sweep plus an excerpt.
FixOp
module-attribute
¶
FixOp = Literal[
"clear-run-properties",
"clear-paragraph-properties",
"clear-paragraph-numbering",
"set-run-language",
"replace-paragraph-text",
"delete-paragraph",
"delete-style",
]
The closed vocabulary a fix is expressed in.
Deliberately a fixed set of named operations rather than arbitrary callables. A plan has to survive being written to JSON, read by a human, and applied by a different process than the one that built it, and none of that works if an edit is a Python object holding a bound method.
Each op and its args:
clear-run-properties
{"paragraph_index": int, "run_index": int, "properties": [str, ...]}
— delete the named direct properties from the run's w:rPr.
clear-paragraph-properties
{"paragraph_index": int, "properties": [str, ...]} — the same for a
paragraph's w:pPr.
clear-paragraph-numbering
{"paragraph_index": int} — delete the paragraph's direct
w:numPr, so the list its style supplies applies again.
set-run-language
{"paragraph_index": int, "run_index": int, "lang": str}.
replace-paragraph-text
{"paragraph_index": int, "spans": [{"start": int, "end": int,
"replacement": str}, ...]} — half-open character spans into the
paragraph's text, all measured against the original text, so the
order they are applied in does not matter and two rules' spans can be
checked for overlap.
delete-paragraph
{"paragraph_index": int}.
delete-style
{"style_id": str}.
Property names are :class:~docx_plus.styles.ResolvedFormatting field
names — the vocabulary the finding already reported in, so a plan reads in
the same terms as the report that produced it.
FixSafety
module-attribute
¶
How much trust applying a fix asks for.
Orthogonal to :attr:Finding.adds_content, which is about what changes
(content or only formatting); this is about how recoverable the change is.
safe— the document renders identically afterwards. Only the XML gets tidier: a property is deleted from a run and the same value arrives from the style instead. This is the classredundant-direct-formattingproduces, and it is provable rather than asserted — the rule found the property precisely by comparing against the value that would surface without it.review— the rendering or the text changes, deliberately. The old value is in the finding'sobserved, so the change is reversible by hand.destructive— something is removed that the document cannot reconstruct: a style definition and everything it declared, a paragraph and its formatting.
RuleKind
module-attribute
¶
What kind of judgement a rule makes — the distinction that keeps this layer honest about opinions.
consistency— a value fights the document's own applied styles. Needs no configuration, because the document supplies the target: the rule only says "this deviates from what you established elsewhere".structural— an objective defect, true regardless of house style: an outline that skips a level, aREFto a bookmark that does not exist.policy— a value differs from a target the user supplied via a profile. Inert without one, so the library ships no opinion of its own.
Severity
module-attribute
¶
How much a finding matters. error is a defect that will misrender or
resolve wrongly; warning is a real inconsistency; info is a nudge.
Finding
dataclass
¶
Finding(
rule: str,
kind: RuleKind,
severity: Severity,
message: str,
location: Location = Location(),
observed: str | None = None,
expected: str | None = None,
fix: Fix | None = None,
adds_content: bool = False,
)
One thing a rule noticed.
Attributes:
| Name | Type | Description |
|---|---|---|
rule |
str
|
The stable id of the rule that produced it. |
kind |
RuleKind
|
The rule's :data: |
severity |
Severity
|
The rule's :data: |
message |
str
|
A sentence describing the problem, in the document's terms. |
location |
Location
|
Where it is. |
observed |
str | None
|
The value found, rendered for display. |
expected |
str | None
|
The value the rule would have expected, where that is
meaningful. |
fix |
Fix | None
|
What would repair it, or |
adds_content |
bool
|
Whether the fix would insert or delete content rather
than only change formatting. :func: |
Issue
dataclass
¶
Issue(
message: str,
location: Location = Location(),
observed: str | None = None,
expected: str | None = None,
severity: Severity | None = None,
fix: Fix | None = None,
adds_content: bool = False,
)
What a rule body yields — the parts only the rule knows.
The rule's id, kind, and severity come from its registration, so a rule
never restates them and they cannot drift from what list_rules
advertises. The engine promotes each :class:Issue to a
:class:Finding by stamping that metadata on.
Attributes:
| Name | Type | Description |
|---|---|---|
message |
str
|
A sentence describing the problem, in the document's terms. |
location |
Location
|
Where it is. |
observed |
str | None
|
The value found, rendered for display. |
expected |
str | None
|
The value the rule would have expected, where that is meaningful. |
severity |
Severity | None
|
Overrides the rule's default severity for this one finding, for rules whose seriousness depends on what they found. |
fix |
Fix | None
|
What would repair it, or |
adds_content |
bool
|
Whether the eventual fix would insert or delete
content rather than only change formatting. Set independently of
:attr: |
Location
dataclass
¶
Location(
paragraph_index: int | None = None,
run_index: int | None = None,
style_id: str | None = None,
excerpt: str = "",
)
Where a finding sits.
Every field is optional because findings are not all positional: a rule
about a style definition (an unused style, two styles that resolve
identically) has no paragraph to point at, and reports style_id
alone.
Attributes:
| Name | Type | Description |
|---|---|---|
paragraph_index |
int | None
|
Position in the sweep's document order — the
|
run_index |
int | None
|
Position within that paragraph, for a run-level finding. |
style_id |
str | None
|
The |
excerpt |
str
|
A short slice of the paragraph's text, so a report reads usefully without the document open alongside it. |
describe ¶
A short human-readable position, for report lines.
Source code in docx_plus/lint/models.py
Rule
dataclass
¶
Rule(
id: str,
kind: RuleKind,
severity: Severity,
description: str,
check: CheckFn,
tags: frozenset[str] = frozenset(),
default_on: bool = True,
)
A registered rule — its metadata and the check that implements it.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Stable, kebab-case, and part of the public surface: users select and exclude by it, so it should not change once shipped. |
kind |
RuleKind
|
See :data: |
severity |
Severity
|
Default severity of the findings it emits. |
description |
str
|
One line, shown by |
check |
CheckFn
|
The implementation. |
tags |
frozenset[str]
|
Cluster names a user can select instead of naming ids
( |
default_on |
bool
|
Whether the rule runs when the caller selects nothing.
Unambiguous defects ship on; heuristic or opinion-flavoured
rules ship off, so default output stays worth reading. Every
|
matches ¶
LintContext
dataclass
¶
Everything a rule is given to work with.
Rules receive the whole swept document rather than one paragraph at a time, because the interesting rules are comparative — "this font is an outlier", "these two styles resolve identically", "the outline skips a level" — and none of those can be decided from a single paragraph.
Attributes:
| Name | Type | Description |
|---|---|---|
doc |
Document
|
The document, for rules that need a part the sweep does not cover (the styles element, the bookmark table). |
paragraphs |
list[ResolvedParagraph]
|
Every swept paragraph, in document order, materialised so rules can walk it more than once. |
profile |
Profile
|
The loaded profile, so a rule can read its own options
through :meth: |
option ¶
One rule-specific value from the profile, or the rule's default.
The seam between a rule's built-in threshold and a team's
override: a rule keeps its constant as the default and a
profile's options block replaces it per document run. The
rule validates the value; the profile only promises it is JSON.
Source code in docx_plus/lint/models.py
resolve ¶
resolve(
target: Paragraph | Run | _Cell, *, stop_below: Layer | None = None
) -> ResolvedFormatting
Resolve one target the sweep did not precompute.
The sweep already carries each paragraph's and run's full resolve
plus its baseline (the same target without its own direct
layer), which is what nearly every rule needs. This covers the rest:
a rule wanting some other slice of the cascade — the numbering a
style would supply if the paragraph did not override it, say.
Deliberately not cache-shared with the sweep, so it costs a full cascade walk per call. Call it for the subset a rule genuinely needs, never for every paragraph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Paragraph | Run | _Cell
|
A paragraph, run, or cell in :attr: |
required |
stop_below
|
Layer | None
|
Stop the walk below this
:data: |
None
|
Returns:
| Type | Description |
|---|---|
ResolvedFormatting
|
The resolved formatting for |
Source code in docx_plus/lint/models.py
excerpt ¶
A one-line slice of a paragraph's text, for a report.
Internal spacing is preserved, because several rules are about
whitespace and an excerpt that tidied it would hide the very thing
being reported — a double-space finding whose excerpt shows single
spaces reads like a false positive. Tabs render as \t so they are
visible at all, and line breaks collapse to a space so one finding
stays one line.
Output is ASCII-only, and enforced rather than hoped for. This
reaches a Windows console via docx-plus lint, where Python
encodes stdout as cp1252 whenever it is redirected to a file or a
pipe — so a document containing CJK text or an emoji used to end
the command in an unhandled UnicodeEncodeError rather than a
report. Anything outside ASCII becomes a \x/\u escape,
which keeps the character visible and greppable instead of
dropping it.
Truncation happens after escaping, so limit bounds the
printed width rather than the source length. See
:func:render_for_report, which is the same treatment for text
that does not come from a paragraph.
Source code in docx_plus/lint/models.py
Fix
dataclass
¶
What a rule would do about the thing it found.
A fix is described, never executed, in this release: lint and
:func:~docx_plus.lint.plan_fixes are both pure reads. Describing it
first is the point — the fix model gets designed and reviewed while
nothing can yet corrupt a document.
Attributes:
| Name | Type | Description |
|---|---|---|
summary |
str
|
One line, in the document's terms: what would change. |
safety |
FixSafety
|
See :data: |
operations |
tuple[FixOperation, ...]
|
The edits, in the order they must be applied. A rule supplying more than one is asserting that order matters — a run of empty paragraphs is deleted back to front so the earlier indices stay valid. |
to_dict ¶
The serializable record for this fix.
docx_plus.lint.profile ¶
Profiles — the one place a house opinion is allowed to live.
The rule kinds keep the library's own opinions out of the linter:
consistency and structural rules judge a document against itself,
and policy rules — the ones that need somebody to say what "right"
looks like — are inert without a target. A profile is where those
targets come from, and the reason docx_plus can ship no house style
while still being useful to a team that has one.
{
"rules": {
"double-space": {"enabled": false},
"style-drift": {"severity": "error"},
"font-outliers": {"enabled": true, "options": {"max_share": 0.02}}
}
}
options reach a rule through :meth:LintContext.option, which falls
back to the rule's own built-in threshold — so a profile overrides a
number a rule already has rather than supplying one it lacks. The knobs
that exist today are font-outliers's max_share / max_runs and
manual-heading-formatting's max_chars; no policy rule ships
yet, so nothing requires an option to run.
What a profile deliberately does not do is select rules. --rule /
--exclude stay the caller's, applied after the profile, so a profile
never stops someone asking a specific question of a specific document.
Profile
dataclass
¶
A loaded lint profile.
Attributes:
| Name | Type | Description |
|---|---|---|
rules |
Mapping[str, RuleSettings]
|
Settings by rule id. A rule the profile does not mention keeps its registered behaviour, so a profile is a set of deltas rather than a replacement catalogue. |
settings ¶
enabled ¶
Whether rule_id runs, given its registered default.
severity ¶
The severity to report rule_id at, given its registered default.
option ¶
One rule-specific value, or default.
The hook a rule reads its thresholds through, via
:meth:LintContext.option — see the module docstring for which
rules honour which keys.
Source code in docx_plus/lint/profile.py
load
classmethod
¶
Build a profile from a path, an already-parsed mapping, or nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str | Path | Mapping[str, Any] | None
|
A path to a JSON file, a mapping in the same shape, or
|
required |
Returns:
| Type | Description |
|---|---|
Profile
|
The profile. |
Raises:
| Type | Description |
|---|---|
InvalidProfileError
|
If the file is unreadable, is not JSON, or is not in the documented shape. |
Example
profile = Profile.load({"rules": {"double-space": {"enabled": False}}}) profile.enabled("double-space", default=True) False profile.enabled("style-drift", default=True) True
Source code in docx_plus/lint/profile.py
discover
classmethod
¶
Find and load :data:DEFAULT_PROFILE_NAME at or above start.
Walks up from start (a file or a directory) to the filesystem
root, so running the linter from anywhere inside a project picks up
the conventions checked in at its top.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
str | Path
|
Where to begin looking. |
required |
Returns:
| Type | Description |
|---|---|
Profile
|
The first profile found, or the empty profile if there is none. |
Raises:
| Type | Description |
|---|---|
InvalidProfileError
|
If a profile is found and is malformed. Silently ignoring a broken checked-in profile would be worse than not having one. |
Source code in docx_plus/lint/profile.py
RuleSettings
dataclass
¶
RuleSettings(
enabled: bool | None = None,
severity: Severity | None = None,
options: Mapping[str, Any] = dict(),
)
What a profile says about one rule.
Attributes:
| Name | Type | Description |
|---|---|---|
enabled |
bool | None
|
Force the rule on or off, overriding its |
severity |
Severity | None
|
Report this rule's findings at another severity — the knob a team reaches for first, since "we treat drift as an error" is a house opinion that changes no detection. |
options |
Mapping[str, Any]
|
Rule-specific values. Untouched by the loader beyond being required to be a JSON object; each rule validates its own. |
InvalidProfileError ¶
Bases: DocxPlusError, ValueError
Raised when a profile is malformed.
Loudly, and on load rather than on use: a profile with a typo in a rule id would otherwise configure nothing and read exactly like a profile that was working.
docx_plus.lint.registry ¶
Rule registration and selection.
Rules register themselves at import time via the :func:rule decorator, so
adding one is a single new function in lint/rules/ — no central list to
keep in sync. :func:select_rules implements the selection semantics the CLI
exposes.
UnknownRuleError ¶
Bases: DocxPlusError, KeyError
Raised when a selector names neither a registered rule id nor a tag.
A typo in a rule name would otherwise silently select nothing, which reads exactly like a clean document.
rule ¶
rule(
*,
id: str,
kind: RuleKind,
severity: Severity,
description: str,
tags: Iterable[str] = (),
default_on: bool = True,
) -> Callable[[CheckFn], CheckFn]
Register the decorated function as a lint rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id
|
str
|
Stable kebab-case identifier; part of the public surface. |
required |
kind
|
RuleKind
|
|
required |
severity
|
Severity
|
|
required |
description
|
str
|
One line for |
required |
tags
|
Iterable[str]
|
Cluster names for bulk selection. |
()
|
default_on
|
bool
|
Whether it runs when nothing is selected. |
True
|
Returns:
| Type | Description |
|---|---|
Callable[[CheckFn], CheckFn]
|
The undecorated function, so rules stay directly callable in tests. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in docx_plus/lint/registry.py
all_rules ¶
select_rules ¶
select_rules(
select: Sequence[str] | None = None,
exclude: Sequence[str] | None = None,
profile: Profile | None = None,
) -> list[Rule]
Resolve selectors to the rules that should run.
Selection semantics, matching the sibling wordlive linter so the two
behave the same way:
select=Noneruns every rule withdefault_on=True, as adjusted byprofile.- A non-empty
selectruns exactly what it names, including off-by-default rules and anything a profile disabled — naming a tag is how a user opts into that cluster's heuristic rules, and asking for a rule by name is not something configuration gets to veto. excludeis applied last and always wins.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
select
|
Sequence[str] | None
|
Rule ids and/or tags to run. |
None
|
exclude
|
Sequence[str] | None
|
Rule ids and/or tags to skip. |
None
|
profile
|
Profile | None
|
A loaded profile whose per-rule |
None
|
Returns:
| Type | Description |
|---|---|
list[Rule]
|
The matching rules, sorted by id. |
Raises:
| Type | Description |
|---|---|
UnknownRuleError
|
If a selector, or a rule id named by |