yaql/yaql/standard_library/system.py

382 lines
11 KiB
Python

# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
The module describes main system functions for working with objects.
"""
import itertools
from yaql.language import contexts
from yaql.language import specs
from yaql.language import utils
from yaql.language import yaqltypes
@specs.parameter('name', yaqltypes.StringConstant())
@specs.name('#get_context_data')
def get_context_data(name, context):
""":yaql:getContextData
Returns the context value by its name. This function is system
and can be overridden to change the way of getting context data.
:signature: getContextData(name)
:arg name: value's key name
:argType name: string
:returnType: any (value type)
"""
return context[name]
@specs.parameter('expr', yaqltypes.Lambda(method=True))
@specs.name('#operator_.')
def op_dot(receiver, expr):
""":yaql:operator .
Returns expr evaluated on receiver.
:signature: receiver.expr
:arg receiver: object to evaluate expression
:argType receiver: any
:arg expr: expression
:argType expr: expression that can be evaluated as a method
:returnType: expression result type
.. code::
yaql> [0, 1].select($+1)
[1, 2]
"""
return expr(receiver)
@specs.parameter('expr', yaqltypes.YaqlExpression())
@specs.inject('operator', yaqltypes.Delegate('#operator_.'))
@specs.name('#operator_?.')
def elvis_operator(operator, receiver, expr):
""":yaql:operator ?.
Evaluates expr on receiver if receiver isn't null and returns the result.
If receiver is null returns null.
:signature: receiver?.expr
:arg receiver: object to evaluate expression
:argType receiver: any
:arg expr: expression
:argType expr: expression that can be evaluated as a method
:returnType: expression result or null
.. code::
yaql> [0, 1]?.select($+1)
[1, 2]
yaql> null?.select($+1)
null
"""
if receiver is None:
return None
return operator(receiver, expr)
@specs.parameter('sequence', yaqltypes.Iterable())
@specs.parameter('args', yaqltypes.String())
@specs.method
def unpack(sequence, context, *args):
""":yaql:unpack
Returns context object with sequence values unpacked to args.
If args size is equal to sequence size then args get appropriate
sequence values.
If args size is 0 then args are 1-based indexes.
Otherwise ValueError is raised.
:signature: sequence.unpack([args])
:receiverArg sequence: iterable of items to be passed as context values
:argType sequence: iterable
:arg [args]: keys to be associated with sequence items. If args size is
equal to sequence size then args get appropriate sequence items. If
args size is 0 then args are indexed start from 1. Otherwise exception
will be thrown
:argType [args]: chain of strings
:returnType: context object
.. code::
yaql> [1, 2].unpack(a, b) -> $a + $b
3
yaql> [2, 3].unpack() -> $1 + $2
5
"""
lst = tuple(itertools.islice(sequence, len(args) + 1))
if 0 < len(args) != len(lst):
raise ValueError('Cannot unpack {0} elements into {1}'.format(
len(lst), len(args)))
if len(args) > 0:
for i in range(len(lst)):
context[args[i]] = lst[i]
else:
for i, t in enumerate(sequence, 1):
context[str(i)] = t
return context
def with_(context, *args):
""":yaql:with
Returns new context object where args are stored with 1-based indexes.
:signature: with([args])
:arg [args]: values to be stored under appropriate numbers $1, $2, ...
:argType [args]: chain of any values
:returnType: context object
.. code::
yaql> with("ab", "cd") -> $1 + $2
"abcd"
"""
for i, t in enumerate(args, 1):
context[str(i)] = t
return context
@specs.inject('__context__', yaqltypes.Context())
def let(__context__, *args, **kwargs):
""":yaql:let
Returns context object where args are stored with 1-based indexes
and kwargs values are stored with appropriate keys.
:signature: let([args], {kwargs})
:arg [args]: values to be stored under appropriate numbers $1, $2, ...
:argType [args]: chain of any values
:arg {kwargs}: values to be stored under appropriate keys
:argType {kwargs}: chain of mappings
:returnType: context object
.. code::
yaql> let(1, 2, a => 3, b => 4) -> $1 + $a + $2 + $b
10
"""
for i, value in enumerate(args, 1):
__context__[str(i)] = value
for key, value in kwargs.items():
__context__[key] = value
return __context__
@specs.parameter('name', yaqltypes.String())
@specs.parameter('func', yaqltypes.Lambda())
def def_(name, func, context):
""":yaql:def
Returns new context object with function name defined.
:signature: def(name, func)
:arg name: name of function
:argType name: string
:arg func: function to be stored under provided name
:argType func: lambda
:returnType: context object
.. code::
yaql> def(sq, $*$) -> [1, 2, 3].select(sq($))
[1, 4, 9]
"""
@specs.name(name)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
context.register_function(wrapper)
return context
@specs.parameter('left', contexts.ContextBase)
@specs.parameter('right', yaqltypes.Lambda(with_context=True))
@specs.name('#operator_->')
def send_context(left, right):
""":yaql:operator ->
Evaluates lambda on provided context and returns the result.
:signature: left -> right
:arg left: context to be used for function
:argType left: context object
:arg right: function
:argType right: lambda
:returnType: any (function return value type)
.. code::
yaql> let(a => 1) -> $a
1
"""
return right(left)
@specs.method
@specs.parameter('condition', yaqltypes.Lambda())
@specs.parameter('message', yaqltypes.String())
def assert__(engine, obj, condition, message=u'Assertion failed'):
""":yaql:assert
Evaluates condition against object. If it evaluates to true returns the
object, otherwise throws an exception with provided message.
:signature: obj.assert(condition, message => "Assertion failed")
:arg obj: object to evaluate condition on
:argType obj: any
:arg condition: lambda function to be evaluated on obj. If result of
function evaluates to false then trows exception message
:argType condition: lambda
:arg message: message to trow if condition returns false
:argType message: string
:returnType: obj type or message
.. code::
yaql> 12.assert($ < 2)
Execution exception: Assertion failed
yaql> 12.assert($ < 20)
12
yaql> [].assert($, "Failed assertion")
Execution exception: Failed assertion
"""
if utils.is_iterator(obj):
obj = utils.memorize(obj, engine)
if not condition(obj):
raise AssertionError(message)
return obj
@specs.name('#call')
@specs.parameter('callable_', yaqltypes.PythonType(
object, False, validators=(callable,)))
def call(callable_, *args, **kwargs):
""":yaql:call
Evaluates function with specified args and kwargs and returns the
result.
This function is used to transform expressions like '$foo(args, kwargs)'
to '#call($foo, args, kwargs)'.
Note that to use this functionality 'delegate' mode has to be enabled.
:signature: call(callable, args, kwargs)
:arg callable: callable function
:argType callable: python type
:arg args: sequence of items to be used for calling
:argType args: sequence
:arg kwargs: dictionary with kwargs to be used for calling
:argType kwargs: mapping
:returnType: any (callable return type)
"""
return callable_(*args, **kwargs)
@specs.parameter('func', yaqltypes.Lambda())
def lambda_(func):
""":yaql:lambda
Constructs a new anonymous function
Note that to use this function 'delegate' mode has to be enabled.
:signature: lambda(func)
:arg func: function to be returned
:argType func: lambda
:returnType: obj type or message
.. code::
yaql> let(func => lambda(2 * $)) -> [1, 2, 3].select($func($))
[2, 4, 6]
yaql> [1, 2, 3, 4].where(lambda($ > 3)($ + 1))
[3, 4]
"""
return func
@specs.name('#operator_.')
@specs.parameter('name', yaqltypes.Keyword())
@specs.inject('func', yaqltypes.Delegate(use_convention=False))
def get_property(func, obj, name):
""":yaql:operator .
Returns value of 'name' property.
:signature: left.right
:arg left: object
:argType left: any
:arg right: object property name
:argType right: keyword
:returnType: any
.. code::
yaql> now().year
2016
"""
func_name = '#property#{0}'.format(name)
return func(func_name, obj)
@specs.name('call')
@specs.parameter('name', yaqltypes.String())
@specs.parameter('args', yaqltypes.Sequence())
@specs.parameter('kwargs', utils.MappingType)
def call_func(context, engine, name, args, kwargs, receiver=utils.NO_VALUE):
""":yaql:call
Evaluates function name with specified args and kwargs and returns the
result.
:signature: call(name, args, kwargs)
:arg name: name of callable
:argType name: string
:arg args: sequence of items to be used for calling
:argType args: sequence
:arg kwargs: dictionary with kwargs to be used for calling
:argType kwargs: mapping
:returnType: any (callable return type)
.. code::
yaql> call(let, [1, 2], {a => 3, b => 4}) -> $1 + $a + $2 + $b
10
"""
return context(name, engine, receiver)(
*args, **utils.filter_parameters_dict(kwargs))
def register(context, delegates=False):
context.register_function(get_context_data)
context.register_function(op_dot)
context.register_function(unpack)
context.register_function(with_)
context.register_function(send_context)
context.register_function(let)
context.register_function(def_)
context.register_function(elvis_operator)
context.register_function(assert__)
context.register_function(call_func)
if delegates:
context.register_function(call)
context.register_function(lambda_)
def register_fallbacks(context):
context.register_function(get_property)