Metadata-Version: 2.4
Name: lossless-yaml
Version: 0.1.1
Summary: Yet Another YAML AST - programmatically transform YAML, preserving whitespace and comments
Project-URL: Homepage, https://github.com/Open-Athena/yaya
Project-URL: Repository, https://github.com/Open-Athena/yaya
Author-email: Ryan Williams <ryan@no.de>
License: MIT
License-File: LICENSE
Keywords: ast,editor,formatting,lossless,round-trip,transformer,yaml
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup
Requires-Python: >=3.10
Requires-Dist: ruamel-yaml>=0.17.0
Description-Content-Type: text/markdown

# yaya

Yet Another YAML AST - programmatically transform YAML, preserving whitespace and comments

[![PyPI version](https://badge.fury.io/py/lossless-yaml.svg)](https://badge.fury.io/py/lossless-yaml)

## Why?

Programmatically edit YAML at the AST level, so re-serializing doesn't introduce extraneous changes. Preserves:

- Comments
- Whitespace (including trailing spaces)
- Quote styles (`'`, `"`, or none)
  - By default, will switch between `'` and `"` if the other is added to a string (as a literal character) 
- Block scalar indicators (`|`, `|-`, `|+`)
- Other formatting choices (e.g. indentation)

Other libraries (e.g. [ruamel.yaml]) make formatting changes when serializing. `yaya` avoids this by:

1. Parsing YAML to get the AST (with position information)
2. Applying modifications only to specific values or subtrees
3. Leaving everything else untouched

It also tries to mimic neighboring formatting, when adding values/trees, while also supporting `dict`-like ergonomics and path-based navigation.

## Installation

```bash
pip install lossless-yaml
```

## Usage

### Basic String Replacement

```python
from yaya import YAYA

# Load a YAML file
doc = YAYA.load('.github/workflows/test.yaml')

# Simple string replacement in all values
doc.replace_in_values('src/marin', 'lib/marin/src/marin')

# Regex-based replacement
doc.replace_in_values_regex(r'\buv sync(?! --package)', 'uv sync --package myapp')

doc.save()
```

### Path-Based Navigation and Assertions

```python
# Navigate using paths
runs_on = doc.get_path("jobs.test.runs-on")
step_name = doc.get_path("jobs.test.steps[0].name")

# Or dict-like access
runs_on = doc["jobs"]["test"]["runs-on"]

# Assert values before making changes
doc.assert_value("on", ["push"])
doc.assert_absent("jobs.test.defaults")
doc.assert_present("jobs.test.steps")
```

### Replacing Values or Subtrees

```python
# Replace a simple value
doc.replace_key("jobs.test.runs-on", "ubuntu-22.04")

# Replace a list item
doc.replace_key("build.commands[1]", "uv sync --package marin --frozen")

# Replace with a complex structure
doc.replace_key("on", {
    "push": {
        "branches": ["main"],
        "paths": ["lib/**", "uv.lock"]
    },
    "pull_request": {
        "paths": ["lib/**", "uv.lock"]
    }
})

doc.save()
```

### Adding Keys

```python
# Add key after another (maintains order)
doc.add_key_after("jobs.test.runs-on", "defaults", {
    "run": {
        "working-directory": "lib/myapp"
    }
})

# Add or replace (convenience method)
doc.ensure_key("jobs.test.timeout-minutes", 30)

doc.save()
```

## Example

Given this YAML file:

```yaml
# Production config
database:
  host: prod-db-1.example.com
  port: 5432
```

This code:

```python
doc = YAYA.load('config.yaml')
doc.replace_in_values('prod-db-1', 'prod-db-2')
doc.save()
```

Produces **exactly**:

```yaml
# Production config
database:
  host: prod-db-2.example.com
  port: 5432
```

No reformatting. No comment loss. Just the change you made.

## How It Works

1. Parse YAML with [ruamel.yaml] to get AST + position information
2. Convert line/column positions to byte offsets
3. Track modifications as you change values
4. Apply byte-level replacements when saving, leaving everything else untouched

## Features

- Byte-for-byte preservation of unchanged content
- String replacement (literal and regex)
- Path-based navigation (`jobs.test.steps[0].name`)
- Replace values or subtrees (scalars, dicts, lists, list items)
- Add keys with proper positioning
- Assertions for validation (`assert_value`, `assert_present`, `assert_absent`)
- Comment preservation
- Block scalar support
- Flow and block style handling

## Limitations

- Removing keys not yet implemented
- Binary data not supported
- Adding keys only supports `add_key_after` currently (not arbitrary positions)

## Comparison with ruamel.yaml

[ruamel.yaml] is excellent for round-trip YAML editing and preserves most formatting. However:

| Feature | [ruamel.yaml] | yaya |
|---------|-------------|---------------|
| Preserves comments | ✅ | ✅ |
| Preserves most whitespace | ✅ | ✅ |
| **Byte-for-byte identical** | ❌ | ✅ |
| Trailing whitespace | ❌ | ✅ |
| Block scalar indicators | ❌ (computes new ones) | ✅ |

`yaya` uses [ruamel.yaml] under the hood but takes a different approach: instead of serializing the AST back to YAML, it modifies the original bytes directly.

## License

MIT

## Contributing

Issues and pull requests welcome!

[ruamel.yaml]: https://pypi.org/project/ruamel.yaml/
