##// END OF EJS Templates
Allow to dispatch getting documentation on objects...
Matthias Bussonnier -
Show More
@@ -1609,10 +1609,19 b' class InteractiveShell(SingletonConfigurable):'
1609 1609
1610 1610 def _ofind(
1611 1611 self, oname: str, namespaces: Optional[Sequence[Tuple[str, AnyType]]] = None
1612 ):
1612 ) -> OInfo:
1613 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 1626 Has special code to detect magic functions.
1618 1627 """
@@ -1771,11 +1780,11 b' class InteractiveShell(SingletonConfigurable):'
1771 1780
1772 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 1784 docformat = (
1776 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 1788 pmethod = getattr(self.inspector, meth)
1780 1789 # TODO: only apply format_screen to the plain/text repr of the mime
1781 1790 # bundle.
@@ -13,18 +13,25 b' reference the name under which an object is being read.'
13 13 __all__ = ['Inspector','InspectColors']
14 14
15 15 # stdlib modules
16 import ast
17 import inspect
16 from dataclasses import dataclass
18 17 from inspect import signature
18 from textwrap import dedent
19 import ast
19 20 import html
21 import inspect
22 import io as stdlib_io
20 23 import linecache
21 import warnings
22 24 import os
23 from textwrap import dedent
25 import sys
24 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 36 # IPython's own
30 37 from IPython.core import page
@@ -46,8 +53,11 b' from pygments import highlight'
46 53 from pygments.lexers import PythonLexer
47 54 from pygments.formatters import HtmlFormatter
48 55
49 from typing import Any, Optional
50 from dataclasses import dataclass
56 HOOK_NAME = "__custom_documentations__"
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 63 @dataclass
@@ -564,34 +574,52 b' class Inspector(Colorable):'
564 574 else:
565 575 return dict(defaults, **formatted)
566 576
567
568 def format_mime(self, bundle):
577 def format_mime(self, bundle: UnformattedBundle) -> Bundle:
569 578 """Format a mimebundle being created by _make_info_unformatted into a real mimebundle"""
570 579 # Format text/plain mimetype
571 if isinstance(bundle["text/plain"], (list, tuple)):
572 # bundle['text/plain'] is a list of (head, formatted body) pairs
573 lines = []
574 _len = max(len(h) for h, _ in bundle["text/plain"])
575
576 for head, body in bundle["text/plain"]:
577 body = body.strip("\n")
578 delim = "\n" if "\n" in body else " "
579 lines.append(
580 f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
581 )
580 assert isinstance(bundle["text/plain"], list)
581 for item in bundle["text/plain"]:
582 assert isinstance(item, tuple)
582 583
583 bundle["text/plain"] = "\n".join(lines)
584 new_b: Bundle = {}
585 lines = []
586 _len = max(len(h) for h, _ in bundle["text/plain"])
584 587
585 # Format the text/html mimetype
586 if isinstance(bundle["text/html"], (list, tuple)):
587 # bundle['text/html'] is a list of (head, formatted body) pairs
588 bundle["text/html"] = "\n".join(
589 (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
588 for head, body in bundle["text/plain"]:
589 body = body.strip("\n")
590 delim = "\n" if "\n" in body else " "
591 lines.append(
592 f"{self.__head(head+':')}{(_len - len(head))*' '}{delim}{body}"
590 593 )
591 return bundle
594
595 new_b["text/plain"] = "\n".join(lines)
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)
601 # Format the text/html mimetype
602 if isinstance(bundle["text/html"], (list, tuple)):
603 # bundle['text/html'] is a list of (head, formatted body) pairs
604 new_b["text/html"] = "\n".join(
605 (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"])
606 )
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 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 624 """Append an info value to the unformatted mimebundle being constructed by _make_info_unformatted"""
597 625 if title in omit_sections or key in omit_sections:
@@ -602,15 +630,19 b' class Inspector(Colorable):'
602 630 bundle["text/plain"].append((title, formatted_field["text/plain"]))
603 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 636 """Assemble the mimebundle as unformatted lists of information"""
607 bundle = {
637 bundle: UnformattedBundle = {
608 638 "text/plain": [],
609 639 "text/html": [],
610 640 }
611 641
612 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 646 self._append_info_field(
615 647 bundle,
616 648 title=title,
@@ -620,7 +652,7 b' class Inspector(Colorable):'
620 652 formatter=formatter,
621 653 )
622 654
623 def code_formatter(text):
655 def code_formatter(text) -> Bundle:
624 656 return {
625 657 'text/plain': self.format(text),
626 658 'text/html': pylight(text)
@@ -678,8 +710,14 b' class Inspector(Colorable):'
678 710
679 711
680 712 def _get_info(
681 self, obj, oname="", formatter=None, info=None, detail_level=0, omit_sections=()
682 ):
713 self,
714 obj: Any,
715 oname: str = "",
716 formatter=None,
717 info: Optional[OInfo] = None,
718 detail_level=0,
719 omit_sections=(),
720 ) -> Bundle:
683 721 """Retrieve an info dict and format it.
684 722
685 723 Parameters
@@ -697,9 +735,13 b' class Inspector(Colorable):'
697 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 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 746 return self.format_mime(bundle)
705 747
@@ -708,7 +750,7 b' class Inspector(Colorable):'
708 750 obj,
709 751 oname="",
710 752 formatter=None,
711 info=None,
753 info: Optional[OInfo] = None,
712 754 detail_level=0,
713 755 enable_html_pager=True,
714 756 omit_sections=(),
@@ -736,12 +778,13 b' class Inspector(Colorable):'
736 778
737 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 783 obj, oname, formatter, info, detail_level, omit_sections=omit_sections
741 784 )
742 785 if not enable_html_pager:
743 del info['text/html']
744 page.page(info)
786 del info_b["text/html"]
787 page.page(info_b)
745 788
746 789 def _info(self, obj, oname="", info=None, detail_level=0):
747 790 """
@@ -758,7 +801,7 b' class Inspector(Colorable):'
758 801 )
759 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 805 """Compute a dict with detailed information about an object.
763 806
764 807 Parameters
@@ -789,7 +832,19 b' class Inspector(Colorable):'
789 832 ospace = info.namespace
790 833
791 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 848 if not callable(obj):
794 849 try:
795 850 ds = "Alias to the system command:\n %s" % obj[1]
@@ -806,8 +861,9 b' class Inspector(Colorable):'
806 861 else:
807 862 ds = ds_or_None
808 863
864 ds = prelude + ds
865
809 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 868 string_max = 200 # max size of strings to show (snipped if longer)
813 869 shalf = int((string_max - 5) / 2)
@@ -980,8 +1036,8 b' class Inspector(Colorable):'
980 1036 source already contains it, avoiding repetition of information.
981 1037 """
982 1038 try:
983 def_node, = ast.parse(dedent(src)).body
984 return ast.get_docstring(def_node) == doc
1039 (def_node,) = ast.parse(dedent(src)).body
1040 return ast.get_docstring(def_node) == doc # type: ignore[arg-type]
985 1041 except Exception:
986 1042 # The source can become invalid or even non-existent (because it
987 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 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 511 def test_pinfo_magic():
475 with AssertPrints('Docstring:'):
476 ip._inspect('pinfo', 'lsmagic', detail_level=0)
512 with AssertPrints("Docstring:"):
513 ip._inspect("pinfo", "lsmagic", detail_level=0)
477 514
478 with AssertPrints('Source:'):
479 ip._inspect('pinfo', 'lsmagic', detail_level=1)
515 with AssertPrints("Source:"):
516 ip._inspect("pinfo", "lsmagic", detail_level=1)
480 517
481 518
482 519 def test_init_colors():
@@ -2,6 +2,81 b''
2 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 80 .. _version 8.11.0:
6 81
7 82 IPython 8.11
@@ -94,7 +169,7 b' Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring'
94 169 work on IPython and related libraries.
95 170
96 171 .. _version 8.10.0:
97
172
98 173 IPython 8.10
99 174 ------------
100 175
@@ -107,7 +182,7 b' This is a really low severity CVE that you most likely are not affected by unles'
107 182 valid shell commands.
108 183
109 184 You can read more on `the advisory
110 <https://github.com/ipython/ipython/security/advisories/GHSA-29gw-9793-fvw7>`__.
185 <https://github.com/ipython/ipython/security/advisories/GHSA-29gw-9793-fvw7>`__.
111 186
112 187 In addition to fixing this CVE we also fix a couple of outstanding bugs and issues.
113 188
@@ -41,6 +41,7 b' install_requires ='
41 41 pygments>=2.4.0
42 42 stack_data
43 43 traitlets>=5
44 typing_extensions ; python_version<'3.10'
44 45
45 46 [options.extras_require]
46 47 black =
General Comments 0
You need to be logged in to leave comments. Login now