Deserialização estrita de YAML em Python com biblioteca marshmallow

Tarefa inicial



  • É necessário ler uma configuração não trivial do arquivo .yaml.
  • A estrutura de configuração é descrita usando classes de dados.
  • É necessário que as verificações de tipo sejam executadas durante a desserialização e uma exceção seja lançada se os dados forem inválidos.


Ou seja, para simplificar, você precisa de uma função da forma:







def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
    """
    Here is some magic
    """
    pass
      
      





E esta função será usada assim:







@dataclass
class MyConfig:
    """
    Here is object tree
    """
    pass

try:
    config = strict_load_yamp(open("config.yaml", "w").read(), MyConfig)
except Exception:
    logging.exception("Config is invalid")
      
      





Classes de configuração



O arquivo config.py



tem a seguinte aparência:







from dataclasses import dataclass
from enum import Enum
from typing import Optional

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@dataclass
class BattleStationConfig:
    @dataclass
    class Processor:
        core_count: int
        manufacturer: str

    processor: Processor
    memory_gb: int
    led_color: Optional[Color] = None

      
      





Opção que não funciona



O problema original é comum, não é? Portanto, a solução deve ser trivial. Basta importar a biblioteca padrão do yaml e pronto?







PyYaml load



:







from pprint import pprint

from yaml import load, SafeLoader

yaml = """
processor:
  core_count: 8
  manufacturer: Intel
memory_gb: 8
led_color: red
"""

loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)

      
      





:







{'led_color': 'red',
 'memory_gb': 8,
 'processor': {'core_count': 8, 'manufacturer': 'Intel'}}
      
      





Yaml , . , **args



:







parsed_config = BattleStationConfig(**loaded)
pprint(parsed_config)
      
      





:







BattleStationConfig(processor={'core_count': 8, 'manufacturer': 'Intel'}, memory_gb=8, led_color='red')
      
      





! ! … -. processor ? .







Python Processor



. stackowerflow.







, yaml-



stackowerflow PyYaml , yaml- . YAMLObject



, config_with_tag.py



:







from dataclasses import dataclass
from enum import Enum
from typing import Optional

from yaml import YAMLObject, SafeLoader

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@dataclass
class BattleStationConfig(YAMLObject):
    yaml_tag = "!BattleStationConfig"
    yaml_loader = SafeLoader

    @dataclass
    class Processor(YAMLObject):
        yaml_tag = "!Processor"
        yaml_loader = SafeLoader

        core_count: int
        manufacturer: str

    processor: Processor
    memory_gb: int
    led_color: Optional[Color] = None
      
      





:







from pprint import pprint

from yaml import load, SafeLoader

from config_with_tag import BattleStationConfig

yaml = """
--- !BattleStationConfig
processor: !Processor
  core_count: 8
  manufacturer: Intel
memory_gb: 8
led_color: red
"""

a = BattleStationConfig

loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)
      
      





?







BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color='red')
      
      





. yaml- . , Color



- . YAMLObject



? ? , .







class Color(Enum, YAMLObject):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"
      
      





:







TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
      
      





. yaml-, .







marshmallow



stackowerflow marshmallow , JSON-. , , , yaml JSON. class_schema



, -:







from pprint import pprint

from yaml import load, SafeLoader
from marshmallow_dataclass import class_schema

from config import BattleStationConfig

yaml = """
processor:
  core_count: 8
  manufacturer: Intel
memory_gb: 8
led_color: red
"""

loaded = load(yaml, Loader=SafeLoader)
pprint(loaded)

BattleStationConfigSchema = class_schema(BattleStationConfig)

result = BattleStationConfigSchema().load(loaded)
pprint(result)

      
      





, , :







marshmallow.exceptions.ValidationError: {'led_color': ['Invalid enum member red']}
      
      





, marshmallow enum, . yaml- :







processor:
  core_count: 8
  manufacturer: Intel
memory_gb: 8
led_color: RED
      
      





, , :







BattleStationConfig(processor=BattleStationConfig.Processor(core_count=8, manufacturer='Intel'), memory_gb=8, led_color=<Color.RED: 'red'>)
      
      





, yaml-. marshmallow :







Setting by_value=True



. This will cause both dumping and loading to use the value of the enum.

, metadata



field



:







@dataclass
class BattleStationConfig:
    led_color: Optional[Color] = field(default=None, metadata={"by_value": True})
      
      





, "" , yaml-.









, :







def strict_load_yaml(yaml: str, loaded_type: Type[Any]):
    schema = class_schema(loaded_type)
    return schema().load(load(yaml, Loader=SafeLoader))
      
      





Esta função pode requerer configuração adicional para classes de dados, mas resolve o problema original e não requer tags no yaml.







Uma nota rápida sobre ForwardRef



Se você definir classes de dados com ForwardRef (string com nome de classe), o marshmallow ficará confuso e não será capaz de analisar esta classe.







Por exemplo, tal configuração







from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, ForwardRef

@dataclass
class BattleStationConfig:
    processor: ForwardRef("Processor")
    memory_gb: int
    led_color: Optional["Color"] = field(default=None, metadata={"by_value": True})

    @dataclass
    class Processor:
        core_count: int
        manufacturer: str

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

      
      





resultará em um erro







marshmallow.exceptions.RegistryError: Class with name 'Processor' was not found. You may need to import the class.
      
      





E se você mover a classe para Processor



cima, o marshmallow perderá a classe Color



com um erro semelhante. Portanto, se possível, não use ForwardRef em suas classes se quiser analisá-las com marshmallow.







Código



Todo o código está disponível no repositório GitHub .








All Articles