Skip to content

Commit

Permalink
Merge pull request #9 from Chilipp/dev
Browse files Browse the repository at this point in the history
Changed handling of decorators for classes in python 2.7
  • Loading branch information
Chilipp committed Apr 19, 2018
2 parents 14c2fd6 + 0ba440b commit 5154fcd
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 15 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
v0.2.3
======
This minor release contains some backward incompatible changes on how to handle
the decorators for classes in python 2.7. Thanks
`@lesteve <https://github.com/lesteve>`__ and
`@guillaumeeb <https://github.com/guillaumeeb>`__ for your input on this.

Changed
-------
* When using the decorators for classes in python 2.7, e.g. via::

>>> @docstrings
... class Something(object):
... "%(replacement)s"

it does not have an effect anymore. This is because class docstrings cannot
be modified in python 2.7 (see issue
`#5 <https://github.com/Chilipp/docrep/issues/5#>`__). The original behaviour
was to raise an error. You can restore the old behaviour by setting
`DocstringProcessor.python2_classes = 'raise'`.
* Some docs have been updated (see PR
`#7 <https://github.com/Chilipp/docrep/pull/7>`__)

Added
-----
* the `DocstringProcessor.python2_classes` to change the handling of classes
in python 2.7

v0.2.2
======
Added
Expand Down
103 changes: 90 additions & 13 deletions docrep/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import types
import six
import inspect
import re
from warnings import warn


__version__ = '0.2.2'
__version__ = '0.2.3'

__author__ = 'Philipp Sommer'

Expand Down Expand Up @@ -226,6 +227,19 @@ class DocstringProcessor(object):
text_sections = ['Warnings', 'Notes', 'Examples', 'See Also',
'References']

#: The action on how to react on classes in python 2
#:
#: When calling::
#:
#: >>> @docstrings
#: ... class NewClass(object):
#: ... """%(replacement)s"""
#:
#: This normaly raises an AttributeError, because the ``__doc__`` attribute
#: of a class in python 2 is not writable. This attribute may be one of
#: ``'ignore', 'raise' or 'warn'``
python2_classes = 'ignore'

def __init__(self, *args, **kwargs):
"""
Parameters
Expand Down Expand Up @@ -256,9 +270,22 @@ def __init__(self, *args, **kwargs):
self.patterns = patterns

def __call__(self, func):
func.__doc__ = func.__doc__ and safe_modulo(func.__doc__, self.params,
stacklevel=3)
return func
"""
Substitute in a docstring of a function with :attr:`params`
Parameters
----------
func: function
function with the documentation whose sections
shall be inserted from the :attr:`params` attribute
See Also
--------
dedent: also dedents the doc
with_indent: also indents the doc"""
doc = func.__doc__ and safe_modulo(func.__doc__, self.params,
stacklevel=3)
return self._set_object_doc(func, doc)

def get_sections(self, s, base,
sections=['Parameters', 'Other Parameters']):
Expand Down Expand Up @@ -342,6 +369,23 @@ def func(f):
return f
return func

def _set_object_doc(self, obj, doc, stacklevel=3):
"""Convenience method to set the __doc__ attribute of a python object
"""
if isinstance(obj, types.MethodType) and six.PY2:
obj = obj.im_func
try:
obj.__doc__ = doc
except AttributeError: # probably python2 class
if (self.python2_classes != 'raise' and
(inspect.isclass(obj) and six.PY2)):
if self.python2_classes == 'warn':
warn("Cannot modify docstring of classes in python2!",
stacklevel=stacklevel)
else:
raise
return obj

def dedent(self, func):
"""
Dedent the docstring of a function and substitute with :attr:`params`
Expand All @@ -351,11 +395,8 @@ def dedent(self, func):
func: function
function with the documentation to dedent and whose sections
shall be inserted from the :attr:`params` attribute"""
if isinstance(func, types.MethodType) and not six.PY3:
func = func.im_func
func.__doc__ = func.__doc__ and self.dedents(func.__doc__,
stacklevel=4)
return func
doc = func.__doc__ and self.dedents(func.__doc__, stacklevel=4)
return self._set_object_doc(func, doc)

def dedents(self, s, stacklevel=3):
"""
Expand All @@ -373,15 +414,51 @@ def dedents(self, s, stacklevel=3):
return safe_modulo(s, self.params, stacklevel=stacklevel)

def with_indent(self, indent=0):
"""
Substitute in the docstring of a function with indented :attr:`params`
Parameters
----------
indent: int
The number of spaces that the substitution should be indented
Returns
-------
function
Wrapper that takes a function as input and substitutes it's
``__doc__`` with the indented versions of :attr:`params`
See Also
--------
with_indents, dedent"""
def replace(func):
if isinstance(func, types.MethodType) and not six.PY3:
func = func.im_func
func.__doc__ = func.__doc__ and self.with_indents(
doc = func.__doc__ and self.with_indents(
func.__doc__, indent=indent, stacklevel=4)
return func
return self._set_object_doc(func, doc)
return replace

def with_indents(self, s, indent=0, stacklevel=3):
"""
Substitute a string with the indented :attr:`params`
Parameters
----------
s: str
The string in which to substitute
indent: int
The number of spaces that the substitution should be indented
stacklevel: int
The stacklevel for the warning raised in :func:`safe_module` when
encountering an invalid key in the string
Returns
-------
str
The substituted string
See Also
--------
with_indent, dedents"""
# we make a new dictionary with objects that indent the original
# strings if necessary. Note that the first line is not indented
d = {key: _StrWithIndentation(val, indent)
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@
'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None),
'numpy': ('http://docs.scipy.org/doc/numpy/', None),
'matplotlib': ('http://matplotlib.org/', None),
'sphinx': ('http://sphinx-doc.org/', None),
'sphinx': ('http://www.sphinx-doc.org/en/stable/', None),
'xarray': ('http://xarray.pydata.org/en/stable/', None),
'cartopy': ('http://scitools.org.uk/cartopy/docs/latest/', None),
'mpl_toolkits': ('http://matplotlib.org/basemap/', None),
Expand Down
23 changes: 23 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,29 @@ or from the source on github_ via::
.. _github: https://github.com/Chilipp/docrep


.. note::

When using docrep in python 2.7, there is to mention that the ``__doc__``
attribute of classes is not writable, so something like

.. ipython::

In [12]: @docstrings
....: class SomeClass(object):
....: """An awesome class
....:
....: Parameters
....: ----------
....: %(repeated.parameters)s
....: """

would raise an error. There are several workarounds (see
`the issue on github <https://github.com/Chilipp/docrep/issues/5#>`__) but the
default for python 2.7 is, to simply not modify class docstrings. You
can, however, change this behaviour using the
:attr:`DocstringProcessor.python2_classes` attribute.


API Reference
=============

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def readme():


setup(name='docrep',
version='0.2.2',
version='0.2.3',
description='Python package for docstring repetition',
long_description=readme(),
classifiers=[
Expand All @@ -24,6 +24,7 @@ def readme():
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Operating System :: OS Independent',
],
keywords='docstrings docs docstring napoleon numpy reStructured text',
Expand Down
18 changes: 18 additions & 0 deletions tests/test_docrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,24 @@ def test_delete_kwargs(self):
with self.assertWarns(UserWarning):
self.ds.delete_kwargs('test')

@unittest.skipIf(not six.PY2, "Only implemented for python 2.7")
def test_py2_classes(self):
"""Test the handling of classes in python 2.7"""
# this should work
@self.ds
class Test(object):
"""docs"""
# this should not
self.ds.python2_classes = 'raise'
try:
@self.ds
class Test2(object):
"""docs"""
except AttributeError:
pass
else:
self.fail("Should have raised AttributeError!")


if __name__ == '__main__':
unittest.main()

0 comments on commit 5154fcd

Please sign in to comment.