Désérialisation stricte de YAML en Python avec la bibliothèque marshmallow

Tâche initiale



  • Il est nécessaire de lire une configuration non triviale à partir du fichier .yaml.
  • La structure de configuration est décrite à l'aide de classes de données.
  • Il est nécessaire que les vérifications de type soient effectuées pendant la désérialisation et une exception est levée si les données ne sont pas valides.


Autrement dit, pour faire simple, vous avez besoin d'une fonction du formulaire:







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





Et cette fonction sera utilisée comme ceci:







@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 configuration



Le fichier config.py



ressemble à ceci:







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

      
      





Option qui ne fonctionne pas



Le problème initial est commun, n'est-ce pas? La solution devrait donc être triviale. Importez simplement la bibliothèque yaml standard et vous avez terminé?







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))
      
      





Cette fonction peut nécessiter une configuration supplémentaire pour les classes de données, mais elle résout le problème d'origine et ne nécessite pas de balises dans yaml.







Un petit mot sur ForwardRef



Si vous définissez des classes de données avec ForwardRef (chaîne avec nom de classe), marshmallow sera confondu et ne pourra pas analyser cette classe.







Par exemple, une telle configuration







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"

      
      





entraînera une erreur







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





Et si vous déplacez la classe Processor



plus haut, la guimauve perdra la classe Color



avec une erreur similaire. Donc, si possible, n'utilisez pas ForwardRef sur vos classes si vous souhaitez les analyser avec de la guimauve.







Code



Tout le code est disponible dans le référentiel GitHub .








All Articles