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