sphinx icon indicating copy to clipboard operation
sphinx copied to clipboard

How to display an argument as optional

Open liebsc21 opened this issue 2 years ago • 13 comments

Is your feature request related to a problem? Please describe. I am using autodoc_typehints = "description" (and napoleon) in conf.py to add type hints in the rendered docstring. Then the following docstring

def func(param1: int, param2: int = 0):
    """...
    Args:
      param1: description1
      param2: description2
    """

is rendered like this:

param1 (int): description1
param2 (int): description2

So far so good. But next I would like my rendered docstring to display whether a parameter is optional, i.e. has a default. In other words I want it to look like this:

param1 (int): description1
param2 (int, optional): description2

Describe the solution you'd like Is there a way to achieve this automatically?

Describe alternatives you've considered When I type

def func(param1: int, param2: int = 0):
    """...
    Args:
      param1: description1
      param2(optional): description2
    """

the automatically generated typehint is overwritten:

param1 (int): description1
param2 (optional): description2

Additional context I already opened an issue in autoapi, but they redirected me to here.

liebsc21 avatar May 23 '23 07:05 liebsc21

AFAIK, this is not yet supported. The reason is that (optional) is treated as a type itself and rendering the type annotation only works if there is no type yet. What you want is to merge the type hints and the type description but this may lead to some issues:

  • If two types are equivalent but named differently, there will be a duplication.
  • What should we do if the type annotation and the description are incompatible ?

One possibility instead is to modify the behaviour of autodoc_typehints as follows:

  • If there is a default value and a type annotation, then Sphinx automatically adds the "optional". That way, you won't even need to put (optional). If you however put it, we won't duplicate it (i.e., "optional, optional" will collapse to "optional").
  • If the type description is not explicitly "(optional)", we keep the current behaviour.

Related: #11376.

picnixz avatar May 23 '23 10:05 picnixz

If there is a default value and a type annotation, then Sphinx automatically adds the "optional". That way, you won't even need to put (optional). If you however put it, we won't duplicate it (i.e., "optional, optional" will collapse to "optional").

Is exactly what I would like. But I do not understand

If the type description is not explicitly "(optional)", we keep the current behaviour.

I would like to have the docstring of

def func(param1: int, param2: int = 0):
    """...
    Args:
      param1: description1
      param2: description2
    """

rendered as

param1 (int): description1
param2 (int, optional): description2

In other words: I never want to write anything explicitly in the docstring. Both the type - e.g. int - and optional should automatically be added in the rendered docs based on the function's signature.

liebsc21 avatar Jun 13 '23 07:06 liebsc21

I don't remember why I said

If the type description is not explicitly "(optional)", we keep the current behaviour.

I think it was an answer to the alternative you considered but I put it in the bullet list itself. As for what you actually want, it should be the first bullet point. I think it should be easy enough to actually add the optional properly by changing this:

https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/ext/autodoc/typehints.py#L31-L35

and

https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/ext/autodoc/typehints.py#L152-L155

When recording type hints, we would also record whether there is an optional value and then we would put some optional in the node, if needed.


Note for myself: only add , optional) if the node does not already ends with , optional).

picnixz avatar Jun 13 '23 07:06 picnixz

Can you estimate when this could be done?

liebsc21 avatar Jun 27 '23 20:06 liebsc21

I am currently travelling and am quite busy until late September. In addition, the PR needs to be approved but the current owner is also extremely busy. I should be able to make a PR either by the end of August, or in October but I have no ides how long it will take to merge it upstream.

If anyone wants to pick the task, they can. I don't call dibs on this one!

picnixz avatar Jun 28 '23 07:06 picnixz

Are there any updates? This is still relevant.

liebsc21 avatar Jan 03 '24 09:01 liebsc21

Unfortunately I don't have time for that. I know it's relevant but this kind of issje is likely to stay opened until someone is willing to help (we lack contributors so help is always welcomed).

Also, I would like to know the opinion of other maintainers to know whether it's worth our time or not for the moment.

picnixz avatar Jan 03 '24 10:01 picnixz

@picnixz This is somewhat of a less common syntax (and perhaps older if not legacy? Predating the current state of the typing module):

param2 (int, optional): description2

Here optional doesn't express the None type as in param2 (Optional[int]): description2 in Python 3.10< or as param2 (int | None): description2 in Python 3.10+. Parameters with default values in a signature are implicitly "optional" (optional being plain English unrelated to the None type).

The Sphinx docs still have a few scattered examples of this kind of syntax (for example here) but where I think it's seen most is in the numpydoc context. I have to wonder if this syntax is still used or if it should be considered legacy altogether (and a poor practice at present, that's especially prone to add confusion for beginners), because reading through the PEPs and numpydoc I can't find a solid reference or need for it in 2024.

electric-coder avatar Jan 03 '24 12:01 electric-coder

I did not consult any PEPs, but why do you think, it's a poor practise or even deprecated when numpy (one of the most popular Python packages) uses this style, see e.g. numpy.trace

Personally, I think that this style adds clarity rather than confusion - especially for beginners.

liebsc21 avatar Jan 03 '24 20:01 liebsc21

when numpy (one of the most popular Python packages) uses this style

NumPy uses its namesake documentation style, but writing the types as list of int in prose is only understandable so long as the types are short. Most of the NumPy API uses flat collections with only a few builtin type but APIs with more complex types e.g. (Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[[object]]]]]) of pytest.fixture wouldn't be readable using the NumpPy style. In other words, while the style is common in Python's scientific libraries, it's because for the particular and quite narrow case of flat and homogeneous collections with numerical data types it works just well enough to still be readable.

I did not consult any PEPs

The use of the optional keyword is motivated by a single sentence in PEP 257 with no clear indication of how it should be done.

PEP 257 – Docstring Conventions

Optional arguments should be indicated.

In fact, if you look at the NumPy documentation the use of optional/default is often inconsistent throughout the library (the linked example is one of the few cases where default is used but most times it's omitted), There's no clear mention or explanation of optional/default at least not one that can be quickly found, and it's also absent from the Google Style guide that NumPy follows.

Personally, I think that this style adds clarity rather than confusion

What it adds is duplication of the signature in prose. Saying something twice doesn't make it clearer. I haven't found one single consistent explanation in any of the style guides out there of where, how and why optional/default should be used, if it's use might marginally help beginner users it certainly adds confusion through ambiguity for documentation authors.

electric-coder avatar Jan 11 '24 18:01 electric-coder

This issue is solely about adding the word optional next to type in the rendered docstring, see my first post in this conversation.

Hence, I do not understand the point about long type names. However, you've got a point about the duplication with the function signature. Let me digest this. ;) If I do not come up with another argument, I will close this issue presently.

liebsc21 avatar Jan 16 '24 20:01 liebsc21

@liebsc21 leave the issue open. I made the argument because I felt it needed to be made. What I think is needed is a brief context in the Sphinx docs and the NumPy docs putting the use of optional/default into perspective as I did here, but that is an argument for another post not this one.

When you say:

Let me digest this.

This stuff should be made obvious for documentation authors the first time they read the Napoleon/NumPy docs.

electric-coder avatar Jan 16 '24 20:01 electric-coder

There is a case for having the ability to specify optional.

This would be the way I would see it working.

class DataManager:

...

    def set_data(self, **kwargs) -> dict:
        """Method to set specific data for a request.

        :param headers: If True return headers as part of the data.
        :type headers: bool
        :optional headers: true
        # If optional is false, or omitted then it's required
        :param body: The body contents of the request.
        :type body: str
        # Here optional is omitted so 'body' is required.
        """
        data: dict = {}
        if 'headers' in kwargs.keys():
            headers = """Some code to build headers goes here"
            data['headers'] = headers
        ...
        return data

bmmcwhirt avatar Aug 08 '24 16:08 bmmcwhirt