Source code for byoc.cast

#!/usr/bin/env python3

import sys
import inspect

from .errors import Error
from more_itertools import first
from pathlib import Path
from typing import Union, Callable, Any

class Context:
    """
    Extra information that can be made available to *cast* functions.

    The *cast* argument to `param` is must be a function that takes one 
    argument and returns one value.  Normally, this argument is simply the 
    value to cast.  However, BYOC will instead provide a `Context` object if 
    the type annotation of that argument is `Context`::

        >>> import byoc
        >>> def f(context: byoc.Context):
        ...     return context.value

    Context objects have the following attributes:

    - :attr:`value`: The value to convert.  This is the same value that would 
      normally be passed directly to the *cast* function.
    - :attr:`meta`: The metadata object associated with the parameter.
    - :attr:`obj`: The object that owns the parameter, i.e. *self*.
    """

[docs] def __init__(self, value, meta, obj): self.value = value self.meta = meta self.obj = obj
def call_with_context(f, context): try: sig = inspect.signature(f) param = first(sig.parameters.values()) except ValueError: pass else: if param.annotation is Context: return f(context) return f(context.value) def relpath( context: Context, root_from_meta: Callable[[Any], Path]=\ lambda meta: Path(meta.location).parent, ) -> Path: """ Resolve paths loaded from a file. Relative paths are interpreted as being relative to the parent directory of the file they were loaded from. Arguments: context: The context object provided by BYOC to cast functions. root_from_meta: A callable that returns the parent directory for relative paths, given a metadata object describing how the value in question was loaded. The default implementation assumes that the metadata object has a :attr:`location` attribute that specifies the path to the relevant file. This will work if (i) the value was actually loaded from a file and (ii) the default pick function was used (i.e. `first`). For other pick functions, you may need to modify this argument accordingly. Returns: An absolute path. """ path = Path(context.value) if path.is_absolute(): return path root = root_from_meta(context.meta) return root.resolve() / path def arithmetic_eval(expr: str) -> Union[int, float]: """\ Evaluate the given arithmetic expression. Arguments: expr: The expression to evaluate. The syntax is identical to python, but only `int` literals, `float` literals, binary operators (except left/right shift, bitwise and/or/xor, and matrix multiplication), and unary operators are allowed. Returns: The value of the given expression. Raises: SyntaxError: If *expr* cannot be parsed for any reason. TypeError: If the *expr* argument is not a string. ZeroDivisionError: If *expr* divides by zero. It is safe to call this function on untrusted input, as there is no way to construct an expression that will execute arbitrary code. """ import ast, operator operators = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow, ast.USub: operator.neg, ast.UAdd: operator.pos, } def eval_node(node): if sys.version_info[:2] < (3, 8): if isinstance(node, ast.Num): return node.n else: if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value else: err = ArithmeticError(expr, non_number=node.value) err.blame += "{non_number!r} is not a number" raise err if isinstance(node, ast.BinOp): try: op = operators[type(node.op)] except KeyError: err = ArithmeticError(expr, op=node.op) err.blame += "the {op.__class__.__name__} operator is not supported" raise err left = eval_node(node.left) right = eval_node(node.right) return op(left, right) if isinstance(node, ast.UnaryOp): assert type(node.op) in operators op = operators[type(node.op)] value = eval_node(node.operand) return op(value) raise ArithmeticError(expr) root = ast.parse(expr.lstrip(" \t"), mode='eval') return eval_node(root.body) def int_eval(expr: str) -> int: """\ Same as `arithmetic_eval()`, but convert the result to `int`. """ return int(arithmetic_eval(expr)) def float_eval(expr: str) -> float: """\ Same as `arithmetic_eval()`, but convert the result to `float`. """ return float(arithmetic_eval(expr)) class ArithmeticError(Error, SyntaxError): def __init__(self, expr, **kwargs): super().__init__(expr=expr, **kwargs) self.brief = "unable to evaluate arithmetic expression" self.info += "expression: {expr}"