Metadata-Version: 2.4
Name: sheridan-iceberg
Version: 1.1.0
Summary: Enforces the presence and correctness of __all__ in Python modules
Project-URL: Repository, https://github.com/andrewasheridan/iceberg
Project-URL: Issues, https://github.com/andrewasheridan/iceberg/issues
Project-URL: Changelog, https://github.com/andrewasheridan/iceberg/blob/main/CHANGELOG.md
Author-email: sheridan <andrewasheridan@gmail.com>
License: MIT
License-File: LICENSE
Keywords: all,api,linting,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.14
Provides-Extra: dev
Requires-Dist: bandit[toml]>=1.9.4; extra == 'dev'
Requires-Dist: commitizen>=4.13.9; extra == 'dev'
Requires-Dist: mkdocstrings[python]>=1.0.3; extra == 'dev'
Requires-Dist: mypy>=1.19.1; extra == 'dev'
Requires-Dist: pre-commit>=4.5.1; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
Requires-Dist: pytest>=9.0.2; extra == 'dev'
Requires-Dist: ruff>=0.15.7; extra == 'dev'
Requires-Dist: zensical; extra == 'dev'
Description-Content-Type: text/markdown

# sheridan-iceberg

[![CI](https://github.com/andrewasheridan/iceberg/actions/workflows/ci.yaml/badge.svg)](https://github.com/andrewasheridan/iceberg/actions/workflows/ci.yaml)
[![PyPI](https://img.shields.io/pypi/v/sheridan-iceberg)](https://pypi.org/project/sheridan-iceberg/)
[![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen)](https://github.com/andrewasheridan/iceberg)
[![Mutation Score](https://img.shields.io/badge/mutation-tracked-blue)](https://github.com/andrewasheridan/iceberg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.14+](https://img.shields.io/badge/python-3.14+-blue.svg)](https://www.python.org/downloads/)

> 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

```bash
pip install sheridan-iceberg
```

## Usage

```bash
# 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`:

```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`:

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

## Programmatic usage

```python
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.

```python
# 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:

```python
# 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

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

## Development

```bash
# 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](https://dagger.io).
Podman is the default runtime; Docker is supported via `CONTAINER_RUNTIME=docker`.

```bash
# 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](CONTRIBUTING.md) for details.
