Skip to content

Commit

Permalink
Add initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
cthoyt committed Feb 20, 2021
1 parent 76e5083 commit 7ad70d7
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 9 deletions.
13 changes: 4 additions & 9 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ classifiers =
Framework :: tox
Framework :: Sphinx
Programming Language :: Python
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3 :: Only
Expand All @@ -47,20 +49,13 @@ keywords =

[options]
install_requires =
# Missing itertools from the standard library you didn't know you needed
more_itertools
# Use progress bars excessively
tqdm
# Command line tools
click
more_click
# TODO your requirements go here
pyyaml


# Random options
zip_safe = false
include_package_data = True
python_requires = >=3.8
python_requires = >=3.6

# Where is my code
packages = find:
Expand Down
7 changes: 7 additions & 0 deletions src/docdata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# -*- coding: utf-8 -*-

"""Add structured information to the end of your python docstrings."""

from .api import docdata, parse_docdata

__all__ = [
'docdata',
'parse_docdata',
]
64 changes: 64 additions & 0 deletions src/docdata/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-

"""Utilities for documentation."""

import textwrap
from typing import Optional, TypeVar

import yaml

__all__ = [
'docdata',
'parse_docdata',
]

X = TypeVar('X')

DOCDATA_DUNDER = '__docdata__'


def docdata(obj: X) -> Optional[str]:
"""Get the docdata if it is available."""
return getattr(obj, DOCDATA_DUNDER, None)


def parse_docdata(obj: X, delimiter: str = '---') -> X:
"""Parse the structured data from the end of the docstr and store it in ``__docdata__``.
The data after the delimiter should be in the YAML form.
It is parsed with :func:`yaml.safe_load` then stored in the ``__docdata__`` field of the
object.
:param obj: Any object that can has a ``__doc__`` field.
:param delimiter: The delimiter between the actual docstring and structured YAML.
:return: The same object with a modified docstr.
:raises AttributeError: if the object has no ``__doc__`` field.
"""
try:
docstr = obj.__doc__
except AttributeError:
raise AttributeError(f'no __doc__ available in {obj}')
if docstr is None: # no docstr to modify
return obj

lines = docstr.splitlines()
try:
index = min(
i
for i, line in enumerate(lines)
if line.strip() == delimiter
)
except ValueError:
return obj

# The docstr is all of the lines before the line with the delimiter. No
# modification to the text wrapping is necessary.
obj.__doc__ = '\n'.join(lines[:index])

# The YAML structured data is on all lines following the line with the delimiter.
# The text must be dedented before YAML parsing.
yaml_str = textwrap.dedent('\n'.join(lines[index + 1:]))
yaml_data = yaml.safe_load(yaml_str)
setattr(obj, DOCDATA_DUNDER, yaml_data)
return obj
58 changes: 58 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-

"""Test the docdata parser."""

import unittest

from docdata import docdata, parse_docdata


class TestParse(unittest.TestCase):
"""Test parsing docdata."""

def _help(self, a, b):
self.assertEqual(a.__doc__, b.__doc__)
self.assertIsNone(docdata(b))
self.assertEqual({'name': 'A'}, docdata(a))

def test_parse_no_params(self):
"""Test parsing docdata."""

@parse_docdata
class A:
"""This class has a docdata.
---
name: A
"""

class B:
"""This class has a docdata."""

self._help(A, B)

def test_parse_with_params(self):
"""Test parsing docdata."""

@parse_docdata
class A:
"""This class has a docdata.
:param args: Nope.
---
name: A
"""

def __init__(self, *args):
self.args = args

class B:
"""This class has a docdata.
:param args: Nope.
"""

def __init__(self, *args):
self.args = args

self._help(A, B)

0 comments on commit 7ad70d7

Please sign in to comment.