-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathhelpers.py
297 lines (252 loc) · 9.03 KB
/
helpers.py
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
"""Some helper functions builtin based upon core features"""
from __future__ import annotations
import inspect
from functools import partial, wraps
from os import PathLike
from typing import Any, Callable, Dict, Tuple, Type, Union
from .utils import IgnoreType
from .ignore import IgnoreList
from .core import argname, varname
def register(
cls_or_func: type = None,
frame: int = 1,
ignore: IgnoreType = None,
multi_vars: bool = False,
raise_exc: bool = True,
strict: bool = True,
) -> Union[Type, Callable]:
"""A decorator to register __varname__ to a class or function
When registered to a class, it can be accessed by `self.__varname__`;
while to a function, it is registered to globals, meaning that it can be
accessed directly.
Args:
frame: The call stack index, indicating where this class
is instantiated relative to where the variable is finally retrieved
multi_vars: Whether allow multiple variables on left-hand side (LHS).
If `True`, this function returns a tuple of the variable names,
even there is only one variable on LHS.
If `False`, and multiple variables on LHS, a
`VarnameRetrievingError` will be raised.
raise_exc: Whether we should raise an exception if failed
to retrieve the name.
strict: Whether to only return the variable name if the result of
the call is assigned to it directly.
Examples:
>>> @varname.register
>>> class Foo: pass
>>> foo = Foo()
>>> # foo.__varname__ == 'foo'
>>>
>>> @varname.register
>>> def func():
>>> return __varname__
>>> foo = func() # foo == 'foo'
Returns:
The wrapper function or the class/function itself
if it is specified explictly.
"""
if inspect.isclass(cls_or_func):
orig_init = cls_or_func.__init__ # type: ignore
@wraps(cls_or_func.__init__) # type: ignore
def wrapped_init(self, *args, **kwargs):
"""Wrapped init function to replace the original one"""
self.__varname__ = varname(
frame - 1,
ignore=ignore,
multi_vars=multi_vars,
raise_exc=raise_exc,
strict=strict,
)
orig_init(self, *args, **kwargs)
cls_or_func.__init__ = wrapped_init # type: ignore
return cls_or_func
if inspect.isfunction(cls_or_func):
@wraps(cls_or_func)
def wrapper(*args, **kwargs):
"""The wrapper to register `__varname__` to a function"""
cls_or_func.__globals__["__varname__"] = varname(
frame - 1,
ignore=ignore,
multi_vars=multi_vars,
raise_exc=raise_exc,
strict=strict,
)
try:
return cls_or_func(*args, **kwargs)
finally:
del cls_or_func.__globals__["__varname__"]
return wrapper
# None, meaning we have other arguments
return partial(
register,
frame=frame,
ignore=ignore,
multi_vars=multi_vars,
raise_exc=raise_exc,
strict=strict,
)
class Wrapper:
"""A wrapper with ability to retrieve the variable name
Examples:
>>> foo = Wrapper(True)
>>> # foo.name == 'foo'
>>> # foo.value == True
>>> val = {}
>>> bar = Wrapper(val)
>>> # bar.name == 'bar'
>>> # bar.value is val
Args:
value: The value to be wrapped
raise_exc: Whether to raise exception when varname is failed to retrieve
strict: Whether to only return the variable name if the wrapper is
assigned to it directly.
Attributes:
name: The variable name to which the instance is assigned
value: The value this wrapper wraps
"""
def __init__(
self,
value: Any,
frame: int = 1,
ignore: IgnoreType = None,
raise_exc: bool = True,
strict: bool = True,
):
# This call is ignored, since it's inside varname
self.name = varname(
frame=frame - 1,
ignore=ignore,
raise_exc=raise_exc,
strict=strict,
)
self.value = value
def __str__(self) -> str:
return repr(self.value)
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} "
f"(name={self.name!r}, value={self.value!r})>"
)
def jsobj(*args: Any, **kwargs: Any) -> Dict[str, Any]:
"""A wrapper to create a JavaScript-like object
When an argument is passed as positional argument, the name of the variable
will be used as the key, while the value will be used as the value.
Examples:
>>> obj = jsobj(a=1, b=2)
>>> # obj == {'a': 1, 'b': 2}
>>> # obj.a == 1
>>> # obj.b == 2
>>> a = 1
>>> b = 2
>>> obj = jsobj(a, b, c=3)
>>> # obj == {'a': 1, 'b': 2, 'c': 3}
Args:
*args: The positional arguments
**kwargs: The keyword arguments
Returns:
A dict-like object
"""
argnames: Tuple[str, ...] = argname("args") # type: ignore
out = dict(zip(argnames, args))
out.update(kwargs)
return out
def debug(
var,
*more_vars,
prefix: str = "DEBUG: ",
merge: bool = False,
repr: bool = True,
sep: str = "=",
vars_only: bool = False,
) -> None:
"""Print variable names and values.
Examples:
>>> a = 1
>>> b = object
>>> print(f'a={a}') # previously, we have to do
>>> print(f'{a=}') # or with python3.8
>>> # instead we can do:
>>> debug(a) # DEBUG: a=1
>>> debug(a, prefix='') # a=1
>>> debug(a, b, merge=True) # a=1, b=<object object at 0x2b9a4c89cf00>
Args:
var: The variable to print
*more_vars: Other variables to print
prefix: A prefix to print for each line
merge: Whether merge all variables in one line or not
sep: The separator between the variable name and value
repr: Print the value as `repr(var)`? otherwise `str(var)`
"""
var_names = argname("var", "*more_vars", vars_only=vars_only, func=debug)
values = (var, *more_vars)
name_and_values = [
f"{var_name}{sep}{value!r}" if repr else f"{var_name}{sep}{value}"
for var_name, value in zip(var_names, values) # type: ignore
]
if merge:
print(f"{prefix}{', '.join(name_and_values)}")
else:
for name_and_value in name_and_values:
print(f"{prefix}{name_and_value}")
def exec_code(
code: str,
globals: Dict[str, Any] = None,
locals: Dict[str, Any] = None,
/,
sourcefile: PathLike | str = None,
frame: int = 1,
ignore: IgnoreType = None,
**kwargs: Any,
) -> None:
"""Execute code where source code is visible at runtime.
This function is useful when you want to execute some code, where you want to
retrieve the AST node of the code at runtime. This function will create a
temporary file and write the code into it, then execute the code in the
file.
Examples:
>>> from varname import varname
>>> def func(): return varname()
>>> exec('var = func()') # VarnameRetrievingError:
>>> # Unable to retrieve the ast node.
>>> from varname.helpers import code_exec
>>> code_exec('var = func()') # var == 'var'
Args:
code: The code to execute.
globals: The globals to use.
locals: The locals to use.
sourcefile: The source file to write the code into.
if not given, a temporary file will be used.
This file will be deleted after the code is executed.
frame: The call stack index. You can understand this as the number of
wrappers around this function. This is used to fetch `globals` and
`locals` from where the destination function (include the wrappers
of this function)
is called.
ignore: The intermediate calls to be ignored. See `varname.ignore`
Note that if both `globals` and `locals` are given, `frame` and
`ignore` will be ignored.
**kwargs: The keyword arguments to pass to `exec`.
"""
if sourcefile is None:
import tempfile
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as f:
f.write(code)
sourcefile = f.name
else:
sourcefile = str(sourcefile)
with open(sourcefile, "w") as f:
f.write(code)
if globals is None or locals is None:
ignore_list = IgnoreList.create(ignore)
frame_info = ignore_list.get_frame(frame)
if globals is None:
globals = frame_info.f_globals
if locals is None:
locals = frame_info.f_locals
try:
exec(compile(code, sourcefile, "exec"), globals, locals, **kwargs)
finally:
import os
os.remove(sourcefile)