Skip to content

Commit 4cf89db

Browse files
authored
Merge pull request #11 from ourstudio-se/10-implement-a-constant-function-1
10 implement a constant function 1
2 parents e1ebeed + 597f807 commit 4cf89db

File tree

4 files changed

+203
-13
lines changed

4 files changed

+203
-13
lines changed

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ You can use `maz` to compose function calls such that they call one another in a
1919
```
2020

2121
##### Partial function
22-
When you want to fix a parameter in a function, you could just use the `functools.partial`. However, that doesn't support fixing a positional argument on a specific index, which are quite common later on when you want to build complex compositions. Here you can use the `maz.pospartial` instead
22+
When you want to fix a parameter in a function, you could just use the `functools.partial`. However, that doesn't support fixing a positional argument on a specific index, which are quite common later on when you want to build complex compositions. Here you can use the `maz.partialpos` instead
2323
```python
2424
>>> import maz
2525
>>> def add(x,y): return x+y
26-
>>> add_two = maz.pospartial(add, [(1,2)]) # meaning fix argument on index 1 with the value 2
26+
>>> add_two = maz.partialpos(add, {1:2}) # fixing argument for index 1 to 2 (y=2)
2727
>>> add_two(3)
2828
>>> 5
2929
```
@@ -33,6 +33,7 @@ To support more complex compositions we've added some special functions such as
3333
```python
3434
import maz
3535
import operator
36+
import itertools
3637

3738
# "indexing" function - since only way to index a list in python is to do lst[x]
3839
lst = [2,1,3]
@@ -63,4 +64,21 @@ fn = maz.ifttt(
6364
fn(3) # >>> 4
6465
fn(4) # >>> 3
6566

67+
# "starfilter" just as itertools.starmap but a filter instead
68+
# Example, filter tuples who's sum is greater than 4
69+
next(
70+
maz.starfilter(
71+
maz.compose(
72+
lambda i: i > 4,
73+
add
74+
),
75+
itertools.product(range(3), range(3,6))
76+
)
77+
) # >>> (0,5)
78+
79+
# "constant" returns a function which, no matter the argument, returns a constant value.
80+
cnst_fn = maz.constant(True)
81+
cnst_fn() # >>> True
82+
cnst_fn(cnst_fn()) # >>> True
83+
6684
```

maz/__init__.py

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
import functools
23
import typing
34
import operator
@@ -56,6 +57,8 @@ def starzip(iterables: list):
5657
def pospartial(function, positional_arguments):
5758

5859
"""
60+
DEPRECATED. Use partialpos instead.
61+
5962
`pospartial` is a complementing function to functools.partial, where
6063
one can say which positional argument should be hardcoded into function.
6164
Usually, this is solved by instead setting a keyword argument and thus
@@ -89,6 +92,71 @@ def wrapper(*args, fn=function, pas=positional_arguments, **kwargs):
8992
return fn(*nargs, **kwargs)
9093
return wrapper
9194

95+
class partialpos:
96+
97+
"""
98+
Return a new partial function object which when called behave like
99+
`function` called with positional arguments given in `positional_arguments`.
100+
Each key `i` corresponds to the `i`'th argument in `function`.
101+
102+
"""
103+
104+
def __init__(self, function, positional_arguments: typing.Dict[int, typing.Any]):
105+
self.function = function
106+
self.positional_arguments = positional_arguments
107+
108+
def _defaults(self) -> dict:
109+
return dict(
110+
itertools.starmap(
111+
lambda k,v: (
112+
self.function.__code__.co_varnames.index(k),
113+
v.default
114+
),
115+
filter(
116+
compose(
117+
lambda x: x is not inspect.Parameter.empty,
118+
operator.attrgetter("default"),
119+
operator.itemgetter(1),
120+
),
121+
inspect.signature(self.function).parameters.items()
122+
)
123+
)
124+
)
125+
126+
def __call__(self, *args, **kwargs):
127+
128+
# First we collect function's default arguments,
129+
# all positional arguments hard coded in self.positional_arguments
130+
# and then given from kwargs input. It is also priorized in that order,
131+
# so if e.g. default value on argument a=1, but user gives a=2 in kwargs,
132+
# then a=2 is what will be calculated on.
133+
# Finally will append the positional arguments given from
134+
# user into function. So the experience will be as if it is
135+
# calling any other function.
136+
137+
left = set(range(len(self.function.__code__.co_varnames))).difference(self.positional_arguments.keys())
138+
return self.function(
139+
*map(
140+
operator.itemgetter(1),
141+
sorted(
142+
dict(
143+
itertools.chain(
144+
self._defaults().items(),
145+
self.positional_arguments.items(),
146+
itertools.starmap(
147+
lambda k,v: (
148+
self.function.__code__.co_varnames.index(k),
149+
v
150+
),
151+
kwargs.items(),
152+
),
153+
zip(left, args)
154+
)
155+
).items(),
156+
key=operator.itemgetter(0),
157+
)
158+
)
159+
)
92160

93161
class compose_pair:
94162
"""
@@ -228,8 +296,16 @@ def kwargs2dict(**kwargs):
228296
class fnexcept:
229297

230298
"""
231-
Wrapping a raising function to return the exception
232-
as the second item in a tuple.
299+
Wrapping a raising function and a handler function that
300+
returns an alternative to the exception.
301+
302+
Parameters
303+
----------
304+
raising_function: Callable
305+
a function that may raise exceptions for perticular input values
306+
307+
handler_function: Callable
308+
a function, taking same arguments as raising_function, returning some alternative value
233309
234310
Examples
235311
--------
@@ -246,7 +322,7 @@ class fnexcept:
246322
... raise Exception("not allowed")
247323
... return a+1
248324
>>> raising_wrapper = fnexcept(raising, lambda: 0)
249-
>>> raising_wrapper(2)
325+
>>> raising_wrapper(1)
250326
2
251327
252328
Returns
@@ -264,7 +340,7 @@ def __call__(self, *args, **kwargs) -> typing.Any:
264340
*args,
265341
**kwargs
266342
)
267-
except Exception as e:
343+
except:
268344
return self.handler_function(*args, **kwargs)
269345

270346

@@ -353,13 +429,71 @@ def __call__(self, objects: typing.Iterable[typing.Any]) -> typing.Iterable[typi
353429

354430
class ifttt:
355431

432+
"""
433+
Returns a function object that will evaluate `fnif` first.
434+
If it evaluated to true, then `fnthen` will be called, or else
435+
`fnelse` will be called. `fnif`, `fnthen` and `fnelse` all has
436+
the same function signature.
437+
438+
Parameters
439+
----------
440+
fnif: Callable[typing.Any, bool]
441+
a predicate function returning true or false
442+
443+
fnthen: Callable[typing.Any, typing.Any]
444+
any function object
445+
446+
fnelse: Callable[typing.Any, typing.Any]
447+
any function object
448+
449+
Examples
450+
--------
451+
>>> list(
452+
... map(
453+
... ifttt(
454+
... lambda x: x > 2, # if number is greater than 2
455+
... lambda x: x + 1, # then add by 1
456+
... lambda x: x - 1 # else subtract by 1
457+
... ),
458+
... range(5)
459+
... )
460+
... )
461+
[-1, 0, 1, 4, 5]
462+
463+
Returns
464+
-------
465+
out : Callable[Any, Any]
466+
"""
467+
356468
def __init__(self, fnif, fnthen, fnelse):
357469
self.fnif = fnif
358470
self.fnthen = fnthen
359471
self.fnelse = fnelse
360472

361-
def __call__(self, obj: typing.Any) -> typing.Any:
362-
if self.fnif(obj):
363-
return self.fnthen(obj)
473+
def __call__(self, *args, **kwargs) -> typing.Any:
474+
if self.fnif(*args, **kwargs):
475+
return self.fnthen(*args, **kwargs)
364476
else:
365-
return self.fnelse(obj)
477+
return self.fnelse(*args, **kwargs)
478+
479+
def starfilter(function, iterable):
480+
481+
"""
482+
As built in `filter` but function is called with each item in iterable as a starred argument.
483+
"""
484+
return filter(
485+
lambda x: function(*x),
486+
iterable,
487+
)
488+
489+
class constant:
490+
491+
"""
492+
Returns a function which returns `val`.
493+
"""
494+
495+
def __init__(self, val):
496+
self.val = val
497+
498+
def __call__(self, *args, **kwargs):
499+
return self.val

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = maz
3-
version = 0.0.6
3+
version = 0.0.7
44
author = ourstudio
55
author_email = rikard@ourstudio.se
66
description = Functional programming tools.

tests/test_maz.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import functools
12
import maz
23
import operator
3-
import functools
4+
import pytest
45

56

67
def test_compose():
@@ -163,4 +164,41 @@ def __eq__(self, o):
163164
Var("d", 5),
164165
Var("e", 6),
165166
]
166-
assert actual == expected
167+
assert actual == expected
168+
169+
def test_partialpos():
170+
171+
def f(a,b,c,d=1):
172+
return 1*a + 2*b + 3*c + 4*d
173+
174+
assert maz.partialpos(f, {1:2, 2:2})(1,2) == 1*1 + 2*2 + 3*2 + 4*2
175+
assert maz.partialpos(f, {1:2, 2:2})(1) == 1*1 + 2*2 + 3*2 + 4*1
176+
assert maz.partialpos(f, {1:2, 3:3})(1,3) == 1*1 + 2*2 + 3*3 + 4*3
177+
178+
def f(a,b):
179+
return a+b
180+
181+
assert maz.partialpos(f, {1: 2})(1) == 3
182+
assert maz.partialpos(f, {1: 2})(1,2) == 3
183+
184+
with pytest.raises(Exception):
185+
assert maz.partialpos(f, {"1":2})(1)
186+
187+
with pytest.raises(Exception):
188+
assert maz.partialpos(f, {1:2})()
189+
190+
def test_starfilter():
191+
192+
assert list(
193+
maz.starfilter(
194+
lambda x,y: x+y >= 4,
195+
[(1,2),(2,3),(3,4),(4,5)]
196+
)
197+
) == [(2,3),(3,4),(4,5)]
198+
199+
def test_constant():
200+
201+
cnst_fn = maz.constant(True)
202+
assert cnst_fn(0) == True
203+
assert cnst_fn("hello") == True
204+
assert cnst_fn(lambda x: x+1) == True

0 commit comments

Comments
 (0)