Python API¶
behave-doctor can be used as a library for custom tooling, IDE plugins, CI integrations, or anything that needs programmatic access to Behave suite diagnostics.
Installation¶
No extra dependencies are needed — the library API is part of the core package.
scan_project¶
The high-level entry point. Scans a Behave project and returns a
ProjectReport with diagnostics, statistics, and an exit code.
behave_doctor.scan_project
¶
Scan a Behave project and return a diagnostic report.
This is the high-level orchestrator: it loads configuration, scans
features and step definitions, builds the dependency graph, runs all
enabled rules, and returns a :class:ProjectReport.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Root directory of the Behave project. |
required |
config
|
DoctorConfig | None
|
Optional configuration. When |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
ProjectReport
|
class: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
FileNotFoundError
|
If |
ScanError
|
If |
Source code in behave_doctor/__init__.py
Example: basic scan¶
from behave_doctor import scan_project
report = scan_project("path/to/project")
for d in report.diagnostics:
print(f"{d.rule_id}: {d.message} at {d.file}:{d.line}")
print(f"Exit code: {report.exit_code}")
Example: with custom config¶
from behave_doctor import scan_project, DoctorConfig, Severity
config = DoctorConfig(
features_dir="my_features",
steps_dir="my_steps",
min_severity=Severity.WARNING,
rules={
"BD101": {"enabled": False},
"BD401": {"max_steps": 5},
"BD203": {"max_scenarios": 10},
},
)
report = scan_project("path/to/project", config=config)
Example: filter by severity¶
from behave_doctor import scan_project, Severity
report = scan_project("path/to/project")
errors = [d for d in report.diagnostics if d.severity is Severity.ERROR]
warnings = [d for d in report.diagnostics if d.severity is Severity.WARNING]
print(f"{len(errors)} errors, {len(warnings)} warnings")
ProjectReport¶
The result of a scan_project call. Contains diagnostics, statistics, and
an exit code.
behave_doctor.ProjectReport
dataclass
¶
The full diagnostic report for a scanned project.
Attributes:
| Name | Type | Description |
|---|---|---|
project_path |
Path
|
Root path of the scanned project. |
diagnostics |
list[Diagnostic]
|
All diagnostics produced by the rule engine. |
statistics |
ProjectStatistics
|
Aggregate project statistics. |
scanned_at |
datetime
|
Timestamp of the scan. |
scan_duration_ms |
int
|
Scan duration in milliseconds. |
Source code in behave_doctor/model/project_report.py
Attributes¶
| Attribute | Type | Description |
|---|---|---|
diagnostics |
list[Diagnostic] |
All diagnostics found during the scan. |
statistics |
ProjectStatistics |
Aggregate statistics about the project. |
exit_code |
int |
0 = clean, 1 = issues found. |
scan_duration_ms |
int |
Scan duration in milliseconds. |
scanned_at |
datetime |
Timestamp of the scan. |
project_path |
Path |
Root path of the scanned project. |
Diagnostic¶
A single diagnostic finding produced by a rule.
behave_doctor.Diagnostic
dataclass
¶
A single finding produced by a diagnostic rule.
Attributes:
| Name | Type | Description |
|---|---|---|
rule_id |
str
|
Stable rule identifier (e.g. |
rule_name |
str
|
Human-readable rule name (e.g. |
severity |
Severity
|
Severity of the finding. |
category |
Category
|
Category the rule belongs to. |
message |
str
|
Human-readable description of the finding. |
file |
Path | None
|
File where the issue was found, if applicable. |
line |
int | None
|
1-indexed line number, if applicable. |
suggestion |
str | None
|
Optional fix suggestion. |
metadata |
dict[str, Any]
|
Extra context for reporters. |
Source code in behave_doctor/model/diagnostic.py
Attributes¶
| Attribute | Type | Description |
|---|---|---|
rule_id |
str |
Rule identifier (e.g. "BD301"). |
rule_name |
str |
Human-readable rule name (e.g. "unused-step-def"). |
severity |
Severity |
One of ERROR, WARNING, INFO, HINT. |
category |
Category |
One of STRUCTURE, QUALITY, COVERAGE, COMPLEXITY, DEPENDENCY. |
message |
str |
Human-readable description of the issue. |
file |
Path \| None |
File path where the issue was found. |
line |
int \| None |
Line number (1-based), if applicable. |
suggestion |
str \| None |
Optional fix suggestion. |
metadata |
dict |
Optional rule-specific metadata. |
Severity¶
Enum of diagnostic severity levels.
behave_doctor.Severity
¶
| Value | Description |
|---|---|
ERROR |
Must fix — the suite is broken or unsafe. |
WARNING |
Should fix — quality or maintainability. |
INFO |
Informational — no action required. |
HINT |
Suggestion — optional improvement. |
Category¶
Enum of diagnostic categories.
behave_doctor.Category
¶
Bases: Enum
Diagnostic category, grouping rules by concern.
Source code in behave_doctor/model/enums.py
| Value | Description |
|---|---|
STRUCTURE |
Informational metrics (BD101-104). |
QUALITY |
Suite quality issues (BD201-204). |
COVERAGE |
Unused/undefined steps and tags (BD301-304). |
COMPLEXITY |
Size and complexity limits (BD401-403). |
DEPENDENCY |
Import and module analysis (BD501-503). |
DoctorConfig¶
Configuration for a scan. All fields have defaults.
behave_doctor.DoctorConfig
dataclass
¶
Configuration for a behave-doctor scan.
Attributes:
| Name | Type | Description |
|---|---|---|
features_dir |
str
|
Relative path to the features directory. |
steps_dir |
str
|
Relative path to the step definitions directory. |
min_severity |
Severity
|
Minimum severity to report. |
rules |
dict[str, dict[str, Any]]
|
Per-rule overrides keyed by rule ID. Each rule dict defaults
to |
exclude_paths |
list[str]
|
Glob patterns of paths to exclude from scanning. |
exclude_tags |
list[str]
|
Tags to exclude from coverage analysis. |
Source code in behave_doctor/model/config.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
from_pyproject
classmethod
¶
Load configuration from the [tool.behave-doctor] section of a TOML file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
Path to a |
required |
Returns:
| Type | Description |
|---|---|
DoctorConfig
|
A |
DoctorConfig
|
missing, the default config is returned. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
FileNotFoundError
|
If |
IsADirectoryError
|
If |
ValueError
|
If |
Source code in behave_doctor/model/config.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
is_excluded
¶
Return True if path matches any configured exclude glob pattern.
Both the path itself and each of its parent directories are checked
so that directory-level patterns ("excluded/**") and file-level
patterns ("*.feature") both work.
Source code in behave_doctor/model/config.py
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
features_dir |
str |
"features/" |
Relative path to features. |
steps_dir |
str |
"features/steps/" |
Relative path to step definitions. |
min_severity |
Severity |
Severity.INFO |
Minimum severity to report. |
exclude_tags |
list[str] |
[] |
Tags excluded from BD303. |
exclude_paths |
list[str] |
[] |
Glob patterns to exclude from scan. |
rules |
dict[str, dict] |
{} |
Per-rule configuration. |
Loading from pyproject.toml¶
from behave_doctor import DoctorConfig
config = DoctorConfig.from_pyproject("path/to/project/pyproject.toml")
ProjectStatistics¶
Aggregate statistics about the scanned project.
behave_doctor.ProjectStatistics
dataclass
¶
Aggregate metrics for a scanned project.
Attributes:
| Name | Type | Description |
|---|---|---|
features |
int
|
Total number of features. |
scenarios |
int
|
Total number of scenarios. |
steps |
int
|
Total number of steps. |
step_definitions |
int
|
Total number of step definitions. |
tags |
int
|
Number of unique tags. |
total_tag_usages |
int
|
Total tag usages across the project. |
average_steps_per_scenario |
float
|
Mean steps per scenario. |
unused_step_definitions |
int
|
Count of unused step definitions. |
undefined_steps |
int
|
Count of undefined feature steps. |
Source code in behave_doctor/model/project_report.py
Attributes¶
| Attribute | Type | Description |
|---|---|---|
features |
int |
Total number of features. |
scenarios |
int |
Total number of scenarios. |
steps |
int |
Total number of steps. |
step_definitions |
int |
Total number of step definitions. |
tags |
int |
Number of unique tags. |
total_tag_usages |
int |
Total tag usages across all scenarios. |
average_steps_per_scenario |
float |
Average steps per scenario. |
unused_step_definitions |
int |
Step definitions never matched. |
undefined_steps |
int |
Steps with no matching definition. |
Example: custom reporter¶
import json
from behave_doctor import scan_project, Severity
report = scan_project("path/to/project")
output = {
"errors": [],
"warnings": [],
"stats": {
"features": report.statistics.features,
"scenarios": report.statistics.scenarios,
"unused_step_definitions": report.statistics.unused_step_definitions,
"undefined_steps": report.statistics.undefined_steps,
},
}
for d in report.diagnostics:
entry = {
"rule": d.rule_id,
"message": d.message,
"file": d.file,
"line": d.line,
}
if d.severity is Severity.ERROR:
output["errors"].append(entry)
elif d.severity is Severity.WARNING:
output["warnings"].append(entry)
print(json.dumps(output, indent=2))
Example: IDE plugin integration¶
from behave_doctor import scan_project, Severity
def on_save(project_path: str) -> list[dict]:
"""Run behave-doctor on save and return diagnostics for the editor."""
report = scan_project(project_path)
return [
{
"file": d.file,
"line": d.line,
"severity": d.severity.name.lower(),
"message": f"[{d.rule_id}] {d.message}",
"source": "behave-doctor",
}
for d in report.diagnostics
if d.severity in (Severity.ERROR, Severity.WARNING)
]