|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file provides guidance to coding agents, like Claude Code (claude.ai/code), when working with |
| 4 | +code in this repository. |
| 5 | + |
| 6 | +## About pyinfra |
| 7 | + |
| 8 | +pyinfra turns Python code into shell commands and runs them on servers. Think Ansible, but Python |
| 9 | +instead of YAML, and much faster. It supports SSH, local machine, Docker, and more via connectors. |
| 10 | + |
| 11 | +## Development Setup |
| 12 | + |
| 13 | +```bash |
| 14 | +uv sync # Install all dependencies into managed venv |
| 15 | +``` |
| 16 | + |
| 17 | +## Common Commands |
| 18 | + |
| 19 | +```bash |
| 20 | +# Run tests |
| 21 | +scripts/dev-test.sh |
| 22 | +# or directly: |
| 23 | +uv run pytest --cov --disable-warnings -m 'not end_to_end' |
| 24 | + |
| 25 | +# Run fixtures for a single operation or fact |
| 26 | +uv run pytest tests/test_operations.py -k "apt.packages" |
| 27 | +uv run pytest tests/test_facts.py -k "LinuxHardware" |
| 28 | + |
| 29 | +# End-to-end tests (require Docker/SSH/local targets) |
| 30 | +uv run pytest -m end_to_end_local |
| 31 | +uv run pytest -m end_to_end_docker |
| 32 | + |
| 33 | +# Lint and type check |
| 34 | +scripts/dev-lint.sh |
| 35 | +# Individually: |
| 36 | +uv run ruff check |
| 37 | +uv run ruff format --check |
| 38 | +uv run mypy |
| 39 | +uv run python scripts/lint_arguments_sync.py |
| 40 | + |
| 41 | +# Auto-format |
| 42 | +scripts/dev-format.sh |
| 43 | +``` |
| 44 | + |
| 45 | +## Architecture |
| 46 | + |
| 47 | +The repo has two top-level packages under `src/`: |
| 48 | + |
| 49 | +- **`pyinfra/`** — core library (operations, facts, connectors, API) |
| 50 | +- **`pyinfra_cli/`** — CLI wrapper using Click + gevent |
| 51 | + |
| 52 | +### Core Concepts |
| 53 | + |
| 54 | +**Operations** (`src/pyinfra/operations/`) — Declarative functions (e.g. `apt.packages`, |
| 55 | +`files.put`) that describe desired state. Each operation uses the `@operation` decorator from |
| 56 | +`api/operation.py`, generates a list of commands, and is idempotent. Operations call facts to |
| 57 | +read current state, then return commands to reach desired state. |
| 58 | + |
| 59 | +**Facts** (`src/pyinfra/facts/`) — Read system state (e.g. `AptPackages`, `LinuxHardware`). Each |
| 60 | +fact is a class extending `FactBase` with a `command` attribute and a `process()` method that |
| 61 | +parses command output. Facts are cached per-host per-run. |
| 62 | + |
| 63 | +**Connectors** (`src/pyinfra/connectors/`) — Abstractions for how to connect to and execute on a |
| 64 | +target (SSH, local, Docker, chroot, Terraform, Vagrant, etc.). New connectors should be separate |
| 65 | +packages, not added to this repo. |
| 66 | + |
| 67 | +**API** (`src/pyinfra/api/`) — The core engine: |
| 68 | +- `state.py` — Global deploy state, callbacks, host grouping |
| 69 | +- `host.py` — Per-host metadata and fact access |
| 70 | +- `inventory.py` — Collection of hosts and groups |
| 71 | +- `operation.py` — `@operation` decorator that wraps functions into deploy operations |
| 72 | +- `operations.py` — Execution engine that runs operations across hosts |
| 73 | +- `command.py` — Command types: `StringCommand`, `FileUploadCommand`, `RsyncCommand`, |
| 74 | + `QuoteString`, `MaskString`, etc. |
| 75 | +- `facts.py` — Fact base classes and execution logic |
| 76 | +- `deploy.py` — `@deploy` decorator for grouping operations |
| 77 | +- `connect.py` — Connector lifecycle (connect/disconnect) |
| 78 | +- `config.py` — `Config` object with all configuration options |
| 79 | +- `arguments.py` / `arguments_typed.py` — Global operation arguments (e.g. `_sudo`, `_su_user`); |
| 80 | + **these two files must stay in sync** — CI enforces this via `scripts/lint_arguments_sync.py`, |
| 81 | + so touching one requires touching the other |
| 82 | +- `output.py` — Pluggable output functions (decoupled from Click for testability) |
| 83 | + |
| 84 | +**Context** (`src/pyinfra/context.py`) — Thread-local (gevent-safe) context objects: `host`, |
| 85 | +`state`, `config`, `inventory`. Operations access the current host via `pyinfra.context.host` |
| 86 | +rather than explicit passing. |
| 87 | + |
| 88 | +**Concurrency** — Uses gevent greenlets for parallel host execution. `pyinfra_cli/main.py` |
| 89 | +monkey-patches stdlib at startup. |
| 90 | + |
| 91 | +### Adding Operations / Facts |
| 92 | + |
| 93 | +Operations and facts are auto-discovered from their respective directories. A new |
| 94 | +`src/pyinfra/operations/mytool.py` is immediately available as `from pyinfra.operations import |
| 95 | +mytool`. |
| 96 | + |
| 97 | +- Operations must be idempotent and use facts to check current state |
| 98 | +- Facts must implement `command` (shell command to run) and `process(output)` (parse result) |
| 99 | +- Both need corresponding tests (see fixture convention below) |
| 100 | +- Every operation/fact module must be registered in `pyinfra-metadata.toml` as a plugin with |
| 101 | + tags; omitting this won't break tests but will break docs generation |
| 102 | + |
| 103 | +**Operation / fact tests are YAML or JSON fixtures, not Python tests.** Drop a file under |
| 104 | +`tests/operations/<module>.<op>/` or `tests/facts/<module>.<Fact>/` — it is auto-discovered by |
| 105 | +the `testgen` metaclass. Prefer YAML for new fixtures. To cover a new code path, add a fixture — |
| 106 | +do not write a new Python test. |
| 107 | + |
| 108 | +Operation fixture structure (`tests/operations/<module>.<op>/<name>.yaml`): |
| 109 | + |
| 110 | +```yaml |
| 111 | +args: |
| 112 | + - positional_arg |
| 113 | +kwargs: |
| 114 | + param: value |
| 115 | +facts: |
| 116 | + module.FactClass: {} # a dict of mock values keyed by object_id and attribute |
| 117 | +commands: |
| 118 | + - shell command that should be produced |
| 119 | +``` |
| 120 | +
|
| 121 | +Optional keys: `exception` (e.g. `{name: OperationError, message: "..."}`), `noop_description`. |
| 122 | + |
| 123 | +Fact fixture structure (`tests/facts/<module>.<Fact>/<name>.yaml`): |
| 124 | + |
| 125 | +```yaml |
| 126 | +command: shell command the fact runs |
| 127 | +requires_command: binary # optional |
| 128 | +output: | |
| 129 | + raw stdout to parse |
| 130 | +fact: |
| 131 | + item: |
| 132 | + key: value # expected return value of process() |
| 133 | +``` |
| 134 | + |
| 135 | +**Docstring format** — pyinfra uses `+ param: description` bullets (parsed by |
| 136 | +`scripts/generate_operations_docs.py`). Do not use Google/NumPy/Sphinx style — it will silently |
| 137 | +break docs generation. |
| 138 | + |
| 139 | +**Shell safety** — user-supplied values must be composed into shell commands using `StringCommand` |
| 140 | ++ `QuoteString` / `MaskString` from `pyinfra.api`. Do not use plain string formatting (e.g. |
| 141 | +`"rm -f {}".format(path)`) for user-controlled values. |
| 142 | + |
| 143 | +**Optional parameter defaults** — optional parameters must default to `None`, not `""`. Older |
| 144 | +operations in the codebase use `""` defaults; do not replicate this pattern. |
| 145 | + |
| 146 | +## Branch Strategy |
| 147 | + |
| 148 | +PRs target the latest major branch (`3.x`). One branch per major version exists (`2.x`, `1.x`, |
| 149 | +etc.). |
| 150 | + |
| 151 | +## PR Checklist |
| 152 | + |
| 153 | +- Tests pass (`scripts/dev-test.sh`) |
| 154 | +- Lint/types pass (`scripts/dev-lint.sh`) |
| 155 | +- New operations/facts include tests and documentation |
0 commit comments