Creando etiqueta personalizada en PyYAML

Estoy tratando de usar PyYAML de Python para crear una etiqueta personalizada que me permita recuperar las variables de entorno con mi YAML.

import os import yaml class EnvTag(yaml.YAMLObject): yaml_tag = u'!Env' def __init__(self, env_var): self.env_var = env_var def __repr__(self): return os.environ.get(self.env_var) settings_file = open('conf/defaults.yaml', 'r') settings = yaml.load(settings_file) 

Y dentro de defaults.yaml es simplemente:

 example: !ENV foo 

El error que sigo recibiendo:

 yaml.constructor.ConstructorError: could not determine a constructor for the tag '!ENV' in "defaults.yaml", line 1, column 10 

También planeo tener más de una etiqueta personalizada (asumiendo que puedo hacer que funcione)

Tu clase PyYAML tuvo algunos problemas:

  1. yaml_tag mayúsculas y minúsculas, por lo que !Env y !ENV son tags diferentes.
  2. Por lo tanto, según la documentación, yaml.YAMLObject utiliza yaml.YAMLObject para definirse a sí mismo, y tiene to_yaml predeterminadas de to_yaml y from_yaml para esos casos. Sin embargo, de forma predeterminada, esas funciones requieren que su argumento a su etiqueta personalizada (en este caso !ENV ) sea una asignación . Entonces, para trabajar con las funciones predeterminadas, su archivo defaults.yaml debe tener este aspecto (solo por ejemplo):

example: !ENV {env_var: "PWD", test: "test"}

Su código funcionará sin cambios, en mi caso, la print(settings) ahora da como resultado {'example': /home/Fred} Pero está usando load lugar de safe_load . En su respuesta a continuación, Anthon señaló que esto es peligroso. porque el YAML analizado puede sobrescribir / leer datos en cualquier parte del disco.

Aún puede usar fácilmente su formato de archivo YAML, por example: !ENV foo to_yaml example: !ENV foo solo tiene que definir un to_yaml y from_yaml apropiados en la clase EnvTag , los que pueden analizar y emitir variables escalares como la cadena “foo”.

Asi que:

 import os import yaml class EnvTag(yaml.YAMLObject): yaml_tag = u'!ENV' def __init__(self, env_var): self.env_var = env_var def __repr__(self): v = os.environ.get(self.env_var) or '' return 'EnvTag({}, contains={})'.format(self.env_var, v) @classmethod def from_yaml(cls, loader, node): return EnvTag(node.value) @classmethod def to_yaml(cls, dumper, data): return dumper.represent_scalar(cls.yaml_tag, data.env_var) # Required for safe_load yaml.SafeLoader.add_constructor('!ENV', EnvTag.from_yaml) # Required for safe_dump yaml.SafeDumper.add_multi_representer(EnvTag, EnvTag.to_yaml) settings_file = open('defaults.yaml', 'r') settings = yaml.safe_load(settings_file) print(settings) s = yaml.safe_dump(settings) print(s) 

Cuando este progtwig se ejecuta, genera:

 {'example': EnvTag(foo, contains=)} {example: !ENV 'foo'} 

Este código tiene la ventaja de (1) usar el pyyaml ​​original, por lo que no es necesario instalar nada más y (2) agregar un representante. 🙂

Me gustaría compartir cómo resolví esto como un addendum a las excelentes respuestas proporcionadas por Anthon y Fredrick Brennan. Gracias por tu ayuda.

En mi opinión, el documento PyYAML no tiene muy claro cuándo es posible que desee agregar un constructor a través de una clase (o “magia de metaclase” como se describe en el documento), lo que puede implicar la redefinición de from_yaml y to_yaml , o simplemente añadiendo un constructor usando yaml.add_constructor .

De hecho, el doc dice:

Puede definir sus propias tags específicas de la aplicación. La forma más fácil de hacerlo es definir una subclase de yaml.YAMLObject

Yo diría que lo contrario es cierto para casos de uso más simples. Así es como logré implementar mi etiqueta personalizada.

config / __ init__.py

 import yaml import os environment = os.environ.get('PYTHON_ENV', 'development') def __env_constructor(loader, node): value = loader.construct_scalar(node) return os.environ.get(value) yaml.add_constructor(u'!ENV', __env_constructor) # Load and Parse Config __defaults = open('config/defaults.yaml', 'r').read() __env_config = open('config/%s.yaml' % environment, 'r').read() __yaml_contents = ''.join([__defaults, __env_config]) __parsed_yaml = yaml.safe_load(__yaml_contents) settings = __parsed_yaml[environment] 

Con esto, ahora puedo tener un yaml separado para cada entorno usando un env PTYHON_ENV (default.yaml, development.yaml, test.yaml, production.yaml). Y cada uno puede ahora hacer referencia a las variables ENV.

Ejemplo default.yaml:

 defaults: &default app: host: '0.0.0.0' port: 500 

Ejemplo de producción.yaml:

 production: <<: *defaults app: host: !ENV APP_HOST port: !ENV APP_PORT 

Usar:

 from config import settings """ If PYTHON_ENV == 'production', prints value of APP_PORT If PYTHON_ENV != 'production', prints default 5000 """ print(settings['app']['port']) 

Hay varios problemas con su código:

  • !Env en su archivo YAML no es lo mismo que !ENV en su código.
  • Falta el classmethod from_yaml que se debe proporcionar para EnvTag .
  • Su documento YAML especifica un escalar para !Env , pero el mecanismo de subclasificación para yaml.YAMLObject llama a construct_yaml_object que a su vez llama a construct_mapping por lo que no se permite un escalar.
  • Estás utilizando .load() . Esto no es seguro , a menos que tenga un control completo sobre la entrada de YAML, ahora y en el futuro. No es seguro en el sentido de que YAML no controlado puede, por ejemplo, borrar o cargar cualquier información de su disco. PyYAML no te advierte de esa posible pérdida.
  • PyYAML solo es compatible con la mayoría de YAML 1.1, la última especificación de YAML es 1.2 (desde 2009).
  • Siempre debe sangrar su código en 4 espacios en cada nivel (o 3 espacios, pero no 4 en el primer nivel y 3 en el siguiente nivel).
  • su __repr__ no devuelve una cadena si la variable de entorno no está establecida, lo que generará un error.

Entonces cambia tu código a:

 import sys import os from ruamel import yaml yaml_str = """\ example: !Env foo """ class EnvTag: yaml_tag = u'!Env' def __init__(self, env_var): self.env_var = env_var def __repr__(self): return os.environ.get(self.env_var, '') @staticmethod def yaml_constructor(loader, node): return EnvTag(loader.construct_scalar(node)) yaml.add_constructor(EnvTag.yaml_tag, EnvTag.yaml_constructor, constructor=yaml.SafeConstructor) data = yaml.safe_load(yaml_str) print(data) os.environ['foo'] = 'Hello world!' print(data) 

lo que da:

 {'example': } {'example': Hello world!} 

Tenga en cuenta que estoy usando ruamel.yaml (descargo de responsabilidad: soy el autor de ese paquete), por lo que puede usar YAML 1.2 (o 1.1) en su archivo YAML. Con cambios menores, puedes hacer lo anterior con el antiguo PyYAML también.

Puede hacerlo subclasificando YAMLObject también, y de forma segura:

 import sys import os from ruamel import yaml yaml_str = """\ example: !Env foo """ yaml.YAMLObject.yaml_constructor = yaml.SafeConstructor class EnvTag(yaml.YAMLObject): yaml_tag = u'!Env' def __init__(self, env_var): self.env_var = env_var def __repr__(self): return os.environ.get(self.env_var, '') @classmethod def from_yaml(cls, loader, node): return EnvTag(loader.construct_scalar(node)) data = yaml.safe_load(yaml_str) print(data) os.environ['foo'] = 'Hello world!' print(data) 

Esto te dará los mismos resultados que arriba.