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,,
|