File: test_so.py

package info (click to toggle)
python-makefun 1.15.4-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 440 kB
  • sloc: python: 2,384; makefile: 2
file content (349 lines) | stat: -rw-r--r-- 9,846 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
from __future__ import print_function
import sys
from inspect import getmodule

import pytest

from makefun import create_function, wraps, partial, with_partial

try:  # python 3.3+
    from inspect import signature, Signature, Parameter
except ImportError:
    from funcsigs import signature, Signature, Parameter


PY2 = sys.version_info < (3, )


def test_create_facades(capsys):
    """
    Simple test to create multiple functions with the same body
    This corresponds to the answer at
    https://stackoverflow.com/questions/13184281/python-dynamic-function-creation-with-custom-names/55105893#55105893
    :return:
    """

    # generic core implementation
    def generic_impl(f, *args, **kwargs):
        print("This is generic impl called by %s" % f.__name__)
        # here you could use f.__name__ in a if statement to determine what to do
        if f.__name__ == "func1":
            print("called from func1 !")
        return args, kwargs

    my_module = getmodule(generic_impl)

    # generate 3 facade functions with various signatures
    for f_name, f_params in [("func1", "b, *, a"),
                             ("func2", "*args, **kwargs"),
                             ("func3", "c, *, a, d=None")]:
        if f_name in {"func1", "func3"} and sys.version_info < (3, 0):
            # Python 2 does not support function annotations; Python 3.0-3.4 do not support variable annotations.
            pass
        else:
            # the signature to generate
            f_sig = "%s(%s)" % (f_name, f_params)

            # create the function dynamically
            f = create_function(f_sig, generic_impl, inject_as_first_arg=True)

            # assign the symbol somewhere (local context, module...)
            setattr(my_module, f_name, f)

    # grab each function and use it
    if sys.version_info >= (3, 0):
        func1 = getattr(my_module, 'func1')
        assert func1(25, a=12) == ((), dict(b=25, a=12))

    func2 = getattr(my_module, 'func2')
    assert func2(25, a=12) == ((25,), dict(a=12))

    if sys.version_info >= (3, 0):
        func3 = getattr(my_module, 'func3')
        assert func3(25, a=12) == ((), dict(c=25, a=12, d=None))

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

    if sys.version_info >= (3, 0):
        assert captured.out == """This is generic impl called by func1
called from func1 !
This is generic impl called by func2
This is generic impl called by func3
"""
    else:
        assert captured.out == """This is generic impl called by func2
"""


def test_so_decorator():
    """
    Tests that solution at
    https://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators/1594484#1594484
    actually works
    """

    # from functools import wraps

    def makebold(fn):
        @wraps(fn)
        def wrapped():
            return "<b>" + fn() + "</b>"

        return wrapped

    def makeitalic(fn):
        @wraps(fn)
        def wrapped():
            return "<i>" + fn() + "</i>"

        return wrapped

    @makebold
    @makeitalic
    def hello():
        """what?"""
        return "hello world"

    assert hello() == "<b><i>hello world</i></b>"
    assert hello.__name__ == "hello"
    help(hello)  # the help and signature are preserved
    assert hasattr(hello, '__wrapped__')


def test_so_facade():
    def create_initiation_function(cls, gen_init):
        # (1) check which signature we want to create
        params = [Parameter('self', kind=Parameter.POSITIONAL_OR_KEYWORD)]
        for mandatory_arg_name in cls.__init_args__:
            params.append(Parameter(mandatory_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD))
        for default_arg_name, default_arg_val in cls.__opt_init_args__.items():
            params.append(Parameter(default_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default_arg_val))
        sig = Signature(params)

        # (2) create the init function dynamically
        return create_function(sig, generic_init)

    # ----- let's use it

    def generic_init(self, *args, **kwargs):
        """Function to initiate a generic object"""
        assert len(args) == 0
        for name, val in kwargs.items():
            setattr(self, name, val)

    class my_class:
        __init_args__ = ["x", "y"]
        __opt_init_args__ = {"my_opt": None}

    my_class.__init__ = create_initiation_function(my_class, generic_init)

    # check
    o1 = my_class(1, 2)
    assert vars(o1) == {'y': 2, 'x': 1, 'my_opt': None}

    o2 = my_class(1, 2, 3)
    assert vars(o2) == {'y': 2, 'x': 1, 'my_opt': 3}

    o3 = my_class(my_opt='hello', y=3, x=2)
    assert vars(o3) == {'y': 3, 'x': 2, 'my_opt': 'hello'}


def test_so_sig_preserving(capsys):
    """
    Tests that the answer at
    https://stackoverflow.com/a/55163391/7262247
    is correct
    """
    def my_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)

        wrapper._decorator_name_ = 'my_decorator'
        return wrapper

    @my_decorator
    def my_func(x):
        """my function"""
        print('hello %s' % x)

    assert my_func._decorator_name_ == 'my_decorator'
    help(my_func)

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

    assert captured.out == """Help on function my_func in module tests.test_so:

my_func(x)
    my function

"""


def test_sig_preserving_2(capsys):
    """
    Checks that answer at
    https://stackoverflow.com/a/55163816/7262247
    works
    """
    def args_as_ints(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("wrapper executes")
            # convert all to int. note that in a signature-preserving wrapper almost all args will come as kwargs
            args = [int(x) for x in args]
            kwargs = dict((k, int(v)) for k, v in kwargs.items())
            return func(*args, **kwargs)

        return wrapper

    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x * y + 2 * z

    print(funny_function("3", 4.0, z="5"))
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    #
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z

    with pytest.raises(TypeError):
        funny_function(0)  # TypeError: funny_function() takes at least 2 arguments (1 given)

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

    assert captured.out == """wrapper executes
22
Help on function funny_function in module tests.test_so:

funny_function(x, y, z=3)
    Computes x*y + 2*z

"""


def test_so_partial(capsys):
    """
    Tests that the answer at
    https://stackoverflow.com/a/55165541/7262247
    is correct
    """
    def foo(a, b, c=1):
        """Return (a+b)*c."""
        return (a + b) * c

    bar10_p = partial(foo, b=10)

    assert bar10_p(0) == 10
    assert bar10_p(0, c=2) == 20

    help(bar10_p)

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

    sig_actual_call = "(a, b=10, c=1)"
    sig = sig_actual_call if PY2 else "(a, *, b=10, c=1)"

    assert captured.out == """Help on function foo in module tests.test_so:

foo{sig}
    <This function is equivalent to 'foo{sig_actual_call}', see original 'foo' doc below.>
    Return (a+b)*c.

""".format(sig=sig, sig_actual_call=sig_actual_call)


def test_so_partial2(capsys):
    """
    Tests that the solution at
    https://stackoverflow.com/a/55161579/7262247
    works (the one using makefun only. for the other two, see test.so.py in decopatch project)
    """

    @with_partial(a='hello', b='world')
    def test(a, b, x, y):
        print(a, b)
        print(x, y)

    if not PY2:
        # true keywords-only
        with pytest.raises(TypeError):
            test(1, 2)  # TypeError: test() takes 0 positional arguments but 2 were given

    test(x=1, y=2)

    help(test)

    @with_partial(a='hello', b='world')
    def test(a, b, x, y):
        """Here is a doc"""
        print(a, b)
        print(x, y)

    help(test)

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

        sig_actual_call = "(a='hello', b='world', x, y)"
        sig = "(a='hello', b='world', x=KW_ONLY_ARG!, y=KW_ONLY_ARG!)" if PY2 else "(*, a='hello', b='world', x, y)"

        ref_str = """hello world
1 2
Help on function test in module tests.test_so:

test{sig}
    <This function is equivalent to 'test{sig_actual_call}'.>

Help on function test in module tests.test_so:

test{sig}
    <This function is equivalent to 'test{sig_actual_call}', see original 'test' doc below.>
    Here is a doc

""".format(sig=sig, sig_actual_call=sig_actual_call)

        if (3, 0) <= sys.version_info < (3, 6):
            # in older versions of python, the order of **kwargs is not guaranteed (see PEP 468)
            assert captured.out.replace('a=hello', 'b=world') == ref_str.replace('a=hello', 'b=world')
        else:
            assert captured.out == ref_str


def test_so_partial_posargs(capsys):
    """Checks that the generated documentation is ok even in case of positional preset arg"""

    def test(a, b, x, y):
        print(a, b)
        print(x, y)

    fp = partial(test, 'hello', b='world')

    help(fp)

    captured = capsys.readouterr()
    with capsys.disabled():
        print(captured.out)

        sig_actual_call = "('hello', b='world', x, y)"
        sig = "(b='world', x=KW_ONLY_ARG!, y=KW_ONLY_ARG!)" if PY2 else "(*, b='world', x, y)"

        ref_str = """Help on function test in module tests.test_so:

test{sig}
    <This function is equivalent to 'test{sig_actual_call}'.>

""".format(sig=sig, sig_actual_call=sig_actual_call)
        assert captured.out == ref_str