Metadata-Version: 2.4
Name: fastyaml-rs
Version: 0.7.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup
Classifier: Typing :: Typed
Summary: A fast YAML parser and linter for Python, powered by Rust
Keywords: yaml,parser,linter,rust,performance
Author: fast-yaml contributors
License: MIT OR Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/bug-ops/fast-yaml#readme
Project-URL: Homepage, https://github.com/bug-ops/fast-yaml
Project-URL: Repository, https://github.com/bug-ops/fast-yaml

# fastyaml-rs

[![PyPI](https://img.shields.io/pypi/v/fastyaml-rs)](https://pypi.org/project/fastyaml-rs/)
[![Python](https://img.shields.io/pypi/pyversions/fastyaml-rs)](https://pypi.org/project/fastyaml-rs/)
[![License](https://img.shields.io/pypi/l/fastyaml-rs)](https://github.com/bug-ops/fast-yaml/blob/main/LICENSE-MIT)

A fast YAML 1.2.2 parser and linter for Python, powered by Rust.

**Important:** Requires Python 3.10 or later.

## Installation

```bash
pip install fastyaml-rs
```

## Usage

```python
import fast_yaml

# Parse YAML
data = fast_yaml.safe_load("name: test\nvalue: 123")
print(data)  # {'name': 'test', 'value': 123}

# Dump YAML
yaml_str = fast_yaml.safe_dump({"name": "test", "value": 123})
print(yaml_str)  # name: test\nvalue: 123\n
```

## Large Integers

Decimal integers beyond the i64 range load as exact Python `int`s and dump back exactly. Loading such a literal
with more digits than `sys.get_int_max_str_digits()` (4300 by default; sign and leading zeros are not counted), or dumping an `int` with more digits,
raises CPython's `ValueError`; raise the limit with `sys.set_int_max_str_digits()`. Literals that fit i64 never
hit this limit. Hex and octal integers are capped at 14284 bits (at most 4300 decimal digits) and are not
subject to the limit. PyYAML reads leading-zero literals differently (`0012` is octal there).

## Parse Limits

Nesting depth, alias expansion and parser lookahead are capped by default. Raise or lower the caps with keyword arguments on
`safe_load`, `safe_load_all`, `load`, `load_all`, and the `ParallelConfig`, `LintConfig`, and `BatchConfig`
constructors (each config also has `with_max_depth()` / `with_max_alias_bytes()` / `with_max_scan_ahead()`; `None` resets to the default).
`ParallelConfig`, `LintConfig`, and `BatchConfig` also accept `max_input_bytes` / `with_max_input_bytes()`. Every loader and config accepts `max_documents` (each config also has `with_max_documents()`):

```python
fast_yaml.safe_load(text, max_depth=512, max_alias_bytes=256 * 1024 * 1024)
```

| Option | Default | Range |
|--------|---------|-------|
| `max_depth` | 256 | 1..=512 (flow collections stop at 255 levels) |
| `max_alias_bytes` | 64 MiB | 1..=1 GiB |
| `max_scan_ahead` | 4 Mi characters | 1..=1 Gi |
| `max_input_bytes` (`ParallelConfig`, `LintConfig`, `BatchConfig`) | 100 MiB | 1..=1 GiB |
| `max_documents` | 100 000 | 1..=10 000 000 |

`max_scan_ahead` bounds how far the parser reads past the last node it reported, which bounds parser memory (about 190x the value). A flow collection that the parser reads whole (at the document root, in a `- ` entry, nested in another flow collection, or after a tab), so any JSON document longer than the limit, minified or pretty-printed, a single scalar, or a run of comments longer than the limit raises `ValueError`; raise `max_scan_ahead` for such input. Block YAML, `key: [..]` and `--- [..]` are not affected.

Out-of-range values raise `ValueError`; non-integers (including `bool`) raise `TypeError`.
Depth 512 needs about 1 MiB of thread stack (up to 983 KiB measured in release builds) and can abort the process on stacks of 512 KiB or less; the default of 256 is safe.
The dumper keeps a fixed depth of 256, so data parsed deeper may fail to dump.
The alias budget is per stream, so parallel and batch runs can use up to workers x budget.
`max_input_bytes` bounds work on oversized input; an in-memory source is already allocated when checked, so it is not a memory bound there.
`safe_load` and `safe_load_all` reject sources over 100 MiB.
`max_documents` caps the documents in one stream and applies even when no config is passed (`safe_load` and `load` also read the whole stream, so a source over the limit raises `ValueError`); `dump_parallel` enforces it on the input list (`cannot serialize to YAML: document count exceeds N`). `safe_dump`, `safe_dump_all` and `dump_all` take no `max_documents`; they are bounded by the fixed dump limits (depth 256, output size).

## Sets

Floats are written with a dot and a signed exponent so YAML 1.1 readers such as PyYAML read them as floats (`1e300` becomes `1.0e+300`).
`!!set` mappings load as Python `set` in `safe_load` and `parse_parallel`, and `dump` writes `set` and `frozenset` as `!!set`.
A `!!set` cannot be a mapping key (raises `ValueError`, like any sequence or mapping key).
A `!!set` member with a value (`!!set {a: 1}`) raises `ValueError` with its line and column, where PyYAML drops the value; a repeated `<<` key in one mapping raises too.

## Numeric Keys

YAML treats `1`, `true` and `1.0` as three different keys, but a Python `dict` or `set` considers them equal.
Instead of silently dropping one, `safe_load` raises `ValueError` with the key's line and column when a mapping or `!!set` holds such keys (`parse_parallel` raises the same error); repeating a key of the same type is still an ordinary duplicate (last value wins).
This differs from PyYAML, which keeps only one of them.
`.nan` keys collapse to a single entry, as in the Rust core.

```python
fast_yaml.safe_load("1: a\ntrue: b\n")
# ValueError: YAML parse error: bool key true is distinct in YAML but equal as a
#   Python dict key to a key of type int at line 2, column 1
```

## Features

- **YAML 1.2.2 compliant** — Full Core Schema support
- **Fast** — matches PyYAML C on small and medium files, 2-4x faster than pure-Python PyYAML
- **PyYAML compatible** — Drop-in replacement with `load`, `dump`, `Loader`, `Dumper` classes
- **Linter** — Rich diagnostics with 1-based line/column tracking (`Location(line, column, offset)` rejects 0); supports inline `# fy: disable` / `disable-line` / `disable-file` (and `# yamllint ...`) suppression comments
- **Parallel processing** — Multi-threaded parsing for large files
- **Batch processing** — Process multiple files in parallel
- **Type stubs** — Full IDE support with `.pyi` files

## Linter Configuration

Per-rule severity and options are passed through `rules`; the option keys are the same kebab-case keys as in the `fy` config file, and errors use the same messages as `fy lint --config`:

```python
from fast_yaml._core import lint

config = lint.LintConfig(
    rules={
        "line-length": {"max": 120, "severity": "error"},
        "quoted-strings": {"quote-type": "double", "required": True},
        "document-start": {"present": True},
        "duplicate-key": "warning",
    },
)
diagnostics = lint.lint("a: 1\n", config)

# Unknown rules, option keys, wrong types and invalid severities raise ValueError
lint.LintConfig(rules={"quoted-strings": {"quote-type": "singel"}})
```

The `rules` patch is applied first, then the keyword arguments you set (`max_line_length`,
`indent_size`, and `require_document_*` / `allow_duplicate_keys` when `True`; `False` changes nothing), then `disabled_rules` (which always
wins); so `indent_size=2` beats `rules={"indentation": {"spaces": 4}}`. An omitted `max_line_length`
keeps the default 80 and an omitted `indent_size` leaves the width `consistent` (the `indent_size` property is then `None`), as for `lint(source)`
without a config (a `rules` patch can replace both); `max_line_length=None` (or
`{"line-length": {"max": None}}`) removes the line length limit.

## Batch Processing

Process multiple YAML files in parallel:

```python
from fast_yaml._core import batch

# Parse multiple files
result = batch.process_files(
    [
        "config1.yaml",
        "config2.yaml",
        "config3.yaml",
    ]
)
print(f"Processed {result.total} files, {result.failed} failed")

# With configuration
config = batch.BatchConfig(workers=4, indent=2)
result = batch.process_files(paths, config)
```

### Format Files

```python
# Dry-run: get formatted content without writing
results = batch.format_files(["config.yaml"])
for path, content, error in results:
    if content:
        print(f"{path}: {len(content)} bytes")

# In-place: format and write back
result = batch.format_files_in_place(["config.yaml"])
print(f"Changed {result.changed} files")
```

### BatchConfig Options

| Option | Default | Description |
|--------|---------|-------------|
| `workers` | Auto | `None` = auto, `0` = sequential, `1`-`128` = worker threads (larger values raise `ValueError`) |
| `max_input_bytes` | 100 MiB | Maximum file size, 1..=1 GiB |
| `indent` | 2 | Indentation width |
| `width` | 80 | Line width |
| `sort_keys` | False | Sort dictionary keys |
| `max_depth` | 256 | Maximum nesting depth, 1..=512 (`process_files` and `format_files`) |
| `max_alias_bytes` | 64 MiB | Alias-expansion budget per file, 1..=1 GiB (`process_files` only) |
| `max_scan_ahead` | 4 Mi | Characters the parser may read past the last node, 1..=1 Gi |
| `max_documents` | 100 000 | Maximum documents per file, 1..=10 000 000 |

`format_files` applies `max_depth`, `max_scan_ahead` and `max_documents` and ignores `max_alias_bytes`. `indent` must be 1..=9 and `width` 20..=1000; other values raise `ValueError` instead of being clamped.

### BatchResult

```python
result = batch.process_files(paths)
print(f"Total: {result.total}")
print(f"Success: {result.success}")
print(f"Changed: {result.changed}")
print(f"Failed: {result.failed}")
print(f"Duration: {result.duration_ms}ms")
print(f"Files/sec: {result.files_per_second()}")

for path, error in result.errors():
    print(f"Error in {path}: {error}")
```

## Documentation

See the [main repository](https://github.com/bug-ops/fast-yaml) for full documentation.

## License

Licensed under either of [Apache License, Version 2.0](../LICENSE-APACHE) or [MIT License](../LICENSE-MIT) at your option.

