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

Custom template / handller for param.Parameterized classes #515

Open
Jhsmit opened this issue Jan 19, 2023 · 5 comments
Open

Custom template / handller for param.Parameterized classes #515

Jhsmit opened this issue Jan 19, 2023 · 5 comments
Labels
feature New feature or request

Comments

@Jhsmit
Copy link

Jhsmit commented Jan 19, 2023

Is your feature request related to a problem? Please describe.
I have a repository here which generates sphinx docs from param.Parameterized classes. These type of classes are frequently used to define widgets / parameters for panel based web applications. The goal is to generate documentation for the web application users (normal human being readable, not API docs) from the param.Parameters defined on the class.

class FruitStallController(param.Parameterized):
    """
    This controller lets the user choose a type of fruit, set the amount and then buy some.
    The docstring has two lines. Which is ignored. Lets try some whitespace.

    This is the next line after the whitespace.
    """
    title = 'Fruit Stall'
    random_attribute = 10  # not shown in web application
    hidden_param = param.Boolean(precedence=-1,
                                 doc='This parameter is documented but due to low precedence not shown in documentation.')

    fruit = param.Selector(default='apple', objects=['apple', 'pear', 'banana'],
                           doc='Select type of fruit to buy.')

    quantity = param.Integer(5, bounds=(0, None), doc='Number of selected fruit to buy.')

    price = param.Number(0., constant=True, bounds=(0, None), doc='Price of the fruit per piece.')

    button = param.Action(lambda x: print('You bought fruit'),
                                 doc='Press this button to buy some fruit!', label='Make Purchase')

    def __init__(self, **params):
        super(FruitStallController, self).__init__(**params)

    @param.depends('fruit', watch=True)
    def _update_price(self):
        """Update prices"""
        price_list = {'apple': 1.30, 'pear': 1.55, 'banana': 2.00}
        self.price = price_list[self.fruit]

When used in panel, this class can be used to generate simple web application with a selector, two input widgets and a button. The

I'm now trying to do the same in mkdocs. The desired output is something like this:

image

This output is acceptable but not the ideal case. Since its aimed towards people who use the web application rather than python code, the line class ... <etc> could be emitted. But these are details, the most important is that users can see the label attribute (ie button.label) (which is how the widgets is labelled) in connection to the doc attribute, second is the additional attributes such as objects, bounds, etc.

Describe the solution you'd like
I think ideally I would create a custom template which renders these param.Parameterized classes the way I'd like. But after playing around with these for a bit it seems there I only have access to attribute.value, which does more or less contain the information I have but I don't directly see a way to jinja this into the output I'd like.
Is it possible to do what I want via a template?

Describe alternatives you've considered
I've considered a custom handler based on the python handler but if I understand the procedure correctly I'd have to create whole namespace package and deal with a lot of boilerplate.

The other option I've considered is not use mkdocstrings plugin directly but instead manually write a script which import griffe and python handler directly and runs by mkdocs-gen-files
This might be the most flexible approach but for the end users (which is mostly myself atm) a similar API as currently used in mkdocstrings would be preferable.
If this seems like sensible approach, what would be a good starting point?

Additional context
My current mkdocs_param playground repo is here

@Jhsmit Jhsmit added the feature New feature or request label Jan 19, 2023
@pawamoy
Copy link
Member

pawamoy commented Jan 23, 2023

Hi @Jhsmit, thanks for the feature request! This sounds like a great case for a Griffe extension! The docs are a bit terse, but I'll try to give more details later this week 🙂

@pawamoy
Copy link
Member

pawamoy commented Jan 27, 2023

So basically Griffe parses the code into an AST (Abstract Syntax Tree) and then visits this tree. You can write a Griffe extension that will add logic by visiting these attributes' values, find the doc and precedence arguments, and use them to alter the contents of the objects instantiated by Griffe. For example you could set their docstring using the value of the doc parameter, or even delete the object entirely when its precedence is low. You could also attach extra information to the object, and then write a custom template that detects these extra bits and renders the object differently.

@Jhsmit
Copy link
Author

Jhsmit commented Jan 31, 2023

Hi @pawamoy , thanks for your help. I'll give the griffe extension a try (when I find the time)

How do you go about developing / debugging something like this? Do you have suggestions or a MWE example of how I can make a python script with my example class I want to autodoc and how to generate docs so I can use breakpoints etc without having to rely on mkdocs serve and eg write logging statements in the templates?

@pawamoy
Copy link
Member

pawamoy commented Jan 31, 2023

I debug in VSCode. You could start by writing a dummy Griffe extension that does almost nothing, put a breakpoint in there, and add a VSCode run configuration that runs the griffe module with the dump package -e '[{"dummy": {}}]' arguments, package being either param_docs, or griffe itself, or something else. You should probably set justMyCode to false and see what Griffe is doing during the visit.

@pawamoy
Copy link
Member

pawamoy commented Jan 31, 2023

An MWE would look like:

# dummy.py
from griffe.agents.extensions import VisitorExtension, When


class Extension(VisitorExtension):
    when = When.after_all  # then try with before_all
    # see https://mkdocstrings.github.io/griffe/reference/griffe/agents/extensions/#griffe.agents.extensions.When

    def visit_assign(self, node) -> None:
        print("hello")  # <- put breakpoint here,
        # with a condition like node.targets[0].id == "fruit"
        # or self.current.name == "FruitStallController"

Then create a venv, install Griffe and param_docs in it, and run griffe dump param_docs -e '[{"dummy": {}}]' in a debugger 🙂


EDIT: even easier, just follow the example in https://mkdocstrings.github.io/griffe/extensions/#extensions and write a script that declares your extension and instantiate the loader with the extension. Running this script should do the trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants