Python-биндинги для Ktav — простого формата конфигов. Форма JSON, кавычки не обязательны, без запятых, вложенность через точки в ключах. Под капотом — Rust.
Languages: English · Русский · 简体中文
Песочница: конвертация JSON / YAML / TOML / INI ⇄ Ktav прямо в браузере — ktav-lang.github.io.
Спецификация: этот пакет реализует Ktav. Формат версионируется
и поддерживается независимо от этого пакета — нормативный документ см.
в ktav-lang/spec.
pip install ktavГотовые wheel-ы публикуются для всех основных платформ и всех поддерживаемых версий Python:
- Linux (manylinux + musllinux) —
x86_64,aarch64 - macOS —
x86_64,arm64(Apple Silicon) - Windows —
x64,arm64
Требуется Python 3.9+. Wheel-ы собраны под стабильный ABI
(abi3-py39), поэтому одного wheel на платформу хватает для всех
поддерживаемых релизов CPython.
Если под вашу платформу нет готового wheel, pip откатится на исходники
и соберёт расширение локально — для этого нужен Rust toolchain
(rustup) и заголовочные файлы Python-разработки.
import ktav
src = """
service: web
port: 8080
ratio: 0.75
tls: true
tags: [
prod
eu-west-1
]
db.host: primary.internal
db.timeout: 30
"""
cfg = ktav.loads(src)
service: str = cfg["service"]
port: int = cfg["port"]
ratio: float = cfg["ratio"]
tls: bool = cfg["tls"]
tags: list[str] = cfg["tags"]
db_host: str = cfg["db"]["host"]
db_timeout: int = cfg["db"]["timeout"]for k, v in cfg.items():
if v is None:
kind = "null"
elif isinstance(v, bool):
kind = f"bool={v}" # bool first — True is also an int!
elif isinstance(v, int):
kind = f"int={v}"
elif isinstance(v, float):
kind = f"float={v}"
elif isinstance(v, str):
kind = f"str={v!r}"
elif isinstance(v, list):
kind = f"array({len(v)})"
elif isinstance(v, dict):
kind = f"object({len(v)})"
print(f"{k} -> {kind}")doc = {
"name": "frontend",
"port": 8443,
"tls": True,
"ratio": 0.95,
"upstreams": [
{"host": "a.example", "port": 1080},
{"host": "b.example", "port": 1080},
],
"notes": None,
}
text = ktav.dumps(doc)
# name: frontend
# port: 8443
# tls: true
# ratio: 0.95
# upstreams: [
# { host: a.example port: 1080 }
# { host: b.example port: 1080 }
# ]
# notes: nullПолный запускаемый пример — в examples/basic.py.
import ktav
print(ktav.format(open("config.ktav").read()))ktav.format переписывает документ в канонической форме (§ 5.9),
сохраняя каждый комментарий дословно. Серии пустых строк схлопываются в
одну, пустой отступ внутри скобок выбрасывается — поэтому форматирование
является неподвижной точкой и его безопасно ставить в pre-commit хук.
Порядок ключей не меняется никогда.
Канонический вывод дают три функции, и они различаются тем, что принимают и что сохраняют:
| вход | комментарии | пустые строки | |
|---|---|---|---|
ktav.format(src) |
исходный текст | сохраняются | сохраняются (серия схлопывается в одну) |
ktav.canonical_from_source(src) |
исходный текст | отбрасываются | отбрасываются |
ktav.emit_canonical(obj) |
объект Python | сохранять нечего | сохранять нечего |
Выбирайте по тому, что у вас в руках: emit_canonical — когда объект,
canonical_from_source — когда текст и не нужно строить промежуточное
значение Python, format — когда нужно сохранить собственные
комментарии и группировку документа.
В отличие от биндингов для JavaScript, canonical_from_source не даёт
здесь дополнительной численной точности: Python различает int и
float, поэтому различие Integer/Float из Ktav переживает loads, и
emit_canonical(loads(src)) выдаёт те же байты, что и
canonical_from_source(src). Оба пишут float по каноническому правилу
(§ 5.9.8), а не копируют исходную запись, поэтому
1.23456789012345678901 превращается в 1.2345678901234567 любым из
путей.
ktav.dumps_force_strings(obj) выводит как dumps, но приводит каждый
leaf-скаляр — integer, float, bool, null — к String через сырой маркер
::. Составные значения сохраняют структуру.
Четыре точки входа повторяют стандартный модуль json:
| Функция | Назначение |
|---|---|
ktav.loads(s) |
Разобрать строку Ktav (или UTF-8 bytes). |
ktav.dumps(obj) |
Сериализовать нативное значение Python. |
ktav.load(fp) |
Разобрать из файло-подобного объекта. |
ktav.dump(obj, fp) |
Сериализовать в файло-подобный объект. |
load / dump принимают файлы как в текстовом, так и в бинарном режиме.
Для проверки на границах доверия ktav.loads_strict(s) применяет
канонические правила спецификации для скаляров и выбрасывает
KtavDecodeError при потере записи скаляра. Формы canonical writer,
например 1e-3 и 1e10, принимаются и дают те же нативные значения,
что и loads.
| Ktav | Python |
|---|---|
null |
None |
true / false |
bool |
| голое целое | int |
| голое десятичное | float |
| прочий скаляр | str |
[ ... ] |
list |
{ ... } |
dict |
Ktav типизирует числа по лексической форме — голый port: 8080
это int, ratio: 0.5 — float, а всё, что не является голым
числом, остаётся str. Чтобы число-подобное значение осталось
строкой, форсируйте его через :: (zip:: 01007).
dict сохраняет порядок вставки (гарантия Python 3.7+), что совпадает с
семантикой упорядоченных объектов Ktav.
Сериализация — обратное соответствие:
- Python
int→ голое целое (в том числе bigint произвольной точности). - Python
float→ голое десятичное (точка всегда присутствует;NaN/±Infinityотвергаются — Ktav их не представляет). - Python
tupleдопустим как массив, симметричноlist. - Ключи
dict, не являющиесяstr, вызываютKtavEncodeError.
Начиная со spec 0.6.4, литеральные . или : внутри сегмента ключа
записываются через обратный слеш:
a\.b: v
a\:b: v
x.y\.z: v
a."b.c".d: v
"\u0041": v
Литеральный обратный слеш в ключе пишется как \\. Начиная со spec 0.7,
сегмент можно также взять в двойные, одинарные кавычки или обратные апострофы.
Кавычки ограничивают только сегменты ключа, не значения; \uXXXX декодирует
кодовую точку Unicode.
import ktav
try:
ktav.loads("x: [")
except ktav.KtavDecodeError as e:
print("decode:", e)
try:
ktav.dumps({"v": float("nan")})
except ktav.KtavEncodeError as e:
print("encode:", e)
# Catching the base class catches either.
try:
ktav.loads("a: 1\na: 2")
except ktav.KtavError:
...| Исключение | Источник | База |
|---|---|---|
KtavError |
(базовое) | Exception |
KtavDecodeError |
loads / load |
KtavError |
KtavEncodeError |
dumps / dump |
KtavError |
Начиная с 0.7.1 каждое брошенное исключение несёт ещё и структурный
конверт ошибки в виде атрибутов: error, reason, line, line_text,
span, path, body, canonical, spec_section. Отсутствующие
сведения — None, а не пропущенный атрибут. span — это
{"start": …, "end": …}, смещения в байтах по UTF-8-исходнику;
path — список точных декодированных сегментов ключа (ключ, буквально
названный a.b, — это один сегмент, и он никогда не разрезается).
str(e) остаётся человекочитаемым сообщением — сырой конверт вместо
него не подставляется.
e.message несёт тот же текст атрибутом, то есть str(e) == e.message. Здесь он избыточен намеренно: этот биндинг всегда брал для
str(e) собственный рендеринг ядра, а атрибут существует ради того,
чтобы конверт в Python имел ту же форму из десяти полей, что и в любом
другом языке, — там message единственный способ добраться до текста.
Пользуйтесь им, а не собирайте фразу заново из error, line и body:
пересобранное сообщение отличается от биндинга к биндингу, а это — нет.
Ktav намеренно маленький. Пять принципов проектирования
(из spec/CONTRIBUTING.md):
- Локальность — смысл строки не зависит от другой строки.
- Одно предложение — новое правило умещается в одну фразу спеки.
- Нет чувствительности к пробелам (кроме переноса строк).
- Никакой магии в типах — формат не решает, что
"8080"— число. - Явно лучше, чем хитро —
::избыточен намеренно.
Python-биндинги следуют этому: никакой inference-ы схемы, никакого
авто-каста, никаких значений по умолчанию. Хотите типизацию — делайте её
на границе своим инструментом (pydantic, dataclasses, attrs)
поверх нативных Python-структур, которые вернула эта библиотека.
spec— спецификация + conformance-тестыrust— эталонный Rust crate (cargo add ktav); этот Python-биндинг — тонкая PyO3-обёртка над нимcsharp— C# / .NET (dotnet add package Ktav)golang— Go (go get github.com/ktav-lang/golang)java— Java / JVM (io.github.ktav-lang:ktavна Maven Central)js— JS / TS (npm install @ktav-lang/ktav)php— PHP (composer require ktav-lang/ktav)
Пакет следует Semantic Versioning с pre-1.0
соглашением: минорный bump — ломающий. Версия пакета и версия крейта
ktav движутся вместе. ktav.__spec_version__ показывает версию
формата Ktav, которую поддерживает данный биндинг.
Настройка dev-окружения, структура тестов и процесс внесения вклада описаны в CONTRIBUTING.md (и в CONTRIBUTING.ru.md).
У автора много идей, которые могут быть полезны IT во всём мире, — и далеко не только для Ktav. Их реализация требует финансирования. Если вы хотите помочь — пишите на phpcraftdream@gmail.com.
MIT OR Apache-2.0. См. LICENSE-MIT и LICENSE-APACHE.