repl.py 24.5 KB
Newer Older
Benoit Formet's avatar
Benoit Formet committed
1
# -*- coding: utf-8 -*-
2
3
4
#
# This file is part of the bliss project
#
5
# Copyright (c) 2015-2022 Beamline Control Unit, ESRF
6
7
# Distributed under the GNU LGPLv3. See LICENSE for more info.

8
"""Bliss REPL (Read Eval Print Loop)"""
9

10
import asyncio
11
from typing import Optional
12
13
14
15
from prompt_toolkit.styles.pygments import style_from_pygments_cls
from pygments.styles import get_style_by_name
from pygments.util import ClassNotFound

16
import re
17
18
import os
import sys
19
import types
20
import socket
21
import functools
22
import traceback
23
import gevent
24
import signal
25
import logging
26
import platform
Valentin Valls's avatar
Valentin Valls committed
27
from collections import deque
28
from datetime import datetime
29

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
30
import ptpython.layout
Perceval Guillou's avatar
Perceval Guillou committed
31
32
33
34
35
from prompt_toolkit.patch_stdout import (
    patch_stdout as patch_stdout_context,
    StdoutProxy,
)
from prompt_toolkit import patch_stdout as patch_stdout_module
36
from prompt_toolkit.output import DummyOutput
37

Benoit Formet's avatar
Benoit Formet committed
38
# imports needed to have control over _execute of ptpython
39
from prompt_toolkit.keys import Keys
40
from prompt_toolkit.utils import is_windows
41
42
43
from prompt_toolkit.filters import has_focus
from prompt_toolkit.enums import DEFAULT_BUFFER

44
from .. import log_utils
45
from bliss.shell.data.display import ScanDisplayDispatcher
46
47
48
from bliss.shell.cli.prompt import BlissPrompt
from bliss.shell.cli.typing_helper import TypingHelper
from bliss.shell.cli.ptpython_statusbar_patch import NEWstatus_bar, TMUXstatus_bar
49
50
51
52
from bliss.shell.bliss_banners import print_rainbow_banner
from bliss.shell.cli.protected_dict import ProtectedDict
from bliss.shell.cli.no_thread_repl import NoThreadPythonRepl
from bliss.shell import standard
53

Valentin Valls's avatar
Valentin Valls committed
54
from bliss import set_bliss_shell_mode
55
from bliss.common.utils import ShellStr, Singleton
56
from bliss.common import constants
57
58
from bliss.common import session as session_mdl
from bliss.common.session import DefaultSession
59
60
from bliss import release, current_session
from bliss.config import static
61
from bliss.config.conductor.client import get_default_connection
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
62
from bliss.shell.standard import info
63
from bliss.common.logtools import userlogger, elogbook
64
from bliss.common.protocols import ErrorReportInterface
65

66
logger = logging.getLogger(__name__)
67

Perceval Guillou's avatar
Perceval Guillou committed
68
69
70
71
72
73
74
75
76
77
78

class BlissStdoutProxy(StdoutProxy):
    def _write(self, data: str):
        res = super()._write(data)
        if "\r" in data:
            self.flush()
        return res


patch_stdout_module.StdoutProxy = BlissStdoutProxy

79
if is_windows():
80

81
82
83
84
85
86
    class Terminal:
        def __getattr__(self, prop):
            if prop.startswith("__"):
                raise AttributeError(prop)
            return ""

Perceval Guillou's avatar
Perceval Guillou committed
87

88
89
90
91
92
93
94
95
else:
    from blessings import Terminal


session_mdl.set_current_session = functools.partial(
    session_mdl.set_current_session, force=False
)

96

97
# =================== ERROR REPORTING ============================
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115


class LastError:
    def __init__(self):
        self.errors = deque()

    def __getitem__(self, index):
        try:
            return ShellStr(self.errors[index])
        except IndexError:
            return ShellStr(
                f"No exception with index {index} found, size is {len(self.errors)}"
            )

    def __repr__(self):
        try:
            return ShellStr(self.errors[-1])
        except IndexError:
116
            return "None"
117
118
119
120
121
122
123

    def append(self, item):
        self.errors.append(item)
        while len(self.errors) > 100:
            self.errors.popleft()


124
class ErrorReport(ErrorReportInterface):
Benoit Formet's avatar
Benoit Formet committed
125
    """
126
    Manage the behavior of the error reporting in the shell.
Benoit Formet's avatar
Benoit Formet committed
127

128
129
    - ErrorReport.expert_mode = False (default) => prints a user friendly error message without traceback
    - ErrorReport.expert_mode = True            => prints the full error message with traceback
Benoit Formet's avatar
Benoit Formet committed
130

131
132
133
134
    - ErrorReport.last_error stores the last error traceback

    """

135
136
    _orig_sys_excepthook = sys.excepthook
    _orig_gevent_print_exception = gevent.hub.Hub.print_exception
137

138
    def __init__(self):
139
        self._expert_mode = False
140
        self._last_error = LastError()
141
142
143

    @property
    def last_error(self):
144
        return self._last_error
145
146
147
148
149
150
151
152
153
154

    @property
    def expert_mode(self):
        return self._expert_mode

    @expert_mode.setter
    def expert_mode(self, enable):
        self._expert_mode = bool(enable)


155
156
157
158
def install_excepthook():
    """Patch the system exception hook,
    and the print exception for gevent greenlet
    """
159
    error_report = ErrorReport()
160

161
    exc_logger = logging.getLogger("exceptions")
162

163
    def repl_excepthook(exc_type, exc_value, tb, _with_elogbook=True):
164
165
166
        if exc_value is None:
            # filter exceptions from aiogevent(?) with no traceback, no value
            return
167
        err_file = sys.stderr
168

169
        # Store latest traceback (as a string to avoid memory leaks)
170
171
172
173
174
175
176
177
178
179
180
        # next lines are inspired from "_handle_exception()" (ptpython/repl.py)
        # skip bottom calls from ptpython
        tblist = list(traceback.extract_tb(tb))
        to_remove = 0
        for line_nr, tb_tuple in enumerate(tblist):
            if tb_tuple.filename == "<stdin>":
                to_remove = line_nr
        for i in range(to_remove):
            tb = tb.tb_next

        exc_text = "".join(traceback.format_exception(exc_type, exc_value, tb))
181
        error_report._last_error.append(
182
            datetime.now().strftime("%d/%m/%Y %H:%M:%S ") + exc_text
183
        )
184

185
        exc_logger.error(exc_text)
186

187
188
189
        # Adapt the error message depending on the expert_mode
        if error_report._expert_mode:
            print(error_report._last_error, file=err_file)
Perceval Guillou's avatar
Perceval Guillou committed
190
191
192
193
        elif current_session:
            if current_session.is_loading_config:
                print(f"{exc_type.__name__}: {exc_value}", file=err_file)
            else:
194
195
196
197
                print(
                    f"!!! === {exc_type.__name__}: {exc_value} === !!! ( for more details type cmd 'last_error' )",
                    file=err_file,
                )
198

199
        if _with_elogbook:
200
            try:
201
                elogbook.error(f"{exc_type.__name__}: {exc_value}")
202
            except Exception:
203
                repl_excepthook(*sys.exc_info(), _with_elogbook=False)
204

205
    def print_exception(self, context, exc_type, exc_value, tb):
206
        if gevent.getcurrent() is self:
207
208
209
210
            # repl_excepthook tries to yield to the gevent loop
            gevent.spawn(repl_excepthook, exc_type, exc_value, tb)
        else:
            repl_excepthook(exc_type, exc_value, tb)
211

212
    sys.excepthook = repl_excepthook
213
    gevent.hub.Hub.print_exception = types.MethodType(print_exception, gevent.get_hub())
214
    return error_report
215

216

217
218
219
220
221
def reset_excepthook():
    sys.excepthook = ErrorReport._orig_sys_excepthook
    gevent.hub.Hub.print_exception = ErrorReport._orig_gevent_print_exception


222
__all__ = ("BlissRepl", "embed", "cli", "configure_repl")
223

224
225
#############
# patch ptpython signaturetoolbar
226
import bliss.shell.cli.ptpython_signature_patch  # noqa: F401,E402
227

228
# patch ptpython completer, and jedi
229
import bliss.shell.cli.ptpython_completer_patch  # noqa: F401,E402
230

231
#############
232
233


234
235
236
class Info:
    def __init__(self, obj_with_info):
        self.info_repr = info(obj_with_info)
237

238
239
    def __repr__(self):
        return self.info_repr
240
241


242
class WrappedStdout:
243
244
    def __init__(self, output_buffer):
        self._buffer = output_buffer
245
        self._orig_stdout = sys.stdout
246

247
248
249
250
251
    # context manager
    def __enter__(self, *args, **kwargs):
        self._orig_stdout = sys.stdout
        sys.stdout = self
        return self
252

253
254
    def __exit__(self, *args, **kwargs):
        sys.stdout = self._orig_stdout
255

256
257
258
259
    # delegated members
    @property
    def encoding(self):
        return self._orig_stdout.encoding
260

261
262
263
    @property
    def errors(self):
        return self._orig_stdout.errors
264

265
266
267
    def fileno(self) -> int:
        # This is important for code that expects sys.stdout.fileno() to work.
        return self._orig_stdout.fileno()
268

269
270
    def isatty(self) -> bool:
        return self._orig_stdout.isatty()
271

272
273
    def flush(self):
        self._orig_stdout.flush()
274

275
276
277
278
    # extended members
    def write(self, data):
        # wait for stdout to be ready to receive output
        if True:  # if gevent.select.select([],[self.fileno()], []):
279
            self._buffer.append(data)
280
            self._orig_stdout.write(data)
281

282

283
class PromptToolkitOutputWrapper(DummyOutput):
Valentin Valls's avatar
Valentin Valls committed
284
    """This class is used to keep track of the output history."""
285
286
287

    _MAXLEN = 20

288
289
    def __init__(self, output):
        self.__wrapped_output = output
290
        self._output_buffer = []
291
292
        self._cell_counter = 0
        self._cell_output_history = deque(maxlen=self._MAXLEN)
293
294
295
296
297
298
299
300

    def __getattr__(self, attr):
        if attr.startswith("__"):
            raise AttributeError(attr)
        return getattr(self.__wrapped_output, attr)

    @property
    def capture_stdout(self):
301
302
        return WrappedStdout(self._output_buffer)

303
    def finalize_cell(self):
Valentin Valls's avatar
Valentin Valls committed
304
        """Store the current buffered output as 1 cell output in the history."""
305
306
307
308
309
310
311
312
313
314
315
316
        if self._output_buffer:
            output = "".join(
                [x if isinstance(x, str) else str(x) for x in self._output_buffer]
            )
            output = re.sub(
                r"^(\s+Out\s\[\d+\]:\s+)", "", output, count=1, flags=re.MULTILINE
            )
            self._output_buffer.clear()
        else:
            output = None
        self._cell_output_history.append(output)
        self._cell_counter += 1
317

318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    def __getitem__(self, item: int) -> Optional[str]:
        """Note that the ptpython cell index starts counting from 1

        item > 0 will be interpreted as the cell index
        item < 0 will be interpreted as the most recent cell output (-1 is the last output)
        item == 0 raise IndexError

        The output value of a cell without output is `None`.
        """
        if not isinstance(item, int):
            raise TypeError(item)
        if item > 0:
            # convert cell index to queue index
            idx = item - self._cell_counter - 1
            if idx >= 0:
                raise IndexError(f"the last cell is OUT [{self._cell_counter}]")
        elif item == 0:
            idx_min = max(self._cell_counter - self._MAXLEN + 1, 1)
            raise IndexError(f"the first available cell is OUT [{idx_min}]")
        elif (item + self._cell_counter) < 0:
            idx_min = max(self._cell_counter - self._MAXLEN + 1, 1)
            raise IndexError(f"the first available cell is OUT [{idx_min}]")
        else:
            idx = item
        try:
            return self._cell_output_history[idx]
        except IndexError:
            idx_min = max(self._cell_counter - self._MAXLEN + 1, 1)
            raise IndexError(f"the first available cell is OUT [{idx_min}]") from None
347
348

    def write(self, data):
349
        self._output_buffer.append(data)
350
351
        self.__wrapped_output.write(data)

352
353
354
    def fileno(self):
        return self.__wrapped_output.fileno()

355

356
class BlissReplBase(NoThreadPythonRepl, metaclass=Singleton):
357
    def __init__(self, *args, **kwargs):
358
359
360
        prompt_label = kwargs.pop("prompt_label", "BLISS")
        title = kwargs.pop("title", None)
        session = kwargs.pop("session")
361
        style = kwargs.pop("style")
362

363
364
365
        # bliss_bar = status_bar(self)
        # toolbars = list(kwargs.pop("extra_toolbars", ()))
        # kwargs["_extra_toolbars"] = [bliss_bar] + toolbars
366

GUILLOU Perceval's avatar
GUILLOU Perceval committed
367
368
369
        # Catch and remove additional kwargs
        self.session_name = kwargs.pop("session_name", "default")
        self.use_tmux = kwargs.pop("use_tmux", False)
370

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
371
        # patch ptpython statusbar
372
        if self.use_tmux and not is_windows():
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
373
374
375
            ptpython.layout.status_bar = TMUXstatus_bar
        else:
            ptpython.layout.status_bar = NEWstatus_bar
376

377
        super().__init__(*args, **kwargs)
378

379
380
        self.app.output = PromptToolkitOutputWrapper(self.app.output)

381
382
        if title:
            self.terminal_title = title
383

Linus Pithan's avatar
Linus Pithan committed
384
        # self.show_bliss_bar = True
385
386
        # self.bliss_bar = bliss_bar
        # self.bliss_bar_format = "normal"
387
        self.bliss_session = session
388
        self.bliss_prompt = BlissPrompt(self, prompt_label)
Geoffrey Mant's avatar
Geoffrey Mant committed
389
        self.all_prompt_styles["bliss"] = self.bliss_prompt
Linus Pithan's avatar
Linus Pithan committed
390
        self.prompt_style = "bliss"
391

Linus Pithan's avatar
Linus Pithan committed
392
        self.show_signature = True
393

394
395
396
397
398
399
400
401
        self.color_depth = "DEPTH_8_BIT"
        try:
            theme = style_from_pygments_cls(get_style_by_name(style))
        except ClassNotFound:
            print(
                f"Unknown color style class: {style}. using default. (check your bliss.ini)."
            )
            theme = style_from_pygments_cls(get_style_by_name("default"))
Jibril Mammeri's avatar
Jibril Mammeri committed
402

403
404
405
406
        self.install_ui_colorscheme("bliss_ui", theme)
        self.use_ui_colorscheme("bliss_ui")
        self.install_code_colorscheme("bliss_code_ui", theme)
        self.use_code_colorscheme("bliss_code_ui")
407

408
409
410
        # PTPYTHON SHELL PREFERENCES
        self.enable_history_search = True
        self.show_status_bar = True
GUILLOU Perceval's avatar
GUILLOU Perceval committed
411
        self.confirm_exit = True
412
        self.enable_mouse_support = False
413

414
415
416
417
418
        if self.use_tmux:
            self.exit_message = (
                "Do you really want to close session? (CTRL-B D to detach)"
            )

Linus Pithan's avatar
Linus Pithan committed
419
420
        self.typing_helper = TypingHelper(self)

421
422
423
    ##
    # NB: next methods are overloaded
    ##
424
425
426
427
428
429
    def eval(self, text):
        logging.getLogger("user_input").info(text)
        elogbook.command(text)
        with self.app.output.capture_stdout:
            result = super().eval(text)
            if result is None:
430
431
                # show_result will not be called
                self.app.output.finalize_cell()
432
433
            return result

434
    async def eval_async(self, text):
Matias Guijarro's avatar
Matias Guijarro committed
435
436
        logging.getLogger("user_input").info(text)
        elogbook.command(text)
437
438
439
        with self.app.output.capture_stdout:
            result = await super().eval_async(text)
            if result is None:
440
441
                # show_result will not be called
                self.app.output.finalize_cell()
442
443
            return result

444
    def show_result(self, result):
Valentin Valls's avatar
Valentin Valls committed
445
        """This is called when the return value of the command is not None."""
Matias Guijarro's avatar
Matias Guijarro committed
446
447
448
449
450
451
452
453
        try:
            if hasattr(result, "__info__"):
                result = Info(result)
        except BaseException:
            # display exception, but do not propagate and make shell to die
            sys.excepthook(*sys.exc_info())
        else:
            return super().show_result(result)
454
        finally:
455
            self.app.output.finalize_cell()
456

457
458
    def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None:
        sys.excepthook(*sys.exc_info())
459

460
461
    def _handle_exception(self, e):
        sys.excepthook(*sys.exc_info())
462

463

464
465
466
class BlissRepl(BlissReplBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
467

468
469
470
471
472
        self._sigint_handler = gevent.signal_handler(signal.SIGINT, self._handle_ctrl_c)

    def _handle_ctrl_c(self):
        with self._lock:
            if self._current_eval_g:
473
                self._current_eval_g.kill(KeyboardInterrupt, block=False)
474

475

476
def configure_repl(repl):
477

Linus Pithan's avatar
Linus Pithan committed
478
479
480
481
482
483
484
485
486
487
488
489
    # intended to be used for testing as ctrl+t can be send via stdin.write(bytes.fromhex("14"))
    # @repl.add_key_binding(Keys.ControlT)
    # def _(event):
    #    sys.stderr.write("<<BLISS REPL TEST>>")
    #    text = repl.default_buffer.text
    #    sys.stderr.write("<<BUFFER TEST>>")
    #    sys.stderr.write(text)
    #    sys.stderr.write("<<BUFFER TEST>>")
    #    sys.stderr.write("<<HISTORY>>")
    #    sys.stderr.write(repl.default_buffer.history._loaded_strings[-1])
    #    sys.stderr.write("<<HISTORY>>")
    #    sys.stderr.write("<<BLISS REPL TEST>>")
490

491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
    @repl.add_key_binding(
        Keys.ControlSpace, filter=has_focus(DEFAULT_BUFFER), eager=True
    )
    def _(event):
        """
        Initialize autocompletion at cursor.
        If the autocompletion menu is not showing, display it with the
        appropriate completions for the context.
        If the menu is showing, select the next completion.
        """

        b = event.app.current_buffer
        if b.complete_state:
            b.complete_next()
        else:
            b.start_completion(select_first=False)

508

509
def initialize(
510
    session_name=None, session_env=None, expert_error_report=True, early_log_info=None
511
) -> session_mdl.Session:
512
513
514
515
516
517
518
519
520
521
522
523
524
525
    """
    Initialize a session.

    Create a session from its name, and update a provided env dictionary.

    Arguments:
        session_name: Name of the session to load
        session_env: Dictionary containing an initial env to feed. If not defined
                     an empty dict is used
    """
    if session_env is None:
        session_env = {}

    # Add config to the user namespace
Lucas Felix's avatar
Lucas Felix committed
526
    config = static.get_config()
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541

    """ BLISS CLI welcome messages """

    t = Terminal()

    # Version
    _version = "version %s" % release.short_version

    # Hostname
    _hostname = platform.node()

    # Beacon host/port
    try:
        _host = get_default_connection()._host
        _port = str(get_default_connection()._port)
542
    except Exception:
543
544
545
546
547
        _host = "UNKNOWN"
        _port = "UNKNOWN"

    # Conda environment
    try:
548
        _conda_env = "(in %s Conda environment)" % os.environ["CONDA_DEFAULT_ENV"]
549
550
551
552
553
554
555
556
557
    except KeyError:
        _conda_env = ""

    print_rainbow_banner()
    print("")
    print(
        "Welcome to BLISS %s running on {t.blue}%s{t.normal} %s".format(t=t)
        % (_version, _hostname, _conda_env)
    )
558
    print("Copyright (c) 2015-2022 Beamline Control Unit, ESRF")
559
560
561
562
563
564
    print("-")
    print(
        "Connected to Beacon server on {t.blue}%s{t.normal} (port %s)".format(t=t)
        % (_host, _port)
    )

565
566
567
568
569
570
    if early_log_info is not None and early_log_info.count > 0:
        print()
        print(
            f"During the import {early_log_info.count} warnings were ignored. Restart BLISS with --debug to display them."
        )

571
572
573
    if config.invalid_yaml_files:
        print()
        print(
574
            f"Ignored {len(config.invalid_yaml_files)} YAML file(s) due to parsing error(s), use config.parsing_report() for details.\n"
575
576
        )

577
    # Setup(s)
578
579
580
581
582
583
584
585
586
    if session_name is None:
        session = DefaultSession()
    else:
        # we will lock the session name
        # this will prevent to start serveral bliss shell
        # with the same session name
        # lock will only be released at the end of process
        default_cnx = get_default_connection()
        try:
587
            default_cnx.lock(session_name, timeout=1.0)
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
        except RuntimeError:
            try:
                lock_dict = default_cnx.who_locked(session_name)
            except RuntimeError:  # Beacon is to old to answer
                raise RuntimeError(f"{session_name} is already started")
            else:
                raise RuntimeError(
                    f"{session_name} is already running on %s"
                    % lock_dict.get(session_name)
                )
        # set the client name to somethings useful
        try:
            default_cnx.set_client_name(
                f"host:{socket.gethostname()},pid:{os.getpid()} cmd: **bliss -s {session_name}**"
            )
        except RuntimeError:  # Beacon is too old
            pass
        session = config.get(session_name)
        print("%s: Loading config..." % session.name)

    from bliss.shell import standard

    cmds = {k: standard.__dict__[k] for k in standard.__all__}
    session_env.update(cmds)

    session_env["history"] = lambda: print("Please press F3-key to view history!")

615
616
617
    if session.setup(
        session_env, verbose=True, expert_error_report=expert_error_report
    ):
618
        print("Done.")
Matias Guijarro's avatar
Matias Guijarro committed
619
620
621
    else:
        print("Warning: error(s) happened during setup, setup may not be complete.")
    print("")
622

623
624
625
626
627
628
629
630
631
    log = logging.getLogger("startup")
    log.info(
        f"Started BLISS version "
        f"{_version} running on "
        f"{_hostname} "
        f"{_conda_env} "
        f"connected to Beacon server {_host}"
    )

632
633
634
    return session


635
def _archive_history(
Valentin Valls's avatar
Valentin Valls committed
636
    history_filename, file_size_thresh=10**6, keep_active_entries=1000
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
):
    if (
        os.path.exists(history_filename)
        and os.stat(history_filename).st_size > file_size_thresh
    ):
        with open(history_filename, "r") as f:
            lines = f.readlines()

        # history is handled as a list of entries (block of lines) to avoid splitting them while archiving
        entries = []
        entry = []
        for line in lines:
            if not line.isspace():
                entry.append(line)
            elif entry:
                entries.append(entry)
                entry = []
        if entry:
            entries.append(entry)

        now = datetime.now()
        archive_filename = f"{history_filename}_{now.year}{now.month:02}{now.day:02}"
        with open(archive_filename, "a") as f:
            for entry in entries[:-keep_active_entries]:
                f.write("".join(entry) + "\n")

        with open(history_filename, "w") as f:
            for entry in entries[-keep_active_entries:]:
                f.write("".join(entry) + "\n")


668
669
670
671
672
def cli(
    locals=None,
    session_name=None,
    vi_mode=False,
    startup_paths=None,
GUILLOU Perceval's avatar
GUILLOU Perceval committed
673
    use_tmux=False,
674
    expert_error_report=False,
675
    style="default",
676
    early_log_info=None,
677
    **kwargs,
678
):
679
    """
680
    Create a command line interface
Valentin Valls's avatar
Valentin Valls committed
681

682
    Args:
683
        session_name : session to initialize (default: None)
684
685
        vi_mode (bool): Use Vi instead of Emacs key bindings.
    """
686
687
    set_bliss_shell_mode(True)

688
689
690
    # Enable loggers
    userlogger.enable()  # destination: user
    elogbook.enable()  # destination: electronic logbook
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
691

692
693
694
695
    # user namespace
    user_ns = {}
    protected_user_ns = ProtectedDict(user_ns)

696
    # These 2 commands can be used by user script loaded during
697
698
699
    # the initialization
    user_ns["protect"] = protected_user_ns.protect
    user_ns["unprotect"] = protected_user_ns.unprotect
700

701
    if session_name and not session_name.startswith(constants.DEFAULT_SESSION_NAME):
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
702
        try:
703
704
705
706
            session = initialize(
                session_name,
                session_env=user_ns,
                expert_error_report=expert_error_report,
707
                early_log_info=early_log_info,
708
            )
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
709
710
711
712
713
        except RuntimeError as e:
            if use_tmux:
                print("\n", "*" * 20, "\n", e, "\n", "*" * 20)
                gevent.sleep(10)  # just to let the eyes to see the message ;)
            raise
GUILLOU Perceval's avatar
GUILLOU Perceval committed
714
    else:
715
716
717
718
        session = initialize(
            session_name=None,
            session_env=user_ns,
            expert_error_report=expert_error_report,
719
            early_log_info=early_log_info,
720
        )
721

722
    if session.name != constants.DEFAULT_SESSION_NAME:
723
        protected_user_ns._protect(session.object_names)
724
725
726
727
        # protect Aliases if they exist
        if "ALIASES" in protected_user_ns:
            for alias in protected_user_ns["ALIASES"].names_iter():
                if alias in protected_user_ns:
728
                    protected_user_ns._protect(alias)
729

730
    # handle the last error report
731
    # (in the shell env only)
732
    user_ns["last_error"] = user_ns["ERROR_REPORT"].last_error
733

734
735
736
737
738
739
740
741
742
743
    # protect certain imports and Globals
    to_protect = [
        "ERROR_REPORT",
        "last_error",
        "ALIASES",
        "SCAN_DISPLAY",
        "SCAN_SAVING",
        "SCANS",
    ]
    to_protect.extend(standard.__all__)
744
    protected_user_ns._protect(to_protect)
745

746
    def get_globals():
747
        return protected_user_ns
748

749
    if session_name and not session_name.startswith(constants.DEFAULT_SESSION_NAME):
750
        session_id = session_name
Vincent Michel's avatar
Vincent Michel committed
751
        session_title = "Bliss shell ({0})".format(session_name)
752
        prompt_label = session_name.upper()
753
    else:
754
        session_id = "default"
Vincent Michel's avatar
Vincent Michel committed
755
        session_title = "Bliss shell"
756
        prompt_label = "BLISS"
757

758
    history_filename = ".bliss_%s_history" % (session_id)
759
    if is_windows():
760
761
762
        history_filename = os.path.join(os.environ["USERPROFILE"], history_filename)
    else:
        history_filename = os.path.join(os.environ["HOME"], history_filename)
763

764
765
    _archive_history(history_filename)

766
    # Create REPL.
767
    repl = BlissRepl(
768
        get_globals=get_globals,
769
770
771
772
773
774
        session=session,
        vi_mode=vi_mode,
        prompt_label=prompt_label,
        title=session_title,
        history_filename=history_filename,
        startup_paths=startup_paths,
775
        session_name=session_name,
GUILLOU Perceval's avatar
GUILLOU Perceval committed
776
        use_tmux=use_tmux,
777
        style=style,
778
        **kwargs,
779
    )
780

781
    # Custom keybindings
782
783
784
    configure_repl(repl)

    return repl
785

786
787
788

def embed(*args, **kwargs):
    """
789
    Call this to embed bliss shell at the current point in your program
790
    """
791
792
    use_tmux = kwargs.get("use_tmux", False)

793
    with log_utils.filter_warnings():
794
        cmd_line_i = cli(*args, **kwargs)
795
796
797
        scans_display = ScanDisplayDispatcher(cmd_line_i)
        if not is_windows() and use_tmux:
            scans_display.set_use_progress_bar(True)
798

799
        with patch_stdout_context(raw=True):
800
            asyncio.run(cmd_line_i.run_async())