-
Notifications
You must be signed in to change notification settings - Fork 1
/
code.py
297 lines (206 loc) · 8.19 KB
/
code.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
from generallibrary.diagram import TreeDiagram
from generallibrary.functions import Recycle
from generallibrary.iterables import join_with_str
import pyperclip
import os
import inspect
import inspect
import logging
from pathlib import Path
def get_calling_module(add_depth=2):
return inspect.getmodule(inspect.stack()[add_depth][0])
def get_name_from_module(module):
if module.__name__ == "__main__":
return os.path.splitext(os.path.basename(module.__file__))[0]
else:
return module.__name__
class Log(TreeDiagram, Recycle):
ROOT_NAME = "root"
FILE_FORMAT = {
"Level": "%(levelname)-s",
"Module": "%(name)-s",
"Time": "%(asctime)-s",
"MS": "%(msecs)-d",
"Function": "%(funcName)-s",
"Line": "%(lineno)d",
"Message": "%(message)s",
}
STREAM_FORMAT = FILE_FORMAT.copy()
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
@classmethod
def _name(cls, name):
""" Mimic logging.Logger's naming behaviour """
return str(name) if name else cls.ROOT_NAME
_recycle_keys = {"name": lambda x: Log._name(x)}
def __init__(self, name=None, parent=None):
""" Todo: Make Log use __name__ from previous frame so it doesn't write to root. """
name = self._name(name)
self.name = name
self.logger = logging.getLogger(name)
assert name == self.logger.name
def debug(self, *msg): self.logger.debug(join_with_str(" ", msg))
def info(self, *msg): self.logger.info(join_with_str(" ", msg))
def warning(self, *msg): self.logger.warning(join_with_str(" ", msg))
def error(self, *msg): self.logger.error(join_with_str(" ", msg))
def critical(self, *msg): self.logger.critical(join_with_str(" ", msg))
def _configure_helper(self, level, delimiter, format_, handler):
self.logger.setLevel(level=level)
formatter = logging.Formatter(fmt=delimiter.join(format_.values()), datefmt=self.DATE_FORMAT)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
def _file_handler(self):
path = Path(f"{self.name}.log.csv")
file_handler = logging.FileHandler(path)
if not path.exists() or not path.stat().st_size:
path.write_text(f'{",".join(self.FILE_FORMAT.keys())}\n')
return file_handler
def configure_file(self, level=10):
""" Todo: Use another delimiter than , in Log and make sure it can handle quotes. """
self._configure_helper(level=level, delimiter=",", format_=self.FILE_FORMAT, handler=self._file_handler())
def configure_stream(self, level=10):
self._configure_helper(level=level, delimiter=" : ", format_=self.STREAM_FORMAT,
handler=logging.StreamHandler())
@staticmethod
def loggers():
return logging.root.manager.loggerDict
def is_root(self):
return self.name == self.ROOT_NAME
def _logger_is_child(self, name: str):
if self.is_root():
return "." not in name
return name.startswith(self.name) and (name.count(".") - self.name.count(".")) == 1
def _get_parent_name(self):
if self.is_root():
return None
elif "." not in self.name:
return self.ROOT_NAME
else:
return ".".join(self.name.split(".")[0:-1])
# def spawn_all(self): # Idea here was to spawn all Logs with a sorted list of logger names which is faster
# sorted(self.loggers().keys())
def spawn_parents(self):
if not self.is_root():
parent_log = type(self)(name=self._get_parent_name())
self.set_parent(parent=parent_log)
def spawn_children(self):
for name, logger in self.loggers().items():
if self._logger_is_child(name=name):
type(self)(name=name, parent=self)
def __repr__(self):
return f"<Log: '{self.name}'>"
def clipboard_copy(s):
""" Copy a string to clipboard.
Rudely tries to installs xclip on linux if it fails. """
def _call():
return pyperclip.copy(s)
try:
_call()
except pyperclip.PyperclipException:
os.system("sudo apt-get install xclip")
_call()
def clipboard_get():
""" Get clipboard string. """
return pyperclip.paste()
class CodeLine(TreeDiagram):
""" Tool to help with printing code line by line.
Top parent is ignored when printing. """
indent_str = " " * 4
def __init__(self, code_str=None, space_before=0, space_after=0, parent=None):
if code_str is None:
code_str = ""
self.code_str = code_str
self.space_before = space_before
self.space_after = space_after
def __str__(self):
return self.text()
def __contains__(self, item):
return str(self).__contains__(item)
def get_lines(self, watermark=True):
""" Generate a list of formatted code lines by iterating stored _Line instances. """
lines = []
for codeLine in self.get_all():
lines.extend([""] * codeLine.space_before)
if codeLine.code_str:
lines.append(f"{self.indent_str * (len(codeLine.get_parents(depth=-1)) - 1)}{codeLine.code_str}")
lines.extend([""] * codeLine.space_after)
if watermark:
lines.insert(0, "# -------------------- GENERATED CODE --------------------")
lines.append("# --------------------------------------------------------")
return lines
def text(self, watermark=False):
""" Generate and print copyable code. """
code = "\n".join(self.get_lines(watermark=watermark))
return code
def debug(scope, *evals, print_out=True):
"""
Easily call eval() on an arbitrary amount of evaluation strings.
Useful for debugging.
Example:
debug(locals(), "value", "value + jumpValue", print_out=True)
debug(locals()) # Prints all objects in scope
:param dict scope: Just write locals()
:param str evals: Variable names with or without operations
:param print_out: Whether to print directly or not
:return: A nicely formatted string
"""
if not evals:
evals = list(scope.keys())
lines = []
n = max([len(string) for string in evals])
for evalStr in evals:
try:
result = eval(evalStr, scope)
except:
result = "ERROR"
lines.append(f"{evalStr:>{n}} = {result}")
lines.append("")
text = "\n".join(lines)
if print_out:
print(text)
return text
def get_origin(obj, include_depth=False):
""" Dig up original obj that might be wrapped or a property. """
depth = 0
while True:
if isinstance(obj, property):
obj = obj.fget
elif hasattr(obj, "__wrapped__"):
obj = obj.__wrapped__
elif hasattr(obj, "__func__"):
obj = obj.__func__
else:
break
depth += 1
if include_depth:
return obj, depth
else:
return obj
# https://stackoverflow.com/questions/26300594/print-code-link-into-pycharms-console
def print_link(file=None, line=None, print_out=True, add_depth=0):
""" Print a link in PyCharm to a line in file.
Defaults to line where this function was called. """
level = inspect.stack()[1 + add_depth]
if file is None:
file = level.filename
if line is None:
line = level.lineno
string = f'File "{file}", line {max(line, 1)}'.replace("\\", "/")
if print_out:
print(string)
return string
def print_link_to_obj(obj, print_out=True):
""" Print a link in PyCharm to a module, function, class, method or property. """
line = get_definition_line(obj=obj)
obj = get_origin(obj=obj)
file = inspect.getfile(obj)
return print_link(file=file, line=line, print_out=print_out)
def get_definition_line(obj):
""" Get line number of an object's definition. """
obj, depth = get_origin(obj=obj, include_depth=True)
return max(inspect.getsourcelines(obj)[1] + depth, 1)
def warn(msg, add_depth=0, print_out=True):
link = print_link(print_out=False, add_depth=1 + add_depth)
full_msg = f"Warning: {msg}\n {link}"
if print_out:
print(full_msg)
return link