kalong 0.4.3__py3-none-any.whl → 0.5.1__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.
kalong/__init__.py CHANGED
@@ -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()
kalong/__main__.py CHANGED
@@ -1,39 +1,4 @@
1
- import os
2
- from pathlib import Path
3
- from subprocess import run
1
+ from kalong import main
4
2
 
5
- from . import config, run_file, shell
6
-
7
- os.environ["PYTHONBREAKPOINT"] = "kalong.breakpoint"
8
- config.from_args()
9
-
10
- if config.server:
11
- from .server import serve
12
-
13
- serve()
14
-
15
- elif config.inject:
16
- kalong_dir = Path(__file__).resolve().parent.parent
17
- gdb_command = ["gdb", "-p", str(config.inject), "-batch"] + [
18
- "-eval-command=call %s" % hook
19
- for hook in [
20
- "(int) PyGILState_Ensure()", # Getting the GIL
21
- '(int) PyRun_SimpleString("'
22
- f"print('* Kalong injection from {os.getpid()} *');"
23
- "import sys;" # Putting kalong project directory in sys path:
24
- f"sys.path.insert(0, '{kalong_dir}');"
25
- "import kalong;" # Setting breakpoint:
26
- "kalong.break_above(2);"
27
- '")',
28
- # Releasing the GIL with the PyGILState_Ensure handle:
29
- "(void) PyGILState_Release($1)",
30
- ]
31
- ]
32
- print(f'Running: {" ".join(gdb_command)}')
33
- run(gdb_command)
34
-
35
- else:
36
- if config.command:
37
- run_file(*config.command)
38
- else:
39
- shell()
3
+ if __name__ == "__main__":
4
+ main()
kalong/communication.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
2
  import json
3
3
  import linecache
4
4
  import logging
5
+ import sys
5
6
  import threading
6
7
 
7
8
  from aiohttp import WSMsgType
@@ -11,26 +12,35 @@ from .debugger import (
11
12
  get_frame,
12
13
  get_title,
13
14
  serialize_answer,
15
+ serialize_answer_recursive,
14
16
  serialize_diff_eval,
17
+ serialize_exception,
15
18
  serialize_frames,
16
19
  serialize_inspect,
17
20
  serialize_inspect_eval,
18
21
  serialize_suggestion,
19
22
  serialize_table,
20
23
  )
24
+ from .errors import SetFrameError
21
25
  from .loops import get_loop
22
26
  from .stepping import add_step, clear_step, stop_trace
23
27
  from .utils import basicConfig, get_file_from_code
24
28
  from .websockets import die, websocket_state
25
- from .errors import SetFrameError
26
29
 
27
30
  log = logging.getLogger(__name__)
28
31
  basicConfig(level=config.log_level)
29
32
 
30
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
+
31
40
  def communicate(frame, event, arg):
32
41
  loop = get_loop()
33
42
 
43
+ loop.set_exception_handler(exception_handler)
34
44
  try:
35
45
  loop.run_until_complete(communication_loop(frame, event, arg))
36
46
  except asyncio.CancelledError:
@@ -42,19 +52,116 @@ async def init(ws, frame, event, arg):
42
52
  tb = arg[2] if event == "exception" else None
43
53
 
44
54
  await ws.send_json({"type": "SET_THEME", "theme": event})
45
- await ws.send_json(
46
- {"type": "SET_TITLE", "title": get_title(frame, event, arg)}
47
- )
55
+ await ws.send_json({"type": "SET_TITLE", "title": get_title(frame, event, arg)})
48
56
  await ws.send_json(
49
57
  {
50
58
  "type": "SET_FRAMES",
51
- "frames": list(serialize_frames(frame, tb))
52
- if event != "shell"
53
- else [],
59
+ "frames": list(serialize_frames(frame, tb)) if event != "shell" else [],
54
60
  }
55
61
  )
56
62
 
57
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
+
58
165
  async def communication_loop(frame_, event_, arg_):
59
166
  frame = frame_
60
167
  event = event_
@@ -77,110 +184,27 @@ async def communication_loop(frame_, event_, arg_):
77
184
  async for msg in ws:
78
185
  if msg.type == WSMsgType.TEXT:
79
186
  data = json.loads(msg.data)
80
- if data["type"] == "HELLO":
81
- await init(ws, frame, event, arg)
82
- response = {
83
- "type": "SET_INFO",
84
- "config": config.__dict__,
85
- "main": threading.current_thread()
86
- is threading.main_thread(),
87
- }
88
-
89
- elif data["type"] == "GET_FILE":
90
- filename = data["filename"]
91
- file = "".join(linecache.getlines(filename))
92
- if not file:
93
- file = get_file_from_code(frame, filename)
94
- response = {
95
- "type": "SET_FILE",
96
- "filename": filename,
97
- "source": file,
98
- }
99
-
100
- elif (
101
- data["type"] == "SET_PROMPT"
102
- or data["type"] == "REFRESH_PROMPT"
103
- ):
104
- try:
105
- eval_fun = (
106
- serialize_inspect_eval
107
- if data.get("command") == "inspect"
108
- else serialize_diff_eval
109
- if data.get("command") == "diff"
110
- else serialize_table
111
- if data.get("command") == "table"
112
- else serialize_answer
113
- )
114
- response = {
115
- "type": "SET_ANSWER",
116
- "key": data["key"],
117
- "command": data.get("command"),
118
- "frame": data.get("frame"),
119
- **eval_fun(
120
- data["prompt"],
121
- get_frame(frame, data.get("frame")),
122
- ),
123
- }
124
- except SetFrameError as e:
125
- frame = e.frame
126
- event = e.event
127
- arg = e.arg
128
-
129
- await init(ws, frame, event, arg)
130
-
131
- response = {
132
- "type": "SET_ANSWER",
133
- "key": data["key"],
134
- "command": data.get("command"),
135
- "frame": data.get("frame"),
136
- "prompt": data["prompt"].strip(),
137
- "answer": "",
138
- "duration": 0,
139
- }
140
-
141
- elif data["type"] == "REQUEST_INSPECT":
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)
142
191
  response = {
143
192
  "type": "SET_ANSWER",
193
+ "prompt": data.get("prompt", "?").strip(),
144
194
  "key": data["key"],
145
195
  "command": data.get("command"),
146
- **serialize_inspect(data["id"]),
196
+ "frame": data.get("frame"),
197
+ "answer": [serialize_exception(*sys.exc_info(), "internal")],
147
198
  }
199
+ log.debug(f"Got {data} answering with {response}")
200
+ response["local"] = True
148
201
 
149
- elif data["type"] == "REQUEST_SUGGESTION":
150
- response = {
151
- "type": "SET_SUGGESTION",
152
- **serialize_suggestion(
153
- data["prompt"],
154
- data["from"],
155
- data["to"],
156
- data["cursor"],
157
- get_frame(frame, data.get("frame")),
158
- ),
159
- }
202
+ if response.pop("recursive", False):
203
+ await ws.send_json({"type": "PAUSE", "recursive": True})
204
+ await init(ws, frame, event, arg)
160
205
 
161
- elif data["type"] == "DO_COMMAND":
162
- command = data["command"]
163
- response = {"type": "ACK", "command": command}
164
- if command == "run":
165
- clear_step()
166
- stop_trace(frame)
167
- elif command == "stop":
168
- clear_step()
169
- stop_trace(frame)
170
- die()
171
- else:
172
- step_frame = get_frame(frame, data.get("frame"))
173
- add_step(command, step_frame)
174
- stop = True
175
-
176
- else:
177
- response = {
178
- "type": "error",
179
- "message": f"Unknown type {data['type']}",
180
- }
206
+ stop = response.pop("stop", False)
181
207
 
182
- log.debug(f"Got {data} answering with {response}")
183
- response["local"] = True
184
208
  try:
185
209
  await ws.send_json(response)
186
210
  except ConnectionResetError:
kalong/config.py CHANGED
@@ -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,
kalong/debugger.py CHANGED
@@ -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
  ]
kalong/forking.py CHANGED
@@ -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 = (