1import _ast
2import ast
3import builtins
4import dis
5import functools
6import importlib
7import inspect
8import io
9import os.path
10import pkgutil
11import pydoc
12import queue
13import re
14import site
15import subprocess
16import sys
17import tokenize
18import traceback
19import types
20import warnings
21from collections import namedtuple
22from importlib.machinery import SourceFileLoader, PathFinder
23from typing import Optional, Dict
24
25import __main__
26
27import thonny
28from thonny import jedi_utils
29from thonny.backend import MainBackend, interrupt_local_process, logger
30from thonny.common import (
31    InputSubmission,
32    EOFCommand,
33    ToplevelResponse,
34    CommandToBackend,
35    ToplevelCommand,
36    InlineCommand,
37    UserError,
38    InlineResponse,
39    MessageFromBackend,
40    serialize_message,
41    ValueInfo,
42    path_startswith,
43    get_exe_dirs,
44    ImmediateCommand,
45    get_single_dir_child_data,
46    BackendEvent,
47    DebuggerCommand,
48    execute_system_command,
49    FrameInfo,
50    is_same_path,
51    TextRange,
52    range_contains_smaller_or_equal,
53    OBJECT_LINK_START,
54    OBJECT_LINK_END,
55    range_contains_smaller,
56    DebuggerResponse,
57    get_python_version_string,
58    update_system_path,
59    get_augmented_system_path,
60    try_load_modules_with_frontend_sys_path,
61)
62
63BEFORE_STATEMENT_MARKER = "_thonny_hidden_before_stmt"
64BEFORE_EXPRESSION_MARKER = "_thonny_hidden_before_expr"
65AFTER_STATEMENT_MARKER = "_thonny_hidden_after_stmt"
66AFTER_EXPRESSION_MARKER = "_thonny_hidden_after_expr"
67
68_CO_GENERATOR = getattr(inspect, "CO_GENERATOR", 0)
69_CO_COROUTINE = getattr(inspect, "CO_COROUTINE", 0)
70_CO_ITERABLE_COROUTINE = getattr(inspect, "CO_ITERABLE_COROUTINE", 0)
71_CO_ASYNC_GENERATOR = getattr(inspect, "CO_ASYNC_GENERATOR", 0)
72_CO_WEIRDO = _CO_GENERATOR | _CO_COROUTINE | _CO_ITERABLE_COROUTINE | _CO_ASYNC_GENERATOR
73
74_REPL_HELPER_NAME = "_thonny_repl_print"
75
76_CONFIG_FILENAME = os.path.join(thonny.THONNY_USER_DIR, "backend_configuration.ini")
77
78TempFrameInfo = namedtuple(
79    "TempFrameInfo",
80    [
81        "system_frame",
82        "locals",
83        "globals",
84        "event",
85        "focus",
86        "node_tags",
87        "current_statement",
88        "current_root_expression",
89        "current_evaluations",
90    ],
91)
92
93
94_backend = None
95
96
97class MainCPythonBackend(MainBackend):
98    def __init__(self, target_cwd):
99
100        MainBackend.__init__(self)
101
102        global _backend
103        _backend = self
104
105        self._ini = None
106        self._command_handlers = {}
107        self._object_info_tweakers = []
108        self._import_handlers = {}
109        self._input_queue = queue.Queue()
110        self._source_preprocessors = []
111        self._ast_postprocessors = []
112        self._main_dir = os.path.dirname(sys.modules["thonny"].__file__)
113        self._heap = {}  # WeakValueDictionary would be better, but can't store reference to None
114        self._source_info_by_frame = {}
115        site.sethelper()  # otherwise help function is not available
116        pydoc.pager = pydoc.plainpager  # otherwise help command plays tricks
117        self._install_fake_streams()
118        self._install_repl_helper()
119        self._current_executor = None
120        self._io_level = 0
121        self._tty_mode = True
122        self._tcl = None
123
124        update_system_path(os.environ, get_augmented_system_path(get_exe_dirs()))
125
126        # clean __main__ global scope
127        for key in list(__main__.__dict__.keys()):
128            if not key.startswith("__") or key in {"__file__", "__cached__"}:
129                del __main__.__dict__[key]
130
131        # unset __doc__, then exec dares to write doc of the script there
132        __main__.__doc__ = None
133
134        self._load_plugins()
135
136        # preceding code was run in the directory containing thonny module, now switch to provided
137        try:
138            os.chdir(os.path.expanduser(target_cwd))
139        except OSError:
140            try:
141                os.chdir(os.path.expanduser("~"))
142            except OSError:
143                os.chdir("/")  # yes, this works also in Windows
144
145        # ... and replace current-dir path item
146        # start in shell mode (may be later switched to script mode)
147        # required in shell mode and also required to overwrite thonny location dir
148        sys.path[0] = ""
149        sys.argv[:] = [""]  # empty "script name"
150
151        if os.name == "nt":
152            self._install_signal_handler()
153
154    def _init_command_reader(self):
155        # Can't use threaded reader
156        # https://github.com/thonny/thonny/issues/1363
157        pass
158
159    def _install_signal_handler(self):
160        import signal
161
162        def signal_handler(signal_, frame):
163            raise KeyboardInterrupt("Execution interrupted")
164
165        if os.name == "nt":
166            signal.signal(signal.SIGBREAK, signal_handler)  # pylint: disable=no-member
167        else:
168            signal.signal(signal.SIGINT, signal_handler)
169
170    def _fetch_next_incoming_message(self, timeout=None) -> CommandToBackend:
171        # Reading must be done synchronously
172        # https://github.com/thonny/thonny/issues/1363
173        self._read_one_incoming_message()
174        return self._incoming_message_queue.get()
175
176    def add_command(self, command_name, handler):
177        """Handler should be 1-argument function taking command object.
178
179        Handler may return None (in this case no response is sent to frontend)
180        or a BackendResponse
181        """
182        self._command_handlers[command_name] = handler
183
184    def add_object_info_tweaker(self, tweaker):
185        """Tweaker should be 2-argument function taking value and export record"""
186        self._object_info_tweakers.append(tweaker)
187
188    def add_import_handler(self, module_name, handler):
189        if module_name not in self._import_handlers:
190            self._import_handlers[module_name] = []
191        self._import_handlers[module_name].append(handler)
192
193    def add_source_preprocessor(self, func):
194        self._source_preprocessors.append(func)
195
196    def add_ast_postprocessor(self, func):
197        self._ast_postprocessors.append(func)
198
199    def get_main_module(self):
200        return __main__
201
202    def _read_incoming_msg_line(self) -> str:
203        return self._original_stdin.readline()
204
205    def _handle_user_input(self, msg: InputSubmission) -> None:
206        self._input_queue.put(msg)
207
208    def _handle_eof_command(self, msg: EOFCommand) -> None:
209        self.send_message(ToplevelResponse(SystemExit=True))
210        sys.exit()
211
212    def _handle_normal_command(self, cmd: CommandToBackend) -> None:
213        assert isinstance(cmd, (ToplevelCommand, InlineCommand))
214
215        if isinstance(cmd, ToplevelCommand):
216            self._source_info_by_frame = {}
217            self._input_queue = queue.Queue()
218
219        if cmd.name in self._command_handlers:
220            handler = self._command_handlers[cmd.name]
221        else:
222            handler = getattr(self, "_cmd_" + cmd.name, None)
223
224        if handler is None:
225            response = {"error": "Unknown command: " + cmd.name}
226        else:
227            try:
228                response = handler(cmd)
229            except SystemExit as e:
230                # Must be caused by Thonny or plugins code
231                if isinstance(cmd, ToplevelCommand):
232                    logger.exception("Unexpected SystemExit", exc_info=e)
233                response = {"SystemExit": True}
234            except UserError as e:
235                sys.stderr.write(str(e) + "\n")
236                response = {}
237            except KeyboardInterrupt:
238                response = {"user_exception": self._prepare_user_exception()}
239            except Exception as e:
240                self._report_internal_exception(e)
241                response = {"context_info": "other unhandled exception"}
242
243        if response is False:
244            # Command doesn't want to send any response
245            return
246
247        real_response = self._prepare_command_response(response, cmd)
248
249        if isinstance(real_response, ToplevelResponse):
250            real_response["gui_is_active"] = (
251                self._get_tcl() is not None or self._get_qt_app() is not None
252            )
253
254        self.send_message(real_response)
255
256    def _handle_immediate_command(self, cmd: ImmediateCommand) -> None:
257        if cmd.name == "interrupt":
258            with self._interrupt_lock:
259                interrupt_local_process()
260
261    def _should_keep_going(self) -> bool:
262        return True
263
264    def get_option(self, name, default=None):
265        section, subname = self._parse_option_name(name)
266        val = self._get_ini().get(section, subname, fallback=default)
267        try:
268            return ast.literal_eval(val)
269        except Exception:
270            return val
271
272    def set_option(self, name, value):
273        ini = self._get_ini()
274        section, subname = self._parse_option_name(name)
275        if not ini.has_section(section):
276            ini.add_section(section)
277        if not isinstance(value, str):
278            value = repr(value)
279        ini.set(section, subname, value)
280        self.save_settings()
281
282    def _parse_option_name(self, name):
283        if "." in name:
284            return name.split(".", 1)
285        else:
286            return "general", name
287
288    def _get_ini(self):
289        if self._ini is None:
290            import configparser
291
292            self._ini = configparser.ConfigParser(interpolation=None)
293            self._ini.read(_CONFIG_FILENAME)
294
295        return self._ini
296
297    def save_settings(self):
298        if self._ini is None:
299            return
300
301        with open(_CONFIG_FILENAME, "w") as fp:
302            self._ini.write(fp)
303
304    def switch_env_to_script_mode(self, cmd):
305        if "" in sys.path:
306            sys.path.remove("")  # current directory
307
308        filename = cmd.args[0]
309        if os.path.isfile(filename):
310            sys.path.insert(0, os.path.abspath(os.path.dirname(filename)))
311            __main__.__dict__["__file__"] = filename
312
313    def _custom_import(self, *args, **kw):
314        module = self._original_import(*args, **kw)
315
316        if not hasattr(module, "__name__"):
317            return module
318
319        # module specific handlers
320        for handler in self._import_handlers.get(module.__name__, []):
321            try:
322                handler(module)
323            except Exception as e:
324                self._report_internal_exception(e)
325
326        # general handlers
327        for handler in self._import_handlers.get("*", []):
328            try:
329                handler(module)
330            except Exception as e:
331                self._report_internal_exception(e)
332
333        return module
334
335    def _load_plugins(self):
336        # built-in plugins
337        try:
338            import thonny.plugins.backend  # pylint: disable=redefined-outer-name
339        except ImportError:
340            # May happen eg. in ssh session
341            return
342
343        try_load_modules_with_frontend_sys_path("thonnycontrib")
344        self._load_plugins_from_path(thonny.plugins.backend.__path__, "thonny.plugins.backend.")
345
346        # 3rd party plugins from namespace package
347        try:
348            import thonnycontrib.backend  # @UnresolvedImport
349        except ImportError:
350            # No 3rd party plugins installed
351            pass
352        else:
353            self._load_plugins_from_path(thonnycontrib.backend.__path__, "thonnycontrib.backend.")
354
355    def _load_plugins_from_path(self, path, prefix):
356        load_function_name = "load_plugin"
357        for _, module_name, _ in sorted(pkgutil.iter_modules(path, prefix), key=lambda x: x[1]):
358            try:
359                m = importlib.import_module(module_name)
360                if hasattr(m, load_function_name):
361                    f = getattr(m, load_function_name)
362                    sig = inspect.signature(f)
363                    if len(sig.parameters) == 0:
364                        f()
365                    else:
366                        f(self)
367            except Exception as e:
368                logger.exception("Failed loading plugin '" + module_name + "'", exc_info=e)
369
370    def _cmd_get_environment_info(self, cmd):
371        return ToplevelResponse(
372            main_dir=self._main_dir,
373            sys_path=sys.path,
374            usersitepackages=site.getusersitepackages() if site.ENABLE_USER_SITE else None,
375            prefix=sys.prefix,
376            welcome_text="Python " + get_python_version_string(),
377            executable=sys.executable,
378            exe_dirs=get_exe_dirs(),
379            in_venv=(
380                hasattr(sys, "base_prefix")
381                and sys.base_prefix != sys.prefix
382                or hasattr(sys, "real_prefix")
383                and getattr(sys, "real_prefix") != sys.prefix
384            ),
385            python_version=get_python_version_string(),
386            cwd=os.getcwd(),
387        )
388
389    def _cmd_cd(self, cmd):
390        if len(cmd.args) == 1:
391            path = cmd.args[0]
392            try:
393                os.chdir(path)
394                return ToplevelResponse()
395            except FileNotFoundError:
396                raise UserError("No such folder: " + path)
397            except OSError as e:
398                raise UserError("\n".join(traceback.format_exception_only(type(e), e)))
399        else:
400            raise UserError("cd takes one parameter")
401
402    def _cmd_Run(self, cmd):
403        self.switch_env_to_script_mode(cmd)
404        return self._execute_file(cmd, SimpleRunner)
405
406    def _cmd_run(self, cmd):
407        return self._execute_file(cmd, SimpleRunner)
408
409    def _cmd_FastDebug(self, cmd):
410        self.switch_env_to_script_mode(cmd)
411        return self._execute_file(cmd, FastTracer)
412
413    def _cmd_Debug(self, cmd):
414        self.switch_env_to_script_mode(cmd)
415        return self._execute_file(cmd, NiceTracer)
416
417    def _cmd_debug(self, cmd):
418        return self._execute_file(cmd, NiceTracer)
419
420    def _cmd_execute_source(self, cmd):
421        """Executes Python source entered into shell"""
422        self._check_update_tty_mode(cmd)
423        filename = "<pyshell>"
424        source = cmd.source.strip()
425
426        try:
427            root = ast.parse(source, filename=filename, mode="exec")
428        except SyntaxError as e:
429            error = "".join(traceback.format_exception_only(type(e), e))
430            sys.stderr.write(error)
431            return ToplevelResponse()
432
433        assert isinstance(root, ast.Module)
434
435        result_attributes = self._execute_source(
436            source,
437            filename,
438            "repl",
439            NiceTracer if getattr(cmd, "debug_mode", False) else SimpleRunner,
440            cmd,
441        )
442
443        return ToplevelResponse(command_name="execute_source", **result_attributes)
444
445    def _cmd_execute_system_command(self, cmd):
446        self._check_update_tty_mode(cmd)
447        try:
448            returncode = execute_system_command(cmd, disconnect_stdin=True)
449            return {"returncode": returncode}
450        except Exception as e:
451            logger.exception("Could not execute system command %s", cmd, exc_info=e)
452
453    def _cmd_process_gui_events(self, cmd):
454        # advance the event loop
455        try:
456            # First try Tkinter.
457            # Need to update even when tkinter._default_root is None
458            # because otherwise destroyed window will stay up in macOS.
459
460            # When switching between closed user Tk window and another window,
461            # the closed window may reappear in IDLE and CLI REPL
462            tcl = self._get_tcl()
463            if tcl is not None:
464                # http://bugs.python.org/issue989712
465                # http://bugs.python.org/file6090/run.py.diff
466                # https://bugs.python.org/review/989712/diff/4528/Lib/idlelib/run.py
467                tcl.eval("update")
468                return {}
469            else:
470                # Try Qt only when Tkinter is not used
471                app = self._get_qt_app()
472                if app is not None:
473                    app.processEvents()
474                    return {}
475
476        except Exception:
477            pass
478
479        return {"gui_is_active": False}
480
481    def _cmd_get_globals(self, cmd):
482        # warnings.warn("_cmd_get_globals is deprecated for CPython")
483        try:
484            return InlineResponse(
485                "get_globals",
486                module_name=cmd.module_name,
487                globals=self.export_globals(cmd.module_name),
488            )
489        except Exception as e:
490            return InlineResponse("get_globals", module_name=cmd.module_name, error=str(e))
491
492    def _cmd_get_frame_info(self, cmd):
493        atts = {}
494        try:
495            # TODO: make it work also in past states
496            frame, location = self._lookup_frame_by_id(cmd["frame_id"])
497            if frame is None:
498                atts["error"] = "Frame not found"
499            else:
500                atts["code_name"] = frame.f_code.co_name
501                atts["module_name"] = frame.f_globals["__name__"]
502                atts["locals"] = (
503                    None
504                    if frame.f_locals is frame.f_globals
505                    else self.export_variables(frame.f_locals)
506                )
507                atts["globals"] = self.export_variables(frame.f_globals)
508                atts["freevars"] = frame.f_code.co_freevars
509                atts["location"] = location
510        except Exception as e:
511            atts["error"] = str(e)
512
513        return InlineResponse("get_frame_info", frame_id=cmd.frame_id, **atts)
514
515    def _cmd_get_active_distributions(self, cmd):
516        try:
517            # if it is called after first installation to user site packages
518            # this dir is not yet in sys.path
519            if (
520                site.ENABLE_USER_SITE
521                and site.getusersitepackages()
522                and os.path.exists(site.getusersitepackages())
523                and site.getusersitepackages() not in sys.path
524            ):
525                # insert before first site packages item
526                for i, item in enumerate(sys.path):
527                    if "site-packages" in item or "dist-packages" in item:
528                        sys.path.insert(i, site.getusersitepackages())
529                        break
530                else:
531                    sys.path.append(site.getusersitepackages())
532
533            import pkg_resources
534
535            pkg_resources._initialize_master_working_set()
536            dists = {
537                dist.key: {
538                    "project_name": dist.project_name,
539                    "key": dist.key,
540                    "location": dist.location,
541                    "version": dist.version,
542                }
543                for dist in pkg_resources.working_set  # pylint: disable=not-an-iterable
544            }
545
546            return InlineResponse(
547                "get_active_distributions",
548                distributions=dists,
549                usersitepackages=site.getusersitepackages() if site.ENABLE_USER_SITE else None,
550            )
551        except Exception:
552            return InlineResponse("get_active_distributions", error=traceback.format_exc())
553
554    def _cmd_get_locals(self, cmd):
555        for frame in inspect.stack():
556            if id(frame) == cmd.frame_id:
557                return InlineResponse("get_locals", locals=self.export_variables(frame.f_locals))
558
559        raise RuntimeError("Frame '{0}' not found".format(cmd.frame_id))
560
561    def _cmd_get_heap(self, cmd):
562        result = {}
563        for key in self._heap:
564            result[key] = self.export_value(self._heap[key])
565
566        return InlineResponse("get_heap", heap=result)
567
568    def _cmd_shell_autocomplete(self, cmd):
569        try_load_modules_with_frontend_sys_path(["jedi", "parso"])
570
571        error = None
572        try:
573            import jedi
574        except ImportError:
575            jedi = None
576            completions = []
577            error = "Could not import jedi"
578        else:
579            try:
580                with warnings.catch_warnings():
581                    jedi_completions = jedi_utils.get_interpreter_completions(
582                        cmd.source, [__main__.__dict__]
583                    )
584                    completions = self._export_completions(jedi_completions)
585            except Exception as e:
586                completions = []
587                logger.info("Autocomplete error", exc_info=e)
588                error = "Autocomplete error: " + str(e)
589
590        return InlineResponse(
591            "shell_autocomplete", source=cmd.source, completions=completions, error=error
592        )
593
594    def _cmd_editor_autocomplete(self, cmd):
595        try_load_modules_with_frontend_sys_path(["jedi", "parso"])
596
597        error = None
598        try:
599            import jedi
600
601            self._debug(jedi.__file__, sys.path)
602            with warnings.catch_warnings():
603                jedi_completions = jedi_utils.get_script_completions(
604                    cmd.source, cmd.row, cmd.column, cmd.filename
605                )
606                completions = self._export_completions(jedi_completions)
607
608        except ImportError:
609            jedi = None
610            completions = []
611            error = "Could not import jedi"
612        except Exception as e:
613            completions = []
614            logger.info("Autocomplete error", exc_info=e)
615            error = "Autocomplete error: " + str(e)
616
617        return InlineResponse(
618            "editor_autocomplete",
619            source=cmd.source,
620            row=cmd.row,
621            column=cmd.column,
622            filename=cmd.filename,
623            completions=completions,
624            error=error,
625        )
626
627    def _cmd_get_object_info(self, cmd):
628        if isinstance(self._current_executor, NiceTracer) and self._current_executor.is_in_past():
629            info = {"id": cmd.object_id, "error": "past info not available"}
630
631        elif cmd.object_id in self._heap:
632            value = self._heap[cmd.object_id]
633            attributes = {}
634            if cmd.include_attributes:
635                for name in dir(value):
636                    if not name.startswith("__") or cmd.all_attributes:
637                        # attributes[name] = inspect.getattr_static(value, name)
638                        try:
639                            attributes[name] = getattr(value, name)
640                        except Exception:
641                            pass
642
643            self._heap[id(type(value))] = type(value)
644            info = {
645                "id": cmd.object_id,
646                "repr": repr(value),
647                "type": str(type(value)),
648                "full_type_name": str(type(value))
649                .replace("<class '", "")
650                .replace("'>", "")
651                .strip(),
652                "attributes": self.export_variables(attributes),
653            }
654
655            if isinstance(value, io.TextIOWrapper):
656                self._add_file_handler_info(value, info)
657            elif isinstance(
658                value,
659                (
660                    types.BuiltinFunctionType,
661                    types.BuiltinMethodType,
662                    types.FunctionType,
663                    types.LambdaType,
664                    types.MethodType,
665                ),
666            ):
667                self._add_function_info(value, info)
668            elif isinstance(value, (list, tuple, set)):
669                self._add_elements_info(value, info)
670            elif isinstance(value, dict):
671                self._add_entries_info(value, info)
672            elif isinstance(value, float):
673                self._add_float_info(value, info)
674            elif hasattr(value, "image_data"):
675                info["image_data"] = value.image_data
676
677            for tweaker in self._object_info_tweakers:
678                try:
679                    tweaker(value, info, cmd)
680                except Exception:
681                    logger.exception("Failed object info tweaker: " + str(tweaker))
682
683        else:
684            info = {"id": cmd.object_id, "error": "object info not available"}
685
686        return InlineResponse("get_object_info", id=cmd.object_id, info=info)
687
688    def _cmd_mkdir(self, cmd):
689        os.mkdir(cmd.path)
690
691    def _cmd_delete(self, cmd):
692        for path in cmd.paths:
693            try:
694                if os.path.isfile(path):
695                    os.remove(path)
696                elif os.path.isdir(path):
697                    import shutil
698
699                    shutil.rmtree(path)
700            except Exception as e:
701                print("Could not delete %s: %s" % (path, str(e)), file=sys.stderr)
702
703    def _get_sep(self) -> str:
704        return os.path.sep
705
706    def _get_dir_children_info(
707        self, path: str, include_hidden: bool = False
708    ) -> Optional[Dict[str, Dict]]:
709        return get_single_dir_child_data(path, include_hidden)
710
711    def _get_path_info(self, path: str) -> Optional[Dict]:
712
713        try:
714            if not os.path.exists(path):
715                return None
716        except OSError:
717            pass
718
719        try:
720            kind = "dir" if os.path.isdir(path) else "file"
721            return {
722                "path": path,
723                "kind": kind,
724                "size": None if kind == "dir" else os.path.getsize(path),
725                "modified": os.path.getmtime(path),
726                "error": None,
727            }
728        except OSError as e:
729            return {
730                "path": path,
731                "kind": None,
732                "size": None,
733                "modified": None,
734                "error": str(e),
735            }
736
737    def _export_completions(self, jedi_completions):
738        result = []
739        for c in jedi_completions:
740            if not c.name.startswith("__"):
741                record = {
742                    "name": c.name,
743                    "complete": c.complete,
744                    "type": c.type,
745                    "description": c.description,
746                }
747                """ TODO:
748                try:
749                    if c.type in ["class", "module", "function"]:
750                        if c.type == "function":
751                            record["docstring"] = c.docstring()
752                        else:
753                            record["docstring"] = c.description + "\n" + c.docstring()
754                except Exception:
755                    pass
756                """
757                result.append(record)
758        return result
759
760    def _get_tcl(self):
761        if self._tcl is not None:
762            return self._tcl
763
764        tkinter = sys.modules.get("tkinter")
765        if tkinter is None:
766            return None
767
768        if self._tcl is None:
769            try:
770                self._tcl = tkinter.Tcl()
771            except Exception as e:
772                logger.error("Could not get Tcl", exc_info=e)
773                self._tcl = None
774                return None
775
776        return self._tcl
777
778    def _get_qt_app(self):
779        mod = sys.modules.get("PyQt5.QtCore")
780        if mod is None:
781            mod = sys.modules.get("PyQt4.QtCore")
782        if mod is None:
783            mod = sys.modules.get("PySide.QtCore")
784        if mod is None:
785            return None
786
787        app_class = getattr(mod, "QCoreApplication", None)
788        if app_class is not None:
789            try:
790                return app_class.instance()
791            except Exception:
792                return None
793        else:
794            return None
795
796    def _add_file_handler_info(self, value, info):
797        try:
798            assert isinstance(value.name, str)
799            assert value.mode in ("r", "rt", "tr", "br", "rb")
800            assert value.errors in ("strict", None)
801            assert value.newlines is None or value.tell() > 0
802            # TODO: cache the content
803            # TODO: don't read too big files
804            with open(value.name, encoding=value.encoding) as f:
805                info["file_encoding"] = f.encoding
806                info["file_content"] = f.read()
807                info["file_tell"] = value.tell()
808        except Exception as e:
809            info["file_error"] = "Could not get file content, error:" + str(e)
810
811    def _add_function_info(self, value, info):
812        try:
813            info["source"] = inspect.getsource(value)
814        except Exception:
815            pass
816
817    def _add_elements_info(self, value, info):
818        info["elements"] = []
819        for element in value:
820            info["elements"].append(self.export_value(element))
821
822    def _add_entries_info(self, value, info):
823        info["entries"] = []
824        for key in value:
825            info["entries"].append((self.export_value(key), self.export_value(value[key])))
826
827    def _add_float_info(self, value, info):
828        if not value.is_integer():
829            info["as_integer_ratio"] = value.as_integer_ratio()
830
831    def _execute_file(self, cmd, executor_class):
832        self._check_update_tty_mode(cmd)
833
834        if len(cmd.args) >= 1:
835            sys.argv = cmd.args
836            filename = cmd.args[0]
837            if filename == "-c" or os.path.isabs(filename):
838                full_filename = filename
839            else:
840                full_filename = os.path.abspath(filename)
841
842            if full_filename == "-c":
843                source = cmd.source
844            else:
845                with tokenize.open(full_filename) as fp:
846                    source = fp.read()
847
848            for preproc in self._source_preprocessors:
849                source = preproc(source, cmd)
850
851            result_attributes = self._execute_source(
852                source, full_filename, "exec", executor_class, cmd, self._ast_postprocessors
853            )
854            result_attributes["filename"] = full_filename
855            return ToplevelResponse(command_name=cmd.name, **result_attributes)
856        else:
857            raise UserError("Command '%s' takes at least one argument" % cmd.name)
858
859    def _execute_source(
860        self, source, filename, execution_mode, executor_class, cmd, ast_postprocessors=[]
861    ):
862        self._current_executor = executor_class(self, cmd)
863
864        try:
865            return self._current_executor.execute_source(
866                source, filename, execution_mode, ast_postprocessors
867            )
868        finally:
869            self._current_executor = None
870
871    def _install_repl_helper(self):
872        def _handle_repl_value(obj):
873            if obj is not None:
874                try:
875                    obj_repr = repr(obj)
876                    if len(obj_repr) > 5000:
877                        obj_repr = obj_repr[:5000] + "…"
878                except Exception as e:
879                    obj_repr = "<repr error: " + str(e) + ">"
880                print(OBJECT_LINK_START % id(obj), obj_repr, OBJECT_LINK_END, sep="")
881                self._heap[id(obj)] = obj
882                builtins._ = obj
883
884        setattr(builtins, _REPL_HELPER_NAME, _handle_repl_value)
885
886    def _install_fake_streams(self):
887        self._original_stdin = sys.stdin
888        self._original_stdout = sys.stdout
889        self._original_stderr = sys.stderr
890
891        # yes, both out and err will be directed to out (but with different tags)
892        # this allows client to see the order of interleaving writes to stdout/stderr
893        sys.stdin = FakeInputStream(self, sys.stdin)
894        sys.stdout = FakeOutputStream(self, sys.stdout, "stdout")
895        sys.stderr = FakeOutputStream(self, sys.stdout, "stderr")
896
897        # fake it properly: replace also "backup" streams
898        sys.__stdin__ = sys.stdin
899        sys.__stdout__ = sys.stdout
900        sys.__stderr__ = sys.stderr
901
902    def _install_custom_import(self):
903        self._original_import = builtins.__import__
904        builtins.__import__ = self._custom_import
905
906    def _restore_original_import(self):
907        builtins.__import__ = self._original_import
908
909    def send_message(self, msg: MessageFromBackend) -> None:
910        sys.stdout.flush()
911
912        if isinstance(msg, ToplevelResponse):
913            if "cwd" not in msg:
914                msg["cwd"] = os.getcwd()
915            if "globals" not in msg:
916                msg["globals"] = self.export_globals()
917
918        self._original_stdout.write(serialize_message(msg) + "\n")
919        self._original_stdout.flush()
920
921    def export_value(self, value, max_repr_length=5000):
922        self._heap[id(value)] = value
923        try:
924            rep = repr(value)
925        except Exception:
926            # See https://bitbucket.org/plas/thonny/issues/584/problem-with-thonnys-back-end-obj-no
927            rep = "??? <repr error>"
928
929        if len(rep) > max_repr_length:
930            rep = rep[:max_repr_length] + "…"
931
932        return ValueInfo(id(value), rep)
933
934    def export_variables(self, variables):
935        result = {}
936        with warnings.catch_warnings():
937            warnings.simplefilter("ignore")
938            for name in variables:
939                if not name.startswith("__"):
940                    result[name] = self.export_value(variables[name], 100)
941
942        return result
943
944    def export_globals(self, module_name="__main__"):
945        if module_name in sys.modules:
946            return self.export_variables(sys.modules[module_name].__dict__)
947        else:
948            raise RuntimeError("Module '{0}' is not loaded".format(module_name))
949
950    def _debug(self, *args):
951        logger.debug("MainCPythonBackend: " + str(args))
952
953    def _enter_io_function(self):
954        self._io_level += 1
955
956    def _exit_io_function(self):
957        self._io_level -= 1
958
959    def is_doing_io(self):
960        return self._io_level > 0
961
962    def _export_stack(self, newest_frame, relevance_checker=None):
963        result = []
964
965        system_frame = newest_frame
966
967        while system_frame is not None:
968            module_name = system_frame.f_globals["__name__"]
969            code_name = system_frame.f_code.co_name
970
971            if not relevance_checker or relevance_checker(system_frame):
972                source, firstlineno, in_library = self._get_frame_source_info(system_frame)
973
974                result.insert(
975                    0,
976                    FrameInfo(
977                        # TODO: can this id be reused by a later frame?
978                        # Need to store the reference to avoid GC?
979                        # I guess it is not required, as id will be required
980                        # only for stacktrace inspection, and sys.last_exception
981                        # will have the reference anyway
982                        # (NiceTracer has its own reference keeping)
983                        id=id(system_frame),
984                        filename=system_frame.f_code.co_filename,
985                        module_name=module_name,
986                        code_name=code_name,
987                        locals=self.export_variables(system_frame.f_locals),
988                        globals=self.export_variables(system_frame.f_globals),
989                        freevars=system_frame.f_code.co_freevars,
990                        source=source,
991                        lineno=system_frame.f_lineno,
992                        firstlineno=firstlineno,
993                        in_library=in_library,
994                        event="line",
995                        focus=TextRange(system_frame.f_lineno, 0, system_frame.f_lineno + 1, 0),
996                        node_tags=None,
997                        current_statement=None,
998                        current_evaluations=None,
999                        current_root_expression=None,
1000                    ),
1001                )
1002
1003            if module_name == "__main__" and code_name == "<module>":
1004                # this was last frame relevant to the user
1005                break
1006
1007            system_frame = system_frame.f_back
1008
1009        assert result  # not empty
1010        return result
1011
1012    def _lookup_frame_by_id(self, frame_id):
1013        def lookup_from_stack(frame):
1014            if frame is None:
1015                return None
1016            elif id(frame) == frame_id:
1017                return frame
1018            else:
1019                return lookup_from_stack(frame.f_back)
1020
1021        def lookup_from_tb(entry):
1022            if entry is None:
1023                return None
1024            elif id(entry.tb_frame) == frame_id:
1025                return entry.tb_frame
1026            else:
1027                return lookup_from_tb(entry.tb_next)
1028
1029        result = lookup_from_stack(inspect.currentframe())
1030        if result is not None:
1031            return result, "stack"
1032
1033        if getattr(sys, "last_traceback"):
1034            result = lookup_from_tb(getattr(sys, "last_traceback"))
1035            if result:
1036                return result, "last_traceback"
1037
1038        _, _, tb = sys.exc_info()
1039        return lookup_from_tb(tb), "current_exception"
1040
1041    def _get_frame_source_info(self, frame):
1042        fid = id(frame)
1043        if fid not in self._source_info_by_frame:
1044            self._source_info_by_frame[fid] = _fetch_frame_source_info(frame)
1045
1046        return self._source_info_by_frame[fid]
1047
1048    def _prepare_user_exception(self):
1049        e_type, e_value, e_traceback = sys.exc_info()
1050        sys.last_type, sys.last_value, sys.last_traceback = (e_type, e_value, e_traceback)
1051
1052        processed_tb = traceback.extract_tb(e_traceback)
1053
1054        tb = e_traceback
1055        while tb.tb_next is not None:
1056            tb = tb.tb_next
1057        last_frame = tb.tb_frame
1058
1059        if e_type is SyntaxError:
1060            # Don't show ast frame
1061            while last_frame.f_code.co_filename and last_frame.f_code.co_filename == ast.__file__:
1062                last_frame = last_frame.f_back
1063
1064        if e_type is SyntaxError:
1065            msg = (
1066                traceback.format_exception_only(e_type, e_value)[-1]
1067                .replace(e_type.__name__ + ":", "")
1068                .strip()
1069            )
1070        else:
1071            msg = str(e_value)
1072
1073        return {
1074            "type_name": e_type.__name__,
1075            "message": msg,
1076            "stack": self._export_stack(last_frame),
1077            "items": format_exception_with_frame_info(e_type, e_value, e_traceback),
1078            "filename": getattr(e_value, "filename", processed_tb[-1].filename),
1079            "lineno": getattr(e_value, "lineno", processed_tb[-1].lineno),
1080            "col_offset": getattr(e_value, "offset", None),
1081            "line": getattr(e_value, "text", processed_tb[-1].line),
1082        }
1083
1084    def _check_update_tty_mode(self, cmd):
1085        if "tty_mode" in cmd:
1086            self._tty_mode = cmd["tty_mode"]
1087
1088
1089class FakeStream:
1090    def __init__(self, backend: MainCPythonBackend, target_stream):
1091        self._backend = backend
1092        self._target_stream = target_stream
1093        self._processed_symbol_count = 0
1094
1095    def isatty(self):
1096        return self._backend._tty_mode and (os.name != "nt" or "click" not in sys.modules)
1097
1098    def __getattr__(self, name):
1099        # TODO: is it safe to perform those other functions without notifying backend
1100        # via _enter_io_function?
1101        return getattr(self._target_stream, name)
1102
1103
1104class FakeOutputStream(FakeStream):
1105    def __init__(self, backend: MainCPythonBackend, target_stream, stream_name):
1106        FakeStream.__init__(self, backend, target_stream)
1107        self._stream_name = stream_name
1108
1109    def write(self, data):
1110        try:
1111            self._backend._enter_io_function()
1112            # click may send bytes instead of strings
1113            if isinstance(data, bytes):
1114                data = data.decode(errors="replace")
1115
1116            if data != "":
1117                self._backend._send_output(data=data, stream_name=self._stream_name)
1118                self._processed_symbol_count += len(data)
1119        finally:
1120            self._backend._exit_io_function()
1121
1122    def writelines(self, lines):
1123        try:
1124            self._backend._enter_io_function()
1125            self.write("".join(lines))
1126        finally:
1127            self._backend._exit_io_function()
1128
1129
1130class FakeInputStream(FakeStream):
1131    def __init__(self, backend: MainCPythonBackend, target_stream):
1132        super().__init__(backend, target_stream)
1133        self._buffer = ""
1134        self._eof = False
1135
1136    def _generic_read(self, method, original_limit):
1137        if original_limit is None:
1138            effective_limit = -1
1139        elif method == "readlines" and original_limit > -1:
1140            # NB! size hint is defined in weird way
1141            # "no more lines will be read if the total size (in bytes/characters)
1142            # of all lines so far **exceeds** the hint".
1143            effective_limit = original_limit + 1
1144        else:
1145            effective_limit = original_limit
1146
1147        try:
1148            self._backend._enter_io_function()
1149            while True:
1150                if effective_limit == 0:
1151                    result = ""
1152                    break
1153
1154                elif effective_limit > 0 and len(self._buffer) >= effective_limit:
1155                    result = self._buffer[:effective_limit]
1156                    self._buffer = self._buffer[effective_limit:]
1157                    if method == "readlines" and not result.endswith("\n") and "\n" in self._buffer:
1158                        # limit is just a hint
1159                        # https://docs.python.org/3/library/io.html#io.IOBase.readlines
1160                        extra = self._buffer[: self._buffer.find("\n") + 1]
1161                        result += extra
1162                        self._buffer = self._buffer[len(extra) :]
1163                    break
1164
1165                elif method == "readline" and "\n" in self._buffer:
1166                    pos = self._buffer.find("\n") + 1
1167                    result = self._buffer[:pos]
1168                    self._buffer = self._buffer[pos:]
1169                    break
1170
1171                elif self._eof:
1172                    result = self._buffer
1173                    self._buffer = ""
1174                    self._eof = False  # That's how official implementation does
1175                    break
1176
1177                else:
1178                    self._backend.send_message(
1179                        BackendEvent("InputRequest", method=method, limit=original_limit)
1180                    )
1181                    msg = self._backend._fetch_next_incoming_message()
1182                    if isinstance(msg, InputSubmission):
1183                        self._buffer += msg.data
1184                        self._processed_symbol_count += len(msg.data)
1185                    elif isinstance(msg, EOFCommand):
1186                        self._eof = True
1187                    elif isinstance(msg, InlineCommand):
1188                        self._backend._handle_normal_command(msg)
1189                    else:
1190                        raise RuntimeError(
1191                            "Wrong type of command (%r) when waiting for input" % (msg,)
1192                        )
1193
1194            return result
1195
1196        finally:
1197            self._backend._exit_io_function()
1198
1199    def read(self, limit=-1):
1200        return self._generic_read("read", limit)
1201
1202    def readline(self, limit=-1):
1203        return self._generic_read("readline", limit)
1204
1205    def readlines(self, limit=-1):
1206        return self._generic_read("readlines", limit).splitlines(True)
1207
1208    def __next__(self):
1209        result = self.readline()
1210        if not result:
1211            raise StopIteration
1212
1213        return result
1214
1215    def __iter__(self):
1216        return self
1217
1218
1219def prepare_hooks(method):
1220    @functools.wraps(method)
1221    def wrapper(self, *args, **kwargs):
1222        try:
1223            sys.meta_path.insert(0, self)
1224            self._backend._install_custom_import()
1225            return method(self, *args, **kwargs)
1226        finally:
1227            del sys.meta_path[0]
1228            if hasattr(self._backend, "_original_import"):
1229                self._backend._restore_original_import()
1230
1231    return wrapper
1232
1233
1234def return_execution_result(method):
1235    @functools.wraps(method)
1236    def wrapper(self, *args, **kwargs):
1237        try:
1238            result = method(self, *args, **kwargs)
1239            if result is not None:
1240                return result
1241            return {"context_info": "after normal execution"}
1242        except Exception:
1243            return {"user_exception": self._backend._prepare_user_exception()}
1244
1245    return wrapper
1246
1247
1248class Executor:
1249    def __init__(self, backend: MainCPythonBackend, original_cmd):
1250        self._backend = backend
1251        self._original_cmd = original_cmd
1252        self._main_module_path = None
1253
1254    def execute_source(self, source, filename, mode, ast_postprocessors):
1255        if isinstance(source, str):
1256            # TODO: simplify this or make sure encoding is correct
1257            source = source.encode("utf-8")
1258
1259        if os.path.exists(filename):
1260            self._main_module_path = filename
1261
1262        global_vars = __main__.__dict__
1263
1264        try:
1265            if mode == "repl":
1266                assert not ast_postprocessors
1267                # Useful in shell to get last expression value in multi-statement block
1268                root = self._prepare_ast(source, filename, "exec")
1269                # https://bugs.python.org/issue35894
1270                # https://github.com/pallets/werkzeug/pull/1552/files#diff-9e75ca133f8601f3b194e2877d36df0eR950
1271                module = ast.parse("")
1272                module.body = root.body
1273                self._instrument_repl_code(module)
1274                statements = compile(module, filename, "exec")
1275            elif mode == "exec":
1276                root = self._prepare_ast(source, filename, mode)
1277                for func in ast_postprocessors:
1278                    func(root)
1279                statements = compile(root, filename, mode)
1280            else:
1281                raise ValueError("Unknown mode", mode)
1282
1283            return self._execute_prepared_user_code(statements, global_vars)
1284        except SyntaxError:
1285            return {"user_exception": self._backend._prepare_user_exception()}
1286        except SystemExit:
1287            return {"SystemExit": True}
1288        except Exception as e:
1289            self._backend._report_internal_exception(e)
1290            return {}
1291
1292    @return_execution_result
1293    @prepare_hooks
1294    def _execute_prepared_user_code(self, statements, global_vars):
1295        exec(statements, global_vars)
1296
1297    def find_spec(self, fullname, path=None, target=None):
1298        """override in subclass for custom-loading user modules"""
1299        return None
1300
1301    def _prepare_ast(self, source, filename, mode):
1302        return ast.parse(source, filename, mode)
1303
1304    def _instrument_repl_code(self, root):
1305        # modify all expression statements to print and register their non-None values
1306        for node in ast.walk(root):
1307            if (
1308                isinstance(node, ast.FunctionDef)
1309                or hasattr(ast, "AsyncFunctionDef")
1310                and isinstance(node, ast.AsyncFunctionDef)
1311            ):
1312                first_stmt = node.body[0]
1313                if isinstance(first_stmt, ast.Expr) and isinstance(first_stmt.value, ast.Str):
1314                    first_stmt.contains_docstring = True
1315            if isinstance(node, ast.Expr) and not getattr(node, "contains_docstring", False):
1316                node.value = ast.Call(
1317                    func=ast.Name(id=_REPL_HELPER_NAME, ctx=ast.Load()),
1318                    args=[node.value],
1319                    keywords=[],
1320                )
1321                ast.fix_missing_locations(node)
1322
1323
1324class SimpleRunner(Executor):
1325    pass
1326
1327
1328class Tracer(Executor):
1329    def __init__(self, backend, original_cmd):
1330        super().__init__(backend, original_cmd)
1331        self._thonny_src_dir = os.path.dirname(sys.modules["thonny"].__file__)
1332        self._fresh_exception = None
1333        self._prev_breakpoints = {}
1334        self._last_reported_frame_ids = set()
1335        self._affected_frame_ids_per_exc_id = {}
1336        self._canonic_path_cache = {}
1337        self._file_interest_cache = {}
1338        self._file_breakpoints_cache = {}
1339        self._command_completion_handler = None
1340
1341        # first (automatic) stepping command depends on whether any breakpoints were set or not
1342        breakpoints = self._original_cmd.breakpoints
1343        assert isinstance(breakpoints, dict)
1344        if breakpoints:
1345            command_name = "resume"
1346        else:
1347            command_name = "step_into"
1348
1349        self._current_command = DebuggerCommand(
1350            command_name,
1351            state=None,
1352            focus=None,
1353            frame_id=None,
1354            exception=None,
1355            breakpoints=breakpoints,
1356        )
1357
1358        self._initialize_new_command(None)
1359
1360    def _get_canonic_path(self, path):
1361        # adapted from bdb
1362        result = self._canonic_path_cache.get(path)
1363        if result is None:
1364            if path.startswith("<"):
1365                result = path
1366            else:
1367                result = os.path.normcase(os.path.abspath(path))
1368
1369            self._canonic_path_cache[path] = result
1370
1371        return result
1372
1373    def _trace(self, frame, event, arg):
1374        raise NotImplementedError()
1375
1376    def _execute_prepared_user_code(self, statements, global_vars):
1377        try:
1378            sys.settrace(self._trace)
1379            if hasattr(sys, "breakpointhook"):
1380                old_breakpointhook = sys.breakpointhook
1381                sys.breakpointhook = self._breakpointhook
1382
1383            return super()._execute_prepared_user_code(statements, global_vars)
1384        finally:
1385            sys.settrace(None)
1386            if hasattr(sys, "breakpointhook"):
1387                sys.breakpointhook = old_breakpointhook
1388
1389    def _is_interesting_frame(self, frame):
1390        code = frame.f_code
1391
1392        return not (
1393            code is None
1394            or code.co_filename is None
1395            or not self._is_interesting_module_file(code.co_filename)
1396            or code.co_flags & _CO_GENERATOR
1397            and code.co_flags & _CO_COROUTINE
1398            and code.co_flags & _CO_ITERABLE_COROUTINE
1399            and code.co_flags & _CO_ASYNC_GENERATOR
1400            # or "importlib._bootstrap" in code.co_filename
1401            or code.co_name in ["<listcomp>", "<setcomp>", "<dictcomp>"]
1402        )
1403
1404    def _is_interesting_module_file(self, path):
1405        # interesting files are the files in the same directory as main module
1406        # or the ones with breakpoints
1407        # When command is "resume", then only modules with breakpoints are interesting
1408        # (used to be more flexible, but this caused problems
1409        # when main script was in ~/. Then user site library became interesting as well)
1410
1411        result = self._file_interest_cache.get(path, None)
1412        if result is not None:
1413            return result
1414
1415        _, extension = os.path.splitext(path.lower())
1416
1417        result = (
1418            self._get_breakpoints_in_file(path)
1419            or self._main_module_path is not None
1420            and is_same_path(path, self._main_module_path)
1421            or extension in (".py", ".pyw")
1422            and (
1423                self._current_command.get("allow_stepping_into_libraries", False)
1424                or (
1425                    path_startswith(path, os.path.dirname(self._main_module_path))
1426                    # main module may be at the root of the fs
1427                    and not path_startswith(path, sys.prefix)
1428                    and not path_startswith(path, sys.base_prefix)
1429                    and not path_startswith(path, site.getusersitepackages() or "usersitenotexists")
1430                )
1431            )
1432            and not path_startswith(path, self._thonny_src_dir)
1433        )
1434
1435        self._file_interest_cache[path] = result
1436
1437        return result
1438
1439    def _is_interesting_exception(self, frame, arg):
1440        return arg[0] not in (StopIteration, StopAsyncIteration)
1441
1442    def _fetch_next_debugger_command(self, current_frame):
1443        while True:
1444            cmd = self._backend._fetch_next_incoming_message()
1445            if isinstance(cmd, InlineCommand):
1446                self._backend._handle_normal_command(cmd)
1447            else:
1448                assert isinstance(cmd, DebuggerCommand)
1449                self._prev_breakpoints = self._current_command.breakpoints
1450                self._current_command = cmd
1451                self._initialize_new_command(current_frame)
1452                return
1453
1454    def _initialize_new_command(self, current_frame):
1455        self._command_completion_handler = getattr(
1456            self, "_cmd_%s_completed" % self._current_command.name
1457        )
1458
1459        if self._current_command.breakpoints != self._prev_breakpoints:
1460            self._file_interest_cache = {}  # because there may be new breakpoints
1461            self._file_breakpoints_cache = {}
1462            for path, linenos in self._current_command.breakpoints.items():
1463                self._file_breakpoints_cache[path] = linenos
1464                self._file_breakpoints_cache[self._get_canonic_path(path)] = linenos
1465
1466    def _register_affected_frame(self, exception_obj, frame):
1467        # I used to store the frame ids in a new field inside exception object,
1468        # but Python 3.8 doesn't allow this (https://github.com/thonny/thonny/issues/1403)
1469        exc_id = id(exception_obj)
1470        if exc_id not in self._affected_frame_ids_per_exc_id:
1471            self._affected_frame_ids_per_exc_id[exc_id] = set()
1472        self._affected_frame_ids_per_exc_id[exc_id].add(id(frame))
1473
1474    def _get_breakpoints_in_file(self, filename):
1475        result = self._file_breakpoints_cache.get(filename, None)
1476
1477        if result is not None:
1478            return result
1479
1480        canonic_path = self._get_canonic_path(filename)
1481        result = self._file_breakpoints_cache.get(canonic_path, set())
1482        self._file_breakpoints_cache[filename] = result
1483        return result
1484
1485    def _get_current_exception(self):
1486        if self._fresh_exception is not None:
1487            return self._fresh_exception
1488        else:
1489            return sys.exc_info()
1490
1491    def _export_exception_info(self):
1492        exc = self._get_current_exception()
1493
1494        if exc[0] is None:
1495            return {
1496                "id": None,
1497                "msg": None,
1498                "type_name": None,
1499                "lines_with_frame_info": None,
1500                "affected_frame_ids": set(),
1501                "is_fresh": False,
1502            }
1503        else:
1504            return {
1505                "id": id(exc[1]),
1506                "msg": str(exc[1]),
1507                "type_name": exc[0].__name__,
1508                "lines_with_frame_info": format_exception_with_frame_info(*exc),
1509                "affected_frame_ids": self._affected_frame_ids_per_exc_id.get(id(exc[1]), set()),
1510                "is_fresh": exc == self._fresh_exception,
1511            }
1512
1513    def _breakpointhook(self, *args, **kw):
1514        pass
1515
1516    def _check_notify_return(self, frame_id):
1517        if frame_id in self._last_reported_frame_ids:
1518            # Need extra notification, because it may be long time until next interesting event
1519            self._backend.send_message(InlineResponse("debugger_return", frame_id=frame_id))
1520
1521    def _check_store_main_frame_id(self, frame):
1522        # initial command doesn't have a frame id
1523        if self._current_command.frame_id is None and self._get_canonic_path(
1524            frame.f_code.co_filename
1525        ) == self._get_canonic_path(self._main_module_path):
1526            self._current_command.frame_id = id(frame)
1527
1528
1529class FastTracer(Tracer):
1530    def __init__(self, backend, original_cmd):
1531        super().__init__(backend, original_cmd)
1532
1533        self._command_frame_returned = False
1534        self._code_linenos_cache = {}
1535        self._code_breakpoints_cache = {}
1536
1537    def _initialize_new_command(self, current_frame):
1538        super()._initialize_new_command(current_frame)
1539        self._command_frame_returned = False
1540        if self._current_command.breakpoints != self._prev_breakpoints:
1541            self._code_breakpoints_cache = {}
1542
1543            # restore tracing for active frames which were skipped before
1544            # but have breakpoints now
1545            frame = current_frame
1546            while frame is not None:
1547                if (
1548                    frame.f_trace is None
1549                    and frame.f_code is not None
1550                    and self._get_breakpoints_in_code(frame.f_code)
1551                ):
1552                    frame.f_trace = self._trace
1553
1554                frame = frame.f_back
1555
1556    def _breakpointhook(self, *args, **kw):
1557        frame = inspect.currentframe()
1558        while not self._is_interesting_frame(frame):
1559            frame = frame.f_back
1560        self._report_current_state(frame)
1561        self._fetch_next_debugger_command(frame)
1562
1563    def _should_skip_frame(self, frame, event):
1564        if event == "call":
1565            # new frames
1566            return (
1567                (
1568                    self._current_command.name == "resume"
1569                    and not self._get_breakpoints_in_code(frame.f_code)
1570                    or self._current_command.name == "step_over"
1571                    and not self._get_breakpoints_in_code(frame.f_code)
1572                    and id(frame) not in self._last_reported_frame_ids
1573                    or self._current_command.name == "step_out"
1574                    and not self._get_breakpoints_in_code(frame.f_code)
1575                )
1576                or not self._is_interesting_frame(frame)
1577                or self._backend.is_doing_io()
1578            )
1579
1580        else:
1581            # once we have entered a frame, we need to reach the return event
1582            return False
1583
1584    def _trace(self, frame, event, arg):
1585        if self._should_skip_frame(frame, event):
1586            return None
1587
1588        # return None
1589        # return self._trace
1590
1591        frame_id = id(frame)
1592
1593        if event == "call":
1594            self._check_store_main_frame_id(frame)
1595
1596            self._fresh_exception = None
1597            # can we skip this frame?
1598            if self._current_command.name == "step_over" and not self._current_command.breakpoints:
1599                return None
1600
1601        elif event == "return":
1602            self._fresh_exception = None
1603            if frame_id == self._current_command["frame_id"]:
1604                self._command_frame_returned = True
1605            self._check_notify_return(frame_id)
1606
1607        elif event == "exception":
1608            if self._is_interesting_exception(frame, arg):
1609                self._fresh_exception = arg
1610                self._register_affected_frame(arg[1], frame)
1611                # UI doesn't know about separate exception events
1612                self._report_current_state(frame)
1613                self._fetch_next_debugger_command(frame)
1614
1615        elif event == "line":
1616            self._fresh_exception = None
1617
1618            if self._command_completion_handler(frame):
1619                self._report_current_state(frame)
1620                self._fetch_next_debugger_command(frame)
1621
1622        else:
1623            self._fresh_exception = None
1624
1625        return self._trace
1626
1627    def _report_current_state(self, frame):
1628        stack = self._backend._export_stack(frame, self._is_interesting_frame)
1629        msg = DebuggerResponse(
1630            stack=stack,
1631            in_present=True,
1632            io_symbol_count=None,
1633            exception_info=self._export_exception_info(),
1634            tracer_class="FastTracer",
1635        )
1636
1637        self._last_reported_frame_ids = set(map(lambda f: f.id, stack))
1638
1639        self._backend.send_message(msg)
1640
1641    def _cmd_step_into_completed(self, frame):
1642        return True
1643
1644    def _cmd_step_over_completed(self, frame):
1645        return (
1646            id(frame) == self._current_command.frame_id
1647            or self._command_frame_returned
1648            or self._at_a_breakpoint(frame)
1649        )
1650
1651    def _cmd_step_out_completed(self, frame):
1652        return self._command_frame_returned or self._at_a_breakpoint(frame)
1653
1654    def _cmd_resume_completed(self, frame):
1655        return self._at_a_breakpoint(frame)
1656
1657    def _get_breakpoints_in_code(self, f_code):
1658
1659        bps_in_file = self._get_breakpoints_in_file(f_code.co_filename)
1660
1661        code_id = id(f_code)
1662        result = self._code_breakpoints_cache.get(code_id, None)
1663
1664        if result is None:
1665            if not bps_in_file:
1666                result = set()
1667            else:
1668                co_linenos = self._code_linenos_cache.get(code_id, None)
1669                if co_linenos is None:
1670                    co_linenos = {pair[1] for pair in dis.findlinestarts(f_code)}
1671                    self._code_linenos_cache[code_id] = co_linenos
1672
1673                result = bps_in_file.intersection(co_linenos)
1674
1675            self._code_breakpoints_cache[code_id] = result
1676
1677        return result
1678
1679    def _at_a_breakpoint(self, frame):
1680        # TODO: try re-entering same line in loop
1681        return frame.f_lineno in self._get_breakpoints_in_code(frame.f_code)
1682
1683    def _is_interesting_exception(self, frame, arg):
1684        return super()._is_interesting_exception(frame, arg) and (
1685            self._current_command.name in ["step_into", "step_over"]
1686            and (
1687                # in command frame or its parent frames
1688                id(frame) == self._current_command["frame_id"]
1689                or self._command_frame_returned
1690            )
1691        )
1692
1693
1694class NiceTracer(Tracer):
1695    def __init__(self, backend, original_cmd):
1696        super().__init__(backend, original_cmd)
1697        self._instrumented_files = set()
1698        self._install_marker_functions()
1699        self._custom_stack = []
1700        self._saved_states = []
1701        self._current_state_index = 0
1702
1703        from collections import Counter
1704
1705        self._fulltags = Counter()
1706        self._nodes = {}
1707
1708    def _breakpointhook(self, *args, **kw):
1709        self._report_state(len(self._saved_states) - 1)
1710        self._fetch_next_debugger_command(None)
1711
1712    def _install_marker_functions(self):
1713        # Make dummy marker functions universally available by putting them
1714        # into builtin scope
1715        self.marker_function_names = {
1716            BEFORE_STATEMENT_MARKER,
1717            AFTER_STATEMENT_MARKER,
1718            BEFORE_EXPRESSION_MARKER,
1719            AFTER_EXPRESSION_MARKER,
1720        }
1721
1722        for name in self.marker_function_names:
1723            if not hasattr(builtins, name):
1724                setattr(builtins, name, getattr(self, name))
1725
1726    def _prepare_ast(self, source, filename, mode):
1727        # ast_utils need to be imported after asttokens
1728        # is (custom-)imported
1729        try_load_modules_with_frontend_sys_path(["asttokens", "six", "astroid"])
1730        from thonny import ast_utils
1731
1732        root = ast.parse(source, filename, mode)
1733
1734        ast_utils.mark_text_ranges(root, source)
1735        self._tag_nodes(root)
1736        self._insert_expression_markers(root)
1737        self._insert_statement_markers(root)
1738        self._insert_for_target_markers(root)
1739        self._instrumented_files.add(filename)
1740
1741        return root
1742
1743    def _should_skip_frame(self, frame, event):
1744        # nice tracer can't skip any of the frames which need to be
1745        # shown in the stacktrace
1746        code = frame.f_code
1747        if code is None:
1748            return True
1749
1750        if event == "call":
1751            # new frames
1752            if code.co_name in self.marker_function_names:
1753                return False
1754
1755            else:
1756                return not self._is_interesting_frame(frame) or self._backend.is_doing_io()
1757
1758        else:
1759            # once we have entered a frame, we need to reach the return event
1760            return False
1761
1762    def _is_interesting_frame(self, frame):
1763        return (
1764            frame.f_code.co_filename in self._instrumented_files
1765            and super()._is_interesting_frame(frame)
1766        )
1767
1768    def find_spec(self, fullname, path=None, target=None):
1769        spec = PathFinder.find_spec(fullname, path, target)
1770
1771        if (
1772            spec is not None
1773            and isinstance(spec.loader, SourceFileLoader)
1774            and getattr(spec, "origin", None)
1775            and self._is_interesting_module_file(spec.origin)
1776        ):
1777            spec.loader = FancySourceFileLoader(fullname, spec.origin, self)
1778            return spec
1779        else:
1780            return super().find_spec(fullname, path, target)
1781
1782    def is_in_past(self):
1783        return self._current_state_index < len(self._saved_states) - 1
1784
1785    def _trace(self, frame, event, arg):
1786        try:
1787            return self._trace_and_catch(frame, event, arg)
1788        except BaseException as e:
1789            logger.exception("Exception in _trace", exc_info=e)
1790            sys.settrace(None)
1791            return None
1792
1793    def _trace_and_catch(self, frame, event, arg):
1794        """
1795        1) Detects marker calls and responds to client queries in these spots
1796        2) Maintains a customized view of stack
1797        """
1798        # frame skipping test should be done both in new frames and old ones (because of Resume)
1799        # Note that intermediate frames can't be skipped when jumping to a breakpoint
1800        # because of the need to maintain custom stack
1801        if self._should_skip_frame(frame, event):
1802            return None
1803
1804        code_name = frame.f_code.co_name
1805
1806        if event == "call":
1807            self._fresh_exception = (
1808                None  # some code is running, therefore exception is not fresh anymore
1809            )
1810
1811            if code_name in self.marker_function_names:
1812                self._check_store_main_frame_id(frame.f_back)
1813
1814                # the main thing
1815                if code_name == BEFORE_STATEMENT_MARKER:
1816                    event = "before_statement"
1817                elif code_name == AFTER_STATEMENT_MARKER:
1818                    event = "after_statement"
1819                elif code_name == BEFORE_EXPRESSION_MARKER:
1820                    event = "before_expression"
1821                elif code_name == AFTER_EXPRESSION_MARKER:
1822                    event = "after_expression"
1823                else:
1824                    raise AssertionError("Unknown marker function")
1825
1826                marker_function_args = frame.f_locals.copy()
1827                node = self._nodes[marker_function_args["node_id"]]
1828
1829                del marker_function_args["self"]
1830
1831                if "call_function" not in node.tags:
1832                    self._handle_progress_event(frame.f_back, event, marker_function_args, node)
1833                self._try_interpret_as_again_event(frame.f_back, event, marker_function_args, node)
1834
1835                # Don't need any more events from these frames
1836                return None
1837
1838            else:
1839                # Calls to proper functions.
1840                # Client doesn't care about these events,
1841                # it cares about "before_statement" events in the first statement of the body
1842                self._custom_stack.append(CustomStackFrame(frame, "call"))
1843
1844        elif event == "exception":
1845            # Note that Nicer can't filter out exception based on current command
1846            # because it must be possible to go back and replay with different command
1847            if self._is_interesting_exception(frame, arg):
1848                self._fresh_exception = arg
1849                self._register_affected_frame(arg[1], frame)
1850
1851                # Last command (step_into or step_over) produced this exception
1852                # Show red after-state for this focus
1853                # use the state prepared by previous event
1854                last_custom_frame = self._custom_stack[-1]
1855                assert last_custom_frame.system_frame == frame
1856
1857                # TODO: instead of producing an event here, next before_-event
1858                # should create matching after event for each before event
1859                # which would remain unclosed because of this exception.
1860                # Existence of these after events would simplify step_over management
1861
1862                assert last_custom_frame.event.startswith("before_")
1863                pseudo_event = last_custom_frame.event.replace("before_", "after_").replace(
1864                    "_again", ""
1865                )
1866                # print("handle", pseudo_event, {}, last_custom_frame.node)
1867                self._handle_progress_event(frame, pseudo_event, {}, last_custom_frame.node)
1868
1869        elif event == "return":
1870            self._fresh_exception = None
1871
1872            if code_name not in self.marker_function_names:
1873                frame_id = id(self._custom_stack[-1].system_frame)
1874                self._check_notify_return(frame_id)
1875                self._custom_stack.pop()
1876                if len(self._custom_stack) == 0:
1877                    # We popped last frame, this means our program has ended.
1878                    # There may be more events coming from upper (system) frames
1879                    # but we're not interested in those
1880                    sys.settrace(None)
1881            else:
1882                pass
1883
1884        else:
1885            self._fresh_exception = None
1886
1887        return self._trace
1888
1889    def _handle_progress_event(self, frame, event, args, node):
1890        self._save_current_state(frame, event, args, node)
1891        self._respond_to_commands()
1892
1893    def _save_current_state(self, frame, event, args, node):
1894        """
1895        Updates custom stack and stores the state
1896
1897        self._custom_stack always keeps last info,
1898        which gets exported as FrameInfos to _saved_states["stack"]
1899        """
1900        focus = TextRange(node.lineno, node.col_offset, node.end_lineno, node.end_col_offset)
1901
1902        custom_frame = self._custom_stack[-1]
1903        custom_frame.event = event
1904        custom_frame.focus = focus
1905        custom_frame.node = node
1906        custom_frame.node_tags = node.tags
1907
1908        if self._saved_states:
1909            prev_state = self._saved_states[-1]
1910            prev_state_frame = self._create_actual_active_frame(prev_state)
1911        else:
1912            prev_state = None
1913            prev_state_frame = None
1914
1915        # store information about current statement / expression
1916        if "statement" in event:
1917            custom_frame.current_statement = focus
1918
1919            if event == "before_statement_again":
1920                # keep the expression information from last event
1921                pass
1922            else:
1923                custom_frame.current_root_expression = None
1924                custom_frame.current_evaluations = []
1925        else:
1926            assert "expression" in event
1927            assert prev_state_frame is not None
1928
1929            # may need to update current_statement, because the parent statement was
1930            # not the last one visited (eg. with test expression of a loop,
1931            # starting from 2nd iteration)
1932            if hasattr(node, "parent_statement_focus"):
1933                custom_frame.current_statement = node.parent_statement_focus
1934
1935            # see whether current_root_expression needs to be updated
1936            prev_root_expression = prev_state_frame.current_root_expression
1937            if event == "before_expression" and (
1938                id(frame) != id(prev_state_frame.system_frame)
1939                or "statement" in prev_state_frame.event
1940                or prev_root_expression
1941                and not range_contains_smaller_or_equal(prev_root_expression, focus)
1942            ):
1943                custom_frame.current_root_expression = focus
1944                custom_frame.current_evaluations = []
1945
1946            if event == "after_expression" and "value" in args:
1947                # value is missing in case of exception
1948                custom_frame.current_evaluations.append(
1949                    (focus, self._backend.export_value(args["value"]))
1950                )
1951
1952        # Save the snapshot.
1953        # Check if we can share something with previous state
1954        if (
1955            prev_state is not None
1956            and id(prev_state_frame.system_frame) == id(frame)
1957            and prev_state["exception_value"] is self._get_current_exception()[1]
1958            and prev_state["fresh_exception_id"] == id(self._fresh_exception)
1959            and ("before" in event or "skipexport" in node.tags)
1960        ):
1961
1962            exception_info = prev_state["exception_info"]
1963            # share the stack ...
1964            stack = prev_state["stack"]
1965            # ... but override certain things
1966            active_frame_overrides = {
1967                "event": custom_frame.event,
1968                "focus": custom_frame.focus,
1969                "node_tags": custom_frame.node_tags,
1970                "current_root_expression": custom_frame.current_root_expression,
1971                "current_evaluations": custom_frame.current_evaluations.copy(),
1972                "current_statement": custom_frame.current_statement,
1973            }
1974        else:
1975            # make full export
1976            stack = self._export_stack()
1977            exception_info = self._export_exception_info()
1978            active_frame_overrides = {}
1979
1980        msg = {
1981            "stack": stack,
1982            "active_frame_overrides": active_frame_overrides,
1983            "in_client_log": False,
1984            "io_symbol_count": (
1985                sys.stdin._processed_symbol_count
1986                + sys.stdout._processed_symbol_count
1987                + sys.stderr._processed_symbol_count
1988            ),
1989            "exception_value": self._get_current_exception()[1],
1990            "fresh_exception_id": id(self._fresh_exception),
1991            "exception_info": exception_info,
1992        }
1993
1994        self._saved_states.append(msg)
1995
1996    def _respond_to_commands(self):
1997        """Tries to respond to client commands with states collected so far.
1998        Returns if these states don't suffice anymore and Python needs
1999        to advance the program"""
2000
2001        # while the state for current index is already saved:
2002        while self._current_state_index < len(self._saved_states):
2003            state = self._saved_states[self._current_state_index]
2004
2005            # Get current state's most recent frame (together with overrides
2006            frame = self._create_actual_active_frame(state)
2007
2008            # Is this state meant to be seen?
2009            if "skip_" + frame.event not in frame.node_tags:
2010                # if True:
2011                # Has the command completed?
2012                tester = getattr(self, "_cmd_" + self._current_command.name + "_completed")
2013                cmd_complete = tester(frame, self._current_command)
2014
2015                if cmd_complete:
2016                    state["in_client_log"] = True
2017                    self._report_state(self._current_state_index)
2018                    self._fetch_next_debugger_command(frame)
2019
2020            if self._current_command.name == "step_back":
2021                if self._current_state_index == 0:
2022                    # Already in first state. Remain in this loop
2023                    pass
2024                else:
2025                    assert self._current_state_index > 0
2026                    # Current event is no longer present in GUI "undo log"
2027                    self._saved_states[self._current_state_index]["in_client_log"] = False
2028                    self._current_state_index -= 1
2029            else:
2030                # Other commands move the pointer forward
2031                self._current_state_index += 1
2032
2033    def _create_actual_active_frame(self, state):
2034        return state["stack"][-1]._replace(**state["active_frame_overrides"])
2035
2036    def _report_state(self, state_index):
2037        in_present = state_index == len(self._saved_states) - 1
2038        if in_present:
2039            # For reported new events re-export stack to make sure it is not shared.
2040            # (There is tiny chance that sharing previous state
2041            # after executing BinOp, Attribute, Compare or Subscript
2042            # was not the right choice. See tag_nodes for more.)
2043            # Re-exporting reduces the harm by showing correct data at least
2044            # for present states.
2045            self._saved_states[state_index]["stack"] = self._export_stack()
2046
2047        # need to make a copy for applying overrides
2048        # and removing helper fields without modifying original
2049        state = self._saved_states[state_index].copy()
2050        state["stack"] = state["stack"].copy()
2051
2052        state["in_present"] = in_present
2053        if not in_present:
2054            # for past states fix the newest frame
2055            state["stack"][-1] = self._create_actual_active_frame(state)
2056
2057        del state["exception_value"]
2058        del state["active_frame_overrides"]
2059
2060        # Convert stack of TempFrameInfos to stack of FrameInfos
2061        new_stack = []
2062        self._last_reported_frame_ids = set()
2063        for tframe in state["stack"]:
2064            system_frame = tframe.system_frame
2065            module_name = system_frame.f_globals["__name__"]
2066            code_name = system_frame.f_code.co_name
2067
2068            source, firstlineno, in_library = self._backend._get_frame_source_info(system_frame)
2069
2070            assert firstlineno is not None, "nofir " + str(system_frame)
2071            frame_id = id(system_frame)
2072            new_stack.append(
2073                FrameInfo(
2074                    id=frame_id,
2075                    filename=system_frame.f_code.co_filename,
2076                    module_name=module_name,
2077                    code_name=code_name,
2078                    locals=tframe.locals,
2079                    globals=tframe.globals,
2080                    freevars=system_frame.f_code.co_freevars,
2081                    source=source,
2082                    lineno=system_frame.f_lineno,
2083                    firstlineno=firstlineno,
2084                    in_library=in_library,
2085                    event=tframe.event,
2086                    focus=tframe.focus,
2087                    node_tags=tframe.node_tags,
2088                    current_statement=tframe.current_statement,
2089                    current_evaluations=tframe.current_evaluations,
2090                    current_root_expression=tframe.current_root_expression,
2091                )
2092            )
2093
2094            self._last_reported_frame_ids.add(frame_id)
2095
2096        state["stack"] = new_stack
2097        state["tracer_class"] = "NiceTracer"
2098
2099        self._backend.send_message(DebuggerResponse(**state))
2100
2101    def _try_interpret_as_again_event(self, frame, original_event, original_args, original_node):
2102        """
2103        Some after_* events can be interpreted also as
2104        "before_*_again" events (eg. when last argument of a call was
2105        evaluated, then we are just before executing the final stage of the call)
2106        """
2107
2108        if original_event == "after_expression":
2109            value = original_args.get("value")
2110
2111            if (
2112                "last_child" in original_node.tags
2113                or "or_arg" in original_node.tags
2114                and value
2115                or "and_arg" in original_node.tags
2116                and not value
2117            ):
2118
2119                # there may be explicit exceptions
2120                if (
2121                    "skip_after_statement_again" in original_node.parent_node.tags
2122                    or "skip_after_expression_again" in original_node.parent_node.tags
2123                ):
2124                    return
2125
2126                # next step will be finalizing evaluation of parent of current expr
2127                # so let's say we're before that parent expression
2128                again_args = {"node_id": id(original_node.parent_node)}
2129                again_event = (
2130                    "before_expression_again"
2131                    if "child_of_expression" in original_node.tags
2132                    else "before_statement_again"
2133                )
2134
2135                self._handle_progress_event(
2136                    frame, again_event, again_args, original_node.parent_node
2137                )
2138
2139    def _cmd_step_over_completed(self, frame, cmd):
2140        """
2141        Identifies the moment when piece of code indicated by cmd.frame_id and cmd.focus
2142        has completed execution (either successfully or not).
2143        """
2144
2145        if self._at_a_breakpoint(frame, cmd):
2146            return True
2147
2148        # Make sure the correct frame_id is selected
2149        if id(frame.system_frame) == cmd.frame_id:
2150            # We're in the same frame
2151            if "before_" in cmd.state:
2152                if not range_contains_smaller_or_equal(cmd.focus, frame.focus):
2153                    # Focus has changed, command has completed
2154                    return True
2155                else:
2156                    # Keep running
2157                    return False
2158            elif "after_" in cmd.state:
2159                if (
2160                    frame.focus != cmd.focus
2161                    or "before_" in frame.event
2162                    or "_expression" in cmd.state
2163                    and "_statement" in frame.event
2164                    or "_statement" in cmd.state
2165                    and "_expression" in frame.event
2166                ):
2167                    # The state has changed, command has completed
2168                    return True
2169                else:
2170                    # Keep running
2171                    return False
2172        else:
2173            # We're in another frame
2174            if self._frame_is_alive(cmd.frame_id):
2175                # We're in a successor frame, keep running
2176                return False
2177            else:
2178                # Original frame has completed, assumedly because of an exception
2179                # We're done
2180                return True
2181
2182        return True  # not actually required, just to make Pylint happy
2183
2184    def _cmd_step_into_completed(self, frame, cmd):
2185        return frame.event != "after_statement"
2186
2187    def _cmd_step_back_completed(self, frame, cmd):
2188        # Check if the selected message has been previously sent to front-end
2189        return (
2190            self._saved_states[self._current_state_index]["in_client_log"]
2191            or self._current_state_index == 0
2192        )
2193
2194    def _cmd_step_out_completed(self, frame, cmd):
2195        if self._current_state_index == 0:
2196            return False
2197
2198        if frame.event == "after_statement":
2199            return False
2200
2201        if self._at_a_breakpoint(frame, cmd):
2202            return True
2203
2204        prev_state_frame = self._saved_states[self._current_state_index - 1]["stack"][-1]
2205
2206        return (
2207            # the frame has completed
2208            not self._frame_is_alive(cmd.frame_id)
2209            # we're in the same frame but on higher level
2210            # TODO: expression inside statement expression has same range as its parent
2211            or id(frame.system_frame) == cmd.frame_id
2212            and range_contains_smaller(frame.focus, cmd.focus)
2213            # or we were there in prev state
2214            or id(prev_state_frame.system_frame) == cmd.frame_id
2215            and range_contains_smaller(prev_state_frame.focus, cmd.focus)
2216        )
2217
2218    def _cmd_resume_completed(self, frame, cmd):
2219        return self._at_a_breakpoint(frame, cmd)
2220
2221    def _at_a_breakpoint(self, frame, cmd, breakpoints=None):
2222        if breakpoints is None:
2223            breakpoints = cmd["breakpoints"]
2224
2225        return (
2226            frame.event in ["before_statement", "before_expression"]
2227            and frame.system_frame.f_code.co_filename in breakpoints
2228            and frame.focus.lineno in breakpoints[frame.system_frame.f_code.co_filename]
2229            # consider only first event on a line
2230            # (but take into account that same line may be reentered)
2231            and (
2232                cmd.focus is None
2233                or (cmd.focus.lineno != frame.focus.lineno)
2234                or (cmd.focus == frame.focus and cmd.state == frame.event)
2235                or id(frame.system_frame) != cmd.frame_id
2236            )
2237        )
2238
2239    def _frame_is_alive(self, frame_id):
2240        for frame in self._custom_stack:
2241            if id(frame.system_frame) == frame_id:
2242                return True
2243
2244        return False
2245
2246    def _export_stack(self):
2247        result = []
2248
2249        exported_globals_per_module = {}
2250
2251        def export_globals(module_name, frame):
2252            if module_name not in exported_globals_per_module:
2253                exported_globals_per_module[module_name] = self._backend.export_variables(
2254                    frame.f_globals
2255                )
2256            return exported_globals_per_module[module_name]
2257
2258        for custom_frame in self._custom_stack:
2259            system_frame = custom_frame.system_frame
2260            module_name = system_frame.f_globals["__name__"]
2261
2262            result.append(
2263                TempFrameInfo(
2264                    # need to store the reference to the frame to avoid it being GC-d
2265                    # otherwise frame id-s would be reused and this would
2266                    # mess up communication with the frontend.
2267                    system_frame=system_frame,
2268                    locals=None
2269                    if system_frame.f_locals is system_frame.f_globals
2270                    else self._backend.export_variables(system_frame.f_locals),
2271                    globals=export_globals(module_name, system_frame),
2272                    event=custom_frame.event,
2273                    focus=custom_frame.focus,
2274                    node_tags=custom_frame.node_tags,
2275                    current_evaluations=custom_frame.current_evaluations.copy(),
2276                    current_statement=custom_frame.current_statement,
2277                    current_root_expression=custom_frame.current_root_expression,
2278                )
2279            )
2280
2281        assert result  # not empty
2282        return result
2283
2284    def _thonny_hidden_before_stmt(self, node_id):
2285        # The code to be debugged will be instrumented with this function
2286        # inserted before each statement.
2287        # Entry into this function indicates that statement as given
2288        # by the code range is about to be evaluated next.
2289        return None
2290
2291    def _thonny_hidden_after_stmt(self, node_id):
2292        # The code to be debugged will be instrumented with this function
2293        # inserted after each statement.
2294        # Entry into this function indicates that statement as given
2295        # by the code range was just executed successfully.
2296        return None
2297
2298    def _thonny_hidden_before_expr(self, node_id):
2299        # Entry into this function indicates that expression as given
2300        # by the code range is about to be evaluated next
2301        return node_id
2302
2303    def _thonny_hidden_after_expr(self, node_id, value):
2304        # The code to be debugged will be instrumented with this function
2305        # wrapped around each expression (given as 2nd argument).
2306        # Entry into this function indicates that expression as given
2307        # by the code range was just evaluated to given value
2308        return value
2309
2310    def _tag_nodes(self, root):
2311        """Marks interesting properties of AST nodes"""
2312        # ast_utils need to be imported after asttokens
2313        # is (custom-)imported
2314        try_load_modules_with_frontend_sys_path(["asttokens", "six", "astroid"])
2315        from thonny import ast_utils
2316
2317        def add_tag(node, tag):
2318            if not hasattr(node, "tags"):
2319                node.tags = set()
2320                node.tags.add("class=" + node.__class__.__name__)
2321            node.tags.add(tag)
2322
2323        # ignore module docstring if it is before from __future__ import
2324        if (
2325            isinstance(root.body[0], ast.Expr)
2326            and isinstance(root.body[0].value, ast.Str)
2327            and len(root.body) > 1
2328            and isinstance(root.body[1], ast.ImportFrom)
2329            and root.body[1].module == "__future__"
2330        ):
2331            add_tag(root.body[0], "ignore")
2332            add_tag(root.body[0].value, "ignore")
2333            add_tag(root.body[1], "ignore")
2334
2335        for node in ast.walk(root):
2336            if not isinstance(node, (ast.expr, ast.stmt)):
2337                if isinstance(node, ast.comprehension):
2338                    for expr in node.ifs:
2339                        add_tag(expr, "comprehension.if")
2340
2341                continue
2342
2343            # tag last children
2344            last_child = ast_utils.get_last_child(node)
2345            assert last_child in [True, False, None] or isinstance(
2346                last_child, (ast.expr, ast.stmt, type(None))
2347            ), ("Bad last child " + str(last_child) + " of " + str(node))
2348            if last_child is not None:
2349                add_tag(node, "has_children")
2350
2351                if isinstance(last_child, ast.AST):
2352                    last_child.parent_node = node
2353                    add_tag(last_child, "last_child")
2354                    if isinstance(node, _ast.expr):
2355                        add_tag(last_child, "child_of_expression")
2356                    else:
2357                        add_tag(last_child, "child_of_statement")
2358
2359                    if isinstance(node, ast.Call):
2360                        add_tag(last_child, "last_call_arg")
2361
2362            # other cases
2363            if isinstance(node, ast.Call):
2364                add_tag(node.func, "call_function")
2365                node.func.parent_node = node
2366
2367            if isinstance(node, ast.BoolOp) and node.op == ast.Or():
2368                for child in node.values:
2369                    add_tag(child, "or_arg")
2370                    child.parent_node = node
2371
2372            if isinstance(node, ast.BoolOp) and node.op == ast.And():
2373                for child in node.values:
2374                    add_tag(child, "and_arg")
2375                    child.parent_node = node
2376
2377            # TODO: assert (it doesn't evaluate msg when test == True)
2378
2379            if isinstance(node, ast.stmt):
2380                for child in ast.iter_child_nodes(node):
2381                    child.parent_node = node
2382                    child.parent_statement_focus = TextRange(
2383                        node.lineno, node.col_offset, node.end_lineno, node.end_col_offset
2384                    )
2385
2386            if isinstance(node, ast.Str):
2387                add_tag(node, "skipexport")
2388
2389            if hasattr(ast, "JoinedStr") and isinstance(node, ast.JoinedStr):
2390                # can't present children normally without
2391                # ast giving correct locations for them
2392                # https://bugs.python.org/issue29051
2393                add_tag(node, "ignore_children")
2394
2395            elif isinstance(node, ast.Num):
2396                add_tag(node, "skipexport")
2397
2398            elif isinstance(node, ast.List):
2399                add_tag(node, "skipexport")
2400
2401            elif isinstance(node, ast.Tuple):
2402                add_tag(node, "skipexport")
2403
2404            elif isinstance(node, ast.Set):
2405                add_tag(node, "skipexport")
2406
2407            elif isinstance(node, ast.Dict):
2408                add_tag(node, "skipexport")
2409
2410            elif isinstance(node, ast.Name):
2411                add_tag(node, "skipexport")
2412
2413            elif isinstance(node, ast.NameConstant):
2414                add_tag(node, "skipexport")
2415
2416            elif hasattr(ast, "Constant") and isinstance(node, ast.Constant):
2417                add_tag(node, "skipexport")
2418
2419            elif isinstance(node, ast.Expr):
2420                if not isinstance(node.value, (ast.Yield, ast.YieldFrom)):
2421                    add_tag(node, "skipexport")
2422
2423            elif isinstance(node, ast.If):
2424                add_tag(node, "skipexport")
2425
2426            elif isinstance(node, ast.Return):
2427                add_tag(node, "skipexport")
2428
2429            elif isinstance(node, ast.While):
2430                add_tag(node, "skipexport")
2431
2432            elif isinstance(node, ast.Continue):
2433                add_tag(node, "skipexport")
2434
2435            elif isinstance(node, ast.Break):
2436                add_tag(node, "skipexport")
2437
2438            elif isinstance(node, ast.Pass):
2439                add_tag(node, "skipexport")
2440
2441            elif isinstance(node, ast.For):
2442                add_tag(node, "skipexport")
2443
2444            elif isinstance(node, ast.Try):
2445                add_tag(node, "skipexport")
2446
2447            elif isinstance(node, ast.ListComp):
2448                add_tag(node.elt, "ListComp.elt")
2449                if len(node.generators) > 1:
2450                    add_tag(node, "ignore_children")
2451
2452            elif isinstance(node, ast.SetComp):
2453                add_tag(node.elt, "SetComp.elt")
2454                if len(node.generators) > 1:
2455                    add_tag(node, "ignore_children")
2456
2457            elif isinstance(node, ast.DictComp):
2458                add_tag(node.key, "DictComp.key")
2459                add_tag(node.value, "DictComp.value")
2460                if len(node.generators) > 1:
2461                    add_tag(node, "ignore_children")
2462
2463            elif isinstance(node, ast.BinOp):
2464                # TODO: use static analysis to detect type of left child
2465                add_tag(node, "skipexport")
2466
2467            elif isinstance(node, ast.Attribute):
2468                # TODO: use static analysis to detect type of left child
2469                add_tag(node, "skipexport")
2470
2471            elif isinstance(node, ast.Subscript):
2472                # TODO: use static analysis to detect type of left child
2473                add_tag(node, "skipexport")
2474
2475            elif isinstance(node, ast.Compare):
2476                # TODO: use static analysis to detect type of left child
2477                add_tag(node, "skipexport")
2478
2479            if isinstance(node, (ast.Assign)):
2480                # value will be presented in assignment's before_statement_again
2481                add_tag(node.value, "skip_after_expression")
2482
2483            if isinstance(node, (ast.Expr, ast.While, ast.For, ast.If, ast.Try, ast.With)):
2484                add_tag(node, "skip_after_statement_again")
2485
2486            # make sure every node has this field
2487            if not hasattr(node, "tags"):
2488                node.tags = set()
2489
2490    def _should_instrument_as_expression(self, node):
2491        return (
2492            isinstance(node, _ast.expr)
2493            and hasattr(node, "end_lineno")
2494            and hasattr(node, "end_col_offset")
2495            and not getattr(node, "incorrect_range", False)
2496            and "ignore" not in node.tags
2497            and (not hasattr(node, "ctx") or isinstance(node.ctx, ast.Load))
2498            # TODO: repeatedly evaluated subexpressions of comprehensions
2499            # can be supported (but it requires some redesign both in backend and GUI)
2500            and "ListComp.elt" not in node.tags
2501            and "SetComp.elt" not in node.tags
2502            and "DictComp.key" not in node.tags
2503            and "DictComp.value" not in node.tags
2504            and "comprehension.if" not in node.tags
2505        )
2506
2507    def _should_instrument_as_statement(self, node):
2508        return (
2509            isinstance(node, _ast.stmt)
2510            and not getattr(node, "incorrect_range", False)
2511            and "ignore" not in node.tags
2512            # Shouldn't insert anything before from __future__ import
2513            # as this is not a normal statement
2514            # https://bitbucket.org/plas/thonny/issues/183/thonny-throws-false-positive-syntaxerror
2515            and (not isinstance(node, ast.ImportFrom) or node.module != "__future__")
2516        )
2517
2518    def _insert_statement_markers(self, root):
2519        # find lists of statements and insert before/after markers for each statement
2520        for name, value in ast.iter_fields(root):
2521            if isinstance(root, ast.Try) and name == "handlers":
2522                # contains statements but is not statement itself
2523                for handler in value:
2524                    self._insert_statement_markers(handler)
2525            elif isinstance(value, ast.AST):
2526                self._insert_statement_markers(value)
2527            elif isinstance(value, list):
2528                if len(value) > 0:
2529                    new_list = []
2530                    for node in value:
2531                        if self._should_instrument_as_statement(node):
2532                            # self._debug("EBFOMA", node)
2533                            # add before marker
2534                            new_list.append(
2535                                self._create_statement_marker(node, BEFORE_STATEMENT_MARKER)
2536                            )
2537
2538                        # original statement
2539                        if self._should_instrument_as_statement(node):
2540                            self._insert_statement_markers(node)
2541                        new_list.append(node)
2542
2543                        if (
2544                            self._should_instrument_as_statement(node)
2545                            and "skipexport" not in node.tags
2546                        ):
2547                            # add after marker
2548                            new_list.append(
2549                                self._create_statement_marker(node, AFTER_STATEMENT_MARKER)
2550                            )
2551                    setattr(root, name, new_list)
2552
2553    def _create_statement_marker(self, node, function_name):
2554        call = self._create_simple_marker_call(node, function_name)
2555        stmt = ast.Expr(value=call)
2556        ast.copy_location(stmt, node)
2557        ast.fix_missing_locations(stmt)
2558        return stmt
2559
2560    def _insert_for_target_markers(self, root):
2561        """inserts markers which notify assignment to for-loop variables"""
2562        for node in ast.walk(root):
2563            if isinstance(node, ast.For):
2564                old_target = node.target
2565                # print(vars(old_target))
2566                temp_name = "__for_loop_var"
2567                node.target = ast.Name(temp_name, ast.Store())
2568
2569                name_load = ast.Name(temp_name, ast.Load())
2570                # value will be visible in parent's before_statement_again event
2571                name_load.tags = {"skip_before_expression", "skip_after_expression", "last_child"}
2572                name_load.lineno, name_load.col_offset = (node.iter.lineno, node.iter.col_offset)
2573                name_load.end_lineno, name_load.end_col_offset = (
2574                    node.iter.end_lineno,
2575                    node.iter.end_col_offset,
2576                )
2577
2578                before_name_load = self._create_simple_marker_call(
2579                    name_load, BEFORE_EXPRESSION_MARKER
2580                )
2581                after_name_load = ast.Call(
2582                    func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()),
2583                    args=[before_name_load, name_load],
2584                    keywords=[],
2585                )
2586
2587                ass = ast.Assign([old_target], after_name_load)
2588                ass.lineno, ass.col_offset = old_target.lineno, old_target.col_offset
2589                ass.end_lineno, ass.end_col_offset = (
2590                    node.iter.end_lineno,
2591                    node.iter.end_col_offset,
2592                )
2593                ass.tags = {"skip_before_statement"}  # before_statement_again will be shown
2594
2595                name_load.parent_node = ass
2596
2597                ass_before = self._create_statement_marker(ass, BEFORE_STATEMENT_MARKER)
2598                node.body.insert(0, ass_before)
2599                node.body.insert(1, ass)
2600                node.body.insert(2, self._create_statement_marker(ass, AFTER_STATEMENT_MARKER))
2601
2602                ast.fix_missing_locations(node)
2603
2604    def _insert_expression_markers(self, node):
2605        """
2606        TODO: this docstring is outdated
2607        each expression e gets wrapped like this:
2608            _after(_before(_loc, _node_is_zoomable), e, _node_role, _parent_range)
2609        where
2610            _after is function that gives the resulting value
2611            _before is function that signals the beginning of evaluation of e
2612            _loc gives the code range of e
2613            _node_is_zoomable indicates whether this node has subexpressions
2614            _node_role is either 'last_call_arg', 'last_op_arg', 'first_or_arg',
2615                                 'first_and_arg', 'function' or None
2616        """
2617        tracer = self
2618
2619        class ExpressionVisitor(ast.NodeTransformer):
2620            def generic_visit(self, node):
2621                if isinstance(node, _ast.expr):
2622                    if isinstance(node, ast.Starred):
2623                        # keep this node as is, but instrument its children
2624                        return ast.NodeTransformer.generic_visit(self, node)
2625                    elif tracer._should_instrument_as_expression(node):
2626                        # before marker
2627                        before_marker = tracer._create_simple_marker_call(
2628                            node, BEFORE_EXPRESSION_MARKER
2629                        )
2630                        ast.copy_location(before_marker, node)
2631
2632                        if "ignore_children" in node.tags:
2633                            transformed_node = node
2634                        else:
2635                            transformed_node = ast.NodeTransformer.generic_visit(self, node)
2636
2637                        # after marker
2638                        after_marker = ast.Call(
2639                            func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()),
2640                            args=[before_marker, transformed_node],
2641                            keywords=[],
2642                        )
2643                        ast.copy_location(after_marker, node)
2644                        ast.fix_missing_locations(after_marker)
2645                        # further transformations may query original node location from after marker
2646                        if hasattr(node, "end_lineno"):
2647                            after_marker.end_lineno = node.end_lineno
2648                            after_marker.end_col_offset = node.end_col_offset
2649
2650                        return after_marker
2651                    else:
2652                        # This expression (and its children) should be ignored
2653                        return node
2654                else:
2655                    # Descend into statements
2656                    return ast.NodeTransformer.generic_visit(self, node)
2657
2658        return ExpressionVisitor().visit(node)
2659
2660    def _create_simple_marker_call(self, node, fun_name, extra_args=[]):
2661        args = [self._export_node(node)] + extra_args
2662
2663        return ast.Call(func=ast.Name(id=fun_name, ctx=ast.Load()), args=args, keywords=[])
2664
2665    def _export_node(self, node):
2666        assert isinstance(node, (ast.expr, ast.stmt))
2667        node_id = id(node)
2668        self._nodes[node_id] = node
2669        return ast.Num(node_id)
2670
2671    def _debug(self, *args):
2672        logger.debug("TRACER: " + str(args))
2673
2674    def _execute_prepared_user_code(self, statements, global_vars):
2675        try:
2676            return Tracer._execute_prepared_user_code(self, statements, global_vars)
2677        finally:
2678            """
2679            from thonny.misc_utils import _win_get_used_memory
2680            print("Memory:", _win_get_used_memory() / 1024 / 1024)
2681            print("States:", len(self._saved_states))
2682            print(self._fulltags.most_common())
2683            """
2684
2685
2686class CustomStackFrame:
2687    def __init__(self, frame, event, focus=None):
2688        self.system_frame = frame
2689        self.event = event
2690        self.focus = focus
2691        self.current_evaluations = []
2692        self.current_statement = None
2693        self.current_root_expression = None
2694        self.node_tags = set()
2695
2696
2697class FancySourceFileLoader(SourceFileLoader):
2698    """Used for loading and instrumenting user modules during fancy tracing"""
2699
2700    def __init__(self, fullname, path, tracer):
2701        super().__init__(fullname, path)
2702        self._tracer = tracer
2703
2704    def source_to_code(self, data, path, *, _optimize=-1):
2705        old_tracer = sys.gettrace()
2706        sys.settrace(None)
2707        try:
2708            root = self._tracer._prepare_ast(data, path, "exec")
2709            return super().source_to_code(root, path)
2710        finally:
2711            sys.settrace(old_tracer)
2712
2713
2714def _get_frame_prefix(frame):
2715    return str(id(frame)) + " " + ">" * len(inspect.getouterframes(frame, 0)) + " "
2716
2717
2718def _fetch_frame_source_info(frame):
2719    if frame.f_code.co_filename is None or not os.path.exists(frame.f_code.co_filename):
2720        return None, None, True
2721
2722    is_libra = _is_library_file(frame.f_code.co_filename)
2723
2724    if frame.f_code.co_name == "<lambda>":
2725        source = inspect.getsource(frame.f_code)
2726        return source, frame.f_code.co_firstlineno, is_libra
2727    elif frame.f_code.co_name == "<module>":
2728        # inspect.getsource and getsourcelines don't help here
2729        with tokenize.open(frame.f_code.co_filename) as fp:
2730            return fp.read(), 1, is_libra
2731    else:
2732        # function or class
2733        try:
2734            source = inspect.getsource(frame.f_code)
2735
2736            # inspect.getsource is not reliable, see eg:
2737            # https://bugs.python.org/issue35101
2738            # If the code name is not present as definition
2739            # in the beginning of the source,
2740            # then play safe and return the whole script
2741            first_line = source.splitlines()[0]
2742            if re.search(r"\b(class|def)\b\s+\b%s\b" % frame.f_code.co_name, first_line) is None:
2743                with tokenize.open(frame.f_code.co_filename) as fp:
2744                    return fp.read(), 1, is_libra
2745
2746            else:
2747                return source, frame.f_code.co_firstlineno, is_libra
2748        except OSError:
2749            logger.exception("Problem getting source")
2750            return None, None, True
2751
2752
2753def format_exception_with_frame_info(e_type, e_value, e_traceback, shorten_filenames=False):
2754    """Need to suppress thonny frames to avoid confusion"""
2755
2756    _traceback_message = "Traceback (most recent call last):\n"
2757
2758    _cause_message = getattr(
2759        traceback,
2760        "_cause_message",
2761        ("\nThe above exception was the direct cause " + "of the following exception:") + "\n\n",
2762    )
2763
2764    _context_message = getattr(
2765        traceback,
2766        "_context_message",
2767        ("\nDuring handling of the above exception, " + "another exception occurred:") + "\n\n",
2768    )
2769
2770    def rec_format_exception_with_frame_info(etype, value, tb, chain=True):
2771        # Based on
2772        # https://www.python.org/dev/peps/pep-3134/#enhanced-reporting
2773        # and traceback.format_exception
2774
2775        if etype is None:
2776            etype = type(value)
2777
2778        if tb is None:
2779            tb = value.__traceback__
2780
2781        if chain:
2782            if value.__cause__ is not None:
2783                yield from rec_format_exception_with_frame_info(None, value.__cause__, None)
2784                yield (_cause_message, None, None, None)
2785            elif value.__context__ is not None and not value.__suppress_context__:
2786                yield from rec_format_exception_with_frame_info(None, value.__context__, None)
2787                yield (_context_message, None, None, None)
2788
2789        if tb is not None:
2790            yield (_traceback_message, None, None, None)
2791
2792            tb_temp = tb
2793            for entry in traceback.extract_tb(tb):
2794                assert tb_temp is not None  # actual tb doesn't end before extract_tb
2795                if (
2796                    "cpython_backend" not in entry.filename
2797                    and "thonny/backend" not in entry.filename.replace("\\", "/")
2798                    and (
2799                        not entry.filename.endswith(os.sep + "ast.py")
2800                        or entry.name != "parse"
2801                        or etype is not SyntaxError
2802                    )
2803                ):
2804                    fmt = '  File "{}", line {}, in {}\n'.format(
2805                        entry.filename, entry.lineno, entry.name
2806                    )
2807
2808                    if entry.line:
2809                        fmt += "    {}\n".format(entry.line.strip())
2810
2811                    yield (fmt, id(tb_temp.tb_frame), entry.filename, entry.lineno)
2812
2813                tb_temp = tb_temp.tb_next
2814
2815            assert tb_temp is None  # tb was exhausted
2816
2817        for line in traceback.format_exception_only(etype, value):
2818            if etype is SyntaxError and line.endswith("^\n"):
2819                # for some reason it may add several empty lines before ^-line
2820                partlines = line.splitlines()
2821                while len(partlines) >= 2 and partlines[-2].strip() == "":
2822                    del partlines[-2]
2823                line = "\n".join(partlines) + "\n"
2824
2825            yield (line, None, None, None)
2826
2827    items = rec_format_exception_with_frame_info(e_type, e_value, e_traceback)
2828
2829    return list(items)
2830
2831
2832def in_debug_mode():
2833    return os.environ.get("THONNY_DEBUG", False) in [1, "1", True, "True", "true"]
2834
2835
2836def _is_library_file(filename):
2837    return (
2838        filename is None
2839        or path_startswith(filename, sys.prefix)
2840        or hasattr(sys, "base_prefix")
2841        and path_startswith(filename, sys.base_prefix)
2842        or hasattr(sys, "real_prefix")
2843        and path_startswith(filename, getattr(sys, "real_prefix"))
2844        or site.ENABLE_USER_SITE
2845        and path_startswith(filename, site.getusersitepackages())
2846    )
2847
2848
2849def get_backend():
2850    return _backend
2851