Skip to main content

Enforces the presence and correctness of __all__ in Python modules

Project description

sheridan-iceberg

CI PyPI Coverage Mutation Score License: MIT Python 3.14+

The public API is the tip of the iceberg. iceberg guards the waterline.

sheridan-iceberg analyzes Python modules and enforces the presence and correctness of __all__. It uses Python's ast module for static analysis — no importing of user code.

Features

  • show — inspect and report the effective public API of any module or project; includes function signatures (parameter names, types, defaults) and class member surfaces (attributes, properties, methods); uses __all__ when present, falls back to AST inference; --use-ast forces AST-only regardless of __all__
  • check — enforce __all__ correctness; IB002 is one-directional (names that appear public in the AST but are absent from __all__), so deliberate re-exports are never flagged as phantom names
  • fix — auto-repair __all__ in place; uses full bidirectional comparison, removing phantom exports as well as adding missing ones
  • Walks a Python project's modules via AST (safe, no imports)
  • Uses __all__ as the authoritative public API surface when present; falls back to inferring non-underscore top-level names when absent
  • Machine-readable JSON output and human-readable text/tree output
  • Works as a pre-commit hook, CLI tool, or GitHub Action

Installation

pip install sheridan-iceberg

Usage

# Report the public API of a project
iceberg show src/

# Show as JSON (machine-readable)
iceberg show src/ --format json

# Ignore __all__ entirely — always use AST inference
iceberg show src/ --use-ast

# Check __all__ declarations against the AST
iceberg check src/

# Suppress IB001 (missing __all__) — only report IB002 and IB003
iceberg check src/ --ignore-missing

# Check with JSON output
iceberg check src/ --format json

# Auto-fix __all__ declarations (bidirectional — also removes phantom exports)
iceberg fix src/

# Preview what fix would change without writing
iceberg fix src/ --dry-run

Example output

iceberg show produces an indented tree by default. Functions are shown with their full signatures; classes are followed by an indented list of their public members. When __init__.py declares __all__, it is the source of truth for the whole package — only that module is shown:

# iceberg show src/mypackage/

mypackage/
  __init__
    Role
      name: str
      level: int
      permissions (property) → list[str]
      classmethod create(cls, name: str, level: int = ...) → Role
      promote(self) → None
    User
      email: str
      role: Role
      is_active: bool
      save(self) → None
    helper(path: Path) → list[str]

Pass --use-ast to bypass __all__ and see every module's inferred names:

# iceberg show src/mypackage/ --use-ast

mypackage/
  __init__
    Role
      ...
    User
      ...
    helper(path: Path) → list[str]
  core
    Alpha
    Beta
    Gamma
  utils
    helper(path: Path) → list[str]
    parse(text: str, strict: bool = ...) → dict[str, object]

iceberg check reports violations:

src/mypackage/utils.py: IB001 missing __all__ (expected ['helper', 'parse'])
src/mypackage/models.py: IB002 names appear public but missing from __all__: ['Role']
src/mypackage/core.py: IB003 __all__ is not sorted (expected ['Alpha', 'Beta', 'Gamma'])

Exit codes:

  • show: 0 always (path existence aside)
  • check: 0 no issues, 1 issues found, 2 path not found
  • fix: 0 success, 2 path not found

JSON output

iceberg show --format json:

[
  {
    "module": "mypackage.utils",
    "path": "src/mypackage/utils.py",
    "source": "ast",
    "names": ["helper", "parse"],
    "detail": {
      "helper": {
        "kind": "function",
        "signature": {
          "params": [
            {"name": "path", "annotation": "Path", "has_default": false, "kind": "positional_or_keyword"}
          ],
          "return_annotation": "list[str]",
          "is_async": false
        }
      },
      "parse": {
        "kind": "function",
        "signature": {
          "params": [
            {"name": "text", "annotation": "str", "has_default": false, "kind": "positional_or_keyword"},
            {"name": "strict", "annotation": "bool", "has_default": true, "kind": "positional_or_keyword"}
          ],
          "return_annotation": "dict[str, object]",
          "is_async": false
        }
      }
    }
  }
]

The source field is "__all__" when the module has an __all__ (and --use-ast is not set), "ast" otherwise.

The detail object maps each public name to its rich info. Functions have kind: "function" (or "async function") and a signature object. Classes have kind: "class", a bases list, and a members array. Plain variables (no static type info available) are absent from detail.

iceberg check --format json:

[
  {
    "code": "IB001",
    "path": "src/mypackage/utils.py",
    "kind": "missing",
    "declared": null,
    "expected": ["helper", "parse"]
  }
]

Programmatic usage

from sheridan.iceberg import check_api, fix_api, get_public_api

# Get the public API surface — __init__.__all__ is the source of truth
api = get_public_api("src/")
# {"mypackage": ["Role", "User", "helper"]}

# Bypass __all__ and see every module's AST-inferred names
api = get_public_api("src/", use_ast=True)
# {"mypackage": [...], "mypackage.core": [...], "mypackage.utils": [...]}

# Check for __all__ issues
issues = check_api("src/")
# [{"code": "IB002", "path": "...", "kind": "incorrect", "declared": [...], "expected": [...]}]

# Suppress IB001 (missing __all__) — only surface IB002 and IB003
issues = check_api("src/", ignore_missing=True)

# Fix __all__ in place — returns paths of modified files
fixed = fix_api("src/")
# [PosixPath("src/mypackage/core.py")]

# Preview what would change without writing
would_fix = fix_api("src/", dry_run=True)

How inference works

For regular modules, iceberg infers the public API from top-level definitions — functions, classes, and assignments whose names do not start with _.

For __init__.py files, names re-exported via from x import y are also counted, since this is the standard Python pattern for building a package's public surface.

# foo/__init__.py
from foo.snap import Widget   # Widget is inferred as public
from foo._bar import _helper  # _helper is excluded (underscore)

Submodules are not automatically included. The existence of foo/snap.py on disk does not add snap to foo.__all__ — the __init__.py is the explicit gatekeeper. To expose a submodule, import it explicitly:

# foo/__init__.py
from foo import snap  # now snap is part of the inferred public API

Test files (test_*.py, *_test.py, conftest.py) are always skipped.

As a pre-commit hook

repos:
  - repo: https://github.com/sheridan/sheridan-iceberg
    rev: v0.1.0
    hooks:
      - id: iceberg

Development

# Install dependencies
task install

# Run all checks (lint, format, typecheck, test, iceberg)
task check

# Run individual checks
task lint:check   # ruff — read-only
task lint         # ruff — autofix
task format:check # formatter — read-only
task format       # formatter — write
task typecheck    # mypy --strict
task test         # pytest --cov
task iceberg:check # dogfood: run iceberg check on itself
task iceberg:show  # dogfood: show iceberg's own public API

# Run tests
task test

# Build docs
task docs-serve

CI pipeline (Dagger)

The full CI pipeline runs each gate in its own container via Dagger. Podman is the default runtime; Docker is supported via CONTAINER_RUNTIME=docker.

# First-time setup (generates ci/sdk/ — run once after clone)
podman machine start   # macOS only
task ci-init

# Run the full CI pipeline locally
task ci

# Use Docker instead
CONTAINER_RUNTIME=docker task ci

See CONTRIBUTING.md for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sheridan_iceberg-1.1.0.tar.gz (59.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sheridan_iceberg-1.1.0-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file sheridan_iceberg-1.1.0.tar.gz.

File metadata

  • Download URL: sheridan_iceberg-1.1.0.tar.gz
  • Upload date:
  • Size: 59.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for sheridan_iceberg-1.1.0.tar.gz
Algorithm Hash digest
SHA256 d23c3744351f6b4a871eec72957698a9511348a493f5238ec1bc9dfc59182984
MD5 60e6d0ba38e2afbc696c0c3f1b1611aa
BLAKE2b-256 66fb0c58fe9ba79a68244c6b11c6e9a73fa9ea52e45451ac0427a51e6bfeca25

See more details on using hashes here.

Provenance

The following attestation bundles were made for sheridan_iceberg-1.1.0.tar.gz:

Publisher: publish.yaml on andrewasheridan/iceberg

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sheridan_iceberg-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for sheridan_iceberg-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a58143aadbf3707b916c50293f7cdf112167693027f34e633f2e6f11d1bf3a14
MD5 cf1f956d2eb6ba0d3478ba89f0254134
BLAKE2b-256 75fcc73a119e81bbb2bb9895912cc79dd41c50c59690235beceeb2c035b5d5aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for sheridan_iceberg-1.1.0-py3-none-any.whl:

Publisher: publish.yaml on andrewasheridan/iceberg

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page