Skip to content

🧠 API Reference

CLI

diagnose(path='.', verbose=False, format='table', output=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 bool

Show detailed hints for failed checks.

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

Output format: 'table' or 'json'.

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

Optional file path to save JSON result.

None
Source code in src/azure_functions_doctor/cli.py
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
@cli.command()
def diagnose(
    path: str = ".",
    verbose: bool = False,
    format: Annotated[str, typer.Option(help="Output format: 'table' or 'json'")] = "table",
    output: Annotated[Optional[Path], typer.Option(help="Optional path to save JSON result")] = 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.
        format: Output format: 'table' or 'json'.
        output: Optional file path to save JSON result.
    """
    doctor = Doctor(path)
    results = doctor.run_all_checks()

    passed = failed = 0

    if format == "json":
        import json

        json_output = results

        if output:
            output.parent.mkdir(parents=True, exist_ok=True)
            output.write_text(json.dumps(json_output, indent=2), encoding="utf-8")
            console.print(f"[green]✓ JSON output saved to:[/green] {output}")
        else:
            print(json.dumps(json_output, indent=2))
        return

    # Print header only for table format
    console.print(f"[bold blue]🩺 Azure Functions Doctor for Python v{__version__}[/bold blue]")
    console.print(f"[bold]📁 Path:[/bold] {Path(path).resolve()}\n")

    # Default: table format
    for section in results:
        console.print(Text.assemble("\n", format_result(section["status"]), " ", (section["title"], "bold")))

        if section["status"] == "pass":
            passed += 1
        else:
            failed += 1

        for item in section["items"]:
            label = item["label"]
            value = item["value"]
            status = item["status"]

            line = Text.assemble(
                ("  • ", "default"),
                (label, "dim"),
                (": ", "default"),
                format_detail(status, value),
            )
            console.print(line)

            if verbose and status != "pass":
                if item.get("hint"):
                    console.print(f"    ↪ [yellow]{item['hint']}[/yellow]")
                hint_url = item.get("hint_url", "")
                if hint_url.strip():
                    console.print(f"    📚 [blue]{hint_url}[/blue]")

    # ✅ Summary section
    console.print()
    console.print("[bold]Summary[/bold]")
    summary = Text.assemble(
        (f"{format_status_icon('pass')} ", "green bold"),
        (f"{passed} Passed    ", "bold"),
        (f"{format_status_icon('fail')} ", "red bold"),
        (f"{failed} Failed", "bold"),
    )
    console.print(summary)

Doctor

Doctor(path='.')

Diagnostic runner for Azure Functions apps. Loads checks from rules.json and executes them against a target project path.

Source code in src/azure_functions_doctor/doctor.py
31
32
def __init__(self, path: str = ".") -> None:
    self.project_path: Path = Path(path).resolve()

Handlers

generic_handler(rule, path)

Execute a diagnostic rule based on its type and condition.

Parameters:

Name Type Description Default
rule Rule

The diagnostic rule to execute.

required

Returns:

Type Description
dict[str, str]

A dictionary with the status and detail of the check.

Source code in src/azure_functions_doctor/handlers.py
 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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def generic_handler(rule: Rule, path: Path) -> dict[str, str]:
    """
    Execute a diagnostic rule based on its type and condition.

    Args:
        rule: The diagnostic rule to execute.

    Returns:
        A dictionary with the status and detail of the check.
    """
    check_type = rule.get("type")
    condition = rule.get("condition", {})

    target = condition.get("target")
    operator = condition.get("operator")
    value = condition.get("value")

    # Compare current Python version with expected version
    if check_type == "compare_version":
        if not (target and operator and value):
            return {"status": "fail", "detail": "Missing condition fields for compare_version"}

        if target == "python":
            current_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
            current = parse_version(current_version)
            expected = parse_version(str(value))
            passed = {
                ">=": current >= expected,
                "<=": current <= expected,
                "==": current == expected,
                ">": current > expected,
                "<": current < expected,
            }.get(operator, False)
            return {
                "status": "pass" if passed else "fail",
                "detail": f"Python version is {current_version}, expected {operator}{value}",
            }

        return {"status": "fail", "detail": f"Unknown target for version comparison: {target}"}

    # Check if an environment variable is set
    if check_type == "env_var_exists":
        if not target:
            return {"status": "fail", "detail": "Missing environment variable name"}

        exists = os.getenv(target) is not None
        return {
            "status": "pass" if exists else "fail",
            "detail": f"{target} is {'set' if exists else 'not set'}",
        }

    # Check if a path exists (including sys.executable)
    if check_type == "path_exists":
        if not target:
            return {"status": "fail", "detail": "Missing target path"}

        resolved_path = sys.executable if target == "sys.executable" else os.path.join(path, target)
        exists = os.path.exists(resolved_path)

        if exists:
            return {"status": "pass", "detail": f"{resolved_path} exists"}

        if not rule.get("required", True):
            return {"status": "pass", "detail": f"{resolved_path} is missing (optional)"}

        return {"status": "fail", "detail": f"{resolved_path} is missing"}

    # Check if a specific file exists
    if check_type == "file_exists":
        if not target:
            return {"status": "fail", "detail": "Missing file path"}

        file_path = os.path.join(path, target)
        exists = os.path.isfile(file_path)

        if exists:
            return {"status": "pass", "detail": f"{file_path} exists"}

        if not rule.get("required", True):
            return {"status": "pass", "detail": f"{file_path} not found (optional for local development)"}

        return {"status": "fail", "detail": f"{file_path} not found"}

    # Check if a Python package is importable
    if check_type == "package_installed":
        if not target:
            return {"status": "fail", "detail": "Missing package name"}

        import_path_str: str = str(target)

        try:
            __import__(import_path_str)
            found = True
            error_msg = ""
        except ImportError as exc:
            found = False
            error_msg = f": {exc}"

        return {
            "status": "pass" if found else "fail",
            "detail": f"Module '{import_path_str}' is {'installed' if found else f'not installed{error_msg}'}",
        }

    # Check if a keyword exists in any .py source files
    if check_type == "source_code_contains":
        keyword = condition.get("keyword")
        if not isinstance(keyword, str):
            return {
                "status": "fail",
                "detail": "Missing or invalid 'keyword' in condition",
            }

        found = False

        for py_file in path.rglob("*.py"):
            try:
                content = py_file.read_text(encoding="utf-8")
                if keyword in content:
                    found = True
                    break
            except Exception:
                continue

        return {
            "status": "pass" if found else "fail",
            "detail": f"Keyword '{keyword}' {'found' if found else 'not found'} in source code",
        }

    # Unknown check type fallback
    return {"status": "fail", "detail": f"Unknown check type: {check_type}"}

Check Logic

run_check(rule, base_path)

Wrap the generic_handler to cast a raw rule into a typed Rule.

Parameters:

Name Type Description Default
rule dict[str, Any]

Dictionary parsed from rules.json

required
base_path Path

Path to Azure Functions app

required

Returns:

Type Description
CheckResult

Structured result with status, label, value, and optional hint.

Source code in src/azure_functions_doctor/check.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def run_check(rule: dict[str, Any], base_path: Path) -> CheckResult:
    """
    Wrap the generic_handler to cast a raw rule into a typed Rule.

    Args:
        rule: Dictionary parsed from rules.json
        base_path: Path to Azure Functions app

    Returns:
        Structured result with status, label, value, and optional hint.
    """
    typed_rule = cast(Rule, rule)
    result = generic_handler(typed_rule, base_path)

    output: CheckResult = {
        "status": result["status"],
        "label": typed_rule.get("label", typed_rule["id"]),
        "value": result["detail"],
    }

    if "hint" in typed_rule:
        output["hint"] = typed_rule["hint"]

    return output

Target Resolver

resolve_target_value(target)

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
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def resolve_target_value(target: str) -> 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.
    """
    if target == "python":
        return sys.version.split()[0]
    if target == "func_core_tools":
        try:
            output = subprocess.check_output(["func", "--version"], text=True)
            return output.strip()
        except Exception:
            return "0.0.0"  # Return a fallback version if resolution fails
    raise ValueError(f"Unknown target: {target}")

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
54
55
56
57
58
59
60
61
62
63
64
65
66
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
39
40
41
42
43
44
45
46
47
48
49
50
51
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:

Type Description
str

A string icon such as ✔, ✖, or ⚠.

Source code in src/azure_functions_doctor/utils.py
26
27
28
29
30
31
32
33
34
35
36
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, "?")