콘텐츠로 이동

Features

pyrs-yaml는 PyYAML의 직접 교체로 설계되었으며, PyYAML이 없는 강력한 기능을 추가합니다.

YAML 1.2 준수

granit-parser로 구동되며, YAML 테스트 스위트에서 99.75% 통과율 (405/406)을 달성.

완벽한 순환 파싱

PyYAML과 달리, pyrs-yaml는 모든 서식과 메타데이터를 유지합니다:

  • 주석 — 독립 주석과 인라인 주석
  • 앵커 (&name)와 별칭 (*name)
  • 태그 (!!str, !!int 등)
  • 촙핑 지시자 (|-, |+, >-, >+)
  • 스칼라 스타일 (일반, 단일 따옴표, 이중 따옴표, 리터럴, 접은 형식)
  • 흐름/블록 서식[]/{}와 블록 스타일 유지

성능

벤치마크 환경

벤치마크는 CodSpeed CI(pytest-codspeed, WallTime 모드)에서 측정된 결과입니다. 절대 시간은 환경에 따라 다를 수 있으나 상대 속도는 일관됩니다.

Rust 백엔드는 PyYAML보다 파싱 21–43배, 직렬화 55–177배 빠릅니다:

Operation pyrs-yaml PyYAML
Parse (large) 1.5 ms 57.7 ms
Serialize (large) 0.17 ms 30.2 ms
Round-trip 1.6 ms 87.9 ms

커스텀 AST

CustomNode AST는 YAML 구조를 완전히 제어할 수 있게 합니다:

  • 프로그래밍 방식으로 노드 검사 및 수정
  • 사용자 정의 메타데이터 (주석, 앵커, 태그) 추가
  • 서식을 완전히 제어하면서 YAML를 처음부터 구축
  • 고급 사용 사례: 템플릿 엔진, 구성 생성자, 코드 포맷터

PyYAML 호환성

익숙한 API로 직접 교체 가능:

PyYAML 호환 API
import pyrs_yaml as yaml  # Use as 'yaml' for easy migration

yaml.safe_load(yaml_text)
yaml.safe_dump(data)
yaml.safe_loads(yaml_text)
yaml.safe_dumps(data)

비동기 I/O

asyncio를 통한 논블로킹 직렬화 및 파싱:

비동기 덤프 및 로드
import asyncio
import pyrs_yaml


async def main():
    yaml = await pyrs_yaml.safe_dump_async({"a": 1})
    data = await pyrs_yaml.safe_loads_async(yaml)
    print(data)  # {'a': 1}


asyncio.run(main())

사용 가능한 함수: safe_dump_async, safe_load_async, safe_loads_async.

JSON Schema 검증

JSON Schema를 기반으로 파싱된 YAML 문서 검증:

JSON Schema 검증
doc = pyrs_yaml.parse("name: Alice\nage: 30")
doc.validate({"type": "object", "properties": {"name": {"type": "string"}}})

# Schema as JSON string
doc.validate('{"type": "object", "required": ["name"]}')

검증 실패 시 YamlValidateError를 발생시킵니다.

중복 키

기본적으로 중복 매핑 키는 YamlDuplicateKeyError를 발생시킵니다:

중복 키 오류
pyrs_yaml.parse("key: first\nkey: second")
# pyrs_yaml.YamlDuplicateKeyError: duplicate key: key

allow_duplicate_keys=True를 전달하면 마지막 값이 유지됩니다:

중복 키 허용
doc = pyrs_yaml.parse("key: first\nkey: second", allow_duplicate_keys=True)
doc.get("key")  # "second"

이 스위치는 parse, safe_load, safe_loads, parse_file, parse_all_docs, YAML(allow_duplicate_keys=True)에 적용됩니다. 순환 모드에서 중복 키가 허용된 문서는 마지막 키-값 쌍을 출력하여 직렬화됩니다.

직렬화 옵션

to_yaml_with_options()는 들여쓰기과 줄 바꿈을 제어합니다:

직렬화 옵션
yaml_str = doc.to_yaml_with_options(
    indent_size=2,  # 기본 들여쓰기 (유형별 옵션 생략 시 사용)
    width=80,  # 줄 바꿈 너비; 0은 줄 바꿈 비활성화
    indent_mapping=4,  # 블록 매핑 레벨별 들여쓰기
    indent_sequence=2,  # 블록 시퀀스 레벨별 들여쓰기
    indent_offset=0,  # 전체 문서의 기본 오프셋
)

indent_mapping / indent_sequence / indent_offset를 생략하면 각각 indent_size / indent_size / 0이 되므로 indent_size=4도 모든 레벨을 4만큼 들여씁니다.

커스텀 태그 핸들러

사용자 정의 YAML 태그에 대한 핸들러를 등록하여 스칼라 값을 변환합니다:

import pyrs_yaml


@pyrs_yaml.register_tag("!custom")
def custom_handler(node):
    return f"custom:{node}"


doc = pyrs_yaml.parse("name: !custom value")
doc.get("name")  # "custom:value"
import pyrs_yaml


pyrs_yaml.register_tag("!custom", lambda node: node.upper())

doc = pyrs_yaml.parse("name: !custom value")
doc.get("name")  # "custom:value"
  • 같은 태그에 대한 여러 핸들러는 priority 오름차순으로 실행됩니다. YamlTagSkip을 발생시키면 다음 핸들러에 위임됩니다.
  • 핸들러는 문자열을 반환해야 합니다. 그렇지 않으면 YamlTagError가 발생합니다.
  • remove_tag("!custom")clear_tag_handlers()로 핸들러를 해제합니다.

커뮤니티 플러그인

직렬화 및 역직렬화에 통합되는 사용자 정의 YAML 노드 유형을 정의합니다:

CustomType 플러그인
import pyrs_yaml
from datetime import datetime


class TimestampType(pyrs_yaml.CustomType):
    python_type = datetime

    def from_yaml(self, value: str):
        return datetime.fromisoformat(value)

    def to_yaml(self, obj) -> str:
        return obj.isoformat()


# 명령형 또는 데코레이터로 등록
pyrs_yaml.register_type("!timestamp", TimestampType())

# 로드: 태그가 지정된 스칼라 → Python 객체
doc = pyrs_yaml.parse("when: !timestamp 2026-08-11T10:30:00")
assert isinstance(doc.get("when"), datetime)

# 덤프: Python 객체 → 태그가 지정된 스칼라
data = {"ts": datetime(2026, 8, 11, 10, 30)}
out = pyrs_yaml.safe_dump(data)
# out contains: ts: !timestamp 2026-08-11T10:30:00

내장 플러그인 (import 시 등록): !timestampdatetime, !datedatetime.date, !timedatetime.time, !uuiduuid.UUID, !decimaldecimal.Decimal, !binarybytes, !regexre.Pattern, !setstr

선택적 서드파티 플러그인 (라이브러리가 설치된 경우 자동 등록): !durationpendulum.Duration, !arrowarrow.Arrow, !ulidulid.ULID

메서드 설명
can_parse(node) 이 유형이 지정된 AST 노드를 처리하는지 여부
from_yaml(value) YAML 문자열 → Python 객체로 변환
to_yaml(obj) Python 객체 → YAML 문자열로 변환
validate(obj) Python 객체를 검증합니다 (bool 반환)

Pydantic 통합

Pydantic 모델로 YAML을 직접 파싱하거나 모델을 YAML로 직렬화:

Pydantic 통합
from pydantic import BaseModel
import pyrs_yaml


class User(BaseModel):
    name: str
    age: int


# Pydantic 모델로 YAML 파싱
user = pyrs_yaml.parse_as(User, "name: Alice\nage: 30")
print(user.name)  # Alice

# 모델을 YAML 문자열로 직렬화
yaml_str = pyrs_yaml.dump_pydantic(user)
print(yaml_str)

pydantic-settings

PyrsYamlConfigSettingsSourcepydantic_settings.YamlConfigSettingsSource의 드롭인 대체품입니다. 동일한 BaseSettings + SettingsConfigDict(yaml_file=...) 워크플로에 연결되지만 PyYAML 대신 pyrs-yaml로 파싱합니다. 값은 YAML 1.2 코어 스키마를 따르며(예: on은 문자열로 유지), pyrs-yaml의 성능을 설정 로딩에도 활용할 수 있습니다.

pydantic-settings 소스
from pydantic_settings import BaseSettings, SettingsConfigDict
import pyrs_yaml


class Settings(BaseSettings):
    app_name: str

    model_config = SettingsConfigDict(yaml_file="config.yaml")

    @classmethod
    def settings_customise_sources(
        cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings
    ):
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            file_secret_settings,
            pyrs_yaml.PyrsYamlConfigSettingsSource(settings_cls),
        )


settings = Settings()  # pyrs-yaml로 config.yaml에서 로드

pip install "pyrs-yaml[settings]"로 설치합니다 (Python 3.10+ 필요).

점진적 재파싱

다른 옵션으로 저장된 소스 텍스트를 제자리에서 재파싱:

점진적 재파싱
doc = pyrs_yaml.parse("x: on")
print(doc.get("x"))  # "on" (string, core schema)

doc.reparse(schema="yaml1.1")
print(doc.get("x"))  # True (bool, yaml1.1 schema)

제자리 편집

파싱된 문서를 서식 메타데이터를 전혀 잃지 않고 편집합니다 — 주석, 앵커, 태그, 스칼라 스타일, 흐름/블록 스타일이 모두 유지됩니다:

제자리 편집
doc = pyrs_yaml.parse("""
server:
  host: localhost  # bind address
  ports:
    - 8080
""")

doc.set("$.server.host", "0.0.0.0")  # (1)!
doc.insert("$.server.ports", 0, 80)  # (2)!
doc.append("$.server.ports", 443)  # (3)!
doc.rename("$.server", "srv")  # (4)!
del doc["server"]  # 또는: doc.delete("$.server")
  1. set은 경로의 값을 교체하며 인라인 주석을 유지합니다.
  2. insert는 시퀀스 인덱스 위치에 요소를 삽입합니다.
  3. append는 시퀀스 끝에 추가합니다.
  4. rename은 매핑 키를 제자리에서 이름 변경하며 위치와 주석을 유지합니다.
  • 경로 API — JSONPath 스타일 경로($.a.b[0]), 루트 슈가(doc["k"] = v, del doc["k"])
  • 노드 APIdoc.node().find(path)Node 객체를 반환하며 set_value / insert / append / delete / rename과 트리 탐색(parent, children, walk, filter)을 지원
  • 원자성 — 실패한 편집은 문서(리비전 포함)를 변경하지 않습니다
  • 메타데이터 보존 — 교체된 스칼라는 주석/앵커/태그/따옴표를 유지; 이름이 변경된 키는 위치와 주석을 유지
  • 별칭 인식 — 별칭 자신의 경로 설정은 그 자리를 교체; 별칭을 통한 편집은 YamlEditError 발생

자세한 내용은 제자리 편집 가이드를 참조하세요.

NumPy ndarray 지원

pyrs-yaml는 모든 차원의 numpy.ndarray 객체를 직접 YAML로 직렬화할 수 있습니다:

NumPy ndarray 직렬화
import numpy as np
import pyrs_yaml

# 1-D array
arr = np.array([1, 2, 3], dtype="int32")
yaml_str = pyrs_yaml.safe_dump(arr)
# - 1
# - 2
# - 3

# 2-D matrix
matrix = np.array([[1, 2], [3, 4]], dtype="float64")
yaml_str = pyrs_yaml.safe_dump(matrix)
# -
#   - 1.0
#   - 2.0
# -
#   - 3.0
#   - 4.0

# Round-trip
loaded = pyrs_yaml.safe_load(yaml_str)
assert loaded == [[1.0, 2.0], [3.0, 4.0]]

지원되는 dtype

타입 Rust 백엔드 YAML 출력
int8/16/32/64 PyUntypedArrayPyArrayDyn<i8/i16/i32/i64> 일반 정수(음수 시 따옴표)
uint8/16/32/64 PyUntypedArrayPyArrayDyn<u8/u16/u32/u64> 일반 정수
float32/64 PyUntypedArrayPyArrayDyn<f32/f64> 일반 실수(음수 시 따옴표)
complex64/128 PyUntypedArrayPyArrayDyn<Complex64/Complex32> (re+imj) 문자열
bool PyUntypedArrayPyArrayDyn<bool> true / false
nan / inf NaN / .inf / -.inf

블록 시퀀스의 음수 스칼라

YAML 1.2 블록 시퀀스는 -로 시작하는 일반 스칼라를 포함할 수 없습니다. 음수 값은 자동으로 따옴표가 추가됩니다.

참고사항

  • 제로 복사: numpy Rust crate의 PyUntypedArray를 사용하여 타입이 지워진 배열 접근을 수행한 후, 올바른 타입의 PyArrayDyn<T>에 디스패치하여 제로 복사 슬라이스 반복을 수행
  • GIL 해제: 슬라이스 반복은 GIL 외부에서 실행되어 큰 배열에서 최대 성능을 발휘
  • 음수: YAML 1.2 블록 시퀀스에는 -로 시작하는 일반 스칼라를 포함할 수 없습니다. 음수 값은 자동으로 따옴표가 추가되며 순환 파싱 시 올바르게 파싱됩니다
  • 0차원 배열: 1차원으로 리셰이프되어 단일 항목 리스트로 직렬화
  • 복소수: YAML에는 네이티브 복소수 타입이 없습니다. (re+imj) 문자열로 직렬화됩니다. safe_load는 Python complex가 아닌 문자열로 반환
  • Markdown frontmatter 추출read_markdown() 블로그/콘텐츠 도구용
  • JSON ↔ YAML 변환from_json() / from_dict()
  • 다중 문서 파싱parse_all_docs()
  • 국제화 오류 메시지set_language("ko") 이중 언어 오류용
  • 타입 힌트 — IDE 지원을 위한 완전한 .pyi 스텁

지원되는 YAML 구조

기능 지원
YAML 1.2 사양 완전
주석 (독립) 유지
주석 (인라인) 유지
앵커 및 별칭 유지
태그 (명시적) 유지
블록 스칼라 (|, >) 유지
촙핑 지시자 유지
흐름 컬렉션 ({}, []) 유지
병합 키 (<<) 해결
복합 키 지원
이스케이프 시퀀스 지원
다중 문서 지원
비동기 I/O safe_*_async
JSON Schema 검증 doc.validate()
점진적 재파싱 doc.reparse()
제자리 편집 doc.set() / insert() / append() / delete() / rename()
JSON 내보내기 doc.to_json()
Metadata editing Node.set_comment() / set_anchor() / set_tag()
Style/format control Node.set_scalar_style() / set_flow_style() / set_chomping()
Deep editing doc.set_many() / sort_keys() / Node.move() / copy()
Schema validation validate_against_schema()
Schema file IO load_schema() / list_schemas()