##// END OF EJS Templates
Allow to dispatch getting documentation on objects. (#13975)...
Matthias Bussonnier -
r28201:d52bf622 merge
parent child Browse files
Show More
@@ -1609,10 +1609,19 b' class InteractiveShell(SingletonConfigurable):'
1609
1609
1610 def _ofind(
1610 def _ofind(
1611 self, oname: str, namespaces: Optional[Sequence[Tuple[str, AnyType]]] = None
1611 self, oname: str, namespaces: Optional[Sequence[Tuple[str, AnyType]]] = None
1612 ):
1612 ) -> OInfo:
1613 """Find an object in the available namespaces.
1613 """Find an object in the available namespaces.
1614
1614
1615 self._ofind(oname) -> dict with keys: found,obj,ospace,ismagic
1615
1616 Returns
1617 -------
1618 OInfo with fields:
1619 - ismagic
1620 - isalias
1621 - found
1622 - obj
1623 - namespac
1624 - parent
1616
1625
1617 Has special code to detect magic functions.
1626 Has special code to detect magic functions.
1618 """
1627 """
@@ -1771,11 +1780,11 b' class InteractiveShell(SingletonConfigurable):'
1771
1780
1772 This function is meant to be called by pdef, pdoc & friends.
1781 This function is meant to be called by pdef, pdoc & friends.
1773 """
1782 """
1774 info = self._object_find(oname, namespaces)
1783 info: OInfo = self._object_find(oname, namespaces)
1775 docformat = (
1784 docformat = (
1776 sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None
1785 sphinxify(self.object_inspect(oname)) if self.sphinxify_docstring else None
1777 )
1786 )
1778 if info.found:
1787 if info.found or hasattr(info.parent, oinspect.HOOK_NAME):
1779 pmethod = getattr(self.inspector, meth)
1788 pmethod = getattr(self.inspector, meth)
1780 # TODO: only apply format_screen to the plain/text repr of the mime
1789 # TODO: only apply format_screen to the plain/text repr of the mime
1781 # bundle.
1790 # bundle.
@@ -13,18 +13,25 b' reference the name under which an object is being read.'
13 __all__ = ['Inspector','InspectColors']
13 __all__ = ['Inspector','InspectColors']
14
14
15 # stdlib modules
15 # stdlib modules
16 import ast
16 from dataclasses import dataclass
17 import inspect
18 from inspect import signature
17 from inspect import signature
18 from textwrap import dedent
19 import ast
19 import html
20 import html
21 import inspect
22 import io as stdlib_io
20 import linecache
23 import linecache
21 import warnings
22 import os
24 import os
23 from textwrap import dedent
25 import sys
24 import types
26 import types
25 import io as stdlib_io
27 import warnings
26
28
27 from typing import Union
29 from typing import Any, Optional, Dict, Union, List, Tuple
30
31 if sys.version_info <= (3, 10):
32 from typing_extensions import TypeAlias
33 else:
34 from typing import TypeAlias
28
35
29 # IPython's own
36 # IPython's own
30 from IPython.core import page
37 from IPython.core import page
@@ -46,8 +53,11 b' from pygments import highlight'
46 from pygments.lexers import PythonLexer
53 from pygments.lexers import PythonLexer
47 from pygments.formatters import HtmlFormatter
54 from pygments.formatters import HtmlFormatter
48
55
49 from typing import Any, Optional
56 HOOK_NAME = "__custom_documentations__"
50 from dataclasses import dataclass
57
58
59 UnformattedBundle: TypeAlias = Dict[str, List[Tuple[str, str]]] # List of (title, body)
60 Bundle: TypeAlias = Dict[str, str]
51
61
52
62
53 @dataclass
63 @dataclass
@@ -564,12 +574,14 b' class Inspector(Colorable):'
564 else:
574 else:
565 return dict(defaults, **formatted)
575 return dict(defaults, **formatted)
566
576
567
577 def format_mime(self, bundle: UnformattedBundle) -> Bundle:
568 def format_mime(self, bundle):
569 """Format a mimebundle being created by _make_info_unformatted into a real mimebundle"""
578 """Format a mimebundle being created by _make_info_unformatted into a real mimebundle"""
570 # Format text/plain mimetype
579 # Format text/plain mimetype
571 if isinstance(bundle["text/plain"], (list, tuple)):
580 assert isinstance(bundle["text/plain"], list)
572 # bundle['text/plain'] is a list of (head, formatted body) pairs
581 for item in bundle["text/plain"]:
582 assert isinstance(item, tuple)
583
584 new_b: Bundle = {}
573 lines = []
585 lines = []
574 _len = max(len(h) for h, _ in bundle["text/plain"])
586 _len = max(len(h) for h, _ in bundle["text/plain"])
575
587
@@ -580,18 +592,34 b' class Inspector(Colorable):'
580 f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
592 f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
581 )
593 )
582
594
583 bundle["text/plain"] = "\n".join(lines)
595 new_b["text/plain"] = "\n".join(lines)
584
596
597 if "text/html" in bundle:
598 assert isinstance(bundle["text/html"], list)
599 for item in bundle["text/html"]:
600 assert isinstance(item, tuple)
585 # Format the text/html mimetype
601 # Format the text/html mimetype
586 if isinstance(bundle["text/html"], (list, tuple)):
602 if isinstance(bundle["text/html"], (list, tuple)):
587 # bundle['text/html'] is a list of (head, formatted body) pairs
603 # bundle['text/html'] is a list of (head, formatted body) pairs
588 bundle["text/html"] = "\n".join(
604 new_b["text/html"] = "\n".join(
589 (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
605 (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
590 )
606 )
591 return bundle
607
608 for k in bundle.keys():
609 if k in ("text/html", "text/plain"):
610 continue
611 else:
612 new_b = bundle[k] # type:ignore
613 return new_b
592
614
593 def _append_info_field(
615 def _append_info_field(
594 self, bundle, title: str, key: str, info, omit_sections, formatter
616 self,
617 bundle: UnformattedBundle,
618 title: str,
619 key: str,
620 info,
621 omit_sections,
622 formatter,
595 ):
623 ):
596 """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted"""
624 """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted"""
597 if title in omit_sections or key in omit_sections:
625 if title in omit_sections or key in omit_sections:
@@ -602,15 +630,19 b' class Inspector(Colorable):'
602 bundle["text/plain"].append((title, formatted_field["text/plain"]))
630 bundle["text/plain"].append((title, formatted_field["text/plain"]))
603 bundle["text/html"].append((title, formatted_field["text/html"]))
631 bundle["text/html"].append((title, formatted_field["text/html"]))
604
632
605 def _make_info_unformatted(self, obj, info, formatter, detail_level, omit_sections):
633 def _make_info_unformatted(
634 self, obj, info, formatter, detail_level, omit_sections
635 ) -> UnformattedBundle:
606 """Assemble the mimebundle as unformatted lists of information"""
636 """Assemble the mimebundle as unformatted lists of information"""
607 bundle = {
637 bundle: UnformattedBundle = {
608 "text/plain": [],
638 "text/plain": [],
609 "text/html": [],
639 "text/html": [],
610 }
640 }
611
641
612 # A convenience function to simplify calls below
642 # A convenience function to simplify calls below
613 def append_field(bundle, title: str, key: str, formatter=None):
643 def append_field(
644 bundle: UnformattedBundle, title: str, key: str, formatter=None
645 ):
614 self._append_info_field(
646 self._append_info_field(
615 bundle,
647 bundle,
616 title=title,
648 title=title,
@@ -620,7 +652,7 b' class Inspector(Colorable):'
620 formatter=formatter,
652 formatter=formatter,
621 )
653 )
622
654
623 def code_formatter(text):
655 def code_formatter(text) -> Bundle:
624 return {
656 return {
625 'text/plain': self.format(text),
657 'text/plain': self.format(text),
626 'text/html': pylight(text)
658 'text/html': pylight(text)
@@ -678,8 +710,14 b' class Inspector(Colorable):'
678
710
679
711
680 def _get_info(
712 def _get_info(
681 self, obj, oname="", formatter=None, info=None, detail_level=0, omit_sections=()
713 self,
682 ):
714 obj: Any,
715 oname: str = "",
716 formatter=None,
717 info: Optional[OInfo] = None,
718 detail_level=0,
719 omit_sections=(),
720 ) -> Bundle:
683 """Retrieve an info dict and format it.
721 """Retrieve an info dict and format it.
684
722
685 Parameters
723 Parameters
@@ -697,9 +735,13 b' class Inspector(Colorable):'
697 Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`)
735 Titles or keys to omit from output (can be set, tuple, etc., anything supporting `in`)
698 """
736 """
699
737
700 info = self.info(obj, oname=oname, info=info, detail_level=detail_level)
738 info_dict = self.info(obj, oname=oname, info=info, detail_level=detail_level)
701 bundle = self._make_info_unformatted(
739 bundle = self._make_info_unformatted(
702 obj, info, formatter, detail_level=detail_level, omit_sections=omit_sections
740 obj,
741 info_dict,
742 formatter,
743 detail_level=detail_level,
744 omit_sections=omit_sections,
703 )
745 )
704 return self.format_mime(bundle)
746 return self.format_mime(bundle)
705
747
@@ -708,7 +750,7 b' class Inspector(Colorable):'
708 obj,
750 obj,
709 oname="",
751 oname="",
710 formatter=None,
752 formatter=None,
711 info=None,
753 info: Optional[OInfo] = None,
712 detail_level=0,
754 detail_level=0,
713 enable_html_pager=True,
755 enable_html_pager=True,
714 omit_sections=(),
756 omit_sections=(),
@@ -736,12 +778,13 b' class Inspector(Colorable):'
736
778
737 - omit_sections: set of section keys and titles to omit
779 - omit_sections: set of section keys and titles to omit
738 """
780 """
739 info = self._get_info(
781 assert info is not None
782 info_b: Bundle = self._get_info(
740 obj, oname, formatter, info, detail_level, omit_sections=omit_sections
783 obj, oname, formatter, info, detail_level, omit_sections=omit_sections
741 )
784 )
742 if not enable_html_pager:
785 if not enable_html_pager:
743 del info['text/html']
786 del info_b["text/html"]
744 page.page(info)
787 page.page(info_b)
745
788
746 def _info(self, obj, oname="", info=None, detail_level=0):
789 def _info(self, obj, oname="", info=None, detail_level=0):
747 """
790 """
@@ -758,7 +801,7 b' class Inspector(Colorable):'
758 )
801 )
759 return self.info(obj, oname=oname, info=info, detail_level=detail_level)
802 return self.info(obj, oname=oname, info=info, detail_level=detail_level)
760
803
761 def info(self, obj, oname="", info=None, detail_level=0) -> dict:
804 def info(self, obj, oname="", info=None, detail_level=0) -> Dict[str, Any]:
762 """Compute a dict with detailed information about an object.
805 """Compute a dict with detailed information about an object.
763
806
764 Parameters
807 Parameters
@@ -789,7 +832,19 b' class Inspector(Colorable):'
789 ospace = info.namespace
832 ospace = info.namespace
790
833
791 # Get docstring, special-casing aliases:
834 # Get docstring, special-casing aliases:
792 if isalias:
835 att_name = oname.split(".")[-1]
836 parents_docs = None
837 prelude = ""
838 if info and info.parent and hasattr(info.parent, HOOK_NAME):
839 parents_docs_dict = getattr(info.parent, HOOK_NAME)
840 parents_docs = parents_docs_dict.get(att_name, None)
841 out = dict(
842 name=oname, found=True, isalias=isalias, ismagic=ismagic, subclasses=None
843 )
844
845 if parents_docs:
846 ds = parents_docs
847 elif isalias:
793 if not callable(obj):
848 if not callable(obj):
794 try:
849 try:
795 ds = "Alias to the system command:\n %s" % obj[1]
850 ds = "Alias to the system command:\n %s" % obj[1]
@@ -806,8 +861,9 b' class Inspector(Colorable):'
806 else:
861 else:
807 ds = ds_or_None
862 ds = ds_or_None
808
863
864 ds = prelude + ds
865
809 # store output in a dict, we initialize it here and fill it as we go
866 # store output in a dict, we initialize it here and fill it as we go
810 out = dict(name=oname, found=True, isalias=isalias, ismagic=ismagic, subclasses=None)
811
867
812 string_max = 200 # max size of strings to show (snipped if longer)
868 string_max = 200 # max size of strings to show (snipped if longer)
813 shalf = int((string_max - 5) / 2)
869 shalf = int((string_max - 5) / 2)
@@ -980,8 +1036,8 b' class Inspector(Colorable):'
980 source already contains it, avoiding repetition of information.
1036 source already contains it, avoiding repetition of information.
981 """
1037 """
982 try:
1038 try:
983 def_node, = ast.parse(dedent(src)).body
1039 (def_node,) = ast.parse(dedent(src)).body
984 return ast.get_docstring(def_node) == doc
1040 return ast.get_docstring(def_node) == doc # type: ignore[arg-type]
985 except Exception:
1041 except Exception:
986 # The source can become invalid or even non-existent (because it
1042 # The source can become invalid or even non-existent (because it
987 # is re-fetched from the source file) so the above code fail in
1043 # is re-fetched from the source file) so the above code fail in
@@ -471,12 +471,49 b' def test_pinfo_docstring_if_detail_and_no_source():'
471 ip._inspect('pinfo', 'foo.bar', detail_level=1)
471 ip._inspect('pinfo', 'foo.bar', detail_level=1)
472
472
473
473
474 def test_pinfo_docstring_dynamic():
475 obj_def = """class Bar:
476 __custom_documentations__ = {
477 "prop" : "cdoc for prop",
478 "non_exist" : "cdoc for non_exist",
479 }
480 @property
481 def prop(self):
482 '''
483 Docstring for prop
484 '''
485 return self._prop
486
487 @prop.setter
488 def prop(self, v):
489 self._prop = v
490 """
491 ip.run_cell(obj_def)
492
493 ip.run_cell("b = Bar()")
494
495 with AssertPrints("Docstring: cdoc for prop"):
496 ip.run_line_magic("pinfo", "b.prop")
497
498 with AssertPrints("Docstring: cdoc for non_exist"):
499 ip.run_line_magic("pinfo", "b.non_exist")
500
501 with AssertPrints("Docstring: cdoc for prop"):
502 ip.run_cell("b.prop?")
503
504 with AssertPrints("Docstring: cdoc for non_exist"):
505 ip.run_cell("b.non_exist?")
506
507 with AssertPrints("Docstring: <no docstring>"):
508 ip.run_cell("b.undefined?")
509
510
474 def test_pinfo_magic():
511 def test_pinfo_magic():
475 with AssertPrints('Docstring:'):
512 with AssertPrints("Docstring:"):
476 ip._inspect('pinfo', 'lsmagic', detail_level=0)
513 ip._inspect("pinfo", "lsmagic", detail_level=0)
477
514
478 with AssertPrints('Source:'):
515 with AssertPrints("Source:"):
479 ip._inspect('pinfo', 'lsmagic', detail_level=1)
516 ip._inspect("pinfo", "lsmagic", detail_level=1)
480
517
481
518
482 def test_init_colors():
519 def test_init_colors():
@@ -2,6 +2,81 b''
2 8.x Series
2 8.x Series
3 ============
3 ============
4
4
5 .. _version 8.12.0:
6
7
8 Dynamic documentation dispatch
9 ------------------------------
10
11
12 We are experimenting with dynamic documentation dispatch for object attribute.
13 See :ghissue:`13860`. The goal is to allow object to define documentation for
14 their attributes, properties, even when those are dynamically defined with
15 `__getattr__`.
16
17 In particular when those objects are base types it can be useful to show the
18 documentation
19
20
21 .. code::
22
23 In [1]: class User:
24 ...:
25 ...: __custom_documentations__ = {
26 ...: "first": "The first name of the user.",
27 ...: "last": "The last name of the user.",
28 ...: }
29 ...:
30 ...: first:str
31 ...: last:str
32 ...:
33 ...: def __init__(self, first, last):
34 ...: self.first = first
35 ...: self.last = last
36 ...:
37 ...: @property
38 ...: def full(self):
39 ...: """`self.first` and `self.last` joined by a space."""
40 ...: return self.first + " " + self.last
41 ...:
42 ...:
43 ...: user = Person('Jane', 'Doe')
44
45 In [2]: user.first?
46 Type: str
47 String form: Jane
48 Length: 4
49 Docstring: the first name of a the person object, a str
50 Class docstring:
51 ....
52
53 In [3]: user.last?
54 Type: str
55 String form: Doe
56 Length: 3
57 Docstring: the last name, also a str
58 ...
59
60
61 We can see here the symmetry with IPython looking for the docstring on the
62 properties::
63
64
65 In [4]: user.full?
66 HERE
67 Type: property
68 String form: <property object at 0x102bb15d0>
69 Docstring: first and last join by a space
70
71
72 Note that while in the above example we use a static dictionary, libraries may
73 decide to use a custom object that define ``__getitem__``, we caution against
74 using objects that would trigger computation to show documentation, but it is
75 sometime preferable for highly dynamic code that for example export ans API as
76 object.
77
78
79
5 .. _version 8.11.0:
80 .. _version 8.11.0:
6
81
7 IPython 8.11
82 IPython 8.11
@@ -41,6 +41,7 b' install_requires ='
41 pygments>=2.4.0
41 pygments>=2.4.0
42 stack_data
42 stack_data
43 traitlets>=5
43 traitlets>=5
44 typing_extensions ; python_version<'3.10'
44
45
45 [options.extras_require]
46 [options.extras_require]
46 black =
47 black =
General Comments 0
You need to be logged in to leave comments. Login now