diff --git a/docinstance/content/base.py b/docinstance/content/base.py deleted file mode 100644 index f1b0e39..0000000 --- a/docinstance/content/base.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Base class for docstring contents.""" - - -class DocContent: - """Base class for all content of a docstring. - - Any special construct that goes inside a docstring (i.e. not a string) should be a child of this - class. - - Methods - ------- - __init__(self) - Initialize. - __eq__(self, other) - Return True if other is DocContent instance with the same contents. False otherwise. - __ne__(self, other) - Return False if other is DocContent instance with the same contents. True otherwise. - make_numpy_docstring(self, width, indent_level, tabsize) - Return the docstring of the content in numpy style. - make_google_docstring(self, width, indent_level, tabsize) - Return the docstring of the content in google style. - make_rst_docstring(self, width, indent_level, tabsize) - Return the docstring of the content in rst style. - - """ - - def __init__(self): - """Initialize. - - Raises - ------ - NotImplementedError - Always. - - """ - raise NotImplementedError - - def __eq__(self, other): - """Return True if other is DocContent instance with the same contents. False otherwise. - - Parameters - ---------- - other : DocContent - - Returns - ------- - bool - - """ - return isinstance(other, DocContent) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - """Return False if other is DocContent instance with the same contents. True otherwise. - - Parameters - ---------- - other : DocContent - - Returns - ------- - bool - - Notes - ----- - The behaviour of __ne__ changed form Python 2 to Python 3. In Python 3, there is a default - behaviour of __ne__ when __eq__ returns NotImplemented, which is the default behaviour for - __eq__. Special care needs to be taken when only __ne__ is defined. However, since we define - the __eq__ here, we don't need to be too careful. See - https://stackoverflow.com/questions/4352244/python-should-i-implement-ne-operator-based-on-eq/50661674#50661674 - for more details. - - """ - return not self == other - - def make_numpy_docstring(self, width, indent_level, tabsize): - """Return the docstring of the content in numpy style. - - Parameters - ---------- - width : int - Maximum number of characters allowed in a line. - indent_level : int - Number of indents (tabs) that are needed for the docstring. - tabsize : int - Number of spaces that corresponds to a tab. - - Returns - ------- - content_docstring : str - Docstring of the given content in numpy style. - - Raises - ------ - NotImplementedError - Always. - - """ - raise NotImplementedError - - def make_google_docstring(self, width, indent_level, tabsize): - """Return the docstring of the content in google style. - - Parameters - ---------- - width : int - Maximum number of characters allowed in a line. - indent_level : int - Number of indents (tabs) that are needed for the docstring. - tabsize : int - Number of spaces that corresponds to a tab. - - Returns - ------- - content_docstring : str - Docstring of the given content in google style. - - Raises - ------ - NotImplementedError - Always. - - """ - raise NotImplementedError - - def make_rst_docstring(self, width, indent_level, tabsize): - """Return the docstring of the content in rst style. - - Parameters - ---------- - width : int - Maximum number of characters allowed in a line. - indent_level : int - Number of indents (tabs) that are needed for the docstring. - tabsize : int - Number of spaces that corresponds to a tab. - - Returns - ------- - content_docstring : str - Docstring of the given content in rst style. - - Raises - ------ - NotImplementedError - Always. - - """ - raise NotImplementedError diff --git a/docinstance/content/test/test_description.py b/docinstance/content/test/test_description.py deleted file mode 100644 index e0992c8..0000000 --- a/docinstance/content/test/test_description.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Test docinstance.content.description.""" -import pytest -from docinstance.content.description import DocDescription - - -def test_init(): - """Test DocDescription.__init__.""" - with pytest.raises(TypeError): - DocDescription([]) - with pytest.raises(TypeError): - DocDescription(2) - with pytest.raises(TypeError): - DocDescription('test', signature=1) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=1) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=[1]) - with pytest.raises(TypeError): - DocDescription('test', signature='', types={str}) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=[str, 1]) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=str, descs=1) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=str, descs=[1]) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=str, descs={'1'}) - with pytest.raises(TypeError): - DocDescription('test', signature='', types=str, descs=['1', 2]) - - test = DocDescription('test') - assert test.name == 'test' - assert test.signature == '' - assert test.types == [] - assert test.descs == [] - test = DocDescription('test', signature='1') - assert test.signature == '1' - test = DocDescription('test', types=str) - assert test.types == [str] - test = DocDescription('test', types=(str,)) - assert test.types == [str] - test = DocDescription('test', descs='2') - assert test.descs == ['2'] - test = DocDescription('test', descs=('2',)) - assert test.descs == ['2'] - - -def test_types_str(): - """Test DocDescription.types_str.""" - test = DocDescription('test', types=[str, int, 'list of str']) - assert test.types_str == ['str', 'int', 'list of str'] - - -def test_make_numpy_docstring(): - """Test DocDescription.make_numpy_docstring.""" - # no type - test = DocDescription('var_name', descs=['hello']) - assert test.make_numpy_docstring(9, 0, 4) == 'var_name\n hello\n' - # one type - test = DocDescription('var_name', types=str, descs=['hello']) - with pytest.raises(ValueError): - test.make_numpy_docstring(13, 0, 4) - with pytest.raises(ValueError): - test.make_numpy_docstring(14, 1, 2) - assert test.make_numpy_docstring(14, 0, 4) == 'var_name : str\n hello\n' - assert test.make_numpy_docstring(16, 1, 2) == ' var_name : str\n hello\n' - # multiple types - test = DocDescription('var_name', types=[str, int, bool], descs=['hello']) - with pytest.raises(ValueError): - test.make_numpy_docstring(15, 0, 4) - with pytest.raises(ValueError): - test.make_numpy_docstring(16, 1, 2) - # NOTE: following raises an error even though width is big enough for 'var_name : {str,' because - # the `utils.wrap` complains - with pytest.raises(ValueError): - test.make_numpy_docstring(16, 0, 4) - assert test.make_numpy_docstring(27, 0, 4) == 'var_name : {str, int, bool}\n hello\n' - assert (test.make_numpy_docstring(26, 0, 4) == - 'var_name : {str, int,\n bool}\n hello\n') - assert (test.make_numpy_docstring(27, 1, 2) == - ' var_name : {str, int,\n bool}\n hello\n') - assert (test.make_numpy_docstring(17, 0, 4) == - 'var_name : {str,\n int,\n bool}\n hello\n') - # signature does nothing - test2 = DocDescription('var_name', signature='(a, b, c)', types=[str, int, bool], - descs=['hello']) - assert test.make_numpy_docstring(17, 0, 4) == test2.make_numpy_docstring(17, 0, 4) - # multiple paragraphs - test = DocDescription('var_name', types=[str, int, bool], - descs=['description 1', 'description 2', 'description 3']) - assert (test.make_numpy_docstring(27, 0, 4) == - 'var_name : {str, int, bool}\n description 1\n description 2\n' - ' description 3\n') - - -def test_make_numpy_docstring_signature(): - """Test DocDescription.make_numpy_docstring_signature.""" - test = DocDescription('var_name', signature='(a, b, c)', types=[str, int, bool], - descs=['hello']) - assert (test.make_numpy_docstring_signature(36, 0, 4) == - 'var_name(a, b, c) : {str, int, bool}\n hello\n') - assert (test.make_numpy_docstring_signature(26, 0, 4) == - 'var_name(a, b, c) : {str,\n int,\n bool}\n' - ' hello\n') - - -def test_make_google_docstring(): - """Test DocDescription.make_google_docstring.""" - # no type, desc - test = DocDescription('var_name', descs=['hello']) - assert test.make_google_docstring(15, 0, 4) == 'var_name: hello\n' - # no type, no descs - test = DocDescription('var_name') - assert test.make_google_docstring(9, 0, 4) == 'var_name:\n' - with pytest.raises(ValueError): - test.make_google_docstring(8, 0, 4) - # one type, no descs - test = DocDescription('var_name', types=str) - assert test.make_google_docstring(22, 0, 4) == 'var_name (:obj:`str`):\n' - with pytest.raises(ValueError): - test.make_google_docstring(21, 0, 4) - # one type, no descs - test = DocDescription('var_name', types='str') - assert test.make_google_docstring(15, 0, 4) == 'var_name (str):\n' - with pytest.raises(ValueError): - test.make_google_docstring(14, 0, 4) - # many types, no descs - test = DocDescription('var_name', types=['str', int]) - assert test.make_google_docstring(22, 0, 4) == ('var_name (str,\n' - ' :obj:`int`):\n') - with pytest.raises(ValueError): - test.make_google_docstring(21, 0, 4) - assert test.make_google_docstring(23, 1, 1) == (' var_name (str,\n' - ' :obj:`int`):\n') - # one type, desc - test = DocDescription('var_name', types=str, descs=['hello']) - assert test.make_google_docstring(22, 0, 4) == 'var_name (:obj:`str`):\n hello\n' - # FIXME - assert test.make_google_docstring(24, 1, 2) == ' var_name (:obj:`str`):\n hello\n' - # multiple types - test = DocDescription('var_name', types=[str, int, bool], descs=['hello']) - with pytest.raises(ValueError): - test.make_google_docstring(22, 0, 4) - with pytest.raises(ValueError): - test.make_google_docstring(24, 1, 2) - assert test.make_google_docstring(23, 0, 4) == ('var_name (:obj:`str`,\n' - ' :obj:`int`,\n' - ' :obj:`bool`):\n' - ' hello\n') - assert test.make_google_docstring(26, 1, 2) == (' var_name (:obj:`str`,\n' - ' :obj:`int`,\n' - ' :obj:`bool`):\n' - ' hello\n') - assert (test.make_google_docstring(35, 1, 2) == - (' var_name (:obj:`str`, :obj:`int`,\n' - ' :obj:`bool`): hello\n')) - # signature does nothing - test2 = DocDescription('var_name', signature='(a, b, c)', types=[str, int, bool], - descs=['hello']) - assert test.make_google_docstring(23, 0, 4) == test2.make_google_docstring(23, 0, 4) - # multiple paragraphs - test = DocDescription('var_name', types=[str, int, bool], - descs=['description 1', 'description 2', 'description 3']) - assert test.make_google_docstring(26, 0, 2) == ('var_name (:obj:`str`,\n' - ' :obj:`int`,\n' - ' :obj:`bool`):\n' - ' description 1\n' - ' description 2\n' - ' description 3\n') - - -def test_make_rst_docstring(): - """Test DocDescription.make_rst_docstring.""" - # only name - test = DocDescription('var_name') - assert test.make_rst_docstring(16, 0, 1) == ':param var_name:\n' - assert test.make_rst_docstring(17, 1, 1) == ' :param var_name:\n' - with pytest.raises(ValueError): - test.make_rst_docstring(15, 0, 4) - # name + desc - test = DocDescription('var_name', descs='hello') - assert test.make_rst_docstring(22, 0, 1) == ':param var_name: hello\n' - assert test.make_rst_docstring(21, 0, 1) == (':param var_name:\n' - ' hello\n') - assert test.make_rst_docstring(21, 0, 4) == (':param var_name:\n' - ' hello\n') - test = DocDescription('var_name', descs=['hello my name is', 'Example 2.']) - assert test.make_rst_docstring(25, 0, 4) == (':param var_name: hello my\n' - ' name is\n' - ' Example 2.\n') - # name + type - test = DocDescription('var_name', types=['str', int]) - assert test.make_rst_docstring(20, 0, 2) == (':param var_name:\n' - ':type var_name: str,\n' - ' :obj:`int`\n') - # name + desc + type - test = DocDescription('var_name', types=['str', int], descs=['Example 1.', 'Example 2.']) - assert test.make_rst_docstring(27, 0, 4) == (':param var_name: Example 1.\n' - ' Example 2.\n' - ':type var_name: str,\n' - ' :obj:`int`\n') diff --git a/docinstance/content/test/test_equation.py b/docinstance/content/test/test_equation.py deleted file mode 100644 index 4f8fc0c..0000000 --- a/docinstance/content/test/test_equation.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Test docinstance.content.equation.""" -import pytest -from docinstance.content.equation import DocEquation - - -def test_init(): - """Test DocEquation.__init__.""" - with pytest.raises(TypeError): - DocEquation(1) - with pytest.raises(TypeError): - DocEquation(['x + 1', 'y + 2']) - test = DocEquation('a + b = 2') - assert test.equations == ['a + b = 2'] - test = DocEquation('a + b &= 2\\\\\nc + d &= 3') - assert test.equations == ['a + b &= 2\\\\', 'c + d &= 3'] - test = DocEquation('a + b &= 2\\\\\nc + d &= 3\\\\\n') - assert test.equations == ['a + b &= 2\\\\', 'c + d &= 3\\\\'] - test = DocEquation('a + b &= 2\\\\\nc + d &= 3\\\\') - assert test.equations == ['a + b &= 2\\\\', 'c + d &= 3\\\\'] - - -def test_make_numpy_docstring(): - """Test DocEquation.make_numpy_docstring.""" - test = DocEquation('a + b = 2') - assert test.make_numpy_docstring(19, 0, 4) == '.. math:: a + b = 2\n\n' - assert test.make_numpy_docstring(18, 0, 4) == '.. math::\n\n a + b = 2\n\n' - with pytest.raises(ValueError): - test.make_numpy_docstring(8, 0, 4) - test = DocEquation('a + b &= 2\\\\\nc + d &= 3\\\\\n') - assert (test.make_numpy_docstring(18, 0, 4) == - '.. math::\n\n' - ' a + b &= 2\\\\\n' - ' c + d &= 3\\\\\n\n') - test = DocEquation('a + b &= 2\\\\\nc + d &= 3\n') - assert (test.make_numpy_docstring(18, 0, 4) == - '.. math::\n\n' - ' a + b &= 2\\\\\n' - ' c + d &= 3\n\n') - test = DocEquation('a + b &= 2\nc + d &= 3\n') - assert (test.make_numpy_docstring(18, 0, 4) == - '.. math::\n\n' - ' a + b &= 2\n' - ' c + d &= 3\n\n') diff --git a/docinstance/content/test/test_section.py b/docinstance/content/test/test_section.py deleted file mode 100644 index 833865e..0000000 --- a/docinstance/content/test/test_section.py +++ /dev/null @@ -1,313 +0,0 @@ -"""Test docinstance.content.section.""" -import pytest -from docinstance.content.section import (DocSection, Summary, ExtendedSummary, Parameters, - Attributes, Methods, Returns, Yields, OtherParameters, - Raises, Warns, Warnings, SeeAlso, Notes, References, - Examples) -from docinstance.content.description import DocDescription - - -def test_init(): - """Test DocSection.__init__.""" - with pytest.raises(TypeError): - DocSection(1, '') - with pytest.raises(TypeError): - DocSection(['1'], '') - with pytest.raises(TypeError): - DocSection('1', 1) - with pytest.raises(TypeError): - DocSection('1', {'1'}) - with pytest.raises(TypeError): - DocSection('1', ['1', DocDescription('test')]) - with pytest.raises(TypeError): - DocSection('1', [DocDescription('test'), '1']) - - test = DocSection('header name', 'hello') - assert test.header == 'header name' - assert test.contents == ['hello'] - test = DocSection('header name', ['hello', 'i am']) - assert test.header == 'header name' - assert test.contents == ['hello', 'i am'] - doc1 = DocDescription('hello') - doc2 = DocDescription('i am') - test = DocSection('header name', doc1) - assert test.header == 'header name' - assert test.contents == [doc1] - test = DocSection('header name', [doc1, doc2]) - assert test.header == 'header name' - assert test.contents == [doc1, doc2] - - -def test_make_numpy_docstring(): - """Test DocSection.make_numpy_docstring.""" - # string content - test = DocSection('header name', 'hello') - assert test.make_numpy_docstring(11, 0, 4) == 'Header Name\n-----------\nhello\n\n' - assert test.make_numpy_docstring(11, 0, 4) == 'Header Name\n-----------\nhello\n\n' - # multiple string contents - test = DocSection('header name', ['hello', 'i am']) - assert test.make_numpy_docstring(11, 0, 4) == 'Header Name\n-----------\nhello\n\ni am\n\n' - # doc description - test = DocSection('header name', DocDescription('var_name', types=str, descs='Example.')) - assert (test.make_numpy_docstring(20, 0, 4) == - 'Header Name\n-----------\nvar_name : str\n Example.\n\n') - # multiple doc descriptions - test = DocSection('header name', [DocDescription('var_name1', types=str, descs='Example 1.'), - DocDescription('var_name2', types=int, descs='Example 2.')]) - assert (test.make_numpy_docstring(20, 0, 4) == - 'Header Name\n-----------\nvar_name1 : str\n Example 1.\nvar_name2 : int\n' - ' Example 2.\n\n') - # signature does nothing - test = DocSection('header name', - DocDescription('var_name', signature='(a, b)', types=str, descs='Example.')) - assert (test.make_numpy_docstring(20, 0, 4) == - 'Header Name\n-----------\nvar_name : str\n Example.\n\n') - - -def test_make_numpy_docstring_signature(): - """Test DocSection.make_numpy_docstring_signature.""" - test = DocSection('header name', - DocDescription('var_name', signature='(a, b)', types=str, descs='Example.')) - assert (test.make_numpy_docstring_signature(20, 0, 4) == - 'Header Name\n-----------\nvar_name(a, b) : str\n Example.\n\n') - - -def test_make_google_docstring(): - """Test DocSection.make_google_docstring.""" - with pytest.raises(ValueError): - test = DocSection('quite long header name', '') - test.make_google_docstring(10, 0, 4) - # no header - test = DocSection('', 'Some text.') - assert test.make_google_docstring(10, 0, 4) == ('Some text.\n\n') - # one docdescription - test = DocSection('header name', - DocDescription('var_name', signature='(a, b)', types=str, descs='Example.')) - assert test.make_google_docstring(35, 0, 4) == ('Header Name:\n' - ' var_name (:obj:`str`): Example.\n\n') - # multiple docdescription - test = DocSection('header name', - [DocDescription('var1', signature='(a, b)', types=str, descs='Example1.'), - DocDescription('var2', signature='(c)', types='int', descs='Example2.')]) - assert test.make_google_docstring(35, 0, 4) == ('Header Name:\n' - ' var1 (:obj:`str`): Example1.\n' - ' var2 (int): Example2.\n\n') - # one string - test = DocSection('header name', 'Some text.') - assert test.make_google_docstring(14, 0, 4) == ('Header Name:\n' - ' Some text.\n\n') - assert test.make_google_docstring(13, 0, 4) == ('Header Name:\n' - ' Some\n' - ' text.\n\n') - # multiple string - test = DocSection('header name', ['Some text.', 'Another text.']) - assert test.make_google_docstring(17, 0, 4) == ('Header Name:\n' - ' Some text.\n\n' - ' Another text.\n\n') - assert test.make_google_docstring(14, 0, 4) == ('Header Name:\n' - ' Some text.\n\n' - ' Another\n' - ' text.\n\n') - assert test.make_google_docstring(13, 0, 4) == ('Header Name:\n' - ' Some\n' - ' text.\n\n' - ' Another\n' - ' text.\n\n') - - -def test_make_rst_docstring(): - """Test DocSection.make_rst_docstring.""" - # no header - test = DocSection('', 'Some text.') - assert test.make_rst_docstring(10, 0, 4) == ('Some text.\n\n') - # normal header, one docdescription - test = DocSection('header name', - DocDescription('var_name', signature='(a, b)', types=str, descs='Example.')) - assert test.make_rst_docstring(35, 0, 4) == (':Header Name:\n\n' - ':param var_name: Example.\n' - ':type var_name: :obj:`str`\n\n') - # normal header, multiple docdescription - test = DocSection('header name', - [DocDescription('var1', signature='(a, b)', types=str, descs='Example1.'), - DocDescription('var2', signature='(c)', types='int', descs='Example2.')]) - assert test.make_rst_docstring(35, 0, 4) == (':Header Name:\n\n' - ':param var1: Example1.\n' - ':type var1: :obj:`str`\n' - ':param var2: Example2.\n' - ':type var2: int\n\n') - # normal header, one string - test = DocSection('header name', 'Some text.') - assert test.make_rst_docstring(13, 0, 4) == (':Header Name:\n\n' - 'Some text.\n\n') - # normal header, multiple string - test = DocSection('header name', ['Some text.', 'Another text.']) - assert test.make_rst_docstring(13, 0, 4) == (':Header Name:\n\n' - 'Some text.\n\n' - 'Another text.\n\n') - - # special header, doc description - test = DocSection('see also', - [DocDescription('var1', signature='(a, b)', types=str, descs='Example1.'), - DocDescription('var2', signature='(c)', types='int', descs='Example2.')]) - assert test.make_rst_docstring(35, 0, 4) == ('.. seealso::\n' - ' :param var1: Example1.\n' - ' :type var1: :obj:`str`\n' - ' :param var2: Example2.\n' - ' :type var2: int\n\n') - # special header, string - test = DocSection('to do', ['Example 1, something.', 'Example 2.']) - assert test.make_rst_docstring(20, 0, 4) == ('.. todo:: Example 1,\n' - ' something.\n' - ' Example 2.\n\n') - - -def test_section_summary_init(): - """Test Summary.__init__.""" - with pytest.raises(TypeError): - Summary(['summary']) - with pytest.raises(TypeError): - Summary(DocDescription('something')) - test = Summary('very very long summary') - assert test.header == '' - assert test.contents == ['very very long summary'] - - -def test_section_summary_make_docstring(): - """Test Summary.make_docstring.""" - test = Summary('very very long summary') - assert (test.make_docstring(25, 0, 4, summary_only=True, special=False) == - 'very very long summary\n\n') - assert (test.make_docstring(24, 0, 4, summary_only=True, special=False) == - '\nvery very long summary\n\n') - assert (test.make_docstring(28, 0, 4, summary_only=True, special=False) == - 'very very long summary') - assert (test.make_docstring(25, 0, 4, summary_only=False, special=False) == - 'very very long summary\n\n') - assert (test.make_docstring(24, 0, 4, summary_only=False, special=False) == - '\nvery very long summary\n\n') - assert (test.make_docstring(28, 0, 4, summary_only=False, special=False) == - 'very very long summary\n\n') - - assert not (test.make_docstring(25, 0, 4, summary_only=True, special=True) == - 'very very long summary\n\n') - assert (test.make_docstring(26, 0, 4, summary_only=True, special=True) == - 'very very long summary\n\n') - assert not (test.make_docstring(28, 0, 4, summary_only=True, special=True) == - 'very very long summary') - assert (test.make_docstring(29, 0, 4, summary_only=True, special=True) == - 'very very long summary') - - test = Summary('very very very very very very long summary') - with pytest.raises(ValueError): - test.make_docstring(30, 0, 4) - - -def test_section_extended_summary(): - """Test ExtendedSummary.__init__.""" - test = ExtendedSummary('This is an extended summary.') - assert test.header == '' - assert test.contents == ['This is an extended summary.'] - - -def test_section_parameters(): - """Test Parameters.__init__.""" - desc1 = DocDescription('a', types=str, descs='Example 1.') - desc2 = DocDescription('b', types=int, descs='Example 2.') - test = Parameters([desc1, desc2]) - assert test.header == 'parameters' - assert test.contents == [desc1, desc2] - - -def test_section_attributes(): - """Test Attributes.__init__.""" - desc1 = DocDescription('a', types=str, descs='Example 1.') - desc2 = DocDescription('b', types=int, descs='Example 2.') - test = Attributes([desc1, desc2]) - assert test.header == 'attributes' - assert test.contents == [desc1, desc2] - - -def test_section_methods(): - """Test Methods.__init__.""" - desc1 = DocDescription('f', signature='(x, y)', types=int, descs='Example 1.') - desc2 = DocDescription('g', signature='(z=1)', types=int, descs='Example 2.') - test = Methods([desc1, desc2]) - assert test.header == 'methods' - assert test.contents == [desc1, desc2] - - -def test_section_returns(): - """Test Returns.__init__.""" - desc1 = DocDescription('a', types=int, descs='Example 1.') - desc2 = DocDescription('b', types=str, descs='Example 2.') - test = Returns([desc1, desc2]) - assert test.header == 'returns' - assert test.contents == [desc1, desc2] - - -def test_section_yields(): - """Test Yields.__init__.""" - desc = DocDescription('a', types=int, descs='Example 1.') - test = Yields(desc) - assert test.header == 'yields' - assert test.contents == [desc] - - -def test_section_otherparameters(): - """Test OtherParameters.__init__.""" - desc1 = DocDescription('a', types=int, descs='Example 1.') - desc2 = DocDescription('b', types=str, descs='Example 2.') - test = OtherParameters([desc1, desc2]) - assert test.header == 'other parameters' - assert test.contents == [desc1, desc2] - - -def test_section_raises(): - """Test Raises.__init__.""" - desc = DocDescription('TypeError', descs='If something.') - test = Raises(desc) - assert test.header == 'raises' - assert test.contents == [desc] - - -def test_section_warns(): - """Test Warns.__init__.""" - desc = DocDescription('Warning', descs='If something.') - test = Warns(desc) - assert test.header == 'warns' - assert test.contents == [desc] - - -def test_section_warnings(): - """Test Warnings.__init__.""" - test = Warnings('Not to be used.') - assert test.header == 'warnings' - assert test.contents == ['Not to be used.'] - - -def test_section_seealso(): - """Test SeeAlso.__init__.""" - test = SeeAlso('Some other code.') - assert test.header == 'see also' - assert test.contents == ['Some other code.'] - - -def test_section_notes(): - """Test Notes.__init__.""" - test = Notes('Some comment.') - assert test.header == 'notes' - assert test.contents == ['Some comment.'] - - -def test_section_references(): - """Test References.__init__.""" - test = References('Some reference.') - assert test.header == 'references' - assert test.contents == ['Some reference.'] - - -def test_section_examples(): - """Test Examples.__init__.""" - test = Examples('Some example.') - assert test.header == 'examples' - assert test.contents == ['Some example.'] diff --git a/docinstance/parser/test/test_latex.py b/docinstance/parser/test/test_latex.py deleted file mode 100644 index ffca320..0000000 --- a/docinstance/parser/test/test_latex.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Tests for docinstance.parser.latex.""" -from docinstance.parser.latex import is_math, parse_equation -from docinstance.content.equation import DocEquation - - -def test_is_math(): - """Test docinstance.parser.numpy.is_math.""" - assert is_math('.. math::\n\n x&=2\\\\\n &=3') - assert is_math('.. math::\n\n x&=2\\\\\n &=3\n') - assert is_math('\n.. math::\n\n x&=2\\\\\n &=3\n\n\n') - assert not is_math('x\n.. math::\n\n x&=2\\\\\n &=3\n\n\n') - assert is_math('.. math::\n\n x=2') - assert is_math(' .. math::\n\n x=2') - assert is_math('\n .. math::\n\n x=2') - - -def test_parse_equation(): - """Test docinstance.parser.numpy.parse_equation.""" - test = parse_equation('.. math::\n\n x &= 2\\\\\n &= 3\n') - assert len(test) == 1 - assert isinstance(test[0], DocEquation) - assert test[0].equations == ['x &= 2\\\\', '&= 3'] - - test = parse_equation('x\n.. math::\n\n x &= 2\\\\\n &= 3\n') - assert len(test) == 2 - assert test[0] == 'x' - assert isinstance(test[1], DocEquation) - assert test[1].equations == ['x &= 2\\\\', '&= 3'] - - test = parse_equation('x\n.. math::\n\n x &= 2\\\\\n &= 3\n\n\n') - assert len(test) == 2 - assert test[0] == 'x' - assert isinstance(test[1], DocEquation) - assert test[1].equations == ['x &= 2\\\\', '&= 3'] - - test = parse_equation('x\n.. math::\n\n x &= 2\\\\\n &= 3\n\ny') - assert len(test) == 3 - assert test[0] == 'x' - assert isinstance(test[1], DocEquation) - assert test[1].equations == ['x &= 2\\\\', '&= 3'] - assert test[2] == 'y' diff --git a/docinstance/parser/test/test_numpy.py b/docinstance/parser/test/test_numpy.py deleted file mode 100644 index 7f35b73..0000000 --- a/docinstance/parser/test/test_numpy.py +++ /dev/null @@ -1,283 +0,0 @@ -"""Tests for docinstance.parser.numpy.""" -import pytest -from docinstance.docstring import Docstring -from docinstance.content.section import (DocSection, Summary, ExtendedSummary, Parameters) -from docinstance.content.description import DocDescription -from docinstance.content.equation import DocEquation -from docinstance.parser.numpy import parse_numpy - - -def test_compare_docinstances(): - """Test docinstance.parser.test.test_numpy.""" - doc1 = Docstring(['summary', - DocSection('section1', ['content1', 'content2']), - DocSection('section2', [DocDescription('name1', '(a, b)', str, - ['desc1', 'desc2']), - DocDescription('name2', '(c, d)', int, - ['desc3', DocEquation('desc4')])])]) - doc2 = Docstring(['summary', - DocSection('section1', ['content1', 'content2']), - DocSection('section2', [DocDescription('name1', '(a, b)', str, - ['desc1', 'desc2']), - DocDescription('name2', '(c, d)', int, - ['desc3', DocEquation('desc4')])])]) - assert doc1 != 1 - assert Docstring(['a', 'b']) != Docstring(['a', 'b', 'c']) - assert Docstring(['a', 'b']) != Docstring(['a', DocSection('x', 'b')]) - assert Docstring(DocSection('a', 'b')) != Docstring(DocSection('a', ['b', 'c'])) - assert Docstring(DocSection('a', 'b')) != Docstring(DocSection('a', 'c')) - assert (Docstring(DocSection('a', DocDescription('x', 'y', 'z', 'k'))) != - Docstring(DocSection('a', DocDescription('1', 'y', 'z', 'k')))) - assert (Docstring(DocSection('a', DocDescription('x', 'y', 'z', 'k'))) != - Docstring(DocSection('a', DocDescription('x', '1', 'z', 'k')))) - assert (Docstring(DocSection('a', DocDescription('x', 'y', 'z', 'k'))) != - Docstring(DocSection('a', DocDescription('x', 'y', '1', 'k')))) - assert (Docstring(DocSection('a', DocDescription('x', 'y', 'z', 'k'))) != - Docstring(DocSection('a', DocDescription('x', 'y', 'z', '1')))) - assert (Docstring(DocSection('a', DocDescription('x', 'y', 'z', DocEquation('k')))) != - Docstring(DocSection('a', DocDescription('x', 'y', 'z', DocEquation('1'))))) - assert doc1.__dict__ == doc2.__dict__ - - -def test_parse_numpy(): - """Tests docinstance.numpy.parse_numpy.""" - # monkeypatch equality for Docstring - Docstring.__eq__ = lambda self, other: self.__dict__ == other.__dict__ - # summary - docstring = 'summary' - assert parse_numpy(docstring) == Docstring(Summary('summary')) - docstring = 'summary\n' - assert parse_numpy(docstring) == Docstring(Summary('summary')) - docstring = '\nsummary\n' - assert parse_numpy(docstring) == Docstring(Summary('summary')) - docstring = ' """summary\n """' - assert parse_numpy(docstring, contains_quotes=True) == Docstring(Summary('summary')) - # FIXME: this should raise an error - docstring = '\n\nsummary\n' - assert parse_numpy(docstring), Docstring([Summary('summary')]) - docstring = '"""\n\nsummary\n"""' - with pytest.raises(ValueError): - parse_numpy(docstring, contains_quotes=True) - docstring = ' """\n\n summary\n """' - with pytest.raises(ValueError): - parse_numpy(docstring, contains_quotes=True) - docstring = 'summary\na' - with pytest.raises(ValueError): - parse_numpy(docstring) - - # extended - docstring = 'summary\n\nblock1\n\nblock2' - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(['block1', 'block2'])])) - docstring = '\nsummary\n\nblock1\n\nblock2' - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(['block1', 'block2'])])) - docstring = '\nsummary\n\n\n\nblock2\n\n' - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(['block2'])])) - # FIXME: is this a bug? - docstring = '\n\nsummary\n\nblock1\n\nblock2' - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(['block1', 'block2'])])) - # extended + headers - docstring = 'summary\n\nblock1\n\nblock2\n\nheader\n------\nstuff' - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(['block1', 'block2']), - DocSection('header', 'stuff')])) - - # header with bad divider (-----) - docstring = 'summary\n\nblock1\n\nblock2\n\nheader1\n--\nstuff\n\n' - with pytest.raises(ValueError): - parse_numpy(docstring) - - for header in ['parameters', 'attributes', 'methods', 'returns', 'yields', 'raises', - 'other parameters', 'see also']: - # name + multiple descriptions - docstring = ('summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc\n description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(['block1', 'block2']), - DocSection(header, - DocDescription('abc', - descs=['description1.', 'description2.']))])) - # name + types + multiple descriptions - docstring = ('summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : str\n description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(['block1', 'block2']), - DocSection(header, - DocDescription('abc', types='str', - descs=['description1.', 'description2.']))])) - docstring = ('summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : {{str, int}}\n' - ' description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(['block1', 'block2']), - DocSection(header, - DocDescription('abc', types=['str', 'int'], - descs=['description1.', 'description2.']))])) - # name + signature + multiple descriptions - docstring = ('summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y)\n description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(['block1', 'block2']), - DocSection(header, - DocDescription('abc', signature='(x, y)', - descs=['description1.', 'description2.']))])) - # name + types + signature + multiple descriptions - docstring = ('summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y): str\n description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(['block1', 'block2']), - DocSection(header, - DocDescription('abc', types='str', signature='(x, y)', - descs=['description1.', 'description2.']))])) - # name + types + signature + multiple descriptions - extended summary - docstring = ('summary\n\n{0}\n{1}\nabc(x, y): str\n description1.\n' - ' description2.'.format(header.title(), '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - DocSection(header, - DocDescription('abc', types='str', signature='(x, y)', - descs=['description1.', 'description2.']))])) - # name + types - docstring = ('summary\n\n{0}\n{1}\nabc: str\ndef: int'.format(header.title(), - '-'*len(header))) - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), DocSection(header, - [DocDescription('abc', types='str'), - DocDescription('def', types='int')])])) - - -def test_parse_numpy_raw(): - """Test pydocstring.numpy_docstring.parse_numpy with raw strings.""" - docstring = '"""summary\n\nextended"""' - assert (parse_numpy(docstring, contains_quotes=True) == - Docstring([Summary('summary'), ExtendedSummary('extended')])) - docstring = 'r"""summary\n\nextended"""' - with pytest.raises(NotImplementedError): - parse_numpy(docstring, contains_quotes=True) - - -def test_parse_numpy_self(): - """Test pydocstring.numpy_docstring.parse_numpy using itself as an example.""" - docstring = parse_numpy.__doc__ - # summary - assert (parse_numpy(docstring, contains_quotes=False).sections[0].contents[0] == - 'Parse a docstring in numpy format into a Docstring instance.') - # extended - assert (parse_numpy(docstring, contains_quotes=False).sections[1].contents == - ['Multiple descriptions of the indented information (e.g. parameters, ' - 'attributes, methods, returns, yields, raises, see also) are ' - 'distinguished from one another with a period. If the period is not ' - 'present, then the description is assumed to be a multiline ' - 'description.']) - # parameters - assert (parse_numpy(docstring, contains_quotes=False).sections[2].header == - 'parameters') - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].name == - 'docstring') - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].types == - ['str']) - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].descs == - ['Numpy docstring.']) - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].name == - 'contains_quotes') - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].types == - ['bool']) - assert (parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].descs == - [r'True if docstring contains \"\"\" or \'\'\'.']) - # returns - assert (parse_numpy(docstring, contains_quotes=False).sections[3].header == - 'returns') - assert (parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].name == - 'docstring') - assert (parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].types == - ['Docstring']) - assert (parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].descs == - ['Instance of Docstring that contains the necessary information.']) - # raises - assert (parse_numpy(docstring, contains_quotes=False).sections[4].header == - 'raises') - assert (parse_numpy(docstring, contains_quotes=False).sections[4].contents[0].name == - 'ValueError') - assert (parse_numpy(docstring, contains_quotes=False).sections[4].contents[0].descs == - ['If summary is not in the first or second line.', - 'If summary is now followed with a blank line.', - 'If number of \'-\' does not match the number of characters in the header.', - 'If given entry of the tabbed information (parameters, attributes, methods, returns, ' - 'yields, raises, see also) had an unexpected pattern.']) - assert (parse_numpy(docstring, contains_quotes=False).sections[4].contents[1].name == - 'NotImplementedError') - assert (parse_numpy(docstring, contains_quotes=False).sections[4].contents[1].descs == - [r'If quotes corresponds to a raw string, i.e. r\"\"\".']) - - -def test_parse_numpy_equations(): - """Test pydocstring.numpy_docstring.parse_numpy with equations.""" - # monkeypatch equality for Docstring - Docstring.__eq__ = lambda self, other: self.__dict__ == other.__dict__ - # equation in extended - docstring = ('summary\n\n.. math::\n\n \\frac{1}{2}') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), ExtendedSummary(DocEquation('\\frac{1}{2}'))])) - docstring = ('summary\n\n' - '.. math::\n\n' - ' x &= 2\\\\\n' - ' &= y\\\\\n') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(DocEquation('x &= 2\\\\\n&= y\\\\'))])) - docstring = ('summary\n\n' - '.. math::\n\n' - ' x &= 2\\\\\n' - ' &= y\\\\\n' - 'Parameters\n' - '----------\n' - 'a') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - ExtendedSummary(DocEquation('x &= 2\\\\\n&= y\\\\')), - Parameters(DocDescription('a'))])) - - # equation in parameter - # single line equation - docstring = ('summary\n\nParameters\n----------\na : float\n .. math::\n\n ' - ' \\frac{1}{2}') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - Parameters(DocDescription('a', types='float', - descs=DocEquation('\\frac{1}{2}')))])) - # multi line equation - docstring = ('summary\n\nParameters\n----------\na : float\n .. math::\n\n' - ' \\frac{1}{2}\\\\\n \\frac{1}{3}') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - Parameters(DocDescription('a', types='float', - descs=DocEquation('\\frac{1}{2}\\\\\n' - '\\frac{1}{3}\n')))])) - # multiple equations - docstring = ('summary\n\nParameters\n----------\na : float\n .. math::\n\n' - ' \\frac{1}{2}\n ..math::\n \\frac{1}{3}') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - Parameters(DocDescription('a', types='float', - descs=[DocEquation('\\frac{1}{2}'), - DocEquation('\\frac{1}{3}')]))])) - # multiple equations and other descriptions - docstring = ('summary\n\nParameters\n----------\na : float\n Some float.\n .. math::\n\n' - ' \\frac{1}{2}\n\n Yes.\n ..math::\n \\frac{1}{3}\n' - ' This is the float.') - assert (parse_numpy(docstring) == - Docstring([Summary('summary'), - Parameters(DocDescription('a', types='float', - descs=['Some float.', - DocEquation('\\frac{1}{2}'), - 'Yes.', - DocEquation('\\frac{1}{3}'), - 'This is the float.']))])) diff --git a/docinstance/test/test_docstring.py b/docinstance/test/test_docstring.py deleted file mode 100644 index 1d95c1d..0000000 --- a/docinstance/test/test_docstring.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Test docinstance.docstring.""" -import pytest -from docinstance.docstring import Docstring -from docinstance.content.section import DocSection -from docinstance.content.description import DocDescription - - -def test_init(): - """Test Docstring.__init__.""" - with pytest.raises(TypeError): - Docstring(1) - with pytest.raises(TypeError): - Docstring({'1'}) - with pytest.raises(TypeError): - Docstring([1]) - with pytest.raises(TypeError): - Docstring(['1', 1]) - with pytest.raises(ValueError): - Docstring([]) - with pytest.raises(ValueError): - Docstring('1', 'nothing') - with pytest.raises(ValueError): - Docstring('1', None) - - test = Docstring('some text') - assert isinstance(test.sections, list) - assert len(test.sections) == 1 - assert isinstance(test.sections[0], DocSection) - assert test.sections[0].header == '' - assert test.sections[0].contents == ['some text'] - - test = Docstring(DocSection('some header', 'Hello World')) - assert isinstance(test.sections, list) - assert len(test.sections) == 1 - assert isinstance(test.sections[0], DocSection) - assert test.sections[0].header == 'some header' - assert test.sections[0].contents == ['Hello World'] - - test = Docstring([DocSection('some header', 'Hello World'), 'some text']) - assert isinstance(test.sections, list) - assert len(test.sections) == 2 - assert isinstance(test.sections[0], DocSection) - assert test.sections[0].header == 'some header' - assert test.sections[0].contents == ['Hello World'] - assert isinstance(test.sections[1], DocSection) - assert test.sections[1].header == '' - assert test.sections[1].contents == ['some text'] - - test = Docstring('some text', 'numpy') - assert test.default_style == 'numpy' - test = Docstring('some text', 'rst') - assert test.default_style == 'rst' - - -def test_make_docstring(): - """Test Docstring.make_docstring.""" - test = Docstring(['summary', 'extended summary', DocSection('parameters', '')]) - # standard input check - with pytest.raises(TypeError): - test.make_docstring(width=100.0) - with pytest.raises(ValueError): - test.make_docstring(width=-2) - with pytest.raises(ValueError): - test.make_docstring(width=0) - with pytest.raises(TypeError): - test.make_docstring(indent_level=2.0) - with pytest.raises(ValueError): - test.make_docstring(indent_level=-1) - with pytest.raises(TypeError): - test.make_docstring(tabsize=2.0) - with pytest.raises(ValueError): - test.make_docstring(tabsize=-2) - with pytest.raises(ValueError): - test.make_docstring(tabsize=0) - with pytest.raises(ValueError): - test.make_docstring(style='random style') - # bad ordering - test = Docstring(['summary', DocSection('parameters', ''), 'extended summary']) - with pytest.raises(ValueError): - test.make_docstring(style='numpy') - # check summary errors - test = Docstring([DocSection('parameters', DocDescription('something'))]) - with pytest.raises(ValueError): - test.make_docstring() - # one line summary - test = Docstring('very very long summary') - assert test.make_docstring(width=25) == 'very very long summary\n\n' - assert test.make_docstring(width=24) == '\nvery very long summary\n\n' - # multiple line summary - test = Docstring(['very very long summary', 'extended summary']) - assert test.make_docstring(width=25) == 'very very long summary\n\nextended summary\n\n' - assert test.make_docstring(width=24) == '\nvery very long summary\n\nextended summary\n\n' - test = Docstring(DocSection('', ['very very long summary', 'extended summary'])) - assert test.make_docstring(width=25) == 'very very long summary\n\nextended summary\n\n' - assert test.make_docstring(width=24) == '\nvery very long summary\n\nextended summary\n\n' - # other sections - test = Docstring([DocSection('', ['very very long summary', 'extended summary']), - DocSection('parameters', - DocDescription('var1', types=str, descs='Example.'))]) - assert (test.make_docstring(width=25) == - 'very very long summary\n\nextended summary\n\n' - 'Parameters\n----------\nvar1 : str\n Example.\n\n') - assert (test.make_docstring(width=24) == - '\nvery very long summary\n\nextended summary\n\n' - 'Parameters\n----------\nvar1 : str\n Example.\n\n') - # numpy with signature - test = Docstring(['summary', - DocSection('methods', - DocDescription('func1', signature='(a, b)', types=str, - descs='Example.'))]) - assert (test.make_docstring(width=25, style='numpy with signature') == - 'summary\n\nMethods\n-------\nfunc1(a, b) : str\n Example.\n\n') - # google - assert test.make_docstring(width=25, style='google') == ('summary\n\n' - 'Methods:\n' - ' func1 (:obj:`str`):\n' - ' Example.\n\n') - # rst - assert test.make_docstring(width=25, style='rst') == ('summary\n\n' - ':Methods:\n\n' - ':param func1: Example.\n' - ':type func1: :obj:`str`\n\n') - - -def test_check_section_order(): - """Test Docstring.check_section_order.""" - test = Docstring(['summary', 'extended', DocSection('parameters', ''), DocSection('warns', '')]) - assert test.check_section_order('numpy') is True - test = Docstring(['summary', DocSection('parameters', ''), 'extended', DocSection('warns', '')]) - assert test.check_section_order('numpy') is False - test = Docstring(['summary', 'extended', DocSection('warns', ''), DocSection('parameters', '')]) - assert test.check_section_order('numpy') is False - # note that the unidentified seections are not permitted for numpy style - test = Docstring(['summary', DocSection('asdfdsaf', ''), 'extended', DocSection('warns', ''), - DocSection('parameters', '')]) - with pytest.raises(ValueError): - test.check_section_order('numpy') - test = Docstring(['summary', 'extended', DocSection('warns', ''), DocSection('parameters', ''), - DocSection('asdfdsaf', '')]) - with pytest.raises(ValueError): - test.check_section_order('numpy') - - # other styles do not enforce such ordering - test = Docstring(['summary', DocSection('asdfdsaf', ''), 'extended', DocSection('warns', ''), - DocSection('parameters', '')]) - assert test.check_section_order('rst') is True - assert test.check_section_order('random') is True - # note that the unidentified sections are permitted for other styles - # NOTE: following does not actually check for the effect of adding unidentified section for non - # numpy style because all sections are unidentified for non numpy styles - test = Docstring(['summary', 'extended', DocSection('warns', ''), DocSection('parameters', ''), - DocSection('asdfdsaf', '')]) - assert test.check_section_order('random') is True diff --git a/docinstance/test/test_utils.py b/docinstance/test/test_utils.py deleted file mode 100644 index cc3db89..0000000 --- a/docinstance/test/test_utils.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Test docinstance.utils.""" -import pytest -import docinstance.utils - - -def test_wrap(): - """Test docinstance.utils.wrap.""" - # normal usage - assert (docinstance.utils.wrap('hello my name is', width=5, indent_level=0, tabsize=4) == - ['hello', 'my', 'name', 'is']) - assert (docinstance.utils.wrap('hello my name is', width=6, indent_level=1, tabsize=1) == - [' hello', ' my', ' name', ' is']) - assert (docinstance.utils.wrap('hello my name is', width=10, indent_level=2, tabsize=2) == - [' hello', ' my', ' name', ' is']) - assert (docinstance.utils.wrap('hello\n my', width=20, indent_level=0, tabsize=4) == - ['hello', ' my']) - assert (docinstance.utils.wrap('hello\n my', width=20, indent_level=1, tabsize=4) == - [' hello', ' my']) - assert (docinstance.utils.wrap('.. math:\n\n 1 + 2\n', width=20, indent_level=0, - tabsize=4) == ['.. math:', '', ' 1 + 2', '']) - # white spaces - assert (docinstance.utils.wrap(' hello', width=9, indent_level=0, tabsize=4) == - [' hello']) - assert docinstance.utils.wrap(' hello', width=8, indent_level=0, tabsize=4) == ['hello'] - assert docinstance.utils.wrap('hello ', width=8, indent_level=0, tabsize=4) == ['hello'] - assert (docinstance.utils.wrap(' hello my ', width=9, indent_level=0, tabsize=4) == - [' hello', 'my']) - assert (docinstance.utils.wrap(' hello my name is ', width=8, indent_level=0, - tabsize=4) == ['hello', 'my', 'name', 'is']) - assert (docinstance.utils.wrap('\n\n hello my name is\n\n', width=5, indent_level=0, - tabsize=4) == ['', '', 'hello', 'my', 'name', 'is', '', '']) - assert (docinstance.utils.wrap('\n\n hello\n my name is\n\n', width=5, indent_level=0, - tabsize=4) == ['', '', 'hello', ' my', 'name', 'is', '', '']) - # example - assert (docinstance.utils.wrap(' Text that will be wrapped into different lines such that each' - ' line is indented and is less than the given length.', - width=30) == [' Text that will be wrapped', - 'into different lines such that', - 'each line is indented and is', - 'less than the given length.']) - - # too much indentation - with pytest.raises(ValueError): - docinstance.utils.wrap('hello my name is', width=5, indent_level=3, tabsize=4) - # long words - with pytest.raises(ValueError): - docinstance.utils.wrap('hello my name is', width=1, indent_level=0, tabsize=1) - with pytest.raises(ValueError): - docinstance.utils.wrap('hello my name is', width=5, indent_level=1, tabsize=1) - # subsequent indent - assert (docinstance.utils.wrap('a b c d e', width=4, indent_level=0, tabsize=4, - subsequent_indent='xxx') == - ['a b', 'xxxc', 'xxxd', 'xxxe']) - assert (docinstance.utils.wrap('a b c d e', width=4, indent_level=1, tabsize=1, - subsequent_indent='xx') == - [' a b', ' xxc', ' xxd', ' xxe']) - with pytest.raises(ValueError): - docinstance.utils.wrap('a b c d e', width=4, indent_level=0, tabsize=4, - subsequent_indent='xxxx') - - -def test_wrap_indent_subsequent(): - """Test docinstance.utils.wrap_indent_subsequent.""" - assert (docinstance.utils.wrap_indent_subsequent('a b c d e', width=4, indent_level=1, - tabsize=3) == - ['a b', ' c', ' d', ' e']) - with pytest.raises(ValueError): - docinstance.utils.wrap_indent_subsequent('a b c d e', width=4, indent_level=1, tabsize=4) - - -def test_extract_members(): - """Test docinstance.utils.extract_members.""" - class Test: # pragma: no cover - """Test class.""" - @property - def f(self): - """Test property.""" - return (self, 1) - - def g(self): - """Test function.""" - return (self, 2) - - h = pytest - - assert docinstance.utils.extract_members(Test) == {'f': Test.f, 'g': Test.g} diff --git a/docinstance/test/test_wrapper.py b/docinstance/test/test_wrapper.py deleted file mode 100644 index 7f05fbc..0000000 --- a/docinstance/test/test_wrapper.py +++ /dev/null @@ -1,383 +0,0 @@ -"""Test docinstance.wrapper.""" -import importlib -import os -import sys -import pytest -from docinstance.wrapper import (kwarg_wrapper, docstring, docstring_recursive, - docstring_current_module) -from docinstance.docstring import Docstring -from docinstance.content.section import DocSection -from docinstance.content.description import DocDescription - - -def test_kwarg_wrapper(): - """Test docinstance.wrapper.kwarg_wrapper.""" - @kwarg_wrapper - def test(func, x=1): - """Test function.""" - func.x = x - return func - - def f(): # pragma: no cover - """Test function.""" - pass - - with pytest.raises(AttributeError): - f.x - g = test(f) - assert g.x == 1 - g = test(f, x=2) - assert g.x == 2 - - @test - def f(): # pragma: no cover - """Test function.""" - pass - assert f.x == 1 - - @test(x=2) - def f(): # pragma: no cover - """Test function.""" - pass - assert f.x == 2 - - -def test_wrapper_docstring_on_func(): - """Test docinstance.wrapper.docstring on a function.""" - # no _docinstance - @docstring - def test(): # pragma: no cover - """Test docstring. - - Parameters - ---------- - x : int - Something. - - """ - pass - - # NOTE: the indentation can be removed with inspect.cleandoc - assert test.__doc__ == ('Test docstring.\n' - '\n' - ' Parameters\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - assert not hasattr(test, '_docinstance') - - # _docinstance - docinstance = Docstring([ - 'Test docstring.', - DocSection('parameters', - DocDescription('x', types=int, descs='Something.')) - ]) - - def test(): # pragma: no cover - """Test function.""" - pass - - # FIXME: this is terrible because contents inside function is ignored when declaring a function - # i.e. we cannot set the attribute of a function within a function definition. Docstrings are - # special so they are not ignored. Using docstring that get parsed seems to be only option for - # standalone functions - test._docinstance = docinstance - docstring(test) - - assert test.__doc__ == ('Test docstring.\n' - '\n' - 'Parameters\n' - '----------\n' - 'x : int\n' - ' Something.\n\n') - assert test._docinstance == docinstance - - docstring(test, indent_level=2) - assert test.__doc__ == ('Test docstring.\n' - '\n' - ' Parameters\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - assert test._docinstance == docinstance - - -def test_wrapper_docstring_on_class(): - """Test docinstance.wrapper.docstring on a class.""" - # no _docinstance - @docstring - class Test: # pragma: no cover - """Test docstring. - - Attributes - ---------- - x : int - Something. - - """ - def f(self): - """Test function. - - Returns - ------- - nothing - - """ - pass - - assert Test.__doc__ == ('Test docstring.\n\n' - ' Attributes\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - assert not hasattr(Test, '_docinstance') - - # docinstance - docinstance1 = Docstring([ - 'Test docstring.', - DocSection('attributes', - DocDescription('x', types=int, descs='Something.')) - ]) - docinstance2 = Docstring([ - 'Test function.', - DocSection('returns', 'nothing') - ]) - - # w/o indentation - @docstring - class Test: # pragma: no cover - """Test class.""" - _docinstance = docinstance1 - - def f(self): - """Some docstring.""" - pass - f._docinstance = docinstance2 - - assert Test.__doc__ == ('Test docstring.\n\n' - 'Attributes\n' - '----------\n' - 'x : int\n' - ' Something.\n\n') - assert Test._docinstance == docinstance1 - assert Test.f.__doc__ == 'Some docstring.' - assert Test.f._docinstance == docinstance2 - - # w/ indentation - @docstring(indent_level=1) - class Test: # pragma: no cover - """Test class.""" - _docinstance = docinstance1 - - def f(self): - """Test function.""" - pass - f._docinstance = docinstance2 - - assert Test.__doc__ == ('Test docstring.\n\n' - ' Attributes\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - - -def test_wrapper_docstring_recursive_on_class(): - """Test docinstance.wrapper.docstring_recursive on a class.""" - # no _docinstance - @docstring_recursive - class Test: # pragma: no cover - """Test docstring. - - Attributes - ---------- - x : int - Something. - - """ - def f(self): - """Test function. - - Returns - ------- - nothing - - """ - pass - - assert Test.__doc__ == ('Test docstring.\n\n' - ' Attributes\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - assert not hasattr(Test, '_docinstance') - assert Test.f.__doc__ == ('Test function.\n\n' - ' Returns\n' - ' -------\n' - ' nothing\n\n' - ' ') - assert not hasattr(Test.f, '_docinstance') - - # docinstance - docinstance1 = Docstring([ - 'Test docstring.', - DocSection('attributes', - DocDescription('x', types=int, descs='Something.')) - ]) - docinstance2 = Docstring([ - 'Test function.', - DocSection('returns', 'nothing') - ]) - - # w/o indentation - @docstring_recursive - class Test: # pragma: no cover - """Test class.""" - _docinstance = docinstance1 - - def f(self): - """Test function.""" - pass - f._docinstance = docinstance2 - - assert Test.__doc__ == ('Test docstring.\n\n' - 'Attributes\n' - '----------\n' - 'x : int\n' - ' Something.\n\n') - assert Test._docinstance == docinstance1 - assert Test.f.__doc__ == ('Test function.\n\n' - ' Returns\n' - ' -------\n' - ' nothing\n\n' - ' ') - assert Test.f._docinstance == docinstance2 - - # w indentation - @docstring_recursive(indent_level=2) - class Test: # pragma: no cover - """Test class.""" - _docinstance = docinstance1 - - def f(self): - """Test function.""" - pass - f._docinstance = docinstance2 - - assert Test.__doc__ == ('Test docstring.\n\n' - ' Attributes\n' - ' ----------\n' - ' x : int\n' - ' Something.\n\n' - ' ') - assert Test._docinstance == docinstance1 - assert Test.f.__doc__ == ('Test function.\n\n' - ' Returns\n' - ' -------\n' - ' nothing\n\n' - ' ') - assert Test.f._docinstance == docinstance2 - - -def test_docstring_current_module(): - """Test docinstance.wrapper.docstring_current_module on the current module.""" - global test_docstring_current_module - test_docstring_current_module._docinstance = Docstring([ - 'Test docinstance.wrapper.docstring_current_module on the current module.', - 'Did it work?' - ]) - - def supplementary_docstring_current_module(): - """To be used to test docstring_current_module in test_docstring_current_module.""" - pass - - supplementary_docstring_current_module._docinstance = Docstring([ - 'Some docstring.', - DocSection('parameters', DocDescription('x', types=int, descs='Example.')) - ]) - test_docstring_current_module.f = supplementary_docstring_current_module - - docstring_current_module() - assert (test_docstring_current_module.__doc__ == - 'Test docinstance.wrapper.docstring_current_module on the current module.\n\n' - ' Did it work?\n\n' - ' ') - - assert (test_docstring_current_module.f.__doc__ == - 'Some docstring.\n\n' - ' Parameters\n' - ' ----------\n' - ' x : int\n' - ' Example.\n\n' - ' ') - - -def test_docstring_modify_import(tmp_path): - """Test docinstance.wrapper.docstring_modify_import on module `dummy`.""" - assert (not hasattr(importlib._bootstrap_external.SourceFileLoader.exec_module, - 'special_cases')) - # create temporary directory - tmp_dir = tmp_path / "test" - tmp_dir.mkdir() - # create python file that calls docstring_modify_import - path1 = tmp_dir / "dummy1.py" - path1.write_text("from docinstance.wrapper import docstring_modify_import\n" - "docstring_modify_import()") - # create another python file with docstrings - path2 = tmp_dir / "dummy2.py" - path2.write_text( - '''"""This will be replaced.""" -from docinstance.docstring import Docstring -from docinstance.content.section import DocSection -from docinstance.content.description import DocDescription - -_docinstance = Docstring([ - 'Dummy module for testing docinstance.wrapper.docstring_modify_import.' -]) - - -class DummyClass: - """This too will be replaced.""" - - _docinstance = Docstring([ - 'Class for testing.', - DocSection('attributes', DocDescription('x', types=str, descs='Example.')) - ]) - ''') - # create yet another python file with docstring_modify_import - path3 = tmp_dir / "dummy3.py" - path3.write_text("from docinstance.wrapper import docstring_modify_import\n" - "docstring_modify_import()") - - # check that dummy2 is decorated - sys.path.append(str(tmp_dir)) - import dummy1 - import dummy2 as test - assert (test.__doc__ == - 'Dummy module for testing docinstance.wrapper.docstring_modify_import.') - assert (test.DummyClass.__doc__ == - 'Class for testing.\n\n' - ' Attributes\n' - ' ----------\n' - ' x : str\n' - ' Example.\n\n' - ' ') - assert (importlib._bootstrap_external.SourceFileLoader.exec_module.special_cases == - set([os.path.dirname(dummy1.__file__)])) - - # check that multiple function calls does not change anything - import dummy3 - assert (importlib._bootstrap_external.SourceFileLoader.exec_module.special_cases == - set([os.path.dirname(dummy3.__file__)])) - - # import package outside current directory - import docinstance.utils - # reload module because it has already been loaded via docinstance.wrapper - importlib.reload(docinstance.utils) - # check that the objects within docinstance.utils has not been touched by wrapper. - with pytest.raises(AttributeError): - docinstance.utils._docinstance diff --git a/pylintrc b/pylintrc deleted file mode 100644 index dd80cd9..0000000 --- a/pylintrc +++ /dev/null @@ -1,28 +0,0 @@ -[MESSAGES CONTROL] -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=W0223,W0231,E0611 - -# abstract-method (W0223): Used when an abstract method (i.e. raise NotImplementedError) is not overridden in concrete class. -# super-init-not-called (W0231): Used when an ancestor class method has an __init__ method which is not called by a derived class. -# no-name-in-module (E0611): Used when a name cannot be found in a module. - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -# notes=FIXME,XXX,TODO -notes=XXX - - -[DESIGN] - -# Maximum number of argunmnts for function/method -max-args = 6 \ No newline at end of file diff --git a/setup.py b/setup.py index 5cc9191..12d980e 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,15 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/kimt33/docinstance", - packages=setuptools.find_packages(), + packages=setuptools.find_packages(where="src"), + package_dir={"": "src"}, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], + extras_require={ + "napoleon": ["sphinxcontrib-napoleon"], + "test": ["tox", "pytest", "pytest-cov"], + }, ) diff --git a/docinstance/__init__.py b/src/docinstance/__init__.py similarity index 83% rename from docinstance/__init__.py rename to src/docinstance/__init__.py index a0b5a7e..12517ef 100644 --- a/docinstance/__init__.py +++ b/src/docinstance/__init__.py @@ -1,3 +1,3 @@ """Module for representing a docstring as an instance of a Docstring class.""" # pylint: disable=C0103 -name = 'docinstance' +name = "docinstance" diff --git a/docinstance/content/__init__.py b/src/docinstance/content/__init__.py similarity index 100% rename from docinstance/content/__init__.py rename to src/docinstance/content/__init__.py diff --git a/src/docinstance/content/base.py b/src/docinstance/content/base.py new file mode 100644 index 0000000..bf10f6c --- /dev/null +++ b/src/docinstance/content/base.py @@ -0,0 +1,97 @@ +"""Base class for docstring contents.""" + + +class DocContent: + """Base class for all content of a docstring. + + Any special construct that goes inside a docstring (i.e. not a string) should be a child of this + class. + + Methods + ------- + __init__(self) + Initialize. + __eq__(self, other) + Return True if other is DocContent instance with the same contents. False otherwise. + __ne__(self, other) + Return False if other is DocContent instance with the same contents. True otherwise. + make_docstring(self, width, indent_level, tabsize, style) + Return the docstring of the content in the given style. + + """ + + def __eq__(self, other): + """Return True if other is DocContent instance with the same contents. False otherwise. + + Parameters + ---------- + other : DocContent + + Returns + ------- + bool + + """ + return isinstance(other, DocContent) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Return False if other is DocContent instance with the same contents. True otherwise. + + Parameters + ---------- + other : DocContent + + Returns + ------- + bool + + Notes + ----- + The behaviour of __ne__ changed form Python 2 to Python 3. In Python 3, there is a default + behaviour of __ne__ when __eq__ returns NotImplemented, which is the default behaviour for + __eq__. Special care needs to be taken when only __ne__ is defined. However, since we define + the __eq__ here, we don't need to be too careful. See + https://stackoverflow.com/questions/4352244/python-should-i-implement-ne-operator-based-on-eq/50661674#50661674 + for more details. + + """ + return not self == other + + def make_docstring(self, width, indent_level, tabsize, style): + """Return the docstring as a string in the given style. + + Parameters + ---------- + width : int + Maximum number of characters allowed in a line. + indent_level : int + Number of indents (tabs) that are needed for the docstring. + tabsize : int + Number of spaces that corresponds to a tab. + style : str + Name of the docstring style. + + Returns + ------- + content_docstring : str + Docstring of the given content in the given style. + + Raises + ------ + TypeError + If `style` is not given as a non-empty string. + NotImplementedError + If the corresponding method for the given `style` is not defined. + + """ + if not (isinstance(style, str) and style != ""): + raise TypeError("The `style` of the docstring must be given as a non-empty string.") + method_name = "make_docstring_{}".format(style) + if hasattr(self, method_name): + return getattr(self, method_name)(width, indent_level, tabsize) + raise NotImplementedError( + "To make a docstring of style, {}, the given instance of {} must have a method called " + "{} with arguments (width, indent_level, tabsize).".format( + style, self.__class__, method_name + ) + ) diff --git a/docinstance/content/description.py b/src/docinstance/content/description.py similarity index 59% rename from docinstance/content/description.py rename to src/docinstance/content/description.py index 3d9de03..7effc38 100644 --- a/docinstance/content/description.py +++ b/src/docinstance/content/description.py @@ -1,7 +1,7 @@ """Class for representing a description of objects/errors in the docstring.""" -from docinstance.utils import wrap, wrap_indent_subsequent from docinstance.content.base import DocContent from docinstance.content.equation import DocEquation +from docinstance.utils import wrap, wrap_indent_subsequent class DocDescription(DocContent): @@ -34,16 +34,20 @@ class DocDescription(DocContent): ------- __init__(self, name, signature='', types='', descs='') Initialize. - make_docstring(self, style, width, indent_level, tabsize) - Return docstring in correponding style. - make_numpy_docstring(self, width, indent_level, tabsize) + make_docstring(self, width, indent_level, tabsize, style) + Return the docstring of the content in the given style. + make_docstring_numpy(self, width, indent_level, tabsize) Return the docstring in numpy style. - make_numpy_docstring_signature(self, width, indent_level, tabsize) + make_docstring_numpy_signature(self, width, indent_level, tabsize) Return the docstring in numpy style modified to include signature. + make_docstring_google(self, width, indent_level, tabsize) + Return the docstring in google style. + make_docstring_rst(self, width, indent_level, tabsize) + Return the docstring in rst style. """ - def __init__(self, name, signature='', types=None, descs=None): + def __init__(self, name, signature="", types=None, descs=None): """Initialize the object. Parameters @@ -86,20 +90,27 @@ def __init__(self, name, signature='', types=None, descs=None): types = [] elif isinstance(types, (type, str)): types = [types] - elif not (isinstance(types, (list, tuple)) and - all(isinstance(i, (type, str)) for i in types)): - raise TypeError("Types of allowed objects must be given as a class or list/tuple of " - "classes/strings.") + elif not ( + isinstance(types, (list, tuple)) and all(isinstance(i, (type, str)) for i in types) + ): + raise TypeError( + "Types of allowed objects must be given as a class or list/tuple of " + "classes/strings." + ) self.types = list(types) if descs is None: descs = [] elif isinstance(descs, (str, DocEquation)): descs = [descs] - elif not (isinstance(descs, (list, tuple)) and - all(isinstance(i, (str, DocEquation)) for i in descs)): - raise TypeError("Descriptions of the object/error must be given as a string or " - "list/tuple of strings") + elif not ( + isinstance(descs, (list, tuple)) + and all(isinstance(i, (str, DocEquation)) for i in descs) + ): + raise TypeError( + "Descriptions of the object/error must be given as a string or " + "list/tuple of strings" + ) self.descs = list(descs) @property @@ -114,7 +125,7 @@ def types_str(self): """ return [i.__name__ if isinstance(i, type) else i for i in self.types] - def make_numpy_docstring(self, width, indent_level, tabsize): + def make_docstring_numpy(self, width, indent_level, tabsize): """Return the docstring in numpy style. Parameters @@ -143,11 +154,13 @@ def make_numpy_docstring(self, width, indent_level, tabsize): The signature of a function is not included in the numpy docstring. """ - if self.signature != '': - print('Warning: In NumPy docstring format, the signature of a function is not ' - 'included.') + if self.signature != "": + print( + "Warning: In NumPy docstring format, the signature of a function is not " + "included." + ) - output = '' + output = "" # var_name # OR # error_name @@ -157,43 +170,57 @@ def make_numpy_docstring(self, width, indent_level, tabsize): output += wrap(self.name, width=width, indent_level=indent_level, tabsize=tabsize)[0] # var_name : var_type elif len(self.types) == 1: - name_type = wrap('{0} : {1}'.format(self.name, self.types_str[0]), - width=width, indent_level=indent_level, tabsize=tabsize) + name_type = wrap( + "{0} : {1}".format(self.name, self.types_str[0]), + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) # check that the both the name and the type can fit in the given width and indentation if len(name_type) > 1: # FIXME: need a better message - raise ValueError('The name and the type of the variable are too long to fit into ' - 'given width and indentation.') + raise ValueError( + "The name and the type of the variable are too long to fit into " + "given width and indentation." + ) output += name_type[0] # var_name : {var_type1, var_type2, default_type} else: - name_types = wrap('{0} : {{{1}}}'.format(self.name, ', '.join(self.types_str)), - width=width, indent_level=indent_level, tabsize=tabsize) + name_types = wrap( + "{0} : {{{1}}}".format(self.name, ", ".join(self.types_str)), + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) # if there are too many types to fit into one line, the remaining lines should be # indented to line up after "var_name : {" - wrap_point = len('{0} : {{'.format(self.name)) + wrap_point = len("{0} : {{".format(self.name)) # check that the name and first type can fit into the first line - if not name_types[0].startswith('{0}{1} : {{{2}'.format(' ' * indent_level * tabsize, - self.name, self.types_str[0])): + if not name_types[0].startswith( + "{0}{1} : {{{2}".format(" " * indent_level * tabsize, self.name, self.types_str[0]) + ): # FIXME: need a better message - raise ValueError('The name and the first type of the variable are too long to fit ' - 'into the given width and indentation.') + raise ValueError( + "The name and the first type of the variable are too long to fit " + "into the given width and indentation." + ) # wrap the remaining lines name_types_lines = [name_types[0]] for line in name_types[1:]: name_types_lines += wrap(line, width=width, indent_level=1, tabsize=wrap_point) - output += '\n'.join(name_types_lines) - output += '\n' + output += "\n".join(name_types_lines) + output += "\n" # descriptions for paragraph in self.descs: - output += '\n'.join(wrap(paragraph, - width=width, indent_level=indent_level+1, tabsize=tabsize)) - output += '\n' + output += "\n".join( + wrap(paragraph, width=width, indent_level=indent_level + 1, tabsize=tabsize) + ) + output += "\n" return output - def make_numpy_docstring_signature(self, width, indent_level, tabsize): + def make_docstring_numpy_signature(self, width, indent_level, tabsize): """Return the docstring in numpy style modified to include signature. Parameters @@ -212,11 +239,11 @@ def make_numpy_docstring_signature(self, width, indent_level, tabsize): signature. """ - new_name = '{0}{1}'.format(self.name, self.signature) - new_description = self.__class__(new_name, '', self.types, self.descs) - return new_description.make_numpy_docstring(width, indent_level, tabsize) + new_name = "{0}{1}".format(self.name, self.signature) + new_description = self.__class__(new_name, "", self.types, self.descs) + return new_description.make_docstring_numpy(width, indent_level, tabsize) - def make_google_docstring(self, width, indent_level, tabsize): + def make_docstring_google(self, width, indent_level, tabsize): """Return the docstring of the content in google style. Parameters @@ -238,45 +265,58 @@ def make_google_docstring(self, width, indent_level, tabsize): The signature of a function is not included in the google docstring. """ - output = '' + output = "" first_block = [] # var_name: if not self.types: - first_block = wrap('{0}:'.format(self.name), - width=width, indent_level=indent_level, tabsize=tabsize) + first_block = wrap( + "{0}:".format(self.name), width=width, indent_level=indent_level, tabsize=tabsize + ) # var_name (type1, type2): else: # FIXME: optional parameters need to be specified within google doc - types_str = [i if isinstance(i, str) else ':obj:`{0}`'.format(j) - for i, j in zip(self.types, self.types_str)] - text = '{0} ({1}):'.format(self.name, ', '.join(types_str)) + types_str = [ + i if isinstance(i, str) else ":obj:`{0}`".format(j) + for i, j in zip(self.types, self.types_str) + ] + text = "{0} ({1}):".format(self.name, ", ".join(types_str)) # if there are too many types to fit into one line, the remaining lines should be # indented to line up after "var_name (" # FIXME: following can probably be replaced with a better wrapping function - first_block = [' ' * indent_level * tabsize + line for line in - wrap_indent_subsequent(text, width=width - indent_level*tabsize, - indent_level=1, - tabsize=len('{0} ('.format(self.name)))] + first_block = [ + " " * indent_level * tabsize + line + for line in wrap_indent_subsequent( + text, + width=width - indent_level * tabsize, + indent_level=1, + tabsize=len("{0} (".format(self.name)), + ) + ] # add descriptions if self.descs: - output += '\n'.join(first_block[:-1]) + output += "\n".join(first_block[:-1]) if len(first_block) != 1: - output += '\n' + output += "\n" # FIXME: following can probably be replaced with a better wrapping function - first_desc = wrap_indent_subsequent(first_block[-1] + ' ' + self.descs[0], width=width, - indent_level=indent_level+1, tabsize=tabsize) - output += '\n'.join(first_desc) - output += '\n' + first_desc = wrap_indent_subsequent( + first_block[-1] + " " + self.descs[0], + width=width, + indent_level=indent_level + 1, + tabsize=tabsize, + ) + output += "\n".join(first_desc) + output += "\n" for desc in self.descs[1:]: - output += '\n'.join(wrap(desc, width=width, indent_level=indent_level+1, - tabsize=tabsize)) - output += '\n' + output += "\n".join( + wrap(desc, width=width, indent_level=indent_level + 1, tabsize=tabsize) + ) + output += "\n" else: - output += '\n'.join(first_block) - output += '\n' + output += "\n".join(first_block) + output += "\n" return output - def make_rst_docstring(self, width, indent_level, tabsize): + def make_docstring_rst(self, width, indent_level, tabsize): """Return the docstring in sphinx's rst format. Parameters @@ -299,36 +339,51 @@ def make_rst_docstring(self, width, indent_level, tabsize): If the parameter name is too long to fit within the given width and indent. """ - output = '' + output = "" if self.descs: - text = ':param {0}: {1}'.format(self.name, self.descs[0]) + text = ":param {0}: {1}".format(self.name, self.descs[0]) # FIXME: following can probably be replaced with a better wrapping function - block = [' ' * indent_level * tabsize + line for line in - wrap_indent_subsequent(text, width=width - indent_level*tabsize, - indent_level=1, tabsize=tabsize)] - output += '\n'.join(block) - output += '\n' + block = [ + " " * indent_level * tabsize + line + for line in wrap_indent_subsequent( + text, width=width - indent_level * tabsize, indent_level=1, tabsize=tabsize + ) + ] + output += "\n".join(block) + output += "\n" if len(self.descs) > 1: for desc in self.descs[1:]: - output += '\n'.join(wrap(desc, width=width, indent_level=indent_level+1, - tabsize=tabsize)) - output += '\n' + output += "\n".join( + wrap(desc, width=width, indent_level=indent_level + 1, tabsize=tabsize) + ) + output += "\n" else: - block = wrap(':param {0}:'.format(self.name), width=width, indent_level=indent_level, - tabsize=tabsize) + block = wrap( + ":param {0}:".format(self.name), + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) if len(block) > 1: - raise ValueError('Parameter name is too long to fit within the given line width and' - ' indent level.') + raise ValueError( + "Parameter name is too long to fit within the given line width and" + " indent level." + ) output += block[0] - output += '\n' + output += "\n" - types_str = [i if isinstance(i, str) else ':obj:`{0}`'.format(j) - for i, j in zip(self.types, self.types_str)] + types_str = [ + i if isinstance(i, str) else ":obj:`{0}`".format(j) + for i, j in zip(self.types, self.types_str) + ] if self.types: - text = ':type {0}: {1}'.format(self.name, ', '.join(types_str)) - block = [' ' * indent_level * tabsize + line for line in - wrap_indent_subsequent(text, width=width - indent_level*tabsize, - indent_level=1, tabsize=tabsize)] - output += '\n'.join(block) - output += '\n' + text = ":type {0}: {1}".format(self.name, ", ".join(types_str)) + block = [ + " " * indent_level * tabsize + line + for line in wrap_indent_subsequent( + text, width=width - indent_level * tabsize, indent_level=1, tabsize=tabsize + ) + ] + output += "\n".join(block) + output += "\n" return output diff --git a/docinstance/content/equation.py b/src/docinstance/content/equation.py similarity index 59% rename from docinstance/content/equation.py rename to src/docinstance/content/equation.py index d96eed8..27db2d6 100644 --- a/docinstance/content/equation.py +++ b/src/docinstance/content/equation.py @@ -1,6 +1,6 @@ """Class for representing math equations.""" -from docinstance.utils import wrap from docinstance.content.base import DocContent +from docinstance.utils import wrap class DocEquation(DocContent): @@ -15,8 +15,10 @@ class DocEquation(DocContent): ------- __init__(self, equations) Initialize. - make_numpy_docstring(self, style, width, indent_level, tabsize) - Return docstring in correponding style. + make_docstring(self, width, indent_level, tabsize, style) + Return the docstring as a string in the given style. + make_docstring_numpy(self, width, indent_level, tabsize) + Return the docstring in numpy style. """ @@ -35,12 +37,12 @@ def __init__(self, equations): """ if not isinstance(equations, str): - raise TypeError('Equations must be given as one string.') - self.equations = equations.split('\n') - if self.equations[-1] == '': + raise TypeError("Equations must be given as one string.") + self.equations = equations.split("\n") + if self.equations[-1] == "": self.equations = self.equations[:-1] - def make_numpy_docstring(self, width, indent_level, tabsize): + def make_docstring_numpy(self, width, indent_level, tabsize): """Return the docstring in numpy style. Parameters @@ -63,22 +65,29 @@ def make_numpy_docstring(self, width, indent_level, tabsize): If the width is too small to fit the equation for the given indent and tabsize. """ - output = '' + output = "" if len(self.equations) == 1: - first_line = wrap('.. math:: ' + self.equations[0], - width=width, indent_level=indent_level, tabsize=tabsize) + first_line = wrap( + ".. math:: " + self.equations[0], + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) if len(first_line) == 1: output += first_line[0] - output += '\n\n' + output += "\n\n" return output - first_line = wrap('.. math:: ', width=width, indent_level=indent_level, tabsize=tabsize) + first_line = wrap(".. math:: ", width=width, indent_level=indent_level, tabsize=tabsize) if len(first_line) != 1: - raise ValueError('Given line width is too small to fit the equation for the given ' - 'indent and tab size') + raise ValueError( + "Given line width is too small to fit the equation for the given " + "indent and tab size" + ) output += first_line[0] - output += '\n\n' - output += '\n'.join('\n'.join(wrap(equation, width=width, indent_level=indent_level + 1, - tabsize=tabsize)) - for equation in self.equations) - output += '\n\n' + output += "\n\n" + output += "\n".join( + "\n".join(wrap(equation, width=width, indent_level=indent_level + 1, tabsize=tabsize)) + for equation in self.equations + ) + output += "\n\n" return output diff --git a/docinstance/content/section.py b/src/docinstance/content/section.py similarity index 68% rename from docinstance/content/section.py rename to src/docinstance/content/section.py index dc8c177..96dea5e 100644 --- a/docinstance/content/section.py +++ b/src/docinstance/content/section.py @@ -1,7 +1,7 @@ """Class for representing a section in the docstring.""" -from docinstance.utils import wrap, wrap_indent_subsequent from docinstance.content.base import DocContent from docinstance.content.description import DocDescription +from docinstance.utils import wrap, wrap_indent_subsequent class DocSection(DocContent): @@ -18,10 +18,16 @@ class DocSection(DocContent): ------- __init__(self, header, contents) Initialize. - make_numpy_docstring(self, width, indent_level, tabsize, include_signature=False) + make_docstring_numpy(self, width, indent_level, tabsize) + Return the docstring in numpy style. + make_docstring_numpy(self, width, indent_level, tabsize, include_signature=False) Return the docstring in numpy style. - make_numpy_docstring_signature(self, width, indent_level, tabsize) + make_docstring_numpy_signature(self, width, indent_level, tabsize) Return the docstring in numpy style modified to include signature. + make_docstring_google(self, width, indent_level, tabsize) + Return the docstring in google style. + make_docstring_rst(self, width, indent_level, tabsize) + Return the docstring in rst style. """ @@ -57,18 +63,27 @@ def __init__(self, header, contents): contents = [contents] # NOTE: is it really necessary to prevent contents of a section from mixing # strings/DocContent and DocDescription? - elif not (isinstance(contents, (tuple, list)) and - (all(isinstance(content, (str, DocContent)) and - not isinstance(content, DocDescription) for content in contents) or - all(isinstance(content, DocDescription) for content in contents))): - raise TypeError("The parameter `contents` must be a string, a list/tuple of " - "DocDescription, or a list/tuple of strings/DocContent (not " - "DocDescription).") + elif not ( + isinstance(contents, (tuple, list)) + and ( + all( + isinstance(content, (str, DocContent)) + and not isinstance(content, DocDescription) + for content in contents + ) + or all(isinstance(content, DocDescription) for content in contents) + ) + ): + raise TypeError( + "The parameter `contents` must be a string, a list/tuple of " + "DocDescription, or a list/tuple of strings/DocContent (not " + "DocDescription)." + ) self.contents = list(contents) # pylint: disable=W0221 - # the extra argument is used in the make_numpy_docstring_signature - def make_numpy_docstring(self, width, indent_level, tabsize, include_signature=False): + # the extra argument is used in the make_docstring_numpy_signature + def make_docstring_numpy(self, width, indent_level, tabsize, include_signature=False): """Return the docstring in numpy style. Parameters @@ -94,40 +109,43 @@ def make_numpy_docstring(self, width, indent_level, tabsize, include_signature=F If the title is too long for the given width and indentation. """ - output = '' + output = "" # title - if self.header != '': - title = wrap(self.header.title(), width=width, indent_level=indent_level, - tabsize=tabsize) - divider = wrap('-' * len(self.header), width=width, indent_level=indent_level, - tabsize=tabsize) + if self.header != "": + title = wrap( + self.header.title(), width=width, indent_level=indent_level, tabsize=tabsize + ) + divider = wrap( + "-" * len(self.header), width=width, indent_level=indent_level, tabsize=tabsize + ) # NOTE: error will be raised if the line width is not wide enough to fit the title and # the divider in one line because the divider is one word and wrap will complain if the # line width is not big enough to fit the first word - output += '{0}\n{1}\n'.format(title[0], divider[0]) + output += "{0}\n{1}\n".format(title[0], divider[0]) # contents for paragraph in self.contents: # NOTE: since the contents are checked in the initialization, we will assume that the # paragraph can only be string or DocDescription if isinstance(paragraph, str): - output += '\n'.join(wrap(paragraph, width=width, indent_level=indent_level, - tabsize=tabsize)) - output += '\n\n' + output += "\n".join( + wrap(paragraph, width=width, indent_level=indent_level, tabsize=tabsize) + ) + output += "\n\n" # if isinstance(paragraph, DocContent) - elif include_signature and hasattr(paragraph, 'make_numpy_docstring_signature'): - output += paragraph.make_numpy_docstring_signature(width, indent_level, tabsize) + elif include_signature and hasattr(paragraph, "make_docstring_numpy_signature"): + output += paragraph.make_docstring_numpy_signature(width, indent_level, tabsize) else: - output += paragraph.make_numpy_docstring(width, indent_level, tabsize) + output += paragraph.make_docstring_numpy(width, indent_level, tabsize) # pylint: disable=W0120 # following block clause should always be executed else: # end a section with two newlines (note that the section already ends with a newline if # it ends with a paragraph) - output += '\n' * isinstance(paragraph, DocDescription) + output += "\n" * isinstance(paragraph, DocDescription) return output - def make_numpy_docstring_signature(self, width, indent_level, tabsize): + def make_docstring_numpy_signature(self, width, indent_level, tabsize): """Return the docstring in numpy style modified to include signature. Parameters @@ -151,9 +169,9 @@ def make_numpy_docstring_signature(self, width, indent_level, tabsize): If any of the paragraph is neither a string nor a DocDescription instance. """ - return self.make_numpy_docstring(width, indent_level, tabsize, include_signature=True) + return self.make_docstring_numpy(width, indent_level, tabsize, include_signature=True) - def make_google_docstring(self, width, indent_level, tabsize): + def make_docstring_google(self, width, indent_level, tabsize): """Return the docstring of the section in google style. Parameters @@ -176,15 +194,19 @@ def make_google_docstring(self, width, indent_level, tabsize): If the title is too long for the given width and indentation. """ - output = '' + output = "" # title - if self.header != '': - title = wrap('{0}:'.format(self.header.title()), - width=width, indent_level=indent_level, tabsize=tabsize) + if self.header != "": + title = wrap( + "{0}:".format(self.header.title()), + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) if len(title) > 1: - raise ValueError('The header must fit into the given width with the indentation') + raise ValueError("The header must fit into the given width with the indentation") output += title[0] - output += '\n' + output += "\n" else: # don't indent the contents if there is no header indent_level -= 1 @@ -193,22 +215,23 @@ def make_google_docstring(self, width, indent_level, tabsize): # NOTE: since the contents are checked in the initialization, we will assume that the # paragraph can only be string or DocDescription if isinstance(paragraph, str): - output += '\n'.join(wrap(paragraph, width=width, indent_level=indent_level+1, - tabsize=tabsize)) - output += '\n\n' + output += "\n".join( + wrap(paragraph, width=width, indent_level=indent_level + 1, tabsize=tabsize) + ) + output += "\n\n" # if isinstance(paragraph, DocContent) else: - output += paragraph.make_google_docstring(width, indent_level+1, tabsize) + output += paragraph.make_docstring_google(width, indent_level + 1, tabsize) # pylint: disable=W0120 # following block clause should always be executed else: # end a section with two newlines (note that the section already ends with a newline if # it ends with a paragraph) - output += '\n' * isinstance(paragraph, DocDescription) + output += "\n" * isinstance(paragraph, DocDescription) return output - def make_rst_docstring(self, width, indent_level, tabsize): + def make_docstring_rst(self, width, indent_level, tabsize): """Return the docstring in sphinx's rst format. Parameters @@ -226,53 +249,65 @@ def make_rst_docstring(self, width, indent_level, tabsize): Docstring of the given content in sphinx's rst style. """ - output = '' - header = '' - special_headers = {'see also': 'seealso', 'warnings': 'warning', 'warning': 'warning', - 'notes': 'note', 'note': 'note', 'to do': 'todo', 'todo': 'todo'} + output = "" + header = "" + special_headers = { + "see also": "seealso", + "warnings": "warning", + "warning": "warning", + "notes": "note", + "note": "note", + "to do": "todo", + "todo": "todo", + } if self.header.lower() in special_headers: - header = '.. {0}::'.format(special_headers[self.header.lower()]) - elif self.header != '': - output += ':{0}:\n\n'.format(self.header.title()) + header = ".. {0}::".format(special_headers[self.header.lower()]) + elif self.header != "": + output += ":{0}:\n\n".format(self.header.title()) for i, paragraph in enumerate(self.contents): # first content must be treated with care for special headers if i == 0 and self.header.lower() in special_headers: # str if isinstance(paragraph, str): - text = '{0} {1}'.format(header, paragraph) + text = "{0} {1}".format(header, paragraph) # FIXME: following can probably be replaced with a better wrapping function - first_content = [' ' * indent_level * tabsize + line for line in - wrap_indent_subsequent(text, - width=width - indent_level*tabsize, - indent_level=indent_level+1, - tabsize=tabsize)] - output += '\n'.join(first_content) - output += '\n' + first_content = [ + " " * indent_level * tabsize + line + for line in wrap_indent_subsequent( + text, + width=width - indent_level * tabsize, + indent_level=indent_level + 1, + tabsize=tabsize, + ) + ] + output += "\n".join(first_content) + output += "\n" # DocContent else: output += header - output += '\n' - output += paragraph.make_rst_docstring(width=width, - indent_level=indent_level+1, - tabsize=tabsize) + output += "\n" + output += paragraph.make_docstring_rst( + width=width, indent_level=indent_level + 1, tabsize=tabsize + ) # indent all susequent content indent_level += 1 elif isinstance(paragraph, str): - output += '\n'.join(wrap(paragraph, width=width, indent_level=indent_level, - tabsize=tabsize)) + output += "\n".join( + wrap(paragraph, width=width, indent_level=indent_level, tabsize=tabsize) + ) # NOTE: the second newline may cause problems (because the field might not # recognize text that is more than one newline away) - output += '\n\n' + output += "\n\n" else: - output += paragraph.make_rst_docstring(width, indent_level, tabsize) + output += paragraph.make_docstring_rst(width, indent_level, tabsize) # pylint: disable=W0120 # following block clause should always be executed else: # end a section with two newlines (note that the section already ends with a newline # if it ends with a paragraph) - output += '\n' * isinstance(paragraph, DocDescription) + output += "\n" * isinstance(paragraph, DocDescription) return output @@ -292,9 +327,9 @@ class Summary(DocSection): ------- __init__(self, header, contents) Initialize. - make_numpy_docstring(self, width, indent_level, tabsize, include_signature=False) + make_docstring_numpy(self, width, indent_level, tabsize, include_signature=False) Return the docstring in numpy style. - make_numpy_docstring_signature(self, width, indent_level, tabsize) + make_docstring_numpy_signature(self, width, indent_level, tabsize) Return the docstring in numpy style modified to include signature. """ @@ -313,7 +348,7 @@ def __init__(self, contents): If the given content is not a string. """ - self.header = '' + self.header = "" if not isinstance(contents, str): raise TypeError("The parameter `contents` must be a string.") self.contents = [contents] @@ -349,32 +384,47 @@ def make_docstring(self, width, indent_level, tabsize, summary_only=False, speci If the title is too long for the given width and indentation. """ - output = '' + output = "" summary = self.contents[0] # if summary cannot fit into first line with one triple quotation # if len(summary) + ' ' * indent_level * tabsize > width - 3: if len(wrap(summary, width - 3 - int(special), indent_level, tabsize)) > 1: - output += '\n' + output += "\n" # if summary cannot fit into the second line (without tripple quotation) # if len(summary) + ' ' * indent_level * tabsize > width: if len(wrap(summary, width, indent_level, tabsize)) > 1: - raise ValueError('First section of the docstring (summary) must fit completely into' - ' the first line of the docstring (including the triple quotation)' - ' or the second line.') + raise ValueError( + "First section of the docstring (summary) must fit completely into" + " the first line of the docstring (including the triple quotation)" + " or the second line." + ) output += summary # if summary only and summary can fit into the first line with two triple quotations - if not (summary_only and - len(wrap(summary, width - 6 - int(special), indent_level, tabsize)) == 1): - output += '\n\n' + if not ( + summary_only + and len(wrap(summary, width - 6 - int(special), indent_level, tabsize)) == 1 + ): + output += "\n\n" return output # pylint: disable=C0103 -dict_classname_headers = {'ExtendedSummary': '', 'Parameters': None, 'Attributes': None, - 'Methods': None, 'Returns': None, 'Yields': None, - 'OtherParameters': 'other parameters', 'Raises': None, 'Warns': None, - 'Warnings': None, 'SeeAlso': 'see also', 'Notes': None, - 'References': None, 'Examples': None} +dict_classname_headers = { + "ExtendedSummary": "", + "Parameters": None, + "Attributes": None, + "Methods": None, + "Returns": None, + "Yields": None, + "OtherParameters": "other parameters", + "Raises": None, + "Warns": None, + "Warnings": None, + "SeeAlso": "see also", + "Notes": None, + "References": None, + "Examples": None, +} # factory for init @@ -401,7 +451,8 @@ def make_init(header): See https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop for more details. """ - def __init__(self, contents): + + def __init__(self, contents): # noqa: N807 """Initialize. Parameters @@ -434,4 +485,4 @@ def __init__(self, contents): # globals is the dictionary of the current module for the symbols # types is used to instantiate a class (because all classes are instances of type) - globals()[class_name] = type(class_name, (DocSection,), {'__init__': make_init(section_header)}) + globals()[class_name] = type(class_name, (DocSection,), {"__init__": make_init(section_header)}) diff --git a/docinstance/docstring.py b/src/docinstance/docstring.py similarity index 52% rename from docinstance/docstring.py rename to src/docinstance/docstring.py index 736750e..09ba970 100644 --- a/docinstance/docstring.py +++ b/src/docinstance/docstring.py @@ -10,27 +10,23 @@ class Docstring: ---------- sections : list of DocSection Sections of the docstring. - default_style : str - Default style of the docstring. Methods ------- - __init__(self, sections, default_style) + __init__(self, sections) Initialize. - make_docstring(self, style='numpy', width=100, indent_level=0, tabsize=4) + make_docstring(self, width=100, indent_level=0, tabsize=4, style='numpy') Return the docstring in the given style. """ - def __init__(self, sections, default_style='numpy'): + def __init__(self, sections): """Initialize. Parameters ---------- sections : {str, list/tuple of str, list/tuple of DocSection} Sections of the docstring. - default_style : {'numpy with signature', 'google', 'rst', 'numpy'} - Style of the docstring. Raises ------ @@ -39,28 +35,28 @@ def __init__(self, sections, default_style='numpy'): instances. ValueError If there are no sections. - If style is not one of 'numpy', 'numpy with signature', 'google', 'rst'. """ if isinstance(sections, (str, DocContent)): sections = [sections] - elif not (isinstance(sections, (list, tuple)) and - all(isinstance(i, (str, DocContent)) for i in sections)): - raise TypeError('Sections of the docstring must be provided as a string, list/tuple of ' - 'strings, or list/tuple of DocContent instances.') + if not ( + isinstance(sections, (list, tuple)) + and all(isinstance(i, (str, DocContent)) for i in sections) + ): + raise TypeError( + "Sections of the docstring must be provided as a string, list/tuple of strings, or " + "list/tuple of DocContent instances." + ) # NOTE: should the empty sections be allowed? - elif not sections: - raise ValueError('At least one section must be provided.') - self.sections = [section if isinstance(section, DocSection) else DocSection('', section) - for section in sections] - - if default_style not in ['numpy', 'numpy with signature', 'google', 'rst']: - raise ValueError("Default style must be one of 'numpy', 'numpy with signature', " - "'google', 'rst'.") - self.default_style = default_style + if not sections: + raise ValueError("At least one section must be provided.") + self.sections = [ + section if isinstance(section, DocSection) else DocSection("", section) + for section in sections + ] # pylint: disable=R0912 - def make_docstring(self, width=100, indent_level=0, tabsize=4, style=None): + def make_docstring(self, width=100, indent_level=0, tabsize=4, style="numpy"): """Return the docstring in the given style. Parameters @@ -74,9 +70,8 @@ def make_docstring(self, width=100, indent_level=0, tabsize=4, style=None): tabsize : {int, 4} Number of spaces that corresponds to a tab. Default is 4. - style : {'numpy', 'google', 'rst', 'numpy with signature', None} + style : str Style of the docstring. - Default is the `default_style`. Returns ------- @@ -101,60 +96,51 @@ def make_docstring(self, width=100, indent_level=0, tabsize=4, style=None): line of the docstring (including the triple quotation) or the second line. """ - if style is None: - style = self.default_style # check input if not isinstance(width, int): - raise TypeError('Maximum width of the line must be given as an integer.') - elif width <= 0: - raise ValueError('Maximum width of the line must be greater than zero.') + raise TypeError("Maximum width of the line must be given as an integer.") + if width <= 0: + raise ValueError("Maximum width of the line must be greater than zero.") if not isinstance(indent_level, int): - raise TypeError('Level of indentation must be given as an integer.') - elif indent_level < 0: - raise ValueError('Level of indentation must be greater than or equal to zero.') + raise TypeError("Level of indentation must be given as an integer.") + if indent_level < 0: + raise ValueError("Level of indentation must be greater than or equal to zero.") if not isinstance(tabsize, int): - raise TypeError('Number of spaces in a tab must be given as an integer.') - elif tabsize <= 0: - raise ValueError('Number of spaces in a tab must be greater than zero.') - - if style == 'numpy': - docstring_func = 'make_numpy_docstring' - elif style == 'numpy with signature': - docstring_func = 'make_numpy_docstring_signature' - elif style == 'google': - docstring_func = 'make_google_docstring' - elif style == 'rst': - docstring_func = 'make_rst_docstring' - else: - raise ValueError("Given docstring style must be one of 'numpy', 'numpy with signature'," - " 'google', 'rst'.") + raise TypeError("Number of spaces in a tab must be given as an integer.") + if tabsize <= 0: + raise ValueError("Number of spaces in a tab must be greater than zero.") # FIXME: this may not be necessary and can be removed if not self.check_section_order(style): - raise ValueError('Sections must be ordered according to the guideline set by the given ' - 'docstring style.') + raise ValueError( + "Sections must be ordered according to the guideline set by the given " + "docstring style." + ) - output = '' + output = "" # check that first section does not have a header - if self.sections[0].header != '': - raise ValueError('First section of the docstring (summary) must have an empty header.') + if self.sections[0].header != "": + raise ValueError("First section of the docstring (summary) must have an empty header.") # add summary summary = Summary(self.sections[0].contents[0]) - output += summary.make_docstring(width, indent_level, tabsize, - summary_only=(len(self.sections) == - len(self.sections[0].contents) == 1)) + output += summary.make_docstring( + width, + indent_level, + tabsize, + summary_only=(len(self.sections) == len(self.sections[0].contents) == 1), + ) # add remaining summary if len(self.sections[0].contents) > 1: - summary = DocSection('', self.sections[0].contents[1:]) - output += getattr(summary, docstring_func)(width, indent_level, tabsize) + summary = DocSection("", self.sections[0].contents[1:]) + output += summary.make_docstring(width, indent_level, tabsize, style) # add other sections if len(self.sections) > 1: for section in self.sections[1:]: - output += getattr(section, docstring_func)(width, indent_level, tabsize) + output += section.make_docstring(width, indent_level, tabsize, style) # add whitespace to indent the triple quotation - output += ' ' * indent_level * tabsize + output += " " * indent_level * tabsize return output # TODO: add ordering of the other styles. It seems that only numpy cares about the ordering of @@ -186,16 +172,32 @@ def check_section_order(self, style): # sections with values over 99) default_value = 99 # only numpy seems to care about the ordering - if style == 'numpy': - ordering = {'': 0, 'parameters': 1, 'attributes': 2, 'methods': 3, 'returns': 4, - 'yields': 4, 'other parameters': 5, 'raises': 6, 'warns': 7, 'warnings': 8, - 'see also': 9, 'notes': 10, 'references': 11, 'examples': 12} + if style == "numpy": + ordering = { + "": 0, + "parameters": 1, + "attributes": 2, + "methods": 3, + "returns": 4, + "yields": 4, + "other parameters": 5, + "raises": 6, + "warns": 7, + "warnings": 8, + "see also": 9, + "notes": 10, + "references": 11, + "examples": 12, + } allow_other_sections = False else: ordering = {} - order_values = [ordering.get(section.header.lower(), default_value) - for section in self.sections] + order_values = [ + ordering.get(section.header.lower(), default_value) for section in self.sections + ] if not allow_other_sections and any(i == default_value for i in order_values): - raise ValueError('For the docstring style, {0}, the headings of the sections must be ' - 'one of {1}'.format(style, list(ordering.keys()))) + raise ValueError( + "For the docstring style, {0}, the headings of the sections must be one of {1}." + "".format(style, list(ordering.keys())) + ) return all(i <= j for i, j in zip(order_values, order_values[1:])) diff --git a/docinstance/parser/__init__.py b/src/docinstance/parser/__init__.py similarity index 100% rename from docinstance/parser/__init__.py rename to src/docinstance/parser/__init__.py diff --git a/docinstance/parser/latex.py b/src/docinstance/parser/latex.py similarity index 81% rename from docinstance/parser/latex.py rename to src/docinstance/parser/latex.py index 1bf741b..4857c27 100644 --- a/docinstance/parser/latex.py +++ b/src/docinstance/parser/latex.py @@ -1,6 +1,7 @@ """Parser for Latex equations.""" -import re import inspect +import re + from docinstance.content.equation import DocEquation @@ -19,7 +20,7 @@ def is_math(text): False otherwise. """ - re_math = re.compile(r'^\s*\.\.\s*math::\n*(?:\n\s+.+)+\n*$') + re_math = re.compile(r"^\s*\.\.\s*math::\n*(?:\n\s+.+)+\n*$") return bool(re_math.search(text)) @@ -38,17 +39,17 @@ def parse_equation(text): Equations are stored as instances of DocEquation. """ - re_math = re.compile(r'\s*(\.\.\s*math::\n*(?: +.+\n?)+)\n*') + re_math = re.compile(r"\s*(\.\.\s*math::\n*(?: +.+\n?)+)\n*") # split equations split_text = [] for block in re_math.split(text): # remove empty blocks - if block == '': + if block == "": continue # remove trailing newline if is_math(block): - block = re.sub(r'\n*$', '', block) - block = re.sub(r'^\s*\.\.\s*math::\n*', '', block) + block = re.sub(r"\n*$", "", block) + block = re.sub(r"^\s*\.\.\s*math::\n*", "", block) block = inspect.cleandoc(block) block = DocEquation(block) split_text.append(block) diff --git a/src/docinstance/parser/napoleon.py b/src/docinstance/parser/napoleon.py new file mode 100644 index 0000000..48c618b --- /dev/null +++ b/src/docinstance/parser/napoleon.py @@ -0,0 +1,326 @@ +"""Napoleon parser.""" +from docinstance.content.description import DocDescription, DocParagraph +from docinstance.content.section import ( # pylint: disable=E0611 + Attributes, + DocSection, + Examples, + ExtendedSummary, + Methods, + Notes, + OtherParameters, + Parameters, + Raises, + References, + Returns, + SeeAlso, + Summary, + Warnings, + Warns, + Yields, +) +from docinstance.docstring import Docstring +from docinstance.utils import wrap, wrap_indent_subsequent +from sphinxcontrib.napoleon.docstring import GoogleDocstring, NumpyDocstring, _directive_regex + + +class GoogleDocstringDocinstance(GoogleDocstring): + def _parse_attributes_section(self, section): + fields = self._consume_fields() + return [ + Attributes( + [ + DocDescription(field[0], types=field[1] if field[1] else None, descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_keyword_arguments_section(self, section): + fields = self._consume_fields() + return [ + DocSection( + "keyword arguments", + [ + DocDescription(field[0], types=field[1] if field[1] else None, descs=field[2]) + for field in fields + ], + ) + ] + + def _parse_other_parameters_section(self, section): + fields = self._consume_fields() + return [ + OtherParameters( + [ + DocDescription(field[0], types=field[1] if field[1] else None, descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_parameters_section(self, section): + fields = self._consume_fields() + return [ + Parameters( + [ + DocDescription(field[0], types=field[1] if field[1] else None, descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_returns_section(self, section): + fields = self._consume_returns_section() + return [ + Returns( + [ + DocDescription(field[1], descs=field[2]) + if field[0] == "" + else DocDescription(field[0], types=field[1], descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_yields_section(self, section): + fields = self._consume_returns_section() + return [ + Yields( + [ + DocDescription(field[1], descs=field[2]) + if field[0] == "" + else DocDescription(field[0], types=field[1], descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_methods_section(self, section): + fields = self._consume_fields(parse_type=False) + return [Methods([DocDescription(field[0], descs=field[2]) for field in fields])] + + def _parse_raises_section(self, section): + fields = self._consume_fields(parse_type=False, prefer_type=True) + return [Raises([DocDescription(field[1], descs=field[2]) for field in fields])] + + def _parse_see_also_section(self, section): + # type (unicode) -> List[unicode] + lines = self._consume_to_next_section() + return [SeeAlso(lines)] + + def _parse_warns_section(self, section): + fields = self._consume_fields() + return [ + Warns( + [ + DocDescription(field[0], types=field[1] if field[1] else None, descs=field[2]) + for field in fields + ] + ) + ] + + def _parse_generic_section(self, section, use_admonition): + lines = self._strip_empty(self._consume_to_next_section()) + lines = self._dedent(lines) + if use_admonition: + lines = self._indent(lines, 3) + return [DocSection(section, [DocParagraph(line, num_newlines_end=1) for line in lines])] + + def _parse_examples_section(self, section): + labels = {"example": "Example", "examples": "Examples"} + use_admonition = self._config.napoleon_use_admonition_for_examples + label = labels.get(section.lower(), section) + return self._parse_generic_section(label, use_admonition) + + def _parse_notes_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_notes + return self._parse_generic_section("Notes", use_admonition) + + def _parse_references_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_references + return self._parse_generic_section("References", use_admonition) + + +class NumpyDocstringDocinstance(NumpyDocstring, GoogleDocstringDocinstance): + # _parse_admonition = GoogleDocstringDocinstance._parse_admonition + # _parse_generic_section = GoogleDocstringDocinstance._parse_generic_section + # _parse_attributes_section = GoogleDocstringDocinstance._parse_attributes_section + # _parse_examples_section = GoogleDocstringDocinstance._parse_examples_section + # _parse_keyword_arguments_section = GoogleDocstringDocinstance._parse_keyword_arguments_section + # _parse_methods_section = GoogleDocstringDocinstance._parse_methods_section + # _parse_notes_section = GoogleDocstringDocinstance._parse_notes_section + # _parse_other_parameters_section = GoogleDocstringDocinstance._parse_other_parameters_section + # _parse_parameters_section = GoogleDocstringDocinstance._parse_parameters_section + # _parse_returns_section = GoogleDocstringDocinstance._parse_returns_section + # _parse_raises_section = GoogleDocstringDocinstance._parse_raises_section + # _parse_references_section = GoogleDocstringDocinstance._parse_references_section + # _parse_see_also_section = GoogleDocstringDocinstance._parse_see_also_section + # _parse_warns_section = GoogleDocstringDocinstance._parse_warns_section + # _parse_yields_section = GoogleDocstringDocinstance._parse_yields_section + def _parse_see_also_section(self, section): + # type: (unicode) -> List[unicode] + lines = self._consume_to_next_section() + try: + return self._parse_numpydoc_see_also_section(lines) + except ValueError: + lines = self._strip_empty(lines) + lines = self._dedent(lines) + return [SeeAlso(lines)] + + def _parse_numpydoc_see_also_section(self, content): # type: (List[unicode]) -> List[unicode] + """ + Derived from the NumpyDoc implementation of _parse_see_also. + + See Also + -------- + func_name : Descriptive text + continued text + another_func_name : Descriptive text + func_name1, func_name2, :meth:`func_name`, func_name3 + + """ + items = [] + + def parse_item_name(text): + # type: (unicode) -> Tuple[unicode, unicode] + """Match ':role:`name`' or 'name'""" + m = self._name_rgx.match(text) # type: ignore + if m: + g = m.groups() + if g[1] is None: + return g[3], None + else: + return g[2], g[1] + raise ValueError("%s is not a item name" % text) + + def push_item(name, rest): + # type: (unicode, List[unicode]) -> None + if not name: + return + name, role = parse_item_name(name) + items.append((name, list(rest), role)) + del rest[:] + + current_func = None + rest = [] # type: List[unicode] + + for line in content: + if not line.strip(): + continue + + m = self._name_rgx.match(line) # type: ignore + if m and line[m.end() :].strip().startswith(":"): + push_item(current_func, rest) + current_func, line = line[: m.end()], line[m.end() :] + rest = [line.split(":", 1)[1].strip()] + if not rest[0]: + rest = [] + elif not line.startswith(" "): + push_item(current_func, rest) + current_func = None + if "," in line: + for func in line.split(","): + if func.strip(): + push_item(func, []) + elif line.strip(): + current_func = line + elif current_func is not None: + rest.append(line.strip()) + push_item(current_func, rest) + + if not items: + return [] + + roles = { + "method": "meth", + "meth": "meth", + "function": "func", + "func": "func", + "class": "class", + "exception": "exc", + "exc": "exc", + "object": "obj", + "obj": "obj", + "module": "mod", + "mod": "mod", + "data": "data", + "constant": "const", + "const": "const", + "attribute": "attr", + "attr": "attr", + } # type: Dict[unicode, unicode] + if self._what is None: + func_role = "obj" # type: unicode + else: + func_role = roles.get(self._what, "") + lines = [] # type: List[unicode] + for func, desc, role in items: + if not role and func_role: + role = func_role + + lines.append(DocSeeAlsoEntryNumpy(func, role, desc)) + + return [SeeAlso(lines)] + + +class DocSeeAlsoEntryNumpy(DocDescription): + def __init__(self, name, role, desc): + super().__init__(name, types=role, descs=desc) + if len(self.descs) > 1: + raise ValueError + if len(self.types) > 1: + raise ValueError + + def make_docstring_numpy(self, width, indent_level, tabsize): + if not self.descs: + lines = wrap(self.name, width=width, indent_level=indent_level, tabsize=tabsize) + elif self.types: + lines = wrap( + "{0} : {1}".format(self.name, self.descs[0]), + width=width, + indent_level=indent_level, + tabsize=tabsize, + ) + return "\n".join(lines) + "\n" + + def make_docstring_rst(self, width, indent_level, tabsize): + role = self.types + func = self.name + desc = self.descs[0] + + if role: + link = ":%s:`%s`".format(role, func) + else: + link = "`%s`_".format(func) + + output = "\n".join(wrap(link, width=width, indent_level=indent_level, tabsize=tabsize)) + output += "\n" + output += "\n".join(wrap(desc[0], width=width, indent_level=indent_level, tabsize=tabsize)) + output += "\n" + + return output + + +def parse_numpy(docstring, contains_quotes=False): + """Parse a docstring in Numpy format using Napoleon into Docstring instance.""" + parsed_content = [i for i in NumpyDocstringDocinstance(docstring)._parsed_lines if i] + if isinstance(parsed_content[0], str): + parsed_content[0] = Summary(parsed_content[0]) + # check for any other strings + string_contents = [True if isinstance(i, str) else False for i in parsed_content] + if any(string_contents): + start_ind = string_contents.index(True) + end_ind = len(string_contents) - string_contents[::-1].index(True) - 1 + if not all(string_contents[start_ind : end_ind + 1]): + raise ValueError + parsed_content = ( + parsed_content[:start_ind] + + [ExtendedSummary(parsed_content[start_ind : end_ind + 1])] + + parsed_content[end_ind + 1 :] + ) + return Docstring(parsed_content) + + +def parse_google(docstring, contains_quotes=False): + """Parse a docstring in Google format using Napoleon into Docstring instance.""" + return GoogleDocstringDocinstance(docstring)._parsed_lines diff --git a/docinstance/parser/numpy.py b/src/docinstance/parser/numpy.py similarity index 53% rename from docinstance/parser/numpy.py rename to src/docinstance/parser/numpy.py index e726093..9b86782 100644 --- a/docinstance/parser/numpy.py +++ b/src/docinstance/parser/numpy.py @@ -1,18 +1,32 @@ """Parser for numpy docstring.""" -import re import inspect -from docinstance.parser.latex import parse_equation -from docinstance.docstring import Docstring +import re + from docinstance.content.description import DocDescription -from docinstance.content.section import (DocSection, Summary, ExtendedSummary, Parameters, - Attributes, Methods, Returns, Yields, OtherParameters, - Raises, Warns, Warnings, SeeAlso, Notes, References, - Examples) from docinstance.content.equation import DocEquation +from docinstance.content.section import ( # pylint: disable=E0611 + Attributes, + DocSection, + Examples, + ExtendedSummary, + Methods, + Notes, + OtherParameters, + Parameters, + Raises, + References, + Returns, + SeeAlso, + Summary, + Warnings, + Warns, + Yields, +) +from docinstance.docstring import Docstring +from docinstance.parser.latex import parse_equation -# pylint: disable=R0912,R0914,R0915 -def parse_numpy(docstring, contains_quotes=False): +def parse_numpy(docstring, contains_quotes=False): # pylint: disable=R0912,R0914,R0915 r"""Parse a docstring in numpy format into a Docstring instance. Multiple descriptions of the indented information (e.g. parameters, attributes, methods, @@ -47,27 +61,29 @@ def parse_numpy(docstring, contains_quotes=False): Copied from https://github.com/kimt33/pydocstring. """ - docstring = inspect.cleandoc('\n' * contains_quotes + docstring) + docstring = inspect.cleandoc("\n" * contains_quotes + docstring) # remove quotes from docstring if contains_quotes: - quotes = r'[\'\"]{3}' - if re.search(r'^r{0}'.format(quotes), docstring): - raise NotImplementedError('A raw string quotation, i.e. r""" cannot be given as a ' - 'string, i.e. from reading a python file as a string, ' - 'because the backslashes belonging to escape sequences ' - 'cannot be distinguished from those of normal backslash.' - 'You either need to change existing raw string to normal ' - 'i.e. convert all occurences of \\ to \\\\, or import the ' - 'docstring from the instance through `__doc__` attribute.') + quotes = r"[\'\"]{3}" + if re.search(r"^r{0}".format(quotes), docstring): + raise NotImplementedError( + 'A raw string quotation, i.e. r""" cannot be given as a ' + "string, i.e. from reading a python file as a string, " + "because the backslashes belonging to escape sequences " + "cannot be distinguished from those of normal backslash." + "You either need to change existing raw string to normal " + "i.e. convert all occurences of \\ to \\\\, or import the " + "docstring from the instance through `__doc__` attribute." + ) else: - quotes = r'' - docstring = re.sub(r'^{0}'.format(quotes), '', docstring) - docstring = re.sub(r'{0}$'.format(quotes), '', docstring) + quotes = r"" + docstring = re.sub(r"^{0}".format(quotes), "", docstring) + docstring = re.sub(r"{0}$".format(quotes), "", docstring) sections = [] # summary - for regex in [r'^\n?(.+?)\n\n+', r'^\n?(.*?)\n*$']: + for regex in [r"^\n?(.+?)\n\n+", r"^\n?(.*?)\n*$"]: re_summary = re.compile(regex) try: sections.append(Summary(re_summary.search(docstring).group(1))) @@ -75,19 +91,22 @@ def parse_numpy(docstring, contains_quotes=False): except AttributeError: pass else: - raise ValueError('The summary must be in the first or the second line with a blank line ' - 'afterwards.') + raise ValueError( + "The summary must be in the first or the second line with a blank line " "afterwards." + ) # remove summary from docstring - docstring = re_summary.sub('', docstring) - if docstring == '': + docstring = re_summary.sub("", docstring) + if docstring == "": return Docstring(sections) # if headers do not exist - re_header = re.compile(r'\n*(.+)\n(-+)\n+') + re_header = re.compile(r"\n*(.+)\n(-+)\n+") if re_header.search(docstring) is None: # split into blocks by math equations and multiple newlines - extended = [[lines] if isinstance(lines, DocEquation) else re.split(r'\n\n+', lines) - for lines in parse_equation(docstring)] + extended = [ + [lines] if isinstance(lines, DocEquation) else re.split(r"\n\n+", lines) + for lines in parse_equation(docstring) + ] extended = [line for lines in extended for line in lines] extended_contents = [] for block in extended: @@ -97,11 +116,11 @@ def parse_numpy(docstring, contains_quotes=False): # continue if not isinstance(block, DocEquation): # remove quotes - block = re.sub(r'\n*{0}$'.format(quotes), '', block) + block = re.sub(r"\n*{0}$".format(quotes), "", block) # remove trailing newlines - block = re.sub(r'\n+$', '', block) + block = re.sub(r"\n+$", "", block) # replace newlines - block = block.replace('\n', ' ') + block = block.replace("\n", " ") extended_contents.append(block) sections.append(ExtendedSummary(extended_contents)) return Docstring(sections) @@ -113,8 +132,10 @@ def parse_numpy(docstring, contains_quotes=False): extended, *split_docstring = split_docstring # FIXME: repeated code # extract math and split blocks - extended = [[lines] if isinstance(lines, DocEquation) else re.split(r'\n\n+', lines) - for lines in parse_equation(extended)] + extended = [ + [lines] if isinstance(lines, DocEquation) else re.split(r"\n\n+", lines) + for lines in parse_equation(extended) + ] extended = [line for lines in extended for line in lines] # process blocks processed_extended = [] @@ -125,79 +146,95 @@ def parse_numpy(docstring, contains_quotes=False): # continue if not isinstance(block, DocEquation): # remove quotes - block = re.sub(r'\n*{0}$'.format(quotes), '', block) + block = re.sub(r"\n*{0}$".format(quotes), "", block) # remove trailing newlines - block = re.sub(r'\n+$', '', block) + block = re.sub(r"\n+$", "", block) # replace newlines - block = block.replace('\n', ' ') + block = block.replace("\n", " ") processed_extended.append(block) if processed_extended != []: sections.append(ExtendedSummary(processed_extended)) - headers_sections = {'parameters': Parameters, 'other parameters': OtherParameters, - 'attributes': Attributes, 'methods': Methods, 'returns': Returns, - 'yields': Yields, 'raises': Raises, 'see also': SeeAlso, 'warns': Warns, - 'warnings': Warnings, 'examples': Examples, 'references': References, - 'notes': Notes, 'properties': None, 'abstract properties': None, - 'abstract methods': None} - for header, lines, contents in zip(split_docstring[0::3], - split_docstring[1::3], - split_docstring[2::3]): - contents = re.sub(r'\n+$', r'\n', contents) + headers_sections = { + "parameters": Parameters, + "other parameters": OtherParameters, + "attributes": Attributes, + "methods": Methods, + "returns": Returns, + "yields": Yields, + "raises": Raises, + "see also": SeeAlso, + "warns": Warns, + "warnings": Warnings, + "examples": Examples, + "references": References, + "notes": Notes, + "properties": None, + "abstract properties": None, + "abstract methods": None, + } + for header, lines, contents in zip( + split_docstring[0::3], split_docstring[1::3], split_docstring[2::3] + ): + contents = re.sub(r"\n+$", r"\n", contents) if len(header) != len(lines): - raise ValueError('Need {0} of `-` underneath the header title, {1}' - ''.format(len(header), header)) + raise ValueError( + "Need {0} of `-` underneath the header title, {1}" "".format(len(header), header) + ) header = header.lower() header_contents = [] # special headers (special format for each entry) if header in headers_sections: - entries = (entry for entry in re.split(r'\n(?!\s+)', contents) if entry != '') + entries = (entry for entry in re.split(r"\n(?!\s+)", contents) if entry != "") # FIXME: following regular expression would work only if docstring has spaces adjacent # to ':' - re_entry = re.compile(r'^(.+?)(\(.+?\))?(?: *: *(.+))?(?:\n|$)') + re_entry = re.compile(r"^(.+?)(\(.+?\))?(?: *: *(.+))?(?:\n|$)") for entry in entries: # keep only necessary pieces _, name, signature, types, descs = re_entry.split(entry) # process signature if signature is None: - signature = '' + signature = "" else: - signature = ', '.join(i.strip() for i in signature.split(',')) + signature = ", ".join(i.strip() for i in signature.split(",")) # process types if types is None: types = [] - elif re.search(r'\{.+\}', types): - types = re.search(r'^\{((?:(.+?),\s*)*(.+?))\}$', types).group(1) - types = re.split(r',\s*', types) + elif re.search(r"\{.+\}", types): + types = re.search(r"^\{((?:(.+?),\s*)*(.+?))\}$", types).group(1) + types = re.split(r",\s*", types) else: - types = re.search(r'^((?:(.+?),\s*)*(.+?))$', types).group(1) - types = re.split(r',\s*', types) + types = re.search(r"^((?:(.+?),\s*)*(.+?))$", types).group(1) + types = re.split(r",\s*", types) types = [i for i in types if i is not None] # process documentation - descs = inspect.cleandoc('\n' + descs) + descs = inspect.cleandoc("\n" + descs) # NOTE: period is used to terminate a description. i.e. one description is # distinguished from another with a period and a newline. - descs = re.split(r'\.\n+', descs) + descs = re.split(r"\.\n+", descs) # add period (only the last line is not missing the period) - descs = [line + '.' for line in descs[:-1]] + descs[-1:] + descs = [line + "." for line in descs[:-1]] + descs[-1:] # extract equations descs = [line for lines in descs for line in parse_equation(lines)] # non math blocks will replace newlines with spaces. # math blocks will add newline at the end - descs = [line if isinstance(line, DocEquation) else line.replace('\n', ' ') - for line in descs] + descs = [ + line if isinstance(line, DocEquation) else line.replace("\n", " ") + for line in descs + ] # store - header_contents.append(DocDescription(name, signature=signature, types=types, - descs=descs)) + header_contents.append( + DocDescription(name, signature=signature, types=types, descs=descs) + ) else: - header_contents = [i for i in re.split(r'\n\n+', contents) if i != ''] + header_contents = [i for i in re.split(r"\n\n+", contents) if i != ""] try: sections.append(headers_sections[header](header_contents)) except KeyError: diff --git a/docinstance/utils.py b/src/docinstance/utils.py similarity index 82% rename from docinstance/utils.py rename to src/docinstance/utils.py index 8bee280..a7b2cde 100644 --- a/docinstance/utils.py +++ b/src/docinstance/utils.py @@ -1,7 +1,7 @@ """Utility functions for handling strings and attributes of an object.""" -import textwrap import inspect import os +import textwrap def wrap(text, width=100, indent_level=0, tabsize=4, **kwargs): @@ -48,30 +48,34 @@ def wrap(text, width=100, indent_level=0, tabsize=4, **kwargs): If is a word plus its indentation is longer than the width. """ - kwargs.setdefault('expand_tabs', True) - kwargs.setdefault('replace_whitespace', False) - kwargs.setdefault('drop_whitespace', True) - kwargs.setdefault('break_long_words', False) - kwargs['tabsize'] = tabsize + kwargs.setdefault("expand_tabs", True) + kwargs.setdefault("replace_whitespace", False) + kwargs.setdefault("drop_whitespace", True) + kwargs.setdefault("break_long_words", False) + kwargs["tabsize"] = tabsize if width <= tabsize * indent_level: - raise ValueError('Amount of indentation must be less than the maximum width.') - kwargs['width'] = width - tabsize * indent_level + raise ValueError("Amount of indentation must be less than the maximum width.") + kwargs["width"] = width - tabsize * indent_level # NOTE: uncomment to remove whitespace at the beginning and the end # text = text.strip() # Acknowledge all of the newlines (start, middle, and end) - lines = text.split('\n') + lines = text.split("\n") # wrap each line (separated by newline) separately - wrapped_lines = [wrapped_line - for unwrapped_line in lines - for wrapped_line in - (textwrap.wrap(unwrapped_line, **kwargs) if unwrapped_line != '' else [''])] + wrapped_lines = [ + wrapped_line + for unwrapped_line in lines + for wrapped_line in ( + textwrap.wrap(unwrapped_line, **kwargs) if unwrapped_line != "" else [""] + ) + ] # indent - output = [' ' * tabsize * indent_level + line for line in wrapped_lines] + output = [" " * tabsize * indent_level + line for line in wrapped_lines] if any(len(line) > width for line in output): - raise ValueError('There cannot be any word (after indentation) that exceeds the maximum ' - 'width') + raise ValueError( + "There cannot be any word (after indentation) that exceeds the maximum " "width" + ) return output @@ -102,8 +106,9 @@ def wrap_indent_subsequent(text, width=100, indent_level=0, tabsize=4): discarded only if the first word cannot fit into the given line width with the whitespace. """ - output = wrap(text, width=width, indent_level=0, tabsize=0, - subsequent_indent=' ' * tabsize * indent_level) + output = wrap( + text, width=width, indent_level=0, tabsize=0, subsequent_indent=" " * tabsize * indent_level + ) return output diff --git a/docinstance/wrapper.py b/src/docinstance/wrapper.py similarity index 93% rename from docinstance/wrapper.py rename to src/docinstance/wrapper.py index e5d8e53..6b0e799 100644 --- a/docinstance/wrapper.py +++ b/src/docinstance/wrapper.py @@ -1,8 +1,9 @@ """Functions for wrapping a python object to utilize the docinstance object.""" -import inspect -import os from functools import wraps import importlib +import inspect +import os + from docinstance.utils import extract_members @@ -14,6 +15,7 @@ def kwarg_wrapper(wrapper): (with another wrapper). """ + @wraps(wrapper) def new_wrapper(obj=None, **kwargs): """Reconstruction of the provided wrapper so that keyword arguments are rewritten. @@ -67,7 +69,7 @@ def docstring(obj, width=100, indent_level=0, tabsize=4): """ # TODO: if there is a parser, parse the docstring into docinstance - if not hasattr(obj, '_docinstance'): + if not hasattr(obj, "_docinstance"): return obj # generate new docstring from docinstance # pylint: disable=W0212 @@ -127,7 +129,7 @@ def docstring_recursive(obj, width=100, indent_level=0, tabsize=4): # wrap members for member in extract_members(obj).values(): # recurse for all members of member - docstring_recursive(member, width=width, indent_level=indent_level+1, tabsize=tabsize) + docstring_recursive(member, width=width, indent_level=indent_level + 1, tabsize=tabsize) return obj @@ -153,8 +155,11 @@ def docstring_current_module(width=100, tabsize=4): """ # get the module that called this function - module = next(inspect.getmodule(i[0]) for i in inspect.stack()[1:] # pragma: no branch - if inspect.getmodule(i[0]) is not None) + module = next( # pragma: no branch + inspect.getmodule(i[0]) + for i in inspect.stack()[1:] # pragma: no branch + if inspect.getmodule(i[0]) is not None + ) # recursively convert # NOTE: docstring for the module will always not be indented docstring_recursive(module, width=width, indent_level=0, tabsize=tabsize) @@ -190,8 +195,11 @@ def docstring_modify_import(width=100, tabsize=4): """ # find the location from which this function is called - parentfile = next(inspect.getsourcefile(i[0]) for i in inspect.stack()[1:] # pragma: no branch - if inspect.getmodule(i[0]) is not None) + parentfile = next( # pragma: no branch + inspect.getsourcefile(i[0]) + for i in inspect.stack()[1:] # pragma: no branch + if inspect.getmodule(i[0]) is not None + ) parentdir = os.path.dirname(parentfile) def pandora_box(old_import): @@ -211,7 +219,7 @@ def pandora_box(old_import): recursively scanning the __dict__. """ - if not hasattr(old_import, 'special_cases'): + if not hasattr(old_import, "special_cases"): old_import.special_cases = set() old_import.special_cases.add(parentdir) @@ -225,8 +233,10 @@ def new_import(*args, **kwargs): # decorator will be applied only to `exc_module`. module = args[1] sourcefile = inspect.getsourcefile(module) - if any(os.path.commonpath([sourcefile, parent]) == parent - for parent in old_import.special_cases): + if any( + os.path.commonpath([sourcefile, parent]) == parent + for parent in old_import.special_cases + ): docstring_recursive(module, width=width, indent_level=0, tabsize=tabsize) new_import.special_cases = old_import.special_cases diff --git a/docinstance/content/test/test_base.py b/tests/test_base.py similarity index 58% rename from docinstance/content/test/test_base.py rename to tests/test_base.py index 4498f49..6044ebd 100644 --- a/docinstance/content/test/test_base.py +++ b/tests/test_base.py @@ -1,18 +1,20 @@ """Test docinstance.content.base.""" -import pytest from docinstance.content.base import DocContent +import pytest class ModDocContent(DocContent): """DocContent where the init does not raise an error.""" + def __init__(self): + """Initialize.""" pass -def test_base_init(): - """Test DocContent.__init__.""" - with pytest.raises(NotImplementedError): - DocContent() +class Empty: + """Empty class.""" + + pass def test_base_eq(): @@ -25,13 +27,10 @@ def test_base_eq(): test2.y = 2 assert not test1 == test2 - class Empty: - """Empty class.""" - pass test2 = Empty() test2.x = 1 assert not test1 == test2 - test2 = {'x': 1} + test2 = {"x": 1} assert not test1 == test2 @@ -45,32 +44,27 @@ def test_base_ne(): test2.y = 2 assert test1 != test2 - class Empty: - """Empty class.""" - pass test2 = Empty() test2.x = 1 assert test1 != test2 - test2 = {'x': 1} + test2 = {"x": 1} assert test1 != test2 -def test_base_make_numpy_docstring(): - """Test DocContent.make_numpy_docstring.""" +def test_base_make_docstring(): + """Test DocContent.make_docstring.""" test = ModDocContent() - with pytest.raises(NotImplementedError): - test.make_numpy_docstring(100, 0, 4) + with pytest.raises(TypeError): + test.make_docstring(100, 0, 4, "") + with pytest.raises(TypeError): + test.make_docstring(100, 0, 4, ["numpy"]) + test.make_docstring_test = lambda width, indent_level, tabsize: "answer" + assert test.make_docstring(100, 0, 4, "test") == "answer" -def test_base_make_google_docstring(): - """Test DocContent.make_google_docstring.""" - test = ModDocContent() with pytest.raises(NotImplementedError): - test.make_google_docstring(100, 0, 4) - - -def test_base_make_rst_docstring(): - """Test DocContent.make_rst_docstring.""" - test = ModDocContent() + test.make_docstring(100, 0, 4, "numpy") + with pytest.raises(NotImplementedError): + test.make_docstring(100, 0, 4, "google") with pytest.raises(NotImplementedError): - test.make_rst_docstring(100, 0, 4) + test.make_docstring(100, 0, 4, "rst") diff --git a/tests/test_description.py b/tests/test_description.py new file mode 100644 index 0000000..7dfa0d1 --- /dev/null +++ b/tests/test_description.py @@ -0,0 +1,226 @@ +"""Test docinstance.content.description.""" +from docinstance.content.description import DocDescription +import pytest + + +def test_init(): + """Test DocDescription.__init__.""" + with pytest.raises(TypeError): + DocDescription([]) + with pytest.raises(TypeError): + DocDescription(2) + with pytest.raises(TypeError): + DocDescription("test", signature=1) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=1) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=[1]) + with pytest.raises(TypeError): + DocDescription("test", signature="", types={str}) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=[str, 1]) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=str, descs=1) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=str, descs=[1]) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=str, descs={"1"}) + with pytest.raises(TypeError): + DocDescription("test", signature="", types=str, descs=["1", 2]) + + test = DocDescription("test") + assert test.name == "test" + assert test.signature == "" + assert test.types == [] + assert test.descs == [] + test = DocDescription("test", signature="1") + assert test.signature == "1" + test = DocDescription("test", types=str) + assert test.types == [str] + test = DocDescription("test", types=(str,)) + assert test.types == [str] + test = DocDescription("test", descs="2") + assert test.descs == ["2"] + test = DocDescription("test", descs=("2",)) + assert test.descs == ["2"] + + +def test_types_str(): + """Test DocDescription.types_str.""" + test = DocDescription("test", types=[str, int, "list of str"]) + assert test.types_str == ["str", "int", "list of str"] + + +def test_make_docstring_numpy(): + """Test DocDescription.make_docstring_numpy.""" + # no type + test = DocDescription("var_name", descs=["hello"]) + assert test.make_docstring_numpy(9, 0, 4) == "var_name\n hello\n" + # one type + test = DocDescription("var_name", types=str, descs=["hello"]) + with pytest.raises(ValueError): + test.make_docstring_numpy(13, 0, 4) + with pytest.raises(ValueError): + test.make_docstring_numpy(14, 1, 2) + assert test.make_docstring_numpy(14, 0, 4) == "var_name : str\n hello\n" + assert test.make_docstring_numpy(16, 1, 2) == " var_name : str\n hello\n" + # multiple types + test = DocDescription("var_name", types=[str, int, bool], descs=["hello"]) + with pytest.raises(ValueError): + test.make_docstring_numpy(15, 0, 4) + with pytest.raises(ValueError): + test.make_docstring_numpy(16, 1, 2) + # NOTE: following raises an error even though width is big enough for 'var_name : {str,' because + # the `utils.wrap` complains + with pytest.raises(ValueError): + test.make_docstring_numpy(16, 0, 4) + assert test.make_docstring_numpy(27, 0, 4) == "var_name : {str, int, bool}\n hello\n" + assert ( + test.make_docstring_numpy(26, 0, 4) + == "var_name : {str, int,\n bool}\n hello\n" + ) + assert ( + test.make_docstring_numpy(27, 1, 2) + == " var_name : {str, int,\n bool}\n hello\n" + ) + assert ( + test.make_docstring_numpy(17, 0, 4) + == "var_name : {str,\n int,\n bool}\n hello\n" + ) + # signature does nothing + test2 = DocDescription( + "var_name", signature="(a, b, c)", types=[str, int, bool], descs=["hello"] + ) + assert test.make_docstring_numpy(17, 0, 4) == test2.make_docstring_numpy(17, 0, 4) + # multiple paragraphs + test = DocDescription( + "var_name", + types=[str, int, bool], + descs=["description 1", "description 2", "description 3"], + ) + assert ( + test.make_docstring_numpy(27, 0, 4) + == "var_name : {str, int, bool}\n description 1\n description 2\n" + " description 3\n" + ) + + +def test_make_docstring_numpy_signature(): + """Test DocDescription.make_docstring_numpy_signature.""" + test = DocDescription( + "var_name", signature="(a, b, c)", types=[str, int, bool], descs=["hello"] + ) + assert ( + test.make_docstring_numpy_signature(36, 0, 4) + == "var_name(a, b, c) : {str, int, bool}\n hello\n" + ) + assert ( + test.make_docstring_numpy_signature(26, 0, 4) + == "var_name(a, b, c) : {str,\n int,\n bool}\n" + " hello\n" + ) + + +def test_make_docstring_google(): + """Test DocDescription.make_docstring_google.""" + # no type, desc + test = DocDescription("var_name", descs=["hello"]) + assert test.make_docstring_google(15, 0, 4) == "var_name: hello\n" + # no type, no descs + test = DocDescription("var_name") + assert test.make_docstring_google(9, 0, 4) == "var_name:\n" + with pytest.raises(ValueError): + test.make_docstring_google(8, 0, 4) + # one type, no descs + test = DocDescription("var_name", types=str) + assert test.make_docstring_google(22, 0, 4) == "var_name (:obj:`str`):\n" + with pytest.raises(ValueError): + test.make_docstring_google(21, 0, 4) + # one type, no descs + test = DocDescription("var_name", types="str") + assert test.make_docstring_google(15, 0, 4) == "var_name (str):\n" + with pytest.raises(ValueError): + test.make_docstring_google(14, 0, 4) + # many types, no descs + test = DocDescription("var_name", types=["str", int]) + assert test.make_docstring_google(22, 0, 4) == ("var_name (str,\n" " :obj:`int`):\n") + with pytest.raises(ValueError): + test.make_docstring_google(21, 0, 4) + assert test.make_docstring_google(23, 1, 1) == (" var_name (str,\n" " :obj:`int`):\n") + # one type, desc + test = DocDescription("var_name", types=str, descs=["hello"]) + assert test.make_docstring_google(22, 0, 4) == "var_name (:obj:`str`):\n hello\n" + # FIXME + assert test.make_docstring_google(24, 1, 2) == " var_name (:obj:`str`):\n hello\n" + # multiple types + test = DocDescription("var_name", types=[str, int, bool], descs=["hello"]) + with pytest.raises(ValueError): + test.make_docstring_google(22, 0, 4) + with pytest.raises(ValueError): + test.make_docstring_google(24, 1, 2) + assert test.make_docstring_google(23, 0, 4) == ( + "var_name (:obj:`str`,\n" + " :obj:`int`,\n" + " :obj:`bool`):\n" + " hello\n" + ) + assert test.make_docstring_google(26, 1, 2) == ( + " var_name (:obj:`str`,\n" + " :obj:`int`,\n" + " :obj:`bool`):\n" + " hello\n" + ) + assert test.make_docstring_google(35, 1, 2) == ( + " var_name (:obj:`str`, :obj:`int`,\n" " :obj:`bool`): hello\n" + ) + # signature does nothing + test2 = DocDescription( + "var_name", signature="(a, b, c)", types=[str, int, bool], descs=["hello"] + ) + assert test.make_docstring_google(23, 0, 4) == test2.make_docstring_google(23, 0, 4) + # multiple paragraphs + test = DocDescription( + "var_name", + types=[str, int, bool], + descs=["description 1", "description 2", "description 3"], + ) + assert test.make_docstring_google(26, 0, 2) == ( + "var_name (:obj:`str`,\n" + " :obj:`int`,\n" + " :obj:`bool`):\n" + " description 1\n" + " description 2\n" + " description 3\n" + ) + + +def test_make_docstring_rst(): + """Test DocDescription.make_docstring_rst.""" + # only name + test = DocDescription("var_name") + assert test.make_docstring_rst(16, 0, 1) == ":param var_name:\n" + assert test.make_docstring_rst(17, 1, 1) == " :param var_name:\n" + with pytest.raises(ValueError): + test.make_docstring_rst(15, 0, 4) + # name + desc + test = DocDescription("var_name", descs="hello") + assert test.make_docstring_rst(22, 0, 1) == ":param var_name: hello\n" + assert test.make_docstring_rst(21, 0, 1) == (":param var_name:\n" " hello\n") + assert test.make_docstring_rst(21, 0, 4) == (":param var_name:\n" " hello\n") + test = DocDescription("var_name", descs=["hello my name is", "Example 2."]) + assert test.make_docstring_rst(25, 0, 4) == ( + ":param var_name: hello my\n" " name is\n" " Example 2.\n" + ) + # name + type + test = DocDescription("var_name", types=["str", int]) + assert test.make_docstring_rst(20, 0, 2) == ( + ":param var_name:\n" ":type var_name: str,\n" " :obj:`int`\n" + ) + # name + desc + type + test = DocDescription("var_name", types=["str", int], descs=["Example 1.", "Example 2."]) + assert test.make_docstring_rst(27, 0, 4) == ( + ":param var_name: Example 1.\n" + " Example 2.\n" + ":type var_name: str,\n" + " :obj:`int`\n" + ) diff --git a/tests/test_docstring.py b/tests/test_docstring.py new file mode 100644 index 0000000..1b12e32 --- /dev/null +++ b/tests/test_docstring.py @@ -0,0 +1,181 @@ +"""Test docinstance.docstring.""" +from docinstance.content.description import DocDescription +from docinstance.content.section import DocSection +from docinstance.docstring import Docstring +import pytest + + +def test_init(): + """Test Docstring.__init__.""" + with pytest.raises(TypeError): + Docstring(1) + with pytest.raises(TypeError): + Docstring({"1"}) + with pytest.raises(TypeError): + Docstring([1]) + with pytest.raises(TypeError): + Docstring(["1", 1]) + with pytest.raises(ValueError): + Docstring([]) + + test = Docstring("some text") + assert isinstance(test.sections, list) + assert len(test.sections) == 1 + assert isinstance(test.sections[0], DocSection) + assert test.sections[0].header == "" + assert test.sections[0].contents == ["some text"] + + test = Docstring(DocSection("some header", "Hello World")) + assert isinstance(test.sections, list) + assert len(test.sections) == 1 + assert isinstance(test.sections[0], DocSection) + assert test.sections[0].header == "some header" + assert test.sections[0].contents == ["Hello World"] + + test = Docstring([DocSection("some header", "Hello World"), "some text"]) + assert isinstance(test.sections, list) + assert len(test.sections) == 2 + assert isinstance(test.sections[0], DocSection) + assert test.sections[0].header == "some header" + assert test.sections[0].contents == ["Hello World"] + assert isinstance(test.sections[1], DocSection) + assert test.sections[1].header == "" + assert test.sections[1].contents == ["some text"] + + +def test_make_docstring(): + """Test Docstring.make_docstring.""" + test = Docstring(["summary", "extended summary", DocSection("parameters", "")]) + # standard input check + with pytest.raises(TypeError): + test.make_docstring(width=100.0) + with pytest.raises(ValueError): + test.make_docstring(width=-2) + with pytest.raises(ValueError): + test.make_docstring(width=0) + with pytest.raises(TypeError): + test.make_docstring(indent_level=2.0) + with pytest.raises(ValueError): + test.make_docstring(indent_level=-1) + with pytest.raises(TypeError): + test.make_docstring(tabsize=2.0) + with pytest.raises(ValueError): + test.make_docstring(tabsize=-2) + with pytest.raises(ValueError): + test.make_docstring(tabsize=0) + with pytest.raises(NotImplementedError): + test.make_docstring(style="random style") + # bad ordering + test = Docstring(["summary", DocSection("parameters", ""), "extended summary"]) + with pytest.raises(ValueError): + test.make_docstring(style="numpy") + # check summary errors + test = Docstring([DocSection("parameters", DocDescription("something"))]) + with pytest.raises(ValueError): + test.make_docstring() + # one line summary + test = Docstring("very very long summary") + assert test.make_docstring(width=25) == "very very long summary\n\n" + assert test.make_docstring(width=24) == "\nvery very long summary\n\n" + # multiple line summary + test = Docstring(["very very long summary", "extended summary"]) + assert test.make_docstring(width=25) == "very very long summary\n\nextended summary\n\n" + assert test.make_docstring(width=24) == "\nvery very long summary\n\nextended summary\n\n" + test = Docstring(DocSection("", ["very very long summary", "extended summary"])) + assert test.make_docstring(width=25) == "very very long summary\n\nextended summary\n\n" + assert test.make_docstring(width=24) == "\nvery very long summary\n\nextended summary\n\n" + # other sections + test = Docstring( + [ + DocSection("", ["very very long summary", "extended summary"]), + DocSection("parameters", DocDescription("var1", types=str, descs="Example.")), + ] + ) + assert ( + test.make_docstring(width=25) == "very very long summary\n\nextended summary\n\n" + "Parameters\n----------\nvar1 : str\n Example.\n\n" + ) + assert ( + test.make_docstring(width=24) == "\nvery very long summary\n\nextended summary\n\n" + "Parameters\n----------\nvar1 : str\n Example.\n\n" + ) + # numpy with signature + test = Docstring( + [ + "summary", + DocSection( + "methods", DocDescription("func1", signature="(a, b)", types=str, descs="Example.") + ), + ] + ) + assert ( + test.make_docstring(width=25, style="numpy_signature") + == "summary\n\nMethods\n-------\nfunc1(a, b) : str\n Example.\n\n" + ) + # google + assert test.make_docstring(width=25, style="google") == ( + "summary\n\n" "Methods:\n" " func1 (:obj:`str`):\n" " Example.\n\n" + ) + # rst + assert test.make_docstring(width=25, style="rst") == ( + "summary\n\n" ":Methods:\n\n" ":param func1: Example.\n" ":type func1: :obj:`str`\n\n" + ) + + +def test_check_section_order(): + """Test Docstring.check_section_order.""" + test = Docstring(["summary", "extended", DocSection("parameters", ""), DocSection("warns", "")]) + assert test.check_section_order("numpy") is True + test = Docstring(["summary", DocSection("parameters", ""), "extended", DocSection("warns", "")]) + assert test.check_section_order("numpy") is False + test = Docstring(["summary", "extended", DocSection("warns", ""), DocSection("parameters", "")]) + assert test.check_section_order("numpy") is False + # note that the unidentified seections are not permitted for numpy style + test = Docstring( + [ + "summary", + DocSection("asdfdsaf", ""), + "extended", + DocSection("warns", ""), + DocSection("parameters", ""), + ] + ) + with pytest.raises(ValueError): + test.check_section_order("numpy") + test = Docstring( + [ + "summary", + "extended", + DocSection("warns", ""), + DocSection("parameters", ""), + DocSection("asdfdsaf", ""), + ] + ) + with pytest.raises(ValueError): + test.check_section_order("numpy") + + # other styles do not enforce such ordering + test = Docstring( + [ + "summary", + DocSection("asdfdsaf", ""), + "extended", + DocSection("warns", ""), + DocSection("parameters", ""), + ] + ) + assert test.check_section_order("rst") is True + assert test.check_section_order("random") is True + # note that the unidentified sections are permitted for other styles + # NOTE: following does not actually check for the effect of adding unidentified section for non + # numpy style because all sections are unidentified for non numpy styles + test = Docstring( + [ + "summary", + "extended", + DocSection("warns", ""), + DocSection("parameters", ""), + DocSection("asdfdsaf", ""), + ] + ) + assert test.check_section_order("random") is True diff --git a/tests/test_equation.py b/tests/test_equation.py new file mode 100644 index 0000000..88628d5 --- /dev/null +++ b/tests/test_equation.py @@ -0,0 +1,46 @@ +"""Test docinstance.content.equation.""" +from docinstance.content.equation import DocEquation +import pytest + + +def test_init(): + """Test DocEquation.__init__.""" + with pytest.raises(TypeError): + DocEquation(1) + with pytest.raises(TypeError): + DocEquation(["x + 1", "y + 2"]) + test = DocEquation("a + b = 2") + assert test.equations == ["a + b = 2"] + test = DocEquation("a + b &= 2\\\\\nc + d &= 3") + assert test.equations == ["a + b &= 2\\\\", "c + d &= 3"] + test = DocEquation("a + b &= 2\\\\\nc + d &= 3\\\\\n") + assert test.equations == ["a + b &= 2\\\\", "c + d &= 3\\\\"] + test = DocEquation("a + b &= 2\\\\\nc + d &= 3\\\\") + assert test.equations == ["a + b &= 2\\\\", "c + d &= 3\\\\"] + + +def test_make_docstring_numpy(): + """Test DocEquation.make_docstring_numpy.""" + test = DocEquation("a + b = 2") + assert test.make_docstring_numpy(19, 0, 4) == ".. math:: a + b = 2\n\n" + assert test.make_docstring_numpy(18, 0, 4) == ".. math::\n\n a + b = 2\n\n" + with pytest.raises(ValueError): + test.make_docstring_numpy(8, 0, 4) + test = DocEquation("a + b &= 2\\\\\nc + d &= 3\\\\\n") + assert ( + test.make_docstring_numpy(18, 0, 4) == ".. math::\n\n" + " a + b &= 2\\\\\n" + " c + d &= 3\\\\\n\n" + ) + test = DocEquation("a + b &= 2\\\\\nc + d &= 3\n") + assert ( + test.make_docstring_numpy(18, 0, 4) == ".. math::\n\n" + " a + b &= 2\\\\\n" + " c + d &= 3\n\n" + ) + test = DocEquation("a + b &= 2\nc + d &= 3\n") + assert ( + test.make_docstring_numpy(18, 0, 4) == ".. math::\n\n" + " a + b &= 2\n" + " c + d &= 3\n\n" + ) diff --git a/tests/test_latex.py b/tests/test_latex.py new file mode 100644 index 0000000..5f07788 --- /dev/null +++ b/tests/test_latex.py @@ -0,0 +1,41 @@ +"""Tests for docinstance.parser.latex.""" +from docinstance.content.equation import DocEquation +from docinstance.parser.latex import is_math, parse_equation + + +def test_is_math(): + """Test docinstance.parser.numpy.is_math.""" + assert is_math(".. math::\n\n x&=2\\\\\n &=3") + assert is_math(".. math::\n\n x&=2\\\\\n &=3\n") + assert is_math("\n.. math::\n\n x&=2\\\\\n &=3\n\n\n") + assert not is_math("x\n.. math::\n\n x&=2\\\\\n &=3\n\n\n") + assert is_math(".. math::\n\n x=2") + assert is_math(" .. math::\n\n x=2") + assert is_math("\n .. math::\n\n x=2") + + +def test_parse_equation(): + """Test docinstance.parser.numpy.parse_equation.""" + test = parse_equation(".. math::\n\n x &= 2\\\\\n &= 3\n") + assert len(test) == 1 + assert isinstance(test[0], DocEquation) + assert test[0].equations == ["x &= 2\\\\", "&= 3"] + + test = parse_equation("x\n.. math::\n\n x &= 2\\\\\n &= 3\n") + assert len(test) == 2 + assert test[0] == "x" + assert isinstance(test[1], DocEquation) + assert test[1].equations == ["x &= 2\\\\", "&= 3"] + + test = parse_equation("x\n.. math::\n\n x &= 2\\\\\n &= 3\n\n\n") + assert len(test) == 2 + assert test[0] == "x" + assert isinstance(test[1], DocEquation) + assert test[1].equations == ["x &= 2\\\\", "&= 3"] + + test = parse_equation("x\n.. math::\n\n x &= 2\\\\\n &= 3\n\ny") + assert len(test) == 3 + assert test[0] == "x" + assert isinstance(test[1], DocEquation) + assert test[1].equations == ["x &= 2\\\\", "&= 3"] + assert test[2] == "y" diff --git a/tests/test_napoleon.py b/tests/test_napoleon.py new file mode 100644 index 0000000..95fa96d --- /dev/null +++ b/tests/test_napoleon.py @@ -0,0 +1,148 @@ +"""Test docinstance.parser.napoleon.""" +from docinstance.parser.numpy import parse_numpy as parse_ref_numpy +import pytest + +pytest.importorskip("sphinxcontrib.napoleon") +from docinstance.parser.napoleon import parse_numpy + + +def test_parse_numpy(): + """Tests docinstance.numpy.parse_numpy.""" + # summary + docstring = "summary" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "summary\n" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "\nsummary\n" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "\n\nsummary\n" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + + # extended + docstring = "summary\n\nblock1\n\nblock2" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "\nsummary\n\nblock1\n\nblock2" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "\nsummary\n\n\n\nblock2\n\n" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = "\n\nsummary\n\nblock1\n\nblock2" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + # extended + permitted headers + docstring = "summary\n\nblock1\n\nblock2\n\nParameters\n----------\nstuff" + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + + for header in ["parameters", "attributes", "other parameters", "returns", "yields"]: + # name + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # NOTE: in the returns section, if only the name is given, napoleon treats it as a type. + # However, DocDescription requires that the name be provided. So the type is stored as a + # name instead to make it compatible with the existing writer. + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + # name + types + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : {{str, int}}\n" + " description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # NOTE: multiple types are not separated and are treated in bulk in napoleon + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy") + # name + types + docstring = "summary\n\n{0}\n{1}\nabc: str\ndef: int".format( + header.title(), "-" * len(header) + ) + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + + for header in ["methods", "raises"]: + # name + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + # name + types + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy") + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : {{str, int}}\n" + " description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy") + # name + signature + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y)\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy_signature") + # name + types + signature + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y) : str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy_signature") + # name + types + signature + multiple descriptions - extended summary + docstring = ( + "summary\n\n{0}\n{1}\nabc(x, y) : str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + # assert parse_numpy(docstring).__dict__ == parse_ref_numpy(docstring).__dict__ + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy_signature") + # name + types + docstring = "summary\n\n{0}\n{1}\nabc : str\ndef : int".format( + header.title(), "-" * len(header) + ) + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy_signature") + + # See also + docstring = ( + "summary\n\nblock1\n\nblock2\n\nSee Also\n--------\nsome_place.\n" "some_other_place." + ) + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy") + # NOTE: napoleon cuts off the see also past the first word so the following docstring will have + # entries "some" and "some" for napoleon. + # docstring = ( + # "summary\n\nblock1\n\nblock2\n\nSee Also\n--------\nsome place.\n" + # "some other place." + # ) + + # References + docstring = "summary\n\nblock1\n\nblock2\nReferences\n----------\nsome ref.\n" "some other ref." + assert parse_numpy(docstring).make_docstring(style="numpy") == parse_ref_numpy( + docstring + ).make_docstring(style="numpy") + # NOTE: if references are separated by two newlines, then the docinstances' parser removes the + # newline, where as napoleon's keeps it + # docstring = ( + # "summary\n\nblock1\n\nblock2\nReferences\n----------\nsome ref.\n\n" + # "some other ref." + # ) diff --git a/tests/test_numpy.py b/tests/test_numpy.py new file mode 100644 index 0000000..95f04c6 --- /dev/null +++ b/tests/test_numpy.py @@ -0,0 +1,406 @@ +"""Tests for docinstance.parser.numpy.""" +from docinstance.content.description import DocDescription +from docinstance.content.equation import DocEquation +from docinstance.content.section import DocSection, ExtendedSummary, Parameters, Summary +from docinstance.docstring import Docstring +from docinstance.parser.numpy import parse_numpy +import pytest + + +def test_compare_docinstances(): + """Test docinstance.parser.test.test_numpy.""" + doc1 = Docstring( + [ + "summary", + DocSection("section1", ["content1", "content2"]), + DocSection( + "section2", + [ + DocDescription("name1", "(a, b)", str, ["desc1", "desc2"]), + DocDescription("name2", "(c, d)", int, ["desc3", DocEquation("desc4")]), + ], + ), + ] + ) + doc2 = Docstring( + [ + "summary", + DocSection("section1", ["content1", "content2"]), + DocSection( + "section2", + [ + DocDescription("name1", "(a, b)", str, ["desc1", "desc2"]), + DocDescription("name2", "(c, d)", int, ["desc3", DocEquation("desc4")]), + ], + ), + ] + ) + assert doc1 != 1 + assert Docstring(["a", "b"]) != Docstring(["a", "b", "c"]) + assert Docstring(["a", "b"]) != Docstring(["a", DocSection("x", "b")]) + assert Docstring(DocSection("a", "b")) != Docstring(DocSection("a", ["b", "c"])) + assert Docstring(DocSection("a", "b")) != Docstring(DocSection("a", "c")) + assert Docstring(DocSection("a", DocDescription("x", "y", "z", "k"))) != Docstring( + DocSection("a", DocDescription("1", "y", "z", "k")) + ) + assert Docstring(DocSection("a", DocDescription("x", "y", "z", "k"))) != Docstring( + DocSection("a", DocDescription("x", "1", "z", "k")) + ) + assert Docstring(DocSection("a", DocDescription("x", "y", "z", "k"))) != Docstring( + DocSection("a", DocDescription("x", "y", "1", "k")) + ) + assert Docstring(DocSection("a", DocDescription("x", "y", "z", "k"))) != Docstring( + DocSection("a", DocDescription("x", "y", "z", "1")) + ) + assert Docstring(DocSection("a", DocDescription("x", "y", "z", DocEquation("k")))) != Docstring( + DocSection("a", DocDescription("x", "y", "z", DocEquation("1"))) + ) + assert doc1.__dict__ == doc2.__dict__ + + +def test_parse_numpy(): + """Tests docinstance.numpy.parse_numpy.""" + # monkeypatch equality for Docstring + Docstring.__eq__ = lambda self, other: self.__dict__ == other.__dict__ + # summary + docstring = "summary" + assert parse_numpy(docstring) == Docstring(Summary("summary")) + docstring = "summary\n" + assert parse_numpy(docstring) == Docstring(Summary("summary")) + docstring = "\nsummary\n" + assert parse_numpy(docstring) == Docstring(Summary("summary")) + docstring = ' """summary\n """' + assert parse_numpy(docstring, contains_quotes=True) == Docstring(Summary("summary")) + # FIXME: this should raise an error + docstring = "\n\nsummary\n" + assert parse_numpy(docstring), Docstring([Summary("summary")]) + docstring = '"""\n\nsummary\n"""' + with pytest.raises(ValueError): + parse_numpy(docstring, contains_quotes=True) + docstring = ' """\n\n summary\n """' + with pytest.raises(ValueError): + parse_numpy(docstring, contains_quotes=True) + docstring = "summary\na" + with pytest.raises(ValueError): + parse_numpy(docstring) + + # extended + docstring = "summary\n\nblock1\n\nblock2" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(["block1", "block2"])] + ) + docstring = "\nsummary\n\nblock1\n\nblock2" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(["block1", "block2"])] + ) + docstring = "\nsummary\n\n\n\nblock2\n\n" + assert parse_numpy(docstring) == Docstring([Summary("summary"), ExtendedSummary(["block2"])]) + # FIXME: is this a bug? + docstring = "\n\nsummary\n\nblock1\n\nblock2" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(["block1", "block2"])] + ) + # extended + headers + docstring = "summary\n\nblock1\n\nblock2\n\nheader\n------\nstuff" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(["block1", "block2"]), DocSection("header", "stuff")] + ) + + # header with bad divider (-----) + docstring = "summary\n\nblock1\n\nblock2\n\nheader1\n--\nstuff\n\n" + with pytest.raises(ValueError): + parse_numpy(docstring) + + for header in [ + "parameters", + "attributes", + "methods", + "returns", + "yields", + "raises", + "other parameters", + "see also", + ]: + # name + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(["block1", "block2"]), + DocSection(header, DocDescription("abc", descs=["description1.", "description2."])), + ] + ) + # name + types + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(["block1", "block2"]), + DocSection( + header, + DocDescription("abc", types="str", descs=["description1.", "description2."]), + ), + ] + ) + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc : {{str, int}}\n" + " description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(["block1", "block2"]), + DocSection( + header, + DocDescription( + "abc", types=["str", "int"], descs=["description1.", "description2."] + ), + ), + ] + ) + # name + signature + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y)\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(["block1", "block2"]), + DocSection( + header, + DocDescription( + "abc", signature="(x, y)", descs=["description1.", "description2."] + ), + ), + ] + ) + # name + types + signature + multiple descriptions + docstring = ( + "summary\n\nblock1\n\nblock2\n\n{0}\n{1}\nabc(x, y): str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(["block1", "block2"]), + DocSection( + header, + DocDescription( + "abc", + types="str", + signature="(x, y)", + descs=["description1.", "description2."], + ), + ), + ] + ) + # name + types + signature + multiple descriptions - extended summary + docstring = ( + "summary\n\n{0}\n{1}\nabc(x, y): str\n description1.\n" + " description2.".format(header.title(), "-" * len(header)) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + DocSection( + header, + DocDescription( + "abc", + types="str", + signature="(x, y)", + descs=["description1.", "description2."], + ), + ), + ] + ) + # name + types + docstring = "summary\n\n{0}\n{1}\nabc: str\ndef: int".format( + header.title(), "-" * len(header) + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + DocSection( + header, [DocDescription("abc", types="str"), DocDescription("def", types="int")] + ), + ] + ) + + +def test_parse_numpy_raw(): + """Test pydocstring.numpy_docstring.parse_numpy with raw strings.""" + docstring = '"""summary\n\nextended"""' + assert parse_numpy(docstring, contains_quotes=True) == Docstring( + [Summary("summary"), ExtendedSummary("extended")] + ) + docstring = 'r"""summary\n\nextended"""' + with pytest.raises(NotImplementedError): + parse_numpy(docstring, contains_quotes=True) + + +def test_parse_numpy_self(): + """Test pydocstring.numpy_docstring.parse_numpy using itself as an example.""" + docstring = parse_numpy.__doc__ + # summary + assert ( + parse_numpy(docstring, contains_quotes=False).sections[0].contents[0] + == "Parse a docstring in numpy format into a Docstring instance." + ) + # extended + assert parse_numpy(docstring, contains_quotes=False).sections[1].contents == [ + "Multiple descriptions of the indented information (e.g. parameters, " + "attributes, methods, returns, yields, raises, see also) are " + "distinguished from one another with a period. If the period is not " + "present, then the description is assumed to be a multiline " + "description." + ] + # parameters + assert parse_numpy(docstring, contains_quotes=False).sections[2].header == "parameters" + assert parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].name == "docstring" + assert parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].types == ["str"] + assert parse_numpy(docstring, contains_quotes=False).sections[2].contents[0].descs == [ + "Numpy docstring." + ] + assert ( + parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].name + == "contains_quotes" + ) + assert parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].types == ["bool"] + assert parse_numpy(docstring, contains_quotes=False).sections[2].contents[1].descs == [ + r"True if docstring contains \"\"\" or \'\'\'." + ] + # returns + assert parse_numpy(docstring, contains_quotes=False).sections[3].header == "returns" + assert parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].name == "docstring" + assert parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].types == [ + "Docstring" + ] + assert parse_numpy(docstring, contains_quotes=False).sections[3].contents[0].descs == [ + "Instance of Docstring that contains the necessary information." + ] + # raises + assert parse_numpy(docstring, contains_quotes=False).sections[4].header == "raises" + assert ( + parse_numpy(docstring, contains_quotes=False).sections[4].contents[0].name == "ValueError" + ) + assert parse_numpy(docstring, contains_quotes=False).sections[4].contents[0].descs == [ + "If summary is not in the first or second line.", + "If summary is now followed with a blank line.", + "If number of '-' does not match the number of characters in the header.", + "If given entry of the tabbed information (parameters, attributes, methods, returns, " + "yields, raises, see also) had an unexpected pattern.", + ] + assert ( + parse_numpy(docstring, contains_quotes=False).sections[4].contents[1].name + == "NotImplementedError" + ) + assert parse_numpy(docstring, contains_quotes=False).sections[4].contents[1].descs == [ + r"If quotes corresponds to a raw string, i.e. r\"\"\"." + ] + + +def test_parse_numpy_equations(): + """Test pydocstring.numpy_docstring.parse_numpy with equations.""" + # monkeypatch equality for Docstring + Docstring.__eq__ = lambda self, other: self.__dict__ == other.__dict__ + # equation in extended + docstring = "summary\n\n.. math::\n\n \\frac{1}{2}" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(DocEquation("\\frac{1}{2}"))] + ) + docstring = "summary\n\n" ".. math::\n\n" " x &= 2\\\\\n" " &= y\\\\\n" + assert parse_numpy(docstring) == Docstring( + [Summary("summary"), ExtendedSummary(DocEquation("x &= 2\\\\\n&= y\\\\"))] + ) + docstring = ( + "summary\n\n" + ".. math::\n\n" + " x &= 2\\\\\n" + " &= y\\\\\n" + "Parameters\n" + "----------\n" + "a" + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + ExtendedSummary(DocEquation("x &= 2\\\\\n&= y\\\\")), + Parameters(DocDescription("a")), + ] + ) + + # equation in parameter + # single line equation + docstring = ( + "summary\n\nParameters\n----------\na : float\n .. math::\n\n " " \\frac{1}{2}" + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + Parameters(DocDescription("a", types="float", descs=DocEquation("\\frac{1}{2}"))), + ] + ) + # multi line equation + docstring = ( + "summary\n\nParameters\n----------\na : float\n .. math::\n\n" + " \\frac{1}{2}\\\\\n \\frac{1}{3}" + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + Parameters( + DocDescription( + "a", types="float", descs=DocEquation("\\frac{1}{2}\\\\\n" "\\frac{1}{3}\n") + ) + ), + ] + ) + # multiple equations + docstring = ( + "summary\n\nParameters\n----------\na : float\n .. math::\n\n" + " \\frac{1}{2}\n ..math::\n \\frac{1}{3}" + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + Parameters( + DocDescription( + "a", + types="float", + descs=[DocEquation("\\frac{1}{2}"), DocEquation("\\frac{1}{3}")], + ) + ), + ] + ) + # multiple equations and other descriptions + docstring = ( + "summary\n\nParameters\n----------\na : float\n Some float.\n .. math::\n\n" + " \\frac{1}{2}\n\n Yes.\n ..math::\n \\frac{1}{3}\n" + " This is the float." + ) + assert parse_numpy(docstring) == Docstring( + [ + Summary("summary"), + Parameters( + DocDescription( + "a", + types="float", + descs=[ + "Some float.", + DocEquation("\\frac{1}{2}"), + "Yes.", + DocEquation("\\frac{1}{3}"), + "This is the float.", + ], + ) + ), + ] + ) diff --git a/tests/test_section.py b/tests/test_section.py new file mode 100644 index 0000000..e961cf9 --- /dev/null +++ b/tests/test_section.py @@ -0,0 +1,371 @@ +"""Test docinstance.content.section.""" +from docinstance.content.description import DocDescription +from docinstance.content.section import ( + Attributes, + DocSection, + Examples, + ExtendedSummary, + Methods, + Notes, + OtherParameters, + Parameters, + Raises, + References, + Returns, + SeeAlso, + Summary, + Warnings, + Warns, + Yields, +) +import pytest + + +def test_init(): + """Test DocSection.__init__.""" + with pytest.raises(TypeError): + DocSection(1, "") + with pytest.raises(TypeError): + DocSection(["1"], "") + with pytest.raises(TypeError): + DocSection("1", 1) + with pytest.raises(TypeError): + DocSection("1", {"1"}) + with pytest.raises(TypeError): + DocSection("1", ["1", DocDescription("test")]) + with pytest.raises(TypeError): + DocSection("1", [DocDescription("test"), "1"]) + + test = DocSection("header name", "hello") + assert test.header == "header name" + assert test.contents == ["hello"] + test = DocSection("header name", ["hello", "i am"]) + assert test.header == "header name" + assert test.contents == ["hello", "i am"] + doc1 = DocDescription("hello") + doc2 = DocDescription("i am") + test = DocSection("header name", doc1) + assert test.header == "header name" + assert test.contents == [doc1] + test = DocSection("header name", [doc1, doc2]) + assert test.header == "header name" + assert test.contents == [doc1, doc2] + + +def test_make_docstring_numpy(): + """Test DocSection.make_docstring_numpy.""" + # string content + test = DocSection("header name", "hello") + assert test.make_docstring_numpy(11, 0, 4) == "Header Name\n-----------\nhello\n\n" + assert test.make_docstring_numpy(11, 0, 4) == "Header Name\n-----------\nhello\n\n" + # multiple string contents + test = DocSection("header name", ["hello", "i am"]) + assert test.make_docstring_numpy(11, 0, 4) == "Header Name\n-----------\nhello\n\ni am\n\n" + # doc description + test = DocSection("header name", DocDescription("var_name", types=str, descs="Example.")) + assert ( + test.make_docstring_numpy(20, 0, 4) + == "Header Name\n-----------\nvar_name : str\n Example.\n\n" + ) + # multiple doc descriptions + test = DocSection( + "header name", + [ + DocDescription("var_name1", types=str, descs="Example 1."), + DocDescription("var_name2", types=int, descs="Example 2."), + ], + ) + assert ( + test.make_docstring_numpy(20, 0, 4) + == "Header Name\n-----------\nvar_name1 : str\n Example 1.\nvar_name2 : int\n" + " Example 2.\n\n" + ) + # signature does nothing + test = DocSection( + "header name", DocDescription("var_name", signature="(a, b)", types=str, descs="Example.") + ) + assert ( + test.make_docstring_numpy(20, 0, 4) + == "Header Name\n-----------\nvar_name : str\n Example.\n\n" + ) + + +def test_make_docstring_numpy_signature(): + """Test DocSection.make_docstring_numpy_signature.""" + test = DocSection( + "header name", DocDescription("var_name", signature="(a, b)", types=str, descs="Example.") + ) + assert ( + test.make_docstring_numpy_signature(20, 0, 4) + == "Header Name\n-----------\nvar_name(a, b) : str\n Example.\n\n" + ) + + +def test_make_docstring_google(): + """Test DocSection.make_docstring_google.""" + with pytest.raises(ValueError): + test = DocSection("quite long header name", "") + test.make_docstring_google(10, 0, 4) + # no header + test = DocSection("", "Some text.") + assert test.make_docstring_google(10, 0, 4) == ("Some text.\n\n") + # one docdescription + test = DocSection( + "header name", DocDescription("var_name", signature="(a, b)", types=str, descs="Example.") + ) + assert test.make_docstring_google(35, 0, 4) == ( + "Header Name:\n" " var_name (:obj:`str`): Example.\n\n" + ) + # multiple docdescription + test = DocSection( + "header name", + [ + DocDescription("var1", signature="(a, b)", types=str, descs="Example1."), + DocDescription("var2", signature="(c)", types="int", descs="Example2."), + ], + ) + assert test.make_docstring_google(35, 0, 4) == ( + "Header Name:\n" " var1 (:obj:`str`): Example1.\n" " var2 (int): Example2.\n\n" + ) + # one string + test = DocSection("header name", "Some text.") + assert test.make_docstring_google(14, 0, 4) == ("Header Name:\n" " Some text.\n\n") + assert test.make_docstring_google(13, 0, 4) == ("Header Name:\n" " Some\n" " text.\n\n") + # multiple string + test = DocSection("header name", ["Some text.", "Another text."]) + assert test.make_docstring_google(17, 0, 4) == ( + "Header Name:\n" " Some text.\n\n" " Another text.\n\n" + ) + assert test.make_docstring_google(14, 0, 4) == ( + "Header Name:\n" " Some text.\n\n" " Another\n" " text.\n\n" + ) + assert test.make_docstring_google(13, 0, 4) == ( + "Header Name:\n" " Some\n" " text.\n\n" " Another\n" " text.\n\n" + ) + + +def test_make_docstring_rst(): + """Test DocSection.make_docstring_rst.""" + # no header + test = DocSection("", "Some text.") + assert test.make_docstring_rst(10, 0, 4) == ("Some text.\n\n") + # normal header, one docdescription + test = DocSection( + "header name", DocDescription("var_name", signature="(a, b)", types=str, descs="Example.") + ) + assert test.make_docstring_rst(35, 0, 4) == ( + ":Header Name:\n\n" ":param var_name: Example.\n" ":type var_name: :obj:`str`\n\n" + ) + # normal header, multiple docdescription + test = DocSection( + "header name", + [ + DocDescription("var1", signature="(a, b)", types=str, descs="Example1."), + DocDescription("var2", signature="(c)", types="int", descs="Example2."), + ], + ) + assert test.make_docstring_rst(35, 0, 4) == ( + ":Header Name:\n\n" + ":param var1: Example1.\n" + ":type var1: :obj:`str`\n" + ":param var2: Example2.\n" + ":type var2: int\n\n" + ) + # normal header, one string + test = DocSection("header name", "Some text.") + assert test.make_docstring_rst(13, 0, 4) == (":Header Name:\n\n" "Some text.\n\n") + # normal header, multiple string + test = DocSection("header name", ["Some text.", "Another text."]) + assert test.make_docstring_rst(13, 0, 4) == ( + ":Header Name:\n\n" "Some text.\n\n" "Another text.\n\n" + ) + + # special header, doc description + test = DocSection( + "see also", + [ + DocDescription("var1", signature="(a, b)", types=str, descs="Example1."), + DocDescription("var2", signature="(c)", types="int", descs="Example2."), + ], + ) + assert test.make_docstring_rst(35, 0, 4) == ( + ".. seealso::\n" + " :param var1: Example1.\n" + " :type var1: :obj:`str`\n" + " :param var2: Example2.\n" + " :type var2: int\n\n" + ) + # special header, string + test = DocSection("to do", ["Example 1, something.", "Example 2."]) + assert test.make_docstring_rst(20, 0, 4) == ( + ".. todo:: Example 1,\n" " something.\n" " Example 2.\n\n" + ) + + +def test_section_summary_init(): + """Test Summary.__init__.""" + with pytest.raises(TypeError): + Summary(["summary"]) + with pytest.raises(TypeError): + Summary(DocDescription("something")) + test = Summary("very very long summary") + assert test.header == "" + assert test.contents == ["very very long summary"] + + +def test_section_summary_make_docstring(): + """Test Summary.make_docstring.""" + test = Summary("very very long summary") + assert ( + test.make_docstring(25, 0, 4, summary_only=True, special=False) + == "very very long summary\n\n" + ) + assert ( + test.make_docstring(24, 0, 4, summary_only=True, special=False) + == "\nvery very long summary\n\n" + ) + assert ( + test.make_docstring(28, 0, 4, summary_only=True, special=False) == "very very long summary" + ) + assert ( + test.make_docstring(25, 0, 4, summary_only=False, special=False) + == "very very long summary\n\n" + ) + assert ( + test.make_docstring(24, 0, 4, summary_only=False, special=False) + == "\nvery very long summary\n\n" + ) + assert ( + test.make_docstring(28, 0, 4, summary_only=False, special=False) + == "very very long summary\n\n" + ) + + assert not ( + test.make_docstring(25, 0, 4, summary_only=True, special=True) + == "very very long summary\n\n" + ) + assert ( + test.make_docstring(26, 0, 4, summary_only=True, special=True) + == "very very long summary\n\n" + ) + assert not ( + test.make_docstring(28, 0, 4, summary_only=True, special=True) == "very very long summary" + ) + assert ( + test.make_docstring(29, 0, 4, summary_only=True, special=True) == "very very long summary" + ) + + test = Summary("very very very very very very long summary") + with pytest.raises(ValueError): + test.make_docstring(30, 0, 4) + + +def test_section_extended_summary(): + """Test ExtendedSummary.__init__.""" + test = ExtendedSummary("This is an extended summary.") + assert test.header == "" + assert test.contents == ["This is an extended summary."] + + +def test_section_parameters(): + """Test Parameters.__init__.""" + desc1 = DocDescription("a", types=str, descs="Example 1.") + desc2 = DocDescription("b", types=int, descs="Example 2.") + test = Parameters([desc1, desc2]) + assert test.header == "parameters" + assert test.contents == [desc1, desc2] + + +def test_section_attributes(): + """Test Attributes.__init__.""" + desc1 = DocDescription("a", types=str, descs="Example 1.") + desc2 = DocDescription("b", types=int, descs="Example 2.") + test = Attributes([desc1, desc2]) + assert test.header == "attributes" + assert test.contents == [desc1, desc2] + + +def test_section_methods(): + """Test Methods.__init__.""" + desc1 = DocDescription("f", signature="(x, y)", types=int, descs="Example 1.") + desc2 = DocDescription("g", signature="(z=1)", types=int, descs="Example 2.") + test = Methods([desc1, desc2]) + assert test.header == "methods" + assert test.contents == [desc1, desc2] + + +def test_section_returns(): + """Test Returns.__init__.""" + desc1 = DocDescription("a", types=int, descs="Example 1.") + desc2 = DocDescription("b", types=str, descs="Example 2.") + test = Returns([desc1, desc2]) + assert test.header == "returns" + assert test.contents == [desc1, desc2] + + +def test_section_yields(): + """Test Yields.__init__.""" + desc = DocDescription("a", types=int, descs="Example 1.") + test = Yields(desc) + assert test.header == "yields" + assert test.contents == [desc] + + +def test_section_otherparameters(): + """Test OtherParameters.__init__.""" + desc1 = DocDescription("a", types=int, descs="Example 1.") + desc2 = DocDescription("b", types=str, descs="Example 2.") + test = OtherParameters([desc1, desc2]) + assert test.header == "other parameters" + assert test.contents == [desc1, desc2] + + +def test_section_raises(): + """Test Raises.__init__.""" + desc = DocDescription("TypeError", descs="If something.") + test = Raises(desc) + assert test.header == "raises" + assert test.contents == [desc] + + +def test_section_warns(): + """Test Warns.__init__.""" + desc = DocDescription("Warning", descs="If something.") + test = Warns(desc) + assert test.header == "warns" + assert test.contents == [desc] + + +def test_section_warnings(): + """Test Warnings.__init__.""" + test = Warnings("Not to be used.") + assert test.header == "warnings" + assert test.contents == ["Not to be used."] + + +def test_section_seealso(): + """Test SeeAlso.__init__.""" + test = SeeAlso("Some other code.") + assert test.header == "see also" + assert test.contents == ["Some other code."] + + +def test_section_notes(): + """Test Notes.__init__.""" + test = Notes("Some comment.") + assert test.header == "notes" + assert test.contents == ["Some comment."] + + +def test_section_references(): + """Test References.__init__.""" + test = References("Some reference.") + assert test.header == "references" + assert test.contents == ["Some reference."] + + +def test_section_examples(): + """Test Examples.__init__.""" + test = Examples("Some example.") + assert test.header == "examples" + assert test.contents == ["Some example."] diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..e69e0fd --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,114 @@ +"""Test docinstance.utils.""" +import docinstance.utils +import pytest + + +def test_wrap(): + """Test docinstance.utils.wrap.""" + # normal usage + assert docinstance.utils.wrap("hello my name is", width=5, indent_level=0, tabsize=4) == [ + "hello", + "my", + "name", + "is", + ] + assert docinstance.utils.wrap("hello my name is", width=6, indent_level=1, tabsize=1) == [ + " hello", + " my", + " name", + " is", + ] + assert docinstance.utils.wrap("hello my name is", width=10, indent_level=2, tabsize=2) == [ + " hello", + " my", + " name", + " is", + ] + assert docinstance.utils.wrap("hello\n my", width=20, indent_level=0, tabsize=4) == [ + "hello", + " my", + ] + assert docinstance.utils.wrap("hello\n my", width=20, indent_level=1, tabsize=4) == [ + " hello", + " my", + ] + assert docinstance.utils.wrap( + ".. math:\n\n 1 + 2\n", width=20, indent_level=0, tabsize=4 + ) == [".. math:", "", " 1 + 2", ""] + # white spaces + assert docinstance.utils.wrap(" hello", width=9, indent_level=0, tabsize=4) == [" hello"] + assert docinstance.utils.wrap(" hello", width=8, indent_level=0, tabsize=4) == ["hello"] + assert docinstance.utils.wrap("hello ", width=8, indent_level=0, tabsize=4) == ["hello"] + assert docinstance.utils.wrap(" hello my ", width=9, indent_level=0, tabsize=4) == [ + " hello", + "my", + ] + assert docinstance.utils.wrap( + " hello my name is ", width=8, indent_level=0, tabsize=4 + ) == ["hello", "my", "name", "is"] + assert docinstance.utils.wrap( + "\n\n hello my name is\n\n", width=5, indent_level=0, tabsize=4 + ) == ["", "", "hello", "my", "name", "is", "", ""] + assert docinstance.utils.wrap( + "\n\n hello\n my name is\n\n", width=5, indent_level=0, tabsize=4 + ) == ["", "", "hello", " my", "name", "is", "", ""] + # example + assert docinstance.utils.wrap( + " Text that will be wrapped into different lines such that each" + " line is indented and is less than the given length.", + width=30, + ) == [ + " Text that will be wrapped", + "into different lines such that", + "each line is indented and is", + "less than the given length.", + ] + + # too much indentation + with pytest.raises(ValueError): + docinstance.utils.wrap("hello my name is", width=5, indent_level=3, tabsize=4) + # long words + with pytest.raises(ValueError): + docinstance.utils.wrap("hello my name is", width=1, indent_level=0, tabsize=1) + with pytest.raises(ValueError): + docinstance.utils.wrap("hello my name is", width=5, indent_level=1, tabsize=1) + # subsequent indent + assert docinstance.utils.wrap( + "a b c d e", width=4, indent_level=0, tabsize=4, subsequent_indent="xxx" + ) == ["a b", "xxxc", "xxxd", "xxxe"] + assert docinstance.utils.wrap( + "a b c d e", width=4, indent_level=1, tabsize=1, subsequent_indent="xx" + ) == [" a b", " xxc", " xxd", " xxe"] + with pytest.raises(ValueError): + docinstance.utils.wrap( + "a b c d e", width=4, indent_level=0, tabsize=4, subsequent_indent="xxxx" + ) + + +def test_wrap_indent_subsequent(): + """Test docinstance.utils.wrap_indent_subsequent.""" + assert docinstance.utils.wrap_indent_subsequent( + "a b c d e", width=4, indent_level=1, tabsize=3 + ) == ["a b", " c", " d", " e"] + with pytest.raises(ValueError): + docinstance.utils.wrap_indent_subsequent("a b c d e", width=4, indent_level=1, tabsize=4) + + +def test_extract_members(): + """Test docinstance.utils.extract_members.""" + + class Test: # pragma: no cover + """Test class.""" + + @property + def f(self): + """Test property.""" + return (self, 1) + + def g(self): + """Test function.""" + return (self, 2) + + h = pytest + + assert docinstance.utils.extract_members(Test) == {"f": Test.f, "g": Test.g} diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py new file mode 100644 index 0000000..42d1cd7 --- /dev/null +++ b/tests/test_wrapper.py @@ -0,0 +1,416 @@ +"""Test docinstance.wrapper.""" +import importlib +import os +import sys + +from docinstance.content.description import DocDescription +from docinstance.content.section import DocSection +from docinstance.docstring import Docstring +from docinstance.wrapper import ( + docstring, + docstring_current_module, + docstring_recursive, + kwarg_wrapper, +) +import pytest + + +def test_kwarg_wrapper(): + """Test docinstance.wrapper.kwarg_wrapper.""" + + @kwarg_wrapper + def test(func, x=1): + """Test function.""" + func.x = x + return func + + def f(): # pragma: no cover + """Test function.""" + pass + + with pytest.raises(AttributeError): + f.x + g = test(f) + assert g.x == 1 + g = test(f, x=2) + assert g.x == 2 + + @test + def f(): # pragma: no cover + """Test function.""" + pass + + assert f.x == 1 + + @test(x=2) + def f(): # pragma: no cover + """Test function.""" + pass + + assert f.x == 2 + + +def test_wrapper_docstring_on_func(): + """Test docinstance.wrapper.docstring on a function.""" + # no _docinstance + @docstring + def test(): # pragma: no cover + """Test docstring. + + Parameters + ---------- + x : int + Something. + + """ + pass + + # NOTE: the indentation can be removed with inspect.cleandoc + assert test.__doc__ == ( + "Test docstring.\n" + "\n" + " Parameters\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + assert not hasattr(test, "_docinstance") + + # _docinstance + docinstance = Docstring( + [ + "Test docstring.", + DocSection("parameters", DocDescription("x", types=int, descs="Something.")), + ] + ) + + def test(): # pragma: no cover + """Test function.""" + pass + + # FIXME: this is terrible because contents inside function is ignored when declaring a function + # i.e. we cannot set the attribute of a function within a function definition. Docstrings are + # special so they are not ignored. Using docstring that get parsed seems to be only option for + # standalone functions + test._docinstance = docinstance + docstring(test) + + assert test.__doc__ == ( + "Test docstring.\n" "\n" "Parameters\n" "----------\n" "x : int\n" " Something.\n\n" + ) + assert test._docinstance == docinstance + + docstring(test, indent_level=2) + assert test.__doc__ == ( + "Test docstring.\n" + "\n" + " Parameters\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + assert test._docinstance == docinstance + + +def test_wrapper_docstring_on_class(): + """Test docinstance.wrapper.docstring on a class.""" + # no _docinstance + @docstring + class Test: # pragma: no cover + """Test docstring. + + Attributes + ---------- + x : int + Something. + + """ + + def f(self): + """Test function. + + Returns + ------- + nothing + + """ + pass + + assert Test.__doc__ == ( + "Test docstring.\n\n" + " Attributes\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + assert not hasattr(Test, "_docinstance") + + # docinstance + docinstance1 = Docstring( + [ + "Test docstring.", + DocSection("attributes", DocDescription("x", types=int, descs="Something.")), + ] + ) + docinstance2 = Docstring(["Test function.", DocSection("returns", "nothing")]) + + # w/o indentation + @docstring + class Test: # pragma: no cover + """Test class.""" + + _docinstance = docinstance1 + + def f(self): + """Do nothing.""" + pass + + f._docinstance = docinstance2 + + assert Test.__doc__ == ( + "Test docstring.\n\n" "Attributes\n" "----------\n" "x : int\n" " Something.\n\n" + ) + assert Test._docinstance == docinstance1 + assert Test.f.__doc__ == "Do nothing." + assert Test.f._docinstance == docinstance2 + + # w/ indentation + @docstring(indent_level=1) + class Test: # pragma: no cover + """Test class.""" + + _docinstance = docinstance1 + + def f(self): + """Test function.""" + pass + + f._docinstance = docinstance2 + + assert Test.__doc__ == ( + "Test docstring.\n\n" + " Attributes\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + + +def test_wrapper_docstring_recursive_on_class(): + """Test docinstance.wrapper.docstring_recursive on a class.""" + # no _docinstance + @docstring_recursive + class Test: # pragma: no cover + """Test docstring. + + Attributes + ---------- + x : int + Something. + + """ + + def f(self): + """Test function. + + Returns + ------- + nothing + + """ + pass + + assert Test.__doc__ == ( + "Test docstring.\n\n" + " Attributes\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + assert not hasattr(Test, "_docinstance") + assert Test.f.__doc__ == ( + "Test function.\n\n" + " Returns\n" + " -------\n" + " nothing\n\n" + " " + ) + assert not hasattr(Test.f, "_docinstance") + + # docinstance + docinstance1 = Docstring( + [ + "Test docstring.", + DocSection("attributes", DocDescription("x", types=int, descs="Something.")), + ] + ) + docinstance2 = Docstring(["Test function.", DocSection("returns", "nothing")]) + + # w/o indentation + @docstring_recursive + class Test: # pragma: no cover + """Test class.""" + + _docinstance = docinstance1 + + def f(self): + """Test function.""" + pass + + f._docinstance = docinstance2 + + assert Test.__doc__ == ( + "Test docstring.\n\n" "Attributes\n" "----------\n" "x : int\n" " Something.\n\n" + ) + assert Test._docinstance == docinstance1 + assert Test.f.__doc__ == ( + "Test function.\n\n" " Returns\n" " -------\n" " nothing\n\n" " " + ) + assert Test.f._docinstance == docinstance2 + + # w indentation + @docstring_recursive(indent_level=2) + class Test: # pragma: no cover + """Test class.""" + + _docinstance = docinstance1 + + def f(self): + """Test function.""" + pass + + f._docinstance = docinstance2 + + assert Test.__doc__ == ( + "Test docstring.\n\n" + " Attributes\n" + " ----------\n" + " x : int\n" + " Something.\n\n" + " " + ) + assert Test._docinstance == docinstance1 + assert Test.f.__doc__ == ( + "Test function.\n\n" + " Returns\n" + " -------\n" + " nothing\n\n" + " " + ) + assert Test.f._docinstance == docinstance2 + + +def test_docstring_current_module(): + """Test docinstance.wrapper.docstring_current_module on the current module.""" + global test_docstring_current_module + test_docstring_current_module._docinstance = Docstring( + ["Test docinstance.wrapper.docstring_current_module on the current module.", "Did it work?"] + ) + + def supplementary_docstring_current_module(): + """To be used to test docstring_current_module in test_docstring_current_module.""" + pass + + supplementary_docstring_current_module._docinstance = Docstring( + [ + "Some docstring.", + DocSection("parameters", DocDescription("x", types=int, descs="Example.")), + ] + ) + test_docstring_current_module.f = supplementary_docstring_current_module + + docstring_current_module() + assert ( + test_docstring_current_module.__doc__ + == "Test docinstance.wrapper.docstring_current_module on the current module.\n\n" + " Did it work?\n\n" + " " + ) + + assert ( + test_docstring_current_module.f.__doc__ == "Some docstring.\n\n" + " Parameters\n" + " ----------\n" + " x : int\n" + " Example.\n\n" + " " + ) + + +def test_docstring_modify_import(tmp_path): + """Test docinstance.wrapper.docstring_modify_import on module `dummy`.""" + assert not hasattr(importlib._bootstrap_external.SourceFileLoader.exec_module, "special_cases") + # create temporary directory + tmp_dir = tmp_path / "test" + tmp_dir.mkdir() + # create python file that calls docstring_modify_import + path1 = tmp_dir / "dummy1.py" + path1.write_text( + "from docinstance.wrapper import docstring_modify_import\n" "docstring_modify_import()" + ) + # create another python file with docstrings + path2 = tmp_dir / "dummy2.py" + path2.write_text( + '''"""This will be replaced.""" +from docinstance.docstring import Docstring +from docinstance.content.section import DocSection +from docinstance.content.description import DocDescription + +_docinstance = Docstring([ + 'Dummy module for testing docinstance.wrapper.docstring_modify_import.' +]) + + +class DummyClass: + """This too will be replaced.""" + + _docinstance = Docstring([ + 'Class for testing.', + DocSection('attributes', DocDescription('x', types=str, descs='Example.')) + ]) + ''' + ) + # create yet another python file with docstring_modify_import + path3 = tmp_dir / "dummy3.py" + path3.write_text( + "from docinstance.wrapper import docstring_modify_import\n" "docstring_modify_import()" + ) + + # check that dummy2 is decorated + sys.path.append(str(tmp_dir)) + import dummy1 + import dummy2 as test + + assert test.__doc__ == "Dummy module for testing docinstance.wrapper.docstring_modify_import." + assert ( + test.DummyClass.__doc__ == "Class for testing.\n\n" + " Attributes\n" + " ----------\n" + " x : str\n" + " Example.\n\n" + " " + ) + assert importlib._bootstrap_external.SourceFileLoader.exec_module.special_cases == set( + [os.path.dirname(dummy1.__file__)] + ) + + # check that multiple function calls does not change anything + import dummy3 + + assert importlib._bootstrap_external.SourceFileLoader.exec_module.special_cases == set( + [os.path.dirname(dummy3.__file__)] + ) + + # import package outside current directory + import docinstance.utils + + # reload module because it has already been loaded via docinstance.wrapper + importlib.reload(docinstance.utils) + # check that the objects within docinstance.utils has not been touched by wrapper. + with pytest.raises(AttributeError): + docinstance.utils._docinstance diff --git a/tox.ini b/tox.ini index 0a56327..46f645e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,24 +3,147 @@ envlist = py37 py36 py35 - -[flake8] -max-line-length = 100 - -[pycodestyle] -max-line-length = 100 + linters [testenv] deps = pytest pytest-cov +commands = + pytest --cov={envsitepackagesdir}/docinstance --cov-branch tests + +# Linters +[testenv:readme] +basepython = python3 +deps = + readme_renderer + twine +commands = + python setup.py sdist bdist_wheel + twine check dist/* + +[testenv:black] +basepython = python3 +skip_install = true +deps = + black +commands = + black -l 100 --check ./ + black -l 100 --diff ./ + +[testenv:flake8] +basepython = python3 +skip_install = true +deps = flake8 + flake8-docstrings>=0.2.7 + flake8-import-order>=0.9 + pep8-naming + flake8-colors +commands = + flake8 src/docinstance tests/ setup.py + +[testenv:pylint] +basepython = python3 +skip_install = true +deps = pylint - pydocstyle - pycodestyle commands = - flake8 - pylint docinstance - pydocstyle - pycodestyle - pytest --cov={envsitepackagesdir}/docinstance --cov-branch + pylint src/docinstance --rcfile=tox.ini + +[testenv:bandit] +basepython = python3 +skip_install = true +deps = + bandit +commands = + bandit -r src/docinstance + +[testenv:linters] +basepython = python3 +skip_install = true +deps = + {[testenv:black]deps} + {[testenv:flake8]deps} + {[testenv:pylint]deps} + {[testenv:readme]deps} + {[testenv:bandit]deps} +commands = + {[testenv:black]commands} + {[testenv:flake8]commands} + {[testenv:pylint]commands} + {[testenv:readme]commands} + {[testenv:bandit]commands} +ignore_errors = true + +# flake8 +[flake8] +max-line-length = 100 +import-order-style = google +ignore = + # E121 : continuation line under-indented for hanging indent + E121, + # E123 : closing bracket does not match indentation of opening bracket’s line + E123, + # E126 : continuation line over-indented for hanging indent + E126, + # E226 : missing whitespace around arithmetic operator + E226, + # E241 : multiple spaces after ‘,’ + # E242 : tab after ‘,’ + E24, + # E704 : multiple statements on one line (def) + E704, + # W503 : line break occurred before a binary operator + W503, + # W504 : Line break occurred after a binary operator + W504, + # E203 : Whitespace before ':' + E203, + # D202 : No blank lines allowed after function docstring + D202, + +# pylintrc +[FORMAT] +# Maximum number of characters on a single line. +max-line-length=100 + +[MESSAGES CONTROL] +disable= + # attribute-defined-outside-init (W0201): + # Attribute %r defined outside __init__ Used when an instance attribute is + # defined outside the __init__ method. + W0201, + # too-many-instance-attributes (R0902): + # Too many instance attributes (%s/%s) Used when class has too many instance + # attributes, try to reduce this to get a simpler (and so easier to use) + # class. + R0902, + # too-many-arguments (R0913): + # Too many arguments (%s/%s) Used when a function or method takes too many + # arguments. + R0913, + # fixme (W0511): + # Used when a warning note as FIXME or XXX is detected. + W0511, + # bad-continuation (C0330): + # Wrong hanging indentation before block (add 4 spaces). + C0330, + # wrong-import-order (C0411): + # %s comes before %s Used when PEP8 import order is not respected (standard + # imports first, then third-party libraries, then local imports) + C0411, + # arguments-differ (W0221): + # Parameters differ from %s %r method Used when a method has a different + # number of arguments than in the implemented interface or in an overridden + # method. + W0221, + # import-error (E0401): + # Unable to import %s Used when pylint has been unable to import a module. + E0401, + # super-init-not-called (W0231): + # Used when an ancestor class method has an __init__ method which is not called by a derived class. + W0231, + +[SIMILARITIES] +min-similarity-lines=5 \ No newline at end of file