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,12 +574,14 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 |
|
|
|
572 | # bundle['text/plain'] is a list of (head, formatted body) pairs | |
|
580 | assert isinstance(bundle["text/plain"], list) | |
|
581 | for item in bundle["text/plain"]: | |
|
582 | assert isinstance(item, tuple) | |
|
583 | ||
|
584 | new_b: Bundle = {} | |
|
573 | 585 |
|
|
574 | 586 |
|
|
575 | 587 | |
@@ -580,18 +592,34 b' class Inspector(Colorable):' | |||
|
580 | 592 |
|
|
581 | 593 |
|
|
582 | 594 | |
|
583 |
|
|
|
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 | 601 | # Format the text/html mimetype |
|
586 | 602 | if isinstance(bundle["text/html"], (list, tuple)): |
|
587 | 603 | # bundle['text/html'] is a list of (head, formatted body) pairs |
|
588 |
|
|
|
604 | new_b["text/html"] = "\n".join( | |
|
589 | 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 | 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[ |
|
|
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) -> |
|
|
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( |
|
|
476 |
ip._inspect( |
|
|
512 | with AssertPrints("Docstring:"): | |
|
513 | ip._inspect("pinfo", "lsmagic", detail_level=0) | |
|
477 | 514 | |
|
478 |
with AssertPrints( |
|
|
479 |
ip._inspect( |
|
|
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 |
General Comments 0
You need to be logged in to leave comments.
Login now