From 0fef298ad70ff72a8eaa0e929c0c61ddc79fbadf 2023-03-13 11:02:42 From: Matthias Bussonnier Date: 2023-03-13 11:02:42 Subject: [PATCH] Statically type OInfo. In view of working with #13860, some cleanup inspect to be properly typed, and using stricter datastructure. Instead of dict we now use dataclasses, this will make sure that fields type and access can be stricter and verified not only at runtime, but by mypy --- 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 = []