Skip to content

Commit

Permalink
Remove pydantic-yaml as dependency
Browse files Browse the repository at this point in the history
The amount of functionality we use of this library is extremly minimal.
We replace it with the dependency `ruamel.yaml` that parses the yaml
file into a python object. Furthemorer, the library still uses pydantic
API v1 for validating the pydantic model which is deprecated so we can
replace it with pydantic v2. In addition this change gives us more
flexibility as it separates the parsing of the yaml file and the
validation through pydantic.

To have a similar utility function that can be used in the docstring
tests we implement `validate_yaml_content`.
  • Loading branch information
agoscinski committed Jan 27, 2025
1 parent 6051f9c commit d9edeb4
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 13 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"numpy",
"isoduration",
"pydantic",
"pydantic-yaml",
"ruamel.yaml",
"aiida-core>=2.5",
"aiida-workgraph==0.4.10",
"termcolor",
Expand Down
50 changes: 38 additions & 12 deletions src/sirocco/parsing/_yaml_data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ class _NamedBaseModel(BaseModel):
>>> _NamedBaseModel(foo={})
_NamedBaseModel(name='foo')
>>> import pydantic_yaml, textwrap
>>> pydantic_yaml.parse_yaml_raw_as(
>>> import textwrap
>>> validate_yaml_content(
... _NamedBaseModel,
... textwrap.dedent('''
... foo:
... '''),
... )
_NamedBaseModel(name='foo')
>>> pydantic_yaml.parse_yaml_raw_as(
>>> validate_yaml_content(
... _NamedBaseModel,
... textwrap.dedent('''
... name: foo
Expand Down Expand Up @@ -479,15 +479,14 @@ class ConfigBaseData(_NamedBaseModel, ConfigBaseDataSpecs):
yaml snippet:
>>> import textwrap
>>> import pydantic_yaml
>>> snippet = textwrap.dedent(
... '''
... foo:
... type: "file"
... src: "foo.txt"
... '''
... )
>>> pydantic_yaml.parse_yaml_raw_as(ConfigBaseData, snippet)
>>> validate_yaml_content(ConfigBaseData, snippet)
ConfigBaseData(type=<DataType.FILE: 'file'>, src='foo.txt', format=None, computer=None, name='foo', parameters=[])
Expand Down Expand Up @@ -533,7 +532,9 @@ class ConfigData(BaseModel):
yaml snippet:
>>> import textwrap
>>> import pydantic_yaml
>>> from io import StringIO
>>> from pydantic import TypeAdapter
>>> from ruamel.yaml import YAML
>>> snippet = textwrap.dedent(
... '''
... available:
Expand All @@ -546,7 +547,7 @@ class ConfigData(BaseModel):
... src: "bar.txt"
... '''
... )
>>> data = pydantic_yaml.parse_yaml_raw_as(ConfigData, snippet)
>>> data = validate_yaml_content(ConfigData, snippet)
>>> assert data.available[0].name == "foo"
>>> assert data.generated[0].name == "bar"
Expand Down Expand Up @@ -592,7 +593,6 @@ class ConfigWorkflow(BaseModel):
minimal yaml to generate:
>>> import textwrap
>>> import pydantic_yaml
>>> config = textwrap.dedent(
... '''
... cycles:
Expand All @@ -613,7 +613,7 @@ class ConfigWorkflow(BaseModel):
... src: some_task_output
... '''
... )
>>> wf = pydantic_yaml.parse_yaml_raw_as(ConfigWorkflow, config)
>>> wf = validate_yaml_content(ConfigWorkflow, config)
minimum programmatically created instance
Expand Down Expand Up @@ -698,13 +698,11 @@ def load_workflow_config(workflow_config: str) -> CanonicalWorkflow:
:param workflow_config: the string to the config yaml file containing the workflow definition
"""
from pydantic_yaml import parse_yaml_raw_as

config_path = Path(workflow_config)

content = config_path.read_text()

parsed_workflow = parse_yaml_raw_as(ConfigWorkflow, content)
parsed_workflow = validate_yaml_content(ConfigWorkflow, content)

# If name was not specified, then we use filename without file extension
if parsed_workflow.name is None:
Expand All @@ -713,3 +711,31 @@ def load_workflow_config(workflow_config: str) -> CanonicalWorkflow:
rootdir = config_path.resolve().parent

return canonicalize_workflow(config_workflow=parsed_workflow, rootdir=rootdir)


OBJECT_T = typing.TypeVar("OBJECT_T")


def validate_yaml_content(cls: type[OBJECT_T], content: str) -> OBJECT_T:
"""Parses the YAML content into a python object using generic types and subsequently validates it with pydantic.
Args:
cls (type[OBJECT_T]): The class type to which the parsed yaml content should
be validated. It must be compatible with pydantic validation.
content (str): The yaml content as a string.
Returns:
OBJECT_T: An instance of the specified class type with data parsed and
validated from the YAML content.
Raises:
pydantic.ValidationError: If the YAML content cannot be validated
against the specified class type.
ruamel.yaml.YAMLError: If there is an error in parsing the YAML content.
"""
from io import StringIO

from pydantic import TypeAdapter
from ruamel.yaml import YAML

return TypeAdapter(cls).validate_python(YAML(typ="safe", pure=True).load(StringIO(content)))

0 comments on commit d9edeb4

Please sign in to comment.