diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 45ed4e2..4fa266a 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -32,7 +32,7 @@ from io import open as io_open from logging import error from pathlib import Path from typing import Callable -from typing import List as ListType +from typing import List as ListType, Dict as DictType, Any as AnyType from typing import Optional, Tuple from warnings import warn @@ -90,6 +90,8 @@ from IPython.utils.process import getoutput, system from IPython.utils.strdispatch import StrDispatch from IPython.utils.syspathcontext import prepended_to_syspath from IPython.utils.text import DollarFormatter, LSString, SList, format_screen +from IPython.core.oinspect import OInfo + sphinxify: Optional[Callable] @@ -1560,15 +1562,28 @@ class InteractiveShell(SingletonConfigurable): #------------------------------------------------------------------------- # Things related to object introspection #------------------------------------------------------------------------- + @staticmethod + def _find_parts(oname: str) -> ListType[str]: + """ + Given an object name, return a list of parts of this object name. + + Basically split on docs when using attribute access, + and extract the value when using square bracket. + + + For example foo.bar[3].baz[x] -> foo, bar, 3, baz, x + + + Returns + ------- + parts_ok: bool + wether we were properly able to parse parts. + parts: list of str + extracted parts - def _ofind(self, oname, namespaces=None): - """Find an object in the available namespaces. - self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic - Has special code to detect magic functions. """ - oname = oname.strip() raw_parts = oname.split(".") parts = [] parts_ok = True @@ -1590,12 +1605,31 @@ class InteractiveShell(SingletonConfigurable): parts_ok = False parts.append(p) + return parts_ok, parts + + def _ofind(self, oname: str, namespaces: DictType[str, AnyType] = None): + """Find an object in the available namespaces. + + self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic + + Has special code to detect magic functions. + """ + oname = oname.strip() + parts_ok, parts = self._find_parts(oname) + if ( not oname.startswith(ESC_MAGIC) and not oname.startswith(ESC_MAGIC2) and not parts_ok ): - return {"found": False} + return OInfo( + ismagic=False, + isalias=False, + found=False, + obj=None, + namespace="", + parent=None, + ) if namespaces is None: # Namespaces to search in: @@ -1675,14 +1709,16 @@ class InteractiveShell(SingletonConfigurable): found = True ospace = 'Interactive' - return { - 'obj':obj, - 'found':found, - 'parent':parent, - 'ismagic':ismagic, - 'isalias':isalias, - 'namespace':ospace - } + return OInfo( + **{ + "obj": obj, + "found": found, + "parent": parent, + "ismagic": ismagic, + "isalias": isalias, + "namespace": ospace, + } + ) @staticmethod def _getattr_property(obj, attrname): @@ -1726,9 +1762,9 @@ class InteractiveShell(SingletonConfigurable): # Nothing helped, fall back. return getattr(obj, attrname) - def _object_find(self, oname, namespaces=None): + def _object_find(self, oname, namespaces=None) -> OInfo: """Find an object and return a struct with info about it.""" - return Struct(self._ofind(oname, namespaces)) + return self._ofind(oname, namespaces) def _inspect(self, meth, oname, namespaces=None, **kw): """Generic interface to the inspector system. diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index bcaa95c..399a52b 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -46,6 +46,19 @@ from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter +from typing import Any +from dataclasses import dataclass + + +@dataclass +class OInfo: + ismagic: bool + isalias: bool + found: bool + namespace: str + parent: Any + obj: Any + def pylight(code): return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True)) diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py index 0038e5c..e7e82e3 100644 --- a/IPython/core/prefilter.py +++ b/IPython/core/prefilter.py @@ -499,7 +499,7 @@ class AutocallChecker(PrefilterChecker): return None oinfo = line_info.ofind(self.shell) # This can mutate state via getattr - if not oinfo['found']: + if not oinfo.found: return None ignored_funs = ['b', 'f', 'r', 'u', 'br', 'rb', 'fr', 'rf'] @@ -508,10 +508,12 @@ class AutocallChecker(PrefilterChecker): if ifun.lower() in ignored_funs and (line.startswith(ifun + "'") or line.startswith(ifun + '"')): return None - if callable(oinfo['obj']) \ - and (not self.exclude_regexp.match(line_info.the_rest)) \ - and self.function_name_regexp.match(line_info.ifun): - return self.prefilter_manager.get_handler_by_name('auto') + if ( + callable(oinfo.obj) + and (not self.exclude_regexp.match(line_info.the_rest)) + and self.function_name_regexp.match(line_info.ifun) + ): + return self.prefilter_manager.get_handler_by_name("auto") else: return None @@ -601,7 +603,7 @@ class AutoHandler(PrefilterHandler): the_rest = line_info.the_rest esc = line_info.esc continue_prompt = line_info.continue_prompt - obj = line_info.ofind(self.shell)['obj'] + obj = line_info.ofind(self.shell).obj # This should only be active for single-line input! if continue_prompt: diff --git a/IPython/core/splitinput.py b/IPython/core/splitinput.py index 63cdce7..5bc3e32 100644 --- a/IPython/core/splitinput.py +++ b/IPython/core/splitinput.py @@ -25,6 +25,7 @@ import sys from IPython.utils import py3compat from IPython.utils.encoding import get_stream_enc +from IPython.core.oinspect import OInfo #----------------------------------------------------------------------------- # Main function @@ -118,7 +119,7 @@ class LineInfo(object): else: self.pre_whitespace = self.pre - def ofind(self, ip): + def ofind(self, ip) -> OInfo: """Do a full, attribute-walking lookup of the ifun in the various namespaces for the given IPython InteractiveShell instance. diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index 0e8bf19..b832806 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -177,7 +177,7 @@ def test_module_without_init(): try: os.makedirs(os.path.join(tmpdir, fake_module_name)) s = try_import(mod=fake_module_name) - assert s == [] + assert s == [], f"for module {fake_module_name}" finally: sys.path.remove(tmpdir) diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 920d911..0ac9f60 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -25,6 +25,7 @@ from os.path import join from IPython.core.error import InputRejected from IPython.core.inputtransformer import InputTransformer from IPython.core import interactiveshell +from IPython.core.oinspect import OInfo from IPython.testing.decorators import ( skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist, ) @@ -360,7 +361,7 @@ class InteractiveShellTestCase(unittest.TestCase): # Get info on line magic lfind = ip._ofind("lmagic") - info = dict( + info = OInfo( found=True, isalias=False, ismagic=True, @@ -379,7 +380,7 @@ class InteractiveShellTestCase(unittest.TestCase): # Get info on cell magic find = ip._ofind("cmagic") - info = dict( + info = OInfo( found=True, isalias=False, ismagic=True, @@ -397,9 +398,15 @@ class InteractiveShellTestCase(unittest.TestCase): a = A() - found = ip._ofind('a.foo', [('locals', locals())]) - info = dict(found=True, isalias=False, ismagic=False, - namespace='locals', obj=A.foo, parent=a) + found = ip._ofind("a.foo", [("locals", locals())]) + info = OInfo( + found=True, + isalias=False, + ismagic=False, + namespace="locals", + obj=A.foo, + parent=a, + ) self.assertEqual(found, info) def test_ofind_multiple_attribute_lookups(self): @@ -412,9 +419,15 @@ class InteractiveShellTestCase(unittest.TestCase): a.a = A() a.a.a = A() - found = ip._ofind('a.a.a.foo', [('locals', locals())]) - info = dict(found=True, isalias=False, ismagic=False, - namespace='locals', obj=A.foo, parent=a.a.a) + found = ip._ofind("a.a.a.foo", [("locals", locals())]) + info = OInfo( + found=True, + isalias=False, + ismagic=False, + namespace="locals", + obj=A.foo, + parent=a.a.a, + ) self.assertEqual(found, info) def test_ofind_slotted_attributes(self): @@ -424,14 +437,26 @@ class InteractiveShellTestCase(unittest.TestCase): self.foo = 'bar' a = A() - found = ip._ofind('a.foo', [('locals', locals())]) - info = dict(found=True, isalias=False, ismagic=False, - namespace='locals', obj=a.foo, parent=a) + found = ip._ofind("a.foo", [("locals", locals())]) + info = OInfo( + found=True, + isalias=False, + ismagic=False, + namespace="locals", + obj=a.foo, + parent=a, + ) self.assertEqual(found, info) - found = ip._ofind('a.bar', [('locals', locals())]) - info = dict(found=False, isalias=False, ismagic=False, - namespace=None, obj=None, parent=a) + found = ip._ofind("a.bar", [("locals", locals())]) + info = OInfo( + found=False, + isalias=False, + ismagic=False, + namespace=None, + obj=None, + parent=a, + ) self.assertEqual(found, info) def test_ofind_prefers_property_to_instance_level_attribute(self): @@ -443,7 +468,7 @@ class InteractiveShellTestCase(unittest.TestCase): a.__dict__["foo"] = "baz" self.assertEqual(a.foo, "bar") found = ip._ofind("a.foo", [("locals", locals())]) - self.assertIs(found["obj"], A.foo) + self.assertIs(found.obj, A.foo) def test_custom_syntaxerror_exception(self): called = []