Skip to content

Commit

Permalink
Add new config option to convert epytext markup to reST equivalents. F…
Browse files Browse the repository at this point in the history
…ixes GH-7
  • Loading branch information
cbillingham committed Oct 13, 2024
1 parent f503e28 commit aa35962
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 30 deletions.
31 changes: 30 additions & 1 deletion doc/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Example configuration file:
"realign": true,
"max_line_length": 72,
"use_optional": false,
"convert_epytext_markup": "false",
"remove_type_back_ticks": "true",
"use_types": true,
"separate_keywords": false
Expand Down Expand Up @@ -251,14 +252,41 @@ back ticks around type definitions are removed. This option has 3 modes:
- ``:py:class:`Test``` becomes ``Test``
- ``lot`s of `bool`s`` becomes ``lot`s of bools``

convert\_epytext\_markup
''''''''''''''''''''''''

::

"convert_epytext_markup": "false"

Convert epytext markup to reST syntax. Defaults to "false". If this is on,
epytext markup brackets in docstrings will be converted to reST syntax for
supported output formats (all except epytext). See `epytext markup`_.
This option has 3 modes:

- ``"false"``: No conversion happens.
- ``"true"``: Epytext markup is converted in all text. For example:

- ``I{text}`` becomes ``*text*``
- ``B{text}`` stays as ``**text**``
- ``C{source code}`` becomes ``` ``source code`` ```
- ``M{m*x+b}`` becomes ``:math:`m*x+b```

- ``"types"``: All epytext markup is converted. In addition, in type strings,
we fully remove source code markup wrapping (``C{}``). For example:

- ``I{text}`` becomes ``*text*``
- ``B{text}`` stays as ``**text**``
- ``C{MyType}`` becomes ``MyType``

use\_types
''''''''''

::

"use_types": true

Use types in argument output. Defaults to True. If False, argument,
Use types in variable output. Defaults to True. If False, argument,
keyword-argument, attribute, and return type definitions will be skipped
for output formats that support it (google and reST).
This can be turned False for Python 3, where Sphinx recognizes type
Expand All @@ -276,3 +304,4 @@ If set to False, all keyword-arguments are documented with the other arguments.


.. _`type annotations`: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#type-annotations
.. _`epytext markup`: http://epydoc.sourceforge.net/manual-epytext.html#basic-inline-markup
1 change: 1 addition & 0 deletions example_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"realign": true,
"max_line_length": 72,
"use_optional": false,
"convert_epytext_markup": "false",
"remove_type_back_ticks": "true",
"use_types": true,
"separate_keywords": false
Expand Down
3 changes: 2 additions & 1 deletion src/docconvert/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from . import parser
from . import writer
from .writer.base import BackTickRemovalOption
from .writer.base import BackTickRemovalOption, EpytextMarkupConvertOption


_LOGGER = logging.getLogger(__name__)
Expand All @@ -25,6 +25,7 @@
"realign": True,
"max_line_length": PEP8_MAX,
"use_optional": False,
"convert_epytext_markup": EpytextMarkupConvertOption.FALSE,
"remove_type_back_ticks": BackTickRemovalOption.TRUE,
"use_types": True,
"separate_keywords": False,
Expand Down
135 changes: 107 additions & 28 deletions src/docconvert/writer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import abc
import enum
import functools
import re
import textwrap

Expand All @@ -18,15 +19,15 @@ class BaseWriter(object):
This class is meant to be subclassed for each type of docstring.
Attributes:
doc (Docstring): The docstring to
write.
doc (Docstring): The docstring to write.
config (DocconvertConfiguration):
The configuration options for conversion.
output (list(str)): The generated output lines as a result of
writing the new docstring.
"""

_directives = ("example", "note", "seealso", "warning", "reference", "todo")
_supports_epytext_convert = True

def __init__(self, doc, indent, config, kwarg="", vararg=""):
"""
Expand Down Expand Up @@ -66,6 +67,28 @@ def __init__(self, doc, indent, config, kwarg="", vararg=""):
self.config.output.max_line_length
)

# Setup regex for backtick removal if enabled
backtick_option = BackTickRemovalOption.from_bool_or_str(
self.config.output.remove_type_back_ticks
)
self.backtick_regex = None
if backtick_option == BackTickRemovalOption.TRUE:
self.backtick_regex = re.compile(r"(?<!:)`(?P<text>[^\s`]+)`")
elif backtick_option == BackTickRemovalOption.DIRECTIVES:
self.backtick_regex = re.compile(r"[^\s`]*`(?P<text>[^\s`]+)`")

# Setup regex for epytext markup convert if supported and enabled
self.epytext_markup_regex = None
self.remove_epytext_types = False
if self._supports_epytext_convert:
epytext_option = EpytextMarkupConvertOption.from_bool_or_str(
self.config.output.convert_epytext_markup
)
if epytext_option != EpytextMarkupConvertOption.FALSE:
self.epytext_markup_regex = re.compile(r"([IBMC])\{(?P<text>[^\}]*)\}")
if epytext_option == EpytextMarkupConvertOption.TYPES:
self.remove_epytext_types = True

def _calculate_max_line_length(self, max_length):
"""Calculates maximum line length for realigning.
Expand Down Expand Up @@ -185,6 +208,7 @@ def write_raw(self, lines):
for line in lines:
# Append second line adjacent to quotes if first_line specified in config
append = self._elements_written == 1 and self.config.output.first_line
line = self.convert_epytext_markup(line)
self.write_line(line, append=append)

def write_desc(self, desc, header=None, indent=1, hanging=True):
Expand All @@ -201,6 +225,8 @@ def write_desc(self, desc, header=None, indent=1, hanging=True):
hanging (bool): Whether or not lines under the first line
have a hanging indent.
"""
# Convert any lines before realigning so we are converting source
desc = [self.convert_epytext_markup(line) for line in desc]
if header:
if self._is_longer_than_max(header, indent):
self.write_line(header, indent)
Expand Down Expand Up @@ -325,41 +351,49 @@ def remove_back_ticks(self, text):
"""Removes back ticks from text.
Removal depends on configuration of ``remove_type_back_ticks``.
See :py:class:`BackTickRemovalOption`. Option has three modes:
- **FALSE**: No back ticks will be removed.
- **TRUE**: Back ticks will be removed, except from sphinx
directives. For example:
- `` `list` of `str` `` becomes ``list of str``
- `` :py:class:`Test` `` stays as `` :py:class:`Test` ``
- ``lot`s of `bool`s`` becomes ``lot`s of bools``
- **DIRECTIVES**: All back ticks, including directives, will be
removed. For example:
- `` `list` of `str` `` becomes ``list of str``
- `` :py:class:`Test` `` becomes ``Test``
- ``lot`s of `bool`s`` becomes ``lot`s of bools``
See :py:class:`BackTickRemovalOption`.
Args:
text (str): The text to remove back ticks from.
Returns:
str: The string with replaceable back ticks removed.
"""
if not text:
if not text or not self.backtick_regex:
return text
removal_option = BackTickRemovalOption.from_bool_or_str(
self.config.output.remove_type_back_ticks
)
if removal_option == BackTickRemovalOption.FALSE:
return re.sub(self.backtick_regex, r"\g<text>", text)

def _replace_epytext_markup(self, match, in_type=False):
"""Helper used in re.sub to replace epytext markup matches."""
text = match.group(2)
if match.group(1) == "I":
return "*{}*".format(text)
elif match.group(1) == "B":
return "**{}**".format(text)
elif match.group(1) == "M":
return ":math:`{}`".format(text)
elif match.group(1) == "C":
if not in_type or not self.remove_epytext_types:
return "``{}``".format(text)
return text

def convert_epytext_markup(self, text, in_type=False):
"""Converts epytext markup syntax to reST syntax.
Removal depends on configuration of ``convert_epytext_markup``.
See :py:class:`EpytextMarkupConvertOption`.
Args:
text (str): The text to convert.
in_type (bool): Whether the text is in a type definition.
Returns:
str: The string with markup converted.
"""
if not text or not self.epytext_markup_regex:
return text
if removal_option == BackTickRemovalOption.TRUE:
replaceable_back_tick = re.compile(r"(?<!:)`(?P<text>[^\s`]+)`")
else:
replaceable_back_tick = re.compile(r"[^\s`]*`(?P<text>[^\s`]+)`")
return re.sub(replaceable_back_tick, r"\g<text>", text)
replace = functools.partial(self._replace_epytext_markup, in_type=in_type)
return re.sub(self.epytext_markup_regex, replace, text)


class BackTickRemovalOption(enum.Enum):
Expand Down Expand Up @@ -405,5 +439,50 @@ def from_bool_or_str(cls, value):
return cls(value)


class EpytextMarkupConvertOption(enum.Enum):
"""Option for converting epytext inline markup in docstrings.
Option has three modes:
- **FALSE**: No epytext brackets will be converted
- **TRUE**: Epytext brackets will be converted to reST formats.
For example:
- ``I{text}`` becomes ``*text*``
- ``B{text}`` becomes ``**text**``
- ``C{source code}`` becomes ``` ``source code`` ```
- ``M{m*x+b}`` becomes ``:math:`m*x+b```
- **TYPES**: Epytext brackets will be converted to reST formats.
Source code markup will be completely removed from type strings.
For example:
- ``I{text}`` becomes ``*text*``
- ``B{text}`` becomes ``**text**``
- ``C{source code}`` becomes ``source code``
"""

FALSE = "false"
TRUE = "true"
TYPES = "types"

@classmethod
def from_bool_or_str(cls, value):
"""Function to handle getting enum from boolean arguments.
Args:
value (str or bool): The value of the option. Boolean values
will be converted to a string value.
Returns:
EpytextBracketConvertOption: The removal option enum value.
"""
if value is True:
value = "true"
if value is False:
value = "false"
return cls(value)


class InvalidDocstringElementError(Exception):
"""Custom exception for unrecognised docstring elements."""
4 changes: 4 additions & 0 deletions src/docconvert/writer/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def write_directive(self, element):
"""
self.write_section_header(self._directive_title[element[0]])
for line in element[1]:
line = self.convert_epytext_markup(line)
self.write_line(line, indent=1)

def write_args(self, element):
Expand Down Expand Up @@ -91,6 +92,7 @@ def write_var(self, var, use_optional=True):
optional = "optional"
kind = var.kind if self.config.output.use_types else ""
kind = self.remove_back_ticks(kind)
kind = self.convert_epytext_markup(kind, in_type=True)
kind = ", ".join(filter(None, (kind, optional)))
if kind:
kind = " ({0})".format(kind)
Expand Down Expand Up @@ -120,6 +122,7 @@ def write_raises(self, element):
self.write_section_header("Raises")
for var in self.doc.raise_fields:
kind = self.remove_back_ticks(var.kind)
kind = self.convert_epytext_markup(kind, in_type=True)
if var.desc:
header = "{0}:".format(kind) if kind else None
self.write_desc(var.desc, header=header)
Expand All @@ -134,6 +137,7 @@ def write_returns(self, element):
"""
kind = self.doc.return_field.kind if self.config.output.use_types else ""
kind = self.remove_back_ticks(kind)
kind = self.convert_epytext_markup(kind, in_type=True)
if not kind and not self.doc.return_field.desc:
return

Expand Down
5 changes: 5 additions & 0 deletions src/docconvert/writer/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def write_directive(self, element):
"""
self.write_section_header(self._directive_title[element[0]])
for line in element[1]:
line = self.convert_epytext_markup(line)
self.write_line(line)

def write_var(self, var, use_optional=True):
Expand All @@ -92,6 +93,7 @@ def write_var(self, var, use_optional=True):
optional = "optional"
kind = var.kind if self.config.output.use_types else ""
kind = self.remove_back_ticks(kind)
kind = self.convert_epytext_markup(kind, in_type=True)
kind = ", ".join(filter(None, (kind, optional)))
if kind:
kind = " : {0}".format(kind)
Expand All @@ -110,6 +112,7 @@ def write_raises(self, element):
self.write_section_header("Raises")
for var in self.doc.raise_fields:
kind = self.remove_back_ticks(var.kind)
kind = self.convert_epytext_markup(kind, in_type=True)
if kind:
self.write_line(kind)
if var.desc:
Expand All @@ -123,6 +126,7 @@ def write_returns(self, element):
"""
self.write_section_header("Returns")
kind = self.remove_back_ticks(self.doc.return_field.kind)
kind = self.convert_epytext_markup(kind, in_type=True)
# Return type is not optional for numpy docstrings
if not kind:
kind = "unknown"
Expand Down Expand Up @@ -156,4 +160,5 @@ def write_raw(self, lines):
for line in lines:
# Append second line adjacent to quotes if first_line specified in config
append = self._elements_written == 1 and self.config.output.first_line
line = self.convert_epytext_markup(line)
self.write_line(line, append=append)
4 changes: 4 additions & 0 deletions src/docconvert/writer/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def write_directive(self, element):
if i == 0:
self.write_desc([line], header=header, indent=0)
else:
line = self.convert_epytext_markup(line)
self.write_line(line, indent=1)

def write_var(self, var, field, type_field="type", use_optional=True):
Expand All @@ -51,6 +52,7 @@ def write_var(self, var, field, type_field="type", use_optional=True):
optional = "optional"
kind = var.kind if self.config.output.use_types else ""
kind = self.remove_back_ticks(kind)
kind = self.convert_epytext_markup(kind, in_type=True)
kind = ", ".join(filter(None, (kind, optional)))

header = self._var_token.format(field, var.name)
Expand Down Expand Up @@ -94,6 +96,7 @@ def write_raises(self, element):
"""
for var in self.doc.raise_fields:
kind = self.remove_back_ticks(var.kind)
kind = self.convert_epytext_markup(kind, in_type=True)
header = self._var_token.format("raises", kind)
self.write_desc(var.desc, header=header, indent=0)

Expand All @@ -105,6 +108,7 @@ def write_returns(self, element):
"""
kind = self.doc.return_field.kind if self.config.output.use_types else ""
kind = self.remove_back_ticks(kind)
kind = self.convert_epytext_markup(kind, in_type=True)
if self.doc.return_field.desc:
header = self._field_token.format("returns")
self.write_desc(self.doc.return_field.desc, header=header, indent=0)
Expand Down
Loading

0 comments on commit aa35962

Please sign in to comment.