Skip to content

Commit

Permalink
Documentation of dispatchers
Browse files Browse the repository at this point in the history
  • Loading branch information
pfebrer committed Oct 8, 2024
1 parent e35cb57 commit ae41cb4
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 34 deletions.
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

0 comments on commit ae41cb4

Please sign in to comment.