Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to dispatch getting documentation on objects. #13975

Merged
merged 1 commit into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 13 additions & 4 deletions IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,10 +1609,19 @@ def _find_parts(oname: str) -> Tuple[bool, ListType[str]]:

def _ofind(
self, oname: str, namespaces: Optional[Sequence[Tuple[str, AnyType]]] = None
):
) -> OInfo:
"""Find an object in the available namespaces.

self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic

Returns
-------
OInfo with fields:
- ismagic
- isalias
- found
- obj
- namespac
- parent

Has special code to detect magic functions.
"""
Expand Down Expand Up @@ -1771,11 +1780,11 @@ def _inspect(self, meth, oname, namespaces=None, **kw):

This function is meant to be called by pdef, pdoc & friends.
"""
info = self._object_find(oname, namespaces)
info: OInfo = self._object_find(oname, namespaces)
docformat = (
sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None
)
if info.found:
if info.found or hasattr(info.parent, oinspect.HOOK_NAME):
pmethod = getattr(self.inspector, meth)
# TODO: only apply format_screen to the plain/text repr of the mime
# bundle.
Expand Down
148 changes: 102 additions & 46 deletions IPython/core/oinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@
__all__ = ['Inspector','InspectColors']

# stdlib modules
import ast
import inspect
from dataclasses import dataclass
from inspect import signature
from textwrap import dedent
import ast
import html
import inspect
import io as stdlib_io
import linecache
import warnings
import os
from textwrap import dedent
import sys
import types
import io as stdlib_io
import warnings

from typing import Union
from typing import Any, Optional, Dict, Union, List, Tuple

if sys.version_info <= (3, 10):
from typing_extensions import TypeAlias
else:
from typing import TypeAlias

# IPython's own
from IPython.core import page
Expand All @@ -46,8 +53,11 @@
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter

from typing import Any, Optional
from dataclasses import dataclass
HOOK_NAME = "__custom_documentations__"


UnformattedBundle: TypeAlias = Dict[str, List[Tuple[str, str]]] # List of (title, body)
Bundle: TypeAlias = Dict[str, str]


@dataclass
Expand Down Expand Up @@ -564,34 +574,52 @@ def _mime_format(self, text:str, formatter=None) -> dict:
else:
return dict(defaults, **formatted)


def format_mime(self, bundle):
def format_mime(self, bundle: UnformattedBundle) -> Bundle:
"""Format a mimebundle being created by _make_info_unformatted into a real mimebundle"""
# Format text/plain mimetype
if isinstance(bundle["text/plain"], (list, tuple)):
# bundle['text/plain'] is a list of (head, formatted body) pairs
lines = []
_len = max(len(h) for h, _ in bundle["text/plain"])

for head, body in bundle["text/plain"]:
body = body.strip("\n")
delim = "\n" if "\n" in body else " "
lines.append(
f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
)
assert isinstance(bundle["text/plain"], list)
for item in bundle["text/plain"]:
assert isinstance(item, tuple)

bundle["text/plain"] = "\n".join(lines)
new_b: Bundle = {}
lines = []
_len = max(len(h) for h, _ in bundle["text/plain"])

# Format the text/html mimetype
if isinstance(bundle["text/html"], (list, tuple)):
# bundle['text/html'] is a list of (head, formatted body) pairs
bundle["text/html"] = "\n".join(
(f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
for head, body in bundle["text/plain"]:
body = body.strip("\n")
delim = "\n" if "\n" in body else " "
lines.append(
f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
)
return bundle

new_b["text/plain"] = "\n".join(lines)

if "text/html" in bundle:
assert isinstance(bundle["text/html"], list)
for item in bundle["text/html"]:
assert isinstance(item, tuple)
# Format the text/html mimetype
if isinstance(bundle["text/html"], (list, tuple)):
# bundle['text/html'] is a list of (head, formatted body) pairs
new_b["text/html"] = "\n".join(
(f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
)

for k in bundle.keys():
if k in ("text/html", "text/plain"):
continue
else:
new_b = bundle[k] # type:ignore
return new_b

def _append_info_field(
self, bundle, title: str, key: str, info, omit_sections, formatter
self,
bundle: UnformattedBundle,
title: str,
key: str,
info,
omit_sections,
formatter,
):
"""Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted"""
if title in omit_sections or key in omit_sections:
Expand All @@ -602,15 +630,19 @@ def _append_info_field(
bundle["text/plain"].append((title, formatted_field["text/plain"]))
bundle["text/html"].append((title, formatted_field["text/html"]))

def _make_info_unformatted(self, obj, info, formatter, detail_level, omit_sections):
def _make_info_unformatted(
self, obj, info, formatter, detail_level, omit_sections
) -> UnformattedBundle:
"""Assemble the mimebundle as unformatted lists of information"""
bundle = {
bundle: UnformattedBundle = {
"text/plain": [],
"text/html": [],
}

# A convenience function to simplify calls below
def append_field(bundle, title: str, key: str, formatter=None):
def append_field(
bundle: UnformattedBundle, title: str, key: str, formatter=None
):
self._append_info_field(
bundle,
title=title,
Expand All @@ -620,7 +652,7 @@ def append_field(bundle, title: str, key: str, formatter=None):
formatter=formatter,
)

def code_formatter(text):
def code_formatter(text) -> Bundle:
return {
'text/plain': self.format(text),
'text/html': pylight(text)
Expand Down Expand Up @@ -678,8 +710,14 @@ def code_formatter(text):


def _get_info(
self, obj, oname="", formatter=None, info=None, detail_level=0, omit_sections=()
):
self,
obj: Any,
oname: str = "",
formatter=None,
info: Optional[OInfo] = None,
detail_level=0,
omit_sections=(),
) -> Bundle:
"""Retrieve an info dict and format it.

Parameters
Expand All @@ -697,9 +735,13 @@ def _get_info(
Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`)
"""

info = self.info(obj, oname=oname, info=info, detail_level=detail_level)
info_dict = self.info(obj, oname=oname, info=info, detail_level=detail_level)
bundle = self._make_info_unformatted(
obj, info, formatter, detail_level=detail_level, omit_sections=omit_sections
obj,
info_dict,
formatter,
detail_level=detail_level,
omit_sections=omit_sections,
)
return self.format_mime(bundle)

Expand All @@ -708,7 +750,7 @@ def pinfo(
obj,
oname="",
formatter=None,
info=None,
info: Optional[OInfo] = None,
detail_level=0,
enable_html_pager=True,
omit_sections=(),
Expand Down Expand Up @@ -736,12 +778,13 @@ def pinfo(

- omit_sections: set of section keys and titles to omit
"""
info = self._get_info(
assert info is not None
info_b: Bundle = self._get_info(
obj, oname, formatter, info, detail_level, omit_sections=omit_sections
)
if not enable_html_pager:
del info['text/html']
page.page(info)
del info_b["text/html"]
page.page(info_b)

def _info(self, obj, oname="", info=None, detail_level=0):
"""
Expand All @@ -758,7 +801,7 @@ def _info(self, obj, oname="", info=None, detail_level=0):
)
return self.info(obj, oname=oname, info=info, detail_level=detail_level)

def info(self, obj, oname="", info=None, detail_level=0) -> dict:
def info(self, obj, oname="", info=None, detail_level=0) -> Dict[str, Any]:
"""Compute a dict with detailed information about an object.

Parameters
Expand Down Expand Up @@ -789,7 +832,19 @@ def info(self, obj, oname="", info=None, detail_level=0) -> dict:
ospace = info.namespace

# Get docstring, special-casing aliases:
if isalias:
att_name = oname.split(".")[-1]
parents_docs = None
prelude = ""
if info and info.parent and hasattr(info.parent, HOOK_NAME):
parents_docs_dict = getattr(info.parent, HOOK_NAME)
parents_docs = parents_docs_dict.get(att_name, None)
out = dict(
name=oname, found=True, isalias=isalias, ismagic=ismagic, subclasses=None
)

if parents_docs:
ds = parents_docs
elif isalias:
if not callable(obj):
try:
ds = "Alias to the system command:\n %s" % obj[1]
Expand All @@ -806,8 +861,9 @@ def info(self, obj, oname="", info=None, detail_level=0) -> dict:
else:
ds = ds_or_None

ds = prelude + ds

# store output in a dict, we initialize it here and fill it as we go
out = dict(name=oname, found=True, isalias=isalias, ismagic=ismagic, subclasses=None)

string_max = 200 # max size of strings to show (snipped if longer)
shalf = int((string_max - 5) / 2)
Expand Down Expand Up @@ -980,8 +1036,8 @@ def _source_contains_docstring(src, doc):
source already contains it, avoiding repetition of information.
"""
try:
def_node, = ast.parse(dedent(src)).body
return ast.get_docstring(def_node) == doc
(def_node,) = ast.parse(dedent(src)).body
return ast.get_docstring(def_node) == doc # type: ignore[arg-type]
except Exception:
# The source can become invalid or even non-existent (because it
# is re-fetched from the source file) so the above code fail in
Expand Down
45 changes: 41 additions & 4 deletions IPython/core/tests/test_oinspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,49 @@ def bar(self):
ip._inspect('pinfo', 'foo.bar', detail_level=1)


def test_pinfo_docstring_dynamic():
obj_def = """class Bar:
__custom_documentations__ = {
"prop" : "cdoc for prop",
"non_exist" : "cdoc for non_exist",
}
@property
def prop(self):
'''
Docstring for prop
'''
return self._prop

@prop.setter
def prop(self, v):
self._prop = v
"""
ip.run_cell(obj_def)

ip.run_cell("b = Bar()")

with AssertPrints("Docstring: cdoc for prop"):
ip.run_line_magic("pinfo", "b.prop")

with AssertPrints("Docstring: cdoc for non_exist"):
ip.run_line_magic("pinfo", "b.non_exist")

with AssertPrints("Docstring: cdoc for prop"):
ip.run_cell("b.prop?")

with AssertPrints("Docstring: cdoc for non_exist"):
ip.run_cell("b.non_exist?")

with AssertPrints("Docstring: <no docstring>"):
ip.run_cell("b.undefined?")


def test_pinfo_magic():
with AssertPrints('Docstring:'):
ip._inspect('pinfo', 'lsmagic', detail_level=0)
with AssertPrints("Docstring:"):
ip._inspect("pinfo", "lsmagic", detail_level=0)

with AssertPrints('Source:'):
ip._inspect('pinfo', 'lsmagic', detail_level=1)
with AssertPrints("Source:"):
ip._inspect("pinfo", "lsmagic", detail_level=1)


def test_init_colors():
Expand Down