# Requirements Processor

## Purpose

Generates a `requirements.txt` file for a Python project by scanning the
project's `.py` source files for `import` statements and listing the
third-party PyPI distributions they reference.

## How It Works

1. Scans every `.py` file in the project's source directories.
2. Extracts the top-level module name from each `import` / `from` statement.
3. Drops imports that resolve to a local project file (intra-project imports).
4. Drops imports that are part of the Python standard library.
5. Drops imports listed in `exclude`.
6. Maps each remaining import name to its PyPI distribution name using the
   built-in curated table (e.g. `cv2` → `opencv-python`, `yaml` → `PyYAML`).
   User-supplied `mapping` entries win over the built-in table.
7. Appends any distributions listed in `extra` (see below).
8. Writes the deduplicated result to `requirements.txt`.

## Extra distributions

Some packages do `import x` at module top level for a runtime helper they
do not declare as an `install_requires` dependency. The classic case is
`manim-voiceover` doing `import pkg_resources`, which lives in
`setuptools`. When that package's metadata omits `setuptools`, pip will
not install it, and `import manim_voiceover` will fail at runtime.

Static AST scanning cannot see those transitive needs, so the `extra`
field lets you list distributions that should always appear in
`requirements.txt`:

```toml
[processor.requirements]
extra = ["setuptools"]
```

Entries in `extra` bypass the `exclude` and stdlib filters — they are
distribution names the user is explicitly asserting must ship.

## Import → Distribution Mapping

Most Python packages publish under the same name as their top-level import,
so the default is identity (`import requests` → `requests`). A curated table
handles the common exceptions:

| Import | Distribution |
|--------|--------------|
| `cv2` | `opencv-python` |
| `yaml` | `PyYAML` |
| `PIL` | `Pillow` |
| `sklearn` | `scikit-learn` |
| `bs4` | `beautifulsoup4` |
| `dateutil` | `python-dateutil` |
| `dotenv` | `python-dotenv` |
| `jwt` | `PyJWT` |

Projects that import an unusual name should add an override:

```toml
[processor.requirements.mapping]
internal_tools = "acme-internal-tools"
```

## Limitations

- **No version pinning.** The generated file lists bare distribution names.
  Running `pip freeze > requirements.txt` is the right tool if you need
  pinned versions.
- **Static analysis only.** Conditional imports inside `try` blocks, runtime
  `__import__` calls, and string-based imports are not detected.
- **Curated mapping is finite.** Packages with import/distribution name
  mismatches not in the built-in table default to identity; add them to
  `mapping` when needed.

## Source Files

- Input: `**/*.py` (configurable via `src_dirs` / `src_extensions`)
- Output: `requirements.txt` (configurable via `output`)

## Configuration

```toml
[processor.requirements]
output = "requirements.txt"    # Output file path
exclude = []                   # Import names to never emit
sorted = true                  # Sort entries alphabetically
header = true                  # Include a "# Generated by rsconstruct" header
extra = []                     # Distribution names to always include

[processor.requirements.mapping]
# Per-project overrides: import_name = "pypi-distribution-name"
# These win over the built-in curated table.
```

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `output` | string | `"requirements.txt"` | Output file path |
| `exclude` | string[] | `[]` | Import names to never emit |
| `sorted` | bool | `true` | Sort entries alphabetically (false preserves first-seen order) |
| `header` | bool | `true` | Include a comment header line |
| `mapping` | map | `{}` | Per-project import→distribution overrides |
| `extra` | string[] | `[]` | Distribution names to always include (transitive deps undeclared by upstream) |

## Batch support

Runs as a single whole-project operation — all `.py` files feed into one
`requirements.txt` output.

## Clean behavior

This processor is a Generator — `rsconstruct clean outputs` removes each declared output file individually with no directory recursion. After all per-product cleans complete, the orchestrator removes any parent directories that are now empty. Pass `--no-empty-dirs` to keep them. See [Clean behavior](../processors.md#clean-behavior) and [`rsconstruct clean`](../commands.md#rsconstruct-clean).
