Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving documentation of plot handlers #839

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/_templates/autosummary/class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
.. autosummary::
{% for item in attributes %}
{% if not item.startswith('_') %}
{% if '.' in item %}
{{ name }}.{{ item }}
{% else %}
~{{ name }}.{{ item }}
{% endif %}
{% endif %}
{%- endfor %}
{% endif %}
Expand All @@ -26,7 +30,11 @@
'step_either', 'step_to',
'isDataset', 'isDimension', 'isGroup',
'isRoot', 'isVariable']) %}
~{{ name }}.{{ item }}
{% if '.' in item %}
{{ name }}.{{ item }}
{% else %}
~{{ name }}.{{ item }}
{% endif %}
{% endif %}
{%- endfor %}
{% endif %}
Expand Down
1 change: 1 addition & 0 deletions docs/api/viz/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Plot classes are workflow classes that implement some specific plotting.
GridPlot
WavefunctionPlot
PdosPlot
AtomicMatrixPlot

Utilities
---------
Expand Down
96 changes: 96 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ class RevYearPlain(PlainStyle):

nbsphinx_thumbnails = {}

nbsphinx_allow_errors = True

import inspect


Expand Down Expand Up @@ -573,6 +575,100 @@ def sisl_skip(app, what, name, obj, skip, options):
return skip


from functools import wraps

import sisl.viz
from sisl.viz._plotables import ALL_PLOT_HANDLERS


def document_nested_attribute(obj, owner_cls, attribute_path: str):
"""Sets a nested attribute to a class with a placeholder name.

It substitutes dots in the attribute name with a placeholder. This substitution
will be reversed once the documentation is built.

This is needed because autodoc refuses to document attributes with dots in their name
(although python allows for that possibility).
"""

setattr(owner_cls, attribute_path, obj)
setattr(
getattr(owner_cls, attribute_path.split(".")[0]),
".".join(attribute_path.split(".")[1:]),
obj,
)


def document_nested_method(
method, owner_cls, method_path: str, add_signature_self: bool = False
):
"""Takes a nested method, wraps it to make sure is of function type and creates a nested attribute in the owner class."""

@wraps(method)
def method_wrapper(*args, **kwargs):
return method(*args, **kwargs)

if add_signature_self:
wrapper_sig = inspect.signature(method_wrapper)
method_wrapper.__signature__ = wrapper_sig.replace(
parameters=[
inspect.Parameter("self", inspect.Parameter.POSITIONAL_ONLY),
*wrapper_sig.parameters.values(),
]
)

document_nested_attribute(method_wrapper, owner_cls, method_path)

setattr(
getattr(owner_cls, method_path.split(".")[0]),
method_path.split(".")[1],
method_wrapper,
)


def document_class_dispatcher_methods(
dispatcher,
owner_cls,
dispatcher_path: str,
add_signature_self: bool = False,
as_attributes: bool = False,
):
"""Document all methods in a dispatcher class as nested methods in the owner class."""
for key, method in dispatcher._dispatchs.items():
if not isinstance(key, str):
continue
if as_attributes:
document_nested_attribute(method, owner_cls, f"{dispatcher_path}.{key}")
else:
document_nested_method(
method,
owner_cls,
f"{dispatcher_path}.{key}",
add_signature_self=add_signature_self,
)


# Document all plotting possibilities of each plot handler
for plot_handler in ALL_PLOT_HANDLERS:
document_class_dispatcher_methods(
plot_handler, plot_handler._cls, "plot", add_signature_self=True
)

# Document the methods of the Geometry.to dispatcher
document_class_dispatcher_methods(
sisl.Geometry.to, sisl.Geometry, "to", add_signature_self=False
)

# Document the dispatchers within the BrillouinZone.apply dispatcher
document_class_dispatcher_methods(
sisl.BrillouinZone.apply,
sisl.BrillouinZone,
"apply",
add_signature_self=False,
as_attributes=True,
)


def setup(app):
# Setup autodoc skipping
app.connect("autodoc-skip-member", sisl_skip)
137 changes: 111 additions & 26 deletions src/sisl/viz/_plotables.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

__all__ = ["register_plotable", "register_data_source", "register_sile_method"]

ALL_PLOT_HANDLERS = []


class ClassPlotHandler(ClassDispatcher):
"""Handles all plotting possibilities for a class"""
Expand All @@ -25,6 +27,10 @@ def __init__(self, cls, *args, inherited_handlers=(), **kwargs):
kwargs["type_dispatcher"] = None
super().__init__(*args, inherited_handlers=inherited_handlers, **kwargs)

ALL_PLOT_HANDLERS.append(self)

self.__doc__ = f"Plotting functions for the `{cls.__name__}` class."

self._dispatchs = ChainMap(
self._dispatchs, *[handler._dispatchs for handler in inherited_handlers]
)
Expand Down Expand Up @@ -66,7 +72,7 @@ def dispatch(self, *args, **kwargs):
return self._plot(self._obj, *args, **kwargs)


def create_plot_dispatch(function, name):
def create_plot_dispatch(function, name, plot_cls=None):
"""From a function, creates a dispatch class that will be used by the dispatchers.

Parameters
Expand All @@ -84,6 +90,7 @@ def create_plot_dispatch(function, name):
"_plot": staticmethod(function),
"__doc__": function.__doc__,
"__signature__": inspect.signature(function),
"_plot_class": plot_cls,
},
)

Expand All @@ -110,19 +117,25 @@ def _get_plotting_func(plot_cls, setting_key):
def _plot(obj, *args, **kwargs):
return plot_cls(*args, **{setting_key: obj, **kwargs})

_plot.__doc__ = f"""Builds a {plot_cls.__name__} by setting the value of "{setting_key}" to the current object.
from numpydoc.docscrape import FunctionDoc

Documentation for {plot_cls.__name__}
===========
{inspect.cleandoc(plot_cls.__doc__) if plot_cls.__doc__ is not None else None}
"""
fdoc = FunctionDoc(plot_cls)
fdoc["Parameters"] = list(
filter(lambda p: p.name.replace(":", "") != setting_key, fdoc["Parameters"])
)
docstring = str(fdoc)
docstring = docstring[docstring.find("\n") :].lstrip()

_plot.__doc__ = f"""Builds a ``{plot_cls.__name__}`` by setting the value of "{setting_key}" to the current object."""
_plot.__doc__ += "\n\n" + docstring

sig = inspect.signature(plot_cls)

# The signature will be the same as the plot class, but without the setting key, which
# will be added by the _plot function
_plot.__signature__ = sig.replace(
parameters=[p for p in sig.parameters.values() if p.name != setting_key]
parameters=[p for p in sig.parameters.values() if p.name != setting_key],
return_annotation=plot_cls,
)

return _plot
Expand Down Expand Up @@ -206,11 +219,53 @@ def register_plotable(

plot_handler = getattr(plotable, plot_handler_attr)

plot_dispatch = create_plot_dispatch(plotting_func, name)
plot_dispatch = create_plot_dispatch(plotting_func, name, plot_cls=plot_cls)
# Register the function in the plot_handler
plot_handler.register(name, plot_dispatch, default=default, **kwargs)


def _get_merged_parameters(
doc1,
doc2,
excludedoc1: list = (),
replacedoc1: dict = {},
excludedoc2: list = (),
replacedoc2: dict = {},
):
from numpydoc.docscrape import FunctionDoc

def filter_and_replace(params, exclude, replace):
filtered = list(
filter(lambda p: p.name.replace(":", "") not in exclude, params)
)

replaced = []
for p in filtered:
name = p.name.replace(":", "")
if name in replace:
p = p.__class__(name=replace[name], type=p.type, desc=p.desc)
print(p.name)
replaced.append(p)
return replaced

fdoc1 = FunctionDoc(doc1)

fdoc2 = FunctionDoc(doc2)
fdoc1["Parameters"] = [
*filter_and_replace(fdoc1["Parameters"], excludedoc1, replacedoc1),
*filter_and_replace(fdoc2["Parameters"], excludedoc2, replacedoc2),
]
for k in fdoc1:
if k == "Parameters":
continue
fdoc1[k] = fdoc1[k].__class__()

docstring = str(fdoc1)
docstring = docstring[docstring.find("\n") :].lstrip()

return docstring


def register_data_source(
data_source_cls,
plot_cls,
Expand Down Expand Up @@ -272,7 +327,9 @@ def register_data_source(

new_parameters.extend(list(plot_cls_params.values()))

signature = signature.replace(parameters=new_parameters)
signature = signature.replace(
parameters=new_parameters, return_annotation=plot_cls
)

params_info = {
"data_args": data_args,
Expand Down Expand Up @@ -320,16 +377,30 @@ def _plot(
return plot_cls(**{setting_key: data, **bound.arguments, **plot_kwargs})

_plot.__signature__ = signature
doc = f"Read data into {data_source_cls.__name__} and create a {plot_cls.__name__} from it.\n\n"
doc = f"Creates a ``{data_source_cls.__name__}`` object and then plots a ``{plot_cls.__name__}`` from it.\n\n"

doc += (
# "This function accepts the arguments for creating both the data source and the plot. The following"
# " arguments of the data source have been renamed so that they don't clash with the plot arguments:\n"
# + "\n".join(f" - {v} -> {k}" for k, v in replaced_data_args.items())
"\n"
+ _get_merged_parameters(
func,
plot_cls,
excludedoc1=(list(inspect.signature(func).parameters)[0],),
replacedoc1={
v: k for k, v in params_info["replaced_data_args"].items()
},
excludedoc2=(setting_key,),
)
)

doc += (
"This function accepts the arguments for creating both the data source and the plot. The following"
" arguments of the data source have been renamed so that they don't clash with the plot arguments:\n"
+ "\n".join(f" - {v} -> {k}" for k, v in replaced_data_args.items())
+ f"\n\nDocumentation for the {data_source_cls.__name__} creator ({func.__name__})"
f"\n=============\n{inspect.cleandoc(func.__doc__) if func.__doc__ is not None else None}"
f"\n\nDocumentation for {plot_cls.__name__}:"
f"\n=============\n{inspect.cleandoc(plot_cls.__doc__) if plot_cls.__doc__ is not None else None}"
"\n\nSee also\n--------\n"
+ plot_cls.__name__
+ "\n The plot class used to generate the plot.\n"
+ data_source_cls.__name__
+ "\n The class to which data is converted."
)

_plot.__doc__ = doc
Expand Down Expand Up @@ -405,7 +476,7 @@ def register_sile_method(
),
}

signature = signature.replace(parameters=new_parameters)
signature = signature.replace(parameters=new_parameters, return_annotation=plot_cls)

def _plot(obj, *args, **kwargs):
bound = signature.bind_partial(**kwargs)
Expand Down Expand Up @@ -433,16 +504,30 @@ def _plot(obj, *args, **kwargs):
return plot_cls(**{setting_key: data, **bound.arguments, **plot_kwargs})

_plot.__signature__ = signature
doc = f"Calls {method} and creates a {plot_cls.__name__} from its output.\n\n"
doc = (
f"Calls ``{method}`` and creates a ``{plot_cls.__name__}`` from its output.\n\n"
)

doc += (
# f"This function accepts the arguments both for calling {method} and creating the plot. The following"
# f" arguments of {method} have been renamed so that they don't clash with the plot arguments:\n"
# + "\n".join(f" - {k} -> {v}" for k, v in replaced_data_args.items())
"\n"
+ _get_merged_parameters(
func,
plot_cls,
excludedoc1=(list(inspect.signature(func).parameters)[0],),
replacedoc1={v: k for k, v in params_info["replaced_data_args"].items()},
excludedoc2=(setting_key,),
)
)

doc += (
f"This function accepts the arguments both for calling {method} and creating the plot. The following"
f" arguments of {method} have been renamed so that they don't clash with the plot arguments:\n"
+ "\n".join(f" - {k} -> {v}" for k, v in replaced_data_args.items())
+ f"\n\nDocumentation for {method} "
f"\n=============\n{inspect.cleandoc(func.__doc__) if func.__doc__ is not None else None}"
f"\n\nDocumentation for {plot_cls.__name__}:"
f"\n=============\n{inspect.cleandoc(plot_cls.__doc__) if plot_cls.__doc__ is not None else None}"
"\n\nSee also\n--------\n"
+ plot_cls.__name__
+ "\n The plot class used to generate the plot.\n"
+ method
+ "\n The method called to get the data."
)

_plot.__doc__ = doc
Expand Down
Loading
Loading