Skip to content

Commit

Permalink
experiment with minimal OtkNode interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mvo5 committed Nov 26, 2024
1 parent 43d4c12 commit 00c39f3
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 94 deletions.
88 changes: 39 additions & 49 deletions src/otk/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
from typing import Any


class OtkValueMixin:
class OtkNode:
def __init__(self, value, otk_src=None):
self.value = value
if otk_src:
# XXX: add a way to merge
self._otk_src = otk_src

@property
def otk_src(self):
return self._otk_src
Expand All @@ -13,66 +19,50 @@ def otk_src(self):
def otk_src(self, value):
self._otk_src = value

# needed so that we can compare things in dicts
def __eq__(self, other):
if isinstance(other, OtkNode):
return self.value == other.value
return self.value == other

class OtkDict(OtkValueMixin, dict):
def __init__(self, other: dict):
self.update(other)
# needed so that we can put things into dicts
def __hash__(self):
return hash(self.value)

# XXX: not a good nameq
def otk_dup(self, new: dict):
new = OtkDict(new)
new.otk_src = self.otk_src
return new

# needed so that yaml loading "feels" natural
class OtkDict(OtkNode):
def __getitem__(self, item):
return self.value.__getitem__(item)

class OtkList(OtkValueMixin, list):
def __init__(self, other: list):
self.extend(other)

# XXX: not a good name
def otk_dup(self, new: list):
new = OtkList(new)
new.otk_src = self.otk_src
return new
# needed so that yaml loading "feels" natural
class OtkList(OtkNode):
def __getitem__(self, item):
return self.value.__getitem__(item)


class OtkStr(OtkValueMixin, str):
def __new__(cls, other):
val = super().__new__(cls, other)
return val

# XXX: not a good name
def otk_dup(self, new):
# str are immutable
new = OtkStr(new)
new.otk_src = self.otk_src
return new


class OtkInt(OtkValueMixin, int):
def __new__(cls, other):
val = super().__new__(cls, other)
return val


# bool can't be subclassed so we need to workaround
class OtkBool(OtkValueMixin):
def __init__(self, other) -> None:
self._bool = other


# this is only needed to support OtkBool
class OtkJSONEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, OtkBool):
return o._bool
def otk_deep_convert(data):
ret = data
if isinstance(data, OtkNode):
return ret
if isinstance(data, dict):
ret = OtkDict({
key: otk_deep_convert(value) for key, value in data.items()
})
elif isinstance(data, list):
ret = OtkList([otk_deep_convert(item) for item in data])
elif isinstance(data, str):
ret = OtkStr(data)
elif isinstance(data, int):
ret = OtkInt(data)


# this is needed for the external data which does not come in via
# the yaml loader
def otk_deep_convert_from(origin: OtkValueMixin, data: Any):
def otk_deep_convert_from(origin: OtkNode, data: Any):
ret = data
if isinstance(data, OtkValueMixin):
if isinstance(data, OtkNode):
return ret
if isinstance(data, dict):
ret = OtkDict({
Expand Down
6 changes: 3 additions & 3 deletions src/otk/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from abc import ABC, abstractmethod
from typing import Any, Optional

from .annotation import OtkDict
from .annotation import OtkNode
from .constant import VALID_VAR_NAME_RE
from .error import (ParseError,
TransformVariableIndexRangeError,
Expand Down Expand Up @@ -90,7 +90,7 @@ def define(self, name: str, value: Any) -> None:
for i, part in enumerate(parts[:-1]):
if not isinstance(cur_var_scope.get(part), dict):
self._maybe_log_var_override(cur_var_scope, parts[:i+1], {".".join(parts[i+1:]): value})
d = OtkDict({})
d = OtkNode({})
# XXX: make nicer via dataclass
# XXX2: when we construct the parent dict
if value is not None:
Expand All @@ -100,7 +100,7 @@ def define(self, name: str, value: Any) -> None:
cur_var_scope[part] = d
cur_var_scope = cur_var_scope[part]
self._maybe_log_var_override(cur_var_scope, parts, value)
cur_var_scope[parts[-1]] = value
cur_var_scope.value[parts[-1]] = value

def variable(self, name: str) -> Any:
parts = name.split(".")
Expand Down
15 changes: 8 additions & 7 deletions src/otk/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from copy import deepcopy
from typing import Any

from .annotation import OtkDict
from .annotation import OtkNode
from .constant import PREFIX, PREFIX_TARGET, NAME_VERSION
from .context import CommonContext, OSBuildContext
from .error import NoTargetsError, ParseError, ParseVersionError, OTKError
Expand All @@ -23,7 +23,7 @@ class Omnifest:
def __init__(self, path: pathlib.Path, target: str = "", *, warn_duplicated_defs: bool = False) -> None:
self._ctx = CommonContext(target_requested=target, warn_duplicated_defs=warn_duplicated_defs)
# XXX: this can be removed once we find a way to deal with unset variables
d = OtkDict({})
d = OtkNode({})
d.otk_src = "hardcoded:1"
self._ctx.define("user.modifications", d)
self._target = target
Expand All @@ -50,12 +50,12 @@ def ensure(cls, deserialized_data: dict[str, Any]) -> None:

# And that dictionary needs to contain certain keys to indicate this
# being an Omnifest.
if NAME_VERSION not in deserialized_data:
if NAME_VERSION not in deserialized_data.value:
raise ParseVersionError(f"omnifest must contain a key by the name of {NAME_VERSION!r}")

# no toplevel keys without a target or an otk directive
targetless_keys = [key for key in deserialized_data
if not key.startswith(PREFIX)]
targetless_keys = [key for key in deserialized_data.value
if not key.value.startswith(PREFIX)]
if len(targetless_keys):
raise ParseError(f"otk file contains top-level keys {targetless_keys} without a target")

Expand Down Expand Up @@ -83,6 +83,7 @@ def as_target_string(self) -> str:

def _targets(tree: dict[str, Any]) -> dict[str, Any]:
return {
key.removeprefix(PREFIX_TARGET): val
for key, val in tree.items() if key.startswith(PREFIX_TARGET)
key.value.removeprefix(PREFIX_TARGET): val
for key, val in tree.value.items()
if key.value.startswith(PREFIX_TARGET)
}
4 changes: 2 additions & 2 deletions src/otk/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import os
from typing import Any

from .annotation import OtkDict, otk_deep_convert_from, OtkJSONEncoder
from .annotation import OtkNode, otk_deep_convert_from
from .constant import PREFIX_EXTERNAL
from .error import ExternalFailedError
from .traversal import State
Expand All @@ -24,7 +24,7 @@ def call(state: State, directive: str, tree: Any) -> Any:
data = json.dumps(
{
"tree": tree,
}, cls=OtkJSONEncoder
}
)

process = subprocess.run([exe], input=data, encoding="utf8", capture_output=True, check=False)
Expand Down
6 changes: 3 additions & 3 deletions src/otk/loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import yaml

from .annotation import OtkDict, OtkList, OtkStr, OtkInt
from .annotation import OtkNode, OtkDict, OtkList
from .error import (
IncludeNotFoundError, OTKError,
ParseError, ParseTypeError, ParseValueError, ParseDuplicatedYamlKeyError,
Expand Down Expand Up @@ -57,12 +57,12 @@ def otk_list_constructor(self, loader, node):

def otk_construct_yaml_str(self, loader, node):
data = loader.construct_scalar(node)
otk_scalar = OtkStr(data)
otk_scalar = OtkNode(data)
otk_scalar.otk_src = self.otk_src_from(node)
return otk_scalar

def otk_construct_yaml_int(self, loader, node):
data = super().construct_yaml_int(node)
otk_int = OtkInt(data)
otk_int = OtkNode(data)
otk_int.otk_src = self.otk_src_from(node)
return otk_int
15 changes: 7 additions & 8 deletions src/otk/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from abc import ABC, abstractmethod
from typing import Any

from .annotation import OtkJSONEncoder
from .context import CommonContext, OSBuildContext
from .constant import PREFIX_TARGET
from .error import ParseError
Expand All @@ -26,20 +25,20 @@ def ensure_valid(self, _tree: Any) -> None:
pass

def as_string(self, context: CommonContext, tree: Any, pretty: bool = True) -> str:
return json.dumps(tree, indent=2 if pretty else None,
cls=OtkJSONEncoder)
return json.dumps(tree, indent=2 if pretty else None)



class OSBuildTarget(Target):
def ensure_valid(self, tree: Any) -> None:
if "version" in tree:
if "version" in tree.value:
raise ParseError(
"First level below a 'target' must not contain 'version'. "
"The key 'version' is added by otk internally.")

def as_string(self, context: OSBuildContext, tree: Any, pretty: bool = True) -> str:
osbuild_tree = tree[PREFIX_TARGET + context.target_requested]
osbuild_tree["version"] = "2"
osbuild_tree = tree.value[PREFIX_TARGET + context.target_requested]
osbuild_tree.value["version"] = "2"

return json.dumps(osbuild_tree, indent=2 if pretty else None)

return json.dumps(osbuild_tree, indent=2 if pretty else None,
cls=OtkJSONEncoder)
42 changes: 21 additions & 21 deletions src/otk/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import yaml

from . import tree
from .annotation import OtkBool, OtkDict, OtkList, OtkStr, OtkInt
from .annotation import OtkNode, OtkDict, OtkList
from .constant import NAME_VERSION, PREFIX, PREFIX_DEFINE, PREFIX_INCLUDE, PREFIX_OP, PREFIX_TARGET
from .context import Context, validate_var_name
from .error import (
Expand All @@ -36,20 +36,20 @@
log = logging.getLogger(__name__)


def resolve(ctx: Context, state: State, data: Any) -> Any:
def resolve(ctx: Context, state: State, node: Any) -> Any:
"""Resolves a value of any supported type into a new value. Each type has
its own specific handler to replace the data value."""
its own specific handler to replace the node value."""

if isinstance(data, dict):
return resolve_dict(ctx, state, data)
if isinstance(data, list):
return resolve_list(ctx, state, data)
if isinstance(data, str):
return resolve_str(ctx, state, data)
if isinstance(data, (int, float, OtkBool, type(None))):
return data
if isinstance(node.value, dict):
return resolve_dict(ctx, state, node)
if isinstance(node.value, list):
return resolve_list(ctx, state, node)
if isinstance(node.value, str):
return resolve_str(ctx, state, node)
if isinstance(node.value, (int, float, bool, type(None))):
return node

raise ParseTypeError(f"could not look up {data} of type {type(data)} in resolvers", state)
raise ParseTypeError(f"could not look up {node} of type {type(node.value)} in resolvers", state)


# XXX: look into this
Expand All @@ -65,7 +65,7 @@ def resolve_dict(ctx: Context, state: State, tree: dict[str, Any]) -> Any:
- Values under any other key are processed based on their type (see resolve()).
"""

for key, val in tree.copy().items():
for key, val in tree.value.copy().items():
# Replace any variables in a value immediately before doing anything
# else, so that variables defined in strings are considered in the
# processing of all directives.
Expand Down Expand Up @@ -124,7 +124,7 @@ def resolve_dict(ctx: Context, state: State, tree: dict[str, Any]) -> Any:
# return is fine, no siblings allowed
return resolve(ctx, state, call(state, key, resolve(ctx, state, val)))

tree[key] = resolve(ctx, state, val)
tree.value[key] = resolve(ctx, state, val)
return tree


Expand All @@ -134,7 +134,7 @@ def resolve_list(ctx: Context, state: State, tree: OtkList[Any]) -> OtkList[Any]

log.debug("resolving list %r", tree)

return tree.otk_dup([resolve(ctx, state, val) for val in tree])
return OtkList([resolve(ctx, state, val) for val in tree.value], tree.otk_src)


def resolve_str(ctx: Context, state: State, tree: str) -> Any:
Expand Down Expand Up @@ -166,7 +166,7 @@ def process_defines(ctx: Context, state: State, tree: Any) -> None:
return

# Iterate over a copy of the tree so that we can modify it in-place.
for key, value in tree.copy().items():
for key, value in tree.value.copy().items():
if key.startswith("otk.define"):
# nested otk.define: process the nested values directly
process_defines(ctx, state, value)
Expand Down Expand Up @@ -235,8 +235,8 @@ def op(ctx: Context, state: State, tree: Any, key: str) -> Any:
raise TransformDirectiveUnknownError(f"nonexistent op {key!r}", state)


@tree.must_be(dict)
@tree.must_pass(tree.has_keys(["values"]))
#@tree.must_be(dict)
#@tree.must_pass(tree.has_keys(["values"]))
def op_join(ctx: Context, state: State, tree: dict[str, Any]) -> Any:
"""Join a map/seq."""

Expand All @@ -261,7 +261,7 @@ def op_join(ctx: Context, state: State, tree: dict[str, Any]) -> Any:
raise TransformDirectiveTypeError(f"cannot join {values}", state)


@tree.must_be(str)
#@tree.must_be(str)
def substitute_vars(ctx: Context, state: State, data: str) -> Any:
"""Substitute variables in the `data` string.
Expand All @@ -279,15 +279,15 @@ def substitute_vars(ctx: Context, state: State, data: str) -> Any:
pattern = bracket % r"(?P<name>[^}]+)"
# If there is a single match and its span is the entire haystack then we
# return its value directly.
if m := re.fullmatch(pattern, data):
if m := re.fullmatch(pattern, data.value):
validate_var_name(m.group("name"))
try:
var = ctx.variable(m.group("name"))
except OTKError as exc:
raise exc.__class__(str(exc), state)
return var

if matches := re.finditer(pattern, data):
if matches := re.finditer(pattern, data.value):
for m in matches:
name = m.group("name")
validate_var_name(m.group("name"))
Expand Down
Loading

0 comments on commit 00c39f3

Please sign in to comment.