python-yaml-edit_0.1.2.orig/Cargo.lock0000644000000000000000000001137615212774127014612 0ustar00# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ "libc", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", ] [[package]] name = "pyo3-build-config" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", ] [[package]] name = "pyo3-macros" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", "syn", ] [[package]] name = "pyo3-macros-backend" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "rowan" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" dependencies = [ "countme", "hashbrown", "rustc-hash", "text-size", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "target-lexicon" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "yaml-edit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cedf59245d46b241af7c120e20c53f5a4e0249ff5a8cf59095aba0481ffb36fa" dependencies = [ "base64", "rowan", ] [[package]] name = "yaml-edit-py" version = "0.1.2" dependencies = [ "pyo3", "yaml-edit", ] python-yaml-edit_0.1.2.orig/Cargo.toml0000644000000000000000000000067015212774127014630 0ustar00[package] name = "yaml-edit-py" version = "0.1.2" edition = "2021" authors = ["Jelmer Vernooij "] license = "Apache-2.0" description = "Python bindings for the yaml-edit lossless YAML parser and editor" repository = "https://github.com/jelmer/yaml-edit-py" publish = false [lib] name = "_yaml_edit" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.29", features = ["extension-module"] } yaml-edit = "0.2.2" python-yaml-edit_0.1.2.orig/MANIFEST.in0000644000000000000000000000010115210547717014424 0ustar00include Cargo.toml include Cargo.lock recursive-include src *.rs python-yaml-edit_0.1.2.orig/README.md0000644000000000000000000000436515212767342014165 0ustar00# yaml-edit Python bindings for the [yaml-edit](https://github.com/jelmer/yaml-edit) Rust crate: a lossless YAML parser and editor that preserves formatting, comments, and whitespace. Unlike most YAML libraries, yaml-edit does not round-trip through a plain data model. It keeps the original syntax tree, so edits change only the parts you touch and leave everything else - indentation, comment placement, quoting style, blank lines - exactly as it was. ## Installation ```console $ pip install yaml-edit ``` A Rust toolchain is required to build from source. ## Usage ```python from yaml_edit import Document doc = Document.parse("name: old-project\nversion: 1.0.0\n") doc.set("name", "new-project") doc.set("version", "2.0.0") print(doc, end="") # name: new-project # version: 2.0.0 ``` Edits preserve surrounding formatting and comments: ```python doc = Document.parse( "# project config\n" "name: demo # the name\n" "tags:\n" " - a\n" " - b\n" ) mapping = doc.as_mapping() mapping.set("name", "renamed") print(doc, end="") # # project config # name: renamed # the name # tags: # - a # - b ``` Working with nested structures: ```python doc = Document.parse("servers:\n - web\n - db\n") servers = doc.as_mapping().get_sequence("servers") servers.push("cache") print(doc, end="") # servers: # - web # - db # - cache ``` Multi-document streams: ```python from yaml_edit import YamlFile stream = YamlFile.parse("a: 1\n---\nb: 2\n") for document in stream.documents(): print(document.keys()) # ['a'] # ['b'] ``` Resolving anchors and merge keys (`<<`) for reading: ```python doc = Document.parse( "defaults: &d\n" " timeout: 30\n" " retries: 3\n" "prod:\n" " <<: *d\n" " host: prod.example.com\n" " timeout: 60\n" ) prod = doc.merged().get_merged("prod") print(prod.keys()) # ['host', 'timeout', 'retries'] print(str(prod["timeout"])) # the direct entry wins over the merged one # 60 ``` The merged view is read-only; edit through the original `Document` or `Mapping`. ## Supported value types Scalar values passed to `set`, `push`, and `insert` may be `str`, `int`, `float`, or `bool`. `None` is rejected because the underlying editor has no way to emit a YAML null scalar. ## License Apache-2.0 python-yaml-edit_0.1.2.orig/pyproject.toml0000644000000000000000000000324015212774127015610 0ustar00[build-system] requires = ["setuptools>=77", "setuptools-rust>=1.7"] build-backend = "setuptools.build_meta" [project] name = "yaml-edit" version = "0.1.2" description = "Lossless YAML parser and editor that preserves formatting, comments, and whitespace" readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" authors = [{ name = "Jelmer Vernooij", email = "jelmer@debian.org" }] keywords = ["yaml", "parser", "lossless", "edit"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Rust", "Topic :: Software Development :: Libraries", "Topic :: Text Processing :: Markup", ] [project.urls] Homepage = "https://github.com/jelmer/yaml-edit-py" Repository = "https://github.com/jelmer/yaml-edit-py" [tool.setuptools.packages.find] where = ["python"] [[tool.setuptools-rust.ext-modules]] target = "yaml_edit._yaml_edit" path = "Cargo.toml" binding = "PyO3" [tool.setuptools.package-data] yaml_edit = ["py.typed", "*.pyi"] [tool.cibuildwheel] # PyPy ships no Rust toolchain by default and the extension targets CPython. # musllinux is skipped: no prebuilt Rust for the musl i686 target. skip = "pp* *-musllinux*" build-frontend = "build" test-command = "python -m unittest discover -s {project}/tests" [tool.cibuildwheel.linux] # manylinux containers have no Rust; install rustup before building. before-all = "curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal" environment = { PATH = "$HOME/.cargo/bin:$PATH" } [tool.cibuildwheel.macos] # Rust requires macOS 10.12+; cibuildwheel otherwise defaults lower. environment = { MACOSX_DEPLOYMENT_TARGET = "10.12" } python-yaml-edit_0.1.2.orig/python/0000755000000000000000000000000015210544653014213 5ustar00python-yaml-edit_0.1.2.orig/src/0000755000000000000000000000000015210544653013461 5ustar00python-yaml-edit_0.1.2.orig/tests/0000755000000000000000000000000015210544653014034 5ustar00python-yaml-edit_0.1.2.orig/python/yaml_edit/0000755000000000000000000000000015210544653016162 5ustar00python-yaml-edit_0.1.2.orig/python/yaml_edit/__init__.py0000644000000000000000000000120215212767342020272 0ustar00"""Lossless YAML parser and editor. Parse YAML while preserving formatting, comments, and whitespace, then make targeted edits that leave the rest of the document untouched. >>> from yaml_edit import Document >>> doc = Document.parse("name: old\\nversion: 1.0\\n") >>> doc.set("name", "new") >>> print(doc, end="") name: new version: 1.0 """ from ._yaml_edit import ( Document, Entry, Mapping, MergedMapping, Node, Scalar, Sequence, YamlFile, ) __all__ = [ "Document", "Entry", "Mapping", "MergedMapping", "Node", "Scalar", "Sequence", "YamlFile", ] python-yaml-edit_0.1.2.orig/python/yaml_edit/_yaml_edit.pyi0000644000000000000000000001451515212767342021025 0ustar00"""Type stubs for the yaml-edit native extension.""" from typing import Iterator, Optional, Union ScalarValue = Union[str, int, float, bool] class Node: """A YAML node returned from a getter.""" @property def kind(self) -> str: ... def is_scalar(self) -> bool: ... def is_mapping(self) -> bool: ... def is_sequence(self) -> bool: ... def as_scalar(self) -> Optional[Scalar]: ... def as_mapping(self) -> Optional[Mapping]: ... def as_sequence(self) -> Optional[Sequence]: ... def as_int(self) -> Optional[int]: ... def as_float(self) -> Optional[float]: ... def as_bool(self) -> Optional[bool]: ... def __str__(self) -> str: ... class Scalar: """A scalar YAML value (string, number, boolean).""" def value(self) -> str: """The raw scalar text, preserving any surrounding quotes.""" def as_string(self) -> str: """The scalar's value with quotes stripped if present.""" def is_quoted(self) -> bool: ... def unquoted_value(self) -> str: ... def is_null(self) -> bool: ... def as_int(self) -> Optional[int]: ... def as_float(self) -> Optional[float]: ... def as_bool(self) -> Optional[bool]: ... def set_value(self, value: str) -> None: ... def __str__(self) -> str: ... class Entry: """A single key/value entry in a mapping. The fine-grained handle for mappings with repeated keys: each occurrence is a distinct entry that can be inspected and mutated individually. """ def key(self) -> Optional[Node]: ... def value(self) -> Optional[Node]: ... def key_matches(self, key: str) -> bool: ... def set_value(self, value: ScalarValue) -> None: ... def remove(self) -> None: ... class Mapping: """A YAML mapping (key/value block).""" def __len__(self) -> int: ... def is_empty(self) -> bool: ... def is_flow_style(self) -> bool: ... def contains_key(self, key: str) -> bool: ... def __contains__(self, key: str) -> bool: ... def get(self, key: str) -> Optional[Node]: ... def __getitem__(self, key: str) -> Node: ... def get_mapping(self, key: str) -> Optional[Mapping]: ... def get_sequence(self, key: str) -> Optional[Sequence]: ... def set(self, key: str, value: ScalarValue) -> None: ... def __setitem__(self, key: str, value: ScalarValue) -> None: ... def remove(self, key: str) -> bool: ... def __delitem__(self, key: str) -> None: ... def rename_key(self, old_key: str, new_key: str) -> bool: ... def clear(self) -> None: ... def keys(self) -> list[str]: ... def values(self) -> list[Node]: ... def items(self) -> list[tuple[str, Node]]: ... def __iter__(self) -> Iterator[str]: ... def entries(self) -> list[Entry]: ... def find_entry(self, key: str) -> Optional[Entry]: ... def find_all_entries(self, key: str) -> list[Entry]: ... def count_key(self, key: str) -> int: ... def remove_nth(self, key: str, n: int) -> bool: ... def __str__(self) -> str: ... class MergedMapping: """A read-only mapping view with aliases and merge keys (``<<``) resolved. Has no ``__str__``: a merged view has no single source span, so read its resolved contents through ``items``/``get`` rather than ``str()``. """ def __len__(self) -> int: ... def is_empty(self) -> bool: ... def contains_key(self, key: str) -> bool: ... def __contains__(self, key: str) -> bool: ... def get(self, key: str) -> Optional[Node]: ... def __getitem__(self, key: str) -> Node: ... def get_merged(self, key: str) -> Optional[MergedMapping]: ... def keys(self) -> list[str]: ... def values(self) -> list[Node]: ... def items(self) -> list[tuple[str, Node]]: ... def __iter__(self) -> Iterator[str]: ... class Sequence: """A YAML sequence (list).""" def __len__(self) -> int: ... def is_empty(self) -> bool: ... def is_flow_style(self) -> bool: ... def get(self, index: int) -> Optional[Node]: ... def __getitem__(self, index: int) -> Node: ... def first(self) -> Optional[Node]: ... def last(self) -> Optional[Node]: ... def push(self, value: ScalarValue) -> None: ... def insert(self, index: int, value: ScalarValue) -> None: ... def set(self, index: int, value: ScalarValue) -> bool: ... def __setitem__(self, index: int, value: ScalarValue) -> None: ... def __delitem__(self, index: int) -> None: ... def remove(self, index: int) -> Optional[Node]: ... def pop(self) -> Optional[Node]: ... def clear(self) -> None: ... def values(self) -> list[Node]: ... def __iter__(self) -> Iterator[Node]: ... def __str__(self) -> str: ... class Document: """A single YAML document.""" def __init__(self) -> None: ... @staticmethod def parse(text: str) -> Document: ... @staticmethod def from_file(path: str) -> Document: ... def to_file(self, path: str) -> None: ... def is_empty(self) -> bool: ... def as_mapping(self) -> Optional[Mapping]: ... def as_sequence(self) -> Optional[Sequence]: ... def as_scalar(self) -> Optional[Scalar]: ... def merged(self) -> Optional[MergedMapping]: ... def contains_key(self, key: str) -> bool: ... def __contains__(self, key: str) -> bool: ... def get(self, key: str) -> Optional[Node]: ... def __getitem__(self, key: str) -> Node: ... def get_string(self, key: str) -> Optional[str]: ... def set(self, key: str, value: ScalarValue) -> None: ... def __setitem__(self, key: str, value: ScalarValue) -> None: ... def remove(self, key: str) -> bool: ... def __delitem__(self, key: str) -> None: ... def rename_key(self, old_key: str, new_key: str) -> bool: ... def keys(self) -> list[str]: ... def __str__(self) -> str: ... class YamlFile: """A YAML stream, which may hold multiple documents and directives.""" def __init__(self) -> None: ... @staticmethod def parse(text: str) -> YamlFile: ... @staticmethod def from_file(path: str) -> YamlFile: ... def __len__(self) -> int: ... def is_empty(self) -> bool: ... def documents(self) -> list[Document]: ... def document(self) -> Optional[Document]: ... def ensure_document(self) -> Document: ... def push_document(self, document: Document) -> None: ... def directives(self) -> list[str]: ... def add_directive(self, directive_text: str) -> None: ... def __str__(self) -> str: ... python-yaml-edit_0.1.2.orig/python/yaml_edit/py.typed0000644000000000000000000000000015210544653017647 0ustar00python-yaml-edit_0.1.2.orig/src/lib.rs0000644000000000000000000006521615212767342014613 0ustar00//! Python bindings for the `yaml-edit` crate. //! //! The wrapper types in `yaml-edit` are lightweight views over a shared rowan //! syntax tree. rowan uses `Rc` internally, so the wrappers are neither `Send` //! nor `Sync`; every `#[pyclass]` here is declared `unsendable`, which makes //! PyO3 enforce single-thread access at runtime. use pyo3::exceptions::{PyIndexError, PyKeyError, PyTypeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyBool, PyFloat, PyInt, PyString}; use std::str::FromStr; use yaml_edit::anchor_resolution::{AnchorRegistry, DocumentMergedExt, MergedMapping}; use yaml_edit::{Document, Mapping, MappingEntry, Scalar, Sequence, YamlFile, YamlNode}; /// A value accepted where YAML expects a scalar, mirroring the `AsYaml` /// implementations available in the crate (`str`, integers, floats, `bool`). enum ScalarArg { Str(String), Int(i64), Float(f64), Bool(bool), } impl ScalarArg { /// Extract a scalar argument from a Python object. /// /// `bool` is checked before `int` because Python's `bool` is a subclass of /// `int`. `None` is rejected: the high-level crate API has no way to write a /// YAML null, so we surface that rather than writing a quoted "null" string. fn extract(obj: &Bound<'_, PyAny>) -> PyResult { if obj.is_none() { return Err(PyTypeError::new_err( "None is not supported as a YAML value; the editor cannot write null scalars", )); } if obj.is_instance_of::() { return Ok(ScalarArg::Bool(obj.extract()?)); } if obj.is_instance_of::() { return Ok(ScalarArg::Int(obj.extract()?)); } if obj.is_instance_of::() { return Ok(ScalarArg::Float(obj.extract()?)); } if obj.is_instance_of::() { return Ok(ScalarArg::Str(obj.extract()?)); } Err(PyTypeError::new_err(format!( "expected str, int, float or bool, got {}", obj.get_type().name()?, ))) } } /// Apply an `AsYaml`-consuming method with the scalar argument as the matching /// concrete type. /// /// `yaml-edit`'s value methods are generic over `impl AsYaml`, and trait objects /// (`&dyn AsYaml`) do not themselves implement `AsYaml`, so the call must be /// monomorphized per arm rather than dispatched dynamically. macro_rules! call_with_scalar { ($arg:expr, |$v:ident| $body:expr) => { match $arg { ScalarArg::Str(s) => { let $v = s.as_str(); $body } ScalarArg::Int(i) => { let $v = i; $body } ScalarArg::Float(x) => { let $v = x; $body } ScalarArg::Bool(b) => { let $v = b; $body } } }; } /// Wrap a YAML error as a Python `ValueError`. fn map_err(err: yaml_edit::YamlError) -> PyErr { PyValueError::new_err(err.to_string()) } /// A YAML node returned from a getter: scalar, mapping, sequence, or other. /// /// This is a read view; mutate through the owning `Mapping`/`Sequence`/`Document`. #[pyclass(name = "Node", unsendable, module = "yaml_edit._yaml_edit")] struct PyNode { inner: YamlNode, } #[pymethods] impl PyNode { /// The kind of node: "scalar", "mapping", "sequence", "alias" or "tagged". #[getter] fn kind(&self) -> &'static str { match self.inner { YamlNode::Scalar(_) => "scalar", YamlNode::Mapping(_) => "mapping", YamlNode::Sequence(_) => "sequence", YamlNode::Alias(_) => "alias", YamlNode::TaggedNode(_) => "tagged", } } fn is_scalar(&self) -> bool { self.inner.is_scalar() } fn is_mapping(&self) -> bool { self.inner.is_mapping() } fn is_sequence(&self) -> bool { self.inner.is_sequence() } /// View this node as a `Scalar`, or `None` if it is not a scalar. fn as_scalar(&self) -> Option { self.inner .as_scalar() .map(|s| PyScalar { inner: s.clone() }) } /// View this node as a `Mapping`, or `None` if it is not a mapping. fn as_mapping(&self) -> Option { self.inner .as_mapping() .map(|m| PyMapping { inner: m.clone() }) } /// View this node as a `Sequence`, or `None` if it is not a sequence. fn as_sequence(&self) -> Option { self.inner .as_sequence() .map(|s| PySequence { inner: s.clone() }) } /// The node's value as an `int`, if it is an integer scalar. fn as_int(&self) -> Option { self.inner.to_i64() } /// The node's value as a `float`, if it is a numeric scalar. fn as_float(&self) -> Option { self.inner.to_f64() } /// The node's value as a `bool`, if it is a boolean scalar. fn as_bool(&self) -> Option { self.inner.to_bool() } /// The node's source text, including any surrounding formatting. fn __str__(&self) -> String { self.inner.to_string() } fn __repr__(&self) -> String { format!("", self.kind(), self.__str__()) } } /// A scalar YAML value (string, number, boolean). #[pyclass(name = "Scalar", unsendable, module = "yaml_edit._yaml_edit")] struct PyScalar { inner: Scalar, } #[pymethods] impl PyScalar { /// The raw scalar text, preserving any surrounding quotes. fn value(&self) -> String { self.inner.value() } /// The scalar's value as a string, with quotes stripped if present. fn as_string(&self) -> String { self.inner.as_string() } fn is_quoted(&self) -> bool { self.inner.is_quoted() } fn unquoted_value(&self) -> String { self.inner.unquoted_value() } fn is_null(&self) -> bool { self.inner.is_null() } fn as_int(&self) -> Option { self.inner.as_i64() } fn as_float(&self) -> Option { self.inner.as_f64() } fn as_bool(&self) -> Option { self.inner.as_bool() } /// Replace the scalar's value in place, preserving surrounding formatting. fn set_value(&self, value: &str) { self.inner.set_value(value); } fn __str__(&self) -> String { self.inner.as_string() } fn __repr__(&self) -> String { format!("", self.inner.as_string()) } } /// A single key/value entry in a mapping. /// /// Entries are the fine-grained handle for working with mappings that contain /// repeated keys: each occurrence of a key is a distinct entry, so they can be /// inspected and mutated individually rather than collapsing to the first match. #[pyclass(name = "Entry", unsendable, module = "yaml_edit._yaml_edit")] struct PyEntry { inner: MappingEntry, /// Whether the owning mapping is flow-style, needed when rewriting the value. flow_context: bool, } #[pymethods] impl PyEntry { /// The entry's key node, or `None` for a malformed entry with no key. fn key(&self) -> Option { self.inner.key_node().map(|inner| PyNode { inner }) } /// The entry's value node, or `None` for a malformed entry with no value. fn value(&self) -> Option { self.inner.value_node().map(|inner| PyNode { inner }) } /// Return `True` if this entry's key matches `key` (ignoring quoting style). fn key_matches(&self, key: &str) -> bool { self.inner.key_matches(key) } /// Replace this entry's value in place, preserving the key and formatting. fn set_value(&self, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; call_with_scalar!(&arg, |v| self.inner.set_value(v, self.flow_context)); Ok(()) } /// Remove this entry from its mapping. /// /// The entry handle is detached from the tree; calling other methods on it /// afterwards operates on a node that is no longer part of the document. fn remove(&self) { self.inner.clone().remove(); } fn __repr__(&self) -> String { let key = self .inner .key_node() .map(|k| k.to_string()) .unwrap_or_default(); format!("") } } /// A YAML mapping (key/value block). #[pyclass(name = "Mapping", unsendable, module = "yaml_edit._yaml_edit")] struct PyMapping { inner: Mapping, } #[pymethods] impl PyMapping { fn __len__(&self) -> usize { self.inner.len() } fn is_empty(&self) -> bool { self.inner.is_empty() } fn is_flow_style(&self) -> bool { self.inner.is_flow_style() } fn contains_key(&self, key: &str) -> bool { self.inner.contains_key(key) } fn __contains__(&self, key: &str) -> bool { self.inner.contains_key(key) } /// Look up a key, returning its value node or `None` if absent. fn get(&self, key: &str) -> Option { self.inner.get(key).map(|inner| PyNode { inner }) } fn __getitem__(&self, key: &str) -> PyResult { self.inner .get(key) .map(|inner| PyNode { inner }) .ok_or_else(|| PyKeyError::new_err(key.to_string())) } /// Look up a key whose value is a nested mapping. fn get_mapping(&self, key: &str) -> Option { self.inner.get_mapping(key).map(|inner| PyMapping { inner }) } /// Look up a key whose value is a nested sequence. fn get_sequence(&self, key: &str) -> Option { self.inner .get_sequence(key) .map(|inner| PySequence { inner }) } /// Set `key` to `value`, inserting a new entry or updating an existing one. fn set(&self, key: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; call_with_scalar!(&arg, |v| self.inner.set(key, v)); Ok(()) } fn __setitem__(&self, key: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { self.set(key, value) } /// Remove `key`, returning `True` if it was present. fn remove(&self, key: &str) -> bool { self.inner.remove(key).is_some() } fn __delitem__(&self, key: &str) -> PyResult<()> { if self.inner.remove(key).is_some() { Ok(()) } else { Err(PyKeyError::new_err(key.to_string())) } } /// Rename `old_key` to `new_key`, returning `True` on success. fn rename_key(&self, old_key: &str, new_key: &str) -> bool { self.inner.rename_key(old_key, new_key) } fn clear(&self) { self.inner.clear(); } /// The mapping's keys, in document order. fn keys(&self) -> Vec { self.inner.keys().map(|k| k.to_string()).collect() } /// The mapping's value nodes, in document order. fn values(&self) -> Vec { self.inner.values().map(|inner| PyNode { inner }).collect() } /// (key, value) pairs, in document order. fn items(&self) -> Vec<(String, PyNode)> { self.inner .iter() .map(|(k, v)| (k.to_string(), PyNode { inner: v })) .collect() } /// Iterate over the keys, like a Python `dict`. fn __iter__(&self) -> PyKeyIterator { PyKeyIterator { keys: self.keys().into_iter(), } } /// All entries, in document order, as fine-grained handles. /// /// Unlike `items`, this exposes every occurrence of a repeated key as its /// own entry, and lets each be mutated in place. fn entries(&self) -> Vec { let flow_context = self.inner.is_flow_style(); self.inner .entries() .map(|inner| PyEntry { inner, flow_context, }) .collect() } /// The first entry whose key matches `key`, or `None` if absent. fn find_entry(&self, key: &str) -> Option { let flow_context = self.inner.is_flow_style(); self.inner.find_entry_by_key(key).map(|inner| PyEntry { inner, flow_context, }) } /// Every entry whose key matches `key`, in document order. /// /// YAML permits repeated keys; this returns one handle per occurrence. fn find_all_entries(&self, key: &str) -> Vec { let flow_context = self.inner.is_flow_style(); self.inner .find_all_entries_by_key(key) .map(|inner| PyEntry { inner, flow_context, }) .collect() } /// The number of entries whose key matches `key`. fn count_key(&self, key: &str) -> usize { self.inner.find_all_entries_by_key(key).count() } /// Remove the `n`-th (0-based) occurrence of `key`, returning `True` if it /// existed. fn remove_nth(&self, key: &str, n: usize) -> bool { self.inner.remove_nth_occurrence(key, n).is_some() } fn __str__(&self) -> String { self.inner.to_string() } fn __repr__(&self) -> String { format!("", self.inner.len()) } } /// Iterator over a mapping's keys, returned by `Mapping.__iter__`. #[pyclass(module = "yaml_edit._yaml_edit")] struct PyKeyIterator { keys: std::vec::IntoIter, } #[pymethods] impl PyKeyIterator { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } fn __next__(&mut self) -> Option { self.keys.next() } } /// A read-only view of a mapping with aliases and merge keys (`<<`) resolved. /// /// Obtained from `Document.merged()`. Direct entries take precedence over keys /// contributed by merge sources, the synthetic `<<` key is hidden, and aliases /// (`*name`) are expanded to their anchored values. /// /// The view does not mutate the document; mutate through the original /// `Mapping`/`Document` instead. /// /// Unlike the other collection types, this has no `__str__`: a merged view is /// synthetic and has no single source span, so serializing it would have to /// emit the un-merged source and hide the merged-in keys. Read the resolved /// values through `items`/`get` instead. #[pyclass(name = "MergedMapping", unsendable, module = "yaml_edit._yaml_edit")] struct PyMergedMapping { base: Mapping, registry: AnchorRegistry, } impl PyMergedMapping { /// Borrow `base` and `registry` as a `MergedMapping` for a single query. fn view(&self) -> MergedMapping<'_> { MergedMapping::new(self.base.clone(), &self.registry) } } #[pymethods] impl PyMergedMapping { fn __len__(&self) -> usize { self.view().len() } fn is_empty(&self) -> bool { self.view().is_empty() } fn contains_key(&self, key: &str) -> bool { self.view().contains_key(key) } fn __contains__(&self, key: &str) -> bool { self.view().contains_key(key) } /// Look up a key, resolving aliases and merge keys, or `None` if absent. fn get(&self, key: &str) -> Option { self.view().get(key).map(|inner| PyNode { inner }) } fn __getitem__(&self, key: &str) -> PyResult { self.view() .get(key) .map(|inner| PyNode { inner }) .ok_or_else(|| PyKeyError::new_err(key.to_string())) } /// Look up a nested mapping, returning another merged view that resolves /// against the same anchors. fn get_merged(&self, key: &str) -> Option { self.view().get_merged(key).map(|nested| PyMergedMapping { base: nested.base().clone(), registry: self.registry.clone(), }) } /// The merged keys, in resolution order (direct entries first). fn keys(&self) -> Vec { self.view().keys().map(|k| k.to_string()).collect() } /// The merged value nodes, in resolution order. fn values(&self) -> Vec { self.view().values().map(|inner| PyNode { inner }).collect() } /// (key, value) pairs in the merged view, in resolution order. fn items(&self) -> Vec<(String, PyNode)> { self.view() .iter() .map(|(k, v)| (k.to_string(), PyNode { inner: v })) .collect() } /// Iterate over the merged keys, like a Python `dict`. fn __iter__(&self) -> PyKeyIterator { PyKeyIterator { keys: self.keys().into_iter(), } } fn __repr__(&self) -> String { format!("", self.view().len()) } } /// A YAML sequence (list). #[pyclass(name = "Sequence", unsendable, module = "yaml_edit._yaml_edit")] struct PySequence { inner: Sequence, } #[pymethods] impl PySequence { fn __len__(&self) -> usize { self.inner.len() } fn is_empty(&self) -> bool { self.inner.is_empty() } fn is_flow_style(&self) -> bool { self.inner.is_flow_style() } /// Element at `index`, or `None` if out of range. fn get(&self, index: usize) -> Option { self.inner.get(index).map(|inner| PyNode { inner }) } fn __getitem__(&self, index: usize) -> PyResult { self.inner .get(index) .map(|inner| PyNode { inner }) .ok_or_else(|| PyIndexError::new_err("sequence index out of range")) } fn first(&self) -> Option { self.inner.first().map(|inner| PyNode { inner }) } fn last(&self) -> Option { self.inner.last().map(|inner| PyNode { inner }) } /// Append `value` to the end of the sequence. fn push(&self, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; call_with_scalar!(&arg, |v| self.inner.push(v)); Ok(()) } /// Insert `value` before `index`. fn insert(&self, index: usize, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; call_with_scalar!(&arg, |v| self.inner.insert(index, v)); Ok(()) } /// Replace the element at `index`, returning `True` on success. fn set(&self, index: usize, value: &Bound<'_, PyAny>) -> PyResult { let arg = ScalarArg::extract(value)?; Ok(call_with_scalar!(&arg, |v| self.inner.set(index, v))) } fn __setitem__(&self, index: usize, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; if call_with_scalar!(&arg, |v| self.inner.set(index, v)) { Ok(()) } else { Err(PyIndexError::new_err("sequence index out of range")) } } fn __delitem__(&self, index: usize) -> PyResult<()> { if self.inner.remove(index).is_some() { Ok(()) } else { Err(PyIndexError::new_err("sequence index out of range")) } } /// Remove and return the element at `index`. fn remove(&self, index: usize) -> Option { self.inner.remove(index).map(|inner| PyNode { inner }) } /// Remove and return the last element. fn pop(&self) -> Option { self.inner.pop().map(|inner| PyNode { inner }) } fn clear(&self) { self.inner.clear(); } /// The sequence's element nodes, in order. fn values(&self) -> Vec { self.inner.values().map(|inner| PyNode { inner }).collect() } fn __iter__(&self) -> PyNodeIterator { PyNodeIterator { nodes: self.values().into_iter(), } } fn __str__(&self) -> String { self.inner.to_string() } fn __repr__(&self) -> String { format!("", self.inner.len()) } } /// Iterator over a sequence's element nodes, returned by `Sequence.__iter__`. #[pyclass(unsendable, module = "yaml_edit._yaml_edit")] struct PyNodeIterator { nodes: std::vec::IntoIter, } #[pymethods] impl PyNodeIterator { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } fn __next__(&mut self) -> Option { self.nodes.next() } } /// A single YAML document. #[pyclass(name = "Document", unsendable, module = "yaml_edit._yaml_edit")] struct PyDocument { inner: Document, } #[pymethods] impl PyDocument { /// Create an empty document. #[new] fn new() -> Self { PyDocument { inner: Document::new(), } } /// Parse a document from a YAML string. #[staticmethod] fn parse(text: &str) -> PyResult { Document::from_str(text) .map(|inner| PyDocument { inner }) .map_err(map_err) } /// Parse a document from a file on disk. #[staticmethod] fn from_file(path: &str) -> PyResult { Document::from_file(path) .map(|inner| PyDocument { inner }) .map_err(map_err) } /// Write the document to a file on disk. fn to_file(&self, path: &str) -> PyResult<()> { self.inner.to_file(path).map_err(map_err) } fn is_empty(&self) -> bool { self.inner.is_empty() } /// The document root as a mapping, or `None` if it is not a mapping. fn as_mapping(&self) -> Option { self.inner.as_mapping().map(|inner| PyMapping { inner }) } /// The document root as a sequence, or `None` if it is not a sequence. fn as_sequence(&self) -> Option { self.inner.as_sequence().map(|inner| PySequence { inner }) } /// The document root as a scalar, or `None` if it is not a scalar. fn as_scalar(&self) -> Option { self.inner.as_scalar().map(|inner| PyScalar { inner }) } /// A merged view of the root mapping, resolving aliases and merge keys /// (`<<`), or `None` if the root is not a mapping. /// /// The anchor registry is built from the whole document, so aliases /// pointing at anchors defined elsewhere resolve correctly. fn merged(&self) -> Option { self.inner.merged().map(|view| PyMergedMapping { base: view.as_mapping().base().clone(), registry: view.registry().clone(), }) } fn contains_key(&self, key: &str) -> bool { self.inner.contains_key(key) } fn __contains__(&self, key: &str) -> bool { self.inner.contains_key(key) } /// Look up a top-level key, returning its value node or `None`. fn get(&self, key: &str) -> Option { self.inner.get(key).map(|inner| PyNode { inner }) } fn __getitem__(&self, key: &str) -> PyResult { self.inner .get(key) .map(|inner| PyNode { inner }) .ok_or_else(|| PyKeyError::new_err(key.to_string())) } /// The value of a top-level key as a string, or `None`. fn get_string(&self, key: &str) -> Option { self.inner.get_string(key) } /// Set a top-level key, treating the document root as a mapping. fn set(&self, key: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { let arg = ScalarArg::extract(value)?; call_with_scalar!(&arg, |v| self.inner.set(key, v)); Ok(()) } fn __setitem__(&self, key: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { self.set(key, value) } /// Remove a top-level key, returning `True` if it was present. fn remove(&self, key: &str) -> bool { self.inner.remove(key).is_some() } fn __delitem__(&self, key: &str) -> PyResult<()> { if self.inner.remove(key).is_some() { Ok(()) } else { Err(PyKeyError::new_err(key.to_string())) } } fn rename_key(&self, old_key: &str, new_key: &str) -> bool { self.inner.rename_key(old_key, new_key) } /// The top-level keys, in document order. fn keys(&self) -> Vec { self.inner.keys().map(|k| k.to_string()).collect() } /// Serialize the document back to a string, preserving formatting. fn __str__(&self) -> String { self.inner.to_string() } fn __repr__(&self) -> String { format!("", self.__str__()) } } /// A YAML stream, which may hold multiple documents and directives. #[pyclass(name = "YamlFile", unsendable, module = "yaml_edit._yaml_edit")] struct PyYamlFile { inner: YamlFile, } #[pymethods] impl PyYamlFile { /// Create an empty YAML stream. #[new] fn new() -> Self { PyYamlFile { inner: YamlFile::new(), } } /// Parse a YAML stream from a string. #[staticmethod] fn parse(text: &str) -> PyResult { YamlFile::from_str(text) .map(|inner| PyYamlFile { inner }) .map_err(map_err) } /// Parse a YAML stream from a file on disk. #[staticmethod] fn from_file(path: &str) -> PyResult { YamlFile::from_path(path) .map(|inner| PyYamlFile { inner }) .map_err(map_err) } fn __len__(&self) -> usize { self.inner.documents().count() } fn is_empty(&self) -> bool { self.inner.documents().next().is_none() } /// The documents in this stream. fn documents(&self) -> Vec { self.inner .documents() .map(|inner| PyDocument { inner }) .collect() } /// The first document, or `None` if the stream is empty. fn document(&self) -> Option { self.inner.document().map(|inner| PyDocument { inner }) } /// Return the first document, creating one if the stream is empty. fn ensure_document(&self) -> PyDocument { PyDocument { inner: self.inner.ensure_document(), } } /// Append a document to the stream. fn push_document(&self, document: &PyDocument) { self.inner.push_document(document.inner.clone()); } /// The directive lines (e.g. "%YAML 1.2") in this stream. fn directives(&self) -> Vec { self.inner.directives().map(|d| d.to_string()).collect() } /// Add a directive line such as "%YAML 1.2". fn add_directive(&self, directive_text: &str) { self.inner.add_directive(directive_text); } /// Serialize the whole stream back to a string, preserving formatting. fn __str__(&self) -> String { self.inner.to_string() } fn __repr__(&self) -> String { format!( "", self.inner.documents().count() ) } } #[pymodule] fn _yaml_edit(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) } python-yaml-edit_0.1.2.orig/tests/test_yaml_edit.py0000644000000000000000000003211315212767342017420 0ustar00import unittest from yaml_edit import ( Document, Entry, Mapping, MergedMapping, Scalar, Sequence, YamlFile, ) class DocumentTests(unittest.TestCase): def test_parse_and_roundtrip(self): text = "name: demo\nversion: 1.0.0\n" doc = Document.parse(text) self.assertEqual(str(doc), text) def test_parse_error(self): with self.assertRaises(ValueError): Document.parse("a: [1, 2\n") def test_set_preserves_formatting(self): doc = Document.parse("name: old # the name\nversion: 1.0.0\n") doc.set("name", "new") self.assertEqual(str(doc), "name: new # the name\nversion: 1.0.0\n") def test_set_new_key(self): doc = Document.parse("name: demo\n") doc.set("version", "1.0.0") self.assertEqual(doc.get_string("version"), "1.0.0") def test_setitem_and_getitem(self): doc = Document.parse("name: demo\n") doc["name"] = "renamed" self.assertEqual(str(doc["name"]), "renamed") def test_getitem_missing_raises_keyerror(self): doc = Document.parse("name: demo\n") with self.assertRaises(KeyError): doc["missing"] def test_keys_and_contains(self): doc = Document.parse("a: 1\nb: 2\nc: 3\n") self.assertEqual(doc.keys(), ["a", "b", "c"]) self.assertIn("b", doc) self.assertNotIn("z", doc) def test_remove(self): doc = Document.parse("a: 1\nb: 2\n") self.assertTrue(doc.remove("a")) self.assertFalse(doc.remove("a")) self.assertEqual(doc.keys(), ["b"]) def test_delitem_missing_raises_keyerror(self): doc = Document.parse("a: 1\n") with self.assertRaises(KeyError): del doc["missing"] def test_rename_key(self): doc = Document.parse("old: 1\n") self.assertTrue(doc.rename_key("old", "new")) self.assertEqual(doc.keys(), ["new"]) def test_typed_scalars(self): doc = Document() doc.set("count", 42) doc.set("ratio", 1.5) doc.set("enabled", True) self.assertEqual(doc.get("count").as_int(), 42) self.assertEqual(doc.get("ratio").as_float(), 1.5) self.assertEqual(doc.get("enabled").as_bool(), True) def test_bool_not_treated_as_int(self): doc = Document() doc.set("flag", True) self.assertEqual(str(doc["flag"]), "true") def test_none_rejected(self): doc = Document() with self.assertRaises(TypeError): doc.set("x", None) def test_unsupported_type_rejected(self): doc = Document() with self.assertRaises(TypeError): doc.set("x", [1, 2, 3]) class MappingTests(unittest.TestCase): def test_as_mapping(self): doc = Document.parse("a: 1\nb: 2\n") mapping = doc.as_mapping() self.assertIsInstance(mapping, Mapping) self.assertEqual(len(mapping), 2) def test_items(self): doc = Document.parse("a: 1\nb: 2\n") items = doc.as_mapping().items() self.assertEqual([k for k, _ in items], ["a", "b"]) self.assertEqual([str(v) for _, v in items], ["1", "2"]) def test_nested_sequence_mutation_visible_through_doc(self): doc = Document.parse("tags:\n - a\n - b\n") seq = doc.as_mapping().get_sequence("tags") seq.push("c") self.assertEqual(str(doc), "tags:\n - a\n - b\n - c\n") def test_get_mapping(self): doc = Document.parse("outer:\n inner: 1\n") inner = doc.as_mapping().get_mapping("outer") self.assertEqual(inner.keys(), ["inner"]) def test_clear(self): doc = Document.parse("a: 1\nb: 2\n") mapping = doc.as_mapping() mapping.clear() self.assertTrue(mapping.is_empty()) def test_iter_yields_keys(self): doc = Document.parse("a: 1\nb: 2\nc: 3\n") mapping = doc.as_mapping() self.assertEqual(list(mapping), ["a", "b", "c"]) def test_iter_in_comprehension(self): doc = Document.parse("a: 1\nb: 2\n") self.assertEqual({k for k in doc.as_mapping()}, {"a", "b"}) def test_iter_returns_self(self): doc = Document.parse("a: 1\nb: 2\n") it = iter(doc.as_mapping()) self.assertIs(iter(it), it) self.assertEqual(next(it), "a") self.assertEqual(next(it), "b") with self.assertRaises(StopIteration): next(it) class MappingEntryTests(unittest.TestCase): def test_entries_expose_each_pair(self): doc = Document.parse("a: 1\nb: 2\n") entries = doc.as_mapping().entries() self.assertEqual([str(e.key()) for e in entries], ["a", "b"]) self.assertEqual([str(e.value()) for e in entries], ["1", "2"]) def test_find_entry(self): doc = Document.parse("a: 1\nb: 2\n") entry = doc.as_mapping().find_entry("b") self.assertIsInstance(entry, Entry) self.assertEqual(str(entry.value()), "2") def test_find_entry_missing(self): doc = Document.parse("a: 1\n") self.assertIsNone(doc.as_mapping().find_entry("z")) def test_repeated_keys_found_individually(self): doc = Document.parse("ref: first\nref: second\nref: third\n") mapping = doc.as_mapping() self.assertEqual(mapping.count_key("ref"), 3) entries = mapping.find_all_entries("ref") self.assertEqual( [str(e.value()) for e in entries], ["first", "second", "third"] ) def test_set_value_on_specific_occurrence(self): doc = Document.parse("ref: first\nref: second\n") entries = doc.as_mapping().find_all_entries("ref") entries[1].set_value("changed") self.assertEqual(str(doc), "ref: first\nref: changed\n") def test_remove_nth_occurrence(self): doc = Document.parse("ref: first\nref: second\nref: third\n") mapping = doc.as_mapping() self.assertTrue(mapping.remove_nth("ref", 1)) self.assertEqual(str(doc), "ref: first\nref: third\n") def test_remove_nth_out_of_range(self): doc = Document.parse("ref: only\n") self.assertFalse(doc.as_mapping().remove_nth("ref", 5)) def test_entry_remove(self): doc = Document.parse("a: 1\nb: 2\n") mapping = doc.as_mapping() mapping.find_entry("a").remove() self.assertEqual(mapping.keys(), ["b"]) def test_key_matches_ignores_quoting(self): doc = Document.parse('"a": 1\n') entry = doc.as_mapping().find_entry("a") self.assertTrue(entry.key_matches("a")) def test_repr(self): doc = Document.parse("a: 1\n") entry = doc.as_mapping().find_entry("a") self.assertEqual(repr(entry), '') class SequenceTests(unittest.TestCase): def test_as_sequence_and_indexing(self): doc = Document.parse("- a\n- b\n- c\n") seq = doc.as_sequence() self.assertIsInstance(seq, Sequence) self.assertEqual(len(seq), 3) self.assertEqual(str(seq[0]), "a") self.assertEqual(str(seq[2]), "c") def test_index_out_of_range(self): doc = Document.parse("- a\n") with self.assertRaises(IndexError): doc.as_sequence()[5] def test_push_pop(self): doc = Document.parse("- a\n") seq = doc.as_sequence() seq.push("b") self.assertEqual(len(seq), 2) popped = seq.pop() self.assertEqual(str(popped), "b") self.assertEqual(len(seq), 1) def test_first_last(self): doc = Document.parse("- a\n- b\n- c\n") seq = doc.as_sequence() self.assertEqual(str(seq.first()), "a") self.assertEqual(str(seq.last()), "c") def test_values(self): doc = Document.parse("- 1\n- 2\n") seq = doc.as_sequence() self.assertEqual([n.as_int() for n in seq.values()], [1, 2]) def test_iter(self): doc = Document.parse("- 1\n- 2\n- 3\n") seq = doc.as_sequence() self.assertEqual([n.as_int() for n in seq], [1, 2, 3]) def test_iter_returns_self(self): doc = Document.parse("- 1\n- 2\n") it = iter(doc.as_sequence()) self.assertIs(iter(it), it) self.assertEqual(next(it).as_int(), 1) self.assertEqual(next(it).as_int(), 2) with self.assertRaises(StopIteration): next(it) def test_delitem(self): doc = Document.parse("- a\n- b\n- c\n") seq = doc.as_sequence() del seq[1] self.assertEqual([str(n) for n in seq], ["a", "c"]) def test_delitem_out_of_range(self): doc = Document.parse("- a\n") with self.assertRaises(IndexError): del doc.as_sequence()[5] class ScalarTests(unittest.TestCase): def test_quoted(self): doc = Document.parse('name: "quoted"\n') scalar = doc["name"].as_scalar() self.assertIsInstance(scalar, Scalar) self.assertTrue(scalar.is_quoted()) self.assertEqual(scalar.value(), '"quoted"') self.assertEqual(scalar.unquoted_value(), "quoted") def test_set_value_in_place(self): doc = Document.parse("name: old\n") doc["name"].as_scalar().set_value("new") self.assertEqual(doc.get_string("name"), "new") class NodeTests(unittest.TestCase): def test_node_kinds(self): doc = Document.parse("m:\n k: v\ns:\n - 1\nx: scalar\n") mapping = doc.as_mapping() self.assertEqual(mapping.get("m").kind, "mapping") self.assertEqual(mapping.get("s").kind, "sequence") self.assertEqual(mapping.get("x").kind, "scalar") def test_node_views(self): doc = Document.parse("x: scalar\n") node = doc["x"] self.assertTrue(node.is_scalar()) self.assertIsNotNone(node.as_scalar()) self.assertIsNone(node.as_mapping()) class MergedMappingTests(unittest.TestCase): MERGE_YAML = ( "defaults: &d\n" " timeout: 30\n" " retries: 3\n" "prod:\n" " <<: *d\n" " host: prod.example.com\n" " timeout: 60\n" ) def test_non_mapping_root_returns_none(self): self.assertIsNone(Document.parse("- 1\n- 2\n").merged()) def test_merged_root_keys(self): merged = Document.parse(self.MERGE_YAML).merged() self.assertIsInstance(merged, MergedMapping) self.assertEqual(merged.keys(), ["defaults", "prod"]) def test_merge_key_brings_in_entries(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertEqual(prod.keys(), ["host", "timeout", "retries"]) self.assertEqual(len(prod), 3) self.assertFalse(prod.is_empty()) def test_direct_entry_overrides_merged(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertEqual(str(prod["timeout"]), "60") def test_merged_in_entry_is_visible(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertEqual(str(prod.get("retries")), "3") def test_merge_key_itself_is_hidden(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertIsNone(prod.get("<<")) self.assertNotIn("<<", prod) def test_contains_and_missing(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertIn("host", prod) self.assertTrue(prod.contains_key("host")) self.assertNotIn("absent", prod) self.assertFalse(prod.contains_key("absent")) self.assertIsNone(prod.get("absent")) def test_getitem_missing_raises_keyerror(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") with self.assertRaises(KeyError): prod["absent"] def test_items_and_iter(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertEqual( [(k, str(v)) for k, v in prod.items()], [("host", "prod.example.com"), ("timeout", "60"), ("retries", "3")], ) self.assertEqual(list(prod), ["host", "timeout", "retries"]) def test_values(self): prod = Document.parse(self.MERGE_YAML).merged().get_merged("prod") self.assertEqual( [str(v) for v in prod.values()], ["prod.example.com", "60", "3"], ) def test_alias_value_resolves(self): doc = Document.parse("config: &cfg\n port: 8080\nserver: *cfg\n") server = doc.merged().get_merged("server") self.assertEqual(str(server["port"]), "8080") def test_get_merged_missing_returns_none(self): merged = Document.parse(self.MERGE_YAML).merged() self.assertIsNone(merged.get_merged("absent")) class YamlFileTests(unittest.TestCase): def test_multiple_documents(self): stream = YamlFile.parse("a: 1\n---\nb: 2\n") self.assertEqual(len(stream), 2) self.assertEqual([d.keys() for d in stream.documents()], [["a"], ["b"]]) def test_roundtrip(self): text = "a: 1\n---\nb: 2\n" self.assertEqual(str(YamlFile.parse(text)), text) def test_first_document(self): stream = YamlFile.parse("a: 1\n---\nb: 2\n") self.assertEqual(stream.document().keys(), ["a"]) if __name__ == "__main__": unittest.main()