6
\$\begingroup\$

Hello everyone!

In this task I had to create docstring generator. I'm just wondering how you see this problem, I have to "take data" from the dictionary

{'Args': None, 'Returns': None, 'Raises': None, 'Attributes': None, 'Summary': None, 'Description': None}

and then generate docstring in the correct order.

This is my solution:

import textwrap

def gendoc(config):
    docstring = []
    if ("Summary" in config) and (config["Summary"] is not None):
        docstring.append(config["Summary"])
    if ("Args" in config) and (config["Args"] is not None):
        args = ["Args:"] + [f"\t{arg}: Function argument" for arg in config["Args"]]
        docstring += args
    if ("Attributes" in config) and (config["Attributes"] is not None):
        attr = ["Attributes:"] + [f"\t{attr}: Information about parameter {attr}" for attr in config["Attributes"]]
        docstring += attr
    if ("Raises" in config) and (config["Raises"] is not None):
        raises = ["Raises: "] + [f"\t {config.get('Raises')}"]
        docstring += raises
    if ("Returns" in config) and (config["Returns"] is not None):
        returns = ["Returns: "] + [f"\t {config.get('Returns')}"]
        docstring += returns
    if ("Description" in config) and (config["Description"] is not None):
        value = config.get('Description')
        wrapper=textwrap.TextWrapper(subsequent_indent= '\t', width = 120)
        description = ["Description: "] + [f"\t{wrapper.fill(value)}"]
        docstring += description
    if ("Todo" in config) and (config["Todo"] is not None):
        star = "*"
        todo = ["Todo:"] + [f"\t{star} {todo}" for todo in config["Todo"]]
        docstring += todo
        
    docstring = "\n\n".join(docstring)
    return docstring

My output:

The Pear object describes the properties of pears.

Args:

    a: Function argument

    b: Function argument

Attributes:

    a: Information about parameter a

    b: Information about parameter b

Raises: 

     AttributeError: The ``Raises`` section is a list of all exceptions that are relevant to the interface.

Returns: 

     The return value. True for success, False otherwise.

Description: 

    The description may span multiple lines. Following lines should be indented. The type is optional. The description may
    span multiple lines. Following lines should be indented. The type is optional.The description may span multiple lines.
    Following lines should be indented. The type is optional.The description may span multiple lines. Following lines
    should be indented. The type is optional.

Todo:

    * Do something

    * Something else
 

Any tips how to make this SHORTER and more neat, closer to a advanced or just better solution will be definitely on point. Thanks in advance!

Have a great day!

\$\endgroup\$
1
  • 3
    \$\begingroup\$ Welcome to the Code Review Community, I hope you get some good answers. \$\endgroup\$
    – pacmaninbw
    May 7, 2021 at 12:44

2 Answers 2

5
\$\begingroup\$
  • if ("Summary" in config) and (config["Summary"] is not None):
    

    Can be simplified to:

    if config.get("summary") is not None:
    
  • args = ["Args:"] + [f"\t{arg}: Function argument" for arg in config["Args"]]
    attr = ["Attributes:"] + [f"\t{attr}: Information about parameter {attr}" for attr in config["Attributes"]]
    todo = ["Todo:"] + [f"\t{star} {todo}" for todo in config["Todo"]]
    

    Can all be moved into a function.

    • You always add f"{Name}:",
    • You always iterate through config[Name], and
    • You always format the value. Which can be provided as a format string (not an f-string).
    def format_values(config, name, fmt):
        return [f"{name}:"] + [fmt.format(value) for value in config[name]]
    
    format_values(config, "Args", "\t{}: Function argument")
    
  • docstring.append(config["Summary"])
    raises = ["Raises: "] + [f"\t {config.get('Raises')}"]
    returns = ["Returns: "] + [f"\t {config.get('Returns')}"]
    

    Can all be moved into a function.

    • You add f"{Name}: " except for Summary,
    • You always add config[Name], and
    • You always format the value. (Summary would be "{}")
    def format_value(config, name, fmt, head=True):
        return ([f"{name}: "] if head else []) + [fmt.format(config[name])]
    
    format_value(config, "Summary", "{}", False)
    
  • value = config.get('Description')
    wrapper=textwrap.TextWrapper(subsequent_indent= '\t', width = 120)
    description = ["Description: "] + [f"\t{wrapper.fill(value)}"]
    docstring += description
    

    To be able to handle Description we can change format_value (and optionally format_values for consistency) to take a lambda to decide how to display the value.

    def format_value(config, name, fmt, head=True):
        return ([f"{name}: "] if head else []) + [fmt(config[name])]
    
    wrapper = textwrap.TextWrapper(subsequent_indent='\t', width=120)
    docstring += format_value(config, "Description", lambda value: f"\t{wrapper.fill(value)}")
    

Lets look at how the code looks now:

import textwrap


def format_value(config, name, fmt, head=True):
    return ([f"{name}: "] if head else []) + [fmt(config[name])]


def format_values(config, name, fmt):
    return [f"{name}:"] + [fmt(value) for value in config[name]]


def gendoc(config):
    docstring = []
    if None is not config.get("Summary"):
        docstring += format_value(config, "Summary", lambda summary: summary, False)
    if None is not config.get("Args"):
        docstring += format_values(config, "Args", lambda arg: f"\t{arg}: Function argument")
    if None is not config.get("Attributes"):
        docstring += format_values(config, "Attributes", lambda attr: f"\t{arg}: Information about parameter {arg}")
    if None is not config.get("Raises"):
        docstring += format_value(config, "Raises", lambda raises: f"\t {raises}")
    if None is not config.get("Returns"):
        docstring += format_value(config, "Returns", lambda returns: f"\t {returns}")
    if None is not config.get("Description"):
        wrapper = textwrap.TextWrapper(subsequent_indent='\t', width=120)
        docstring += format_value(config, "Description", lambda value: f"\t{wrapper.fill(value)}")
    if None is not config.get("Todo"):
        star = "*"
        docstring += format_values(config, "Todo", lambda todo: f"\t{star} {todo}")

    docstring = "\n\n".join(docstring)
    return docstring
if None is not config.get({name}):
    docstring += format_value(config, {name}, ...)

We can see we are duplicating the above pseudocode. Lets ignore the fmt and head arguments to format_value and format_values.

We could change the code to:

docstring = []
for name, fn in ...:
    if config.get(name) is not None:
        docstring += fn(config, name)

We can change format_value and format_values so we provide the fmt and head. We then return another function which takes config and name to call in the loop.

def format_value(fmt, head=True):
    def inner(config, name):
        return ([f"{name}: "] if head else []) + [fmt(config[name])]
    return inner

format_value("{}")(config, "Summary")

Next we can define a dictionary to store the result of the first call to format_value and format_values. And then change the code to iterate over the dictionary.

Final code:

import textwrap


def format_value(fmt, head=True):
    def inner(config, name):
        return ([f"{name}: "] if head else []) + [fmt(config[name])]
    return inner


def format_values(fmt):
    def inner(config, name):
        return [f"{name}:"] + [fmt(value) for value in config[name]]
    return inner


wrapper = textwrap.TextWrapper(subsequent_indent='\t', width=120)
star = "*"

FORMATS = {
    "Summary": format_value(lambda summary: f"{summary}", False),
    "Args": format_values(lambda arg: f"\t{arg}: Function argument"),
    "Attributes": format_values(lambda attr: f"\t{arg}: Information about parameter {arg}"),
    "Raises": format_value(lambda raises: f"\t {raises}"),
    "Returns": format_value(lambda returns: f"\t {returns}"),
    "Description": format_value(lambda value: f"\t{wrapper.fill(value)}"),
    "Todo": format_values(lambda todo: f"\t{star} {todo}"),
}


def gendoc(config):
    docstring = []
    for name, fn in FORMATS.items():
        if config.get(name) is not None:
            docstring += fn(config, name)
    return "\n\n".join(docstring)
\$\endgroup\$
1
  • 4
    \$\begingroup\$ As an alternative to the "format" lambdas, you could use string.format, e.g., "Args": format_values("\t{}: Function argument".format). I leave it to the reader to decide which is clearer. \$\endgroup\$
    – RootTwo
    May 7, 2021 at 18:35
5
\$\begingroup\$
  • Avoid representing internal data as dictionaries if possible. As @RootTwo indicates, it's trivial to initialize a dataclass from a dict via the ** kwarg-splatting operator if that's needed.
  • Would be nice to support output to string, stdout or a file through wrappers. StringIO makes this easy.
  • Avoid \t - it has medium-dependent indentation; better to have space rendering with a user-configurable indentation level.
  • You should wrap everything, not just your description.
  • This is a matter of opinion, but I disagree with double-newlines on the inside of your sections; reserving double-newlines for section breaks makes things clearer.

Example code

from dataclasses import dataclass
from io import StringIO
from sys import stdout
from textwrap import TextWrapper
from typing import Optional, Tuple, Sequence, TextIO

Pair = Tuple[str, str]
PairSeq = Sequence[Pair]


@dataclass
class Documentation:
    summary: Optional[str] = None
    args: Optional[PairSeq] = None
    attrs: Optional[PairSeq] = None
    raises: Optional[PairSeq] = None
    returns: Optional[str] = None
    desc: Optional[str] = None
    todo: Optional[Sequence[str]] = None
    indent: int = 4
    wrap: int = 80

    def __post_init__(self):
        self.wrap_outer = TextWrapper(width=self.wrap).fill
        indent = ' ' * self.indent
        self.wrap_inner = TextWrapper(
            width=self.wrap,
            initial_indent=indent,
            subsequent_indent=indent,
        ).fill

    def __str__(self) -> str:
        with StringIO() as f:
            self.to_file(f)
            return f.getvalue()

    def print(self) -> None:
        self.to_file(stdout)

    def write_section(self, title: str, content: str, f: TextIO) -> None:
        if content:
            f.write(f'\n{title}:\n')
            f.write(self.wrap_inner(content) + '\n')

    def write_pairs(self, title: str, pairs: PairSeq, f: TextIO) -> None:
        if pairs:
            f.write(f'\n{title}:\n')
            f.writelines(
                self.wrap_inner(f'{name}: {desc}') + '\n'
                for name, desc in pairs
            )

    def to_file(self, f: TextIO) -> None:
        if self.summary:
            f.write(self.wrap_outer(self.summary))

        self.write_pairs('Arguments', self.args, f)
        self.write_pairs('Attributes', self.attrs, f)
        self.write_pairs('Raises', self.raises, f)
        self.write_section('Returns', self.returns, f)
        self.write_section('Description', self.desc, f)

        if self.todo:
            f.write(f'\nTodo:\n')
            f.writelines(
                self.wrap_inner(f'* {todo}') + '\n'
                for todo in self.todo
            )


def test():
    Documentation(
        summary='The Pear object describes the properties of pears.',
        args=(
            ('a', 'Function argument'),
            ('b', 'Function argument'),
        ),
        attrs=(
            ('a', 'Information about parameter a'),
            ('b',
             'Information about parameter b. '
             'Information about parameter b. '
             'Information about parameter b. '
             'Information about parameter b. '
             ),
        ),
        raises=(
            ('AttributeError',
             'The ``Raises`` section is a list of all exceptions that are '
             'relevant to the interface.'),
        ),
        returns='The return value. True for success, False otherwise.',
        desc='The description may span multiple lines. Following lines should '
             'be indented. The type is optional. '
             'The description may span multiple lines. Following lines should '
             'be indented. The type is optional. '
             'The description may span multiple lines. Following lines should '
             'be indented. The type is optional. '
             'The description may span multiple lines. Following lines should '
             'be indented. The type is optional.',
        todo=('Do Something', 'Something else'),
    ).print()


if __name__ == '__main__':
    test()

Output

The Pear object describes the properties of pears.
Arguments:
    a: Function argument
    b: Function argument

Attributes:
    a: Information about parameter a
    b: Information about parameter b. Information about parameter b. Information
    about parameter b. Information about parameter b.

Raises:
    AttributeError: The ``Raises`` section is a list of all exceptions that are
    relevant to the interface.

Returns:
    The return value. True for success, False otherwise.

Description:
    The description may span multiple lines. Following lines should be indented.
    The type is optional. The description may span multiple lines. Following
    lines should be indented. The type is optional. The description may span
    multiple lines. Following lines should be indented. The type is optional.
    The description may span multiple lines. Following lines should be indented.
    The type is optional.

Todo:
    * Do Something
    * Something else
\$\endgroup\$
1
  • 2
    \$\begingroup\$ @Daro, from the problem description, it sounds like the docstring configuration is provided as a dictionary. Fortunately, the Documentation dataclass can be initialized from a dictionary like so: Documentation(**config) \$\endgroup\$
    – RootTwo
    May 7, 2021 at 21:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.