Skip to content

API Reference

The public entry point for programmatic usage is azure_functions_doctor.api.run_diagnostics(path, profile, rules_path). It uses the same diagnostics engine as azure-functions doctor, then returns structured results that can be consumed in scripts, CI pipelines, or custom tooling.

Public API at a Glance

API Purpose Typical usage
run_diagnostics(path, profile, rules_path) Run all checks and return section-level results. CI validation, custom wrappers, pre-commit hooks.
Doctor(path, profile, rules_path) Lower-level runner with explicit lifecycle methods. Advanced control over rule loading and execution.
CheckResult Result object for one check item. Output processing and custom reporting.
SectionResult Result object for a group of checks. Rendering grouped summaries by category.
HandlerRegistry Maps rule type to execution handlers. Handler extension and internal diagnostics flow.

Programmatic Usage with run_diagnostics

Use run_diagnostics when you want behavior that matches the CLI while staying inside Python code.

from pathlib import Path

from azure_functions_doctor.api import run_diagnostics


def summarize_failures(project_path: str) -> int:
    results = run_diagnostics(path=project_path, profile="full", rules_path=None)
    failed_required = 0

    for section in results:
        for item in section["items"]:
            if item["status"] == "fail":
                failed_required += 1

    return failed_required


if __name__ == "__main__":
    target = str(Path(".").resolve())
    failed = summarize_failures(target)
    raise SystemExit(1 if failed else 0)

Parameters

Parameter Type Required Description
path str Yes File system path to the Azure Functions app root.
profile str | None No "full" (default behavior) or "minimal" (required checks only).
rules_path pathlib.Path | None No Optional path to a custom rules file matching the rules schema.

Return Value

run_diagnostics returns list[SectionResult], where each section includes:

  • title: human-readable section title
  • category: machine-friendly section key
  • status: pass or fail at section level
  • items: list of CheckResult entries

Working with CheckResult and SectionResult

The following snippet shows safe access to optional fields (hint, hint_url) while creating a report.

from azure_functions_doctor.api import run_diagnostics


def flatten_results(path: str) -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    for section in run_diagnostics(path=path, profile=None, rules_path=None):
        for item in section["items"]:
            rows.append(
                {
                    "section": section["category"],
                    "label": item["label"],
                    "status": item["status"],
                    "value": item["value"],
                    "hint": item.get("hint", ""),
                    "hint_url": item.get("hint_url", ""),
                }
            )
    return rows

Using Doctor Directly

Use Doctor if you need to separate rule loading, validation, and execution.

from pathlib import Path

from azure_functions_doctor.doctor import Doctor


def run_with_custom_rules(project_dir: str, custom_rules_file: str) -> list[dict]:
    doctor = Doctor(
        path=project_dir,
        profile="minimal",
        rules_path=Path(custom_rules_file),
    )
    rules = doctor.load_rules()
    return doctor.run_all_checks(rules=rules)

Handler Registry Integration

HandlerRegistry stores the mapping from rule type to concrete handler methods. Most users do not need to call it directly, but it is useful in internal extensions.

from pathlib import Path

from azure_functions_doctor.handlers import HandlerRegistry


def run_single_rule(rule: dict, project_path: str) -> dict[str, str]:
    registry = HandlerRegistry()
    result = registry.handle(rule=rule, path=Path(project_path))
    return result

CLI

doctor(path='.', verbose=False, debug=False, format='table', output=None, profile=None, rules=None, summary_json=None, target_python=None)

Run diagnostics on an Azure Functions application.

Parameters:

Name Type Description Default
path str

Path to the Azure Functions app. Defaults to current directory.

'.'
verbose Annotated[bool, Option(-v, --verbose, help='Show detailed hints for failed checks')]

Show detailed hints for failed checks.

False
debug Annotated[bool, Option(help='Enable debug logging')]

Enable debug logging to stderr.

False
format Annotated[str, Option(help="Output format: 'table', 'json', 'sarif', or 'junit'")]

Output format: 'table', 'json', 'sarif', or 'junit'.

'table'
output Annotated[Optional[Path], Option(help='Optional path to save output result')]

Optional file path to save output result.

None
profile Annotated[Optional[str], Option(help="Rule profile: 'minimal' or 'full'")]

Optional rule profile ('minimal' or 'full').

None
rules Annotated[Optional[Path], Option(help='Optional path to a custom rules file')]

Optional path to a custom rules file.

None
summary_json Annotated[Optional[Path], Option(--summary - json, help='Write a JSON summary of counts (passed/warned/failed) to this path')]

Path to write a JSON summary with passed/warned/failed counts.

None
target_python Annotated[Optional[str], Option(--target - python, help='Override target Python runtime')]

Optional target Python runtime override.

None
Source code in src/azure_functions_doctor/cli.py
@cli.command(name="doctor")
def doctor(
    path: str = ".",
    verbose: Annotated[
        bool, typer.Option("-v", "--verbose", help="Show detailed hints for failed checks")
    ] = False,
    debug: Annotated[bool, typer.Option(help="Enable debug logging")] = False,
    format: Annotated[
        str, typer.Option(help="Output format: 'table', 'json', 'sarif', or 'junit'")
    ] = "table",
    output: Annotated[
        Optional[Path], typer.Option(help="Optional path to save output result")
    ] = None,
    profile: Annotated[
        Optional[str], typer.Option(help="Rule profile: 'minimal' or 'full'")
    ] = None,
    rules: Annotated[
        Optional[Path], typer.Option(help="Optional path to a custom rules file")
    ] = None,
    summary_json: Annotated[
        Optional[Path],
        typer.Option(
            "--summary-json",
            help="Write a JSON summary of counts (passed/warned/failed) to this path",
        ),
    ] = None,
    target_python: Annotated[
        Optional[str], typer.Option("--target-python", help="Override target Python runtime")
    ] = None,
) -> None:
    """
    Run diagnostics on an Azure Functions application.

    Args:
        path: Path to the Azure Functions app. Defaults to current directory.
        verbose: Show detailed hints for failed checks.
        debug: Enable debug logging to stderr.
        format: Output format: 'table', 'json', 'sarif', or 'junit'.
        output: Optional file path to save output result.
        profile: Optional rule profile ('minimal' or 'full').
        rules: Optional path to a custom rules file.
        summary_json: Path to write a JSON summary with passed/warned/failed counts.
        target_python: Optional target Python runtime override.
    """
    # Validate inputs before proceeding
    _validate_inputs(path, format, output, target_python)

    if rules is not None and not rules.exists():
        raise typer.BadParameter(f"Rules path does not exist: {rules}")

    # Configure logging based on CLI flags
    if debug:
        setup_logging(level="DEBUG", format_style="structured")
    else:
        # Use environment variable or default to WARNING
        setup_logging(level=None, format_style="simple")

    start_time = time.time()
    doctor = Doctor(path, profile=profile, rules_path=rules, target_python=target_python)
    resolved_path = Path(path).resolve()
    report_properties = doctor.get_report_properties()

    # Log diagnostic start
    loaded_rules = doctor.load_rules()
    log_diagnostic_start(str(resolved_path), len(loaded_rules))
    results = doctor.run_all_checks(rules=loaded_rules)

    # Calculate execution metrics
    end_time = time.time()
    duration_ms = (end_time - start_time) * 1000

    # Count results for logging
    total_checks = sum(len(section["items"]) for section in results)
    passed_items = sum(
        1 for section in results for item in section["items"] if item.get("status") == "pass"
    )
    failed_items = sum(
        1 for section in results for item in section["items"] if item.get("status") == "fail"
    )
    # Note: handlers currently only return "pass"/"fail", not "error"
    errors = 0

    # Log diagnostic completion
    log_diagnostic_complete(total_checks, passed_items, failed_items, errors, duration_ms)

    # Pre-compute aggregated counts from normalized item['status'] values
    passed_count = 0
    warning_count = 0  # explicit 'warn' statuses
    fail_count = 0  # explicit 'fail' statuses
    for section in results:
        for item in section["items"]:
            s = item.get("status")
            if s == "pass":
                passed_count += 1
            elif s == "warn":
                warning_count += 1
            elif s == "fail":
                fail_count += 1
            else:
                warning_count += 1  # unknown treated as warning

    # Write summary JSON sidecar when --summary-json is specified (format-independent)
    if summary_json is not None:
        summary_data = {
            "passed": passed_count,
            "warned": warning_count,
            "failed": fail_count,
        }
        try:
            summary_json.parent.mkdir(parents=True, exist_ok=True)
            summary_json.write_text(json.dumps(summary_data), encoding="utf-8")
        except (OSError, PermissionError) as exc:
            logger.warning(f"Failed to write summary JSON to {summary_json}: {exc}")

    if format == "json":
        generated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
        metadata = {
            "tool_version": __version__,
            "generated_at": generated_at,
            "target_path": str(Path(path).resolve()),
            **report_properties,
        }
        json_output = {
            "metadata": metadata,
            "results": results,
        }
        _write_output(json.dumps(json_output, indent=2), output, "JSON")
        raise typer.Exit(1 if fail_count > 0 else 0)

    if format == "sarif":
        # Build label → rule mapping for enriched ruleId and metadata
        label_to_rule = {r.get("label", "unknown_rule"): r for r in loaded_rules}

        # Build driver.rules from the full loaded ruleset
        driver_rules = []
        for rule in loaded_rules:
            driver_rule: dict[str, object] = {
                "id": rule.get("id", "unknown_rule"),
                "name": rule.get("label", "unknown_rule"),
                "shortDescription": {
                    "text": rule.get("description", rule.get("label", "unknown_rule"))
                },
                "properties": {
                    "category": rule.get("category", ""),
                    "required": rule.get("required", False),
                },
            }
            hint_url = rule.get("hint_url", "")
            if hint_url:
                driver_rule["helpUri"] = hint_url
            driver_rules.append(driver_rule)

        sarif_results = []
        for section in results:
            for item in section["items"]:
                status = item.get("status")
                if status == "pass":
                    continue
                label = item.get("label", "")
                matched_rule = label_to_rule.get(label)
                rule_id = matched_rule.get("id", label) if matched_rule else label
                level = "error" if status == "fail" else "warning"
                sarif_result: dict[str, object] = {
                    "ruleId": rule_id,
                    "message": {"text": item.get("value", "")},
                    "level": level,
                    "locations": [
                        {
                            "physicalLocation": {
                                "artifactLocation": {
                                    "uri": path.replace("\\", "/").rstrip("/") + "/",
                                    "uriBaseId": "%SRCROOT%",
                                }
                            }
                        }
                    ],
                }
                if item.get("hint"):
                    sarif_result["properties"] = {"hint": item.get("hint", "")}
                sarif_results.append(sarif_result)

        sarif_output = {
            "version": "2.1.0",
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "runs": [
                {
                    "properties": report_properties,
                    "tool": {
                        "driver": {
                            "name": "azure-functions-doctor",
                            "version": __version__,
                            "informationUri": "https://github.com/yeongseon/azure-functions-doctor-python",
                            "rules": driver_rules,
                        }
                    },
                    "results": sarif_results,
                }
            ],
        }
        _write_output(json.dumps(sarif_output, indent=2), output, "SARIF")
        raise typer.Exit(1 if fail_count > 0 else 0)

    if format == "junit":
        import xml.etree.ElementTree as ET  # nosec B405

        tests = 0
        failures = 0
        skipped = 0
        suite = ET.Element(
            "testsuite",
            name="func-doctor",
            tests="0",
            failures="0",
            skipped="0",
            time=f"{duration_ms / 1000:.3f}",
        )

        for section in results:
            for item in section["items"]:
                tests += 1
                case = ET.SubElement(
                    suite, "testcase", classname=section["title"], name=item.get("label", "")
                )
                status = item.get("status")
                if status == "fail":
                    failures += 1
                    failure = ET.SubElement(case, "failure", message=item.get("value", ""))
                    failure.text = item.get("hint", "")
                elif status == "warn":
                    skipped += 1
                    skipped_el = ET.SubElement(case, "skipped", message=item.get("value", ""))
                    skipped_el.text = item.get("hint", "")

        suite.set("tests", str(tests))
        suite.set("failures", str(failures))
        suite.set("skipped", str(skipped))
        junit_output = ET.tostring(suite, encoding="utf-8", xml_declaration=True).decode("utf-8")
        _write_output(junit_output, output, "JUnit")
        raise typer.Exit(1 if fail_count > 0 else 0)

    # Note: Top header removed per UI change; programming model header intentionally omitted

    if debug:
        console.print("[dim]Debug logging enabled - check stderr for detailed logs[/dim]\n")

    # Table-format user-facing output (requested design)
    console.print("Azure Functions Doctor   ")
    console.print(f"Path: {resolved_path}")
    if target_python is not None:
        console.print(f"Target Python: {target_python} (override)")

    # Print each section with simple title and items
    for section in results:
        console.print()
        console.print(section["title"])

        for item in section["items"]:
            label = item.get("label", "")
            value = item.get("value", "")
            status = item.get("status", "pass")
            icon = format_status_icon(status)

            # Compose main line: [ICON] Label: value (status)
            line = Text.assemble((f"[{icon}] ", "bold"), (label, "dim"))
            if value:
                line.append(": ")
                line.append(format_detail(status, value))

            # append status in parentheses for clarity on UI when non-pass
            if status != "pass":
                line.append(f" ({status})", "italic dim")

            console.print(line)

            # show hint as 'fix:' only when verbose is enabled
            if status != "pass" and verbose:
                hint = item.get("hint", "")
                if hint:
                    prefix = "↪ "
                    console.print(f"    {prefix}fix: {hint}")

    # Use the precomputed counts from earlier for final output
    console.print()
    # Print Doctor summary at the bottom like the requested sample
    console.print("Doctor summary (to see all details, run azure-functions doctor -v):")
    # Use singular/plural simple form as in sample (error vs errors)
    # Summary now reflects canonical statuses: fails, warnings, passed
    w_label = "warning" if warning_count == 1 else "warnings"
    f_label = "fail" if fail_count == 1 else "fails"
    # 'passed' label remains same for singular/plural in current design
    console.print(f"  {fail_count} {f_label}, {warning_count} {w_label}, {passed_count} passed")
    exit_code = 1 if fail_count > 0 else 0
    console.print(f"Exit code: {exit_code}")
    if exit_code != 0:
        raise typer.Exit(exit_code)

Doctor

Doctor(path='.', profile=None, rules_path=None, target_python=None)

Diagnostic runner for Azure Functions apps.

Loads checks from the built-in Azure Functions Python v2 rule asset located at azure_functions_doctor.assets.rules.v2.json.

Source code in src/azure_functions_doctor/doctor.py
def __init__(
    self,
    path: str = ".",
    profile: Optional[str] = None,
    rules_path: Optional[Path] = None,
    target_python: Optional[str] = None,
) -> None:
    self.project_path: Path = Path(path).resolve()
    self.profile = profile
    self.target_python: Optional[str] = target_python
    self.rules_path: Optional[Path] = None
    if rules_path is not None:
        resolved = rules_path.resolve()
        if not resolved.is_file():
            raise ValueError(f"rules_path must be an existing file: {resolved}")
        self.rules_path = resolved
    self.programming_model: ProgrammingModel = self._detect_programming_model()

get_report_properties()

Return top-level report properties shared across output formats.

Source code in src/azure_functions_doctor/doctor.py
def get_report_properties(self) -> dict[str, Optional[str]]:
    """Return top-level report properties shared across output formats."""
    return {
        "programming_model": self.programming_model,
        "target_python": self.target_python,
    }

load_rules()

Load and validate rules from a custom path or the built-in v2 ruleset.

Source code in src/azure_functions_doctor/doctor.py
def load_rules(self) -> list[Rule]:
    """Load and validate rules from a custom path or the built-in v2 ruleset."""
    if self.rules_path is not None:
        with self.rules_path.open(encoding="utf-8") as f:
            rules: list[Rule] = json.load(f)
    else:
        rules = self._load_v2_rules()

    self._validate_rules(rules)
    return sorted(rules, key=lambda r: r.get("check_order", 999))

Handlers

Diagnostic check handlers for Azure Functions Doctor.

This package was split from a single handlers.py module. The public API is preserved: import from azure_functions_doctor.handlers exactly as before. Implementation lives in :mod:._helpers (pure helpers and types) and :mod:.registry (the HandlerRegistry dispatch class).

HandlerRegistry()

Registry for diagnostic check handlers with individual handler methods.

Source code in src/azure_functions_doctor/handlers/registry.py
def __init__(self) -> None:
    self._handlers: Dict[
        str, Callable[[Rule, Path, Optional[RuleContext]], HandlerResult]
    ] = {
        check_type: getattr(self, method_name)
        for check_type, method_name in _RULE_DISPATCH.items()
    }

handle(rule, path, context=None)

Route rule execution to appropriate handler.

Source code in src/azure_functions_doctor/handlers/registry.py
def handle(
    self, rule: Rule, path: Path, context: Optional[RuleContext] = None
) -> HandlerResult:
    """Route rule execution to appropriate handler."""
    check_type = rule.get("type")
    if check_type is None:
        return _create_result("fail", "Missing check type in rule")
    handler = self._handlers.get(check_type)

    if not handler:
        return _create_result("fail", f"Unknown check type: {check_type}")

    try:
        return handler(rule, path, context)
    except Exception as exc:
        return _handle_specific_exceptions(f"executing {check_type} check", exc)

generic_handler(rule, path, context=None)

Execute a diagnostic rule based on its type and condition.

This function maintains backward compatibility while delegating to the registry.

Parameters:

Name Type Description Default
rule Rule

The diagnostic rule to execute.

required
path Path

Path to the Azure Functions project.

required

Returns:

Type Description
HandlerResult

A dictionary with the status and detail of the check.

Source code in src/azure_functions_doctor/handlers/registry.py
def generic_handler(
    rule: Rule, path: Path, context: Optional[RuleContext] = None
) -> HandlerResult:
    """
    Execute a diagnostic rule based on its type and condition.

    This function maintains backward compatibility while delegating to the registry.

    Args:
        rule: The diagnostic rule to execute.
        path: Path to the Azure Functions project.

    Returns:
        A dictionary with the status and detail of the check.
    """
    return _registry.handle(rule, path, context)

resolve_target_value(target, override=None)

Resolve the current value of a target used in version comparison or diagnostics.

Parameters:

Name Type Description Default
target str

The name of the target to resolve. Examples include "python" or "func_core_tools".

required

Returns:

Type Description
str

A string representing the resolved version or value.

Raises:

Type Description
ValueError

If the target is not recognized.

Source code in src/azure_functions_doctor/target_resolver.py
def resolve_target_value(target: str, override: Optional[str] = None) -> str:
    """
    Resolve the current value of a target used in version comparison or diagnostics.

    Args:
        target: The name of the target to resolve. Examples include "python" or "func_core_tools".

    Returns:
        A string representing the resolved version or value.

    Raises:
        ValueError: If the target is not recognized.
    """
    resolver = _TARGET_RESOLVERS.get(target)
    if resolver is None:
        raise ValueError(f"Unknown target: {target}")
    return resolver(override)

Target Resolver

resolve_target_value(target, override=None)

Resolve the current value of a target used in version comparison or diagnostics.

Parameters:

Name Type Description Default
target str

The name of the target to resolve. Examples include "python" or "func_core_tools".

required

Returns:

Type Description
str

A string representing the resolved version or value.

Raises:

Type Description
ValueError

If the target is not recognized.

Source code in src/azure_functions_doctor/target_resolver.py
def resolve_target_value(target: str, override: Optional[str] = None) -> str:
    """
    Resolve the current value of a target used in version comparison or diagnostics.

    Args:
        target: The name of the target to resolve. Examples include "python" or "func_core_tools".

    Returns:
        A string representing the resolved version or value.

    Raises:
        ValueError: If the target is not recognized.
    """
    resolver = _TARGET_RESOLVERS.get(target)
    if resolver is None:
        raise ValueError(f"Unknown target: {target}")
    return resolver(override)

Utility

format_detail(status, value)

Return a colored Text element based on status and value.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required
value str

Text to display, typically a description.

required

Returns:

Type Description
Text

A Rich Text object styled with status color.

Source code in src/azure_functions_doctor/utils.py
def format_detail(status: str, value: str) -> Text:
    """
    Return a colored Text element based on status and value.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").
        value: Text to display, typically a description.

    Returns:
        A Rich Text object styled with status color.
    """
    color = DETAIL_COLOR_MAP.get(status, "white")
    return Text(value, style=color)

format_result(status)

Return a styled icon Text element based on status.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required

Returns:

Type Description
Text

A Rich Text object with icon and style for headers.

Source code in src/azure_functions_doctor/utils.py
def format_result(status: str) -> Text:
    """
    Return a styled icon Text element based on status.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").

    Returns:
        A Rich Text object with icon and style for headers.
    """
    style = STATUS_STYLES.get(status, Style(color="white"))
    icon = format_status_icon(status)
    return Text(icon, style=style)

format_status_icon(status)

Return a simple icon character based on status.

Parameters:

Name Type Description Default
status str

Diagnostic status ("pass", "fail", "warn").

required

Returns: A string icon such as ✓, !, or ✗.

Source code in src/azure_functions_doctor/utils.py
def format_status_icon(status: str) -> str:
    """
    Return a simple icon character based on status.

    Args:
        status: Diagnostic status ("pass", "fail", "warn").

    Returns:
    A string icon such as ✓, !, or ✗.
    """
    return STATUS_ICONS.get(status, "?")