luckylucy.utils.functions 1.85rc1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ # /// script
2
+ # requires-python = ">=2.7, !=3.0.*, !=3.1.*"
3
+ # dependencies = [
4
+ # "luckylucy.utils.clazzes",
5
+ # ]
6
+ #
7
+ # [tool.luckylucy]
8
+ # module = true
9
+ # ///
10
+ from datetime import datetime, timedelta
11
+
12
+ import functools
13
+ import inspect
14
+ import logging
15
+
16
+ from .clazzes import Kwargs
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ __author__ = 'luckydonald'
21
+ __all__ = [
22
+ "caller", "deprecated", "gone", "cached",
23
+ "CallerResult", "CallerInfo"
24
+ ]
25
+
26
+
27
+ class CallerResult(Kwargs):
28
+ """
29
+ Contains the data of who called us.
30
+
31
+ Can also be used via ** mapping.
32
+ >>> x = CallerResult("aaa", "bbb")
33
+ >>> dict(**x)
34
+ {'self': 'aaa', 'caller': 'bbb'}
35
+ """
36
+ __FIELDS__ = ("self", "caller")
37
+
38
+ # noinspection PyMethodParameters
39
+ def __init__(this, self, caller):
40
+ """
41
+ :type self: CallerInfo
42
+ :type caller: CallerInfo
43
+ """
44
+ this.self = self
45
+ this.caller = caller
46
+ # end def
47
+ # end class
48
+
49
+
50
+ class CallerInfo(Kwargs):
51
+ __FIELDS__ = ("name", "file", "line", "code")
52
+
53
+ def __init__(self, name, file, line, code=None):
54
+ self.name = name
55
+ self.file = file
56
+ self.line = line
57
+ self.code = code
58
+ # end def
59
+ # end class
60
+
61
+
62
+ def caller(level=0, kwarg_name="call"):
63
+ """
64
+ functions decorated with this will be called with an `call` kwarg, containing information about the function itself, and the caller.
65
+ If the caller could not be fetched correctly, the `caller`s attributes all will be `None`.
66
+
67
+ Level is the amount of functions to go upwards. Default is 1.
68
+ kwarg_name is the name of the kwarg parameter we input the caller data as. Default is "call".
69
+
70
+
71
+ Usage:
72
+
73
+ >>> @caller
74
+ ... def whatever(arg, kwarg=None, call=None):
75
+ ... pass # do something with the `call` info
76
+
77
+ >>> @caller(1) # change this if you you are deep within another call. You can adust the stack index here.
78
+ ... def whatever(arg, kwarg=None, call=None):
79
+ ... pass # do something with the `call` info
80
+
81
+ >>> @caller(kwarg_name="_call_info") # change this if you you are deep within another call. You can adust the stack index here.
82
+ ... def whatever(arg, kwarg=None, _call_info=None):
83
+ ... pass # do something with the `_call_info` info
84
+ """
85
+ def caller_func_wrapper(func, level=0, kwarg_name="call"):
86
+ """
87
+ :param func: The function we need to call.
88
+
89
+ :param level: The level to go back in stack.
90
+ :type level: int
91
+
92
+ :param kwarg_name: the **kwargs element to write to.
93
+ :type kwarg_name: str
94
+ :return:
95
+ """
96
+
97
+ @functools.wraps(func)
98
+ def caller_wrapping_func(*args, **kwargs):
99
+ # so the decorated function is the 'self' one.
100
+ try:
101
+ self = CallerInfo(
102
+ name=func.__name__,
103
+ file=func.__code__.co_filename,
104
+ line=func.__code__.co_firstlineno + 1,
105
+ )
106
+ except AttributeError as e:
107
+ logger.debug("self lookup failed", exc_info=True)
108
+ # we just don't know
109
+ self = CallerInfo(
110
+ name=None,
111
+ file=None,
112
+ line=None,
113
+ )
114
+ # end if
115
+ # end try
116
+ try:
117
+ s = inspect.stack()
118
+ # +1 as we need to ignore the caller_wrapping_func
119
+ stack = s[level + 1] # the +1 out-of-bounds is handled by the except IndexError.
120
+ caller = CallerInfo(
121
+ name=stack[3],
122
+ file=stack[1],
123
+ line=stack[2],
124
+ code=stack[4][0],
125
+ )
126
+ except (TypeError, IndexError):
127
+ caller = CallerInfo(
128
+ name=None,
129
+ file=None,
130
+ line=None,
131
+ code=None,
132
+ )
133
+ # end try
134
+ call = CallerResult(
135
+ self=self,
136
+ caller=caller
137
+ )
138
+ kwargs[kwarg_name] = call
139
+ return func(*args, **kwargs)
140
+
141
+ return caller_wrapping_func
142
+ # end def
143
+
144
+ def caller_wrapping_func_applyer(func):
145
+ return caller_func_wrapper(func, level=level, kwarg_name=kwarg_name)
146
+ # end def
147
+
148
+ try:
149
+ is_callable = callable(level)
150
+ except (SyntaxError, NameError):
151
+ is_callable = inspect.isfunction(level) or inspect.isbuiltin(level)
152
+ # end try
153
+ if is_callable: # level was not provided, level is actually our function to decorate
154
+ func = level
155
+ _func = functools.wraps(func)(caller_func_wrapper)
156
+ return _func(func) # keeping the defaults at the defaults.
157
+ return caller_wrapping_func_applyer # else: level is level, we need to return a decorator accepting the function
158
+ # end def
159
+
160
+
161
+ def deprecated(func, message=None):
162
+ """This is a decorator which can be used to mark functions
163
+ as deprecated. It will result in a warning being logged
164
+ when the function is used."""
165
+
166
+ if isinstance(func, str): # @deprecated("message")
167
+ message = func
168
+
169
+ def inners(func): # @deprecated("message")(func)
170
+ return deprecated(func, message) # @deprecated("message")(func) -> @deprecated(func, "message")
171
+
172
+ return inners
173
+
174
+ # end if
175
+ # -> @deprecated(func)
176
+
177
+ @functools.wraps(func)
178
+ def new_func(*args, **kwargs):
179
+ try:
180
+ try: # python 2 + python 3
181
+ f_name = func.__name__
182
+ f_file = func.__code__.co_filename
183
+ f_line = func.__code__.co_firstlineno + 1
184
+ except AttributeError: # maybe some old python 2?
185
+ f_name = func.func_code.co_name
186
+ f_file = func.func_code.co_filename
187
+ f_line = func.func_code.co_firstlineno + 1
188
+ # end try
189
+ try:
190
+ logger.warning(
191
+ "Call to deprecated function {f_name} at file {f_file}:{f_line}\n"
192
+ "{msg}Called from function {c_name} at file {c_file}:{c_line}\nLine: {c_code}".format(
193
+ f_name=f_name, f_file=f_file,
194
+ f_line=f_line,
195
+ msg="Warning: " + message.strip() + "\n" if message else "",
196
+ c_name=inspect.stack()[1][3], c_file=inspect.stack()[1][1],
197
+ c_line=inspect.stack()[1][2], c_code=inspect.stack()[1][4][0],
198
+ ))
199
+ except TypeError:
200
+ logger.warning(
201
+ "Call to deprecated function {f_name} at file {f_file}:{f_line}{msg}".format(
202
+ f_name=f_name, f_file=f_file,
203
+ f_line=f_line, msg="\nWarning: " + message.strip() if message else ""
204
+ ))
205
+ except (AttributeError, TypeError):
206
+ logger.warning(
207
+ "Call to deprecated function.{msg}".format(msg="\nWarning: " + message.strip() if message else ""),
208
+ exc_info=True
209
+ )
210
+ return func(*args, **kwargs)
211
+
212
+ return new_func # @deprecated(func)
213
+
214
+
215
+ # end def
216
+ """
217
+ Test would be
218
+ >> @deprecated
219
+ >> def foo(): pass
220
+
221
+ >> @deprecated("Custom Message")
222
+ >> def foo(): pass
223
+ """
224
+
225
+
226
+ def gone(func):
227
+ """This is a decorator which can be used to mark functions
228
+ as deprecated, and gone. It will raise a NotImplementedError
229
+ when the function is used."""
230
+ @functools.wraps(func)
231
+ def new_func(*args, **kwargs):
232
+ raise NotImplementedError(
233
+ "Call to deprecated function {} at file {}:{}. Called from function {} at file {}:{}\nLine: {}".format(
234
+ func.__name__, func.__code__.co_filename, # python2: func.func_code.co_filename
235
+ func.__code__.co_firstlineno + 1, # python2: func.func_code.co_firstlineno
236
+ inspect.stack()[1][3], inspect.stack()[1][1], inspect.stack()[1][2], inspect.stack()[1][4][0]
237
+ )
238
+ )
239
+
240
+ # end def
241
+ return new_func
242
+ # end def
243
+
244
+
245
+ def cached(max_age=None):
246
+ """
247
+ Cache a function.
248
+ You can specify a max_age (:class:`datetime.timedelta`) after when the function will be called again.
249
+
250
+ :keyword max_age: timedelta of how long we should keep the cache. None means forever.
251
+ :type max_age: None or timedelta
252
+ """
253
+ # http://stackoverflow.com/a/30698822/3423324
254
+ # _format = {"returns": "I am a return value!!", "max_age": timedelta(seconds=60), "last_hit": datetime.now()}
255
+ memo = {}
256
+ import json
257
+
258
+ def stringify(obj):
259
+ if isinstance(obj, (list, tuple)):
260
+ return repr([stringify(x) for x in obj])
261
+ if isinstance(obj, dict):
262
+ res = {}
263
+ for k, v in obj.items():
264
+ res[k] = stringify(k)
265
+ # end for
266
+ return repr(res)
267
+ try:
268
+ return json.dumps(obj)
269
+ except TypeError:
270
+ return str(repr(obj)) + str(obj) + str(id(obj))
271
+ # end if
272
+
273
+ def func_wrapper(function, **decorator_kwargs):
274
+ def wrapper(*args, **kwargs):
275
+ args_key = stringify([args, kwargs])
276
+ now = datetime.now()
277
+ if "max_age" in decorator_kwargs:
278
+ max_age_ = decorator_kwargs["max_age"]
279
+ else:
280
+ max_age_ = max_age
281
+ if (args_key) in memo.keys():
282
+ logger.debug(memo[args_key])
283
+ if not max_age_ or now - memo[args_key]["last_hit"] < memo[args_key]["max_age"]:
284
+ return memo[args_key]["returns"]
285
+ # end if
286
+ # end if
287
+ res = function(*args, **kwargs)
288
+ memo[args_key] = {"returns": res, "last_hit": now, "max_age": max_age_}
289
+ return res
290
+
291
+ return wrapper
292
+
293
+ try:
294
+ is_callable = callable(max_age)
295
+ except (SyntaxError, NameError):
296
+ is_callable = inspect.isfunction(max_age) or inspect.isbuiltin(max_age)
297
+ # end try
298
+ if is_callable: # max_age was not provided, max_age is actually our function to decorate
299
+ func = max_age
300
+ return func_wrapper(func, max_age=None)
301
+ return func_wrapper # else: max_age is max_age, we need to return a decorator accepting the function
302
+ # end def
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.5
2
+ Name: luckylucy.utils.functions
3
+ Version: 1.85.pre1
4
+ Summary: A collection of utilities I use across different projects (luckylucy.utils.functions)
5
+ Project-URL: Homepage, https://github.com/luckydonald/luckylucy.utils
6
+ Project-URL: Changelog, https://github.com/luckydonald/luckylucy.utils/blob/HEAD/CHANGELOG.md
7
+ Author-email: luckylucy <luckylucy.utils._.code@luckydonald.de>
8
+ License-Expression: GPL-2.0-or-later
9
+ Requires-Python: !=3.0.*,!=3.1.*,>=2.7
10
+ Requires-Dist: luckylucy-utils-clazzes==1.85.pre1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # luckylucy.utils.functions 1.85.pre1
14
+
15
+ A collection of utilities I use across different projects, part of [luckylucy.utils](https://github.com/luckydonald/luckylucy.utils).
16
+
17
+ ## Requirements
18
+
19
+ - `luckylucy.utils.clazzes==1.85.pre1`
20
+
21
+ - [Changes in 1.85.pre1](https://github.com/luckydonald/luckylucy.utils/releases/tag/v1.85.pre1)
22
+ - [Changelog](https://github.com/luckydonald/luckylucy.utils/blob/HEAD/CHANGELOG.md)
@@ -0,0 +1,4 @@
1
+ luckylucy/utils/functions.py,sha256=vn4Mfz43vhTPdn3gvezgjPQpn1Iwri3Lc4N1r2PbZbE,10237
2
+ luckylucy_utils_functions-1.85rc1.dist-info/METADATA,sha256=9Sx4ryfORlxS4LTOIClQuAjWz9Po-mFTfhlvGiBLR78,976
3
+ luckylucy_utils_functions-1.85rc1.dist-info/WHEEL,sha256=K_1GFnQyJknHy845BKcqIEBshHiHkrbA-n3al-vZCiY,105
4
+ luckylucy_utils_functions-1.85rc1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any