Source code for python_project_template_AS.calculator
"""Calculator utilities for applying and composing operations.
The :class:`Calculator` provides a thin layer over :class:`OperationRegistry`.
It supports applying named operations, composing unary operations into a
callable, and a small 'chain' helper for mixed sequences of unary/binary
operations used by the examples and tests.
Keep the behaviour minimal: methods raise :class:`OperationError` for
operation-related failures.
"""
from typing import Callable, Iterable, List, Any, Optional, Union
from .registry import OperationRegistry
from .operations import Operation
from .exceptions import OperationError
[docs]
class Calculator:
"""Calculator using an OperationRegistry."""
def __init__(self, registry: Optional[OperationRegistry] = None):
"""Create a Calculator with an optional registry."""
if registry is None:
self.registry = OperationRegistry()
else:
self.registry = registry
[docs]
def register(self, op: Operation, *, replace: bool = False) -> None:
"""Register an operation, optionally replacing existing."""
self.registry.register(op, replace=replace)
[docs]
def apply(self, op_name: str, *args: Any) -> Any:
"""Apply a named operation with arguments."""
op = self.registry.get(op_name)
try:
return op(*args)
except Exception as exc:
raise OperationError(
f"Error applying operation '{op_name}': {exc}"
) from exc
[docs]
def compose(
self, ops: Iterable[str], *, left_to_right: bool = True
) -> Callable[[Any], Any]:
"""Compose unary operations into a single callable."""
op_list: List[Operation] = [self.registry.get(name) for name in ops]
for op in op_list:
if op.arity != 1:
raise OperationError(f"Cannot compose non-unary operation: {op.name}")
if left_to_right:
def composed(x):
val = x
for op in op_list:
val = op(val)
return val
else:
def composed(x):
val = x
for op in reversed(op_list):
val = op(val)
return val
return composed
[docs]
def chain(self, sequence: Iterable[Union[str, int]], initial: Any) -> Any:
"""Apply a sequence of operations and values starting from initial."""
seq = list(sequence)
cur = initial
i = 0
while i < len(seq):
item = seq[i]
if isinstance(item, str):
op = self.registry.get(item)
if op.arity == 1:
cur = op(cur)
i += 1
elif op.arity == 2:
# expect next item as argument
if i + 1 >= len(seq):
raise OperationError(
f"Operation '{op.name}' expects an additional argument in the sequence"
)
arg = seq[i + 1]
cur = op(cur, arg)
i += 2
else:
raise OperationError("Only arity 1 or 2 supported in chain")
else:
# literal encountered: treat as updating current value
cur = item
i += 1
return cur