How to make a custom YAML tag work with a sequence

2019-07-25 12:55发布

I’ve got a yaml file with aliases like this:

vars:
  users: &users ['user1', 'user2', 'user3', 'user4']

refs:
  users: *users
  user: !rand ['user1', 'user2', 'user3', 'user4']

!rand is a custom tag that calls python’s random.choice on a yaml sequence.

I’m using the following implementation of the !rand tag:

import random
import yaml

def load_yaml(file):
    def rand_constructor(loader, node):
        value = loader.construct_sequence(node)
        return random.choice(value)

    yaml.add_constructor('!rand', rand_constructor)

    with open(file) as f:
        return yaml.load(f)

It works as expected, and user gets a random value from users. Now, when I use !rand with the alias *users it stops working:

vars:
  users: &users ['user1', 'user2', 'user3', 'user4']

refs:
  users: *users
  user: !rand *users

The error is:

File ../python3.6/site-packages/yaml/parser.py", line 439, in parse_block_mapping_key
    "expected <block end>, but found %r" % token.id, token.start_mark)
yaml.parser.ParserError: while parsing a block mapping
  in "/temp/config.yml", line 5, column 3
expected <block end>, but found '<alias>'
  in "/temp/config.yml", line 6, column 18

How do I make it work with sequence aliases?

2条回答
别忘想泡老子
2楼-- · 2019-07-25 13:48

In YAML, tags are designed to define content type. They are not designed to process content.

For this reason, you cannot tag an alias, since an alias is just a reference to content which has already been tagged at the location where it's defined. In your case, your sequence will get the !!seq tag according to the YAML core schema. YAML does not provide the ability to re-tag it.

Having said that, you can of course do a workaround: Wrap the !rand parameter in a sequence:

vars:
  users: &users ['user1', 'user2', 'user3', 'user4']

refs:
  users: *users
  user: !rand [*users]

You then just need to change your code to:

import random
import yaml

def load_yaml(file):
    def rand_constructor(loader, node):
        value = loader.construct_sequence(node)
        return random.choice(value[0])

    yaml.add_constructor('!rand', rand_constructor)

    with open(file) as f:
        return yaml.load(f)

But be aware that a direct call to !rand then looks like this:

vars:
  users: &users ['user1', 'user2', 'user3', 'user4']

refs:
  users: *users
  user: !rand [['user1', 'user2', 'user3', 'user4']]

See it like the outer sequence being the parameter list of the function !rand (which takes one parameter) and the inner sequence being that parameter.

查看更多
等我变得足够好
3楼-- · 2019-07-25 13:56

You can also make your list callable and call it each time you need random user:

YAML = """
user: &users !rand 
    - user1
    - user2
    - user3
    - user4
"""

import random
import yaml
from collections.abc import Sequence

class RandomizableList(Sequence):
    def __init__(self, items):
        self.items = items
    def __len__(self):
        return len(self.items)
    def __getitem__(self, value):
        return self.items[value]
    def __call__(self):
        return random.choice(self.items)
    def __repr__(self):
        return repr(self.items)

def rand_constructor(loader, node):
    return RandomizableList(loader.construct_sequence(node))

yaml.add_constructor('!rand', rand_constructor)
result = yaml.load(YAML)
for i in range(4):
    print(result['user']())
print(result['user'])
查看更多
登录 后发表回答