Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOI 2.22.23 Add metaspec builder functions #17

Merged
merged 14 commits into from
May 20, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ jobs:
- uses: TrueBrain/actions-flake8@v2
with:
flake8_version: 6.0.0
plugins: flake8-isort==6.0.0 flake8-quotes==3.3.2
plugins: flake8-isort==6.1.1 flake8-quotes==3.4.0
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
fail-fast: false

steps:
Expand Down
239 changes: 239 additions & 0 deletions mandible/metadata_mapper/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Callable, Dict, Type, TypeVar

from .directive import (
Add,
FloorDiv,
Mapped,
Mul,
Reformatted,
Sub,
TemplateDirective,
TrueDiv,
)
from .types import Key, Template

# For testing purposes to ensure we implement builders for all directives
_DIRECTIVE_BUILDER_REGISTRY: Dict[str, Callable[..., "DirectiveBuilder"]] = {}


@dataclass
class BuildConfig:
directive_marker: str


class Builder(ABC):
@abstractmethod
def build(self, config: BuildConfig) -> Template:
pass


class DirectiveBuilder(Builder):
def __init__(
self,
name: str,
params: Dict[str, Any],
):
self.name = name
self.params = params

def build(self, config: BuildConfig) -> Template:
return {
f"{config.directive_marker}{self.name}": {
k: v.build(config) if isinstance(v, Builder) else v
for k, v in self.params.items()
},
}

def __add__(self, other: Any) -> "DirectiveBuilder":
reweeden marked this conversation as resolved.
Show resolved Hide resolved
return add(self, other)

def __radd__(self, other: Any) -> "DirectiveBuilder":
return add(other, self)

def __floordiv__(self, other: Any) -> "DirectiveBuilder":
return floordiv(self, other)

def __rfloordiv__(self, other: Any) -> "DirectiveBuilder":
return floordiv(other, self)

def __mul__(self, other: Any) -> "DirectiveBuilder":
return mul(self, other)

def __rmul__(self, other: Any) -> "DirectiveBuilder":
return mul(other, self)

def __sub__(self, other: Any) -> "DirectiveBuilder":
return sub(self, other)

def __rsub__(self, other: Any) -> "DirectiveBuilder":
return sub(other, self)

def __truediv__(self, other: Any) -> "DirectiveBuilder":
return truediv(self, other)

def __rtruediv__(self, other: Any) -> "DirectiveBuilder":
return truediv(other, self)


T = TypeVar("T")


def _directive_builder(directive: Type["TemplateDirective"]) -> Callable[[T], T]:
directive_name = directive.directive_name
assert directive_name is not None

def decorator(func):
func.__doc__ = directive.__doc__

_DIRECTIVE_BUILDER_REGISTRY[directive_name] = func
return func

return decorator


@_directive_builder(Mapped)
def mapped(
source: str,
key: Key,
) -> DirectiveBuilder:
directive_name = Mapped.directive_name
assert directive_name is not None

return DirectiveBuilder(
directive_name,
{
"source": source,
"key": key,
},
)


@_directive_builder(Reformatted)
def reformatted(
format: str,
value: Any,
key: Key,
) -> DirectiveBuilder:
directive_name = Reformatted.directive_name
assert directive_name is not None

return DirectiveBuilder(
directive_name,
{
"format": format,
"value": value,
"key": key,
},
)

#
# Operations
#


def _binop_directive(directive_name: str, left: Any, right: Any):
return DirectiveBuilder(
directive_name,
{
"left": left,
"right": right,
},
)


@_directive_builder(Add)
def add(
left: Any,
right: Any,
) -> DirectiveBuilder:
directive_name = Add.directive_name
assert directive_name is not None

return _binop_directive(directive_name, left, right)


@_directive_builder(FloorDiv)
def floordiv(
left: Any,
right: Any,
) -> DirectiveBuilder:
directive_name = FloorDiv.directive_name
assert directive_name is not None

return _binop_directive(directive_name, left, right)


@_directive_builder(Mul)
def mul(
left: Any,
right: Any,
) -> DirectiveBuilder:
directive_name = Mul.directive_name
assert directive_name is not None

return _binop_directive(directive_name, left, right)


@_directive_builder(Sub)
def sub(
left: Any,
right: Any,
) -> DirectiveBuilder:
directive_name = Sub.directive_name
assert directive_name is not None

return _binop_directive(directive_name, left, right)


@_directive_builder(TrueDiv)
def truediv(
left: Any,
right: Any,
) -> DirectiveBuilder:
directive_name = TrueDiv.directive_name
assert directive_name is not None

return _binop_directive(directive_name, left, right)


def build(template: Any, directive_marker: str = "@") -> Template:
"""Convert a template created with builder classes to a standard template
that is ready to be used with the metadata mapper.

When using builder classes, you must convert your template using `build`.

:param template: template with possible `Builder` values
:param directive_marker: marker to use for identifying directives
:returns: Template - a standard template with all `Builder`s replaced
"""
config = BuildConfig(directive_marker=directive_marker)

return build_with_config(template, config)


def build_with_config(template: Any, config: BuildConfig) -> Template:
"""Same as build but takes configuration options as a BuildConfig object.

:param template: template with possible `Builder` values
:param config: BuildConfig object to customize generation
:returns: Template - a standard template with all `Builder`s replaced
"""
if isinstance(template, dict):
return {
k: build_with_config(v, config)
for k, v in template.items()
}
elif isinstance(template, list):
return [build_with_config(v, config) for v in template]
elif isinstance(template, Builder):
return template.build(config)
elif isinstance(template, tuple):
return tuple(build_with_config(v, config) for v in template)
elif isinstance(template, set):
return set(build_with_config(v, config) for v in template)
elif isinstance(template, (str, int, float, bool)):
return template

raise ValueError(template)
105 changes: 0 additions & 105 deletions mandible/metadata_mapper/directive.py

This file was deleted.

18 changes: 18 additions & 0 deletions mandible/metadata_mapper/directive/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .directive import DIRECTIVE_REGISTRY, Key, TemplateDirective, get_key
from .mapped import Mapped
from .operations import Add, FloorDiv, Mul, Sub, TrueDiv
from .reformatted import Reformatted

__all__ = (
"Add",
"DIRECTIVE_REGISTRY",
"FloorDiv",
"Key",
"Mapped",
"Mul",
"Reformatted",
"Sub",
"TemplateDirective",
"TrueDiv",
"get_key",
)
Loading
Loading