Show More
@@ -62,7 +62,7 b' jobs:' | |||
|
62 | 62 | run: | |
|
63 | 63 | python -m pip install --upgrade pip setuptools wheel build |
|
64 | 64 | python -m pip install --upgrade -e .[${{ matrix.deps }}] |
|
65 | python -m pip install --upgrade check-manifest pytest-cov | |
|
65 | python -m pip install --upgrade check-manifest pytest-cov pytest-json-report | |
|
66 | 66 | - name: Try building with Python build |
|
67 | 67 | if: runner.os != 'Windows' # setup.py does not support sdist on Windows |
|
68 | 68 | run: | |
@@ -75,7 +75,13 b' jobs:' | |||
|
75 | 75 | env: |
|
76 | 76 | COLUMNS: 120 |
|
77 | 77 | run: | |
|
78 | pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} | |
|
78 | pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }} --json-report --json-report-file=./report-${{ matrix.python-version }}-${{runner.os}}.json | |
|
79 | - uses: actions/upload-artifact@v3 | |
|
80 | with: | |
|
81 | name: upload pytest timing reports as json | |
|
82 | path: | | |
|
83 | ./report-*.json | |
|
84 | ||
|
79 | 85 | - name: Upload coverage to Codecov |
|
80 | 86 | uses: codecov/codecov-action@v3 |
|
81 | 87 | with: |
@@ -567,8 +567,11 b' class HistoryManager(HistoryAccessor):' | |||
|
567 | 567 | conn = self.db |
|
568 | 568 | |
|
569 | 569 | with conn: |
|
570 | cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL, | |
|
571 | NULL, "") """, (datetime.datetime.now(),)) | |
|
570 | cur = conn.execute( | |
|
571 | """INSERT INTO sessions VALUES (NULL, ?, NULL, | |
|
572 | NULL, '') """, | |
|
573 | (datetime.datetime.now(),), | |
|
574 | ) | |
|
572 | 575 | self.session_number = cur.lastrowid |
|
573 | 576 | |
|
574 | 577 | def end_session(self): |
@@ -32,7 +32,7 b' from io import open as io_open' | |||
|
32 | 32 | from logging import error |
|
33 | 33 | from pathlib import Path |
|
34 | 34 | from typing import Callable |
|
35 | from typing import List as ListType | |
|
35 | from typing import List as ListType, Dict as DictType, Any as AnyType | |
|
36 | 36 | from typing import Optional, Tuple |
|
37 | 37 | from warnings import warn |
|
38 | 38 | |
@@ -90,6 +90,8 b' from IPython.utils.process import getoutput, system' | |||
|
90 | 90 | from IPython.utils.strdispatch import StrDispatch |
|
91 | 91 | from IPython.utils.syspathcontext import prepended_to_syspath |
|
92 | 92 | from IPython.utils.text import DollarFormatter, LSString, SList, format_screen |
|
93 | from IPython.core.oinspect import OInfo | |
|
94 | ||
|
93 | 95 | |
|
94 | 96 | sphinxify: Optional[Callable] |
|
95 | 97 | |
@@ -1560,15 +1562,28 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1560 | 1562 | #------------------------------------------------------------------------- |
|
1561 | 1563 | # Things related to object introspection |
|
1562 | 1564 | #------------------------------------------------------------------------- |
|
1565 | @staticmethod | |
|
1566 | def _find_parts(oname: str) -> ListType[str]: | |
|
1567 | """ | |
|
1568 | Given an object name, return a list of parts of this object name. | |
|
1569 | ||
|
1570 | Basically split on docs when using attribute access, | |
|
1571 | and extract the value when using square bracket. | |
|
1572 | ||
|
1573 | ||
|
1574 | For example foo.bar[3].baz[x] -> foo, bar, 3, baz, x | |
|
1575 | ||
|
1576 | ||
|
1577 | Returns | |
|
1578 | ------- | |
|
1579 | parts_ok: bool | |
|
1580 | wether we were properly able to parse parts. | |
|
1581 | parts: list of str | |
|
1582 | extracted parts | |
|
1563 | 1583 |
|
|
1564 | def _ofind(self, oname, namespaces=None): | |
|
1565 | """Find an object in the available namespaces. | |
|
1566 | 1584 | |
|
1567 | self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic | |
|
1568 | 1585 | |
|
1569 | Has special code to detect magic functions. | |
|
1570 | 1586 | """ |
|
1571 | oname = oname.strip() | |
|
1572 | 1587 | raw_parts = oname.split(".") |
|
1573 | 1588 | parts = [] |
|
1574 | 1589 | parts_ok = True |
@@ -1590,12 +1605,31 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1590 | 1605 | parts_ok = False |
|
1591 | 1606 | parts.append(p) |
|
1592 | 1607 | |
|
1608 | return parts_ok, parts | |
|
1609 | ||
|
1610 | def _ofind(self, oname: str, namespaces: DictType[str, AnyType] = None): | |
|
1611 | """Find an object in the available namespaces. | |
|
1612 | ||
|
1613 | self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic | |
|
1614 | ||
|
1615 | Has special code to detect magic functions. | |
|
1616 | """ | |
|
1617 | oname = oname.strip() | |
|
1618 | parts_ok, parts = self._find_parts(oname) | |
|
1619 | ||
|
1593 | 1620 | if ( |
|
1594 | 1621 | not oname.startswith(ESC_MAGIC) |
|
1595 | 1622 | and not oname.startswith(ESC_MAGIC2) |
|
1596 | 1623 | and not parts_ok |
|
1597 | 1624 | ): |
|
1598 |
return |
|
|
1625 | return OInfo( | |
|
1626 | ismagic=False, | |
|
1627 | isalias=False, | |
|
1628 | found=False, | |
|
1629 | obj=None, | |
|
1630 | namespace="", | |
|
1631 | parent=None, | |
|
1632 | ) | |
|
1599 | 1633 | |
|
1600 | 1634 | if namespaces is None: |
|
1601 | 1635 | # Namespaces to search in: |
@@ -1675,14 +1709,16 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1675 | 1709 | found = True |
|
1676 | 1710 | ospace = 'Interactive' |
|
1677 | 1711 | |
|
1678 |
return |
|
|
1679 | 'obj':obj, | |
|
1680 |
|
|
|
1681 |
|
|
|
1682 | 'ismagic':ismagic, | |
|
1683 |
|
|
|
1684 | 'namespace':ospace | |
|
1685 | } | |
|
1712 | return OInfo( | |
|
1713 | **{ | |
|
1714 | "obj": obj, | |
|
1715 | "found": found, | |
|
1716 | "parent": parent, | |
|
1717 | "ismagic": ismagic, | |
|
1718 | "isalias": isalias, | |
|
1719 | "namespace": ospace, | |
|
1720 | } | |
|
1721 | ) | |
|
1686 | 1722 | |
|
1687 | 1723 | @staticmethod |
|
1688 | 1724 | def _getattr_property(obj, attrname): |
@@ -1726,9 +1762,9 b' class InteractiveShell(SingletonConfigurable):' | |||
|
1726 | 1762 | # Nothing helped, fall back. |
|
1727 | 1763 | return getattr(obj, attrname) |
|
1728 | 1764 | |
|
1729 | def _object_find(self, oname, namespaces=None): | |
|
1765 | def _object_find(self, oname, namespaces=None) -> OInfo: | |
|
1730 | 1766 | """Find an object and return a struct with info about it.""" |
|
1731 |
return |
|
|
1767 | return self._ofind(oname, namespaces) | |
|
1732 | 1768 | |
|
1733 | 1769 | def _inspect(self, meth, oname, namespaces=None, **kw): |
|
1734 | 1770 | """Generic interface to the inspector system. |
@@ -422,7 +422,8 b' class ExecutionMagics(Magics):' | |||
|
422 | 422 | ) |
|
423 | 423 | @no_var_expand |
|
424 | 424 | @line_cell_magic |
|
425 | def debug(self, line='', cell=None): | |
|
425 | @needs_local_scope | |
|
426 | def debug(self, line="", cell=None, local_ns=None): | |
|
426 | 427 | """Activate the interactive debugger. |
|
427 | 428 | |
|
428 | 429 | This magic command support two ways of activating debugger. |
@@ -453,7 +454,7 b' class ExecutionMagics(Magics):' | |||
|
453 | 454 | self._debug_post_mortem() |
|
454 | 455 | elif not (args.breakpoint or cell): |
|
455 | 456 | # If there is no breakpoints, the line is just code to execute |
|
456 | self._debug_exec(line, None) | |
|
457 | self._debug_exec(line, None, local_ns) | |
|
457 | 458 | else: |
|
458 | 459 | # Here we try to reconstruct the code from the output of |
|
459 | 460 | # parse_argstring. This might not work if the code has spaces |
@@ -461,18 +462,20 b' class ExecutionMagics(Magics):' | |||
|
461 | 462 | code = "\n".join(args.statement) |
|
462 | 463 | if cell: |
|
463 | 464 | code += "\n" + cell |
|
464 | self._debug_exec(code, args.breakpoint) | |
|
465 | self._debug_exec(code, args.breakpoint, local_ns) | |
|
465 | 466 | |
|
466 | 467 | def _debug_post_mortem(self): |
|
467 | 468 | self.shell.debugger(force=True) |
|
468 | 469 | |
|
469 | def _debug_exec(self, code, breakpoint): | |
|
470 | def _debug_exec(self, code, breakpoint, local_ns=None): | |
|
470 | 471 | if breakpoint: |
|
471 | 472 | (filename, bp_line) = breakpoint.rsplit(':', 1) |
|
472 | 473 | bp_line = int(bp_line) |
|
473 | 474 | else: |
|
474 | 475 | (filename, bp_line) = (None, None) |
|
475 | self._run_with_debugger(code, self.shell.user_ns, filename, bp_line) | |
|
476 | self._run_with_debugger( | |
|
477 | code, self.shell.user_ns, filename, bp_line, local_ns=local_ns | |
|
478 | ) | |
|
476 | 479 | |
|
477 | 480 | @line_magic |
|
478 | 481 | def tb(self, s): |
@@ -867,8 +870,9 b' class ExecutionMagics(Magics):' | |||
|
867 | 870 | |
|
868 | 871 | return stats |
|
869 | 872 | |
|
870 |
def _run_with_debugger( |
|
|
871 | bp_line=None, bp_file=None): | |
|
873 | def _run_with_debugger( | |
|
874 | self, code, code_ns, filename=None, bp_line=None, bp_file=None, local_ns=None | |
|
875 | ): | |
|
872 | 876 | """ |
|
873 | 877 | Run `code` in debugger with a break point. |
|
874 | 878 | |
@@ -885,6 +889,8 b' class ExecutionMagics(Magics):' | |||
|
885 | 889 | bp_file : str, optional |
|
886 | 890 | Path to the file in which break point is specified. |
|
887 | 891 | `filename` is used if not given. |
|
892 | local_ns : dict, optional | |
|
893 | A local namespace in which `code` is executed. | |
|
888 | 894 | |
|
889 | 895 | Raises |
|
890 | 896 | ------ |
@@ -941,7 +947,7 b' class ExecutionMagics(Magics):' | |||
|
941 | 947 | while True: |
|
942 | 948 | try: |
|
943 | 949 | trace = sys.gettrace() |
|
944 | deb.run(code, code_ns) | |
|
950 | deb.run(code, code_ns, local_ns) | |
|
945 | 951 | except Restart: |
|
946 | 952 | print("Restarting") |
|
947 | 953 | if filename: |
@@ -46,6 +46,19 b' from pygments import highlight' | |||
|
46 | 46 | from pygments.lexers import PythonLexer |
|
47 | 47 | from pygments.formatters import HtmlFormatter |
|
48 | 48 | |
|
49 | from typing import Any | |
|
50 | from dataclasses import dataclass | |
|
51 | ||
|
52 | ||
|
53 | @dataclass | |
|
54 | class OInfo: | |
|
55 | ismagic: bool | |
|
56 | isalias: bool | |
|
57 | found: bool | |
|
58 | namespace: str | |
|
59 | parent: Any | |
|
60 | obj: Any | |
|
61 | ||
|
49 | 62 | def pylight(code): |
|
50 | 63 | return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True)) |
|
51 | 64 |
@@ -499,7 +499,7 b' class AutocallChecker(PrefilterChecker):' | |||
|
499 | 499 | return None |
|
500 | 500 | |
|
501 | 501 | oinfo = line_info.ofind(self.shell) # This can mutate state via getattr |
|
502 |
if not oinfo |
|
|
502 | if not oinfo.found: | |
|
503 | 503 | return None |
|
504 | 504 | |
|
505 | 505 | ignored_funs = ['b', 'f', 'r', 'u', 'br', 'rb', 'fr', 'rf'] |
@@ -508,10 +508,12 b' class AutocallChecker(PrefilterChecker):' | |||
|
508 | 508 | if ifun.lower() in ignored_funs and (line.startswith(ifun + "'") or line.startswith(ifun + '"')): |
|
509 | 509 | return None |
|
510 | 510 | |
|
511 | if callable(oinfo['obj']) \ | |
|
512 | and (not self.exclude_regexp.match(line_info.the_rest)) \ | |
|
513 |
|
|
|
514 | return self.prefilter_manager.get_handler_by_name('auto') | |
|
511 | if ( | |
|
512 | callable(oinfo.obj) | |
|
513 | and (not self.exclude_regexp.match(line_info.the_rest)) | |
|
514 | and self.function_name_regexp.match(line_info.ifun) | |
|
515 | ): | |
|
516 | return self.prefilter_manager.get_handler_by_name("auto") | |
|
515 | 517 | else: |
|
516 | 518 | return None |
|
517 | 519 | |
@@ -601,7 +603,7 b' class AutoHandler(PrefilterHandler):' | |||
|
601 | 603 | the_rest = line_info.the_rest |
|
602 | 604 | esc = line_info.esc |
|
603 | 605 | continue_prompt = line_info.continue_prompt |
|
604 |
obj = line_info.ofind(self.shell) |
|
|
606 | obj = line_info.ofind(self.shell).obj | |
|
605 | 607 | |
|
606 | 608 | # This should only be active for single-line input! |
|
607 | 609 | if continue_prompt: |
@@ -25,6 +25,7 b' import sys' | |||
|
25 | 25 | |
|
26 | 26 | from IPython.utils import py3compat |
|
27 | 27 | from IPython.utils.encoding import get_stream_enc |
|
28 | from IPython.core.oinspect import OInfo | |
|
28 | 29 | |
|
29 | 30 | #----------------------------------------------------------------------------- |
|
30 | 31 | # Main function |
@@ -118,7 +119,7 b' class LineInfo(object):' | |||
|
118 | 119 | else: |
|
119 | 120 | self.pre_whitespace = self.pre |
|
120 | 121 | |
|
121 | def ofind(self, ip): | |
|
122 | def ofind(self, ip) -> OInfo: | |
|
122 | 123 | """Do a full, attribute-walking lookup of the ifun in the various |
|
123 | 124 | namespaces for the given IPython InteractiveShell instance. |
|
124 | 125 |
@@ -177,7 +177,7 b' def test_module_without_init():' | |||
|
177 | 177 | try: |
|
178 | 178 | os.makedirs(os.path.join(tmpdir, fake_module_name)) |
|
179 | 179 | s = try_import(mod=fake_module_name) |
|
180 | assert s == [] | |
|
180 | assert s == [], f"for module {fake_module_name}" | |
|
181 | 181 | finally: |
|
182 | 182 | sys.path.remove(tmpdir) |
|
183 | 183 |
@@ -25,6 +25,7 b' from os.path import join' | |||
|
25 | 25 | from IPython.core.error import InputRejected |
|
26 | 26 | from IPython.core.inputtransformer import InputTransformer |
|
27 | 27 | from IPython.core import interactiveshell |
|
28 | from IPython.core.oinspect import OInfo | |
|
28 | 29 | from IPython.testing.decorators import ( |
|
29 | 30 | skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist, |
|
30 | 31 | ) |
@@ -360,7 +361,7 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
360 | 361 | |
|
361 | 362 | # Get info on line magic |
|
362 | 363 | lfind = ip._ofind("lmagic") |
|
363 |
info = |
|
|
364 | info = OInfo( | |
|
364 | 365 | found=True, |
|
365 | 366 | isalias=False, |
|
366 | 367 | ismagic=True, |
@@ -379,7 +380,7 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
379 | 380 | |
|
380 | 381 | # Get info on cell magic |
|
381 | 382 | find = ip._ofind("cmagic") |
|
382 |
info = |
|
|
383 | info = OInfo( | |
|
383 | 384 | found=True, |
|
384 | 385 | isalias=False, |
|
385 | 386 | ismagic=True, |
@@ -397,9 +398,15 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
397 | 398 | |
|
398 | 399 | a = A() |
|
399 | 400 | |
|
400 |
found = ip._ofind( |
|
|
401 | info = dict(found=True, isalias=False, ismagic=False, | |
|
402 | namespace='locals', obj=A.foo, parent=a) | |
|
401 | found = ip._ofind("a.foo", [("locals", locals())]) | |
|
402 | info = OInfo( | |
|
403 | found=True, | |
|
404 | isalias=False, | |
|
405 | ismagic=False, | |
|
406 | namespace="locals", | |
|
407 | obj=A.foo, | |
|
408 | parent=a, | |
|
409 | ) | |
|
403 | 410 | self.assertEqual(found, info) |
|
404 | 411 | |
|
405 | 412 | def test_ofind_multiple_attribute_lookups(self): |
@@ -412,9 +419,15 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
412 | 419 | a.a = A() |
|
413 | 420 | a.a.a = A() |
|
414 | 421 | |
|
415 |
found = ip._ofind( |
|
|
416 | info = dict(found=True, isalias=False, ismagic=False, | |
|
417 | namespace='locals', obj=A.foo, parent=a.a.a) | |
|
422 | found = ip._ofind("a.a.a.foo", [("locals", locals())]) | |
|
423 | info = OInfo( | |
|
424 | found=True, | |
|
425 | isalias=False, | |
|
426 | ismagic=False, | |
|
427 | namespace="locals", | |
|
428 | obj=A.foo, | |
|
429 | parent=a.a.a, | |
|
430 | ) | |
|
418 | 431 | self.assertEqual(found, info) |
|
419 | 432 | |
|
420 | 433 | def test_ofind_slotted_attributes(self): |
@@ -424,14 +437,26 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
424 | 437 | self.foo = 'bar' |
|
425 | 438 | |
|
426 | 439 | a = A() |
|
427 |
found = ip._ofind( |
|
|
428 | info = dict(found=True, isalias=False, ismagic=False, | |
|
429 | namespace='locals', obj=a.foo, parent=a) | |
|
440 | found = ip._ofind("a.foo", [("locals", locals())]) | |
|
441 | info = OInfo( | |
|
442 | found=True, | |
|
443 | isalias=False, | |
|
444 | ismagic=False, | |
|
445 | namespace="locals", | |
|
446 | obj=a.foo, | |
|
447 | parent=a, | |
|
448 | ) | |
|
430 | 449 | self.assertEqual(found, info) |
|
431 | 450 | |
|
432 |
found = ip._ofind( |
|
|
433 | info = dict(found=False, isalias=False, ismagic=False, | |
|
434 | namespace=None, obj=None, parent=a) | |
|
451 | found = ip._ofind("a.bar", [("locals", locals())]) | |
|
452 | info = OInfo( | |
|
453 | found=False, | |
|
454 | isalias=False, | |
|
455 | ismagic=False, | |
|
456 | namespace=None, | |
|
457 | obj=None, | |
|
458 | parent=a, | |
|
459 | ) | |
|
435 | 460 | self.assertEqual(found, info) |
|
436 | 461 | |
|
437 | 462 | def test_ofind_prefers_property_to_instance_level_attribute(self): |
@@ -443,7 +468,7 b' class InteractiveShellTestCase(unittest.TestCase):' | |||
|
443 | 468 | a.__dict__["foo"] = "baz" |
|
444 | 469 | self.assertEqual(a.foo, "bar") |
|
445 | 470 | found = ip._ofind("a.foo", [("locals", locals())]) |
|
446 |
self.assertIs(found |
|
|
471 | self.assertIs(found.obj, A.foo) | |
|
447 | 472 | |
|
448 | 473 | def test_custom_syntaxerror_exception(self): |
|
449 | 474 | called = [] |
@@ -715,6 +715,7 b' def doctest_precision():' | |||
|
715 | 715 | Out[5]: '3.141593e+00' |
|
716 | 716 | """ |
|
717 | 717 | |
|
718 | ||
|
718 | 719 | def test_debug_magic(): |
|
719 | 720 | """Test debugging a small code with %debug |
|
720 | 721 | |
@@ -727,6 +728,22 b' def test_debug_magic():' | |||
|
727 | 728 | In [2]: |
|
728 | 729 | """ |
|
729 | 730 | |
|
731 | ||
|
732 | def test_debug_magic_locals(): | |
|
733 | """Test debugging a small code with %debug with locals | |
|
734 | ||
|
735 | In [1]: with PdbTestInput(['c']): | |
|
736 | ...: def fun(): | |
|
737 | ...: res = 1 | |
|
738 | ...: %debug print(res) | |
|
739 | ...: fun() | |
|
740 | ...: | |
|
741 | ... | |
|
742 | ipdb> c | |
|
743 | 1 | |
|
744 | In [2]: | |
|
745 | """ | |
|
746 | ||
|
730 | 747 | def test_psearch(): |
|
731 | 748 | with tt.AssertPrints("dict.fromkeys"): |
|
732 | 749 | _ip.run_cell("dict.fr*?") |
@@ -994,7 +994,7 b' class VerboseTB(TBTools):' | |||
|
994 | 994 | pygments_formatter=formatter, |
|
995 | 995 | ) |
|
996 | 996 | |
|
997 |
# |
|
|
997 | # Let's estimate the amount of code we will have to parse/highlight. | |
|
998 | 998 | cf = etb |
|
999 | 999 | max_len = 0 |
|
1000 | 1000 | tbs = [] |
@@ -10,6 +10,7 b' be accessed directly from the outside' | |||
|
10 | 10 | """ |
|
11 | 11 | import importlib.abc |
|
12 | 12 | import sys |
|
13 | import os | |
|
13 | 14 | import types |
|
14 | 15 | from functools import partial, lru_cache |
|
15 | 16 | import operator |
@@ -368,6 +369,10 b' def load_qt(api_options):' | |||
|
368 | 369 | commit_api(api) |
|
369 | 370 | return result |
|
370 | 371 | else: |
|
372 | # Clear the environment variable since it doesn't work. | |
|
373 | if "QT_API" in os.environ: | |
|
374 | del os.environ["QT_API"] | |
|
375 | ||
|
371 | 376 | raise ImportError( |
|
372 | 377 | """ |
|
373 | 378 | Could not load requested Qt binding. Please ensure that |
@@ -913,10 +913,19 b' class TerminalInteractiveShell(InteractiveShell):' | |||
|
913 | 913 | |
|
914 | 914 | active_eventloop = None |
|
915 | 915 | def enable_gui(self, gui=None): |
|
916 | if self._inputhook is None and gui is None: | |
|
917 | print("No event loop hook running.") | |
|
918 | return | |
|
919 | ||
|
916 | 920 | if self._inputhook is not None and gui is not None: |
|
917 |
|
|
|
918 |
f"Shell |
|
|
921 | print( | |
|
922 | f"Shell is already running a gui event loop for {self.active_eventloop}. " | |
|
923 | "Call with no arguments to disable the current loop." | |
|
919 | 924 | ) |
|
925 | return | |
|
926 | if self._inputhook is not None and gui is None: | |
|
927 | self.active_eventloop = self._inputhook = None | |
|
928 | ||
|
920 | 929 | if gui and (gui not in {"inline", "webagg"}): |
|
921 | 930 | # This hook runs with each cycle of the `prompt_toolkit`'s event loop. |
|
922 | 931 | self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) |
@@ -934,15 +943,18 b' class TerminalInteractiveShell(InteractiveShell):' | |||
|
934 | 943 | # same event loop as the rest of the code. don't use an actual |
|
935 | 944 | # input hook. (Asyncio is not made for nesting event loops.) |
|
936 | 945 | self.pt_loop = get_asyncio_loop() |
|
946 | print("Installed asyncio event loop hook.") | |
|
937 | 947 | |
|
938 | 948 | elif self._inputhook: |
|
939 | 949 | # If an inputhook was set, create a new asyncio event loop with |
|
940 | 950 | # this inputhook for the prompt. |
|
941 | 951 | self.pt_loop = new_eventloop_with_inputhook(self._inputhook) |
|
952 | print(f"Installed {self.active_eventloop} event loop hook.") | |
|
942 | 953 | else: |
|
943 | 954 | # When there's no inputhook, run the prompt in a separate |
|
944 | 955 | # asyncio event loop. |
|
945 | 956 | self.pt_loop = asyncio.new_event_loop() |
|
957 | print("GUI event loop hook disabled.") | |
|
946 | 958 | |
|
947 | 959 | # Run !system commands directly, not through pipes, so terminal programs |
|
948 | 960 | # work correctly. |
@@ -69,9 +69,9 b' def set_qt_api(gui):' | |||
|
69 | 69 | if loaded is not None and gui != "qt": |
|
70 | 70 | if qt_env2gui[loaded] != gui: |
|
71 | 71 | print( |
|
72 |
f"Cannot switch Qt versions for this session; |
|
|
72 | f"Cannot switch Qt versions for this session; will use {qt_env2gui[loaded]}." | |
|
73 | 73 | ) |
|
74 | return | |
|
74 | return qt_env2gui[loaded] | |
|
75 | 75 | |
|
76 | 76 | if qt_api is not None and gui != "qt": |
|
77 | 77 | if qt_env2gui[qt_api] != gui: |
@@ -79,6 +79,7 b' def set_qt_api(gui):' | |||
|
79 | 79 | f'Request for "{gui}" will be ignored because `QT_API` ' |
|
80 | 80 | f'environment variable is set to "{qt_api}"' |
|
81 | 81 | ) |
|
82 | return qt_env2gui[qt_api] | |
|
82 | 83 | else: |
|
83 | 84 | if gui == "qt5": |
|
84 | 85 | try: |
@@ -112,6 +113,11 b' def set_qt_api(gui):' | |||
|
112 | 113 | print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".') |
|
113 | 114 | return |
|
114 | 115 | |
|
116 | # Import it now so we can figure out which version it is. | |
|
117 | from IPython.external.qt_for_kernel import QT_API | |
|
118 | ||
|
119 | return qt_env2gui[QT_API] | |
|
120 | ||
|
115 | 121 | |
|
116 | 122 | def get_inputhook_name_and_func(gui): |
|
117 | 123 | if gui in registered: |
@@ -125,7 +131,7 b' def get_inputhook_name_and_func(gui):' | |||
|
125 | 131 | |
|
126 | 132 | gui_mod = gui |
|
127 | 133 | if gui.startswith("qt"): |
|
128 | set_qt_api(gui) | |
|
134 | gui = set_qt_api(gui) | |
|
129 | 135 | gui_mod = "qt" |
|
130 | 136 | |
|
131 | 137 | mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod) |
@@ -29,6 +29,8 b' from IPython.terminal.shortcuts import auto_suggest' | |||
|
29 | 29 | from IPython.terminal.shortcuts.filters import filter_from_string |
|
30 | 30 | from IPython.utils.decorators import undoc |
|
31 | 31 | |
|
32 | from prompt_toolkit.enums import DEFAULT_BUFFER | |
|
33 | ||
|
32 | 34 | __all__ = ["create_ipython_shortcuts"] |
|
33 | 35 | |
|
34 | 36 |
@@ -33,18 +33,18 b' _get_qt_vers()' | |||
|
33 | 33 | len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed." |
|
34 | 34 | ) |
|
35 | 35 | def test_inputhook_qt(): |
|
36 | gui = guis_avail[0] | |
|
37 | ||
|
38 | # Choose a qt version and get the input hook function. This will import Qt... | |
|
39 | get_inputhook_name_and_func(gui) | |
|
40 | ||
|
41 | # ...and now we're stuck with this version of Qt for good; can't switch. | |
|
42 | for not_gui in ["qt6", "qt5"]: | |
|
43 | if not_gui not in guis_avail: | |
|
44 | break | |
|
45 | ||
|
46 | with pytest.raises(ImportError): | |
|
47 | get_inputhook_name_and_func(not_gui) | |
|
48 | ||
|
49 | # A gui of 'qt' means "best available", or in this case, the last one that was used. | |
|
50 | get_inputhook_name_and_func("qt") | |
|
36 | # Choose the "best" Qt version. | |
|
37 | gui_ret, _ = get_inputhook_name_and_func("qt") | |
|
38 | ||
|
39 | assert gui_ret != "qt" # you get back the specific version that was loaded. | |
|
40 | assert gui_ret in guis_avail | |
|
41 | ||
|
42 | if len(guis_avail) > 2: | |
|
43 | # ...and now we're stuck with this version of Qt for good; can't switch. | |
|
44 | for not_gui in ["qt6", "qt5"]: | |
|
45 | if not_gui != gui_ret: | |
|
46 | break | |
|
47 | # Try to import the other gui; it won't work. | |
|
48 | gui_ret2, _ = get_inputhook_name_and_func(not_gui) | |
|
49 | assert gui_ret2 == gui_ret | |
|
50 | assert gui_ret2 != not_gui |
General Comments 0
You need to be logged in to leave comments.
Login now