kalong 0.4.3__tar.gz → 0.5.1__tar.gz

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.
Files changed (34) hide show
  1. kalong-0.5.1/.gitignore +16 -0
  2. kalong-0.5.1/PKG-INFO +22 -0
  3. kalong-0.5.1/README.md +0 -0
  4. {kalong-0.4.3 → kalong-0.5.1}/kalong/__init__.py +59 -11
  5. kalong-0.5.1/kalong/__main__.py +4 -0
  6. kalong-0.5.1/kalong/communication.py +222 -0
  7. {kalong-0.4.3 → kalong-0.5.1}/kalong/config.py +7 -5
  8. {kalong-0.4.3 → kalong-0.5.1}/kalong/debugger.py +86 -24
  9. {kalong-0.4.3 → kalong-0.5.1}/kalong/forking.py +1 -3
  10. {kalong-0.4.3 → kalong-0.5.1}/kalong/loops.py +2 -2
  11. {kalong-0.4.3 → kalong-0.5.1}/kalong/server.py +5 -14
  12. kalong-0.5.1/kalong/static/assets/index-BwfqANhu.js +226 -0
  13. {kalong-0.4.3 → kalong-0.5.1}/kalong/static/index.html +3 -4
  14. {kalong-0.4.3 → kalong-0.5.1}/kalong/stepping.py +21 -3
  15. {kalong-0.4.3 → kalong-0.5.1}/kalong/tracing.py +12 -14
  16. {kalong-0.4.3 → kalong-0.5.1}/kalong/utils/__init__.py +12 -3
  17. kalong-0.5.1/kalong/utils/doc_lookup/lookup.json +9861 -0
  18. {kalong-0.4.3 → kalong-0.5.1}/kalong/utils/iterators.py +14 -0
  19. {kalong-0.4.3 → kalong-0.5.1}/kalong/utils/obj.py +6 -16
  20. {kalong-0.4.3 → kalong-0.5.1}/kalong/websockets.py +5 -14
  21. kalong-0.5.1/pyproject.toml +58 -0
  22. kalong-0.4.3/PKG-INFO +0 -27
  23. kalong-0.4.3/kalong/__main__.py +0 -39
  24. kalong-0.4.3/kalong/communication.py +0 -198
  25. kalong-0.4.3/kalong/static/assets/index-666d143f.js +0 -196
  26. kalong-0.4.3/kalong/utils/doc_lookup/lookup.json +0 -1
  27. kalong-0.4.3/pyproject.toml +0 -56
  28. {kalong-0.4.3 → kalong-0.5.1}/LICENSE +0 -0
  29. {kalong-0.4.3 → kalong-0.5.1}/kalong/errors.py +0 -0
  30. /kalong-0.4.3/kalong/static/assets/FiraCode-Bold-38f445df.otf → /kalong-0.5.1/kalong/static/assets/FiraCode-Bold-Ba6ukUQM.otf +0 -0
  31. /kalong-0.4.3/kalong/static/assets/FiraCode-Regular-825cd631.otf → /kalong-0.5.1/kalong/static/assets/FiraCode-Regular-DP3ilQdk.otf +0 -0
  32. /kalong-0.4.3/kalong/static/assets/favicon-208a422a.svg → /kalong-0.5.1/kalong/static/assets/favicon-rdSYpj7B.svg +0 -0
  33. {kalong-0.4.3 → kalong-0.5.1}/kalong/utils/doc_lookup/__init__.py +0 -0
  34. {kalong-0.4.3 → kalong-0.5.1}/kalong/utils/io.py +0 -0
@@ -0,0 +1,16 @@
1
+ .eggs
2
+ .cache/
3
+ __pycache__/
4
+ build
5
+ .env
6
+ .coverage
7
+ .pytest_cache
8
+ pip-wheel-metadata
9
+ examples/unrest-test.db
10
+ htmlcov
11
+ node_modules/
12
+ lib/
13
+ coverage/
14
+ kalong/static/
15
+ yarn-error.log
16
+ dist/
kalong-0.5.1/PKG-INFO ADDED
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.3
2
+ Name: kalong
3
+ Version: 0.5.1
4
+ Summary: A new take on python debugging
5
+ Author-email: Florian Mounier <paradoxxx.zero@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ License-File: LICENSE
8
+ Keywords: debugger
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.8
16
+ Requires-Dist: aiohttp>=3.10.6
17
+ Requires-Dist: cutter>=0.5.0
18
+ Requires-Dist: jedi>=0.19.1
19
+ Provides-Extra: disassembly
20
+ Requires-Dist: uncompyle6>=3.9.2; extra == 'disassembly'
21
+ Provides-Extra: recursive
22
+ Requires-Dist: nest-asyncio>=1.6.0; extra == 'recursive'
kalong-0.5.1/README.md ADDED
File without changes
@@ -1,7 +1,10 @@
1
1
  """A new take on debugging"""
2
- __version__ = "0.4.3"
2
+
3
+ __version__ = "0.5.1"
3
4
  import os
4
5
  import sys
6
+ from pathlib import Path
7
+ from subprocess import run
5
8
 
6
9
  from .config import Config
7
10
 
@@ -31,12 +34,14 @@ def break_above(level):
31
34
  start_trace(frame)
32
35
 
33
36
 
34
- def start_trace(break_=False, full=False):
37
+ def start_trace(break_=False, full=False, skip_frames=None):
35
38
  from .stepping import add_step, start_trace
36
39
 
37
40
  frame = sys._getframe().f_back
38
41
  add_step(
39
- "stepInto" if break_ else ("trace" if full else "continue"), frame
42
+ "stepInto" if break_ else ("trace" if full else "continue"),
43
+ frame,
44
+ skip_frames,
40
45
  )
41
46
  start_trace(frame)
42
47
 
@@ -49,23 +54,23 @@ def stop_trace():
49
54
 
50
55
 
51
56
  class trace:
52
- def __init__(self, break_=False, full=False):
53
- self.break_ = break_
54
- self.full = full
57
+ def __init__(self, **kwargs):
58
+ self.kwargs = kwargs
55
59
 
56
60
  def __enter__(self):
57
- start_trace(self.break_, self.full)
61
+ stop_trace()
62
+ start_trace(**self.kwargs)
58
63
 
59
64
  def __exit__(self, *args):
60
65
  stop_trace()
61
66
 
62
67
 
63
- def run_file(filename, *args):
64
- from .utils import fake_argv
65
-
68
+ def run_file(filename, *args, break_=True):
66
69
  # Cleaning __main__ namespace
67
70
  import __main__
68
71
 
72
+ from .utils import fake_argv
73
+
69
74
  __main__.__dict__.clear()
70
75
  __main__.__dict__.update(
71
76
  {
@@ -82,7 +87,7 @@ def run_file(filename, *args):
82
87
  globals = __main__.__dict__
83
88
  locals = globals
84
89
  with fake_argv(filename, *args):
85
- with trace(break_=True):
90
+ with trace(break_=break_):
86
91
  exec(statement, globals, locals)
87
92
 
88
93
 
@@ -93,3 +98,46 @@ def shell():
93
98
  frame = sys._getframe()
94
99
  # Enter the websocket communication loop that pauses the execution
95
100
  communicate(frame, "shell", [])
101
+
102
+
103
+ def main():
104
+ os.environ["PYTHONBREAKPOINT"] = "kalong.breakpoint"
105
+ config.from_args()
106
+
107
+ if config.server:
108
+ from .server import serve
109
+
110
+ serve()
111
+
112
+ elif config.inject:
113
+ kalong_dir = Path(__file__).resolve().parent.parent
114
+ gdb_command = (
115
+ ["gdb", "-p", str(config.inject), "-batch"]
116
+ + [
117
+ "-eval-command=call %s" % hook
118
+ for hook in [
119
+ "(int) PyGILState_Ensure()", # Getting the GIL
120
+ '(int) PyRun_SimpleString("'
121
+ f"print('* Kalong injection from {os.getpid()} *');"
122
+ "import sys;" # Putting kalong project directory in sys path:
123
+ f"sys.path.insert(0, '{kalong_dir}');"
124
+ "import kalong;" # Setting breakpoint:
125
+ "kalong.break_above(2);"
126
+ '")',
127
+ # Releasing the GIL with the PyGILState_Ensure handle:
128
+ "(void) PyGILState_Release($1)",
129
+ ]
130
+ ]
131
+ )
132
+ print(f'Running: {" ".join(gdb_command)}')
133
+ run(gdb_command)
134
+
135
+ else:
136
+ if config.command:
137
+ run_file(*config.command, break_=config.break_at_start)
138
+ else:
139
+ shell()
140
+
141
+
142
+ if __name__ == "__main__":
143
+ main()
@@ -0,0 +1,4 @@
1
+ from kalong import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,222 @@
1
+ import asyncio
2
+ import json
3
+ import linecache
4
+ import logging
5
+ import sys
6
+ import threading
7
+
8
+ from aiohttp import WSMsgType
9
+
10
+ from . import config
11
+ from .debugger import (
12
+ get_frame,
13
+ get_title,
14
+ serialize_answer,
15
+ serialize_answer_recursive,
16
+ serialize_diff_eval,
17
+ serialize_exception,
18
+ serialize_frames,
19
+ serialize_inspect,
20
+ serialize_inspect_eval,
21
+ serialize_suggestion,
22
+ serialize_table,
23
+ )
24
+ from .errors import SetFrameError
25
+ from .loops import get_loop
26
+ from .stepping import add_step, clear_step, stop_trace
27
+ from .utils import basicConfig, get_file_from_code
28
+ from .websockets import die, websocket_state
29
+
30
+ log = logging.getLogger(__name__)
31
+ basicConfig(level=config.log_level)
32
+
33
+
34
+ def exception_handler(loop, context):
35
+ exception = context["exception"]
36
+ message = context["message"]
37
+ logging.error(f"Task failed, msg={message}, exception={exception}")
38
+
39
+
40
+ def communicate(frame, event, arg):
41
+ loop = get_loop()
42
+
43
+ loop.set_exception_handler(exception_handler)
44
+ try:
45
+ loop.run_until_complete(communication_loop(frame, event, arg))
46
+ except asyncio.CancelledError:
47
+ log.info("Loop got cancelled")
48
+ die()
49
+
50
+
51
+ async def init(ws, frame, event, arg):
52
+ tb = arg[2] if event == "exception" else None
53
+
54
+ await ws.send_json({"type": "SET_THEME", "theme": event})
55
+ await ws.send_json({"type": "SET_TITLE", "title": get_title(frame, event, arg)})
56
+ await ws.send_json(
57
+ {
58
+ "type": "SET_FRAMES",
59
+ "frames": list(serialize_frames(frame, tb)) if event != "shell" else [],
60
+ }
61
+ )
62
+
63
+
64
+ async def handle_message(ws, data, frame, event, arg):
65
+ tb = arg[2] if event == "exception" else None
66
+ if data["type"] == "HELLO":
67
+ await init(ws, frame, event, arg)
68
+ response = {
69
+ "type": "SET_INFO",
70
+ "config": config.__dict__,
71
+ "main": threading.current_thread() is threading.main_thread(),
72
+ }
73
+
74
+ elif data["type"] == "GET_FILE":
75
+ filename = data["filename"]
76
+ file = "".join(linecache.getlines(filename))
77
+ if not file:
78
+ file = get_file_from_code(frame, filename)
79
+ response = {
80
+ "type": "SET_FILE",
81
+ "filename": filename,
82
+ "source": file,
83
+ }
84
+
85
+ elif data["type"] == "SET_PROMPT" or data["type"] == "REFRESH_PROMPT":
86
+ try:
87
+ eval_fun = (
88
+ serialize_inspect_eval
89
+ if data.get("command") == "inspect"
90
+ else serialize_diff_eval
91
+ if data.get("command") == "diff"
92
+ else serialize_table
93
+ if data.get("command") == "table"
94
+ else serialize_answer_recursive
95
+ if data.get("command") == "recursive_debug"
96
+ else serialize_answer
97
+ )
98
+ response = {
99
+ "type": "SET_ANSWER",
100
+ "key": data["key"],
101
+ "command": data.get("command"),
102
+ "frame": data.get("frame"),
103
+ **eval_fun(
104
+ data["prompt"],
105
+ get_frame(frame, data.get("frame"), tb),
106
+ ),
107
+ }
108
+ except SetFrameError as e:
109
+ frame = e.frame
110
+ event = e.event
111
+ arg = e.arg
112
+
113
+ await init(ws, frame, event, arg)
114
+
115
+ response = {
116
+ "type": "SET_ANSWER",
117
+ "key": data["key"],
118
+ "command": data.get("command"),
119
+ "frame": data.get("frame"),
120
+ "prompt": data["prompt"].strip(),
121
+ "answer": "",
122
+ "duration": 0,
123
+ }
124
+
125
+ elif data["type"] == "REQUEST_INSPECT":
126
+ response = {
127
+ "type": "SET_ANSWER",
128
+ "key": data["key"],
129
+ "command": data.get("command"),
130
+ **serialize_inspect(data["id"]),
131
+ }
132
+
133
+ elif data["type"] == "REQUEST_SUGGESTION":
134
+ response = {
135
+ "type": "SET_SUGGESTION",
136
+ **serialize_suggestion(
137
+ data["prompt"],
138
+ data["from"],
139
+ data["to"],
140
+ data["cursor"],
141
+ get_frame(frame, data.get("frame"), tb),
142
+ ),
143
+ }
144
+
145
+ elif data["type"] == "DO_COMMAND":
146
+ command = data["command"]
147
+ response = {"type": "ACK", "command": command}
148
+ if command == "run":
149
+ clear_step()
150
+ stop_trace(frame)
151
+ elif command == "stop":
152
+ clear_step()
153
+ stop_trace(frame)
154
+ die()
155
+ else:
156
+ step_frame = get_frame(frame, data.get("frame"), tb)
157
+ add_step(command, step_frame)
158
+ response["stop"] = True
159
+
160
+ else:
161
+ raise ValueError(f"Unknown type {data['type']}")
162
+ return response
163
+
164
+
165
+ async def communication_loop(frame_, event_, arg_):
166
+ frame = frame_
167
+ event = event_
168
+ arg = arg_
169
+
170
+ ws, existing = await websocket_state()
171
+ try:
172
+ await ws.send_json({"type": "PAUSE"})
173
+ except ConnectionResetError:
174
+ log.info("Connection was reset")
175
+ stop_trace(frame)
176
+ return
177
+
178
+ if existing:
179
+ # If the socket is already opened we need to update client state
180
+ await init(ws, frame, event, arg)
181
+ # Otherwise if it's new, just wait for HELLO to answer current state
182
+
183
+ stop = False
184
+ async for msg in ws:
185
+ if msg.type == WSMsgType.TEXT:
186
+ data = json.loads(msg.data)
187
+ try:
188
+ response = await handle_message(ws, data, frame, event, arg)
189
+ except Exception as e:
190
+ log.error(f"Error handling message {data}", exc_info=e)
191
+ response = {
192
+ "type": "SET_ANSWER",
193
+ "prompt": data.get("prompt", "?").strip(),
194
+ "key": data["key"],
195
+ "command": data.get("command"),
196
+ "frame": data.get("frame"),
197
+ "answer": [serialize_exception(*sys.exc_info(), "internal")],
198
+ }
199
+ log.debug(f"Got {data} answering with {response}")
200
+ response["local"] = True
201
+
202
+ if response.pop("recursive", False):
203
+ await ws.send_json({"type": "PAUSE", "recursive": True})
204
+ await init(ws, frame, event, arg)
205
+
206
+ stop = response.pop("stop", False)
207
+
208
+ try:
209
+ await ws.send_json(response)
210
+ except ConnectionResetError:
211
+ break
212
+
213
+ if stop:
214
+ break
215
+
216
+ elif msg.type == WSMsgType.ERROR:
217
+ log.error("WebSocket closed", exc_info=ws.exception())
218
+ break
219
+
220
+ # Browser exited, stopping debug if we are not stepping
221
+ if not stop:
222
+ stop_trace(frame)
@@ -24,17 +24,14 @@ class Config:
24
24
  parser.add_argument(
25
25
  "--server",
26
26
  action="store_true",
27
- help="Launch the kalong server. "
28
- "This option is used by kalong itself",
27
+ help="Launch the kalong server. " "This option is used by kalong itself",
29
28
  )
30
29
  parser.add_argument(
31
30
  "--protocol",
32
31
  default=self.protocol,
33
32
  help="Protocol for contacting kalong server",
34
33
  )
35
- parser.add_argument(
36
- "--host", default=self.host, help="Host of kalong server"
37
- )
34
+ parser.add_argument("--host", default=self.host, help="Host of kalong server")
38
35
  parser.add_argument(
39
36
  "--port", type=int, default=self.port, help="Port of kalong server"
40
37
  )
@@ -64,6 +61,11 @@ class Config:
64
61
  help="Pid of a running process in which a debugger will be "
65
62
  "injected with gdb. This needs a working gdb and ptrace enabled",
66
63
  )
64
+ parser.add_argument(
65
+ "--break-at-start",
66
+ action="store_true",
67
+ help="Break at the start of the python file",
68
+ )
67
69
  parser.add_argument(
68
70
  "command",
69
71
  nargs=REMAINDER,
@@ -21,9 +21,10 @@ from pprint import pformat
21
21
 
22
22
  from jedi import Interpreter
23
23
 
24
- from .utils import cutter_mock, discompile, universal_travel
24
+ from .errors import SetFrameError
25
+ from .utils import cutter_mock, dedent, discompile, universal_travel
25
26
  from .utils.io import capture_display, capture_exception, capture_std
26
- from .utils.iterators import iter_cause, iter_stack, iter_frame
27
+ from .utils.iterators import force_iterable, iter_cause, iter_stack
27
28
  from .utils.obj import (
28
29
  get_code,
29
30
  get_infos,
@@ -32,7 +33,6 @@ from .utils.obj import (
32
33
  safe_repr,
33
34
  sync_locals,
34
35
  )
35
- from .errors import SetFrameError
36
36
 
37
37
  try:
38
38
  from cutter import cut
@@ -59,14 +59,14 @@ def get_title(frame, event, arg):
59
59
  return "???"
60
60
 
61
61
 
62
- def get_frame(frame, key):
62
+ def get_frame(frame, key, tb=None):
63
63
  if not key:
64
64
  return frame
65
65
 
66
- for f in iter_frame(frame):
66
+ for f, _lno in iter_stack(frame, tb):
67
67
  if id(f) == key:
68
68
  return f
69
- log.warn(f"Frame {key} not found")
69
+ log.warning(f"Frame {key} not found")
70
70
  return frame
71
71
 
72
72
 
@@ -89,7 +89,11 @@ def serialize_frames(current_frame, current_tb):
89
89
  line = linecache.getline(filename, lno, frame.f_globals)
90
90
  line = line and line.strip()
91
91
  startlnos = dis.findlinestarts(code)
92
- lastlineno = list(startlnos)[-1][1]
92
+ lnos = list(startlnos)
93
+ lastlineno = None
94
+ if lnos:
95
+ lastlineno = lnos[-1][1]
96
+
93
97
  yield {
94
98
  "key": id(frame),
95
99
  "absoluteFilename": str(fn),
@@ -103,8 +107,42 @@ def serialize_frames(current_frame, current_tb):
103
107
  }
104
108
 
105
109
 
106
- def serialize_answer(prompt, frame):
107
- prompt = prompt.strip()
110
+ def serialize_answer_recursive(prompt, frame):
111
+ return serialize_answer(prompt, frame, True)
112
+
113
+
114
+ def _rec_exec(code, globals, locals):
115
+ from .stepping import start_trace, steppings, stop_trace
116
+ from .utils import current_origin
117
+
118
+ frame = sys._getframe()
119
+ stop_trace(frame)
120
+
121
+ origin = current_origin()
122
+ steppings[origin] = {
123
+ "type": "stepInto",
124
+ "frame": frame,
125
+ "lno": frame.f_lineno,
126
+ "skip_frames": 1, # Skip exec frame
127
+ "_parent": steppings.get(origin),
128
+ }
129
+
130
+ start_trace(frame)
131
+ try:
132
+ exec(code, globals, locals)
133
+ finally:
134
+ parent = steppings[origin].get("_parent")
135
+ if parent:
136
+ steppings[origin] = parent
137
+ else:
138
+ stop_trace(frame)
139
+
140
+
141
+ def rec_exec(code, globals, locals):
142
+ sys.call_tracing(_rec_exec, (code, globals, locals))
143
+
144
+
145
+ def serialize_answer(prompt, frame, recursive=False):
108
146
  duration = 0
109
147
  answer = []
110
148
  f_globals = dict(frame.f_globals)
@@ -117,16 +155,16 @@ def serialize_answer(prompt, frame):
117
155
  last_key = "_" if "_" not in f_locals else "__"
118
156
  f_locals[last_key] = frame.f_globals["__kalong_last_value__"]
119
157
 
120
- with capture_exception(answer), capture_display(
121
- answer
122
- ) as out, capture_std(answer):
158
+ with capture_exception(answer), capture_display(answer) as out, capture_std(answer):
123
159
  compiled_code = None
124
160
  try:
125
- compiled_code = compile(prompt, "<stdin>", "single")
161
+ compiled_code = compile(prompt.strip(), "<stdin>", "single")
162
+ prompt = prompt.strip()
126
163
  except SetFrameError:
127
164
  raise
128
165
  except Exception:
129
166
  try:
167
+ prompt = dedent(prompt)
130
168
  compiled_code = compile(prompt, "<stdin>", "exec")
131
169
  except SetFrameError:
132
170
  raise
@@ -137,29 +175,36 @@ def serialize_answer(prompt, frame):
137
175
  start = time.time()
138
176
  if compiled_code is not None:
139
177
  try:
140
- exec(compiled_code, f_globals, f_locals)
178
+ (rec_exec if recursive else exec)(compiled_code, f_globals, f_locals)
141
179
  except SetFrameError:
142
180
  raise
143
181
  except Exception:
144
182
  # handle ex
145
183
  sys.excepthook(*sys.exc_info())
146
- del f_locals["__kalong_current_frame__"]
147
- del f_locals["cut"]
148
- if last_key:
184
+ if "__kalong_current_frame__" in f_locals:
185
+ del f_locals["__kalong_current_frame__"]
186
+ if "cut" in f_locals:
187
+ del f_locals["cut"]
188
+ if last_key and last_key in f_locals:
149
189
  del f_locals[last_key]
150
190
  sync_locals(frame, f_locals)
151
- if out.obj:
191
+ if out.obj is not None:
152
192
  frame.f_globals["__kalong_last_value__"] = out.obj
153
193
  duration = int((time.time() - start) * 1000 * 1000 * 1000)
154
194
 
155
- return {"prompt": prompt, "answer": answer, "duration": duration}
195
+ return {
196
+ "prompt": prompt,
197
+ "answer": answer,
198
+ "duration": duration,
199
+ "recursive": recursive,
200
+ }
156
201
 
157
202
 
158
- def serialize_exception(type_, value, tb):
203
+ def serialize_exception(type_, value, tb, subtype="root"):
159
204
  return {
160
205
  "type": "exception",
161
206
  "id": obj_cache.register(value),
162
- "subtype": "root",
207
+ "subtype": subtype,
163
208
  "name": type_.__name__,
164
209
  "description": str(value),
165
210
  "traceback": list(serialize_frames(None, tb)),
@@ -358,8 +403,12 @@ def serialize_suggestion(prompt, from_, to, cursor, frame):
358
403
 
359
404
 
360
405
  def serialize_table(prompt, frame):
361
- valueStr, columns = (s.strip() for s in prompt.split(table_separator))
362
- columns = [c.strip() for c in columns.split(",")]
406
+ if table_separator in prompt:
407
+ valueStr, columns = (s.strip() for s in prompt.split(table_separator))
408
+ columns = [c.strip() for c in columns.split(",")]
409
+ else:
410
+ valueStr = prompt.strip()
411
+ columns = None
363
412
 
364
413
  try:
365
414
  valueKey = get_id_from_expression(valueStr, frame)
@@ -370,6 +419,19 @@ def serialize_table(prompt, frame):
370
419
  }
371
420
 
372
421
  value = obj_cache.get(valueKey)
422
+ values = list(force_iterable(value, True))
423
+ if not columns:
424
+ if all(isinstance(value, dict) for value in values):
425
+ columns = list({column for value in values for column in value.keys()})
426
+ else:
427
+ columns = list(
428
+ {
429
+ column
430
+ for value in values
431
+ for column in dir(value)
432
+ if not column.startswith("__")
433
+ }
434
+ )
373
435
 
374
436
  answer = [
375
437
  {
@@ -386,7 +448,7 @@ def serialize_table(prompt, frame):
386
448
  for column in columns
387
449
  for value in [universal_travel(row, column)]
388
450
  }
389
- for row in value
451
+ for row in values
390
452
  ],
391
453
  }
392
454
  ]
@@ -15,9 +15,7 @@ def forkserver():
15
15
  kalong_dir = Path(__file__).parent.parent
16
16
  env = dict(os.environ)
17
17
  env["PYTHONPATH"] = (
18
- f'{kalong_dir}:{env["PYTHONPATH"]}'
19
- if env.get("PYTHONPATH")
20
- else kalong_dir
18
+ f'{kalong_dir}:{env["PYTHONPATH"]}' if env.get("PYTHONPATH") else kalong_dir
21
19
  )
22
20
 
23
21
  popen_args = (
@@ -22,13 +22,13 @@ def get_loop():
22
22
  origin = current_origin()
23
23
  try:
24
24
  loop = asyncio.get_running_loop()
25
- if origin not in loops or loops[origin] != loop:
25
+ if origin not in loops or loop.is_running():
26
26
  if nest_asyncio:
27
27
  nest_asyncio.apply(loop)
28
28
  else:
29
29
  raise ImportError(
30
30
  "If you want to use kalong in an asyncio environment "
31
- "please install nest_asyncio"
31
+ "or use recursive debugging please install nest_asyncio"
32
32
  )
33
33
  except RuntimeError:
34
34
  pass