Skip to content

depdetect API Reference

Auto-generated API reference for the depdetect module.

depdetect

Dependency detection and verification — zero dependencies, stdlib only, Python 3.10+.

Part of zerodep: https://github.com/Oaklight/zerodep Copyright (c) 2026 Peng Ding. MIT License.

Parse dependency information from Python source code, requirements files, and free-text compatibility strings. Verify that binaries, Python packages, and environment variables are present on the current system.

Analyze imports in Python source::

from depdetect import parse_imports, analyze_source

# Raw import names (third-party only, stdlib filtered out)
imports = parse_imports("import requests\nimport os\n")
# → {"requests"}

# Resolved to pip-installable names
pip_names = analyze_source("import yaml\nimport PIL\n")
# → {"pyyaml", "pillow"}

Check system dependencies::

from depdetect import check_binary, check_python_package, get_binary_version

path = check_binary("git")          # "/usr/bin/git" or None
ver = get_binary_version("git")     # "2.43.0" or None
ok = check_python_package("requests")  # True / False

Parse and verify requirements::

from depdetect import parse_requirements, check_requirements

reqs = parse_requirements("requests>=2.28\npillow>=10.0\n")
report = check_requirements(reqs)
print(report.summary())

Requires Python 3.10+.

DepdetectError

Bases: Exception

Base exception for dependency detection errors.

Source code in depdetect/depdetect.py
class DepdetectError(Exception):
    """Base exception for dependency detection errors."""

Requirement dataclass

A parsed dependency requirement.

Attributes:

Name Type Description
name str

Package or binary name.

category str

One of "binary", "python", "env", "runtime".

op str | None

Version comparison operator (">=", "==", etc.) or None.

version str | None

Required version string, or None.

extras str | None

Extras specifier for Python packages (e.g. "security").

Source code in depdetect/depdetect.py
@dataclass(frozen=True, slots=True)
class Requirement:
    """A parsed dependency requirement.

    Attributes:
        name: Package or binary name.
        category: One of ``"binary"``, ``"python"``, ``"env"``, ``"runtime"``.
        op: Version comparison operator (``">="``, ``"=="``, etc.) or ``None``.
        version: Required version string, or ``None``.
        extras: Extras specifier for Python packages (e.g. ``"security"``).
    """

    name: str
    category: str = "python"
    op: str | None = None
    version: str | None = None
    extras: str | None = None

DependencyStatus dataclass

Result of checking a single dependency.

Attributes:

Name Type Description
name str

Dependency name.

category str

One of "binary", "python", "env", "runtime".

required str | None

Version constraint string (e.g. ">=3.10"), or None.

found bool

Whether the dependency was found.

found_version str | None

Detected version string, or None.

path str | None

Binary path for "binary" category, or None.

message str

Human-readable status description.

Source code in depdetect/depdetect.py
@dataclass(frozen=True, slots=True)
class DependencyStatus:
    """Result of checking a single dependency.

    Attributes:
        name: Dependency name.
        category: One of ``"binary"``, ``"python"``, ``"env"``, ``"runtime"``.
        required: Version constraint string (e.g. ``">=3.10"``), or ``None``.
        found: Whether the dependency was found.
        found_version: Detected version string, or ``None``.
        path: Binary path for ``"binary"`` category, or ``None``.
        message: Human-readable status description.
    """

    name: str
    category: str
    required: str | None
    found: bool
    found_version: str | None = None
    path: str | None = None
    message: str = ""

DependencyReport dataclass

Aggregated results for multiple dependency checks.

Attributes:

Name Type Description
dependencies list[DependencyStatus]

List of individual dependency check results.

Source code in depdetect/depdetect.py
@dataclass
class DependencyReport:
    """Aggregated results for multiple dependency checks.

    Attributes:
        dependencies: List of individual dependency check results.
    """

    dependencies: list[DependencyStatus] = field(default_factory=list)

    @property
    def satisfied(self) -> bool:
        """Whether all dependencies are found and version-compatible."""
        return all(d.found for d in self.dependencies)

    @property
    def missing(self) -> list[DependencyStatus]:
        """Dependencies that were not found or version-incompatible."""
        return [d for d in self.dependencies if not d.found]

    def summary(self) -> str:
        """Human-readable summary of all dependency checks."""
        lines: list[str] = []
        for d in self.dependencies:
            status = "OK" if d.found else "MISSING"
            ver = f" ({d.found_version})" if d.found_version else ""
            msg = f": {d.message}" if d.message else ""
            lines.append(f"[{status}] {d.name}{ver}{msg}")
        total = len(self.dependencies)
        ok = total - len(self.missing)
        lines.append(f"\n{ok}/{total} dependencies satisfied.")
        return "\n".join(lines)

satisfied property

Whether all dependencies are found and version-compatible.

missing property

Dependencies that were not found or version-incompatible.

summary()

Human-readable summary of all dependency checks.

Source code in depdetect/depdetect.py
def summary(self) -> str:
    """Human-readable summary of all dependency checks."""
    lines: list[str] = []
    for d in self.dependencies:
        status = "OK" if d.found else "MISSING"
        ver = f" ({d.found_version})" if d.found_version else ""
        msg = f": {d.message}" if d.message else ""
        lines.append(f"[{status}] {d.name}{ver}{msg}")
    total = len(self.dependencies)
    ok = total - len(self.missing)
    lines.append(f"\n{ok}/{total} dependencies satisfied.")
    return "\n".join(lines)

parse_imports(source, *, file_path=None)

Extract third-party import names from Python source code.

Parses the source with :mod:ast and collects all import and from ... import statements. Standard library modules (detected via sys.stdlib_module_names) are filtered out, and only top-level package names are returned.

Parameters:

Name Type Description Default
source str

Python source code string.

required
file_path str | None

Optional file path for error messages.

None

Returns:

Type Description
set[str]

Set of top-level third-party package names (import names, not

set[str]

pip names). Use :func:resolve_pip_name to convert.

Raises:

Type Description
DepdetectError

If the source cannot be parsed.

Example::

parse_imports("import requests\nfrom os.path import join\n")
# → {"requests"}
Source code in depdetect/depdetect.py
def parse_imports(
    source: str,
    *,
    file_path: str | None = None,
) -> set[str]:
    """Extract third-party import names from Python source code.

    Parses the source with :mod:`ast` and collects all ``import`` and
    ``from ... import`` statements.  Standard library modules (detected
    via ``sys.stdlib_module_names``) are filtered out, and only top-level
    package names are returned.

    Args:
        source: Python source code string.
        file_path: Optional file path for error messages.

    Returns:
        Set of top-level third-party package names (import names, not
        pip names).  Use :func:`resolve_pip_name` to convert.

    Raises:
        DepdetectError: If the source cannot be parsed.

    Example::

        parse_imports("import requests\\nfrom os.path import join\\n")
        # → {"requests"}
    """
    try:
        tree = ast.parse(source, filename=file_path or "<string>")
    except SyntaxError as exc:
        src = file_path or "<string>"
        raise DepdetectError(f"cannot parse {src}: {exc}") from exc

    stdlib = getattr(sys, "stdlib_module_names", frozenset())
    imports: set[str] = set()

    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            _collect_from_import(node, stdlib, imports)
        elif isinstance(node, ast.ImportFrom):
            _collect_from_import_from(node, stdlib, imports)

    return imports

parse_requirements(text)

Parse pip requirements.txt format into :class:Requirement objects.

Handles version constraints (>=, ==, ~=, etc.), extras (package[extra]), comments, blank lines, and skips -r/-e directives.

Parameters:

Name Type Description Default
text str

Contents of a requirements.txt file.

required

Returns:

Type Description
list[Requirement]

List of parsed requirements with category="python".

Example::

parse_requirements("requests>=2.28.0\npillow>=10.0\n")
# → [Requirement("requests", "python", ">=", "2.28.0"),
#    Requirement("pillow", "python", ">=", "10.0")]
Source code in depdetect/depdetect.py
def parse_requirements(text: str) -> list[Requirement]:
    """Parse pip requirements.txt format into :class:`Requirement` objects.

    Handles version constraints (``>=``, ``==``, ``~=``, etc.), extras
    (``package[extra]``), comments, blank lines, and skips ``-r``/``-e``
    directives.

    Args:
        text: Contents of a requirements.txt file.

    Returns:
        List of parsed requirements with ``category="python"``.

    Example::

        parse_requirements("requests>=2.28.0\\npillow>=10.0\\n")
        # → [Requirement("requests", "python", ">=", "2.28.0"),
        #    Requirement("pillow", "python", ">=", "10.0")]
    """
    results: list[Requirement] = []
    for raw_line in text.splitlines():
        line = raw_line.strip()
        # Skip empty lines, comments, -r/-c includes, -e editable installs
        if not line or line.startswith("#") or line.startswith("-"):
            continue
        m = _REQ_RE.match(line)
        if m:
            name = m.group(1)
            extras = m.group(2)
            op = m.group(3)
            version = m.group(4)
            results.append(
                Requirement(
                    name=name,
                    category="python",
                    op=op,
                    version=version,
                    extras=extras,
                )
            )
    return results

parse_compatibility(text)

Parse free-text compatibility notes into structured requirements.

Best-effort extraction from strings like "Python 3.10+, Node.js >= 18, pandoc >= 3.0". Noise words ("or", "and", "requires", etc.) are filtered out.

Parameters:

Name Type Description Default
text str

Compatibility string (typically from SKILL.md frontmatter).

required

Returns:

Type Description
list[Requirement]

List of parsed requirements. May be incomplete for ambiguous input.

Example::

parse_compatibility("Python 3.10+, pandoc >= 3.0")
# → [Requirement("Python", "runtime", ">=", "3.10"),
#    Requirement("pandoc", "binary", ">=", "3.0")]
Source code in depdetect/depdetect.py
def parse_compatibility(text: str) -> list[Requirement]:
    """Parse free-text compatibility notes into structured requirements.

    Best-effort extraction from strings like
    ``"Python 3.10+, Node.js >= 18, pandoc >= 3.0"``.  Noise words
    (``"or"``, ``"and"``, ``"requires"``, etc.) are filtered out.

    Args:
        text: Compatibility string (typically from SKILL.md frontmatter).

    Returns:
        List of parsed requirements.  May be incomplete for ambiguous input.

    Example::

        parse_compatibility("Python 3.10+, pandoc >= 3.0")
        # → [Requirement("Python", "runtime", ">=", "3.10"),
        #    Requirement("pandoc", "binary", ">=", "3.0")]
    """
    results: list[Requirement] = []
    for m in _COMPAT_RE.finditer(text):
        name = m.group(1)
        # Skip noise words
        if name.lower() in _COMPAT_NOISE:
            continue
        op = m.group(2)
        version = m.group(3)
        # If there's a trailing + in the original text and no explicit op,
        # treat as >=
        if version and not op:
            # Check if there's a + right after the version in the source
            end = m.end()
            if end <= len(text) and text[m.start(3) : end].endswith("+"):
                pass  # the + was already stripped by regex
            op = ">="

        # Infer category
        category = "runtime" if name.lower() in _RUNTIME_NAMES else "binary"

        results.append(
            Requirement(name=name, category=category, op=op, version=version)
        )
    return results

parse_tool_hints(allowed_tools)

Extract binary name hints from an allowed-tools string.

Parses parenthesized parameters like Bash(git:* npm:*) to extract ["git", "npm"] as binaries the skill expects to use via the shell.

Parameters:

Name Type Description Default
allowed_tools str

The allowed-tools field value from SKILL.md.

required

Returns:

Type Description
list[str]

List of binary names found in tool parameters.

Example::

parse_tool_hints("Bash(git:* docker:*) Read Write")
# → ["git", "docker"]
Source code in depdetect/depdetect.py
def parse_tool_hints(allowed_tools: str) -> list[str]:
    """Extract binary name hints from an ``allowed-tools`` string.

    Parses parenthesized parameters like ``Bash(git:* npm:*)`` to extract
    ``["git", "npm"]`` as binaries the skill expects to use via the shell.

    Args:
        allowed_tools: The ``allowed-tools`` field value from SKILL.md.

    Returns:
        List of binary names found in tool parameters.

    Example::

        parse_tool_hints("Bash(git:* docker:*) Read Write")
        # → ["git", "docker"]
    """
    binaries: list[str] = []
    for paren_match in _TOOL_HINT_PAREN_RE.finditer(allowed_tools):
        content = paren_match.group(1)
        for bin_match in _TOOL_HINT_BINARY_RE.finditer(content):
            binaries.append(bin_match.group(1))
    return binaries

resolve_pip_name(import_name)

Resolve a Python import name to its pip distribution name.

Uses a three-level fallback strategy:

  1. Installed metadata — queries importlib.metadata for the exact mapping from installed packages (cached after first call).
  2. Static mapping — covers ~30 high-frequency mismatches (e.g. PILpillow, yamlpyyaml).
  3. Heuristic — replaces underscores with hyphens, which covers the majority of conventional packages.

Parameters:

Name Type Description Default
import_name str

The name used in import statements.

required

Returns:

Type Description
str

The pip-installable package name.

Example::

resolve_pip_name("PIL")     # → "pillow"
resolve_pip_name("yaml")    # → "pyyaml"
resolve_pip_name("dotenv")  # → "python-dotenv"
Source code in depdetect/depdetect.py
def resolve_pip_name(import_name: str) -> str:
    """Resolve a Python import name to its pip distribution name.

    Uses a three-level fallback strategy:

    1. **Installed metadata** — queries ``importlib.metadata`` for the
       exact mapping from installed packages (cached after first call).
    2. **Static mapping** — covers ~30 high-frequency mismatches
       (e.g. ``PIL`` → ``pillow``, ``yaml`` → ``pyyaml``).
    3. **Heuristic** — replaces underscores with hyphens, which covers
       the majority of conventional packages.

    Args:
        import_name: The name used in ``import`` statements.

    Returns:
        The pip-installable package name.

    Example::

        resolve_pip_name("PIL")     # → "pillow"
        resolve_pip_name("yaml")    # → "pyyaml"
        resolve_pip_name("dotenv")  # → "python-dotenv"
    """
    # Level 1: dynamic metadata lookup
    cache = _get_import_to_pip_cache()
    if import_name in cache:
        return cache[import_name]

    # Level 2: static mapping
    if import_name in _IMPORT_TO_PIP:
        return _IMPORT_TO_PIP[import_name]

    # Level 3: heuristic
    return import_name.replace("_", "-")

check_binary(name)

Check if a binary is available on the system PATH.

Resolves aliases (e.g. "node.js""node") and returns the first matching path. Uses :func:shutil.which which handles Windows PATHEXT automatically.

Parameters:

Name Type Description Default
name str

Binary name or alias.

required

Returns:

Type Description
str | None

Absolute path to the binary, or None if not found.

Source code in depdetect/depdetect.py
def check_binary(name: str) -> str | None:
    """Check if a binary is available on the system PATH.

    Resolves aliases (e.g. ``"node.js"`` → ``"node"``) and returns
    the first matching path.  Uses :func:`shutil.which` which handles
    Windows ``PATHEXT`` automatically.

    Args:
        name: Binary name or alias.

    Returns:
        Absolute path to the binary, or ``None`` if not found.
    """
    for candidate in _resolve_binary_candidates(name):
        path = shutil.which(candidate)
        if path:
            return path
    return None

check_python_package(name)

Check if a Python package is importable in the current environment.

Uses :func:importlib.util.find_spec to check availability without triggering the actual import. Handles pip-to-import name mapping (e.g. "pillow""PIL").

Parameters:

Name Type Description Default
name str

Package name (pip distribution name or import name).

required

Returns:

Type Description
bool

True if the package can be imported.

Source code in depdetect/depdetect.py
def check_python_package(name: str) -> bool:
    """Check if a Python package is importable in the current environment.

    Uses :func:`importlib.util.find_spec` to check availability without
    triggering the actual import.  Handles pip-to-import name mapping
    (e.g. ``"pillow"`` → ``"PIL"``).

    Args:
        name: Package name (pip distribution name or import name).

    Returns:
        ``True`` if the package can be imported.
    """
    import_name = _resolve_import_name(name)
    try:
        return importlib.util.find_spec(import_name) is not None
    except (ModuleNotFoundError, ValueError):
        return False

check_env_var(name)

Check if an environment variable is set and non-empty.

Parameters:

Name Type Description Default
name str

Environment variable name.

required

Returns:

Type Description
bool

True if the variable is set and has a non-empty value.

Source code in depdetect/depdetect.py
def check_env_var(name: str) -> bool:
    """Check if an environment variable is set and non-empty.

    Args:
        name: Environment variable name.

    Returns:
        ``True`` if the variable is set and has a non-empty value.
    """
    return bool(os.environ.get(name))

get_binary_version(name)

Get the version string of an installed binary.

Runs the binary with --version (or an appropriate flag) and extracts a dotted version number from the output.

Parameters:

Name Type Description Default
name str

Binary name or alias.

required

Returns:

Type Description
str | None

Version string (e.g. "2.43.0"), or None if the binary

str | None

is not found or the version cannot be determined.

Source code in depdetect/depdetect.py
def get_binary_version(name: str) -> str | None:
    """Get the version string of an installed binary.

    Runs the binary with ``--version`` (or an appropriate flag) and
    extracts a dotted version number from the output.

    Args:
        name: Binary name or alias.

    Returns:
        Version string (e.g. ``"2.43.0"``), or ``None`` if the binary
        is not found or the version cannot be determined.
    """
    path = check_binary(name)
    if not path:
        return None
    return _run_version_cmd(path, name)

check_requirements(requirements)

Check a list of requirements against the current system.

Dispatches each requirement to the appropriate detection function based on its :attr:~Requirement.category, performs version comparison if a constraint is specified, and aggregates results into a report.

Parameters:

Name Type Description Default
requirements list[Requirement]

List of requirements to check.

required

Returns:

Name Type Description
A DependencyReport

class:DependencyReport with one :class:DependencyStatus

DependencyReport

per requirement.

Example::

reqs = [
    Requirement("git", "binary"),
    Requirement("requests", "python", ">=", "2.28"),
]
report = check_requirements(reqs)
if not report.satisfied:
    print(report.summary())
Source code in depdetect/depdetect.py
def check_requirements(requirements: list[Requirement]) -> DependencyReport:
    """Check a list of requirements against the current system.

    Dispatches each requirement to the appropriate detection function
    based on its :attr:`~Requirement.category`, performs version comparison
    if a constraint is specified, and aggregates results into a report.

    Args:
        requirements: List of requirements to check.

    Returns:
        A :class:`DependencyReport` with one :class:`DependencyStatus`
        per requirement.

    Example::

        reqs = [
            Requirement("git", "binary"),
            Requirement("requests", "python", ">=", "2.28"),
        ]
        report = check_requirements(reqs)
        if not report.satisfied:
            print(report.summary())
    """
    statuses: list[DependencyStatus] = []
    for req in requirements:
        statuses.append(_check_one(req))
    return DependencyReport(dependencies=statuses)

analyze_source(source, *, file_path=None)

Analyze Python source code for third-party dependencies.

Combines :func:parse_imports (AST-based import extraction) with :func:resolve_pip_name (import→pip name resolution) to produce a set of pip-installable package names.

Parameters:

Name Type Description Default
source str

Python source code string.

required
file_path str | None

Optional path for error messages.

None

Returns:

Type Description
set[str]

Set of pip-installable package names.

Raises:

Type Description
DepdetectError

If the source cannot be parsed.

Example::

analyze_source("import yaml\nfrom PIL import Image\n")
# → {"pyyaml", "pillow"}
Source code in depdetect/depdetect.py
def analyze_source(
    source: str,
    *,
    file_path: str | None = None,
) -> set[str]:
    """Analyze Python source code for third-party dependencies.

    Combines :func:`parse_imports` (AST-based import extraction) with
    :func:`resolve_pip_name` (import→pip name resolution) to produce
    a set of pip-installable package names.

    Args:
        source: Python source code string.
        file_path: Optional path for error messages.

    Returns:
        Set of pip-installable package names.

    Raises:
        DepdetectError: If the source cannot be parsed.

    Example::

        analyze_source("import yaml\\nfrom PIL import Image\\n")
        # → {"pyyaml", "pillow"}
    """
    imports = parse_imports(source, file_path=file_path)
    return {resolve_pip_name(name) for name in imports}