Skip to content

Commit

Permalink
Encode ordered dicts as regular dicts with ruamel
Browse files Browse the repository at this point in the history
Older snakemake versions read config files as ordered dicts. Ruamel
would then render these back to yaml with an !!omap type, which would
then in turn be interpreted by pyyaml as lists of tuple pairs.

Fix by handling all OrderedDicts as regular dicts

Additionally, test a template dry run using the current repository state
(where previously we used only the latest pip release)
  • Loading branch information
pvandyken committed Jan 24, 2024
1 parent 939aac0 commit e4586e1
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 5 deletions.
9 changes: 8 additions & 1 deletion snakebids/io/yaml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import collections
from pathlib import Path, PosixPath, WindowsPath
from typing import Any, OrderedDict

from ruamel.yaml import YAML, Dumper

Expand All @@ -12,8 +14,13 @@ def path2str(dumper: Dumper, data: Path):
return dumper.represent_scalar(
"tag:yaml.org,2002:str",
str(data),
)
) # type: ignore

def to_dict(dumper: Dumper, data: OrderedDict[Any, Any]):
return dumper.represent_dict(dict(data))

yaml.representer.add_representer(PosixPath, path2str)
yaml.representer.add_representer(WindowsPath, path2str)
yaml.representer.add_representer(collections.OrderedDict, to_dict)
yaml.representer.add_representer(OrderedDict, to_dict)
return yaml
6 changes: 3 additions & 3 deletions snakebids/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def wrapper(*args: _P.args, **kwargs: _P.kwargs):
sp.run(["docker", "image", "inspect", container], check=True)
except sp.CalledProcessError as err:
if not (
match := re.match(r"snakebids/([\w\-])+\:[a-zA-Z0-9\-]+", container)
match := re.match(r"snakebids/([\w\-]+)\:[a-zA-Z0-9\-]+", container)
):
pytest.fail(
f"Unrecognized docker image: {container}. Should be "
Expand All @@ -307,8 +307,8 @@ def wrapper(*args: _P.args, **kwargs: _P.kwargs):
container_id = match[1]
pytest.fail(
f"{container} is not built on this machine. To build container, "
f"run `poetry run poe build-container {container_id}`. To skip "
f"docker tests, use '-m \"not docker\"'. (got error {err})"
f"run:\n\n\t`poetry run poe build-container {container_id}`\n\nTo "
f"skip docker tests, use '-m \"not docker\"'.\n\n(error:) {err}"
)
return func(*args, **kwargs)

Expand Down
33 changes: 33 additions & 0 deletions snakebids/tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,36 @@ def test_template_dry_runs_successfully(tmp_path: Path, build: BuildBackend, ven
print(cmd.stderr, file=sys.stderr)
raise
assert "All set" in cmd.stdout.decode()


def test_template_dry_runs_with_current_repository(tmp_path: Path):
app_name = "snakebids_app"
data = get_empty_data(app_name, "setuptools")

copier.run_copy(
str(Path(itx.first(snakebids.__path__), "project_template")),
tmp_path / app_name,
data=data,
unsafe=True,
)
app_path = tmp_path / app_name
cmd = sp.run(
[
sys.executable,
app_path / app_name / "run.py",
app_path / "tests/data",
app_path / "tests/results",
"participant",
"-c1",
"--skip-bids-validation",
],
capture_output=True,
check=False,
)
try:
cmd.check_returncode()
except Exception:
print(cmd.stdout.decode())
print(cmd.stderr.decode(), file=sys.stderr)
raise
assert "All set" in cmd.stdout.decode()
25 changes: 24 additions & 1 deletion snakebids/tests/test_yaml.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import io
from io import StringIO
from pathlib import Path
from typing import Any, OrderedDict

import pytest
from hypothesis import given
Expand All @@ -14,8 +16,10 @@
import snakebids.io.yaml as yamlio
from snakebids.tests.helpers import allow_function_scoped

YAML_SAFE_CHARS = st.characters(blacklist_characters=["\x85"])

@given(path=st.text(st.characters(blacklist_characters=["\x85"]), min_size=1).map(Path))

@given(path=st.text(YAML_SAFE_CHARS, min_size=1).map(Path))
def test_paths_formatted_as_str(path: Path):
string = StringIO()
yaml = yamlio.get_yaml_io()
Expand Down Expand Up @@ -71,3 +75,22 @@ def test_overwrites_file_if_forced(
mocks = self.io_mocks(mocker)
configio.write_config(path, {}, force_overwrite=True)
assert mocks["jsondump"].call_count ^ mocks["yamldump"].call_count

@given(
data=st.recursive(
st.text(YAML_SAFE_CHARS),
lambda children: st.dictionaries(st.text(YAML_SAFE_CHARS), children),
)
)
def test_ordered_dict_roundtrips_as_dict(self, data: dict[str, Any]):
def to_odict(data: dict[str, Any] | str) -> OrderedDict[str, Any] | str:
if isinstance(data, str):
return data
return OrderedDict({key: to_odict(value) for key, value in data.items()})

stream = io.StringIO()
yaml = yamlio.get_yaml_io()
odict = to_odict(data)
yaml.dump(odict, stream)
stream.seek(0)
assert yaml.load(stream) == data

0 comments on commit e4586e1

Please sign in to comment.