Skip to content

Commit

Permalink
feat: allow eval on non existing namespace (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
nichmor authored Jul 22, 2024
1 parent 1a7d514 commit d6aff49
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,4 @@ mapping/*# pixi environments
.pixi

.vscode/
.DS_store
.DS_Store
2 changes: 1 addition & 1 deletion pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/rattler_build_conda_compat/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ def lint_recipe_yaml_by_schema(recipe_file):
meta = yaml.load(fh)

validator = Draft202012Validator(schema)

lints = []

for error in validator.iter_errors(meta):
Expand Down
68 changes: 52 additions & 16 deletions src/rattler_build_conda_compat/loader.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import itertools
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any

Expand All @@ -11,23 +12,55 @@
from collections.abc import Iterator
from os import PathLike

SELECTOR_OPERATORS = ("and", "or", "not")


def _remove_empty_keys(some_dict: dict[str, Any]) -> dict[str, Any]:
filtered_dict = {}
for key, value in some_dict.items():
if isinstance(value, list) and len(value) == 0:
continue
filtered_dict[key] = value

return filtered_dict


def _flatten_lists(some_dict: dict[str, Any]) -> dict[str, Any]:
result_dict: dict[str, Any] = {}
for key, value in some_dict.items():
if isinstance(value, dict):
result_dict[key] = _flatten_lists(value)
elif isinstance(value, list) and value and isinstance(value[0], list):
result_dict[key] = list(itertools.chain(*value))
else:
result_dict[key] = value

return result_dict


class RecipeLoader(yaml.BaseLoader):
_namespace: dict[str, Any] | None = None
_allow_missing_selector: bool = False

@classmethod
@contextmanager
def with_namespace(cls: type[RecipeLoader], namespace: dict[str, Any] | None) -> Iterator[None]:
def with_namespace(
cls: type[RecipeLoader],
namespace: dict[str, Any] | None,
*,
allow_missing_selector: bool = False,
) -> Iterator[None]:
try:
cls._namespace = namespace
cls._allow_missing_selector = allow_missing_selector
yield
finally:
del cls._namespace

def construct_sequence( # noqa: C901
def construct_sequence( # noqa: C901, PLR0912
self,
node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
deep: bool = False, # noqa: FBT002, FBT001
deep: bool = False, # noqa: FBT002, FBT001,
) -> list[yaml.ScalarNode]:
"""deep is True when creating an object/mapping recursively,
in that case want the underlying elements available during construction
Expand Down Expand Up @@ -56,6 +89,17 @@ def construct_sequence( # noqa: C901

to_be_eval = f"{value_node.value}"

if self._allow_missing_selector:
split_selectors = [
selector
for selector in to_be_eval.split()
if selector not in SELECTOR_OPERATORS
]
for selector in split_selectors:
if self._namespace and selector not in self._namespace:
cleaned_selector = selector.strip("(").rstrip(")")
self._namespace[cleaned_selector] = True

evaled = eval(to_be_eval, self._namespace) # noqa: S307
if evaled:
the_evaluated_one = then_node_value
Expand Down Expand Up @@ -84,22 +128,14 @@ def load_yaml(content: str | bytes) -> Any: # noqa: ANN401
return yaml.load(content, Loader=yaml.BaseLoader) # noqa: S506


def remove_empty_keys(variant_dict: dict[str, Any]) -> dict[str, Any]:
filtered_dict = {}
for key, value in variant_dict.items():
if isinstance(value, list) and len(value) == 0:
continue
filtered_dict[key] = value

return filtered_dict


def parse_recipe_config_file(
path: PathLike[str], namespace: dict[str, Any] | None
path: PathLike[str], namespace: dict[str, Any] | None, *, allow_missing_selector: bool = False
) -> dict[str, Any]:
with open(path) as f, RecipeLoader.with_namespace(namespace):
with open(path) as f, RecipeLoader.with_namespace(
namespace, allow_missing_selector=allow_missing_selector
):
content = yaml.load(f, Loader=RecipeLoader) # noqa: S506
return remove_empty_keys(content)
return _flatten_lists(_remove_empty_keys(content))


def load_all_requirements(content: dict[str, Any]) -> dict[str, Any]:
Expand Down
15 changes: 15 additions & 0 deletions tests/__snapshots__/test_rattler_loader.ambr
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# serializer version: 1
# name: test_load_recipe_with_missing_selectors
dict({
'package': dict({
'name': 'some-osx-recipe',
'version': '1.0.0',
}),
'requirements': dict({
'build': list([
'python',
'ruby',
'rust',
]),
}),
})
# ---
# name: test_load_variants
dict({
'numpy': list([
Expand Down
16 changes: 16 additions & 0 deletions tests/data/osx_recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package:
name: some-osx-recipe
version: 1.0.0
requirements:
build:
- if: osx and arm64
then:
- python

- if: osx and aarch64
then:
- ruby

- if: unix
then:
- rust
12 changes: 12 additions & 0 deletions tests/test_rattler_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ def test_load_all_requirements() -> None:

content = load_all_requirements(recipe_content)
print(content)


def test_load_recipe_with_missing_selectors(snapshot) -> None:
osx_recipe = Path("tests/data/osx_recipe.yaml")

namespace = {"osx": True, "unix": True}

loaded_variants = parse_recipe_config_file(
str(osx_recipe), namespace, allow_missing_selector=True
)

assert loaded_variants == snapshot

0 comments on commit d6aff49

Please sign in to comment.