The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    └── workflows
    │   └── test.yaml
├── .gitignore
├── CHANGELOG
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs
    ├── concurrency-challenges.rst
    └── images
    │   ├── example1.png
    │   ├── file-completion.png
    │   ├── ipython.png
    │   ├── multiline.png
    │   ├── ptpython-history-help.png
    │   ├── ptpython-menu.png
    │   ├── ptpython.png
    │   ├── validation.png
    │   └── windows.png
├── examples
    ├── asyncio-python-embed.py
    ├── asyncio-ssh-python-embed.py
    ├── ptpython_config
    │   └── config.py
    ├── python-embed-with-custom-prompt.py
    ├── python-embed.py
    ├── python-input.py
    ├── ssh-and-telnet-embed.py
    └── test-cases
    │   └── ptpython-in-other-thread.py
├── pyproject.toml
├── src
    └── ptpython
    │   ├── __init__.py
    │   ├── __main__.py
    │   ├── completer.py
    │   ├── contrib
    │       ├── __init__.py
    │       └── asyncssh_repl.py
    │   ├── entry_points
    │       ├── __init__.py
    │       ├── run_ptipython.py
    │       └── run_ptpython.py
    │   ├── eventloop.py
    │   ├── filters.py
    │   ├── history_browser.py
    │   ├── ipython.py
    │   ├── key_bindings.py
    │   ├── layout.py
    │   ├── lexer.py
    │   ├── printer.py
    │   ├── prompt_style.py
    │   ├── py.typed
    │   ├── python_input.py
    │   ├── repl.py
    │   ├── signatures.py
    │   ├── style.py
    │   ├── utils.py
    │   └── validator.py
└── tests
    └── test_dummy.py


/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
 1 | name: test
 2 | 
 3 | on:
 4 |   push: # any branch
 5 |   pull_request:
 6 |     branches: [master]
 7 | 
 8 | jobs:
 9 |   test-ubuntu:
10 |     runs-on: ubuntu-latest
11 |     strategy:
12 |       matrix:
13 |         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
14 | 
15 |     steps:
16 |       - uses: actions/checkout@v4
17 |       - uses: astral-sh/setup-uv@v5
18 |         with:
19 |           python-version: ${{ matrix.python-version }}
20 |       - name: Type Checking
21 |         run: |
22 |           uvx --with . mypy src/ptpython/
23 |           uvx --with . mypy examples/
24 |       - name: Code formatting
25 |         if: ${{ matrix.python-version == '3.13' }}
26 |         run: |
27 |           uvx ruff check .
28 |           uvx ruff format --check .
29 |       - name: Typos
30 |         if: ${{ matrix.python-version == '3.13' }}
31 |         run: |
32 |           uvx typos .
33 |       - name: Unit test
34 |         run: |
35 |           uvx --with . pytest tests/
36 |       - name: Validate README.md
37 |         if: ${{ matrix.python-version == '3.13' }}
38 |         # Ensure that the README renders correctly (required for uploading to PyPI).
39 |         run: |
40 |           uv pip install readme_renderer
41 |           python -m readme_renderer README.rst > /dev/null
42 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Byte-compiled / optimized / DLL files
 2 | __pycache__/
 3 | *.py[cod]
 4 | 
 5 | # C extensions
 6 | *.so
 7 | 
 8 | # Distribution / packaging
 9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 | 
25 | # PyInstaller
26 | #  Usually these files are written by a python script from a template
27 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
28 | *.manifest
29 | *.spec
30 | 
31 | # Installer logs
32 | pip-log.txt
33 | pip-delete-this-directory.txt
34 | 
35 | # Unit test / coverage reports
36 | htmlcov/
37 | .tox/
38 | .coverage
39 | .cache
40 | nosetests.xml
41 | coverage.xml
42 | 
43 | # Translations
44 | *.mo
45 | *.pot
46 | 
47 | # Django stuff:
48 | *.log
49 | 
50 | # Sphinx documentation
51 | docs/_build/
52 | 
53 | # PyBuilder
54 | target/
55 | 


--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
  1 | CHANGELOG
  2 | =========
  3 | 
  4 | 3.0.31: 2025-08-27
  5 | ------------------
  6 | 
  7 | Fixes:
  8 | - Add `patch_stdout_raw` parameter to `embed()`.
  9 | - Fix repl.min_brightness and repl.max_brightness config in history.
 10 | - Fix positioning of exit confirmation (compatibilitiy with latest prompt_toolkit).
 11 | 
 12 | 
 13 | 3.0.30: 2025-04-15
 14 | ------------------
 15 | 
 16 | New features:
 17 | - Show exception cause/context when printing chained exceptions.
 18 | - Reworked project layout and use pyproject.toml instead of setup.py.
 19 | 
 20 | Breaking changes:
 21 | - Drop Python 3.7 support.
 22 | 
 23 | 
 24 | 3.0.29: 2024-07-22
 25 | ------------------
 26 | 
 27 | Fixes:
 28 | - Further improve performance of dictionary completions.
 29 | 
 30 | 
 31 | 3.0.28: 2024-07-22
 32 | ------------------
 33 | 
 34 | New features:
 35 | - Custom 'exit' function to return from REPL that
 36 |   * doesn't terminate `sys.stdin` when `exit` is called (important for
 37 |     `embed()`).
 38 |   * doesn't require to be called with parentheses.
 39 | 
 40 | Fixes:
 41 | - Clean up signatures on control-c.
 42 | 
 43 | 
 44 | 3.0.27: 2024-05-27
 45 | ------------------
 46 | 
 47 | - Limit number of completions to 5k (for performance).
 48 | - Several typing fixes.
 49 | 
 50 | 
 51 | 3.0.26: 2024-02-06
 52 | ------------------
 53 | 
 54 | Fixes:
 55 | - Handle `GeneratorExit` exception when leaving the paginator.
 56 | 
 57 | 
 58 | 3.0.25: 2023-12-14
 59 | ------------------
 60 | 
 61 | Fixes:
 62 | - Fix handling of 'config file does not exist' when embedding ptpython.
 63 | 
 64 | 
 65 | 3.0.24: 2023-12-13
 66 | ------------------
 67 | 
 68 | Fixes:
 69 | - Don't show "Impossible to read config file" warnings when no config file was
 70 |   passed to `run_config()`.
 71 | - IPython integration fixes:
 72 |   * Fix top-level await in IPython.
 73 |   * Fix IPython `DeprecationWarning`.
 74 | - Output printing fixes:
 75 |   * Paginate exceptions if pagination is enabled.
 76 |   * Handle big outputs without running out of memory.
 77 | - Asyncio REPL improvements:
 78 |   * From now on, passing `--asyncio` is required to activate the asyncio-REPL.
 79 |     This will ensure that an event loop is created at the start in which we can
 80 |     run top-level await statements.
 81 |   * Use `get_running_loop()` instead of `get_event_loop()`.
 82 |   * Better handling of `SystemExit` and control-c in the async REPL.
 83 | 
 84 | 
 85 | 3.0.23: 2023-02-22
 86 | ------------------
 87 | 
 88 | Fixes:
 89 | - Don't print exception messages twice for unhandled exceptions.
 90 | - Added cursor shape support.
 91 | 
 92 | Breaking changes:
 93 | - Drop Python 3.6 support.
 94 | 
 95 | 
 96 | 3.0.22: 2022-12-06
 97 | ------------------
 98 | 
 99 | New features:
100 | - Improve rendering performance when there are many completions.
101 | 
102 | 
103 | 3.0.21: 2022-11-25
104 | ------------------
105 | 
106 | New features:
107 | - Make ptipython respect more config changes.
108 |   (See: https://github.com/prompt-toolkit/ptpython/pull/110 )
109 | - Improved performance of `DictionaryCompleter` for slow mappings.
110 | 
111 | Fixes:
112 | - Call `super()` in `PythonInputFilter`. This will prevent potentially breakage
113 |   with an upcoming prompt_toolkit change.
114 |   (See: https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1690 )
115 | - Improved type annotations.
116 | - Added `py.typed` to the `package_data`.
117 | 
118 | 
119 | 3.0.20: 2021-09-14
120 | ------------------
121 | 
122 | New features:
123 | - For `DictionaryCompleter`: show parentheses after methods.
124 | 
125 | Fixes:
126 | - Don't crash when trying to complete broken mappings in `DictionaryCompleter`.
127 | - Don't crash when an older version of `black` is installed that is not
128 |   compatible.
129 | 
130 | 
131 | 3.0.19: 2021-07-08
132 | ------------------
133 | 
134 | Fixes:
135 | - Fix handling of `SystemExit` (fixes "ValueError: I/O operation on closed
136 |   file").
137 | - Allow usage of `await` in assignment expressions or for-loops.
138 | 
139 | 
140 | 3.0.18: 2021-06-26
141 | ------------------
142 | 
143 | Fixes:
144 | - Made "black" an optional dependency.
145 | 
146 | 
147 | 3.0.17: 2021-03-22
148 | ------------------
149 | 
150 | Fixes:
151 | - Fix leaking file descriptors due to not closing the asyncio event loop after
152 |   reading input in a thread.
153 | - Fix race condition during retrieval of signatures.
154 | 
155 | 
156 | 3.0.16: 2021-02-11
157 | ------------------
158 | 
159 | (Commit 7f619e was missing in previous release.)
160 | 
161 | Fixes:
162 | - Several fixes to the completion code:
163 |   * Give dictionary completions priority over path completions.
164 |   * Always call non-fuzzy completer after fuzzy completer to prevent that some
165 |     completions were missed out if the fuzzy completer doesn't find them.
166 | 
167 | 
168 | 3.0.15: 2021-02-11
169 | ------------------
170 | 
171 | New features:
172 | - When pressing control-w, only delete characters until a punctuation.
173 | 
174 | Fixes:
175 | - Fix `AttributeError` during retrieval of signatures with type annotations.
176 | 
177 | 
178 | 3.0.14: 2021-02-10
179 | ------------------
180 | 
181 | New features:
182 | - Display of signature and completion drop down together.
183 | - If `DictionaryCompleter` is enabled, also retrieve signatures when Jedi
184 |   fails, using the same logic.
185 | - List function parameters first and private attributes at the end in the
186 |   completion menu.
187 | - Cleanup of the completion code.
188 | 
189 | Fixes:
190 | - Handle exceptions raised when `repr()` is called.
191 | - Fix leakage of `exc_info` from eval to exec call.
192 | - Fix handling of `KeyboardInterrupt` in REPL during evaluation of `__repr__`.
193 | - Fix style for signature toolbar.
194 | - Hide signature when sidebar is visible.
195 | 
196 | 
197 | 3.0.13: 2021-01-26
198 | ------------------
199 | 
200 | New features:
201 | - Added 'print all' option to pager.
202 | - Improve handling of indented code:
203 |   * Allow multiline input to be indented as a whole (we will unindent before
204 |     executing).
205 |   * Correctly visualize tabs (instead of ^I, which happens when pasted in
206 |     bracketed paste).
207 | 
208 | Fixes:
209 | - Fix line ending bug in pager.
210 | 
211 | 
212 | 3.0.12: 2021-01-24
213 | ------------------
214 | 
215 | New features:
216 | - Expose a `get_ptpython` function in the global namespace, to get programmatic
217 |   access to the REPL.
218 | - Expose `embed()` at the top level of the package. Make it possible to do
219 |   `from ptpython import embed`.
220 | 
221 | Fixes:
222 | - Properly handle exceptions when trying to access `__pt_repr__`.
223 | - Properly handle `SystemExit`.
224 | 
225 | 
226 | 3.0.11: 2021-01-20
227 | ------------------
228 | 
229 | New features:
230 | - Add support for top-level await.
231 | - Refactoring of event loop usage:
232 | 
233 |   * The ptpython input UI will now run in a separate thread. This makes it
234 |     possible to properly embed ptpython in an asyncio application, without
235 |     having to deal with nested event loops (which asyncio does not support).
236 | 
237 |   * The "eval" part doesn't anymore take place within a ptpython coroutine, so
238 |     it can spawn its own loop if needed. This also fixes `asyncio.run()` usage
239 |     in the REPL, which was broken before.
240 | 
241 | - Added syntax highlighting and autocompletion for !-style system commands.
242 | 
243 | Fixes:
244 | - Remove unexpected additional line after output.
245 | - Fix system prompt. Accept !-style inputs again.
246 | - Don't execute PYTHONSTARTUP when -i flag was given.
247 | 
248 | 
249 | 3.0.10: 2021-01-13
250 | ------------------
251 | 
252 | Fixes:
253 | - Do dictionary completion on Sequence and Mapping objects (from
254 |   collections.abc). Note that dictionary completion is still turned off by
255 |   default.
256 | 
257 | 
258 | 3.0.9: 2021-01-10
259 | -----------------
260 | 
261 | New features:
262 | - Allow replacing `PythonInput.completer` at runtime (useful for tools build on
263 |   top of ptpython).
264 | - Show REPL title in pager.
265 | 
266 | 
267 | 3.0.8: 2021-01-05
268 | -----------------
269 | 
270 | New features:
271 | - Optional output formatting using Black.
272 | - Optional pager for displaying outputs that don't fit on the screen.
273 | - Added --light-bg and --dark-bg flags to automatically optimize the brightness
274 |   of the colors according to the terminal background.
275 | - Add `PTPYTHON_CONFIG_HOME` for explicitly setting the config directory.
276 | - Show completion suffixes (like '(' for functions).
277 | 
278 | Fixes:
279 | - Fix dictionary completion on Pandas objects.
280 | - Stop using deprecated Jedi functions.
281 | 
282 | 
283 | 3.0.7: 2020-09-25
284 | -----------------
285 | 
286 | New features:
287 | - Option to show/hide private attributes during a completion
288 | - Added `insert_blank_line_after_input` option similar to
289 |   `insert_blank_line_after_output`.
290 | 
291 | Fixes:
292 | - Fixed some formatting issues of `__pt_repr__`.
293 | - Abbreviate completion meta information for dictionary completer if needed.
294 | 
295 | 
296 | 3.0.6: 2020-09-23
297 | -----------------
298 | 
299 | New features:
300 | - (Experimental) support for `__pt_repr__` methods. If objects implement this
301 |   method, this will be used to print the result in the REPL instead of the
302 |   normal `__repr__`.
303 | - Added py.typed file, to enable type checking for applications that are
304 |   embedding ptpython.
305 | 
306 | 
307 | 3.0.5: 2020-08-10
308 | -----------------
309 | 
310 | Fixes:
311 | - Handle bug in dictionary completion when numeric keys are used.
312 | 
313 | 
314 | 3.0.4: 2020-08-10
315 | -----------------
316 | 
317 | New features:
318 | - Allow leading whitespace before single line expressions.
319 | - Show full syntax error in validator.
320 | - Added `vi_start_in_navigation_mode` and `vi_keep_last_used_mode` options.
321 | 
322 | Fixes:
323 | - Improved dictionary completion: handle keys that contain spaces and don't
324 |   recognize numbers as variable names.
325 | - Fix in exit confirmation.
326 | 
327 | 
328 | 3.0.3: 2020-07-10
329 | -----------------
330 | 
331 | Fixes:
332 | - Sort attribute names for `DictionaryCompleter` and move underscored
333 |   attributes to the end.
334 | - Handle unhandled exceptions in `get_compiler_flags`.
335 | - Improved `run_async` code.
336 | - Fix --version parameter.
337 | 
338 | 
339 | 3.0.2: 2020-04-14
340 | -----------------
341 | 
342 | New features:
343 | - Improved custom dictionary completion:
344 |     * Also complete list indexes.
345 |     * Also complete attributes after doing a dictionary lookup.
346 |     * Also complete iterators in a for-loop.
347 | - Added a 'title' option, so that applications embedding ptpython can set a
348 |   title in the status bar.
349 | 
350 | 
351 | 3.0.1: 2020-02-24
352 | -----------------
353 | 
354 | - Fix backwards-compatibility of the `run_config` function. (used by
355 |   django-extensions).
356 | - Fix input mode in status bar for block selection.
357 | 
358 | 
359 | 3.0.0: 2020-01-29
360 | -----------------
361 | 
362 | Upgrade to prompt_toolkit 3.0.
363 | Requires at least Python 3.6.
364 | 
365 | New features:
366 | - Uses XDG base directory specification.
367 | 
368 | 
369 | 2.0.5: 2019-10-09
370 | -----------------
371 | 
372 | New features:
373 | - Added dictionary completer (off by default).
374 | - Added fuzzy completion (off by default).
375 | - Highlight keywords in completion dropdown menu.
376 | - Enable universal wheels.
377 | 
378 | Fixes:
379 | - Fixed embedding repl as asyncio coroutine.
380 | - Fixed patching stdout in embedded repl.
381 | - Fixed ResourceWarning in setup.py.
382 | 
383 | 
384 | 2.0.4: 2018-10-30
385 | -----------------
386 | 
387 | - Fixed ptipython.
388 | - Fixed config: setting of color depth.
389 | - Fixed auto-suggest key bindings.
390 | - Fixed Control-D key binding for exiting REPL when (confirm_exit=False).
391 | - Correctly focus/unfocus sidebar.
392 | - Fixed open_in_editor and suspend key bindings.
393 | 
394 | 
395 | 2.0.3: 2018-10-12
396 | -----------------
397 | 
398 | - Allow changing the min/max brightness.
399 | - Some changes for compatibility with the latest prompt_toolkit.
400 | 
401 | 2.0.2: 2018-09-30
402 | -----------------
403 | 
404 | Fixes:
405 | - Don't crash the history browser when there was no history.
406 | - Set last exception in the sys module, when an exception was raised.
407 | - Require prompt_toolkit 2.0.5.
408 | 
409 | 
410 | 2.0.1: 2018-09-30
411 | -----------------
412 | 
413 | Upgrade to prompt_toolkit 2.0.x.
414 | 
415 | 
416 | 0.36: 2016-10-16
417 | ----------------
418 | 
419 | New features:
420 | - Support for editing in Vi block mode. (Only enabled for
421 |   prompt_toolkit>=1.0.8.)
422 | 
423 | Fixes:
424 | - Handle two Jedi crashes. (GitHub ptpython issues #136 and #91.)
425 | 
426 | 0.35: 2016-07-19
427 | ----------------
428 | 
429 | Fixes:
430 | - Fix in completer. Don't hang when pasting a long string with many
431 |   backslashes.
432 | - Fix Python2 bug: crash when filenames contain non-ascii characters.
433 | - Added `pt[i]pythonX` and `pt[i]pythonX.X` commands.
434 | - Compatibility with IPython 5.0.
435 | 
436 | 
437 | 0.34: 2016-05-06
438 | ---------------
439 | 
440 | Bugfix in ptipython: reset input buffer before every read in run().
441 | 
442 | 
443 | 0.33: 2016-05-05
444 | ---------------
445 | 
446 | Upgrade to prompt_toolkit 1.0.0
447 | 
448 | Improvements:
449 | - Unindent after typing 'pass'.
450 | - Make it configurable whether or not a blank line has to be inserted after the output.
451 | 
452 | 
453 | 0.32: 2016-03-29
454 | ---------------
455 | 
456 | Fixes:
457 | - Fixed bug when PYTHONSTARTUP was not found.
458 | - Support $PYTHONSTARTUP for ptipython.
459 | 
460 | 
461 | 0.31: 2016-03-14
462 | ---------------
463 | 
464 | Upgrade to prompt_toolkit 0.60
465 | 
466 | 
467 | 0.30: 2016-02-27
468 | ---------------
469 | 
470 | Upgrade to prompt_toolkit 0.59
471 | 
472 | 
473 | 0.29: 2016-02-24
474 | ----------------
475 | 
476 | Upgrade to prompt_toolkit 0.58
477 | 
478 | New features:
479 | - Improved mouse support
480 | 
481 | 
482 | 0.28: 2016-01-04
483 | ----------------
484 | 
485 | Upgrade to prompt_toolkit 0.57
486 | 
487 | 
488 | 0.27: 2016-01-03
489 | ----------------
490 | 
491 | Upgrade to prompt_toolkit 0.56
492 | 
493 | 
494 | 0.26: 2016-01-03
495 | ----------------
496 | 
497 | Upgrade to prompt_toolkit 0.55
498 | 
499 | Fixes:
500 | - Handle several bugs in Jedi.
501 | - Correctly handle sys.argv when pt(i)python is started with --interactive.
502 | - Support for 24bit true color.
503 | - Take compiler flags into account for ptipython.
504 | 
505 | 
506 | 0.25: 2015-10-29
507 | ----------------
508 | 
509 | Upgrade to prompt_toolkit 0.54
510 | 
511 | Fixes:
512 | - Consider input multiline when there's a colon at the end of the line.
513 | - Handle bug in Jedi.
514 | - Enable search bindings in history browser.
515 | 
516 | 
517 | 0.24: 2015-09-24
518 | ----------------
519 | 
520 | Upgrade to prompt_toolkit 0.52
521 | 
522 | 
523 | 0.23: 2015-09-24
524 | ----------------
525 | 
526 | Upgrade to prompt_toolkit 0.51
527 | 
528 | New features:
529 | - Mouse support
530 | - Fish style auto suggestion.
531 | - Optionally disabling of line wraps.
532 | - Use Python3Lexer for Python 3.
533 | 
534 | 
535 | 0.22: 2015-09-06
536 | ----------------
537 | 
538 | Upgrade to prompt_toolkit 0.50
539 | 
540 | Fixes:
541 | - Correctly accept file parameter in the print function of
542 |   asyncssh_repl.ReplSSHServerSession.
543 | - Create config directory if it doesn't exist yet (For IPython entry point.)
544 | 
545 | New features:
546 | - Implementation of history-selection: a tool to select lines from the history.
547 | - Make exit message configurable.
548 | - Improve start-up time: Lazy load completer grammar and lazy-import Jedi.
549 | - Make multi-column the default completion visualisation.
550 | - Implementation of a custom prompts. In_tokens and out_tokens can be
551 |   customized.
552 | - Made an option to show/hide highlighting for matching parenthesis.
553 | - Some styling improvements.
554 | 
555 | 
556 | 0.21: 2015-08-08
557 | ---------------
558 | 
559 | Upgrade to prompt_toolkit 0.46
560 | 
561 | Fixes:
562 | - Correctly add current directory to sys.path.
563 | - Only show search highlighting when the search is the current input buffer.
564 | - Styling fix.
565 | 
566 | 
567 | 0.20: 2015-07-30
568 | ---------------
569 | 
570 | Upgrade to prompt_toolkit 0.45
571 | 
572 | 
573 | 0.19: 2015-07-30
574 | ---------------
575 | 
576 | Upgrade to prompt_toolkit 0.44
577 | 
578 | New features:
579 | - Added --interactive option for ptipython.
580 | - A few style improvements.
581 | 
582 | 
583 | 0.18: 2015-07-15
584 | ---------------
585 | 
586 | Fixes:
587 | - Python 2.6 compatibility.
588 | 
589 | 
590 | 0.17: 2015-07-15
591 | ---------------
592 | 
593 | Upgrade to prompt_toolkit 0.43
594 | 
595 | New features:
596 | - Integration with Tk eventloop. (This makes turtle and other Tk stuff work
597 |   again from the REPL.)
598 | - Multi column completion visualisation.
599 | 
600 | 
601 | 0.16: 2015-06-25
602 | ---------------
603 | 
604 | Upgrade to prompt_toolkit 0.42
605 | 
606 | Fixes:
607 | - Workaround for Jedi bug. (Signatures of functions with keyword-only arguments.)
608 | - Correctly show traceback on Python 3.
609 | - Better styling of the options sidebar.
610 | 
611 | New features:
612 | - Exit REPL when input starts with Control-Z.
613 | - Set terminal title.
614 | - Display help text in options sidebar.
615 | - Better colorscheme for Windows.
616 | 
617 | 
618 | 0.15: 2015-06-20
619 | ---------------
620 | 
621 | Upgrade to prompt_toolkit 0.41
622 | 
623 | Fixes:
624 | - Correct tokens for IPython prompt.
625 | - Syntax fix in asyncssh_repl.
626 | 
627 | 
628 | 0.14: 2015-06-16
629 | ---------------
630 | 
631 | Fix:
632 | - Correct dependency for prompt_toolkit.
633 | 
634 | 0.13: 2015-06-15
635 | ---------------
636 | 
637 | New features:
638 | - Upgrade to prompt_toolkit 0.40
639 | - Options sidebar.
640 | - Custom color schemes.
641 | - Syntax highlighting of the output.
642 | - Input validation can now be turned off.
643 | - Determine compiler flags dynamically. (Fixes importing unicode_literals).
644 | - Exit confirmation and dialog.
645 | - Autocompletion of IPython %cat command.
646 | - Correctly render tracebacks on Windows.
647 | 
648 | 0.12: 2015-06-04
649 | ---------------
650 | 
651 | Upgrade to prompt_toolkit 0.39
652 | 
653 | 0.11: 2015-05-31
654 | ---------------
655 | 
656 | New features:
657 | - Upgrade to prompt-toolkit 0.38.
658 | - Upgrade to Jedi 0.9.0
659 | - Fixed default globals for repl (correct __name, __builtins__, etc...)
660 | - Display deprecation warnings in the REPL.
661 | - Added configuration support.
662 | - Added asyncio-ssh-python-embed example.
663 | 
664 | 
665 | 0.10: 2015-05-11
666 | ---------------
667 | 
668 | Upgrade to prompt-toolkit 0.37.
669 | 
670 | 
671 | 0.9: 2015-05-07
672 | ---------------
673 | 
674 | Upgrade to prompt-toolkit 0.35.
675 | 
676 | 
677 | 0.8: 2015-04-26
678 | ---------------
679 | 
680 | Fixed:
681 | - eval() doesn't run using unicode_literals anymore.
682 | - Upgrade to prompt-toolkit 0.34.
683 | 
684 | 
685 | 0.7: 2015-04-25
686 | ---------------
687 | 
688 | Fixed:
689 | - Upgrade to prompt-toolkit 0.33.
690 | 
691 | New features:
692 | - Added complete_while_typing option.
693 | 
694 | 
695 | 0.6: 2015-04-22
696 | ---------------
697 | 
698 | Fixed:
699 | - Upgrade to prompt-toolkit 0.32 which has many new features.
700 | 
701 | Changes:
702 | - Removal of tab pages + clean up.
703 | - Pressing enter twice will now always automatically execute the input.
704 | - Improved Control-D key binding.
705 | - Hide docstring by default.
706 | 
707 | 
708 | 0.5: 2015-01-30
709 | ---------------
710 | 
711 | Fixed:
712 | - Tab autocompletion on first line.
713 | - Upgrade to prompt-toolkit 0.31
714 | 
715 | New features:
716 | - Simple autocompletion for IPython magics.
717 | 
718 | 0.4: 2015-01-26
719 | ---------------
720 | 
721 | Fixed:
722 | - Upgrade to prompt-toolkit 0.30
723 | 
724 | 0.3: 2015-01-25
725 | ---------------
726 | 
727 | Fixed:
728 | - Upgrade to prompt-toolkit 0.28
729 | 
730 | 0.2: 2015-01-25
731 | ---------------
732 | 
733 | Moved ptpython code from prompt-toolkit inside this repository.
734 | 
735 | 0.1: 2014-09-29
736 | ---------------
737 | 
738 | Initial ptpython version. (Source code was still in the
739 | prompt-toolkit repository itself.)
740 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2015-2023, Jonathan Slenders
 2 | All rights reserved.
 3 | 
 4 | Redistribution and use in source and binary forms, with or without modification,
 5 | are permitted provided that the following conditions are met:
 6 | 
 7 | * Redistributions of source code must retain the above copyright notice, this
 8 |   list of conditions and the following disclaimer.
 9 | 
10 | * Redistributions in binary form must reproduce the above copyright notice, this
11 |   list of conditions and the following disclaimer in the documentation and/or
12 |   other materials provided with the distribution.
13 | 
14 | * Neither the name of the {organization} nor the names of its
15 |   contributors may be used to endorse or promote products derived from
16 |   this software without specific prior written permission.
17 | 
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 | 


--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *rst LICENSE CHANGELOG MANIFEST.in
2 | recursive-include examples *.py
3 | prune examples/sample?/build
4 | 


--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
  1 | ptpython
  2 | ========
  3 | 
  4 | |Build Status|  |PyPI|  |License|
  5 | 
  6 | *A better Python REPL*
  7 | 
  8 | ::
  9 | 
 10 |     pip install ptpython
 11 | 
 12 | .. image :: ./docs/images/example1.png
 13 | 
 14 | Ptpython is an advanced Python REPL. It should work on all
 15 | Python versions from 2.6 up to 3.11 and work cross platform (Linux,
 16 | BSD, OS X and Windows).
 17 | 
 18 | Note: this version of ptpython requires at least Python 3.6. Install ptpython
 19 | 2.0.5 for older Python versions.
 20 | 
 21 | 
 22 | Installation
 23 | ************
 24 | 
 25 | Install it using pip:
 26 | 
 27 | ::
 28 | 
 29 |     pip install ptpython
 30 | 
 31 | Start it by typing ``ptpython``.
 32 | 
 33 | 
 34 | Features
 35 | ********
 36 | 
 37 | - Syntax highlighting.
 38 | - Multiline editing (the up arrow works).
 39 | - Autocompletion.
 40 | - Mouse support. [1]
 41 | - Support for color schemes.
 42 | - Support for `bracketed paste <https://cirw.in/blog/bracketed-paste>`_ [2].
 43 | - Both Vi and Emacs key bindings.
 44 | - Support for double width (Chinese) characters.
 45 | - ... and many other things.
 46 | 
 47 | 
 48 | [1] Disabled by default. (Enable in the menu.)
 49 | 
 50 | [2] If the terminal supports it (most terminals do), this allows pasting
 51 | without going into paste mode. It will keep the indentation.
 52 | 
 53 | Command Line Options
 54 | ********************
 55 | 
 56 | The help menu shows basic command-line options.
 57 | 
 58 | ::
 59 | 
 60 |     $ ptpython --help
 61 |     usage: ptpython [-h] [--vi] [-i] [--light-bg] [--dark-bg] [--config-file CONFIG_FILE]
 62 |                     [--history-file HISTORY_FILE] [-V]
 63 |                     [args ...]
 64 | 
 65 |     ptpython: Interactive Python shell.
 66 | 
 67 |     positional arguments:
 68 |       args                  Script and arguments
 69 | 
 70 |     optional arguments:
 71 |       -h, --help            show this help message and exit
 72 |       --vi                  Enable Vi key bindings
 73 |       -i, --interactive     Start interactive shell after executing this file.
 74 |       --asyncio             Run an asyncio event loop to support top-level "await".
 75 |       --light-bg            Run on a light background (use dark colors for text).
 76 |       --dark-bg             Run on a dark background (use light colors for text).
 77 |       --config-file CONFIG_FILE
 78 |                             Location of configuration file.
 79 |       --history-file HISTORY_FILE
 80 |                             Location of history file.
 81 |       -V, --version         show program's version number and exit
 82 | 
 83 |     environment variables:
 84 |       PTPYTHON_CONFIG_HOME: a configuration directory to use
 85 |       PYTHONSTARTUP: file executed on interactive startup (no default)
 86 | 
 87 | 
 88 | __pt_repr__: A nicer repr with colors
 89 | *************************************
 90 | 
 91 | When classes implement a ``__pt_repr__`` method, this will be used instead of
 92 | ``__repr__`` for printing. Any `prompt_toolkit "formatted text"
 93 | <https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html>`_
 94 | can be returned from here. In order to avoid writing a ``__repr__`` as well,
 95 | the ``ptpython.utils.ptrepr_to_repr`` decorator can be applied. For instance:
 96 | 
 97 | .. code:: python
 98 | 
 99 |     from ptpython.utils import ptrepr_to_repr
100 |     from prompt_toolkit.formatted_text import HTML
101 | 
102 |     @ptrepr_to_repr
103 |     class MyClass:
104 |         def __pt_repr__(self):
105 |             return HTML('<yellow>Hello world!</yellow>')
106 | 
107 | More screenshots
108 | ****************
109 | 
110 | The configuration menu:
111 | 
112 | .. image :: ./docs/images/ptpython-menu.png
113 | 
114 | The history page and its help:
115 | 
116 | .. image :: ./docs/images/ptpython-history-help.png
117 | 
118 | Autocompletion:
119 | 
120 | .. image :: ./docs/images/file-completion.png
121 | 
122 | 
123 | Embedding the REPL
124 | ******************
125 | 
126 | Embedding the REPL in any Python application is easy:
127 | 
128 | .. code:: python
129 | 
130 |     from ptpython.repl import embed
131 |     embed(globals(), locals())
132 | 
133 | You can make ptpython your default Python REPL by creating a `PYTHONSTARTUP file
134 | <https://docs.python.org/3/tutorial/appendix.html#the-interactive-startup-file>`_ containing code
135 | like this:
136 | 
137 | .. code:: python
138 | 
139 |    import sys
140 |    try:
141 |        from ptpython.repl import embed
142 |    except ImportError:
143 |        print("ptpython is not available: falling back to standard prompt")
144 |    else:
145 |        sys.exit(embed(globals(), locals()))
146 | 
147 | Note config file support currently only works when invoking `ptpython` directly.
148 | That it, the config file will be ignored when embedding ptpython in an application.
149 | 
150 | Multiline editing
151 | *****************
152 | 
153 | Multi-line editing mode will automatically turn on when you press enter after a
154 | colon.
155 | 
156 | To execute the input in multi-line mode, you can either press ``Alt+Enter``, or
157 | ``Esc`` followed by ``Enter``. (If you want the first to work in the OS X
158 | terminal, you have to check the "Use option as meta key" checkbox in your
159 | terminal settings. For iTerm2, you have to check "Left option acts as +Esc" in
160 | the options.)
161 | 
162 | .. image :: ./docs/images/multiline.png
163 | 
164 | 
165 | Syntax validation
166 | *****************
167 | 
168 | Before execution, ``ptpython`` will see whether the input is syntactically
169 | correct Python code. If not, it will show a warning, and move the cursor to the
170 | error.
171 | 
172 | .. image :: ./docs/images/validation.png
173 | 
174 | 
175 | Asyncio REPL and top level await
176 | ********************************
177 | 
178 | In order to get top-level ``await`` support, start ptpython as follows:
179 | 
180 | .. code::
181 | 
182 |    ptpython --asyncio
183 | 
184 | This will spawn an asyncio event loop and embed the async REPL in the event
185 | loop. After this, top-level await will work and statements like ``await
186 | asyncio.sleep(10)`` will execute.
187 | 
188 | 
189 | Additional features
190 | *******************
191 | 
192 | Running system commands: Press ``Meta-!`` in Emacs mode or just ``!`` in Vi
193 | navigation mode to see the "Shell command" prompt. There you can enter system
194 | commands without leaving the REPL.
195 | 
196 | Selecting text: Press ``Control+Space`` in Emacs mode or ``V`` (major V) in Vi
197 | navigation mode.
198 | 
199 | 
200 | Configuration
201 | *************
202 | 
203 | It is possible to create a ``config.py`` file to customize configuration.
204 | ptpython will look in an appropriate platform-specific directory via `appdirs
205 | <https://pypi.org/project/appdirs/>`. See the ``appdirs`` documentation for the
206 | precise location for your platform. A ``PTPYTHON_CONFIG_HOME`` environment
207 | variable, if set, can also be used to explicitly override where configuration
208 | is looked for.
209 | 
210 | Have a look at this example to see what is possible:
211 | `config.py <https://github.com/prompt-toolkit/ptpython/blob/master/examples/ptpython_config/config.py>`_
212 | 
213 | Note config file support currently only works when invoking `ptpython` directly.
214 | That it, the config file will be ignored when embedding ptpython in an application.
215 | 
216 | 
217 | IPython support
218 | ***************
219 | 
220 | Run ``ptipython`` (prompt_toolkit - IPython), to get a nice interactive shell
221 | with all the power that IPython has to offer, like magic functions and shell
222 | integration. Make sure that IPython has been installed. (``pip install
223 | ipython``)
224 | 
225 | .. image :: ./docs/images/ipython.png
226 | 
227 | This is also available for embedding:
228 | 
229 | .. code:: python
230 | 
231 |     from ptpython.ipython import embed
232 |     embed(globals(), locals())
233 | 
234 | 
235 | Django support
236 | **************
237 | 
238 | `django-extensions <https://github.com/django-extensions/django-extensions>`_
239 | has a ``shell_plus`` management command. When ``ptpython`` has been installed,
240 | it will by default use ``ptpython`` or ``ptipython``.
241 | 
242 | 
243 | PDB
244 | ***
245 | 
246 | There is an experimental PDB replacement: `ptpdb
247 | <https://github.com/jonathanslenders/ptpdb>`_.
248 | 
249 | 
250 | Windows support
251 | ***************
252 | 
253 | ``prompt_toolkit`` and ``ptpython`` works better on Linux and OS X than on
254 | Windows. Some things might not work, but it is usable:
255 | 
256 | .. image :: ./docs/images/windows.png
257 | 
258 | Windows terminal integration
259 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
260 | 
261 | If you are using the `Windows Terminal <https://aka.ms/terminal>`_ and want to 
262 | integrate ``ptpython`` as a profile, go to *Settings -> Open JSON file* and add the
263 | following profile under *profiles.list*:
264 | 
265 | .. code-block:: JSON
266 | 
267 |     {
268 |         "commandline": "%SystemRoot%\\System32\\cmd.exe /k ptpython",
269 |         "guid": "{f91d49a3-741b-409c-8a15-c4360649121f}",
270 |         "hidden": false,
271 |         "icon": "https://upload.wikimedia.org/wikipedia/commons/e/e6/Python_Windows_interpreter_icon_2006%E2%80%932016_Tiny.png",
272 |         "name": "ptpython@cmd"
273 |     }
274 | 
275 | FAQ
276 | ***
277 | 
278 | **Q**: The ``Ctrl-S`` forward search doesn't work and freezes my terminal.
279 | 
280 | **A**: Try to run ``stty -ixon`` in your terminal to disable flow control.
281 | 
282 | **Q**: The ``Meta``-key doesn't work.
283 | 
284 | **A**: For some terminals you have to enable the Alt-key to act as meta key, but you
285 | can also type ``Escape`` before any key instead.
286 | 
287 | 
288 | Alternatives
289 | ************
290 | 
291 | - `BPython <http://bpython-interpreter.org/downloads.html>`_
292 | - `IPython <https://ipython.org/>`_
293 | 
294 | If you find another alternative, you can create an issue and we'll list it
295 | here. If you find a nice feature somewhere that is missing in ``ptpython``,
296 | also create a GitHub issue and maybe we'll implement it.
297 | 
298 | 
299 | Special thanks to
300 | *****************
301 | 
302 | - `Pygments <http://pygments.org/>`_: Syntax highlighter.
303 | - `Jedi <http://jedi.jedidjah.ch/en/latest/>`_: Autocompletion library.
304 | - `wcwidth <https://github.com/jquast/wcwidth>`_: Determine columns needed for a wide characters.
305 | - `prompt_toolkit <http://github.com/jonathanslenders/python-prompt-toolkit>`_ for the interface.
306 | 
307 | .. |Build Status| image:: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml/badge.svg
308 |     :target: https://github.com/prompt-toolkit/ptpython/actions/workflows/test.yaml
309 | 
310 | .. |License| image:: https://img.shields.io/github/license/prompt-toolkit/ptpython.svg
311 |     :target: https://github.com/prompt-toolkit/ptpython/blob/master/LICENSE
312 | 
313 | .. |PyPI| image:: https://img.shields.io/pypi/v/ptpython.svg
314 |     :target: https://pypi.org/project/ptpython/
315 |     :alt: Latest Version
316 | 


--------------------------------------------------------------------------------
/docs/concurrency-challenges.rst:
--------------------------------------------------------------------------------
 1 | 
 2 | Concurrency-related challenges regarding embedding of ptpython in asyncio code
 3 | ==============================================================================
 4 | 
 5 | Things we want to be possible
 6 | -----------------------------
 7 | 
 8 | - Embed blocking ptpython in non-asyncio code (the normal use case).
 9 | - Embed blocking ptpython in asyncio code (the event loop will block).
10 | - Embed awaitable ptpython in asyncio code (the loop will continue).
11 | - React to resize events (SIGWINCH).
12 | - Support top-level await.
13 | - Be able to patch_stdout, so that logging messages from another thread will be
14 |   printed above the prompt.
15 | - It should be possible to handle `KeyboardInterrupt` during evaluation of an
16 |   expression.
17 | - The "eval" should happen in the same thread from where embed() was called.
18 | 
19 | 
20 | Limitations of asyncio/python
21 | -----------------------------
22 | 
23 | - We can only listen to SIGWINCH signal (resize) events in the main thread.
24 | 
25 | - Usage of Control-C for triggering a `KeyboardInterrupt` only works for code
26 |   running in the main thread. (And only if the terminal was not set in raw
27 |   input mode).
28 | 
29 | - Spawning a new event loop from within a coroutine, that's being executed in
30 |   an existing event loop is not allowed in asyncio. We can however spawn any
31 |   event loop in a separate thread, and wait for that thread to finish.
32 | 
33 | - For patch_stdout to work correctly, we have to know what prompt_toolkit
34 |   application is running on the terminal, then tell that application to print
35 |   the output and redraw itself.
36 | 
37 | 
38 | Additional challenges for IPython
39 | ---------------------------------
40 | 
41 | IPython supports integration of 3rd party event loops (for various GUI
42 | toolkits). These event loops are supposed to continue running while we are
43 | prompting for input. In an asyncio environment, it means that there are
44 | situations where we have to juggle three event loops:
45 | 
46 | - The asyncio loop in which the code was embedded.
47 | - The asyncio loop from the prompt.
48 | - The 3rd party GUI loop. 
49 | 
50 | Approach taken in ptpython 3.0.11
51 | ---------------------------------
52 | 
53 | For ptpython, the most reliable solution is to to run the prompt_toolkit input
54 | prompt in a separate background thread. This way it can use its own asyncio
55 | event loop without ever having to interfere with whatever runs in the main
56 | thread.
57 | 
58 | Then, depending on how we embed, we do the following:
59 | When a normal blocking embed is used:
60 |     * We start the UI thread for the input, and do a blocking wait on
61 |       `thread.join()` here.
62 |     * The "eval" happens in the main thread.
63 |     * The "print" happens also in the main thread. Unless a pager is shown,
64 |       which is also a prompt_toolkit application, then the pager itself is runs
65 |       also in another thread, similar to the way we do the input.
66 | 
67 | When an awaitable embed is used, for embedding in a coroutine, but having the
68 | event loop continue:
69 |     * We run the input method from the blocking embed in an asyncio executor
70 |       and do an `await loop.run_in_executor(...)`.
71 |     * The "eval" happens again in the main thread.
72 |     * "print" is also similar, except that the pager code (if used) runs in an
73 |       executor too.
74 | 
75 | This means that the prompt_toolkit application code will always run in a
76 | different thread. It means it won't be able to respond to SIGWINCH (window
77 | resize events), but prompt_toolkit's 3.0.11 has now terminal size polling which
78 | solves this.
79 | 
80 | Control-C key presses won't interrupt the main thread while we wait for input,
81 | because the prompt_toolkit application turns the terminal in raw mode, while
82 | it's reading, which means that it will receive control-c key presses as raw
83 | data in its own thread.
84 | 
85 | Top-level await works in most situations as expected.
86 | - If a blocking embed is used. We execute ``loop.run_until_complete(code)``.
87 |   This assumes that the blocking embed is not used in a coroutine of a running
88 |   event loop, otherwise, this will attempt to start a nested event loop, which
89 |   asyncio does not support. In that case we will get an exception.
90 | - If an awaitable embed is used. We literally execute ``await code``. This will
91 |   integrate nicely in the current event loop.
92 | 


--------------------------------------------------------------------------------
/docs/images/example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/example1.png


--------------------------------------------------------------------------------
/docs/images/file-completion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/file-completion.png


--------------------------------------------------------------------------------
/docs/images/ipython.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/ipython.png


--------------------------------------------------------------------------------
/docs/images/multiline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/multiline.png


--------------------------------------------------------------------------------
/docs/images/ptpython-history-help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/ptpython-history-help.png


--------------------------------------------------------------------------------
/docs/images/ptpython-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/ptpython-menu.png


--------------------------------------------------------------------------------
/docs/images/ptpython.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/ptpython.png


--------------------------------------------------------------------------------
/docs/images/validation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/validation.png


--------------------------------------------------------------------------------
/docs/images/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/docs/images/windows.png


--------------------------------------------------------------------------------
/examples/asyncio-python-embed.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """
 3 | (Python >3.3)
 4 | 
 5 | This is an example of how we can embed a Python REPL into an asyncio
 6 | application. In this example, we have one coroutine that runs in the
 7 | background, prints some output and alters a global state. The REPL, which runs
 8 | inside another coroutine can access and change this global state, interacting
 9 | with the running asyncio application.
10 | The ``patch_stdout`` option makes sure that when another coroutine is writing
11 | to stdout, it won't break the input line, but instead writes nicely above the
12 | prompt.
13 | """
14 | 
15 | import asyncio
16 | 
17 | from ptpython.repl import embed
18 | 
19 | loop = asyncio.get_event_loop()
20 | counter = [0]
21 | 
22 | 
23 | async def print_counter() -> None:
24 |     """
25 |     Coroutine that prints counters and saves it in a global variable.
26 |     """
27 |     while True:
28 |         print(f"Counter: {counter[0]}")
29 |         counter[0] += 1
30 |         await asyncio.sleep(3)
31 | 
32 | 
33 | async def interactive_shell() -> None:
34 |     """
35 |     Coroutine that starts a Python REPL from which we can access the global
36 |     counter variable.
37 |     """
38 |     print(
39 |         'You should be able to read and update the "counter[0]" variable from this shell.'
40 |     )
41 |     try:
42 |         await embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True)
43 |     except EOFError:
44 |         # Stop the loop when quitting the repl. (Ctrl-D press.)
45 |         loop.stop()
46 | 
47 | 
48 | async def main() -> None:
49 |     asyncio.create_task(print_counter())
50 |     await interactive_shell()
51 | 
52 | 
53 | if __name__ == "__main__":
54 |     asyncio.run(main())
55 | 


--------------------------------------------------------------------------------
/examples/asyncio-ssh-python-embed.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """
 3 | Example of running the Python REPL through an SSH connection in an asyncio process.
 4 | This requires Python 3, asyncio and asyncssh.
 5 | 
 6 | Run this example and then SSH to localhost, port 8222.
 7 | """
 8 | 
 9 | import asyncio
10 | import logging
11 | 
12 | import asyncssh
13 | 
14 | from ptpython.contrib.asyncssh_repl import ReplSSHServerSession
15 | 
16 | logging.basicConfig()
17 | logging.getLogger().setLevel(logging.INFO)
18 | 
19 | 
20 | class MySSHServer(asyncssh.SSHServer):
21 |     """
22 |     Server without authentication, running `ReplSSHServerSession`.
23 |     """
24 | 
25 |     def __init__(self, get_namespace):
26 |         self.get_namespace = get_namespace
27 | 
28 |     def begin_auth(self, username):
29 |         # No authentication.
30 |         return False
31 | 
32 |     def session_requested(self):
33 |         return ReplSSHServerSession(self.get_namespace)
34 | 
35 | 
36 | async def main(port: int = 8222) -> None:
37 |     """
38 |     Example that starts the REPL through an SSH server.
39 |     """
40 |     # Namespace exposed in the REPL.
41 |     environ = {"hello": "world"}
42 | 
43 |     # Start SSH server.
44 |     def create_server() -> MySSHServer:
45 |         return MySSHServer(lambda: environ)
46 | 
47 |     print(f"Listening on: {port}")
48 |     print(f'To connect, do "ssh localhost -p {port}"')
49 | 
50 |     await asyncssh.create_server(
51 |         create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"]
52 |     )
53 |     await asyncio.Future()  # Wait forever.
54 | 
55 | 
56 | if __name__ == "__main__":
57 |     asyncio.run(main())
58 | 


--------------------------------------------------------------------------------
/examples/ptpython_config/config.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Configuration example for ``ptpython``.
  3 | 
  4 | Copy this file to $XDG_CONFIG_HOME/ptpython/config.py
  5 | On Linux, this is: ~/.config/ptpython/config.py
  6 | On macOS, this is: ~/Library/Application Support/ptpython/config.py
  7 | """
  8 | 
  9 | from prompt_toolkit.filters import ViInsertMode
 10 | from prompt_toolkit.key_binding.key_processor import KeyPress
 11 | from prompt_toolkit.keys import Keys
 12 | from prompt_toolkit.styles import Style
 13 | 
 14 | from ptpython.layout import CompletionVisualisation
 15 | 
 16 | __all__ = ["configure"]
 17 | 
 18 | 
 19 | def configure(repl):
 20 |     """
 21 |     Configuration method. This is called during the start-up of ptpython.
 22 | 
 23 |     :param repl: `PythonRepl` instance.
 24 |     """
 25 |     # Show function signature (bool).
 26 |     repl.show_signature = True
 27 | 
 28 |     # Show docstring (bool).
 29 |     repl.show_docstring = False
 30 | 
 31 |     # Show the "[Meta+Enter] Execute" message when pressing [Enter] only
 32 |     # inserts a newline instead of executing the code.
 33 |     repl.show_meta_enter_message = True
 34 | 
 35 |     # Show completions. (NONE, POP_UP, MULTI_COLUMN or TOOLBAR)
 36 |     repl.completion_visualisation = CompletionVisualisation.POP_UP
 37 | 
 38 |     # When CompletionVisualisation.POP_UP has been chosen, use this
 39 |     # scroll_offset in the completion menu.
 40 |     repl.completion_menu_scroll_offset = 0
 41 | 
 42 |     # Show line numbers (when the input contains multiple lines.)
 43 |     repl.show_line_numbers = False
 44 | 
 45 |     # Show status bar.
 46 |     repl.show_status_bar = True
 47 | 
 48 |     # When the sidebar is visible, also show the help text.
 49 |     repl.show_sidebar_help = True
 50 | 
 51 |     # Swap light/dark colors on or off
 52 |     repl.swap_light_and_dark = False
 53 | 
 54 |     # Highlight matching parentheses.
 55 |     repl.highlight_matching_parenthesis = True
 56 | 
 57 |     # Line wrapping. (Instead of horizontal scrolling.)
 58 |     repl.wrap_lines = True
 59 | 
 60 |     # Mouse support.
 61 |     repl.enable_mouse_support = True
 62 | 
 63 |     # Complete while typing. (Don't require tab before the
 64 |     # completion menu is shown.)
 65 |     repl.complete_while_typing = True
 66 | 
 67 |     # Fuzzy and dictionary completion.
 68 |     repl.enable_fuzzy_completion = False
 69 |     repl.enable_dictionary_completion = False
 70 | 
 71 |     # Vi mode.
 72 |     repl.vi_mode = False
 73 | 
 74 |     # Enable the modal cursor (when using Vi mode). Other options are 'Block', 'Underline',  'Beam',  'Blink under', 'Blink block', and 'Blink beam'
 75 |     repl.cursor_shape_config = "Modal (vi)"
 76 | 
 77 |     # Paste mode. (When True, don't insert whitespace after new line.)
 78 |     repl.paste_mode = False
 79 | 
 80 |     # Use the classic prompt. (Display '>>>' instead of 'In [1]'.)
 81 |     repl.prompt_style = "classic"  # 'classic' or 'ipython'
 82 | 
 83 |     # Don't insert a blank line after the output.
 84 |     repl.insert_blank_line_after_output = False
 85 | 
 86 |     # History Search.
 87 |     # When True, going back in history will filter the history on the records
 88 |     # starting with the current input. (Like readline.)
 89 |     # Note: When enable, please disable the `complete_while_typing` option.
 90 |     #       otherwise, when there is a completion available, the arrows will
 91 |     #       browse through the available completions instead of the history.
 92 |     repl.enable_history_search = False
 93 | 
 94 |     # Enable auto suggestions. (Pressing right arrow will complete the input,
 95 |     # based on the history.)
 96 |     repl.enable_auto_suggest = False
 97 | 
 98 |     # Enable open-in-editor. Pressing C-x C-e in emacs mode or 'v' in
 99 |     # Vi navigation mode will open the input in the current editor.
100 |     repl.enable_open_in_editor = True
101 | 
102 |     # Enable system prompt. Pressing meta-! will display the system prompt.
103 |     # Also enables Control-Z suspend.
104 |     repl.enable_system_bindings = True
105 | 
106 |     # Ask for confirmation on exit.
107 |     repl.confirm_exit = True
108 | 
109 |     # Enable input validation. (Don't try to execute when the input contains
110 |     # syntax errors.)
111 |     repl.enable_input_validation = True
112 | 
113 |     # Use this colorscheme for the code.
114 |     # Ptpython uses Pygments for code styling, so you can choose from Pygments'
115 |     # color schemes. See:
116 |     # https://pygments.org/docs/styles/
117 |     # https://pygments.org/demo/
118 |     repl.use_code_colorscheme("default")
119 |     # A colorscheme that looks good on dark backgrounds is 'native':
120 |     # repl.use_code_colorscheme("native")
121 | 
122 |     # Set color depth (keep in mind that not all terminals support true color).
123 | 
124 |     # repl.color_depth = "DEPTH_1_BIT"  # Monochrome.
125 |     # repl.color_depth = "DEPTH_4_BIT"  # ANSI colors only.
126 |     repl.color_depth = "DEPTH_8_BIT"  # The default, 256 colors.
127 |     # repl.color_depth = "DEPTH_24_BIT"  # True color.
128 | 
129 |     # Min/max brightness
130 |     repl.min_brightness = 0.0  # Increase for dark terminal backgrounds.
131 |     repl.max_brightness = 1.0  # Decrease for light terminal backgrounds.
132 | 
133 |     # Syntax.
134 |     repl.enable_syntax_highlighting = True
135 | 
136 |     # Get into Vi navigation mode at startup
137 |     repl.vi_start_in_navigation_mode = False
138 | 
139 |     # Preserve last used Vi input mode between main loop iterations
140 |     repl.vi_keep_last_used_mode = False
141 | 
142 |     # Install custom colorscheme named 'my-colorscheme' and use it.
143 |     """
144 |     repl.install_ui_colorscheme("my-colorscheme", Style.from_dict(_custom_ui_colorscheme))
145 |     repl.use_ui_colorscheme("my-colorscheme")
146 |     """
147 | 
148 |     # Add custom key binding for PDB.
149 |     """
150 |     @repl.add_key_binding("c-b")
151 |     def _(event):
152 |         " Pressing Control-B will insert "pdb.set_trace()" "
153 |         event.cli.current_buffer.insert_text("\nimport pdb; pdb.set_trace()\n")
154 |     """
155 | 
156 |     # Typing ControlE twice should also execute the current command.
157 |     # (Alternative for Meta-Enter.)
158 |     """
159 |     @repl.add_key_binding("c-e", "c-e")
160 |     def _(event):
161 |         event.current_buffer.validate_and_handle()
162 |     """
163 | 
164 |     # Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation
165 |     # mode.)
166 |     """
167 |     @repl.add_key_binding("j", "j", filter=ViInsertMode())
168 |     def _(event):
169 |         " Map 'jj' to Escape. "
170 |         event.cli.key_processor.feed(KeyPress(Keys("escape")))
171 |     """
172 | 
173 |     # Custom key binding for some simple autocorrection while typing.
174 |     """
175 |     corrections = {
176 |         "impotr": "import",
177 |         "pritn": "print",
178 |     }
179 | 
180 |     @repl.add_key_binding(" ")
181 |     def _(event):
182 |         " When a space is pressed. Check & correct word before cursor. "
183 |         b = event.cli.current_buffer
184 |         w = b.document.get_word_before_cursor()
185 | 
186 |         if w is not None:
187 |             if w in corrections:
188 |                 b.delete_before_cursor(count=len(w))
189 |                 b.insert_text(corrections[w])
190 | 
191 |         b.insert_text(" ")
192 |     """
193 | 
194 |     # Add a custom title to the status bar. This is useful when ptpython is
195 |     # embedded in other applications.
196 |     """
197 |     repl.title = "My custom prompt."
198 |     """
199 | 
200 | 
201 | # Custom colorscheme for the UI. See `ptpython/layout.py` and
202 | # `ptpython/style.py` for all possible tokens.
203 | _custom_ui_colorscheme = {
204 |     # Blue prompt.
205 |     "prompt": "bg:#eeeeff #000000 bold",
206 |     # Make the status toolbar red.
207 |     "status-toolbar": "bg:#ff0000 #000000",
208 | }
209 | 


--------------------------------------------------------------------------------
/examples/python-embed-with-custom-prompt.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """
 3 | Example of embedding a Python REPL, and setting a custom prompt.
 4 | """
 5 | 
 6 | from prompt_toolkit.formatted_text import HTML, AnyFormattedText
 7 | 
 8 | from ptpython.prompt_style import PromptStyle
 9 | from ptpython.repl import embed
10 | 
11 | 
12 | def configure(repl) -> None:
13 |     # Probably, the best is to add a new PromptStyle to `all_prompt_styles` and
14 |     # activate it. This way, the other styles are still selectable from the
15 |     # menu.
16 |     class CustomPrompt(PromptStyle):
17 |         def in_prompt(self) -> AnyFormattedText:
18 |             return HTML("<ansigreen>Input[%s]</ansigreen>: ") % (
19 |                 repl.current_statement_index,
20 |             )
21 | 
22 |         def in2_prompt(self, width: int) -> AnyFormattedText:
23 |             return "...: ".rjust(width)
24 | 
25 |         def out_prompt(self) -> AnyFormattedText:
26 |             return HTML("<ansired>Result[%s]</ansired>: ") % (
27 |                 repl.current_statement_index,
28 |             )
29 | 
30 |     repl.all_prompt_styles["custom"] = CustomPrompt()
31 |     repl.prompt_style = "custom"
32 | 
33 | 
34 | def main() -> None:
35 |     embed(globals(), locals(), configure=configure)
36 | 
37 | 
38 | if __name__ == "__main__":
39 |     main()
40 | 


--------------------------------------------------------------------------------
/examples/python-embed.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """ """
 3 | 
 4 | from ptpython.repl import embed
 5 | 
 6 | 
 7 | def main() -> None:
 8 |     embed(globals(), locals(), vi_mode=False)
 9 | 
10 | 
11 | if __name__ == "__main__":
12 |     main()
13 | 


--------------------------------------------------------------------------------
/examples/python-input.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """ """
 3 | 
 4 | from ptpython.python_input import PythonInput
 5 | 
 6 | 
 7 | def main():
 8 |     prompt = PythonInput()
 9 | 
10 |     text = prompt.app.run()
11 |     print("You said: " + text)
12 | 
13 | 
14 | if __name__ == "__main__":
15 |     main()
16 | 


--------------------------------------------------------------------------------
/examples/ssh-and-telnet-embed.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """
 3 | Serve a ptpython console using both telnet and ssh.
 4 | 
 5 | Thanks to Vincent Michel for this!
 6 | https://gist.github.com/vxgmichel/7685685b3e5ead04ada4a3ba75a48eef
 7 | """
 8 | 
 9 | from __future__ import annotations
10 | 
11 | import asyncio
12 | import pathlib
13 | 
14 | import asyncssh
15 | from prompt_toolkit import print_formatted_text
16 | from prompt_toolkit.contrib.ssh.server import (
17 |     PromptToolkitSSHServer,
18 |     PromptToolkitSSHSession,
19 | )
20 | from prompt_toolkit.contrib.telnet.server import TelnetConnection, TelnetServer
21 | 
22 | from ptpython.repl import embed
23 | 
24 | 
25 | def ensure_key(filename: str = "ssh_host_key") -> str:
26 |     path = pathlib.Path(filename)
27 |     if not path.exists():
28 |         rsa_key = asyncssh.generate_private_key("ssh-rsa")
29 |         path.write_bytes(rsa_key.export_private_key())
30 |     return str(path)
31 | 
32 | 
33 | async def interact(connection: PromptToolkitSSHSession | TelnetConnection) -> None:
34 |     global_dict = {**globals(), "print": print_formatted_text}
35 |     await embed(return_asyncio_coroutine=True, globals=global_dict)
36 | 
37 | 
38 | async def main(ssh_port: int = 8022, telnet_port: int = 8023) -> None:
39 |     ssh_server = PromptToolkitSSHServer(interact=interact)
40 |     await asyncssh.create_server(
41 |         lambda: ssh_server, "", ssh_port, server_host_keys=[ensure_key()]
42 |     )
43 |     print(f"Running ssh server on port {ssh_port}...")
44 | 
45 |     telnet_server = TelnetServer(interact=interact, port=telnet_port)
46 |     telnet_server.start()
47 |     print(f"Running telnet server on port {telnet_port}...")
48 | 
49 |     while True:
50 |         await asyncio.sleep(60)
51 | 
52 | 
53 | if __name__ == "__main__":
54 |     asyncio.run(main())
55 | 


--------------------------------------------------------------------------------
/examples/test-cases/ptpython-in-other-thread.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | """
 3 | Example of running ptpython in another thread.
 4 | 
 5 | (For testing whether it's working fine if it's not embedded in the main
 6 | thread.)
 7 | """
 8 | 
 9 | import threading
10 | 
11 | from ptpython.repl import embed
12 | 
13 | 
14 | def in_thread():
15 |     embed(globals(), locals(), vi_mode=False)
16 | 
17 | 
18 | def main():
19 |     th = threading.Thread(target=in_thread)
20 |     th.start()
21 |     th.join()
22 | 
23 | 
24 | if __name__ == "__main__":
25 |     main()
26 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
 1 | [project]
 2 | name = "ptpython"
 3 | version = "3.0.31"
 4 | description = "Python REPL build on top of prompt_toolkit"
 5 | readme = "README.rst"
 6 | authors = [{ name = "Jonathan Slenders" }]
 7 | classifiers = [
 8 |     "License :: OSI Approved :: BSD License",
 9 |     "Programming Language :: Python :: 3",
10 |     "Programming Language :: Python :: 3.8",
11 |     "Programming Language :: Python :: 3.9",
12 |     "Programming Language :: Python :: 3.10",
13 |     "Programming Language :: Python :: 3.11",
14 |     "Programming Language :: Python :: 3.12",
15 |     "Programming Language :: Python :: 3.13",
16 |     "Programming Language :: Python :: 3 :: Only",
17 |     "Programming Language :: Python",
18 | ]
19 | requires-python = ">=3.8"
20 | dependencies = [
21 |     "appdirs",
22 |     "jedi>=0.16.0",
23 |     # Use prompt_toolkit 3.0.43, because of `OneStyleAndTextTuple` import.
24 |     "prompt_toolkit>=3.0.43,<3.1.0",
25 |     "pygments",
26 | ]
27 | 
28 | 
29 | [project.urls]
30 | Homepage = "https://github.com/prompt-toolkit/ptpython"
31 | Changelog = "https://github.com/prompt-toolkit/ptpython/blob/master/CHANGELOG"
32 | "Bug Tracker" = "https://github.com/prompt-toolkit/ptpython/issues"
33 | "Source Code" = "https://github.com/prompt-toolkit/ptpython"
34 | 
35 | 
36 | [project.scripts]
37 | ptpython = "ptpython.entry_points.run_ptpython:run"
38 | ptipython = "ptpython.entry_points.run_ptipython:run"
39 | 
40 | 
41 | [project.optional-dependencies]
42 | ptipython = ["ipython"] # For ptipython, we need to have IPython
43 | 
44 | 
45 | [tool.mypy]
46 | ignore_missing_imports = true
47 | no_implicit_optional = true
48 | platform = "win32"
49 | strict_equality = true
50 | strict_optional = true
51 | 
52 | 
53 | [tool.ruff]
54 | target-version = "py38"
55 | lint.select = [
56 |     "E",  # pycodestyle errors
57 |     "W",  # pycodestyle warnings
58 |     "F",  # pyflakes
59 |     "C",  # flake8-comprehensions
60 |     "T",  # Print.
61 |     "I",  # isort
62 |     # "B",  # flake8-bugbear
63 |     "UP",  # pyupgrade
64 |     "RUF100",  # unused-noqa
65 |     "Q", # quotes
66 | ]
67 | lint.ignore = [
68 |     "E501",  # Line too long, handled by black
69 |     "C901",  # Too complex
70 |     "E722",  # bare except.
71 | ]
72 | 
73 | 
74 | [tool.ruff.lint.per-file-ignores]
75 | "examples/*" = ["T201"]  # Print allowed in examples.
76 | "examples/ptpython_config/config.py" = ["F401"]  # Unused imports in config.
77 | "src/ptpython/entry_points/run_ptipython.py" = ["T201", "F401"] # Print, import usage.
78 | "src/ptpython/entry_points/run_ptpython.py" = ["T201"]  # Print usage.
79 | "src/ptpython/ipython.py" = ["T100"]  # Import usage.
80 | "src/ptpython/repl.py" = ["T201"]  # Print usage.
81 | "src/ptpython/printer.py" = ["T201"]  # Print usage.
82 | 
83 | 
84 | [tool.ruff.lint.isort]
85 | known-first-party = ["ptpython"]
86 | known-third-party = ["prompt_toolkit", "pygments", "asyncssh"]
87 | 
88 | [tool.typos.default]
89 | extend-ignore-re = [
90 |     "impotr" # Intentional typo in: ./examples/ptpython_config/config.py
91 | ]
92 | 
93 | [build-system]
94 | requires = ["setuptools>=68"]
95 | build-backend = "setuptools.build_meta"
96 | 


--------------------------------------------------------------------------------
/src/ptpython/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | 
3 | from .repl import embed
4 | 
5 | __all__ = ["embed"]
6 | 


--------------------------------------------------------------------------------
/src/ptpython/__main__.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Make `python -m ptpython` an alias for running `./ptpython`.
 3 | """
 4 | 
 5 | from __future__ import annotations
 6 | 
 7 | from .entry_points.run_ptpython import run
 8 | 
 9 | run()
10 | 


--------------------------------------------------------------------------------
/src/ptpython/completer.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import ast
  4 | import collections.abc as collections_abc
  5 | import inspect
  6 | import keyword
  7 | import re
  8 | from enum import Enum
  9 | from itertools import islice
 10 | from typing import TYPE_CHECKING, Any, Callable, Iterable
 11 | 
 12 | from prompt_toolkit.completion import (
 13 |     CompleteEvent,
 14 |     Completer,
 15 |     Completion,
 16 |     PathCompleter,
 17 | )
 18 | from prompt_toolkit.contrib.completers.system import SystemCompleter
 19 | from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar
 20 | from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
 21 | from prompt_toolkit.document import Document
 22 | from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_text
 23 | 
 24 | from ptpython.utils import get_jedi_script_from_document
 25 | 
 26 | if TYPE_CHECKING:
 27 |     import jedi.api.classes
 28 |     from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar
 29 | 
 30 | __all__ = ["PythonCompleter", "CompletePrivateAttributes", "HidePrivateCompleter"]
 31 | 
 32 | 
 33 | class CompletePrivateAttributes(Enum):
 34 |     """
 35 |     Should we display private attributes in the completion pop-up?
 36 |     """
 37 | 
 38 |     NEVER = "NEVER"
 39 |     IF_NO_PUBLIC = "IF_NO_PUBLIC"
 40 |     ALWAYS = "ALWAYS"
 41 | 
 42 | 
 43 | class PythonCompleter(Completer):
 44 |     """
 45 |     Completer for Python code.
 46 |     """
 47 | 
 48 |     def __init__(
 49 |         self,
 50 |         get_globals: Callable[[], dict[str, Any]],
 51 |         get_locals: Callable[[], dict[str, Any]],
 52 |         enable_dictionary_completion: Callable[[], bool],
 53 |     ) -> None:
 54 |         super().__init__()
 55 | 
 56 |         self.get_globals = get_globals
 57 |         self.get_locals = get_locals
 58 |         self.enable_dictionary_completion = enable_dictionary_completion
 59 | 
 60 |         self._system_completer = SystemCompleter()
 61 |         self._jedi_completer = JediCompleter(get_globals, get_locals)
 62 |         self._dictionary_completer = DictionaryCompleter(get_globals, get_locals)
 63 | 
 64 |         self._path_completer_cache: GrammarCompleter | None = None
 65 |         self._path_completer_grammar_cache: _CompiledGrammar | None = None
 66 | 
 67 |     @property
 68 |     def _path_completer(self) -> GrammarCompleter:
 69 |         if self._path_completer_cache is None:
 70 |             self._path_completer_cache = GrammarCompleter(
 71 |                 self._path_completer_grammar,
 72 |                 {
 73 |                     "var1": PathCompleter(expanduser=True),
 74 |                     "var2": PathCompleter(expanduser=True),
 75 |                 },
 76 |             )
 77 |         return self._path_completer_cache
 78 | 
 79 |     @property
 80 |     def _path_completer_grammar(self) -> _CompiledGrammar:
 81 |         """
 82 |         Return the grammar for matching paths inside strings inside Python
 83 |         code.
 84 |         """
 85 |         # We make this lazy, because it delays startup time a little bit.
 86 |         # This way, the grammar is build during the first completion.
 87 |         if self._path_completer_grammar_cache is None:
 88 |             self._path_completer_grammar_cache = self._create_path_completer_grammar()
 89 |         return self._path_completer_grammar_cache
 90 | 
 91 |     def _create_path_completer_grammar(self) -> _CompiledGrammar:
 92 |         def unwrapper(text: str) -> str:
 93 |             return re.sub(r"\\(.)", r"\1", text)
 94 | 
 95 |         def single_quoted_wrapper(text: str) -> str:
 96 |             return text.replace("\\", "\\\\").replace("'", "\\'")
 97 | 
 98 |         def double_quoted_wrapper(text: str) -> str:
 99 |             return text.replace("\\", "\\\\").replace('"', '\\"')
100 | 
101 |         grammar = r"""
102 |                 # Text before the current string.
103 |                 (
104 |                     [^'"#]                                  |  # Not quoted characters.
105 |                     '''  ([^'\\]|'(?!')|''(?!')|\\.])*  ''' |  # Inside single quoted triple strings
106 |                     "" " ([^"\\]|"(?!")|""(?!^)|\\.])* "" " |  # Inside double quoted triple strings
107 | 
108 |                     \#[^\n]*(\n|$)           |  # Comment.
109 |                     "(?!"") ([^"\\]|\\.)*"   |  # Inside double quoted strings.
110 |                     '(?!'') ([^'\\]|\\.)*'      # Inside single quoted strings.
111 | 
112 |                         # Warning: The negative lookahead in the above two
113 |                         #          statements is important. If we drop that,
114 |                         #          then the regex will try to interpret every
115 |                         #          triple quoted string also as a single quoted
116 |                         #          string, making this exponentially expensive to
117 |                         #          execute!
118 |                 )*
119 |                 # The current string that we're completing.
120 |                 (
121 |                     ' (?P<var1>([^\n'\\]|\\.)*) |  # Inside a single quoted string.
122 |                     " (?P<var2>([^\n"\\]|\\.)*)    # Inside a double quoted string.
123 |                 )
124 |         """
125 | 
126 |         return compile_grammar(
127 |             grammar,
128 |             escape_funcs={"var1": single_quoted_wrapper, "var2": double_quoted_wrapper},
129 |             unescape_funcs={"var1": unwrapper, "var2": unwrapper},
130 |         )
131 | 
132 |     def _complete_path_while_typing(self, document: Document) -> bool:
133 |         char_before_cursor = document.char_before_cursor
134 |         return bool(
135 |             document.text
136 |             and (char_before_cursor.isalnum() or char_before_cursor in "/.~")
137 |         )
138 | 
139 |     def _complete_python_while_typing(self, document: Document) -> bool:
140 |         """
141 |         When `complete_while_typing` is set, only return completions when this
142 |         returns `True`.
143 |         """
144 |         text = document.text_before_cursor  # .rstrip()
145 |         char_before_cursor = text[-1:]
146 |         return bool(
147 |             text and (char_before_cursor.isalnum() or char_before_cursor in "_.([,")
148 |         )
149 | 
150 |     def get_completions(
151 |         self, document: Document, complete_event: CompleteEvent
152 |     ) -> Iterable[Completion]:
153 |         """
154 |         Get Python completions.
155 |         """
156 |         # If the input starts with an exclamation mark. Use the system completer.
157 |         if document.text.lstrip().startswith("!"):
158 |             yield from self._system_completer.get_completions(
159 |                 Document(
160 |                     text=document.text[1:], cursor_position=document.cursor_position - 1
161 |                 ),
162 |                 complete_event,
163 |             )
164 |             return
165 | 
166 |         # Do dictionary key completions.
167 |         if complete_event.completion_requested or self._complete_python_while_typing(
168 |             document
169 |         ):
170 |             if self.enable_dictionary_completion():
171 |                 has_dict_completions = False
172 |                 for c in self._dictionary_completer.get_completions(
173 |                     document, complete_event
174 |                 ):
175 |                     if c.text not in "[.":
176 |                         # If we get the [ or . completion, still include the other
177 |                         # completions.
178 |                         has_dict_completions = True
179 |                     yield c
180 |                 if has_dict_completions:
181 |                     return
182 | 
183 |         # Do Path completions (if there were no dictionary completions).
184 |         if complete_event.completion_requested or self._complete_path_while_typing(
185 |             document
186 |         ):
187 |             yield from self._path_completer.get_completions(document, complete_event)
188 | 
189 |         # Do Jedi completions.
190 |         if complete_event.completion_requested or self._complete_python_while_typing(
191 |             document
192 |         ):
193 |             # If we are inside a string, Don't do Jedi completion.
194 |             if not self._path_completer_grammar.match(document.text_before_cursor):
195 |                 # Do Jedi Python completions.
196 |                 yield from self._jedi_completer.get_completions(
197 |                     document, complete_event
198 |                 )
199 | 
200 | 
201 | class JediCompleter(Completer):
202 |     """
203 |     Autocompleter that uses the Jedi library.
204 |     """
205 | 
206 |     def __init__(
207 |         self,
208 |         get_globals: Callable[[], dict[str, Any]],
209 |         get_locals: Callable[[], dict[str, Any]],
210 |     ) -> None:
211 |         super().__init__()
212 | 
213 |         self.get_globals = get_globals
214 |         self.get_locals = get_locals
215 | 
216 |     def get_completions(
217 |         self, document: Document, complete_event: CompleteEvent
218 |     ) -> Iterable[Completion]:
219 |         script = get_jedi_script_from_document(
220 |             document, self.get_locals(), self.get_globals()
221 |         )
222 | 
223 |         if script:
224 |             try:
225 |                 jedi_completions = script.complete(
226 |                     column=document.cursor_position_col,
227 |                     line=document.cursor_position_row + 1,
228 |                 )
229 |             except TypeError:
230 |                 # Issue #9: bad syntax causes completions() to fail in jedi.
231 |                 # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9
232 |                 pass
233 |             except UnicodeDecodeError:
234 |                 # Issue #43: UnicodeDecodeError on OpenBSD
235 |                 # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43
236 |                 pass
237 |             except AttributeError:
238 |                 # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513
239 |                 pass
240 |             except ValueError:
241 |                 # Jedi issue: "ValueError: invalid \x escape"
242 |                 pass
243 |             except KeyError:
244 |                 # Jedi issue: "KeyError: u'a_lambda'."
245 |                 # https://github.com/jonathanslenders/ptpython/issues/89
246 |                 pass
247 |             except OSError:
248 |                 # Jedi issue: "IOError: No such file or directory."
249 |                 # https://github.com/jonathanslenders/ptpython/issues/71
250 |                 pass
251 |             except AssertionError:
252 |                 # In jedi.parser.__init__.py: 227, in remove_last_newline,
253 |                 # the assertion "newline.value.endswith('\n')" can fail.
254 |                 pass
255 |             except SystemError:
256 |                 # In jedi.api.helpers.py: 144, in get_stack_at_position
257 |                 # raise SystemError("This really shouldn't happen. There's a bug in Jedi.")
258 |                 pass
259 |             except NotImplementedError:
260 |                 # See: https://github.com/jonathanslenders/ptpython/issues/223
261 |                 pass
262 |             except Exception:
263 |                 # Suppress all other Jedi exceptions.
264 |                 pass
265 |             else:
266 |                 # Move function parameters to the top.
267 |                 jedi_completions = sorted(
268 |                     jedi_completions,
269 |                     key=lambda jc: (
270 |                         # Params first.
271 |                         jc.type != "param",
272 |                         # Private at the end.
273 |                         jc.name.startswith("_"),
274 |                         # Then sort by name.
275 |                         jc.name_with_symbols.lower(),
276 |                     ),
277 |                 )
278 | 
279 |                 for jc in jedi_completions:
280 |                     if jc.type == "function":
281 |                         suffix = "()"
282 |                     else:
283 |                         suffix = ""
284 | 
285 |                     if jc.type == "param":
286 |                         suffix = "..."
287 | 
288 |                     yield Completion(
289 |                         jc.name_with_symbols,
290 |                         len(jc.complete) - len(jc.name_with_symbols),
291 |                         display=jc.name_with_symbols + suffix,
292 |                         display_meta=jc.type,
293 |                         style=_get_style_for_jedi_completion(jc),
294 |                     )
295 | 
296 | 
297 | class DictionaryCompleter(Completer):
298 |     """
299 |     Experimental completer for Python dictionary keys.
300 | 
301 |     Warning: This does an `eval` and `repr` on some Python expressions before
302 |              the cursor, which is potentially dangerous. It doesn't match on
303 |              function calls, so it only triggers attribute access.
304 |     """
305 | 
306 |     def __init__(
307 |         self,
308 |         get_globals: Callable[[], dict[str, Any]],
309 |         get_locals: Callable[[], dict[str, Any]],
310 |     ) -> None:
311 |         super().__init__()
312 | 
313 |         self.get_globals = get_globals
314 |         self.get_locals = get_locals
315 | 
316 |         # Pattern for expressions that are "safe" to eval for auto-completion.
317 |         # These are expressions that contain only attribute and index lookups.
318 |         varname = r"[a-zA-Z_][a-zA-Z0-9_]*"
319 | 
320 |         expression = rf"""
321 |             # Any expression safe enough to eval while typing.
322 |             # No operators, except dot, and only other dict lookups.
323 |             # Technically, this can be unsafe of course, if bad code runs
324 |             # in `__getattr__` or ``__getitem__``.
325 |             (
326 |                 # Variable name
327 |                 {varname}
328 | 
329 |                 \s*
330 | 
331 |                 (?:
332 |                     # Attribute access.
333 |                     \s* \. \s* {varname} \s*
334 | 
335 |                     |
336 | 
337 |                     # Item lookup.
338 |                     # (We match the square brackets. The key can be anything.
339 |                     # We don't care about matching quotes here in the regex.
340 |                     # Nested square brackets are not supported.)
341 |                     \s* \[ [^\[\]]+ \] \s*
342 |                 )*
343 |             )
344 |         """
345 | 
346 |         # Pattern for recognizing for-loops, so that we can provide
347 |         # autocompletion on the iterator of the for-loop. (According to the
348 |         # first item of the collection we're iterating over.)
349 |         self.for_loop_pattern = re.compile(
350 |             rf"""
351 |                 for \s+ ([a-zA-Z0-9_]+) \s+ in \s+ {expression} \s* :
352 |             """,
353 |             re.VERBOSE,
354 |         )
355 | 
356 |         # Pattern for matching a simple expression (for completing [ or .
357 |         # operators).
358 |         self.expression_pattern = re.compile(
359 |             rf"""
360 |                 {expression}
361 |                 $
362 |             """,
363 |             re.VERBOSE,
364 |         )
365 | 
366 |         # Pattern for matching item lookups.
367 |         self.item_lookup_pattern = re.compile(
368 |             rf"""
369 |                 {expression}
370 | 
371 |                 # Dict lookup to complete (square bracket open + start of
372 |                 # string).
373 |                 \[
374 |                 \s* ([^\[\]]*)$
375 |             """,
376 |             re.VERBOSE,
377 |         )
378 | 
379 |         # Pattern for matching attribute lookups.
380 |         self.attribute_lookup_pattern = re.compile(
381 |             rf"""
382 |                 {expression}
383 | 
384 |                 # Attribute lookup to complete (dot + varname).
385 |                 \.
386 |                 \s* ([a-zA-Z0-9_]*)$
387 |             """,
388 |             re.VERBOSE,
389 |         )
390 | 
391 |     def _lookup(self, expression: str, temp_locals: dict[str, Any]) -> object:
392 |         """
393 |         Do lookup of `object_var` in the context.
394 |         `temp_locals` is a dictionary, used for the locals.
395 |         """
396 |         try:
397 |             return eval(expression.strip(), self.get_globals(), temp_locals)
398 |         except BaseException:
399 |             return None  # Many exception, like NameError can be thrown here.
400 | 
401 |     def get_completions(
402 |         self, document: Document, complete_event: CompleteEvent
403 |     ) -> Iterable[Completion]:
404 |         # First, find all for-loops, and assign the first item of the
405 |         # collections they're iterating to the iterator variable, so that we
406 |         # can provide code completion on the iterators.
407 |         temp_locals = self.get_locals().copy()
408 | 
409 |         for match in self.for_loop_pattern.finditer(document.text_before_cursor):
410 |             varname, expression = match.groups()
411 |             expression_val = self._lookup(expression, temp_locals)
412 | 
413 |             # We do this only for lists and tuples. Calling `next()` on any
414 |             # collection would create undesired side effects.
415 |             if isinstance(expression_val, (list, tuple)) and expression_val:
416 |                 temp_locals[varname] = expression_val[0]
417 | 
418 |         # Get all completions.
419 |         yield from self._get_expression_completions(
420 |             document, complete_event, temp_locals
421 |         )
422 |         yield from self._get_item_lookup_completions(
423 |             document, complete_event, temp_locals
424 |         )
425 |         yield from self._get_attribute_completions(
426 |             document, complete_event, temp_locals
427 |         )
428 | 
429 |     def _do_repr(self, obj: object) -> str:
430 |         try:
431 |             return str(repr(obj))
432 |         except BaseException:
433 |             raise ReprFailedError
434 | 
435 |     def eval_expression(self, document: Document, locals: dict[str, Any]) -> object:
436 |         """
437 |         Evaluate
438 |         """
439 |         match = self.expression_pattern.search(document.text_before_cursor)
440 |         if match is not None:
441 |             object_var = match.groups()[0]
442 |             return self._lookup(object_var, locals)
443 | 
444 |         return None
445 | 
446 |     def _get_expression_completions(
447 |         self,
448 |         document: Document,
449 |         complete_event: CompleteEvent,
450 |         temp_locals: dict[str, Any],
451 |     ) -> Iterable[Completion]:
452 |         """
453 |         Complete the [ or . operator after an object.
454 |         """
455 |         result = self.eval_expression(document, temp_locals)
456 | 
457 |         if result is not None:
458 |             if isinstance(
459 |                 result,
460 |                 (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence),
461 |             ):
462 |                 yield Completion("[", 0)
463 | 
464 |             else:
465 |                 # Note: Don't call `if result` here. That can fail for types
466 |                 #       that have custom truthness checks.
467 |                 yield Completion(".", 0)
468 | 
469 |     def _get_item_lookup_completions(
470 |         self,
471 |         document: Document,
472 |         complete_event: CompleteEvent,
473 |         temp_locals: dict[str, Any],
474 |     ) -> Iterable[Completion]:
475 |         """
476 |         Complete dictionary keys.
477 |         """
478 | 
479 |         def meta_repr(obj: object, key: object) -> Callable[[], str]:
480 |             "Abbreviate meta text, make sure it fits on one line."
481 |             cached_result: str | None = None
482 | 
483 |             # We return a function, so that it gets computed when it's needed.
484 |             # When there are many completions, that improves the performance
485 |             # quite a bit (for the multi-column completion menu, we only need
486 |             # to display one meta text).
487 |             # Note that we also do the lookup itself in here (`obj[key]`),
488 |             # because this part can also be slow for some mapping
489 |             # implementations.
490 |             def get_value_repr() -> str:
491 |                 nonlocal cached_result
492 |                 if cached_result is not None:
493 |                     return cached_result
494 | 
495 |                 try:
496 |                     value = obj[key]  # type: ignore
497 | 
498 |                     text = self._do_repr(value)
499 |                 except BaseException:
500 |                     return "-"
501 | 
502 |                 # Take first line, if multiple lines.
503 |                 if "\n" in text:
504 |                     text = text.split("\n", 1)[0] + "..."
505 | 
506 |                 cached_result = text
507 |                 return text
508 | 
509 |             return get_value_repr
510 | 
511 |         match = self.item_lookup_pattern.search(document.text_before_cursor)
512 |         if match is not None:
513 |             object_var, key = match.groups()
514 | 
515 |             # Do lookup of `object_var` in the context.
516 |             result = self._lookup(object_var, temp_locals)
517 | 
518 |             # If this object is a dictionary, complete the keys.
519 |             if isinstance(result, (dict, collections_abc.Mapping)):
520 |                 # Try to evaluate the key.
521 |                 key_obj_str = str(key)
522 |                 for k in [key, key + '"', key + "'"]:
523 |                     try:
524 |                         key_obj_str = str(ast.literal_eval(k))
525 |                     except (SyntaxError, ValueError):
526 |                         continue
527 |                     else:
528 |                         break
529 | 
530 |                 for k in result:
531 |                     if str(k).startswith(key_obj_str):
532 |                         try:
533 |                             k_repr = self._do_repr(k)
534 |                             yield Completion(
535 |                                 k_repr + "]",
536 |                                 -len(key),
537 |                                 display=f"[{k_repr}]",
538 |                                 display_meta=meta_repr(result, k),
539 |                             )
540 |                         except ReprFailedError:
541 |                             pass
542 | 
543 |             # Complete list/tuple index keys.
544 |             elif isinstance(result, (list, tuple, collections_abc.Sequence)):
545 |                 if not key or key.isdigit():
546 |                     for k in range(min(len(result), 1000)):
547 |                         if str(k).startswith(key):
548 |                             try:
549 |                                 k_repr = self._do_repr(k)
550 |                                 yield Completion(
551 |                                     k_repr + "]",
552 |                                     -len(key),
553 |                                     display=f"[{k_repr}]",
554 |                                     display_meta=meta_repr(result, k),
555 |                                 )
556 |                             except KeyError:
557 |                                 # `result[k]` lookup failed. Trying to complete
558 |                                 # broken object.
559 |                                 pass
560 |                             except ReprFailedError:
561 |                                 pass
562 | 
563 |     def _get_attribute_completions(
564 |         self,
565 |         document: Document,
566 |         complete_event: CompleteEvent,
567 |         temp_locals: dict[str, Any],
568 |     ) -> Iterable[Completion]:
569 |         """
570 |         Complete attribute names.
571 |         """
572 |         match = self.attribute_lookup_pattern.search(document.text_before_cursor)
573 |         if match is not None:
574 |             object_var, attr_name = match.groups()
575 | 
576 |             # Do lookup of `object_var` in the context.
577 |             result = self._lookup(object_var, temp_locals)
578 | 
579 |             names = self._sort_attribute_names(dir(result))
580 | 
581 |             def get_suffix(name: str) -> str:
582 |                 try:
583 |                     obj = getattr(result, name, None)
584 |                     if inspect.isfunction(obj) or inspect.ismethod(obj):
585 |                         return "()"
586 |                     if isinstance(obj, collections_abc.Mapping):
587 |                         return "{}"
588 |                     if isinstance(obj, collections_abc.Sequence):
589 |                         return "[]"
590 |                 except:
591 |                     pass
592 |                 return ""
593 | 
594 |             for name in names:
595 |                 if name.startswith(attr_name):
596 |                     suffix = get_suffix(name)
597 |                     yield Completion(name, -len(attr_name), display=name + suffix)
598 | 
599 |     def _sort_attribute_names(self, names: list[str]) -> list[str]:
600 |         """
601 |         Sort attribute names alphabetically, but move the double underscore and
602 |         underscore names to the end.
603 |         """
604 | 
605 |         def sort_key(name: str) -> tuple[int, str]:
606 |             if name.startswith("__"):
607 |                 return (2, name)  # Double underscore comes latest.
608 |             if name.startswith("_"):
609 |                 return (1, name)  # Single underscore before that.
610 |             return (0, name)  # Other names first.
611 | 
612 |         return sorted(names, key=sort_key)
613 | 
614 | 
615 | class HidePrivateCompleter(Completer):
616 |     """
617 |     Wrapper around completer that hides private fields, depending on whether or
618 |     not public fields are shown.
619 | 
620 |     (The reason this is implemented as a `Completer` wrapper is because this
621 |     way it works also with `FuzzyCompleter`.)
622 |     """
623 | 
624 |     def __init__(
625 |         self,
626 |         completer: Completer,
627 |         complete_private_attributes: Callable[[], CompletePrivateAttributes],
628 |     ) -> None:
629 |         self.completer = completer
630 |         self.complete_private_attributes = complete_private_attributes
631 | 
632 |     def get_completions(
633 |         self, document: Document, complete_event: CompleteEvent
634 |     ) -> Iterable[Completion]:
635 |         completions = list(
636 |             # Limit at 5k completions for performance.
637 |             islice(self.completer.get_completions(document, complete_event), 0, 5000)
638 |         )
639 |         complete_private_attributes = self.complete_private_attributes()
640 |         hide_private = False
641 | 
642 |         def is_private(completion: Completion) -> bool:
643 |             text = fragment_list_to_text(to_formatted_text(completion.display))
644 |             return text.startswith("_")
645 | 
646 |         if complete_private_attributes == CompletePrivateAttributes.NEVER:
647 |             hide_private = True
648 | 
649 |         elif complete_private_attributes == CompletePrivateAttributes.IF_NO_PUBLIC:
650 |             hide_private = any(not is_private(completion) for completion in completions)
651 | 
652 |         if hide_private:
653 |             completions = [
654 |                 completion for completion in completions if not is_private(completion)
655 |             ]
656 | 
657 |         return completions
658 | 
659 | 
660 | class ReprFailedError(Exception):
661 |     "Raised when the repr() call in `DictionaryCompleter` fails."
662 | 
663 | 
664 | try:
665 |     import builtins
666 | 
667 |     _builtin_names = dir(builtins)
668 | except ImportError:  # Python 2.
669 |     _builtin_names = []
670 | 
671 | 
672 | def _get_style_for_jedi_completion(
673 |     jedi_completion: jedi.api.classes.Completion,
674 | ) -> str:
675 |     """
676 |     Return completion style to use for this name.
677 |     """
678 |     name = jedi_completion.name_with_symbols
679 | 
680 |     if jedi_completion.type == "param":
681 |         return "class:completion.param"
682 | 
683 |     if name in _builtin_names:
684 |         return "class:completion.builtin"
685 | 
686 |     if keyword.iskeyword(name):
687 |         return "class:completion.keyword"
688 | 
689 |     return ""
690 | 


--------------------------------------------------------------------------------
/src/ptpython/contrib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/src/ptpython/contrib/__init__.py


--------------------------------------------------------------------------------
/src/ptpython/contrib/asyncssh_repl.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Tool for embedding a REPL inside a Python 3 asyncio process.
  3 | See ./examples/asyncio-ssh-python-embed.py for a demo.
  4 | 
  5 | Note that the code in this file is Python 3 only. However, we
  6 | should make sure not to use Python 3-only syntax, because this
  7 | package should be installable in Python 2 as well!
  8 | """
  9 | 
 10 | from __future__ import annotations
 11 | 
 12 | import asyncio
 13 | from typing import Any, AnyStr, TextIO, cast
 14 | 
 15 | import asyncssh
 16 | from prompt_toolkit.data_structures import Size
 17 | from prompt_toolkit.input import create_pipe_input
 18 | from prompt_toolkit.output.vt100 import Vt100_Output
 19 | 
 20 | from ptpython.python_input import _GetNamespace, _Namespace
 21 | from ptpython.repl import PythonRepl
 22 | 
 23 | __all__ = ["ReplSSHServerSession"]
 24 | 
 25 | 
 26 | class ReplSSHServerSession(asyncssh.SSHServerSession[str]):
 27 |     """
 28 |     SSH server session that runs a Python REPL.
 29 | 
 30 |     :param get_globals: callable that returns the current globals.
 31 |     :param get_locals: (optional) callable that returns the current locals.
 32 |     """
 33 | 
 34 |     def __init__(
 35 |         self, get_globals: _GetNamespace, get_locals: _GetNamespace | None = None
 36 |     ) -> None:
 37 |         self._chan: Any = None
 38 | 
 39 |         def _globals() -> _Namespace:
 40 |             data = get_globals()
 41 |             data.setdefault("print", self._print)
 42 |             return data
 43 | 
 44 |         # PipInput object, for sending input in the CLI.
 45 |         # (This is something that we can use in the prompt_toolkit event loop,
 46 |         # but still write date in manually.)
 47 |         self._input_pipe = create_pipe_input()
 48 | 
 49 |         # Output object. Don't render to the real stdout, but write everything
 50 |         # in the SSH channel.
 51 |         class Stdout:
 52 |             def write(s, data: str) -> None:
 53 |                 if self._chan is not None:
 54 |                     data = data.replace("\n", "\r\n")
 55 |                     self._chan.write(data)
 56 | 
 57 |             def flush(s) -> None:
 58 |                 pass
 59 | 
 60 |         self.repl = PythonRepl(
 61 |             get_globals=_globals,
 62 |             get_locals=get_locals or _globals,
 63 |             input=self._input_pipe,
 64 |             output=Vt100_Output(cast(TextIO, Stdout()), self._get_size),
 65 |         )
 66 | 
 67 |         # Disable open-in-editor and system prompt. Because it would run and
 68 |         # display these commands on the server side, rather than in the SSH
 69 |         # client.
 70 |         self.repl.enable_open_in_editor = False
 71 |         self.repl.enable_system_bindings = False
 72 | 
 73 |     def _get_size(self) -> Size:
 74 |         """
 75 |         Callable that returns the current `Size`, required by Vt100_Output.
 76 |         """
 77 |         if self._chan is None:
 78 |             return Size(rows=20, columns=79)
 79 |         else:
 80 |             width, height, pixwidth, pixheight = self._chan.get_terminal_size()
 81 |             return Size(rows=height, columns=width)
 82 | 
 83 |     def connection_made(self, chan: Any) -> None:
 84 |         """
 85 |         Client connected, run repl in coroutine.
 86 |         """
 87 |         self._chan = chan
 88 | 
 89 |         # Run REPL interface.
 90 |         f = asyncio.ensure_future(self.repl.run_async())
 91 | 
 92 |         # Close channel when done.
 93 |         def done(_: object) -> None:
 94 |             chan.close()
 95 |             self._chan = None
 96 | 
 97 |         f.add_done_callback(done)
 98 | 
 99 |     def shell_requested(self) -> bool:
100 |         return True
101 | 
102 |     def terminal_size_changed(
103 |         self, width: int, height: int, pixwidth: int, pixheight: int
104 |     ) -> None:
105 |         """
106 |         When the terminal size changes, report back to CLI.
107 |         """
108 |         self.repl.app._on_resize()
109 | 
110 |     def data_received(self, data: AnyStr, datatype: int | None) -> None:
111 |         """
112 |         When data is received, send to inputstream of the CLI and repaint.
113 |         """
114 |         self._input_pipe.send(data)  # type: ignore
115 | 
116 |     def _print(
117 |         self, *data: object, sep: str = " ", end: str = "\n", file: Any = None
118 |     ) -> None:
119 |         """
120 |         Alternative 'print' function that prints back into the SSH channel.
121 |         """
122 |         # Pop keyword-only arguments. (We cannot use the syntax from the
123 |         # signature. Otherwise, Python2 will give a syntax error message when
124 |         # installing.)
125 |         data_as_str = sep.join(map(str, data))
126 |         self._chan.write(data_as_str + end)
127 | 


--------------------------------------------------------------------------------
/src/ptpython/entry_points/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/src/ptpython/entry_points/__init__.py


--------------------------------------------------------------------------------
/src/ptpython/entry_points/run_ptipython.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | from __future__ import annotations
 3 | 
 4 | import os
 5 | import sys
 6 | 
 7 | from .run_ptpython import create_parser, get_config_and_history_file
 8 | 
 9 | 
10 | def run(user_ns=None):
11 |     a = create_parser().parse_args()
12 | 
13 |     config_file, history_file = get_config_and_history_file(a)
14 | 
15 |     # If IPython is not available, show message and exit here with error status
16 |     # code.
17 |     try:
18 |         import IPython
19 |     except ImportError:
20 |         print("IPython not found. Please install IPython (pip install ipython).")
21 |         sys.exit(1)
22 |     else:
23 |         from ptpython.ipython import embed
24 |         from ptpython.repl import enable_deprecation_warnings, run_config
25 | 
26 |     # Add the current directory to `sys.path`.
27 |     if sys.path[0] != "":
28 |         sys.path.insert(0, "")
29 | 
30 |     # When a file has been given, run that, otherwise start the shell.
31 |     if a.args and not a.interactive:
32 |         sys.argv = a.args
33 |         path = a.args[0]
34 |         with open(path, "rb") as f:
35 |             code = compile(f.read(), path, "exec")
36 |             exec(code, {"__name__": "__main__", "__file__": path})
37 |     else:
38 |         enable_deprecation_warnings()
39 | 
40 |         # Create an empty namespace for this interactive shell. (If we don't do
41 |         # that, all the variables from this function will become available in
42 |         # the IPython shell.)
43 |         if user_ns is None:
44 |             user_ns = {}
45 | 
46 |         # Startup path
47 |         startup_paths = []
48 |         if "PYTHONSTARTUP" in os.environ:
49 |             startup_paths.append(os.environ["PYTHONSTARTUP"])
50 | 
51 |         # --interactive
52 |         if a.interactive:
53 |             startup_paths.append(a.args[0])
54 |             sys.argv = a.args
55 | 
56 |         # exec scripts from startup paths
57 |         for path in startup_paths:
58 |             if os.path.exists(path):
59 |                 with open(path, "rb") as f:
60 |                     code = compile(f.read(), path, "exec")
61 |                     exec(code, user_ns, user_ns)
62 |             else:
63 |                 print(f"File not found: {path}\n\n")
64 |                 sys.exit(1)
65 | 
66 |         # Apply config file
67 |         def configure(repl):
68 |             if os.path.exists(config_file):
69 |                 run_config(repl, config_file)
70 | 
71 |         # Run interactive shell.
72 |         embed(
73 |             vi_mode=a.vi,
74 |             history_filename=history_file,
75 |             configure=configure,
76 |             user_ns=user_ns,
77 |             title="IPython REPL (ptipython)",
78 |         )
79 | 
80 | 
81 | if __name__ == "__main__":
82 |     run()
83 | 


--------------------------------------------------------------------------------
/src/ptpython/entry_points/run_ptpython.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python
  2 | """
  3 | ptpython: Interactive Python shell.
  4 | 
  5 | positional arguments:
  6 |   args                  Script and arguments
  7 | 
  8 | optional arguments:
  9 |   -h, --help            show this help message and exit
 10 |   --vi                  Enable Vi key bindings
 11 |   -i, --interactive     Start interactive shell after executing this file.
 12 |   --asyncio             Run an asyncio event loop to support top-level "await".
 13 |   --light-bg            Run on a light background (use dark colors for text).
 14 |   --dark-bg             Run on a dark background (use light colors for text).
 15 |   --config-file CONFIG_FILE
 16 |                         Location of configuration file.
 17 |   --history-file HISTORY_FILE
 18 |                         Location of history file.
 19 |   -V, --version         show program's version number and exit
 20 | 
 21 | environment variables:
 22 |   PTPYTHON_CONFIG_HOME: a configuration directory to use
 23 |   PYTHONSTARTUP: file executed on interactive startup (no default)
 24 | """
 25 | 
 26 | from __future__ import annotations
 27 | 
 28 | import argparse
 29 | import asyncio
 30 | import os
 31 | import pathlib
 32 | import sys
 33 | from importlib import metadata
 34 | from textwrap import dedent
 35 | from typing import Protocol
 36 | 
 37 | import appdirs
 38 | from prompt_toolkit.formatted_text import HTML
 39 | from prompt_toolkit.shortcuts import print_formatted_text
 40 | 
 41 | from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config
 42 | 
 43 | __all__ = ["create_parser", "get_config_and_history_file", "run"]
 44 | 
 45 | 
 46 | class _SupportsWrite(Protocol):
 47 |     def write(self, s: str, /) -> object: ...
 48 | 
 49 | 
 50 | class _Parser(argparse.ArgumentParser):
 51 |     def print_help(self, file: _SupportsWrite | None = None) -> None:
 52 |         super().print_help()
 53 |         print(
 54 |             dedent(
 55 |                 """
 56 |                 environment variables:
 57 |                   PTPYTHON_CONFIG_HOME: a configuration directory to use
 58 |                   PYTHONSTARTUP: file executed on interactive startup (no default)
 59 |                 """,
 60 |             ).rstrip(),
 61 |         )
 62 | 
 63 | 
 64 | def create_parser() -> _Parser:
 65 |     parser = _Parser(description="ptpython: Interactive Python shell.")
 66 |     parser.add_argument("--vi", action="store_true", help="Enable Vi key bindings")
 67 |     parser.add_argument(
 68 |         "-i",
 69 |         "--interactive",
 70 |         action="store_true",
 71 |         help="Start interactive shell after executing this file.",
 72 |     )
 73 |     parser.add_argument(
 74 |         "--asyncio",
 75 |         action="store_true",
 76 |         help='Run an asyncio event loop to support top-level "await".',
 77 |     )
 78 |     parser.add_argument(
 79 |         "--light-bg",
 80 |         action="store_true",
 81 |         help="Run on a light background (use dark colors for text).",
 82 |     )
 83 |     parser.add_argument(
 84 |         "--dark-bg",
 85 |         action="store_true",
 86 |         help="Run on a dark background (use light colors for text).",
 87 |     )
 88 |     parser.add_argument(
 89 |         "--config-file", type=str, help="Location of configuration file."
 90 |     )
 91 |     parser.add_argument("--history-file", type=str, help="Location of history file.")
 92 |     parser.add_argument(
 93 |         "-V",
 94 |         "--version",
 95 |         action="version",
 96 |         version=metadata.version("ptpython"),
 97 |     )
 98 |     parser.add_argument("args", nargs="*", help="Script and arguments")
 99 |     return parser
100 | 
101 | 
102 | def get_config_and_history_file(namespace: argparse.Namespace) -> tuple[str, str]:
103 |     """
104 |     Check which config/history files to use, ensure that the directories for
105 |     these files exist, and return the config and history path.
106 |     """
107 |     config_dir = os.environ.get(
108 |         "PTPYTHON_CONFIG_HOME",
109 |         appdirs.user_config_dir("ptpython", "prompt_toolkit"),
110 |     )
111 |     data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit")
112 | 
113 |     # Create directories.
114 |     for d in (config_dir, data_dir):
115 |         pathlib.Path(d).mkdir(parents=True, exist_ok=True)
116 | 
117 |     # Determine config file to be used.
118 |     config_file = os.path.join(config_dir, "config.py")
119 |     legacy_config_file = os.path.join(os.path.expanduser("~/.ptpython"), "config.py")
120 | 
121 |     warnings = []
122 | 
123 |     # Config file
124 |     if namespace.config_file:
125 |         # Override config_file.
126 |         config_file = os.path.expanduser(namespace.config_file)
127 | 
128 |     elif os.path.isfile(legacy_config_file):
129 |         # Warn about the legacy configuration file.
130 |         warnings.append(
131 |             HTML(
132 |                 "    <i>~/.ptpython/config.py</i> is deprecated, move your configuration to <i>%s</i>\n"
133 |             )
134 |             % config_file
135 |         )
136 |         config_file = legacy_config_file
137 | 
138 |     # Determine history file to be used.
139 |     history_file = os.path.join(data_dir, "history")
140 |     legacy_history_file = os.path.join(os.path.expanduser("~/.ptpython"), "history")
141 | 
142 |     if namespace.history_file:
143 |         # Override history_file.
144 |         history_file = os.path.expanduser(namespace.history_file)
145 | 
146 |     elif os.path.isfile(legacy_history_file):
147 |         # Warn about the legacy history file.
148 |         warnings.append(
149 |             HTML(
150 |                 "    <i>~/.ptpython/history</i> is deprecated, move your history to <i>%s</i>\n"
151 |             )
152 |             % history_file
153 |         )
154 |         history_file = legacy_history_file
155 | 
156 |     # Print warnings.
157 |     if warnings:
158 |         print_formatted_text(HTML("<u>Warning:</u>"))
159 |         for w in warnings:
160 |             print_formatted_text(w)
161 | 
162 |     return config_file, history_file
163 | 
164 | 
165 | def run() -> None:
166 |     a = create_parser().parse_args()
167 | 
168 |     config_file, history_file = get_config_and_history_file(a)
169 | 
170 |     # Startup path
171 |     startup_paths = []
172 |     if "PYTHONSTARTUP" in os.environ:
173 |         startup_paths.append(os.environ["PYTHONSTARTUP"])
174 | 
175 |     # --interactive
176 |     if a.interactive and a.args:
177 |         # Note that we shouldn't run PYTHONSTARTUP when -i is given.
178 |         startup_paths = [a.args[0]]
179 |         sys.argv = a.args
180 | 
181 |     # Add the current directory to `sys.path`.
182 |     if sys.path[0] != "":
183 |         sys.path.insert(0, "")
184 | 
185 |     # When a file has been given, run that, otherwise start the shell.
186 |     if a.args and not a.interactive:
187 |         sys.argv = a.args
188 |         path = a.args[0]
189 |         with open(path, "rb") as f:
190 |             code = compile(f.read(), path, "exec")
191 |             # NOTE: We have to pass a dict as namespace. Omitting this argument
192 |             #       causes imports to not be found. See issue #326.
193 |             #       However, an empty dict sets __name__ to 'builtins', which
194 |             #       breaks `if __name__ == '__main__'` checks. See issue #444.
195 |             exec(code, {"__name__": "__main__", "__file__": path})
196 | 
197 |     # Run interactive shell.
198 |     else:
199 |         enable_deprecation_warnings()
200 | 
201 |         # Apply config file
202 |         def configure(repl: PythonRepl) -> None:
203 |             if os.path.exists(config_file):
204 |                 run_config(repl, config_file)
205 | 
206 |             # Adjust colors if dark/light background flag has been given.
207 |             if a.light_bg:
208 |                 repl.min_brightness = 0.0
209 |                 repl.max_brightness = 0.60
210 |             elif a.dark_bg:
211 |                 repl.min_brightness = 0.60
212 |                 repl.max_brightness = 1.0
213 | 
214 |         import __main__
215 | 
216 |         embed_result = embed(  # type: ignore
217 |             vi_mode=a.vi,
218 |             history_filename=history_file,
219 |             configure=configure,
220 |             locals=__main__.__dict__,
221 |             globals=__main__.__dict__,
222 |             startup_paths=startup_paths,
223 |             title="Python REPL (ptpython)",
224 |             return_asyncio_coroutine=a.asyncio,
225 |         )
226 | 
227 |         if a.asyncio:
228 |             print("Starting ptpython asyncio REPL")
229 |             print('Use "await" directly instead of "asyncio.run()".')
230 |             asyncio.run(embed_result)
231 | 
232 | 
233 | if __name__ == "__main__":
234 |     run()
235 | 


--------------------------------------------------------------------------------
/src/ptpython/eventloop.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Wrapper around the eventloop that gives some time to the Tkinter GUI to process
 3 | events when it's loaded and while we are waiting for input at the REPL. This
 4 | way we don't block the UI of for instance ``turtle`` and other Tk libraries.
 5 | 
 6 | (Normally Tkinter registers it's callbacks in ``PyOS_InputHook`` to integrate
 7 | in readline. ``prompt-toolkit`` doesn't understand that input hook, but this
 8 | will fix it for Tk.)
 9 | """
10 | 
11 | from __future__ import annotations
12 | 
13 | import sys
14 | import time
15 | 
16 | from prompt_toolkit.eventloop import InputHookContext
17 | 
18 | __all__ = ["inputhook"]
19 | 
20 | 
21 | def _inputhook_tk(inputhook_context: InputHookContext) -> None:
22 |     """
23 |     Inputhook for Tk.
24 |     Run the Tk eventloop until prompt-toolkit needs to process the next input.
25 |     """
26 |     # Get the current TK application.
27 |     import _tkinter  # Keep this imports inline!
28 |     import tkinter
29 | 
30 |     root = tkinter._default_root  # type: ignore
31 | 
32 |     def wait_using_filehandler() -> None:
33 |         """
34 |         Run the TK eventloop until the file handler that we got from the
35 |         inputhook becomes readable.
36 |         """
37 |         # Add a handler that sets the stop flag when `prompt-toolkit` has input
38 |         # to process.
39 |         stop = [False]
40 | 
41 |         def done(*a: object) -> None:
42 |             stop[0] = True
43 | 
44 |         root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done)
45 | 
46 |         # Run the TK event loop as long as we don't receive input.
47 |         while root.dooneevent(_tkinter.ALL_EVENTS):
48 |             if stop[0]:
49 |                 break
50 | 
51 |         root.deletefilehandler(inputhook_context.fileno())
52 | 
53 |     def wait_using_polling() -> None:
54 |         """
55 |         Windows TK doesn't support 'createfilehandler'.
56 |         So, run the TK eventloop and poll until input is ready.
57 |         """
58 |         while not inputhook_context.input_is_ready():
59 |             while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
60 |                 pass
61 |             # Sleep to make the CPU idle, but not too long, so that the UI
62 |             # stays responsive.
63 |             time.sleep(0.01)
64 | 
65 |     if root is not None:
66 |         if hasattr(root, "createfilehandler"):
67 |             wait_using_filehandler()
68 |         else:
69 |             wait_using_polling()
70 | 
71 | 
72 | def inputhook(inputhook_context: InputHookContext) -> None:
73 |     # Only call the real input hook when the 'Tkinter' library was loaded.
74 |     if "Tkinter" in sys.modules or "tkinter" in sys.modules:
75 |         _inputhook_tk(inputhook_context)
76 | 


--------------------------------------------------------------------------------
/src/ptpython/filters.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import TYPE_CHECKING
 4 | 
 5 | from prompt_toolkit.filters import Filter
 6 | 
 7 | if TYPE_CHECKING:
 8 |     from .python_input import PythonInput
 9 | 
10 | __all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"]
11 | 
12 | 
13 | class PythonInputFilter(Filter):
14 |     def __init__(self, python_input: PythonInput) -> None:
15 |         super().__init__()
16 |         self.python_input = python_input
17 | 
18 |     def __call__(self) -> bool:
19 |         raise NotImplementedError
20 | 
21 | 
22 | class HasSignature(PythonInputFilter):
23 |     def __call__(self) -> bool:
24 |         return bool(self.python_input.signatures)
25 | 
26 | 
27 | class ShowSidebar(PythonInputFilter):
28 |     def __call__(self) -> bool:
29 |         return self.python_input.show_sidebar
30 | 
31 | 
32 | class ShowSignature(PythonInputFilter):
33 |     def __call__(self) -> bool:
34 |         return self.python_input.show_signature
35 | 
36 | 
37 | class ShowDocstring(PythonInputFilter):
38 |     def __call__(self) -> bool:
39 |         return self.python_input.show_docstring
40 | 


--------------------------------------------------------------------------------
/src/ptpython/history_browser.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Utility to easily select lines from the history and execute them again.
  3 | 
  4 | `create_history_application` creates an `Application` instance that runs will
  5 | run as a sub application of the Repl/PythonInput.
  6 | """
  7 | 
  8 | from __future__ import annotations
  9 | 
 10 | from functools import partial
 11 | from typing import TYPE_CHECKING, Callable
 12 | 
 13 | from prompt_toolkit.application import Application
 14 | from prompt_toolkit.application.current import get_app
 15 | from prompt_toolkit.buffer import Buffer
 16 | from prompt_toolkit.document import Document
 17 | from prompt_toolkit.enums import DEFAULT_BUFFER
 18 | from prompt_toolkit.filters import Condition, has_focus
 19 | from prompt_toolkit.formatted_text.base import StyleAndTextTuples
 20 | from prompt_toolkit.formatted_text.utils import fragment_list_to_text
 21 | from prompt_toolkit.history import History
 22 | from prompt_toolkit.key_binding import KeyBindings
 23 | from prompt_toolkit.key_binding.key_processor import KeyPressEvent
 24 | from prompt_toolkit.layout.containers import (
 25 |     ConditionalContainer,
 26 |     Container,
 27 |     Float,
 28 |     FloatContainer,
 29 |     HSplit,
 30 |     ScrollOffsets,
 31 |     VSplit,
 32 |     Window,
 33 |     WindowAlign,
 34 |     WindowRenderInfo,
 35 | )
 36 | from prompt_toolkit.layout.controls import (
 37 |     BufferControl,
 38 |     FormattedTextControl,
 39 |     UIContent,
 40 | )
 41 | from prompt_toolkit.layout.dimension import Dimension as D
 42 | from prompt_toolkit.layout.layout import Layout
 43 | from prompt_toolkit.layout.margins import Margin, ScrollbarMargin
 44 | from prompt_toolkit.layout.processors import (
 45 |     Processor,
 46 |     Transformation,
 47 |     TransformationInput,
 48 | )
 49 | from prompt_toolkit.lexers import PygmentsLexer
 50 | from prompt_toolkit.mouse_events import MouseEvent
 51 | from prompt_toolkit.widgets import Frame
 52 | from prompt_toolkit.widgets.toolbars import ArgToolbar, SearchToolbar
 53 | from pygments.lexers import Python3Lexer as PythonLexer
 54 | from pygments.lexers import RstLexer
 55 | 
 56 | from ptpython.layout import get_inputmode_fragments
 57 | 
 58 | from .utils import if_mousedown
 59 | 
 60 | if TYPE_CHECKING:
 61 |     from typing_extensions import TypeAlias
 62 | 
 63 |     from .python_input import PythonInput
 64 | 
 65 | HISTORY_COUNT = 2000
 66 | 
 67 | __all__ = ["HistoryLayout", "PythonHistory"]
 68 | 
 69 | E: TypeAlias = KeyPressEvent
 70 | 
 71 | HELP_TEXT = """
 72 | This interface is meant to select multiple lines from the
 73 | history and execute them together.
 74 | 
 75 | Typical usage
 76 | -------------
 77 | 
 78 | 1. Move the ``cursor up`` in the history pane, until the
 79 |    cursor is on the first desired line.
 80 | 2. Hold down the ``space bar``, or press it multiple
 81 |    times. Each time it will select one line and move to
 82 |    the next one. Each selected line will appear on the
 83 |    right side.
 84 | 3. When all the required lines are displayed on the right
 85 |    side, press ``Enter``. This will go back to the Python
 86 |    REPL and show these lines as the current input. They
 87 |    can still be edited from there.
 88 | 
 89 | Key bindings
 90 | ------------
 91 | 
 92 | Many Emacs and Vi navigation key bindings should work.
 93 | Press ``F4`` to switch between Emacs and Vi mode.
 94 | 
 95 | Additional bindings:
 96 | 
 97 | - ``Space``: Select or delect a line.
 98 | - ``Tab``: Move the focus between the history and input
 99 |   pane. (Alternative: ``Ctrl-W``)
100 | - ``Ctrl-C``: Cancel. Ignore the result and go back to
101 |   the REPL. (Alternatives: ``q`` and ``Control-G``.)
102 | - ``Enter``: Accept the result and go back to the REPL.
103 | - ``F1``: Show/hide help. Press ``Enter`` to quit this
104 |   help message.
105 | 
106 | Further, remember that searching works like in Emacs
107 | (using ``Ctrl-R``) or Vi (using ``/``).
108 | """
109 | 
110 | 
111 | class BORDER:
112 |     "Box drawing characters."
113 | 
114 |     HORIZONTAL = "\u2501"
115 |     VERTICAL = "\u2503"
116 |     TOP_LEFT = "\u250f"
117 |     TOP_RIGHT = "\u2513"
118 |     BOTTOM_LEFT = "\u2517"
119 |     BOTTOM_RIGHT = "\u251b"
120 |     LIGHT_VERTICAL = "\u2502"
121 | 
122 | 
123 | def _create_popup_window(title: str, body: Container) -> Frame:
124 |     """
125 |     Return the layout for a pop-up window. It consists of a title bar showing
126 |     the `title` text, and a body layout. The window is surrounded by borders.
127 |     """
128 |     return Frame(body=body, title=title)
129 | 
130 | 
131 | class HistoryLayout:
132 |     """
133 |     Create and return a `Container` instance for the history
134 |     application.
135 |     """
136 | 
137 |     def __init__(self, history: PythonHistory) -> None:
138 |         search_toolbar = SearchToolbar()
139 | 
140 |         self.help_buffer_control = BufferControl(
141 |             buffer=history.help_buffer, lexer=PygmentsLexer(RstLexer)
142 |         )
143 | 
144 |         help_window = _create_popup_window(
145 |             title="History Help",
146 |             body=Window(
147 |                 content=self.help_buffer_control,
148 |                 right_margins=[ScrollbarMargin(display_arrows=True)],
149 |                 scroll_offsets=ScrollOffsets(top=2, bottom=2),
150 |             ),
151 |         )
152 | 
153 |         self.default_buffer_control = BufferControl(
154 |             buffer=history.default_buffer,
155 |             input_processors=[GrayExistingText(history.history_mapping)],
156 |             lexer=PygmentsLexer(PythonLexer),
157 |         )
158 | 
159 |         self.history_buffer_control = BufferControl(
160 |             buffer=history.history_buffer,
161 |             lexer=PygmentsLexer(PythonLexer),
162 |             search_buffer_control=search_toolbar.control,
163 |             preview_search=True,
164 |         )
165 | 
166 |         history_window = Window(
167 |             content=self.history_buffer_control,
168 |             wrap_lines=False,
169 |             left_margins=[HistoryMargin(history)],
170 |             scroll_offsets=ScrollOffsets(top=2, bottom=2),
171 |         )
172 | 
173 |         self.root_container = HSplit(
174 |             [
175 |                 #  Top title bar.
176 |                 Window(
177 |                     content=FormattedTextControl(_get_top_toolbar_fragments),
178 |                     align=WindowAlign.CENTER,
179 |                     style="class:status-toolbar",
180 |                 ),
181 |                 FloatContainer(
182 |                     content=VSplit(
183 |                         [
184 |                             # Left side: history.
185 |                             history_window,
186 |                             # Separator.
187 |                             Window(
188 |                                 width=D.exact(1),
189 |                                 char=BORDER.LIGHT_VERTICAL,
190 |                                 style="class:separator",
191 |                             ),
192 |                             # Right side: result.
193 |                             Window(
194 |                                 content=self.default_buffer_control,
195 |                                 wrap_lines=False,
196 |                                 left_margins=[ResultMargin(history)],
197 |                                 scroll_offsets=ScrollOffsets(top=2, bottom=2),
198 |                             ),
199 |                         ]
200 |                     ),
201 |                     floats=[
202 |                         # Help text as a float.
203 |                         Float(
204 |                             width=60,
205 |                             top=3,
206 |                             bottom=2,
207 |                             content=ConditionalContainer(
208 |                                 content=help_window,
209 |                                 filter=has_focus(history.help_buffer),
210 |                             ),
211 |                         )
212 |                     ],
213 |                 ),
214 |                 # Bottom toolbars.
215 |                 ArgToolbar(),
216 |                 search_toolbar,
217 |                 Window(
218 |                     content=FormattedTextControl(
219 |                         partial(_get_bottom_toolbar_fragments, history=history)
220 |                     ),
221 |                     style="class:status-toolbar",
222 |                 ),
223 |             ]
224 |         )
225 | 
226 |         self.layout = Layout(self.root_container, history_window)
227 | 
228 | 
229 | def _get_top_toolbar_fragments() -> StyleAndTextTuples:
230 |     return [("class:status-bar.title", "History browser - Insert from history")]
231 | 
232 | 
233 | def _get_bottom_toolbar_fragments(history: PythonHistory) -> StyleAndTextTuples:
234 |     python_input = history.python_input
235 | 
236 |     @if_mousedown
237 |     def f1(mouse_event: MouseEvent) -> None:
238 |         _toggle_help(history)
239 | 
240 |     @if_mousedown
241 |     def tab(mouse_event: MouseEvent) -> None:
242 |         _select_other_window(history)
243 | 
244 |     return (
245 |         [("class:status-toolbar", " ")]
246 |         + get_inputmode_fragments(python_input)
247 |         + [
248 |             ("class:status-toolbar", " "),
249 |             ("class:status-toolbar.key", "[Space]"),
250 |             ("class:status-toolbar", " Toggle "),
251 |             ("class:status-toolbar.key", "[Tab]", tab),
252 |             ("class:status-toolbar", " Focus ", tab),
253 |             ("class:status-toolbar.key", "[Enter]"),
254 |             ("class:status-toolbar", " Accept "),
255 |             ("class:status-toolbar.key", "[F1]", f1),
256 |             ("class:status-toolbar", " Help ", f1),
257 |         ]
258 |     )
259 | 
260 | 
261 | class HistoryMargin(Margin):
262 |     """
263 |     Margin for the history buffer.
264 |     This displays a green bar for the selected entries.
265 |     """
266 | 
267 |     def __init__(self, history: PythonHistory) -> None:
268 |         self.history_buffer = history.history_buffer
269 |         self.history_mapping = history.history_mapping
270 | 
271 |     def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
272 |         return 2
273 | 
274 |     def create_margin(
275 |         self, window_render_info: WindowRenderInfo, width: int, height: int
276 |     ) -> StyleAndTextTuples:
277 |         document = self.history_buffer.document
278 | 
279 |         lines_starting_new_entries = self.history_mapping.lines_starting_new_entries
280 |         selected_lines = self.history_mapping.selected_lines
281 | 
282 |         current_lineno = document.cursor_position_row
283 | 
284 |         visible_line_to_input_line = window_render_info.visible_line_to_input_line
285 |         result: StyleAndTextTuples = []
286 | 
287 |         for y in range(height):
288 |             line_number = visible_line_to_input_line.get(y)
289 | 
290 |             # Show stars at the start of each entry.
291 |             # (Visualises multiline entries.)
292 |             if line_number in lines_starting_new_entries:
293 |                 char = "*"
294 |             else:
295 |                 char = " "
296 | 
297 |             if line_number in selected_lines:
298 |                 t = "class:history-line,selected"
299 |             else:
300 |                 t = "class:history-line"
301 | 
302 |             if line_number == current_lineno:
303 |                 t = t + ",current"
304 | 
305 |             result.append((t, char))
306 |             result.append(("", "\n"))
307 | 
308 |         return result
309 | 
310 | 
311 | class ResultMargin(Margin):
312 |     """
313 |     The margin to be shown in the result pane.
314 |     """
315 | 
316 |     def __init__(self, history: PythonHistory) -> None:
317 |         self.history_mapping = history.history_mapping
318 |         self.history_buffer = history.history_buffer
319 | 
320 |     def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
321 |         return 2
322 | 
323 |     def create_margin(
324 |         self, window_render_info: WindowRenderInfo, width: int, height: int
325 |     ) -> StyleAndTextTuples:
326 |         document = self.history_buffer.document
327 | 
328 |         current_lineno = document.cursor_position_row
329 |         offset = (
330 |             self.history_mapping.result_line_offset
331 |         )  # original_document.cursor_position_row
332 | 
333 |         visible_line_to_input_line = window_render_info.visible_line_to_input_line
334 | 
335 |         result: StyleAndTextTuples = []
336 | 
337 |         for y in range(height):
338 |             line_number = visible_line_to_input_line.get(y)
339 | 
340 |             if (
341 |                 line_number is None
342 |                 or line_number < offset
343 |                 or line_number >= offset + len(self.history_mapping.selected_lines)
344 |             ):
345 |                 t = ""
346 |             elif line_number == current_lineno:
347 |                 t = "class:history-line,selected,current"
348 |             else:
349 |                 t = "class:history-line,selected"
350 | 
351 |             result.append((t, " "))
352 |             result.append(("", "\n"))
353 | 
354 |         return result
355 | 
356 |     def invalidation_hash(self, document: Document) -> int:
357 |         return document.cursor_position_row
358 | 
359 | 
360 | class GrayExistingText(Processor):
361 |     """
362 |     Turn the existing input, before and after the inserted code gray.
363 |     """
364 | 
365 |     def __init__(self, history_mapping: HistoryMapping) -> None:
366 |         self.history_mapping = history_mapping
367 |         self._lines_before = len(
368 |             history_mapping.original_document.text_before_cursor.splitlines()
369 |         )
370 | 
371 |     def apply_transformation(
372 |         self, transformation_input: TransformationInput
373 |     ) -> Transformation:
374 |         lineno = transformation_input.lineno
375 |         fragments = transformation_input.fragments
376 | 
377 |         if lineno < self._lines_before or lineno >= self._lines_before + len(
378 |             self.history_mapping.selected_lines
379 |         ):
380 |             text = fragment_list_to_text(fragments)
381 |             return Transformation(fragments=[("class:history.existing-input", text)])
382 |         else:
383 |             return Transformation(fragments=fragments)
384 | 
385 | 
386 | class HistoryMapping:
387 |     """
388 |     Keep a list of all the lines from the history and the selected lines.
389 |     """
390 | 
391 |     def __init__(
392 |         self,
393 |         history: PythonHistory,
394 |         python_history: History,
395 |         original_document: Document,
396 |     ) -> None:
397 |         self.history = history
398 |         self.python_history = python_history
399 |         self.original_document = original_document
400 | 
401 |         self.lines_starting_new_entries = set()
402 |         self.selected_lines: set[int] = set()
403 | 
404 |         # Process history.
405 |         history_strings = python_history.get_strings()
406 |         history_lines: list[str] = []
407 | 
408 |         for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]:
409 |             self.lines_starting_new_entries.add(len(history_lines))
410 | 
411 |             for line in entry.splitlines():
412 |                 history_lines.append(line)
413 | 
414 |         if len(history_strings) > HISTORY_COUNT:
415 |             history_lines[0] = (
416 |                 f"# *** History has been truncated to {HISTORY_COUNT} lines ***"
417 |             )
418 | 
419 |         self.history_lines = history_lines
420 |         self.concatenated_history = "\n".join(history_lines)
421 | 
422 |         # Line offset.
423 |         if self.original_document.text_before_cursor:
424 |             self.result_line_offset = self.original_document.cursor_position_row + 1
425 |         else:
426 |             self.result_line_offset = 0
427 | 
428 |     def get_new_document(self, cursor_pos: int | None = None) -> Document:
429 |         """
430 |         Create a `Document` instance that contains the resulting text.
431 |         """
432 |         lines = []
433 | 
434 |         # Original text, before cursor.
435 |         if self.original_document.text_before_cursor:
436 |             lines.append(self.original_document.text_before_cursor)
437 | 
438 |         # Selected entries from the history.
439 |         for line_no in sorted(self.selected_lines):
440 |             lines.append(self.history_lines[line_no])
441 | 
442 |         # Original text, after cursor.
443 |         if self.original_document.text_after_cursor:
444 |             lines.append(self.original_document.text_after_cursor)
445 | 
446 |         # Create `Document` with cursor at the right position.
447 |         text = "\n".join(lines)
448 |         if cursor_pos is not None and cursor_pos > len(text):
449 |             cursor_pos = len(text)
450 |         return Document(text, cursor_pos)
451 | 
452 |     def update_default_buffer(self) -> None:
453 |         b = self.history.default_buffer
454 | 
455 |         b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True)
456 | 
457 | 
458 | def _toggle_help(history: PythonHistory) -> None:
459 |     "Display/hide help."
460 |     help_buffer_control = history.history_layout.help_buffer_control
461 | 
462 |     if history.app.layout.current_control == help_buffer_control:
463 |         history.app.layout.focus_previous()
464 |     else:
465 |         history.app.layout.current_control = help_buffer_control
466 | 
467 | 
468 | def _select_other_window(history: PythonHistory) -> None:
469 |     "Toggle focus between left/right window."
470 |     current_buffer = history.app.current_buffer
471 |     layout = history.history_layout.layout
472 | 
473 |     if current_buffer == history.history_buffer:
474 |         layout.current_control = history.history_layout.default_buffer_control
475 | 
476 |     elif current_buffer == history.default_buffer:
477 |         layout.current_control = history.history_layout.history_buffer_control
478 | 
479 | 
480 | def create_key_bindings(
481 |     history: PythonHistory,
482 |     python_input: PythonInput,
483 |     history_mapping: HistoryMapping,
484 | ) -> KeyBindings:
485 |     """
486 |     Key bindings.
487 |     """
488 |     bindings = KeyBindings()
489 |     handle = bindings.add
490 | 
491 |     @handle(" ", filter=has_focus(history.history_buffer))
492 |     def _(event: E) -> None:
493 |         """
494 |         Space: select/deselect line from history pane.
495 |         """
496 |         b = event.current_buffer
497 |         line_no = b.document.cursor_position_row
498 | 
499 |         if not history_mapping.history_lines:
500 |             # If we've no history, then nothing to do
501 |             return
502 | 
503 |         if line_no in history_mapping.selected_lines:
504 |             # Remove line.
505 |             history_mapping.selected_lines.remove(line_no)
506 |             history_mapping.update_default_buffer()
507 |         else:
508 |             # Add line.
509 |             history_mapping.selected_lines.add(line_no)
510 |             history_mapping.update_default_buffer()
511 | 
512 |             # Update cursor position
513 |             default_buffer = history.default_buffer
514 |             default_lineno = (
515 |                 sorted(history_mapping.selected_lines).index(line_no)
516 |                 + history_mapping.result_line_offset
517 |             )
518 |             default_buffer.cursor_position = (
519 |                 default_buffer.document.translate_row_col_to_index(default_lineno, 0)
520 |             )
521 | 
522 |         # Also move the cursor to the next line. (This way they can hold
523 |         # space to select a region.)
524 |         b.cursor_position = b.document.translate_row_col_to_index(line_no + 1, 0)
525 | 
526 |     @handle(" ", filter=has_focus(DEFAULT_BUFFER))
527 |     @handle("delete", filter=has_focus(DEFAULT_BUFFER))
528 |     @handle("c-h", filter=has_focus(DEFAULT_BUFFER))
529 |     def _(event: E) -> None:
530 |         """
531 |         Space: remove line from default pane.
532 |         """
533 |         b = event.current_buffer
534 |         line_no = b.document.cursor_position_row - history_mapping.result_line_offset
535 | 
536 |         if line_no >= 0:
537 |             try:
538 |                 history_lineno = sorted(history_mapping.selected_lines)[line_no]
539 |             except IndexError:
540 |                 pass  # When `selected_lines` is an empty set.
541 |             else:
542 |                 history_mapping.selected_lines.remove(history_lineno)
543 | 
544 |             history_mapping.update_default_buffer()
545 | 
546 |     help_focussed = has_focus(history.help_buffer)
547 |     main_buffer_focussed = has_focus(history.history_buffer) | has_focus(
548 |         history.default_buffer
549 |     )
550 | 
551 |     @handle("tab", filter=main_buffer_focussed)
552 |     @handle("c-x", filter=main_buffer_focussed, eager=True)
553 |     # Eager: ignore the Emacs [Ctrl-X Ctrl-X] binding.
554 |     @handle("c-w", filter=main_buffer_focussed)
555 |     def _(event: E) -> None:
556 |         "Select other window."
557 |         _select_other_window(history)
558 | 
559 |     @handle("f4")
560 |     def _(event: E) -> None:
561 |         "Switch between Emacs/Vi mode."
562 |         python_input.vi_mode = not python_input.vi_mode
563 | 
564 |     @handle("f1")
565 |     def _(event: E) -> None:
566 |         "Display/hide help."
567 |         _toggle_help(history)
568 | 
569 |     @handle("enter", filter=help_focussed)
570 |     @handle("c-c", filter=help_focussed)
571 |     @handle("c-g", filter=help_focussed)
572 |     @handle("escape", filter=help_focussed)
573 |     def _(event: E) -> None:
574 |         "Leave help."
575 |         event.app.layout.focus_previous()
576 | 
577 |     @handle("q", filter=main_buffer_focussed)
578 |     @handle("f3", filter=main_buffer_focussed)
579 |     @handle("c-c", filter=main_buffer_focussed)
580 |     @handle("c-g", filter=main_buffer_focussed)
581 |     def _(event: E) -> None:
582 |         "Cancel and go back."
583 |         event.app.exit(result=None)
584 | 
585 |     @handle("enter", filter=main_buffer_focussed)
586 |     def _(event: E) -> None:
587 |         "Accept input."
588 |         event.app.exit(result=history.default_buffer.text)
589 | 
590 |     enable_system_bindings = Condition(lambda: python_input.enable_system_bindings)
591 | 
592 |     @handle("c-z", filter=enable_system_bindings)
593 |     def _(event: E) -> None:
594 |         "Suspend to background."
595 |         event.app.suspend_to_background()
596 | 
597 |     return bindings
598 | 
599 | 
600 | class PythonHistory:
601 |     def __init__(self, python_input: PythonInput, original_document: Document) -> None:
602 |         """
603 |         Create an `Application` for the history screen.
604 |         This has to be run as a sub application of `python_input`.
605 | 
606 |         When this application runs and returns, it returns the selected lines.
607 |         """
608 |         self.python_input = python_input
609 | 
610 |         history_mapping = HistoryMapping(self, python_input.history, original_document)
611 |         self.history_mapping = history_mapping
612 | 
613 |         document = Document(history_mapping.concatenated_history)
614 |         document = Document(
615 |             document.text,
616 |             cursor_position=document.cursor_position
617 |             + document.get_start_of_line_position(),
618 |         )
619 | 
620 |         def accept_handler(buffer: Buffer) -> bool:
621 |             get_app().exit(result=self.default_buffer.text)
622 |             return False
623 | 
624 |         self.history_buffer = Buffer(
625 |             document=document,
626 |             on_cursor_position_changed=self._history_buffer_pos_changed,
627 |             accept_handler=accept_handler,
628 |             read_only=True,
629 |         )
630 | 
631 |         self.default_buffer = Buffer(
632 |             name=DEFAULT_BUFFER,
633 |             document=history_mapping.get_new_document(),
634 |             on_cursor_position_changed=self._default_buffer_pos_changed,
635 |             read_only=True,
636 |         )
637 | 
638 |         self.help_buffer = Buffer(document=Document(HELP_TEXT, 0), read_only=True)
639 | 
640 |         self.history_layout = HistoryLayout(self)
641 | 
642 |         self.app: Application[str] = Application(
643 |             layout=self.history_layout.layout,
644 |             full_screen=True,
645 |             style=python_input._current_style,
646 |             style_transformation=python_input.style_transformation,
647 |             mouse_support=Condition(lambda: python_input.enable_mouse_support),
648 |             key_bindings=create_key_bindings(self, python_input, history_mapping),
649 |         )
650 | 
651 |     def _default_buffer_pos_changed(self, _: Buffer) -> None:
652 |         """When the cursor changes in the default buffer. Synchronize with
653 |         history buffer."""
654 |         # Only when this buffer has the focus.
655 |         if self.app.current_buffer == self.default_buffer:
656 |             try:
657 |                 line_no = (
658 |                     self.default_buffer.document.cursor_position_row
659 |                     - self.history_mapping.result_line_offset
660 |                 )
661 | 
662 |                 if line_no < 0:  # When the cursor is above the inserted region.
663 |                     raise IndexError
664 | 
665 |                 history_lineno = sorted(self.history_mapping.selected_lines)[line_no]
666 |             except IndexError:
667 |                 pass
668 |             else:
669 |                 self.history_buffer.cursor_position = (
670 |                     self.history_buffer.document.translate_row_col_to_index(
671 |                         history_lineno, 0
672 |                     )
673 |                 )
674 | 
675 |     def _history_buffer_pos_changed(self, _: Buffer) -> None:
676 |         """When the cursor changes in the history buffer. Synchronize."""
677 |         # Only when this buffer has the focus.
678 |         if self.app.current_buffer == self.history_buffer:
679 |             line_no = self.history_buffer.document.cursor_position_row
680 | 
681 |             if line_no in self.history_mapping.selected_lines:
682 |                 default_lineno = (
683 |                     sorted(self.history_mapping.selected_lines).index(line_no)
684 |                     + self.history_mapping.result_line_offset
685 |                 )
686 | 
687 |                 self.default_buffer.cursor_position = (
688 |                     self.default_buffer.document.translate_row_col_to_index(
689 |                         default_lineno, 0
690 |                     )
691 |                 )
692 | 


--------------------------------------------------------------------------------
/src/ptpython/ipython.py:
--------------------------------------------------------------------------------
  1 | """
  2 | 
  3 | Adaptor for using the input system of `prompt_toolkit` with the IPython
  4 | backend.
  5 | 
  6 | This gives a powerful interactive shell that has a nice user interface, but
  7 | also the power of for instance all the %-magic functions that IPython has to
  8 | offer.
  9 | 
 10 | """
 11 | 
 12 | from __future__ import annotations
 13 | 
 14 | from typing import Iterable
 15 | from warnings import warn
 16 | 
 17 | from IPython import utils as ipy_utils
 18 | from IPython.core.inputtransformer2 import TransformerManager
 19 | from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed
 20 | from IPython.terminal.ipapp import load_default_config
 21 | from prompt_toolkit.completion import (
 22 |     CompleteEvent,
 23 |     Completer,
 24 |     Completion,
 25 |     PathCompleter,
 26 |     WordCompleter,
 27 | )
 28 | from prompt_toolkit.contrib.completers import SystemCompleter
 29 | from prompt_toolkit.contrib.regular_languages.compiler import compile
 30 | from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
 31 | from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer
 32 | from prompt_toolkit.document import Document
 33 | from prompt_toolkit.formatted_text import AnyFormattedText, PygmentsTokens
 34 | from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer
 35 | from prompt_toolkit.styles import Style
 36 | from pygments.lexers import BashLexer, PythonLexer
 37 | 
 38 | from ptpython.prompt_style import PromptStyle
 39 | 
 40 | from .completer import PythonCompleter
 41 | from .python_input import PythonInput
 42 | from .repl import PyCF_ALLOW_TOP_LEVEL_AWAIT
 43 | from .style import default_ui_style
 44 | from .validator import PythonValidator
 45 | 
 46 | __all__ = ["embed"]
 47 | 
 48 | 
 49 | class IPythonPrompt(PromptStyle):
 50 |     """
 51 |     Style for IPython >5.0, use the prompt_toolkit tokens directly.
 52 |     """
 53 | 
 54 |     def __init__(self, prompts):
 55 |         self.prompts = prompts
 56 | 
 57 |     def in_prompt(self) -> AnyFormattedText:
 58 |         return PygmentsTokens(self.prompts.in_prompt_tokens())
 59 | 
 60 |     def in2_prompt(self, width: int) -> AnyFormattedText:
 61 |         return PygmentsTokens(self.prompts.continuation_prompt_tokens())
 62 | 
 63 |     def out_prompt(self) -> AnyFormattedText:
 64 |         return []
 65 | 
 66 | 
 67 | class IPythonValidator(PythonValidator):
 68 |     def __init__(self, *args, **kwargs):
 69 |         super().__init__(*args, **kwargs)
 70 |         self.isp = TransformerManager()
 71 | 
 72 |     def validate(self, document: Document) -> None:
 73 |         document = Document(text=self.isp.transform_cell(document.text))
 74 |         super().validate(document)
 75 | 
 76 | 
 77 | def create_ipython_grammar():
 78 |     """
 79 |     Return compiled IPython grammar.
 80 |     """
 81 |     return compile(
 82 |         r"""
 83 |         \s*
 84 |         (
 85 |             (?P<percent>%)(
 86 |                 (?P<magic>pycat|run|loadpy|load)  \s+ (?P<py_filename>[^\s]+)  |
 87 |                 (?P<magic>cat)                    \s+ (?P<filename>[^\s]+)     |
 88 |                 (?P<magic>pushd|cd|ls)            \s+ (?P<directory>[^\s]+)    |
 89 |                 (?P<magic>pdb)                    \s+ (?P<pdb_arg>[^\s]+)      |
 90 |                 (?P<magic>autocall)               \s+ (?P<autocall_arg>[^\s]+) |
 91 |                 (?P<magic>time|timeit|prun)       \s+ (?P<python>.+)           |
 92 |                 (?P<magic>psource|pfile|pinfo|pinfo2) \s+ (?P<python>.+)       |
 93 |                 (?P<magic>system)                 \s+ (?P<system>.+)           |
 94 |                 (?P<magic>unalias)                \s+ (?P<alias_name>.+)       |
 95 |                 (?P<magic>[^\s]+)   .* |
 96 |             ) .*            |
 97 |             !(?P<system>.+) |
 98 |             (?![%!]) (?P<python>.+)
 99 |         )
100 |         \s*
101 |     """
102 |     )
103 | 
104 | 
105 | def create_completer(
106 |     get_globals,
107 |     get_locals,
108 |     magics_manager,
109 |     alias_manager,
110 |     get_enable_dictionary_completion,
111 | ):
112 |     g = create_ipython_grammar()
113 | 
114 |     return GrammarCompleter(
115 |         g,
116 |         {
117 |             "python": PythonCompleter(
118 |                 get_globals, get_locals, get_enable_dictionary_completion
119 |             ),
120 |             "magic": MagicsCompleter(magics_manager),
121 |             "alias_name": AliasCompleter(alias_manager),
122 |             "pdb_arg": WordCompleter(["on", "off"], ignore_case=True),
123 |             "autocall_arg": WordCompleter(["0", "1", "2"], ignore_case=True),
124 |             "py_filename": PathCompleter(
125 |                 only_directories=False, file_filter=lambda name: name.endswith(".py")
126 |             ),
127 |             "filename": PathCompleter(only_directories=False),
128 |             "directory": PathCompleter(only_directories=True),
129 |             "system": SystemCompleter(),
130 |         },
131 |     )
132 | 
133 | 
134 | def create_lexer():
135 |     g = create_ipython_grammar()
136 | 
137 |     return GrammarLexer(
138 |         g,
139 |         lexers={
140 |             "percent": SimpleLexer("class:pygments.operator"),
141 |             "magic": SimpleLexer("class:pygments.keyword"),
142 |             "filename": SimpleLexer("class:pygments.name"),
143 |             "python": PygmentsLexer(PythonLexer),
144 |             "system": PygmentsLexer(BashLexer),
145 |         },
146 |     )
147 | 
148 | 
149 | class MagicsCompleter(Completer):
150 |     def __init__(self, magics_manager):
151 |         self.magics_manager = magics_manager
152 | 
153 |     def get_completions(
154 |         self, document: Document, complete_event: CompleteEvent
155 |     ) -> Iterable[Completion]:
156 |         text = document.text_before_cursor.lstrip()
157 | 
158 |         for m in sorted(self.magics_manager.magics["line"]):
159 |             if m.startswith(text):
160 |                 yield Completion(f"{m}", -len(text))
161 | 
162 | 
163 | class AliasCompleter(Completer):
164 |     def __init__(self, alias_manager):
165 |         self.alias_manager = alias_manager
166 | 
167 |     def get_completions(
168 |         self, document: Document, complete_event: CompleteEvent
169 |     ) -> Iterable[Completion]:
170 |         text = document.text_before_cursor.lstrip()
171 |         # aliases = [a for a, _ in self.alias_manager.aliases]
172 |         aliases = self.alias_manager.aliases
173 | 
174 |         for a, cmd in sorted(aliases, key=lambda a: a[0]):
175 |             if a.startswith(text):
176 |                 yield Completion(f"{a}", -len(text), display_meta=cmd)
177 | 
178 | 
179 | class IPythonInput(PythonInput):
180 |     """
181 |     Override our `PythonCommandLineInterface` to add IPython specific stuff.
182 |     """
183 | 
184 |     def __init__(self, ipython_shell, *a, **kw):
185 |         kw["_completer"] = create_completer(
186 |             kw["get_globals"],
187 |             kw["get_globals"],
188 |             ipython_shell.magics_manager,
189 |             ipython_shell.alias_manager,
190 |             lambda: self.enable_dictionary_completion,
191 |         )
192 |         kw["_lexer"] = create_lexer()
193 |         kw["_validator"] = IPythonValidator(get_compiler_flags=self.get_compiler_flags)
194 | 
195 |         super().__init__(*a, **kw)
196 |         self.ipython_shell = ipython_shell
197 | 
198 |         self.all_prompt_styles["ipython"] = IPythonPrompt(ipython_shell.prompts)
199 |         self.prompt_style = "ipython"
200 | 
201 |         # UI style for IPython. Add tokens that are used by IPython>5.0
202 |         style_dict = {}
203 |         style_dict.update(default_ui_style)
204 |         style_dict.update(
205 |             {
206 |                 "pygments.prompt": "#009900",
207 |                 "pygments.prompt-num": "#00ff00 bold",
208 |                 "pygments.out-prompt": "#990000",
209 |                 "pygments.out-prompt-num": "#ff0000 bold",
210 |             }
211 |         )
212 | 
213 |         self.ui_styles = {"default": Style.from_dict(style_dict)}
214 |         self.use_ui_colorscheme("default")
215 | 
216 |     def get_compiler_flags(self):
217 |         flags = super().get_compiler_flags()
218 |         if self.ipython_shell.autoawait:
219 |             flags |= PyCF_ALLOW_TOP_LEVEL_AWAIT
220 |         return flags
221 | 
222 | 
223 | class InteractiveShellEmbed(_InteractiveShellEmbed):
224 |     """
225 |     Override the `InteractiveShellEmbed` from IPython, to replace the front-end
226 |     with our input shell.
227 | 
228 |     :param configure: Callable for configuring the repl.
229 |     """
230 | 
231 |     def __init__(self, *a, **kw):
232 |         vi_mode = kw.pop("vi_mode", False)
233 |         history_filename = kw.pop("history_filename", None)
234 |         configure = kw.pop("configure", None)
235 |         title = kw.pop("title", None)
236 | 
237 |         # Don't ask IPython to confirm for exit. We have our own exit prompt.
238 |         self.confirm_exit = False
239 | 
240 |         super().__init__(*a, **kw)
241 | 
242 |         def get_globals():
243 |             return self.user_ns
244 | 
245 |         python_input = IPythonInput(
246 |             self,
247 |             get_globals=get_globals,
248 |             vi_mode=vi_mode,
249 |             history_filename=history_filename,
250 |         )
251 | 
252 |         if title:
253 |             python_input.terminal_title = title
254 | 
255 |         if configure:
256 |             configure(python_input)
257 |             python_input.prompt_style = "ipython"  # Don't take from config.
258 | 
259 |         self.python_input = python_input
260 | 
261 |     def prompt_for_code(self) -> str:
262 |         try:
263 |             return self.python_input.app.run()
264 |         except KeyboardInterrupt:
265 |             self.python_input.default_buffer.document = Document()
266 |             return ""
267 | 
268 | 
269 | def initialize_extensions(shell, extensions):
270 |     """
271 |     Partial copy of `InteractiveShellApp.init_extensions` from IPython.
272 |     """
273 |     try:
274 |         iter(extensions)
275 |     except TypeError:
276 |         pass  # no extensions found
277 |     else:
278 |         for ext in extensions:
279 |             try:
280 |                 shell.extension_manager.load_extension(ext)
281 |             except:
282 |                 warn(
283 |                     f"Error in loading extension: {ext}"
284 |                     + f"\nCheck your config files in {ipy_utils.path.get_ipython_dir()}"
285 |                 )
286 |                 shell.showtraceback()
287 | 
288 | 
289 | def run_exec_lines(shell, exec_lines):
290 |     """
291 |     Partial copy of  run_exec_lines code from IPython.core.shellapp .
292 |     """
293 |     try:
294 |         iter(exec_lines)
295 |     except TypeError:
296 |         pass
297 |     else:
298 |         try:
299 |             for line in exec_lines:
300 |                 try:
301 |                     shell.run_cell(line, store_history=False)
302 |                 except:
303 |                     shell.showtraceback()
304 |         except:
305 |             shell.showtraceback()
306 | 
307 | 
308 | def embed(**kwargs):
309 |     """
310 |     Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead.
311 |     """
312 |     config = kwargs.get("config")
313 |     header = kwargs.pop("header", "")
314 |     compile_flags = kwargs.pop("compile_flags", None)
315 |     if config is None:
316 |         config = load_default_config()
317 |         config.InteractiveShellEmbed = config.TerminalInteractiveShell
318 |         kwargs["config"] = config
319 |     shell = InteractiveShellEmbed.instance(**kwargs)
320 |     initialize_extensions(shell, config["InteractiveShellApp"]["extensions"])
321 |     run_exec_lines(shell, config["InteractiveShellApp"]["exec_lines"])
322 |     run_startup_scripts(shell)
323 |     shell(header=header, stack_depth=2, compile_flags=compile_flags)
324 | 
325 | 
326 | def run_startup_scripts(shell):
327 |     """
328 |     Contributed by linyuxu:
329 |     https://github.com/prompt-toolkit/ptpython/issues/126#issue-161242480
330 |     """
331 |     import glob
332 |     import os
333 | 
334 |     startup_dir = shell.profile_dir.startup_dir
335 |     startup_files = []
336 |     startup_files += glob.glob(os.path.join(startup_dir, "*.py"))
337 |     startup_files += glob.glob(os.path.join(startup_dir, "*.ipy"))
338 |     for file in startup_files:
339 |         shell.run_cell(open(file).read())
340 | 


--------------------------------------------------------------------------------
/src/ptpython/key_bindings.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | from typing import TYPE_CHECKING
  4 | 
  5 | from prompt_toolkit.application import get_app
  6 | from prompt_toolkit.buffer import Buffer
  7 | from prompt_toolkit.document import Document
  8 | from prompt_toolkit.enums import DEFAULT_BUFFER
  9 | from prompt_toolkit.filters import (
 10 |     Condition,
 11 |     emacs_insert_mode,
 12 |     emacs_mode,
 13 |     has_focus,
 14 |     has_selection,
 15 |     vi_insert_mode,
 16 | )
 17 | from prompt_toolkit.key_binding import KeyBindings
 18 | from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
 19 | from prompt_toolkit.key_binding.key_processor import KeyPressEvent
 20 | from prompt_toolkit.keys import Keys
 21 | 
 22 | from .utils import document_is_multiline_python
 23 | 
 24 | if TYPE_CHECKING:
 25 |     from typing_extensions import TypeAlias
 26 | 
 27 |     from .python_input import PythonInput
 28 | 
 29 | __all__ = [
 30 |     "load_python_bindings",
 31 |     "load_sidebar_bindings",
 32 |     "load_confirm_exit_bindings",
 33 | ]
 34 | 
 35 | E: TypeAlias = KeyPressEvent
 36 | 
 37 | 
 38 | @Condition
 39 | def tab_should_insert_whitespace() -> bool:
 40 |     """
 41 |     When the 'tab' key is pressed with only whitespace character before the
 42 |     cursor, do autocompletion. Otherwise, insert indentation.
 43 | 
 44 |     Except for the first character at the first line. Then always do a
 45 |     completion. It doesn't make sense to start the first line with
 46 |     indentation.
 47 |     """
 48 |     b = get_app().current_buffer
 49 |     before_cursor = b.document.current_line_before_cursor
 50 | 
 51 |     return bool(b.text and (not before_cursor or before_cursor.isspace()))
 52 | 
 53 | 
 54 | def load_python_bindings(python_input: PythonInput) -> KeyBindings:
 55 |     """
 56 |     Custom key bindings.
 57 |     """
 58 |     bindings = KeyBindings()
 59 | 
 60 |     sidebar_visible = Condition(lambda: python_input.show_sidebar)
 61 |     handle = bindings.add
 62 | 
 63 |     @handle("c-l")
 64 |     def _(event: E) -> None:
 65 |         """
 66 |         Clear whole screen and render again -- also when the sidebar is visible.
 67 |         """
 68 |         event.app.renderer.clear()
 69 | 
 70 |     @handle("c-z")
 71 |     def _(event: E) -> None:
 72 |         """
 73 |         Suspend.
 74 |         """
 75 |         if python_input.enable_system_bindings:
 76 |             event.app.suspend_to_background()
 77 | 
 78 |     # Delete word before cursor, but use all Python symbols as separators
 79 |     # (WORD=False).
 80 |     handle("c-w")(get_by_name("backward-kill-word"))
 81 | 
 82 |     @handle("f2")
 83 |     def _(event: E) -> None:
 84 |         """
 85 |         Show/hide sidebar.
 86 |         """
 87 |         python_input.show_sidebar = not python_input.show_sidebar
 88 |         if python_input.show_sidebar:
 89 |             event.app.layout.focus(python_input.ptpython_layout.sidebar)
 90 |         else:
 91 |             event.app.layout.focus_last()
 92 | 
 93 |     @handle("f3")
 94 |     def _(event: E) -> None:
 95 |         """
 96 |         Select from the history.
 97 |         """
 98 |         python_input.enter_history()
 99 | 
100 |     @handle("f4")
101 |     def _(event: E) -> None:
102 |         """
103 |         Toggle between Vi and Emacs mode.
104 |         """
105 |         python_input.vi_mode = not python_input.vi_mode
106 | 
107 |     @handle("f6")
108 |     def _(event: E) -> None:
109 |         """
110 |         Enable/Disable paste mode.
111 |         """
112 |         python_input.paste_mode = not python_input.paste_mode
113 | 
114 |     @handle(
115 |         "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace
116 |     )
117 |     def _(event: E) -> None:
118 |         """
119 |         When tab should insert whitespace, do that instead of completion.
120 |         """
121 |         event.app.current_buffer.insert_text("    ")
122 | 
123 |     @Condition
124 |     def is_multiline() -> bool:
125 |         return document_is_multiline_python(python_input.default_buffer.document)
126 | 
127 |     @handle(
128 |         "enter",
129 |         filter=~sidebar_visible
130 |         & ~has_selection
131 |         & (vi_insert_mode | emacs_insert_mode)
132 |         & has_focus(DEFAULT_BUFFER)
133 |         & ~is_multiline,
134 |     )
135 |     @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode)
136 |     def _(event: E) -> None:
137 |         """
138 |         Accept input (for single line input).
139 |         """
140 |         b = event.current_buffer
141 | 
142 |         if b.validate():
143 |             # When the cursor is at the end, and we have an empty line:
144 |             # drop the empty lines, but return the value.
145 |             b.document = Document(
146 |                 text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
147 |             )
148 | 
149 |             b.validate_and_handle()
150 | 
151 |     @handle(
152 |         "enter",
153 |         filter=~sidebar_visible
154 |         & ~has_selection
155 |         & (vi_insert_mode | emacs_insert_mode)
156 |         & has_focus(DEFAULT_BUFFER)
157 |         & is_multiline,
158 |     )
159 |     def _(event: E) -> None:
160 |         """
161 |         Behaviour of the Enter key.
162 | 
163 |         Auto indent after newline/Enter.
164 |         (When not in Vi navigation mode, and when multiline is enabled.)
165 |         """
166 |         b = event.current_buffer
167 |         empty_lines_required = python_input.accept_input_on_enter or 10000
168 | 
169 |         def at_the_end(b: Buffer) -> bool:
170 |             """we consider the cursor at the end when there is no text after
171 |             the cursor, or only whitespace."""
172 |             text = b.document.text_after_cursor
173 |             return text == "" or (text.isspace() and "\n" not in text)
174 | 
175 |         if python_input.paste_mode:
176 |             # In paste mode, always insert text.
177 |             b.insert_text("\n")
178 | 
179 |         elif at_the_end(b) and b.document.text.replace(" ", "").endswith(
180 |             "\n" * (empty_lines_required - 1)
181 |         ):
182 |             # When the cursor is at the end, and we have an empty line:
183 |             # drop the empty lines, but return the value.
184 |             if b.validate():
185 |                 b.document = Document(
186 |                     text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
187 |                 )
188 | 
189 |                 b.validate_and_handle()
190 |         else:
191 |             auto_newline(b)
192 | 
193 |     @handle(
194 |         "c-d",
195 |         filter=~sidebar_visible
196 |         & has_focus(python_input.default_buffer)
197 |         & Condition(
198 |             lambda:
199 |             # The current buffer is empty.
200 |             not get_app().current_buffer.text
201 |         ),
202 |     )
203 |     def _(event: E) -> None:
204 |         """
205 |         Override Control-D exit, to ask for confirmation.
206 |         """
207 |         if python_input.confirm_exit:
208 |             # Show exit confirmation and focus it (focusing is important for
209 |             # making sure the default buffer key bindings are not active).
210 |             python_input.show_exit_confirmation = True
211 |             python_input.app.layout.focus(
212 |                 python_input.ptpython_layout.exit_confirmation
213 |             )
214 |         else:
215 |             event.app.exit(exception=EOFError)
216 | 
217 |     @handle("c-c", filter=has_focus(python_input.default_buffer))
218 |     def _(event: E) -> None:
219 |         "Abort when Control-C has been pressed."
220 |         event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
221 | 
222 |     return bindings
223 | 
224 | 
225 | def load_sidebar_bindings(python_input: PythonInput) -> KeyBindings:
226 |     """
227 |     Load bindings for the navigation in the sidebar.
228 |     """
229 |     bindings = KeyBindings()
230 | 
231 |     handle = bindings.add
232 |     sidebar_visible = Condition(lambda: python_input.show_sidebar)
233 | 
234 |     @handle("up", filter=sidebar_visible)
235 |     @handle("c-p", filter=sidebar_visible)
236 |     @handle("k", filter=sidebar_visible)
237 |     def _(event: E) -> None:
238 |         "Go to previous option."
239 |         python_input.selected_option_index = (
240 |             python_input.selected_option_index - 1
241 |         ) % python_input.option_count
242 | 
243 |     @handle("down", filter=sidebar_visible)
244 |     @handle("c-n", filter=sidebar_visible)
245 |     @handle("j", filter=sidebar_visible)
246 |     def _(event: E) -> None:
247 |         "Go to next option."
248 |         python_input.selected_option_index = (
249 |             python_input.selected_option_index + 1
250 |         ) % python_input.option_count
251 | 
252 |     @handle("right", filter=sidebar_visible)
253 |     @handle("l", filter=sidebar_visible)
254 |     @handle(" ", filter=sidebar_visible)
255 |     def _(event: E) -> None:
256 |         "Select next value for current option."
257 |         option = python_input.selected_option
258 |         option.activate_next()
259 | 
260 |     @handle("left", filter=sidebar_visible)
261 |     @handle("h", filter=sidebar_visible)
262 |     def _(event: E) -> None:
263 |         "Select previous value for current option."
264 |         option = python_input.selected_option
265 |         option.activate_previous()
266 | 
267 |     @handle("c-c", filter=sidebar_visible)
268 |     @handle("c-d", filter=sidebar_visible)
269 |     @handle("c-d", filter=sidebar_visible)
270 |     @handle("enter", filter=sidebar_visible)
271 |     @handle("escape", filter=sidebar_visible)
272 |     def _(event: E) -> None:
273 |         "Hide sidebar."
274 |         python_input.show_sidebar = False
275 |         event.app.layout.focus_last()
276 | 
277 |     return bindings
278 | 
279 | 
280 | def load_confirm_exit_bindings(python_input: PythonInput) -> KeyBindings:
281 |     """
282 |     Handle yes/no key presses when the exit confirmation is shown.
283 |     """
284 |     bindings = KeyBindings()
285 | 
286 |     handle = bindings.add
287 |     confirmation_visible = Condition(lambda: python_input.show_exit_confirmation)
288 | 
289 |     @handle("y", filter=confirmation_visible)
290 |     @handle("Y", filter=confirmation_visible)
291 |     @handle("enter", filter=confirmation_visible)
292 |     @handle("c-d", filter=confirmation_visible)
293 |     def _(event: E) -> None:
294 |         """
295 |         Really quit.
296 |         """
297 |         event.app.exit(exception=EOFError, style="class:exiting")
298 | 
299 |     @handle(Keys.Any, filter=confirmation_visible)
300 |     def _(event: E) -> None:
301 |         """
302 |         Cancel exit.
303 |         """
304 |         python_input.show_exit_confirmation = False
305 |         python_input.app.layout.focus_previous()
306 | 
307 |     return bindings
308 | 
309 | 
310 | def auto_newline(buffer: Buffer) -> None:
311 |     r"""
312 |     Insert \n at the cursor position. Also add necessary padding.
313 |     """
314 |     insert_text = buffer.insert_text
315 | 
316 |     if buffer.document.current_line_after_cursor:
317 |         # When we are in the middle of a line. Always insert a newline.
318 |         insert_text("\n")
319 |     else:
320 |         # Go to new line, but also add indentation.
321 |         current_line = buffer.document.current_line_before_cursor.rstrip()
322 |         insert_text("\n")
323 | 
324 |         # Unident if the last line ends with 'pass', remove four spaces.
325 |         unindent = current_line.rstrip().endswith(" pass")
326 | 
327 |         # Copy whitespace from current line
328 |         current_line2 = current_line[4:] if unindent else current_line
329 | 
330 |         for c in current_line2:
331 |             if c.isspace():
332 |                 insert_text(c)
333 |             else:
334 |                 break
335 | 
336 |         # If the last line ends with a colon, add four extra spaces.
337 |         if current_line[-1:] == ":":
338 |             for x in range(4):
339 |                 insert_text(" ")
340 | 


--------------------------------------------------------------------------------
/src/ptpython/lexer.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Callable
 4 | 
 5 | from prompt_toolkit.document import Document
 6 | from prompt_toolkit.formatted_text import StyleAndTextTuples
 7 | from prompt_toolkit.lexers import Lexer, PygmentsLexer
 8 | from pygments.lexers import BashLexer
 9 | from pygments.lexers import Python3Lexer as PythonLexer
10 | 
11 | __all__ = ["PtpythonLexer"]
12 | 
13 | 
14 | class PtpythonLexer(Lexer):
15 |     """
16 |     Lexer for ptpython input.
17 | 
18 |     If the input starts with an exclamation mark, use a Bash lexer, otherwise,
19 |     use a Python 3 lexer.
20 |     """
21 | 
22 |     def __init__(self, python_lexer: Lexer | None = None) -> None:
23 |         self.python_lexer = python_lexer or PygmentsLexer(PythonLexer)
24 |         self.system_lexer = PygmentsLexer(BashLexer)
25 | 
26 |     def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
27 |         if document.text.startswith("!"):
28 |             return self.system_lexer.lex_document(document)
29 | 
30 |         return self.python_lexer.lex_document(document)
31 | 


--------------------------------------------------------------------------------
/src/ptpython/printer.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | import traceback
  4 | from dataclasses import dataclass
  5 | from enum import Enum
  6 | from typing import Generator, Iterable
  7 | 
  8 | from prompt_toolkit.formatted_text import (
  9 |     HTML,
 10 |     AnyFormattedText,
 11 |     FormattedText,
 12 |     OneStyleAndTextTuple,
 13 |     StyleAndTextTuples,
 14 |     fragment_list_width,
 15 |     merge_formatted_text,
 16 |     to_formatted_text,
 17 | )
 18 | from prompt_toolkit.formatted_text.utils import split_lines
 19 | from prompt_toolkit.input import Input
 20 | from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
 21 | from prompt_toolkit.output import Output
 22 | from prompt_toolkit.shortcuts import PromptSession, print_formatted_text
 23 | from prompt_toolkit.styles import BaseStyle, StyleTransformation
 24 | from prompt_toolkit.styles.pygments import pygments_token_to_classname
 25 | from prompt_toolkit.utils import get_cwidth
 26 | from pygments.lexers import PythonLexer, PythonTracebackLexer
 27 | 
 28 | __all__ = ["OutputPrinter"]
 29 | 
 30 | # Never reformat results larger than this:
 31 | MAX_REFORMAT_SIZE = 1_000_000
 32 | 
 33 | 
 34 | @dataclass
 35 | class OutputPrinter:
 36 |     """
 37 |     Result printer.
 38 | 
 39 |     Usage::
 40 | 
 41 |         printer = OutputPrinter(...)
 42 |         printer.display_result(...)
 43 |         printer.display_exception(...)
 44 |     """
 45 | 
 46 |     output: Output
 47 |     input: Input
 48 |     style: BaseStyle
 49 |     title: AnyFormattedText
 50 |     style_transformation: StyleTransformation
 51 | 
 52 |     def display_result(
 53 |         self,
 54 |         result: object,
 55 |         *,
 56 |         out_prompt: AnyFormattedText,
 57 |         reformat: bool,
 58 |         highlight: bool,
 59 |         paginate: bool,
 60 |     ) -> None:
 61 |         """
 62 |         Show __repr__ (or `__pt_repr__`) for an `eval` result and print to output.
 63 | 
 64 |         :param reformat: Reformat result using 'black' before printing if the
 65 |             result is parsable as Python code.
 66 |         :param highlight: Syntax highlight the result.
 67 |         :param paginate: Show paginator when the result does not fit on the
 68 |             screen.
 69 |         """
 70 |         out_prompt = to_formatted_text(out_prompt)
 71 |         out_prompt_width = fragment_list_width(out_prompt)
 72 | 
 73 |         result = self._insert_out_prompt_and_split_lines(
 74 |             self._format_result_output(
 75 |                 result,
 76 |                 reformat=reformat,
 77 |                 highlight=highlight,
 78 |                 line_length=self.output.get_size().columns - out_prompt_width,
 79 |                 paginate=paginate,
 80 |             ),
 81 |             out_prompt=out_prompt,
 82 |         )
 83 |         self._display_result(result, paginate=paginate)
 84 | 
 85 |     def display_exception(
 86 |         self, e: BaseException, *, highlight: bool, paginate: bool
 87 |     ) -> None:
 88 |         """
 89 |         Render an exception.
 90 |         """
 91 |         result = self._insert_out_prompt_and_split_lines(
 92 |             self._format_exception_output(e, highlight=highlight),
 93 |             out_prompt="",
 94 |         )
 95 |         self._display_result(result, paginate=paginate)
 96 | 
 97 |     def display_style_and_text_tuples(
 98 |         self,
 99 |         result: Iterable[OneStyleAndTextTuple],
100 |         *,
101 |         paginate: bool,
102 |     ) -> None:
103 |         self._display_result(
104 |             self._insert_out_prompt_and_split_lines(result, out_prompt=""),
105 |             paginate=paginate,
106 |         )
107 | 
108 |     def _display_result(
109 |         self,
110 |         lines: Iterable[StyleAndTextTuples],
111 |         *,
112 |         paginate: bool,
113 |     ) -> None:
114 |         if paginate:
115 |             self._print_paginated_formatted_text(lines)
116 |         else:
117 |             for line in lines:
118 |                 self._print_formatted_text(line)
119 | 
120 |         self.output.flush()
121 | 
122 |     def _print_formatted_text(self, line: StyleAndTextTuples, end: str = "\n") -> None:
123 |         print_formatted_text(
124 |             FormattedText(line),
125 |             style=self.style,
126 |             style_transformation=self.style_transformation,
127 |             include_default_pygments_style=False,
128 |             output=self.output,
129 |             end=end,
130 |         )
131 | 
132 |     def _format_result_output(
133 |         self,
134 |         result: object,
135 |         *,
136 |         reformat: bool,
137 |         highlight: bool,
138 |         line_length: int,
139 |         paginate: bool,
140 |     ) -> Generator[OneStyleAndTextTuple, None, None]:
141 |         """
142 |         Format __repr__ for an `eval` result.
143 | 
144 |         Note: this can raise `KeyboardInterrupt` if either calling `__repr__`,
145 |               `__pt_repr__` or formatting the output with "Black" takes to long
146 |               and the user presses Control-C.
147 |         """
148 |         # If __pt_repr__ is present, take this. This can return prompt_toolkit
149 |         # formatted text.
150 |         try:
151 |             if hasattr(result, "__pt_repr__"):
152 |                 formatted_result_repr = to_formatted_text(
153 |                     getattr(result, "__pt_repr__")()
154 |                 )
155 |                 yield from formatted_result_repr
156 |                 return
157 |         except (GeneratorExit, KeyboardInterrupt):
158 |             raise  # Don't catch here.
159 |         except:
160 |             # For bad code, `__getattr__` can raise something that's not an
161 |             # `AttributeError`. This happens already when calling `hasattr()`.
162 |             pass
163 | 
164 |         # Call `__repr__` of given object first, to turn it in a string.
165 |         try:
166 |             result_repr = repr(result)
167 |         except KeyboardInterrupt:
168 |             raise  # Don't catch here.
169 |         except BaseException as e:
170 |             # Calling repr failed.
171 |             self.display_exception(e, highlight=highlight, paginate=paginate)
172 |             return
173 | 
174 |         # Determine whether it's valid Python code. If not,
175 |         # reformatting/highlighting won't be applied.
176 |         if len(result_repr) < MAX_REFORMAT_SIZE:
177 |             try:
178 |                 compile(result_repr, "", "eval")
179 |             except SyntaxError:
180 |                 valid_python = False
181 |             else:
182 |                 valid_python = True
183 |         else:
184 |             valid_python = False
185 | 
186 |         if valid_python and reformat:
187 |             # Inline import. Slightly speed up start-up time if black is
188 |             # not used.
189 |             try:
190 |                 import black
191 | 
192 |                 if not hasattr(black, "Mode"):
193 |                     raise ImportError
194 |             except ImportError:
195 |                 pass  # no Black package in your installation
196 |             else:
197 |                 result_repr = black.format_str(
198 |                     result_repr,
199 |                     mode=black.Mode(line_length=line_length),
200 |                 )
201 | 
202 |         if valid_python and highlight:
203 |             yield from _lex_python_result(result_repr)
204 |         else:
205 |             yield ("", result_repr)
206 | 
207 |     def _insert_out_prompt_and_split_lines(
208 |         self, result: Iterable[OneStyleAndTextTuple], out_prompt: AnyFormattedText
209 |     ) -> Iterable[StyleAndTextTuples]:
210 |         r"""
211 |         Split styled result in lines (based on the \n characters in the result)
212 |         an insert output prompt on whitespace in front of each line. (This does
213 |         not yet do the soft wrapping.)
214 | 
215 |         Yield lines as a result.
216 |         """
217 |         out_prompt = to_formatted_text(out_prompt)
218 |         out_prompt_width = fragment_list_width(out_prompt)
219 |         prefix = ("", " " * out_prompt_width)
220 | 
221 |         for i, line in enumerate(split_lines(result)):
222 |             if i == 0:
223 |                 line = [*out_prompt, *line]
224 |             else:
225 |                 line = [prefix, *line]
226 |             yield line
227 | 
228 |     def _apply_soft_wrapping(
229 |         self, lines: Iterable[StyleAndTextTuples]
230 |     ) -> Iterable[StyleAndTextTuples]:
231 |         """
232 |         Apply soft wrapping to the given lines. Wrap according to the terminal
233 |         width. Insert whitespace in front of each wrapped line to align it with
234 |         the output prompt.
235 |         """
236 |         line_length = self.output.get_size().columns
237 | 
238 |         # Iterate over hard wrapped lines.
239 |         for lineno, line in enumerate(lines):
240 |             columns_in_buffer = 0
241 |             current_line: list[OneStyleAndTextTuple] = []
242 | 
243 |             for style, text, *_ in line:
244 |                 for c in text:
245 |                     width = get_cwidth(c)
246 | 
247 |                     # (Soft) wrap line if it doesn't fit.
248 |                     if columns_in_buffer + width > line_length:
249 |                         yield current_line
250 |                         columns_in_buffer = 0
251 |                         current_line = []
252 | 
253 |                     columns_in_buffer += width
254 |                     current_line.append((style, c))
255 | 
256 |             yield current_line
257 | 
258 |     def _print_paginated_formatted_text(
259 |         self, lines: Iterable[StyleAndTextTuples]
260 |     ) -> None:
261 |         """
262 |         Print formatted text, using --MORE-- style pagination.
263 |         (Avoid filling up the terminal's scrollback buffer.)
264 |         """
265 |         lines = self._apply_soft_wrapping(lines)
266 |         pager_prompt = create_pager_prompt(
267 |             self.style, self.title, output=self.output, input=self.input
268 |         )
269 | 
270 |         abort = False
271 |         print_all = False
272 | 
273 |         # Max number of lines allowed in the buffer before painting.
274 |         size = self.output.get_size()
275 |         max_rows = size.rows - 1
276 | 
277 |         # Page buffer.
278 |         page: StyleAndTextTuples = []
279 | 
280 |         def show_pager() -> None:
281 |             nonlocal abort, max_rows, print_all
282 | 
283 |             # Run pager prompt in another thread.
284 |             # Same as for the input. This prevents issues with nested event
285 |             # loops.
286 |             pager_result = pager_prompt.prompt(in_thread=True)
287 | 
288 |             if pager_result == PagerResult.ABORT:
289 |                 print("...")
290 |                 abort = True
291 | 
292 |             elif pager_result == PagerResult.NEXT_LINE:
293 |                 max_rows = 1
294 | 
295 |             elif pager_result == PagerResult.NEXT_PAGE:
296 |                 max_rows = size.rows - 1
297 | 
298 |             elif pager_result == PagerResult.PRINT_ALL:
299 |                 print_all = True
300 | 
301 |         # Loop over lines. Show --MORE-- prompt when page is filled.
302 |         rows = 0
303 | 
304 |         for lineno, line in enumerate(lines):
305 |             page.extend(line)
306 |             page.append(("", "\n"))
307 |             rows += 1
308 | 
309 |             if rows >= max_rows:
310 |                 self._print_formatted_text(page, end="")
311 |                 page = []
312 |                 rows = 0
313 | 
314 |                 if not print_all:
315 |                     show_pager()
316 |                     if abort:
317 |                         return
318 | 
319 |         self._print_formatted_text(page)
320 | 
321 |     def _format_exception_output(
322 |         self, e: BaseException, highlight: bool
323 |     ) -> Generator[OneStyleAndTextTuple, None, None]:
324 |         if e.__cause__:
325 |             yield from self._format_exception_output(e.__cause__, highlight=highlight)
326 |             yield (
327 |                 "",
328 |                 "\nThe above exception was the direct cause of the following exception:\n\n",
329 |             )
330 |         elif e.__context__:
331 |             yield from self._format_exception_output(e.__context__, highlight=highlight)
332 |             yield (
333 |                 "",
334 |                 "\nDuring handling of the above exception, another exception occurred:\n\n",
335 |             )
336 | 
337 |         tblist = list(traceback.extract_tb(e.__traceback__))
338 | 
339 |         for line_nr, tb_tuple in enumerate(tblist):
340 |             if tb_tuple[0] == "<stdin>":
341 |                 tblist = tblist[line_nr:]
342 |                 break
343 | 
344 |         tb_list = traceback.format_list(tblist)
345 |         if tb_list:
346 |             tb_list.insert(0, "Traceback (most recent call last):\n")
347 |         tb_list.extend(traceback.format_exception_only(type(e), e))
348 | 
349 |         tb_str = "".join(tb_list)
350 | 
351 |         # Format exception and write to output.
352 |         # (We use the default style. Most other styles result
353 |         # in unreadable colors for the traceback.)
354 |         if highlight:
355 |             for index, tokentype, text in PythonTracebackLexer().get_tokens_unprocessed(
356 |                 tb_str
357 |             ):
358 |                 yield ("class:" + pygments_token_to_classname(tokentype), text)
359 |         else:
360 |             yield ("", tb_str)
361 | 
362 | 
363 | class PagerResult(Enum):
364 |     ABORT = "ABORT"
365 |     NEXT_LINE = "NEXT_LINE"
366 |     NEXT_PAGE = "NEXT_PAGE"
367 |     PRINT_ALL = "PRINT_ALL"
368 | 
369 | 
370 | def create_pager_prompt(
371 |     style: BaseStyle,
372 |     title: AnyFormattedText = "",
373 |     input: Input | None = None,
374 |     output: Output | None = None,
375 | ) -> PromptSession[PagerResult]:
376 |     """
377 |     Create a "--MORE--" prompt for paginated output.
378 |     """
379 |     bindings = KeyBindings()
380 | 
381 |     @bindings.add("enter")
382 |     @bindings.add("down")
383 |     def next_line(event: KeyPressEvent) -> None:
384 |         event.app.exit(result=PagerResult.NEXT_LINE)
385 | 
386 |     @bindings.add("space")
387 |     def next_page(event: KeyPressEvent) -> None:
388 |         event.app.exit(result=PagerResult.NEXT_PAGE)
389 | 
390 |     @bindings.add("a")
391 |     def print_all(event: KeyPressEvent) -> None:
392 |         event.app.exit(result=PagerResult.PRINT_ALL)
393 | 
394 |     @bindings.add("q")
395 |     @bindings.add("c-c")
396 |     @bindings.add("c-d")
397 |     @bindings.add("escape", eager=True)
398 |     def no(event: KeyPressEvent) -> None:
399 |         event.app.exit(result=PagerResult.ABORT)
400 | 
401 |     @bindings.add("<any>")
402 |     def _(event: KeyPressEvent) -> None:
403 |         "Disallow inserting other text."
404 |         pass
405 | 
406 |     session: PromptSession[PagerResult] = PromptSession(
407 |         merge_formatted_text(
408 |             [
409 |                 title,
410 |                 HTML(
411 |                     "<status-toolbar>"
412 |                     "<more> -- MORE -- </more> "
413 |                     "<key>[Enter]</key> Scroll "
414 |                     "<key>[Space]</key> Next page "
415 |                     "<key>[a]</key> Print all "
416 |                     "<key>[q]</key> Quit "
417 |                     "</status-toolbar>: "
418 |                 ),
419 |             ]
420 |         ),
421 |         key_bindings=bindings,
422 |         erase_when_done=True,
423 |         style=style,
424 |         input=input,
425 |         output=output,
426 |     )
427 |     return session
428 | 
429 | 
430 | def _lex_python_result(result: str) -> Generator[tuple[str, str], None, None]:
431 |     "Return token list for Python string."
432 |     lexer = PythonLexer()
433 |     # Use `get_tokens_unprocessed`, so that we get exactly the same string,
434 |     # without line endings appended. `print_formatted_text` already appends a
435 |     # line ending, and otherwise we'll have two line endings.
436 |     tokens = lexer.get_tokens_unprocessed(result)
437 | 
438 |     for index, tokentype, text in tokens:
439 |         yield ("class:" + pygments_token_to_classname(tokentype), text)
440 | 


--------------------------------------------------------------------------------
/src/ptpython/prompt_style.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from abc import ABCMeta, abstractmethod
 4 | from typing import TYPE_CHECKING
 5 | 
 6 | from prompt_toolkit.formatted_text import AnyFormattedText
 7 | 
 8 | if TYPE_CHECKING:
 9 |     from .python_input import PythonInput
10 | 
11 | __all__ = ["PromptStyle", "IPythonPrompt", "ClassicPrompt"]
12 | 
13 | 
14 | class PromptStyle(metaclass=ABCMeta):
15 |     """
16 |     Base class for all prompts.
17 |     """
18 | 
19 |     @abstractmethod
20 |     def in_prompt(self) -> AnyFormattedText:
21 |         "Return the input tokens."
22 |         return []
23 | 
24 |     @abstractmethod
25 |     def in2_prompt(self, width: int) -> AnyFormattedText:
26 |         """
27 |         Tokens for every following input line.
28 | 
29 |         :param width: The available width. This is coming from the width taken
30 |                       by `in_prompt`.
31 |         """
32 |         return []
33 | 
34 |     @abstractmethod
35 |     def out_prompt(self) -> AnyFormattedText:
36 |         "Return the output tokens."
37 |         return []
38 | 
39 | 
40 | class IPythonPrompt(PromptStyle):
41 |     """
42 |     A prompt resembling the IPython prompt.
43 |     """
44 | 
45 |     def __init__(self, python_input: PythonInput) -> None:
46 |         self.python_input = python_input
47 | 
48 |     def in_prompt(self) -> AnyFormattedText:
49 |         return [
50 |             ("class:in", "In ["),
51 |             ("class:in.number", f"{self.python_input.current_statement_index}"),
52 |             ("class:in", "]: "),
53 |         ]
54 | 
55 |     def in2_prompt(self, width: int) -> AnyFormattedText:
56 |         return [("class:in", "...: ".rjust(width))]
57 | 
58 |     def out_prompt(self) -> AnyFormattedText:
59 |         return [
60 |             ("class:out", "Out["),
61 |             ("class:out.number", f"{self.python_input.current_statement_index}"),
62 |             ("class:out", "]:"),
63 |             ("", " "),
64 |         ]
65 | 
66 | 
67 | class ClassicPrompt(PromptStyle):
68 |     """
69 |     The classic Python prompt.
70 |     """
71 | 
72 |     def in_prompt(self) -> AnyFormattedText:
73 |         return [("class:prompt", ">>> ")]
74 | 
75 |     def in2_prompt(self, width: int) -> AnyFormattedText:
76 |         return [("class:prompt.dots", "...")]
77 | 
78 |     def out_prompt(self) -> AnyFormattedText:
79 |         return []
80 | 


--------------------------------------------------------------------------------
/src/ptpython/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prompt-toolkit/ptpython/0066c7ee392d46103b8bd2968c7a645a4c04f02d/src/ptpython/py.typed


--------------------------------------------------------------------------------
/src/ptpython/repl.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Utility for creating a Python repl.
  3 | 
  4 | ::
  5 | 
  6 |     from ptpython.repl import embed
  7 |     embed(globals(), locals(), vi_mode=False)
  8 | 
  9 | """
 10 | 
 11 | from __future__ import annotations
 12 | 
 13 | import asyncio
 14 | import builtins
 15 | import os
 16 | import signal
 17 | import sys
 18 | import traceback
 19 | import types
 20 | import warnings
 21 | from dis import COMPILER_FLAG_NAMES
 22 | from pathlib import Path
 23 | from typing import (
 24 |     Any,
 25 |     Callable,
 26 |     ContextManager,
 27 |     Coroutine,
 28 |     Iterable,
 29 |     Literal,
 30 |     NoReturn,
 31 |     Sequence,
 32 |     overload,
 33 | )
 34 | 
 35 | from prompt_toolkit.formatted_text import OneStyleAndTextTuple
 36 | from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context
 37 | from prompt_toolkit.shortcuts import (
 38 |     clear_title,
 39 |     set_title,
 40 | )
 41 | from prompt_toolkit.utils import DummyContext
 42 | from pygments.lexers import PythonTracebackLexer  # noqa: F401
 43 | 
 44 | from .printer import OutputPrinter
 45 | from .python_input import PythonInput
 46 | 
 47 | PyCF_ALLOW_TOP_LEVEL_AWAIT: int
 48 | try:
 49 |     from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT  # type: ignore
 50 | except ImportError:
 51 |     PyCF_ALLOW_TOP_LEVEL_AWAIT = 0
 52 | 
 53 | 
 54 | __all__ = [
 55 |     "PythonRepl",
 56 |     "enable_deprecation_warnings",
 57 |     "run_config",
 58 |     "embed",
 59 |     "exit",
 60 |     "ReplExit",
 61 | ]
 62 | 
 63 | 
 64 | def _get_coroutine_flag() -> int | None:
 65 |     for k, v in COMPILER_FLAG_NAMES.items():
 66 |         if v == "COROUTINE":
 67 |             return k
 68 | 
 69 |     # Flag not found.
 70 |     return None
 71 | 
 72 | 
 73 | COROUTINE_FLAG: int | None = _get_coroutine_flag()
 74 | 
 75 | 
 76 | def _has_coroutine_flag(code: types.CodeType) -> bool:
 77 |     if COROUTINE_FLAG is None:
 78 |         # Not supported on this Python version.
 79 |         return False
 80 | 
 81 |     return bool(code.co_flags & COROUTINE_FLAG)
 82 | 
 83 | 
 84 | class PythonRepl(PythonInput):
 85 |     def __init__(self, *a, **kw) -> None:
 86 |         self._startup_paths: Sequence[str | Path] | None = kw.pop("startup_paths", None)
 87 |         super().__init__(*a, **kw)
 88 |         self._load_start_paths()
 89 | 
 90 |     def _load_start_paths(self) -> None:
 91 |         "Start the Read-Eval-Print Loop."
 92 |         if self._startup_paths:
 93 |             for path in self._startup_paths:
 94 |                 if os.path.exists(path):
 95 |                     with open(path, "rb") as f:
 96 |                         code = compile(f.read(), path, "exec")
 97 |                         exec(code, self.get_globals(), self.get_locals())
 98 |                 else:
 99 |                     output = self.app.output
100 |                     output.write(f"WARNING | File not found: {path}\n\n")
101 | 
102 |     def run_and_show_expression(self, expression: str) -> None:
103 |         try:
104 |             # Eval.
105 |             try:
106 |                 result = self.eval(expression)
107 |             except KeyboardInterrupt:
108 |                 # KeyboardInterrupt doesn't inherit from Exception.
109 |                 raise
110 |             except SystemExit:
111 |                 raise
112 |             except ReplExit:
113 |                 raise
114 |             except BaseException as e:
115 |                 self._handle_exception(e)
116 |             else:
117 |                 if isinstance(result, exit):
118 |                     # When `exit` is evaluated without parentheses.
119 |                     # Automatically trigger the `ReplExit` exception.
120 |                     raise ReplExit
121 | 
122 |                 # Print.
123 |                 if result is not None:
124 |                     self._show_result(result)
125 |                     if self.insert_blank_line_after_output:
126 |                         self.app.output.write("\n")
127 | 
128 |                 # Loop.
129 |                 self.current_statement_index += 1
130 |                 self.signatures = []
131 | 
132 |         except KeyboardInterrupt as e:
133 |             # Handle all possible `KeyboardInterrupt` errors. This can
134 |             # happen during the `eval`, but also during the
135 |             # `show_result` if something takes too long.
136 |             # (Try/catch is around the whole block, because we want to
137 |             # prevent that a Control-C keypress terminates the REPL in
138 |             # any case.)
139 |             self._handle_keyboard_interrupt(e)
140 | 
141 |     def _get_output_printer(self) -> OutputPrinter:
142 |         return OutputPrinter(
143 |             output=self.app.output,
144 |             input=self.app.input,
145 |             style=self._current_style,
146 |             style_transformation=self.style_transformation,
147 |             title=self.title,
148 |         )
149 | 
150 |     def _show_result(self, result: object) -> None:
151 |         self._get_output_printer().display_result(
152 |             result=result,
153 |             out_prompt=self.get_output_prompt(),
154 |             reformat=self.enable_output_formatting,
155 |             highlight=self.enable_syntax_highlighting,
156 |             paginate=self.enable_pager,
157 |         )
158 | 
159 |     def run(self) -> None:
160 |         """
161 |         Run the REPL loop.
162 |         """
163 |         if self.terminal_title:
164 |             set_title(self.terminal_title)
165 | 
166 |         self._add_to_namespace()
167 | 
168 |         try:
169 |             while True:
170 |                 # Pull text from the user.
171 |                 try:
172 |                     text = self.read()
173 |                 except EOFError:
174 |                     return
175 |                 except BaseException:
176 |                     # Something went wrong while reading input.
177 |                     # (E.g., a bug in the completer that propagates. Don't
178 |                     # crash the REPL.)
179 |                     traceback.print_exc()
180 |                     continue
181 | 
182 |                 # Run it; display the result (or errors if applicable).
183 |                 try:
184 |                     self.run_and_show_expression(text)
185 |                 except ReplExit:
186 |                     return
187 |         finally:
188 |             if self.terminal_title:
189 |                 clear_title()
190 |             self._remove_from_namespace()
191 | 
192 |     async def run_and_show_expression_async(self, text: str) -> Any:
193 |         loop = asyncio.get_running_loop()
194 |         system_exit: SystemExit | None = None
195 | 
196 |         try:
197 |             try:
198 |                 # Create `eval` task. Ensure that control-c will cancel this
199 |                 # task.
200 |                 async def eval() -> Any:
201 |                     nonlocal system_exit
202 |                     try:
203 |                         return await self.eval_async(text)
204 |                     except SystemExit as e:
205 |                         # Don't propagate SystemExit in `create_task()`. That
206 |                         # will kill the event loop. We want to handle it
207 |                         # gracefully.
208 |                         system_exit = e
209 | 
210 |                 task = asyncio.create_task(eval())
211 |                 loop.add_signal_handler(signal.SIGINT, lambda *_: task.cancel())
212 |                 result = await task
213 | 
214 |                 if system_exit is not None:
215 |                     raise system_exit
216 |             except KeyboardInterrupt:
217 |                 # KeyboardInterrupt doesn't inherit from Exception.
218 |                 raise
219 |             except SystemExit:
220 |                 raise
221 |             except BaseException as e:
222 |                 self._handle_exception(e)
223 |             else:
224 |                 # Print.
225 |                 if result is not None:
226 |                     await loop.run_in_executor(None, lambda: self._show_result(result))
227 | 
228 |                 # Loop.
229 |                 self.current_statement_index += 1
230 |                 self.signatures = []
231 |                 # Return the result for future consumers.
232 |                 return result
233 |             finally:
234 |                 loop.remove_signal_handler(signal.SIGINT)
235 | 
236 |         except KeyboardInterrupt as e:
237 |             # Handle all possible `KeyboardInterrupt` errors. This can
238 |             # happen during the `eval`, but also during the
239 |             # `show_result` if something takes too long.
240 |             # (Try/catch is around the whole block, because we want to
241 |             # prevent that a Control-C keypress terminates the REPL in
242 |             # any case.)
243 |             self._handle_keyboard_interrupt(e)
244 | 
245 |     async def run_async(self) -> None:
246 |         """
247 |         Run the REPL loop, but run the blocking parts in an executor, so that
248 |         we don't block the event loop. Both the input and output (which can
249 |         display a pager) will run in a separate thread with their own event
250 |         loop, this way ptpython's own event loop won't interfere with the
251 |         asyncio event loop from where this is called.
252 | 
253 |         The "eval" however happens in the current thread, which is important.
254 |         (Both for control-C to work, as well as for the code to see the right
255 |         thread in which it was embedded).
256 |         """
257 |         loop = asyncio.get_running_loop()
258 | 
259 |         if self.terminal_title:
260 |             set_title(self.terminal_title)
261 | 
262 |         self._add_to_namespace()
263 | 
264 |         try:
265 |             while True:
266 |                 try:
267 |                     # Read.
268 |                     try:
269 |                         text = await loop.run_in_executor(None, self.read)
270 |                     except EOFError:
271 |                         return
272 |                     except BaseException:
273 |                         # Something went wrong while reading input.
274 |                         # (E.g., a bug in the completer that propagates. Don't
275 |                         # crash the REPL.)
276 |                         traceback.print_exc()
277 |                         continue
278 | 
279 |                     # Eval.
280 |                     await self.run_and_show_expression_async(text)
281 | 
282 |                 except KeyboardInterrupt as e:
283 |                     # XXX: This does not yet work properly. In some situations,
284 |                     # `KeyboardInterrupt` exceptions can end up in the event
285 |                     # loop selector.
286 |                     self._handle_keyboard_interrupt(e)
287 |                 except SystemExit:
288 |                     return
289 |         finally:
290 |             if self.terminal_title:
291 |                 clear_title()
292 |             self._remove_from_namespace()
293 | 
294 |     def eval(self, line: str) -> object:
295 |         """
296 |         Evaluate the line and print the result.
297 |         """
298 |         # WORKAROUND: Due to a bug in Jedi, the current directory is removed
299 |         # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
300 |         if "" not in sys.path:
301 |             sys.path.insert(0, "")
302 | 
303 |         if line.lstrip().startswith("!"):
304 |             # Run as shell command
305 |             os.system(line[1:])
306 |         else:
307 |             # Try eval first
308 |             try:
309 |                 code = self._compile_with_flags(line, "eval")
310 |             except SyntaxError:
311 |                 pass
312 |             else:
313 |                 # No syntax errors for eval. Do eval.
314 |                 result = eval(code, self.get_globals(), self.get_locals())
315 | 
316 |                 if _has_coroutine_flag(code):
317 |                     result = asyncio.get_running_loop().run_until_complete(result)
318 | 
319 |                 self._store_eval_result(result)
320 |                 return result
321 | 
322 |             # If not a valid `eval` expression, run using `exec` instead.
323 |             # Note that we shouldn't run this in the `except SyntaxError` block
324 |             # above, then `sys.exc_info()` would not report the right error.
325 |             # See issue: https://github.com/prompt-toolkit/ptpython/issues/435
326 |             code = self._compile_with_flags(line, "exec")
327 |             result = eval(code, self.get_globals(), self.get_locals())
328 | 
329 |             if _has_coroutine_flag(code):
330 |                 result = asyncio.get_running_loop().run_until_complete(result)
331 | 
332 |         return None
333 | 
334 |     async def eval_async(self, line: str) -> object:
335 |         """
336 |         Evaluate the line and print the result.
337 |         """
338 |         # WORKAROUND: Due to a bug in Jedi, the current directory is removed
339 |         # from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
340 |         if "" not in sys.path:
341 |             sys.path.insert(0, "")
342 | 
343 |         if line.lstrip().startswith("!"):
344 |             # Run as shell command
345 |             os.system(line[1:])
346 |         else:
347 |             # Try eval first
348 |             try:
349 |                 code = self._compile_with_flags(line, "eval")
350 |             except SyntaxError:
351 |                 pass
352 |             else:
353 |                 # No syntax errors for eval. Do eval.
354 |                 result = eval(code, self.get_globals(), self.get_locals())
355 | 
356 |                 if _has_coroutine_flag(code):
357 |                     result = await result
358 | 
359 |                 self._store_eval_result(result)
360 |                 return result
361 | 
362 |             # If not a valid `eval` expression, compile as `exec` expression
363 |             # but still run with eval to get an awaitable in case of a
364 |             # awaitable expression.
365 |             code = self._compile_with_flags(line, "exec")
366 |             result = eval(code, self.get_globals(), self.get_locals())
367 | 
368 |             if _has_coroutine_flag(code):
369 |                 result = await result
370 | 
371 |         return None
372 | 
373 |     def _store_eval_result(self, result: object) -> None:
374 |         locals: dict[str, Any] = self.get_locals()
375 |         locals["_"] = locals[f"_{self.current_statement_index}"] = result
376 | 
377 |     def get_compiler_flags(self) -> int:
378 |         return super().get_compiler_flags() | PyCF_ALLOW_TOP_LEVEL_AWAIT
379 | 
380 |     def _compile_with_flags(self, code: str, mode: str) -> Any:
381 |         "Compile code with the right compiler flags."
382 |         return compile(
383 |             code,
384 |             "<stdin>",
385 |             mode,
386 |             flags=self.get_compiler_flags(),
387 |             dont_inherit=True,
388 |         )
389 | 
390 |     def _handle_exception(self, e: BaseException) -> None:
391 |         # Required for pdb.post_mortem() to work.
392 |         t, v, tb = sys.exc_info()
393 |         sys.last_type, sys.last_value, sys.last_traceback = t, v, tb
394 | 
395 |         self._get_output_printer().display_exception(
396 |             e,
397 |             highlight=self.enable_syntax_highlighting,
398 |             paginate=self.enable_pager,
399 |         )
400 | 
401 |     def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None:
402 |         output = self.app.output
403 | 
404 |         output.write("\rKeyboardInterrupt\n\n")
405 |         output.flush()
406 | 
407 |     def _add_to_namespace(self) -> None:
408 |         """
409 |         Add ptpython built-ins to global namespace.
410 |         """
411 |         globals = self.get_globals()
412 | 
413 |         # Add a 'get_ptpython', similar to 'get_ipython'
414 |         def get_ptpython() -> PythonInput:
415 |             return self
416 | 
417 |         globals["get_ptpython"] = get_ptpython
418 |         globals["exit"] = exit()
419 | 
420 |     def _remove_from_namespace(self) -> None:
421 |         """
422 |         Remove added symbols from the globals.
423 |         """
424 |         globals = self.get_globals()
425 |         del globals["get_ptpython"]
426 | 
427 |     def print_paginated_formatted_text(
428 |         self,
429 |         formatted_text: Iterable[OneStyleAndTextTuple],
430 |         end: str = "\n",
431 |     ) -> None:
432 |         # Warning: This is mainly here backwards-compatibility. Some projects
433 |         # call `print_paginated_formatted_text` on the Repl object.
434 |         self._get_output_printer().display_style_and_text_tuples(
435 |             formatted_text, paginate=True
436 |         )
437 | 
438 | 
439 | def enable_deprecation_warnings() -> None:
440 |     """
441 |     Show deprecation warnings, when they are triggered directly by actions in
442 |     the REPL. This is recommended to call, before calling `embed`.
443 | 
444 |     e.g. This will show an error message when the user imports the 'sha'
445 |          library on Python 2.7.
446 |     """
447 |     warnings.filterwarnings("default", category=DeprecationWarning, module="__main__")
448 | 
449 | 
450 | DEFAULT_CONFIG_FILE = "~/.config/ptpython/config.py"
451 | 
452 | 
453 | def run_config(repl: PythonInput, config_file: str | None = None) -> None:
454 |     """
455 |     Execute REPL config file.
456 | 
457 |     :param repl: `PythonInput` instance.
458 |     :param config_file: Path of the configuration file.
459 |     """
460 |     explicit_config_file = config_file is not None
461 | 
462 |     # Expand tildes.
463 |     config_file = os.path.expanduser(
464 |         config_file if config_file is not None else DEFAULT_CONFIG_FILE
465 |     )
466 | 
467 |     def enter_to_continue() -> None:
468 |         input("\nPress ENTER to continue...")
469 | 
470 |     # Check whether this file exists.
471 |     if not os.path.exists(config_file):
472 |         if explicit_config_file:
473 |             print(f"Impossible to read {config_file}")
474 |             enter_to_continue()
475 |         return
476 | 
477 |     # Run the config file in an empty namespace.
478 |     try:
479 |         namespace: dict[str, Any] = {}
480 | 
481 |         with open(config_file, "rb") as f:
482 |             code = compile(f.read(), config_file, "exec")
483 |             exec(code, namespace, namespace)
484 | 
485 |         # Now we should have a 'configure' method in this namespace. We call this
486 |         # method with the repl as an argument.
487 |         if "configure" in namespace:
488 |             namespace["configure"](repl)
489 | 
490 |     except Exception:
491 |         traceback.print_exc()
492 |         enter_to_continue()
493 | 
494 | 
495 | class exit:
496 |     """
497 |     Exit the ptpython REPL.
498 |     """
499 | 
500 |     # This custom exit function ensures that the `embed` function returns from
501 |     # where we are embedded, and Python doesn't close `sys.stdin` like
502 |     # the default `exit` from `_sitebuiltins.Quitter` does.
503 | 
504 |     def __call__(self) -> NoReturn:
505 |         raise ReplExit
506 | 
507 |     def __repr__(self) -> str:
508 |         # (Same message as the built-in Python REPL.)
509 |         return "Use exit() or Ctrl-D (i.e. EOF) to exit"
510 | 
511 | 
512 | class ReplExit(Exception):
513 |     """
514 |     Exception raised by ptpython's exit function.
515 |     """
516 | 
517 | 
518 | @overload
519 | def embed(
520 |     globals: dict[str, Any] | None = ...,
521 |     locals: dict[str, Any] | None = ...,
522 |     configure: Callable[[PythonRepl], None] | None = ...,
523 |     vi_mode: bool = ...,
524 |     history_filename: str | None = ...,
525 |     title: str | None = ...,
526 |     startup_paths: Sequence[str | Path] | None = ...,
527 |     patch_stdout: bool = ...,
528 |     patch_stdout_raw: bool = ...,
529 |     return_asyncio_coroutine: Literal[False] = ...,
530 | ) -> None: ...
531 | 
532 | 
533 | @overload
534 | def embed(
535 |     globals: dict[str, Any] | None = ...,
536 |     locals: dict[str, Any] | None = ...,
537 |     configure: Callable[[PythonRepl], None] | None = ...,
538 |     vi_mode: bool = ...,
539 |     history_filename: str | None = ...,
540 |     title: str | None = ...,
541 |     startup_paths: Sequence[str | Path] | None = ...,
542 |     patch_stdout: bool = ...,
543 |     patch_stdout_raw: bool = ...,
544 |     return_asyncio_coroutine: Literal[True] = ...,
545 | ) -> Coroutine[Any, Any, None]: ...
546 | 
547 | 
548 | def embed(
549 |     globals: dict[str, Any] | None = None,
550 |     locals: dict[str, Any] | None = None,
551 |     configure: Callable[[PythonRepl], None] | None = None,
552 |     vi_mode: bool = False,
553 |     history_filename: str | None = None,
554 |     title: str | None = None,
555 |     startup_paths: Sequence[str | Path] | None = None,
556 |     patch_stdout: bool = False,
557 |     patch_stdout_raw: bool = False,
558 |     return_asyncio_coroutine: bool = False,
559 | ) -> None | Coroutine[Any, Any, None]:
560 |     """
561 |     Call this to embed  Python shell at the current point in your program.
562 |     It's similar to `IPython.embed` and `bpython.embed`. ::
563 | 
564 |         from prompt_toolkit.contrib.repl import embed
565 |         embed(globals(), locals())
566 | 
567 |     :param vi_mode: Boolean. Use Vi instead of Emacs key bindings.
568 |     :param configure: Callable that will be called with the `PythonRepl` as a first
569 |                       argument, to trigger configuration.
570 |     :param title: Title to be displayed in the terminal titlebar. (None or string.)
571 |     :param patch_stdout:  When true, patch `sys.stdout` so that background
572 |         threads that are printing will print nicely above the prompt.
573 |     :param patch_stdout_raw:  When true, patch_stdout will not escape/remove vt100 terminal escape sequences.
574 |     """
575 |     # Default globals/locals
576 |     if globals is None:
577 |         globals = {
578 |             "__name__": "__main__",
579 |             "__package__": None,
580 |             "__doc__": None,
581 |             "__builtins__": builtins,
582 |         }
583 | 
584 |     locals = locals or globals
585 | 
586 |     def get_globals() -> dict[str, Any]:
587 |         return globals
588 | 
589 |     def get_locals() -> dict[str, Any]:
590 |         return locals
591 | 
592 |     # Create REPL.
593 |     repl = PythonRepl(
594 |         get_globals=get_globals,
595 |         get_locals=get_locals,
596 |         vi_mode=vi_mode,
597 |         history_filename=history_filename,
598 |         startup_paths=startup_paths,
599 |     )
600 | 
601 |     if title:
602 |         repl.terminal_title = title
603 | 
604 |     if configure:
605 |         configure(repl)
606 | 
607 |     # Start repl.
608 |     patch_context: ContextManager[None] = (
609 |         patch_stdout_context(raw=patch_stdout_raw) if patch_stdout else DummyContext()
610 |     )
611 | 
612 |     if return_asyncio_coroutine:
613 | 
614 |         async def coroutine() -> None:
615 |             with patch_context:
616 |                 await repl.run_async()
617 | 
618 |         return coroutine()  # type: ignore
619 |     else:
620 |         with patch_context:
621 |             repl.run()
622 |         return None
623 | 


--------------------------------------------------------------------------------
/src/ptpython/signatures.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Helpers for retrieving the function signature of the function call that we are
  3 | editing.
  4 | 
  5 | Either with the Jedi library, or using `inspect.signature` if Jedi fails and we
  6 | can use `eval()` to evaluate the function object.
  7 | """
  8 | 
  9 | from __future__ import annotations
 10 | 
 11 | import inspect
 12 | from inspect import Signature as InspectSignature
 13 | from inspect import _ParameterKind as ParameterKind
 14 | from typing import TYPE_CHECKING, Any, Sequence
 15 | 
 16 | from prompt_toolkit.document import Document
 17 | 
 18 | from .completer import DictionaryCompleter
 19 | from .utils import get_jedi_script_from_document
 20 | 
 21 | if TYPE_CHECKING:
 22 |     import jedi.api.classes
 23 | 
 24 | __all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"]
 25 | 
 26 | 
 27 | class Parameter:
 28 |     def __init__(
 29 |         self,
 30 |         name: str,
 31 |         annotation: str | None,
 32 |         default: str | None,
 33 |         kind: ParameterKind,
 34 |     ) -> None:
 35 |         self.name = name
 36 |         self.kind = kind
 37 | 
 38 |         self.annotation = annotation
 39 |         self.default = default
 40 | 
 41 |     def __repr__(self) -> str:
 42 |         return f"Parameter(name={self.name!r})"
 43 | 
 44 |     @property
 45 |     def description(self) -> str:
 46 |         """
 47 |         Name + annotation.
 48 |         """
 49 |         description = self.name
 50 | 
 51 |         if self.annotation is not None:
 52 |             description += f": {self.annotation}"
 53 | 
 54 |         return description
 55 | 
 56 | 
 57 | class Signature:
 58 |     """
 59 |     Signature definition used wrap around both Jedi signatures and
 60 |     python-inspect signatures.
 61 | 
 62 |     :param index: Parameter index of the current cursor position.
 63 |     :param bracket_start: (line, column) tuple for the open bracket that starts
 64 |         the function call.
 65 |     """
 66 | 
 67 |     def __init__(
 68 |         self,
 69 |         name: str,
 70 |         docstring: str,
 71 |         parameters: Sequence[Parameter],
 72 |         index: int | None = None,
 73 |         returns: str = "",
 74 |         bracket_start: tuple[int, int] = (0, 0),
 75 |     ) -> None:
 76 |         self.name = name
 77 |         self.docstring = docstring
 78 |         self.parameters = parameters
 79 |         self.index = index
 80 |         self.returns = returns
 81 |         self.bracket_start = bracket_start
 82 | 
 83 |     @classmethod
 84 |     def from_inspect_signature(
 85 |         cls,
 86 |         name: str,
 87 |         docstring: str,
 88 |         signature: InspectSignature,
 89 |         index: int,
 90 |     ) -> Signature:
 91 |         parameters = []
 92 | 
 93 |         def get_annotation_name(annotation: object) -> str:
 94 |             """
 95 |             Get annotation as string from inspect signature.
 96 |             """
 97 |             try:
 98 |                 # In case the annotation is a class like "int", "float", ...
 99 |                 return str(annotation.__name__)  # type: ignore
100 |             except AttributeError:
101 |                 pass  # No attribute `__name__`, e.g., in case of `List[int]`.
102 | 
103 |             annotation = str(annotation)
104 |             if annotation.startswith("typing."):
105 |                 annotation = annotation[len("typing:") :]
106 |             return annotation
107 | 
108 |         for p in signature.parameters.values():
109 |             parameters.append(
110 |                 Parameter(
111 |                     name=p.name,
112 |                     annotation=get_annotation_name(p.annotation),
113 |                     default=repr(p.default)
114 |                     if p.default is not inspect.Parameter.empty
115 |                     else None,
116 |                     kind=p.kind,
117 |                 )
118 |             )
119 | 
120 |         return cls(
121 |             name=name,
122 |             docstring=docstring,
123 |             parameters=parameters,
124 |             index=index,
125 |             returns="",
126 |         )
127 | 
128 |     @classmethod
129 |     def from_jedi_signature(cls, signature: jedi.api.classes.Signature) -> Signature:
130 |         parameters = []
131 | 
132 |         for p in signature.params:
133 |             if p is None:
134 |                 # We just hit the "*".
135 |                 continue
136 | 
137 |             parameters.append(
138 |                 Parameter(
139 |                     name=p.to_string(),  # p.name, (`to_string()` already includes the annotation).
140 |                     annotation=None,  # p.infer_annotation()
141 |                     default=None,  # p.infer_default()
142 |                     kind=p.kind,
143 |                 )
144 |             )
145 | 
146 |         docstring = signature.docstring()
147 |         if not isinstance(docstring, str):
148 |             docstring = docstring.decode("utf-8")
149 | 
150 |         return cls(
151 |             name=signature.name,
152 |             docstring=docstring,
153 |             parameters=parameters,
154 |             index=signature.index,
155 |             returns="",
156 |             bracket_start=signature.bracket_start,
157 |         )
158 | 
159 |     def __repr__(self) -> str:
160 |         return f"Signature({self.name!r}, parameters={self.parameters!r})"
161 | 
162 | 
163 | def get_signatures_using_jedi(
164 |     document: Document, locals: dict[str, Any], globals: dict[str, Any]
165 | ) -> list[Signature]:
166 |     script = get_jedi_script_from_document(document, locals, globals)
167 | 
168 |     # Show signatures in help text.
169 |     if not script:
170 |         return []
171 | 
172 |     try:
173 |         signatures = script.get_signatures()
174 |     except ValueError:
175 |         # e.g. in case of an invalid \\x escape.
176 |         signatures = []
177 |     except Exception:
178 |         # Sometimes we still get an exception (TypeError), because
179 |         # of probably bugs in jedi. We can silence them.
180 |         # See: https://github.com/davidhalter/jedi/issues/492
181 |         signatures = []
182 |     else:
183 |         # Try to access the params attribute just once. For Jedi
184 |         # signatures containing the keyword-only argument star,
185 |         # this will crash when retrieving it the first time with
186 |         # AttributeError. Every following time it works.
187 |         # See: https://github.com/jonathanslenders/ptpython/issues/47
188 |         #      https://github.com/davidhalter/jedi/issues/598
189 |         try:
190 |             if signatures:
191 |                 signatures[0].params
192 |         except AttributeError:
193 |             pass
194 | 
195 |     return [Signature.from_jedi_signature(sig) for sig in signatures]
196 | 
197 | 
198 | def get_signatures_using_eval(
199 |     document: Document, locals: dict[str, Any], globals: dict[str, Any]
200 | ) -> list[Signature]:
201 |     """
202 |     Look for the signature of the function before the cursor position without
203 |     use of Jedi. This uses a similar approach as the `DictionaryCompleter` of
204 |     running `eval()` over the detected function name.
205 |     """
206 |     # Look for open parenthesis, before cursor position.
207 |     pos = document.cursor_position - 1
208 | 
209 |     paren_mapping = {")": "(", "}": "{", "]": "["}
210 |     paren_stack = [
211 |         ")"
212 |     ]  # Start stack with closing ')'. We are going to look for the matching open ')'.
213 |     comma_count = 0  # Number of comma's between start of function call and cursor pos.
214 |     found_start = False  # Found something.
215 | 
216 |     while pos >= 0:
217 |         char = document.text[pos]
218 |         if char in ")]}":
219 |             paren_stack.append(char)
220 |         elif char in "([{":
221 |             if not paren_stack:
222 |                 # Open paren, while no closing paren was found. Mouse cursor is
223 |                 # positioned in nested parentheses. Not at the "top-level" of a
224 |                 # function call.
225 |                 break
226 |             if paren_mapping[paren_stack[-1]] != char:
227 |                 # Unmatching parentheses: syntax error?
228 |                 break
229 | 
230 |             paren_stack.pop()
231 | 
232 |             if len(paren_stack) == 0:
233 |                 found_start = True
234 |                 break
235 | 
236 |         elif char == "," and len(paren_stack) == 1:
237 |             comma_count += 1
238 | 
239 |         pos -= 1
240 | 
241 |     if not found_start:
242 |         return []
243 | 
244 |     # We found the start of the function call. Now look for the object before
245 |     # this position on which we can do an 'eval' to retrieve the function
246 |     # object.
247 |     obj = DictionaryCompleter(lambda: globals, lambda: locals).eval_expression(
248 |         Document(document.text, cursor_position=pos), locals
249 |     )
250 |     if obj is None:
251 |         return []
252 | 
253 |     try:
254 |         name = obj.__name__  # type:ignore
255 |     except Exception:
256 |         name = obj.__class__.__name__
257 | 
258 |     try:
259 |         signature = inspect.signature(obj)  # type: ignore
260 |     except TypeError:
261 |         return []  # Not a callable object.
262 |     except ValueError:
263 |         return []  # No signature found, like for build-ins like "print".
264 | 
265 |     try:
266 |         doc = obj.__doc__ or ""
267 |     except:
268 |         doc = ""
269 | 
270 |     # TODO: `index` is not yet correct when dealing with keyword-only arguments.
271 |     return [Signature.from_inspect_signature(name, doc, signature, index=comma_count)]
272 | 


--------------------------------------------------------------------------------
/src/ptpython/style.py:
--------------------------------------------------------------------------------
  1 | from __future__ import annotations
  2 | 
  3 | from prompt_toolkit.styles import BaseStyle, Style, merge_styles
  4 | from prompt_toolkit.styles.pygments import style_from_pygments_cls
  5 | from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported
  6 | from pygments.styles import get_all_styles, get_style_by_name
  7 | 
  8 | __all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"]
  9 | 
 10 | 
 11 | def get_all_code_styles() -> dict[str, BaseStyle]:
 12 |     """
 13 |     Return a mapping from style names to their classes.
 14 |     """
 15 |     result: dict[str, BaseStyle] = {
 16 |         name: style_from_pygments_cls(get_style_by_name(name))
 17 |         for name in get_all_styles()
 18 |     }
 19 |     result["win32"] = Style.from_dict(win32_code_style)
 20 |     return result
 21 | 
 22 | 
 23 | def get_all_ui_styles() -> dict[str, BaseStyle]:
 24 |     """
 25 |     Return a dict mapping {ui_style_name -> style_dict}.
 26 |     """
 27 |     return {
 28 |         "default": Style.from_dict(default_ui_style),
 29 |         "blue": Style.from_dict(blue_ui_style),
 30 |     }
 31 | 
 32 | 
 33 | def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle:
 34 |     """
 35 |     Generate Pygments Style class from two dictionaries
 36 |     containing style rules.
 37 |     """
 38 |     return merge_styles([python_style, ui_style])
 39 | 
 40 | 
 41 | # Code style for Windows consoles. They support only 16 colors,
 42 | # so we choose a combination that displays nicely.
 43 | win32_code_style = {
 44 |     "pygments.comment": "#00ff00",
 45 |     "pygments.keyword": "#44ff44",
 46 |     "pygments.number": "",
 47 |     "pygments.operator": "",
 48 |     "pygments.string": "#ff44ff",
 49 |     "pygments.name": "",
 50 |     "pygments.name.decorator": "#ff4444",
 51 |     "pygments.name.class": "#ff4444",
 52 |     "pygments.name.function": "#ff4444",
 53 |     "pygments.name.builtin": "#ff4444",
 54 |     "pygments.name.attribute": "",
 55 |     "pygments.name.constant": "",
 56 |     "pygments.name.entity": "",
 57 |     "pygments.name.exception": "",
 58 |     "pygments.name.label": "",
 59 |     "pygments.name.namespace": "",
 60 |     "pygments.name.tag": "",
 61 |     "pygments.name.variable": "",
 62 | }
 63 | 
 64 | 
 65 | default_ui_style = {
 66 |     "control-character": "ansiblue",
 67 |     # Classic prompt.
 68 |     "prompt": "bold",
 69 |     "prompt.dots": "noinherit",
 70 |     # (IPython <5.0) Prompt: "In [1]:"
 71 |     "in": "bold #008800",
 72 |     "in.number": "",
 73 |     # Return value.
 74 |     "out": "#ff0000",
 75 |     "out.number": "#ff0000",
 76 |     # Completions.
 77 |     "completion.builtin": "",
 78 |     "completion.param": "#006666 italic",
 79 |     "completion.keyword": "fg:#008800",
 80 |     "completion.keyword fuzzymatch.inside": "fg:#008800",
 81 |     "completion.keyword fuzzymatch.outside": "fg:#44aa44",
 82 |     # Separator between windows. (Used above docstring.)
 83 |     "separator": "#bbbbbb",
 84 |     # System toolbar
 85 |     "system-toolbar": "#22aaaa noinherit",
 86 |     # "arg" toolbar.
 87 |     "arg-toolbar": "#22aaaa noinherit",
 88 |     "arg-toolbar.text": "noinherit",
 89 |     # Signature toolbar.
 90 |     "signature-toolbar": "bg:#44bbbb #000000",
 91 |     "signature-toolbar current-name": "bg:#008888 #ffffff bold",
 92 |     "signature-toolbar operator": "#000000 bold",
 93 |     "docstring": "#888888",
 94 |     # Validation toolbar.
 95 |     "validation-toolbar": "bg:#440000 #aaaaaa",
 96 |     # Status toolbar.
 97 |     "status-toolbar": "bg:#222222 #aaaaaa",
 98 |     "status-toolbar.title": "underline",
 99 |     "status-toolbar.inputmode": "bg:#222222 #ffffaa",
100 |     "status-toolbar.key": "bg:#000000 #888888",
101 |     "status-toolbar key": "bg:#000000 #888888",
102 |     "status-toolbar.pastemodeon": "bg:#aa4444 #ffffff",
103 |     "status-toolbar.pythonversion": "bg:#222222 #ffffff bold",
104 |     "status-toolbar paste-mode-on": "bg:#aa4444 #ffffff",
105 |     "record": "bg:#884444 white",
106 |     "status-toolbar more": "#ffff44",
107 |     "status-toolbar.input-mode": "#ffff44",
108 |     # The options sidebar.
109 |     "sidebar": "bg:#bbbbbb #000000",
110 |     "sidebar.title": "bg:#668866 #ffffff",
111 |     "sidebar.label": "bg:#bbbbbb #222222",
112 |     "sidebar.status": "bg:#dddddd #000011",
113 |     "sidebar.label selected": "bg:#222222 #eeeeee",
114 |     "sidebar.status selected": "bg:#444444 #ffffff bold",
115 |     "sidebar.separator": "underline",
116 |     "sidebar.key": "bg:#bbddbb #000000 bold",
117 |     "sidebar.key.description": "bg:#bbbbbb #000000",
118 |     "sidebar.helptext": "bg:#fdf6e3 #000011",
119 |     #        # Styling for the history layout.
120 |     #        history.line:                          '',
121 |     #        history.line.selected:                 'bg:#008800  #000000',
122 |     #        history.line.current:                  'bg:#ffffff #000000',
123 |     #        history.line.selected.current:         'bg:#88ff88 #000000',
124 |     #        history.existinginput:                  '#888888',
125 |     # Help Window.
126 |     "window-border": "#aaaaaa",
127 |     "window-title": "bg:#bbbbbb #000000",
128 |     # Meta-enter message.
129 |     "accept-message": "bg:#ffff88 #444444",
130 |     # Exit confirmation.
131 |     "exit-confirmation": "bg:#884444 #ffffff",
132 | }
133 | 
134 | 
135 | # Some changes to get a bit more contrast on Windows consoles.
136 | # (They only support 16 colors.)
137 | if is_windows() and not is_conemu_ansi() and not is_windows_vt100_supported():
138 |     default_ui_style.update(
139 |         {
140 |             "sidebar.title": "bg:#00ff00 #ffffff",
141 |             "exitconfirmation": "bg:#ff4444 #ffffff",
142 |             "toolbar.validation": "bg:#ff4444 #ffffff",
143 |             "menu.completions.completion": "bg:#ffffff #000000",
144 |             "menu.completions.completion.current": "bg:#aaaaaa #000000",
145 |         }
146 |     )
147 | 
148 | 
149 | blue_ui_style = {}
150 | blue_ui_style.update(default_ui_style)
151 | # blue_ui_style.update({
152 | #        # Line numbers.
153 | #        Token.LineNumber:                             '#aa6666',
154 | #
155 | #        # Highlighting of search matches in document.
156 | #        Token.SearchMatch:                            '#ffffff bg:#4444aa',
157 | #        Token.SearchMatch.Current:                    '#ffffff bg:#44aa44',
158 | #
159 | #        # Highlighting of select text in document.
160 | #        Token.SelectedText:                           '#ffffff bg:#6666aa',
161 | #
162 | #        # Completer toolbar.
163 | #        Token.Toolbar.Completions:                    'bg:#44bbbb #000000',
164 | #        Token.Toolbar.Completions.Arrow:              'bg:#44bbbb #000000 bold',
165 | #        Token.Toolbar.Completions.Completion:         'bg:#44bbbb #000000',
166 | #        Token.Toolbar.Completions.Completion.Current: 'bg:#008888 #ffffff',
167 | #
168 | #        # Completer menu.
169 | #        Token.Menu.Completions.Completion:            'bg:#44bbbb #000000',
170 | #        Token.Menu.Completions.Completion.Current:    'bg:#008888 #ffffff',
171 | #        Token.Menu.Completions.Meta:                  'bg:#449999 #000000',
172 | #        Token.Menu.Completions.Meta.Current:          'bg:#00aaaa #000000',
173 | #        Token.Menu.Completions.ProgressBar:           'bg:#aaaaaa',
174 | #        Token.Menu.Completions.ProgressButton:        'bg:#000000',
175 | # })
176 | 


--------------------------------------------------------------------------------
/src/ptpython/utils.py:
--------------------------------------------------------------------------------
  1 | """
  2 | For internal use only.
  3 | """
  4 | 
  5 | from __future__ import annotations
  6 | 
  7 | import re
  8 | from typing import TYPE_CHECKING, Any, Callable, Iterable, TypeVar, cast
  9 | 
 10 | from prompt_toolkit.document import Document
 11 | from prompt_toolkit.formatted_text import to_formatted_text
 12 | from prompt_toolkit.formatted_text.utils import fragment_list_to_text
 13 | from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
 14 | 
 15 | if TYPE_CHECKING:
 16 |     from jedi import Interpreter
 17 | 
 18 |     # See: prompt_toolkit/key_binding/key_bindings.py
 19 |     # Annotating these return types as `object` is what works best, because
 20 |     # `NotImplemented` is typed `Any`.
 21 |     NotImplementedOrNone = object
 22 | 
 23 | __all__ = [
 24 |     "has_unclosed_brackets",
 25 |     "get_jedi_script_from_document",
 26 |     "document_is_multiline_python",
 27 |     "unindent_code",
 28 | ]
 29 | 
 30 | 
 31 | def has_unclosed_brackets(text: str) -> bool:
 32 |     """
 33 |     Starting at the end of the string. If we find an opening bracket
 34 |     for which we didn't had a closing one yet, return True.
 35 |     """
 36 |     stack = []
 37 | 
 38 |     # Ignore braces inside strings
 39 |     text = re.sub(r"""('[^']*'|"[^"]*")""", "", text)  # XXX: handle escaped quotes.!
 40 | 
 41 |     for c in reversed(text):
 42 |         if c in "])}":
 43 |             stack.append(c)
 44 | 
 45 |         elif c in "[({":
 46 |             if stack:
 47 |                 if (
 48 |                     (c == "[" and stack[-1] == "]")
 49 |                     or (c == "{" and stack[-1] == "}")
 50 |                     or (c == "(" and stack[-1] == ")")
 51 |                 ):
 52 |                     stack.pop()
 53 |             else:
 54 |                 # Opening bracket for which we didn't had a closing one.
 55 |                 return True
 56 | 
 57 |     return False
 58 | 
 59 | 
 60 | def get_jedi_script_from_document(
 61 |     document: Document, locals: dict[str, Any], globals: dict[str, Any]
 62 | ) -> Interpreter:
 63 |     import jedi  # We keep this import in-line, to improve start-up time.
 64 | 
 65 |     # Importing Jedi is 'slow'.
 66 | 
 67 |     try:
 68 |         return jedi.Interpreter(
 69 |             document.text,
 70 |             path="input-text",
 71 |             namespaces=[locals, globals],
 72 |         )
 73 |     except ValueError:
 74 |         # Invalid cursor position.
 75 |         # ValueError('`column` parameter is not in a valid range.')
 76 |         return None
 77 |     except AttributeError:
 78 |         # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65
 79 |         # See also: https://github.com/davidhalter/jedi/issues/508
 80 |         return None
 81 |     except IndexError:
 82 |         # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514
 83 |         return None
 84 |     except KeyError:
 85 |         # Workaround for a crash when the input is "u'", the start of a unicode string.
 86 |         return None
 87 |     except Exception:
 88 |         # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91
 89 |         return None
 90 | 
 91 | 
 92 | _multiline_string_delims = re.compile("""[']{3}|["]{3}""")
 93 | 
 94 | 
 95 | def document_is_multiline_python(document: Document) -> bool:
 96 |     """
 97 |     Determine whether this is a multiline Python document.
 98 |     """
 99 | 
100 |     def ends_in_multiline_string() -> bool:
101 |         """
102 |         ``True`` if we're inside a multiline string at the end of the text.
103 |         """
104 |         delims = _multiline_string_delims.findall(document.text)
105 |         opening = None
106 |         for delim in delims:
107 |             if opening is None:
108 |                 opening = delim
109 |             elif delim == opening:
110 |                 opening = None
111 |         return bool(opening)
112 | 
113 |     if "\n" in document.text or ends_in_multiline_string():
114 |         return True
115 | 
116 |     def line_ends_with_colon() -> bool:
117 |         return document.current_line.rstrip()[-1:] == ":"
118 | 
119 |     # If we just typed a colon, or still have open brackets, always insert a real newline.
120 |     if (
121 |         line_ends_with_colon()
122 |         or (
123 |             document.is_cursor_at_the_end
124 |             and has_unclosed_brackets(document.text_before_cursor)
125 |         )
126 |         or document.text.startswith("@")
127 |     ):
128 |         return True
129 | 
130 |     # If the character before the cursor is a backslash (line continuation
131 |     # char), insert a new line.
132 |     elif document.text_before_cursor[-1:] == "\\":
133 |         return True
134 | 
135 |     return False
136 | 
137 | 
138 | _T = TypeVar("_T", bound=Callable[[MouseEvent], None])
139 | 
140 | 
141 | def if_mousedown(handler: _T) -> _T:
142 |     """
143 |     Decorator for mouse handlers.
144 |     Only handle event when the user pressed mouse down.
145 | 
146 |     (When applied to a token list. Scroll events will bubble up and are handled
147 |     by the Window.)
148 |     """
149 | 
150 |     def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone:
151 |         if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
152 |             return handler(mouse_event)
153 |         else:
154 |             return NotImplemented
155 | 
156 |     return cast(_T, handle_if_mouse_down)
157 | 
158 | 
159 | _T_type = TypeVar("_T_type", bound=type)
160 | 
161 | 
162 | def ptrepr_to_repr(cls: _T_type) -> _T_type:
163 |     """
164 |     Generate a normal `__repr__` method for classes that have a `__pt_repr__`.
165 |     """
166 |     if not hasattr(cls, "__pt_repr__"):
167 |         raise TypeError(
168 |             "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method."
169 |         )
170 | 
171 |     def __repr__(self: object) -> str:
172 |         assert hasattr(cls, "__pt_repr__")
173 |         return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self)))
174 | 
175 |     cls.__repr__ = __repr__  # type:ignore
176 |     return cls
177 | 
178 | 
179 | def unindent_code(text: str) -> str:
180 |     """
181 |     Remove common leading whitespace when all lines are indented.
182 |     """
183 |     lines = text.splitlines(keepends=True)
184 | 
185 |     # Look for common prefix.
186 |     common_prefix = _common_whitespace_prefix(lines)
187 | 
188 |     # Remove indentation.
189 |     lines = [line[len(common_prefix) :] for line in lines]
190 | 
191 |     return "".join(lines)
192 | 
193 | 
194 | def _common_whitespace_prefix(strings: Iterable[str]) -> str:
195 |     """
196 |     Return common prefix for a list of lines.
197 |     This will ignore lines that contain whitespace only.
198 |     """
199 |     # Ignore empty lines and lines that have whitespace only.
200 |     strings = [s for s in strings if not s.isspace() and not len(s) == 0]
201 | 
202 |     if not strings:
203 |         return ""
204 | 
205 |     else:
206 |         s1 = min(strings)
207 |         s2 = max(strings)
208 | 
209 |         for i, c in enumerate(s1):
210 |             if c != s2[i] or c not in " \t":
211 |                 return s1[:i]
212 | 
213 |         return s1
214 | 


--------------------------------------------------------------------------------
/src/ptpython/validator.py:
--------------------------------------------------------------------------------
 1 | from __future__ import annotations
 2 | 
 3 | from typing import Callable
 4 | 
 5 | from prompt_toolkit.document import Document
 6 | from prompt_toolkit.validation import ValidationError, Validator
 7 | 
 8 | from .utils import unindent_code
 9 | 
10 | __all__ = ["PythonValidator"]
11 | 
12 | 
13 | class PythonValidator(Validator):
14 |     """
15 |     Validation of Python input.
16 | 
17 |     :param get_compiler_flags: Callable that returns the currently
18 |         active compiler flags.
19 |     """
20 | 
21 |     def __init__(self, get_compiler_flags: Callable[[], int] | None = None) -> None:
22 |         self.get_compiler_flags = get_compiler_flags
23 | 
24 |     def validate(self, document: Document) -> None:
25 |         """
26 |         Check input for Python syntax errors.
27 |         """
28 |         text = unindent_code(document.text)
29 | 
30 |         # When the input starts with Ctrl-Z, always accept. This means EOF in a
31 |         # Python REPL.
32 |         if text.startswith("\x1a"):
33 |             return
34 | 
35 |         # When the input starts with an exclamation mark. Accept as shell
36 |         # command.
37 |         if text.lstrip().startswith("!"):
38 |             return
39 | 
40 |         try:
41 |             if self.get_compiler_flags:
42 |                 flags = self.get_compiler_flags()
43 |             else:
44 |                 flags = 0
45 | 
46 |             compile(text, "<input>", "exec", flags=flags, dont_inherit=True)
47 |         except SyntaxError as e:
48 |             # Note, the 'or 1' for offset is required because Python 2.7
49 |             # gives `None` as offset in case of '4=4' as input. (Looks like
50 |             # fixed in Python 3.)
51 |             # TODO: This is not correct if indentation was removed.
52 |             index = document.translate_row_col_to_index(
53 |                 (e.lineno or 1) - 1, (e.offset or 1) - 1
54 |             )
55 |             raise ValidationError(index, f"Syntax Error: {e}")
56 |         except TypeError as e:
57 |             # e.g. "compile() expected string without null bytes"
58 |             raise ValidationError(0, str(e))
59 |         except ValueError as e:
60 |             # In Python 2, compiling "\x9" (an invalid escape sequence) raises
61 |             # ValueError instead of SyntaxError.
62 |             raise ValidationError(0, f"Syntax Error: {e}")
63 | 


--------------------------------------------------------------------------------
/tests/test_dummy.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | from __future__ import annotations
 3 | 
 4 | import ptpython.completer
 5 | import ptpython.eventloop
 6 | import ptpython.filters
 7 | import ptpython.history_browser
 8 | import ptpython.key_bindings
 9 | import ptpython.layout
10 | import ptpython.python_input
11 | import ptpython.repl
12 | import ptpython.style
13 | import ptpython.utils
14 | import ptpython.validator
15 | 
16 | # For now there are no tests here.
17 | # However this is sufficient to do at least a syntax check.
18 | 
19 | 
20 | def test_dummy() -> None:
21 |     assert ptpython.completer
22 |     assert ptpython.eventloop
23 |     assert ptpython.filters
24 |     assert ptpython.history_browser
25 |     assert ptpython.key_bindings
26 |     assert ptpython.layout
27 |     assert ptpython.python_input
28 |     assert ptpython.repl
29 |     assert ptpython.style
30 |     assert ptpython.utils
31 |     assert ptpython.validator
32 | 


--------------------------------------------------------------------------------