From 7003b9dd424874bbf8a03c7bd799bfadba8596fa Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 15 Sep 2019 21:34:22 +0200 Subject: [PATCH] documentation/python, m.sphinx: support documenting raised exceptions. --- doc/documentation/python.rst | 23 +++++- doc/plugins/sphinx.rst | 23 +++--- documentation/python.py | 24 ++++++- .../templates/python/details-function.html | 17 ++++- .../templates/python/details-property.html | 15 ++++ .../test_python/content/content.Class.html | 63 ++++++++++++++++ .../test_python/content/content.html | 31 ++++++++ .../test_python/content/content/__init__.py | 13 ++++ documentation/test_python/content/docs.rst | 14 ++++ .../test_python/inspect_type_links/docs.rst | 8 +++ .../inspect_type_links.Foo.html | 72 +++++++++++++++++++ .../inspect_type_links.html | 25 ++++++- .../inspect_type_links/__init__.py | 4 ++ documentation/test_python/test_inspect.py | 1 + plugins/m/sphinx.py | 25 ++++++- 15 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 documentation/test_python/inspect_type_links/inspect_type_links.Foo.html diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index f539bbdd..50b7630f 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -1257,6 +1257,8 @@ Property Description cross-linked types :py:`function.params` List of function parameters. See below for details. +:py:`function.exceptions` List of exceptions raised by this function. + See below for details. :py:`function.has_complex_params` Set to :py:`True` if the parameter list should be wrapped on several lines for better readability (for example when it @@ -1276,8 +1278,8 @@ Property Description :py:`False` otherwise. =================================== =========================================== -The :py:`func.params` is a list of function parameters and their description. -Each item has the following properties: +The :py:`function.params` is a list of function parameters and their +description. Each item has the following properties: .. class:: m-table m-fullwidth @@ -1298,6 +1300,19 @@ In some cases (for example in case of native APIs), the parameters can't be introspected. In that case, the parameter list is a single entry with ``name`` set to :py:`"..."` and the rest being empty. +The :py:`function.exceptions` is a list of exceptions types and descriptions. +Each item has the following properties: + +.. class:: m-table m-fullwidth + +=========================== =================================================== +Property Description +=========================== =================================================== +:py:`exception.type` Exception type +:py:`exception.type_link` Like :py:`exception`, but with a cross-linked type +:py:`exception.content` Detailed documentation +=========================== =================================================== + `Property properties`_ `````````````````````` @@ -1313,6 +1328,10 @@ Property Description cross-linked types :py:`property.summary` Doc summary :py:`property.content` Detailed documentation, if any +:py:`property.exceptions` List of exceptions raised when accessing + this property. Same as + :py:`function.exceptions` described in + `function properties`_. :py:`property.is_gettable` If the property is gettable :py:`property.is_settable` If the property is settable :py:`property.is_deletable` If the property is deletable with :py:`del` diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst index 00486ec1..2c8d534b 100644 --- a/doc/plugins/sphinx.rst +++ b/doc/plugins/sphinx.rst @@ -421,11 +421,14 @@ conveniently directly in the :rst:`.. py:enum::` directive via ------------ The :rst:`.. py:function::` directive supports additional options --- -:py:`:param :` for documenting parameters and :py:`:return:` for -documenting the return value. It's allowed to have either none or all -parameters documented (the ``self`` parameter can be omitted), having them -documented only partially or documenting parameters that are not present in the -function signature will cause a warning. Example: +:rst:`:param name:` for documenting parameters, :rst:`:raise name:` for +documenting raised exceptions and :rst:`:return:` for documenting the return +value. It's allowed to have either none or all parameters documented (the +``self`` parameter can be omitted), having them documented only partially or +documenting parameters that are not present in the function signature will +cause a warning. Documenting one parameter multiple times causes a warning, on +the other hand listing one exception multiple times is a valid use case. +Example: .. code:: rst @@ -435,6 +438,7 @@ function signature will cause a warning. Example: :param value: Corresponding value :param overwrite_existing: Overwrite existing value if already present in the container + :raise ValueError: If the key type is not hashable :return: The inserted tuple or the existing key/value pair in case :p:`overwrite_existing` is not set @@ -470,10 +474,11 @@ Example: `Properties`_ ------------- -Use :rst:`.. py:property::` for documenting properties. This directive doesn't -support any additional options besides :rst:`:summary:`. For convenience, -properties that have just a summary can be also documented directly in the -enclosing :rst:`.. py:class::` directive `as shown above <#classes>`__. +Use :rst:`.. py:property::` for documenting properties. This directive supports +the :rst:`:raise name:` option similarly as for `functions`_, plus the usual +:rst:`:summary:`. For convenience, properties that have just a summary can be +also documented directly in the enclosing :rst:`.. py:class::` directive +`as shown above <#classes>`__. .. code:: rst diff --git a/documentation/python.py b/documentation/python.py index ad21e8db..ece774ee 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -1524,7 +1524,7 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: overloads = [out] - # Common path for parameter / return value docs and search + # Common path for parameter / exception / return value docs and search path_str = '.'.join(entry.path) for out in overloads: signature = '({})'.format(', '.join(['{}: {}'.format(param.name, param.type) if param.type else param.name for param in out.params])) @@ -1568,6 +1568,16 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: if name not in used_params: logging.warning("%s%s documents parameter %s, which isn't in the signature", path_str, signature, name) + if function_docs.get('raise'): + out.exceptions = [] + for type_, content in function_docs['raise']: + exception = Empty() + exception.type = type_ + exception.type_link = make_name_link(state, entry.path, type_) + exception.content = render_inline_rst(state, content) + out.exceptions += [exception] + out.has_details = True + if function_docs.get('return'): try: out.return_value = render_inline_rst(state, function_docs['return']) @@ -1739,6 +1749,18 @@ def extract_property_doc(state: State, parent, entry: Empty): for hook in state.hooks_post_scope: hook(type=entry.type, path=entry.path) + # Exception docs, if any + exception_docs = state.property_docs.get('.'.join(entry.path), {}).get('raise') + if exception_docs: + out.exceptions = [] + for type_, content in exception_docs: + exception = Empty() + exception.type = type_ + exception.type_link = make_name_link(state, entry.path, type_) + exception.content = render_inline_rst(state, content) + out.exceptions += [exception] + out.has_details = True + if not state.config['SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.from_type(ResultFlag.NONE, EntryType.PROPERTY) diff --git a/documentation/templates/python/details-function.html b/documentation/templates/python/details-function.html index 1bce6420..25440508 100644 --- a/documentation/templates/python/details-function.html +++ b/documentation/templates/python/details-function.html @@ -6,7 +6,7 @@ {% if function.summary %}

{{ function.summary }}

{% endif %} - {% if function.has_param_details or function.return_value %} + {% if function.has_param_details or function.exceptions or function.return_value %} {% if function.has_param_details %} @@ -23,10 +23,23 @@ {% endfor %} {% endif %} + {% if function.exceptions %} + + + + + {% for exception in function.exceptions %} + + {{ exception.type_link }} + + + {% endfor %} + + {% endif %} {% if function.return_value %} - Returns + Returns diff --git a/documentation/templates/python/details-property.html b/documentation/templates/python/details-property.html index 0b72f950..1aa22e5a 100644 --- a/documentation/templates/python/details-property.html +++ b/documentation/templates/python/details-property.html @@ -5,6 +5,21 @@ {% if property.summary %}

{{ property.summary }}

{% endif %} + {% if property.exceptions %} +
Exceptions
{{ exception.content }}
{{ function.return_value }}
+ + + + + {% for exception in property.exceptions %} + + {{ exception.type_link }} + + + {% endfor %} + +
Exceptions
{{ exception.content }}
+ {% endif %} {% if property.content %} {{ property.content }} {% endif %} diff --git a/documentation/test_python/content/content.Class.html b/documentation/test_python/content/content.Class.html index 81d2eff4..a723fbf3 100644 --- a/documentation/test_python/content/content.Class.html +++ b/documentation/test_python/content/content.Class.html @@ -71,6 +71,10 @@ add any detailed block. def method_param_docs(self, a, b)
This method gets its params except self documented
+
+ def method_param_exception_return_docs(self, a, b) +
+
This one documents params and raised exceptions
def method_with_details(self)
@@ -102,6 +106,11 @@ add any detailed block. annotated_property: float get
This is an annotated property
+
+ property_exception_docs get +
+
This one documents raised exceptions in an (otherwise unneeded) +detail view
@@ -151,6 +160,42 @@ add any detailed block.

The self isn't documented and thus also not included in the list.

+
+

+ def content.Class.method_param_exception_return_docs(self, a, b) +

+

This one documents params and raised exceptions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Parameters
aThe first parameter
bThe second parameter
Exceptions
AttributeErrorIf you do bad things to it
ReturnsIf you don't do bad things to it
+

def content.Class.method_with_details(self) @@ -181,6 +226,24 @@ add any detailed block.

This is an annotated property

Annotated property, using summary from the docstring.

+
+

+ content.Class.property_exception_docs get +

+

This one documents raised exceptions in an (otherwise unneeded) +detail view

+ + + + + + + + + + +
Exceptions
AttributeErrorIf you do bad things to it
+

Data documentation

diff --git a/documentation/test_python/content/content.html b/documentation/test_python/content/content.html index b21bebd8..3bc42508 100644 --- a/documentation/test_python/content/content.html +++ b/documentation/test_python/content/content.html @@ -84,6 +84,11 @@ add any detailed block.

Functions

+
+ def exception_docs() +
+
This one documents raised exceptions in an (otherwise unneeded) detail +view
def foo(a, b)
@@ -180,6 +185,32 @@ directive...

Function documentation

+
+

+ def content.exception_docs() +

+

This one documents raised exceptions in an (otherwise unneeded) detail +view

+ + + + + + + + + + + + + + + + + + +
Exceptions
ValueErrorThis thing fires
ValueErrorThis same thing fires also for this reason
RuntimeErrorThis another thing fires too
+

def content.foo_with_details(a, b) diff --git a/documentation/test_python/content/content/__init__.py b/documentation/test_python/content/content/__init__.py index ace7afb6..e555cf0f 100644 --- a/documentation/test_python/content/content/__init__.py +++ b/documentation/test_python/content/content/__init__.py @@ -26,6 +26,9 @@ class Class: def method_param_docs(self, a, b): """This method gets its params except self documented""" + def method_param_exception_return_docs(self, a, b): + """This one documents params and raised exceptions""" + @property def a_property(self): """This summary is not shown either""" @@ -38,6 +41,11 @@ class Class: def annotated_property(self) -> float: """This is an annotated property""" + @property + def property_exception_docs(self): + """This one documents raised exceptions in an (otherwise unneeded) + detail view""" + DATA_WITH_DETAILS: str = 'this blows' class ClassWithSummary: @@ -102,6 +110,11 @@ def full_docstring(a, b) -> str: Like this. """ +def exception_docs(): + """This one documents raised exceptions in an (otherwise unneeded) detail + view + """ + # This should check we handle reST parsing errors in external docs gracefully. # Will probably look extra weird in the output tho, but that's okay -- it's an # error after all. diff --git a/documentation/test_python/content/docs.rst b/documentation/test_python/content/docs.rst index fd02d935..21065c3f 100644 --- a/documentation/test_python/content/docs.rst +++ b/documentation/test_python/content/docs.rst @@ -59,6 +59,12 @@ The ``self`` isn't documented and thus also not included in the list. +.. py:function:: content.Class.method_param_exception_return_docs + :param a: The first parameter + :param b: The second parameter + :raise AttributeError: If you do bad things to it + :return: If you don't do bad things to it + .. py:property:: content.Class.a_property :summary: This overwrites the docstring for :ref:`a_property`, but doesn't add any detailed block. @@ -72,6 +78,9 @@ Annotated property, using summary from the docstring. +.. py:property:: content.Class.property_exception_docs + :raise AttributeError: If you do bad things to it + .. py:data:: content.Class.DATA_WITH_DETAILS Detailed docs for :ref:`DATA_WITH_DETAILS` in a class to check @@ -136,6 +145,11 @@ Doing :p:`this` here is not good either. :param a: First parameter :param b: Second +.. py:function:: content.exception_docs + :raise ValueError: This thing fires + :raise ValueError: This *same* thing fires *also* for this reason + :raise RuntimeError: This another thing fires too + .. py:data:: content.CONSTANT :summary: This is finally a docstring for :ref:`CONSTANT` diff --git a/documentation/test_python/inspect_type_links/docs.rst b/documentation/test_python/inspect_type_links/docs.rst index 4548d45b..015f93e6 100644 --- a/documentation/test_python/inspect_type_links/docs.rst +++ b/documentation/test_python/inspect_type_links/docs.rst @@ -7,6 +7,14 @@ function we need to say :ref:`inspect_type_links.open()`. If it would be the other way around, there would be no simple way to link to builtins. +.. py:function:: inspect_type_links.open + :raise ValueError: If this is not a can, crosslinking to :ref:`ValueError` + of course. + +.. py:property:: inspect_type_links.Foo.prop + :raise SystemError: If you look at it wrong, crosslinking to + :ref:`SystemError` of course. + .. py:module:: inspect_type_links.first :ref:`Foo`, :ref:`first.Foo` and :ref:`inspect_type_links.first.Foo` should diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.Foo.html b/documentation/test_python/inspect_type_links/inspect_type_links.Foo.html new file mode 100644 index 00000000..a1f90474 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.Foo.html @@ -0,0 +1,72 @@ + + + + + inspect_type_links.Foo | My Python Project + + + + + +
+
+
+
+
+

+ inspect_type_links.Foo class +

+

A class in the root module

+
+

Contents

+ +
+
+

Properties

+
+
+ prop get +
+
Here just to test the raise option
+
+
+
+

Property documentation

+
+

+ inspect_type_links.Foo.prop get +

+

Here just to test the raise option

+ + + + + + + + + + +
Exceptions
SystemErrorIf you look at it wrong, crosslinking to +SystemError of course.
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.html b/documentation/test_python/inspect_type_links/inspect_type_links.html index 16df2d11..994ac451 100644 --- a/documentation/test_python/inspect_type_links/inspect_type_links.html +++ b/documentation/test_python/inspect_type_links/inspect_type_links.html @@ -61,12 +61,33 @@ the other way around, there would be no simple way to link to builtins.

Functions

-
- def open() +
+ def open()
A function that opens cans.
+
+

Function documentation

+
+

+ def inspect_type_links.open() +

+

A function that opens cans.

+ + + + + + + + + + +
Exceptions
ValueErrorIf this is not a can, crosslinking to ValueError +of course.
+
+

diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py b/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py index b248126e..590c8b9a 100644 --- a/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py +++ b/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py @@ -3,6 +3,10 @@ from . import first, second class Foo: """A class in the root module""" + @property + def prop(self): + """Here just to test the raise option""" + class Bar: """Another class in the root module""" diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index 4ba365f9..57599dba 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -175,6 +175,7 @@ class TypeLinks(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('index.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.Foo.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.Foo.html')) diff --git a/plugins/m/sphinx.py b/plugins/m/sphinx.py index 3446581e..ba7db45e 100755 --- a/plugins/m/sphinx.py +++ b/plugins/m/sphinx.py @@ -173,6 +173,7 @@ class PyFunction(rst.Directive): required_arguments = 1 option_spec = {'summary': directives.unchanged, 'param': directives_unchanged_list, + 'raise': directives_unchanged_list, 'return': directives.unchanged} def run(self): @@ -182,12 +183,21 @@ class PyFunction(rst.Directive): for name, content in self.options.get('param', []): if name in params: raise KeyError("duplicate param {}".format(name)) params[name] = content + # Check that exceptions are parsed properly. This will blow up if the + # exception name is not specified. Unlike function params not turning + # these into a dict since a single type can be raised for multiple + # reasons. + raises = [] + for name, content in self.options.get('raise', []): + raises += [(name, content)] output = function_doc_output.setdefault(self.arguments[0], {}) if self.options.get('summary'): output['summary'] = self.options['summary'] if params: output['params'] = params + if raises: + output['raise'] = raises if self.options.get('return'): output['return'] = self.options['return'] if self.content: @@ -198,10 +208,21 @@ class PyProperty(rst.Directive): final_argument_whitespace = True has_content = True required_arguments = 1 - option_spec = {'summary': directives.unchanged} + option_spec = {'summary': directives.unchanged, + 'raise': directives_unchanged_list} def run(self): + # Check that exceptions are parsed properly. This will blow up if the + # exception name is not specified. Unlike function params not turning + # these into a dict since a single type can be raised for multiple + # reasons. + raises = [] + for name, content in self.options.get('raise', []): + raises += [(name, content)] + output = property_doc_output.setdefault(self.arguments[0], {}) + if raises: + output['raise'] = raises if self.options.get('summary'): output['summary'] = self.options['summary'] if self.content: @@ -539,6 +560,8 @@ def merge_inventories(name_map, **kwargs): # TODO: this will blow up if the above loop is never entered (which is # unlikely) as EntryType is defined there (EntryType.CLASS, 'py:class'), + # Otherwise we can't link to standard exceptions from :raise: + (EntryType.CLASS, 'py:exception'), # TODO: special type for these? (EntryType.DATA, 'py:data'), # typing.Tuple or typing.Any is data # Those are custom to m.css, not in Sphinx (EntryType.ENUM, 'py:enum'), -- 2.30.2