Show More
@@ -1609,10 +1609,19 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 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 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 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,34 +574,52 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 |
|
|
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"]: | |
573 | lines = [] |
|
582 | assert isinstance(item, tuple) | |
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 | ) |
|
|||
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 |
|
588 | for head, body in bundle["text/plain"]: | |
586 | if isinstance(bundle["text/html"], (list, tuple)): |
|
589 | body = body.strip("\n") | |
587 | # bundle['text/html'] is a list of (head, formatted body) pairs |
|
590 | delim = "\n" if "\n" in body else " " | |
588 | bundle["text/html"] = "\n".join( |
|
591 | lines.append( | |
589 | (f"<h1>{head}</h1>\n{body}" for (head, body) in bundle["text/html"]) |
|
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 | 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 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 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 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 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 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 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[ |
|
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 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) -> |
|
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 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 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 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 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( |
|
512 | with AssertPrints("Docstring:"): | |
476 |
ip._inspect( |
|
513 | ip._inspect("pinfo", "lsmagic", detail_level=0) | |
477 |
|
514 | |||
478 |
with AssertPrints( |
|
515 | with AssertPrints("Source:"): | |
479 |
ip._inspect( |
|
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 | |||||
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 | |
@@ -94,7 +169,7 Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring | |||||
94 | work on IPython and related libraries. |
|
169 | work on IPython and related libraries. | |
95 |
|
170 | |||
96 | .. _version 8.10.0: |
|
171 | .. _version 8.10.0: | |
97 |
|
172 | |||
98 | IPython 8.10 |
|
173 | IPython 8.10 | |
99 | ------------ |
|
174 | ------------ | |
100 |
|
175 | |||
@@ -107,7 +182,7 This is a really low severity CVE that you most likely are not affected by unles | |||||
107 | valid shell commands. |
|
182 | valid shell commands. | |
108 |
|
183 | |||
109 | You can read more on `the advisory |
|
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 | In addition to fixing this CVE we also fix a couple of outstanding bugs and issues. |
|
187 | In addition to fixing this CVE we also fix a couple of outstanding bugs and issues. | |
113 |
|
188 |
General Comments 0
You need to be logged in to leave comments.
Login now