2025, Nov 20 15:02

Порядок ключей в ruamel.yaml и CommentedMap: безопасная итерация и автоправки без потери комментариев

Как ruamel.yaml и CommentedMap сохраняют порядок ключей при итерации, когда на это можно опираться в линтинге YAML и делать автоправки без потери комментариев.

Когда нужно линтить и автоматически исправлять внутренние YAML‑файлы, сохраняя встроенную документацию, оптимальным выбором будет ruamel.yaml. Одна из типичных задач в таких пайплайнах — проверка и соблюдение заданного порядка ключей в отображениях. Отсюда возникает простой вопрос: сохраняется ли порядок вставки при итерации по ключам в ruamel.yaml и можно ли безопасно опираться на это для проверок и автоправок?

Пример, иллюстрирующий вопрос

Предположим, вы загружаете YAML‑документ, в котором нужно сохранить комментарии, и хотите проверить порядок ключей в секции settings. YAML может выглядеть так:

settings:
  # foo
  key2:
    prop: value
  # bar
  key1:
    prop: value
  ...

И вы хотите привести его к виду:

settings:
  # bar
  key1:
    prop: value
  # foo
  key2:
    prop: value
  ...

Первым делом стоит посмотреть текущий порядок. Примерный фрагмент на ruamel.yaml может выглядеть так:

loader = YAML()
loader.preserve_quotes = True

doc_map = loader.load(path_ref)
sect = doc_map.get("settings", default=[])
if not isinstance(sect, CommentedMap):
    return

ordered_keys = list(sect.keys())

Что происходит под капотом

Для отображений вы получаете объект CommentedMap. Это подкласс ordereddict, который сегодня, в свою очередь, является подклассом collections.OrderedDict с добавленным методом .insert(). Вызов .keys() возвращает CommentedMapKeysView, удерживающий ссылку на исходное отображение. Итерация по этому представлению отдаёт ключи в том порядке, в котором они были вставлены в CommentedMap. Иными словами, формирование списка из .keys() сохраняет порядок вставки.

Есть важный нюанс, связанный с типами. Хотя во многих внутренних конфигурациях ключи в settings действительно строковые, по спецификации YAML любой скаляр или коллекция могут выступать ключом отображения. Поэтому в общем случае не следует жёстко задавать тип ключей как list[str]. Загрузчик корректно сделает круговое преобразование для записей вроде:

[2011, 10, 2]: anniversary

Решение

Опирайтесь на то, что ключи из CommentedMap выдаются в порядке вставки. Если нужно зафиксировать их для валидации или логики переупорядочивания, используйте .keys() и итерируйтесь по ним или приводите к list. Не предполагайте, что это всегда строки. Минимальный шаблон выглядит так:

yml = YAML()
yml.preserve_quotes = True

root_doc = yml.load(src_ref)
conf_section = root_doc.get("settings", default=[])
if isinstance(conf_section, CommentedMap):
    key_view = conf_section.keys()
    insertion_order = list(key_view)

Если планируете переставлять элементы, можно использовать поведение упорядочивания самой мапы: итерация по key_view отражает текущий порядок вставки — именно то, что нужно для сравнения с желаемой последовательностью.

Почему это важно

Когда вы автоматически исправляете конфигурационные файлы и при этом обязаны сохранить комментарии и форматирование, корректность зависит от предсказуемой семантики порядка. CommentedMap гарантирует, что обход ключей повторяет фактический порядок в документе, поэтому проверки и переписывания не разрушат структуру, которую вы хотите сохранить. Понимание того, что ключи в YAML не обязаны быть строками, также предотвращает скрытые ошибки из‑за неверных предположений о типах на валидных, но редких входных данных.

Выводы

Если вы загружаете YAML с помощью ruamel.yaml и получаете CommentedMap, итерация по его ключам идёт в порядке вставки. Это позволяет безопасно проверять и навязывать порядок ключей без потери комментариев. Просто не сужайте тип ключей до строк, если вы не полностью контролируете формат входных данных: YAML допускает не только простые строковые скаляры в качестве ключей.