Skip to content

Commit 8bd369a

Browse files
committed
Added the possibility to transform normal methods into lambda-friendly methods
* Added `make_lambda_friendly` (fixes #1) * Separated goodies from main and added in goodies auto-generation of common methods (all the ones from the math.py module) * Added corresponding tests * minor display fix in add_unbound_method_to_stack
1 parent d52209d commit 8bd369a

File tree

5 files changed

+130
-10
lines changed

5 files changed

+130
-10
lines changed

mini_lambda/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from mini_lambda.generated import *
55
from mini_lambda.main import *
66
from mini_lambda.main import _
7+
from mini_lambda.goodies import *
78
# more user-friendly: provide a mapping to numbers package ? No, it might confuse users..
89
# from numbers import *
910

1011
# allow users to do
1112
# import mini_lambda as v
12-
__all__ = ['base', 'generated', 'main']
13+
__all__ = ['base', 'generated', 'main', 'goodies']

mini_lambda/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def evaluate_inner_function_and_apply_method(input):
125125

126126
# return a new InputEvaluator of the same type than self, with the new function as inner function
127127
# Note: we use precedence=None for coma-separated items inside the parenthesis
128-
string_expr = method.__name__ + '(' + get_repr(self, None) + ', ' \
128+
string_expr = method.__name__ + '(' + get_repr(self, None) + (', ' if len(m_args) > 0 else '') \
129129
+ ', '.join([get_repr(arg, None) for arg in m_args]) + ')'
130130
return type(self)(fun=evaluate_inner_function_and_apply_method,
131131
precedence_level=PRECEDENCE_SUBSCRIPTION_SLICING_CALL_ATTRREF,

mini_lambda/goodies.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import math
2+
3+
from mini_lambda.main import InputVar, make_lambda_friendly
4+
from inspect import getmembers
5+
6+
# Useful input variables
7+
s = InputVar('s', str)
8+
x = InputVar('x', int)
9+
l = InputVar('l', list)
10+
11+
12+
# Useful functions
13+
for package in [math]:
14+
# import pprint
15+
# pprint(getmembers(math, callable))
16+
for method_name, method in getmembers(package, callable):
17+
if not method_name.startswith('_'):
18+
# TODO maybe further filter only on methods with a single argument using signature ?
19+
20+
# create an equivalent method compliant with lambda expressions
21+
print('Creating method ' + method_name.capitalize())
22+
globals()[method_name.capitalize()] = make_lambda_friendly(method)

mini_lambda/main.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Type, TypeVar, Union, Tuple
1+
from copy import copy
2+
from typing import Type, TypeVar, Union, Tuple, Callable
23
from warnings import warn
34
import sys
45

@@ -543,6 +544,43 @@ def _(*evaluators: _LambdaExpression) -> Union[_LambdaExpression.LambdaFunction,
543544
""" Alias for '_' """
544545

545546

547+
def make_lambda_friendly(method: Callable, name: str = None):
548+
"""
549+
Utility method to transform any standard method, for example math.log, into a method usable inside lambda
550+
expressions. For example
551+
552+
```python
553+
from mini_lambda import x, _
554+
from math import log, e
555+
556+
Log = make_lambda_friendly(log)
557+
558+
# now you can use Log in your expressions
559+
complex_identity = _(Log(e ** x))
560+
```
561+
562+
:param method:
563+
:param name: an optional name for the method when used to display the expressions.
564+
:return:
565+
"""
566+
567+
# If the provided method does not have a name then name is mandatory
568+
if not hasattr(method, '__name__') and name is None:
569+
raise ValueError('This method does not have a name (it is either a partial or a lambda) so you have to '
570+
'provide one: the \'name\' argument is mandatory')
571+
572+
# create a named method if a new name is provided
573+
if name is not None:
574+
method = copy(method) # work on a copy just in case
575+
method.__name__ = name
576+
577+
def lambda_friendly_method(evaluator: _LambdaExpression):
578+
""" This is a replacement method for your method """
579+
return evaluator.add_unbound_method_to_stack(method)
580+
581+
return lambda_friendly_method
582+
583+
546584
def InputVar(symbol: str = None, typ: Type[T] = None) -> Union[T, _LambdaExpression]:
547585
"""
548586
Creates a variable to use in validator expression. The optional `typ` argument may be used to get a variable with
@@ -557,9 +595,3 @@ def InputVar(symbol: str = None, typ: Type[T] = None) -> Union[T, _LambdaExpress
557595
raise TypeError("symbol should be a string. It is recommended to use a very small string that is identical "
558596
"to the python variable name, for example s = InputVar('s')")
559597
return _LambdaExpression(symbol)
560-
561-
562-
# Useful input variables
563-
s = InputVar('s', str)
564-
x = InputVar('x', int)
565-
l = InputVar('l', list)

mini_lambda/tests/test_mini_lambda.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66

77
from mini_lambda import InputVar, Len, Str, Int, Repr, Bytes, Sizeof, Hash, Bool, Complex, Float, Oct, Iter, \
8-
Any, All, _, Slice, Get, Not, FunctionDefinitionError
8+
Any, All, _, Slice, Get, Not, FunctionDefinitionError, make_lambda_friendly
99
from math import sin, pi, cos
1010
from numbers import Real
1111

@@ -641,3 +641,68 @@ def test_evaluator_different_vars():
641641
with pytest.raises(FunctionDefinitionError):
642642
# getattr(a, b) # getattr(): attribute name must be string
643643
a.__getattr__(b)
644+
645+
646+
def test_add_new_simple():
647+
""" Tests that the mechanism provided to support additional functions works, by testing that log2 can be
648+
converted """
649+
650+
from mini_lambda import x, _
651+
from math import log, e
652+
653+
with pytest.raises(FunctionDefinitionError):
654+
log(x)
655+
656+
Log = make_lambda_friendly(log)
657+
complex_identity = _(Log(e ** x))
658+
659+
assert abs(complex_identity(3.5) - 3.5) < 10e-5
660+
print(complex_identity)
661+
# this is the remaining issue: the value of math.e is displayed instead of 'e'. We have to define 'constants'
662+
assert str(complex_identity) == "log(" + str(e) + " ** x)"
663+
664+
665+
def test_add_new_complex_partial():
666+
""" Tests that the mechanism provided to support additional functions works with partial functions. """
667+
668+
from functools import partial
669+
670+
from mini_lambda import x, _
671+
from math import log
672+
673+
with pytest.raises(FunctionDefinitionError):
674+
log(x, 10)
675+
676+
# option 1
677+
def log10(x):
678+
return log(x, 10)
679+
LogBase10 = make_lambda_friendly(log10)
680+
complex_identity = _(LogBase10(10 ** x))
681+
assert complex_identity(3.5) == 3.5
682+
assert str(complex_identity) == 'log10(10 ** x)'
683+
684+
# option 2: partial (only to fix keyword arguments or to leftmost positional arguments)
685+
Log15BaseX = make_lambda_friendly(partial(log, 15), name='log15baseX')
686+
complex_identity = _(1 / Log15BaseX(15 ** x))
687+
assert complex_identity(3.5) == 3.5
688+
assert str(complex_identity) == '1 / log15baseX(15 ** x)'
689+
690+
# option 3: lambda ! :)
691+
LogBase10 = make_lambda_friendly(lambda x: log(x, 10), name='log10')
692+
complex_identity = _(LogBase10(10 ** x))
693+
assert complex_identity(3.5) == 3.5
694+
assert str(complex_identity) == 'log10(10 ** x)'
695+
696+
697+
def test_generated_methods():
698+
""" Tests that equivalent methods generated by the package from various packages (currently, only math) work"""
699+
700+
from mini_lambda import x, _, Sin
701+
from math import sin
702+
703+
sine = _(Sin(x))
704+
705+
assert sine(3.5) == sin(3.5)
706+
print(sine)
707+
708+
assert str(sine) == "sin(x)"

0 commit comments

Comments
 (0)