Skip to content

Commit

Permalink
Automatically generate docstring for each existing and newly written …
Browse files Browse the repository at this point in the history
…processes (#67)

* Automatically generates a docstring for each existing and newly written process, if autodoc=True in the process decorator.

* Update autodoc to deal with pytest

* Reorganize how docstring is generated/incorporated

The formatting of the docstring has been outsourced to a separate
function in formatting.add_attribute_section(). This way, the
render_docstrings() only needs to call that function and pass its
return to the __doc__ attribute of the base class (and subclasses).

There are more suggestions in the PR but I had the impression that it's
beneficial to update the recent code.

* Set conditional string assignment, wrap variable description

For readability, variable description in var_details() now assigned
with a separate conditional expression.

To account for indents in the docstring, variable description is now
wrapped after 62 characters.

* explicit if-clause

* Include a test for `add_attribute_section()`

With this commit, a test for the autodoc-feature will be implemented.
For now, however, pytest returns an AssertionError because
`add_attribute_section()` in `test_add_attribute_section()` returns
the docstring twice. Reason unknown.

Also, `@pytest.mark.xfail` was introduced to circumvent failures in
`test_process_decorator()` and `test_add_attribute_section()`. This
will be only temporary.

* Align recent changes of master with autodoc

Fork is now synched with latest commit 1d995b4 of original repo.
whats_new.rst has been updated. Placeholder and if-clause if no
description has been given for individual variables. Docstring for
`process.process.py` has been updated. Lastly, tests included for
`add_attribute_section()` and `process_decorator()` in `test_formatting`
and `test_process`, respectively.

* Added parameter note for test_add_attribute_section()

* Rewrite whats_new.rst and xsimlab.process docstring
  • Loading branch information
Raphael Lange authored and benbovy committed Jan 10, 2020
1 parent df3d609 commit d94c629
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 13 deletions.
4 changes: 4 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Enhancements
literals (f-strings) (:issue:`90`).
- :func:`~xsimlab.foreign` has been updated to get the original description
of a foreign variable (:issue:`91`)
- The ``autodoc`` parameter of the :func:`xsimlab.process` decorator now allows
to automatically add an attributes section to the docstring of the class to
which the decorator is applied, using the metadata of each variable declared
in the class (:issue:`67`).

Bug fixes
~~~~~~~~~
Expand Down
31 changes: 28 additions & 3 deletions xsimlab/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ def _summarize_var(var, process, col_width):
return left_col + right_col


def var_details(var):
max_line_length = 70

def var_details(var, max_line_length=70):
var_metadata = var.metadata.copy()

description = textwrap.fill(
var_metadata.pop("description").capitalize(), width=max_line_length
)
if not description:
description = "(no description given)"

detail_items = [
("type", var_metadata.pop("var_type").value),
Expand All @@ -101,6 +101,31 @@ def var_details(var):
return description + "\n\n" + details + "\n"


def add_attribute_section(process, placeholder="{{attributes}}"):
data_type = "object" # placeholder until issue #34 is solved

fmt_vars = []

for vname, var in variables_dict(process).items():
var_header = f"{vname} : {data_type}"
var_content = textwrap.indent(var_details(var, max_line_length=62), " " * 4)

fmt_vars.append(f"{var_header}\n{var_content}")

fmt_section = textwrap.indent(
"Attributes\n" "----------\n" + "\n".join(fmt_vars), " " * 4
)

current_doc = process.__doc__ or ""

if placeholder in current_doc:
new_doc = current_doc.replace(placeholder, fmt_section[4:])
else:
new_doc = f"{current_doc}\n{fmt_section}\n"

return new_doc


def repr_process(process):
process_cls = type(process)

Expand Down
14 changes: 8 additions & 6 deletions xsimlab/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import attr

from .variable import VarIntent, VarType
from .formatting import repr_process, var_details
from .formatting import add_attribute_section, repr_process, var_details
from .utils import has_method, variables_dict


Expand Down Expand Up @@ -470,8 +470,9 @@ def add_properties(self):
self._p_cls_dict[var_name] = make_prop_func(var)

def render_docstrings(self):
# self._p_cls_dict['__doc__'] = "Process-ified class."
raise NotImplementedError("autodoc is not yet implemented.")
new_doc = add_attribute_section(self._base_cls)

self._base_cls.__doc__ = new_doc

def build_class(self):
p_cls = self._make_process_subclass()
Expand All @@ -483,7 +484,7 @@ def build_class(self):
return p_cls


def process(maybe_cls=None, autodoc=False):
def process(maybe_cls=None, autodoc=True):
"""A class decorator that adds everything needed to use the class
as a process.
Expand All @@ -510,8 +511,9 @@ def process(maybe_cls=None, autodoc=False):
Allows to apply this decorator to a class either as ``@process`` or
``@process(*args)``.
autodoc : bool, optional
If True, render the docstrings template and fill the
corresponding sections with variable metadata (default: False).
(default: True) Automatically adds an attributes section to the
docstring of the class to which the decorator is applied, using the
metadata of each variable declared in the class.
"""

Expand Down
55 changes: 55 additions & 0 deletions xsimlab/tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import xsimlab as xs
from xsimlab.formatting import (
add_attribute_section,
maybe_truncate,
pretty_print,
repr_process,
Expand Down Expand Up @@ -42,6 +43,60 @@ def test_var_details(example_process_obj):
assert "- dims : (('x',),)" in var_details_str


def test_add_attribute_section():
"""For testing, autodoc is set to False to avoid redundancy"""

@xs.process(autodoc=False)
class Dummy:
"""This is a Dummy class
to test `add_attribute_section()`
"""

var1 = xs.variable(dims="x", description="a variable")
var2 = xs.variable()

@xs.process(autodoc=False)
class Dummy_placeholder:
"""This is a Dummy class
to test `add_attribute_section()`
{{attributes}}
"""

var1 = xs.variable(dims="x", description="a variable")
var2 = xs.variable()

expected = """This is a Dummy class
to test `add_attribute_section()`
Attributes
----------
var1 : object
A variable
- type : variable
- intent : in
- dims : (('x',),)
- groups : ()
- static : False
- attrs : {}
var2 : object
(no description given)
- type : variable
- intent : in
- dims : ((),)
- groups : ()
- static : False
- attrs : {}
"""

assert add_attribute_section(Dummy) == expected
assert add_attribute_section(Dummy_placeholder) == expected


def test_process_repr(
example_process_obj,
processes_with_store,
Expand Down
13 changes: 9 additions & 4 deletions xsimlab/tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,16 @@ def run_step(self, a, b):


def test_process_decorator():
with pytest.raises(NotImplementedError):
@xs.process(autodoc=True)
class Dummy_t:
pass

@xs.process(autodoc=True)
class Dummy:
pass
@xs.process(autodoc=False)
class Dummy_f:
pass

assert "Attributes" in Dummy_t.__doc__
assert Dummy_f.__doc__ is None


def test_process_no_model():
Expand Down

0 comments on commit d94c629

Please sign in to comment.