Repository: heavenshell/py-doq

Issue:
constructor (__init__) in a class
The docstring is added into `__init__`. But per [style guide of numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html),

> Use the same sections as outlined above (all except Returns are applicable). The constructor (__init__) should also be documented here, the Parameters section of the docstring details the constructor’s parameters.

They should be documented in the docstring of a class.

![image](https://user-images.githubusercontent.com/9496702/130314317-06c571f1-06fd-4e29-bcdc-96071b942891.png)


Discussion:

Patch:
diff --git a/README.rst b/README.rst
index ac2a2c5..19963e8 100644
--- a/README.rst
+++ b/README.rst
@@ -89,6 +89,7 @@ Usage
   usage: doq [-h] [-f FILE] [--start START] [--end END] [-t TEMPLATE_PATH]
            [-s STYLE] [--formatter FORMATTER] [--indent INDENT] [--omit OMIT]
            [-r] [-d DIRECTORY] [-w] [-v] [--ignore_exception] [--ignore_yield]
+           [--ignore_init]
 
   Docstring generator.
 
@@ -112,6 +113,7 @@ Usage
     -v, --version         Output the version number
     --ignore_exception    Ignore exception statements
     --ignore_yield        Ignore yield statements
+    --ignore_init         Ignore genereate docstring to __init__ method
 
 Customize template
 ==================
@@ -157,4 +159,5 @@ See `examples <https://github.com/heavenshell/py-doq/tree/master/examples>`_
 
 LICENSE
 =======
+
 NEW BSD LICENSE.
diff --git a/doq/cli.py b/doq/cli.py
index 1d7b7ff..bf0e9b1 100644
--- a/doq/cli.py
+++ b/doq/cli.py
@@ -54,7 +54,7 @@ def get_template_path(template_path, formatter):
     return os.path.abspath(template_path)
 
 
-def generate_def_docstrings(signature, template, is_exception=False, is_yield=False):
+def generate_def_docstrings(signature, template, is_exception=False, is_yield=False, ignore_init=False):
     docstrings = []
     for d in signature['defs']:
         if d['is_doc_exists'] is False:
@@ -67,18 +67,24 @@ def generate_def_docstrings(signature, template, is_exception=False, is_yield=Fa
             elif 'defs' in d:
                 filename = 'class.txt'
 
-            docstring = template.load(params=d, filename=filename)
-            docstrings.append(
-                {
-                    'docstring': docstring,
-                    'start_lineno': d['start_lineno'],
-                    'start_col': d['start_col'],
-                    'end_lineno': d['end_lineno'],
-                    'end_col': d['start_col'],
-                },
-            )
+            if ignore_init and d['name'] == '__init__':
+                # numpy style guide says constructor's docstring should
+                # documented at class docstring.
+                # https://numpydoc.readthedocs.io/en/latest/format.html#class-docstring
+                pass
+            else:
+                docstring = template.load(params=d, filename=filename)
+                docstrings.append(
+                    {
+                        'docstring': docstring,
+                        'start_lineno': d['start_lineno'],
+                        'start_col': d['start_col'],
+                        'end_lineno': d['end_lineno'],
+                        'end_col': d['start_col'],
+                    },
+                )
             if 'defs' in d:
-                results = generate_def_docstrings(d, template, is_exception, is_yield)
+                results = generate_def_docstrings(d, template, is_exception, is_yield, ignore_init=ignore_init)
                 if len(results):
                     docstrings += results
 
@@ -101,6 +107,7 @@ def generate_docstrings(
     omissions=None,
     ignore_exception=False,
     ignore_yield=False,
+    ignore_init=False,
 ):
     template = Template(paths=[path])
     signatures = parse(
@@ -108,6 +115,7 @@ def generate_docstrings(
         omissions=omissions,
         ignore_exception=ignore_exception,
         ignore_yield=ignore_yield,
+        ignore_init=ignore_init,
     )
     is_exception = False if ignore_exception else is_exception_enabled(os.path.join(path, 'def.txt'))
     is_yield = False if ignore_yield else is_yield_enabled(os.path.join(path, 'def.txt'))
@@ -129,7 +137,7 @@ def generate_docstrings(
                 )
 
             # Method docstring
-            docstrings += generate_def_docstrings(signature, template, is_exception, is_yield)
+            docstrings += generate_def_docstrings(signature, template, is_exception, is_yield, ignore_init)
         else:
             if signature['is_doc_exists'] is False:
                 filename = 'noarg.txt'
@@ -209,6 +217,7 @@ def run(args):
             omissions=omissions,
             ignore_exception=args.ignore_exception,
             ignore_yield=args.ignore_yield,
+            ignore_init=args.ignore_init,
         )
         if len(docstrings) == 0:
             continue
@@ -326,6 +335,11 @@ def parse_options():
         action='store_true',
         help='Ignore yield statements',
     )
+    parser.add_argument(
+        '--ignore_init',
+        action='store_true',
+        help='Ignore genereate docstring to __init__ method',
+    )
 
     args = parser.parse_args()
 
diff --git a/doq/parser.py b/doq/parser.py
index 70755bc..a376bd0 100644
--- a/doq/parser.py
+++ b/doq/parser.py
@@ -52,7 +52,7 @@ def parse_return_type(code, start_lineno, end_lineno):
     return None
 
 
-def parse_defs(module, omissions=None, ignore_exception=False, ignore_yield=False):  # noqa C901
+def parse_defs(module, omissions=None, ignore_exception=False, ignore_yield=False, ignore_init=False):  # noqa C901
     if omissions is None:
         omissions = []
 
@@ -117,7 +117,12 @@ def parse_defs(module, omissions=None, ignore_exception=False, ignore_yield=Fals
             },
         )
 
-        nested = parse_defs(d, ignore_exception=ignore_exception, ignore_yield=ignore_yield)
+        nested = parse_defs(
+            d,
+            ignore_exception=ignore_exception,
+            ignore_yield=ignore_yield,
+            ignore_init=ignore_init,
+        )
         if len(nested):
             results += nested
 
@@ -128,7 +133,7 @@ def parse_defs(module, omissions=None, ignore_exception=False, ignore_yield=Fals
     return results
 
 
-def parse_classdefs(module, ignore_exception=False, ignore_yield=False):
+def parse_classdefs(module, ignore_exception=False, ignore_yield=False, ignore_init=False):
     results = []
 
     for c in module.iter_classdefs():
@@ -138,7 +143,13 @@ def parse_classdefs(module, ignore_exception=False, ignore_yield=False):
         (end_lineno, end_col) = c.end_pos
 
         name = c.name.value
-        defs = parse_defs(c, omissions=['self'], ignore_exception=ignore_exception, ignore_yield=ignore_yield)
+        defs = parse_defs(
+            c,
+            omissions=['self'],
+            ignore_exception=ignore_exception,
+            ignore_yield=ignore_yield,
+            ignore_init=ignore_init,
+        )
         results.append(
             {
                 'name': name,
@@ -160,17 +171,23 @@ def parse_classdefs(module, ignore_exception=False, ignore_yield=False):
     return results
 
 
-def parse(code, omissions=None, ignore_exception=False, ignore_yield=False):
+def parse(code, omissions=None, ignore_exception=False, ignore_yield=False, ignore_init=False):
     m = parso.parse(code)
     results = []
     if 'class' in code:
-        results = parse_classdefs(m, ignore_exception=ignore_exception, ignore_yield=ignore_yield)
+        results = parse_classdefs(
+            m,
+            ignore_exception=ignore_exception,
+            ignore_yield=ignore_yield,
+            ignore_init=ignore_init,
+        )
 
     results += parse_defs(
         m,
         omissions=omissions,
         ignore_exception=ignore_exception,
         ignore_yield=ignore_yield,
+        ignore_init=ignore_init,
     )
 
     return results
diff --git a/tox.ini b/tox.ini
index 414c6de..1416cc3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py36,py37,py38,flake8
+envlist = py36,py37,py38,py39,flake8
 
 [testenv]
 commands=python setup.py test


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 9a3ae16..af0ac13 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -54,6 +54,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -79,6 +80,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -103,6 +105,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -128,6 +131,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -152,6 +156,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -177,6 +182,7 @@ class CliTestCase(TestCase):
                     omit=None,
                     ignore_exception=False,
                     ignore_yield=False,
+                    ignore_init=False,
                 )
                 with patch('doq.cli.sys.stdout', new_callable=StringIO) as p:
                     run(args)
@@ -559,3 +565,73 @@ class CliTestCase(TestCase):
         self.assertEqual(0, results[0]['end_col'])
         self.assertEqual(1, results[0]['start_lineno'])
         self.assertEqual(3, results[0]['end_lineno'])
+
+    def test_ignore_init(self):
+        docstrings = [
+            'class Foo:',
+            '   def __init__(self, arg1):',
+            '       pass',
+            '',
+            '',
+            'class Bar:',
+            '   def bar(self, arg1, arg2):',
+            '      pass',
+        ]
+
+        template_path = os.path.join(
+            self.basepath,
+            'doq',
+            'templates',
+            'sphinx',
+        )
+        results = generate_docstrings(
+            docstrings,
+            template_path,
+            omissions=['self'],
+            ignore_exception=False,
+            ignore_yield=False,
+            ignore_init=True,
+        )
+        expected_docstrings = [
+            [
+                '"""Foo."""',
+                '',
+            ],
+            [
+                '"""Bar."""',
+                '',
+            ],
+            [
+                '"""bar.',
+                '',
+                ':param arg1:',
+                ':param arg2:',
+                '"""',
+            ],
+        ]
+        self.assertEqual(
+            '\n'.join(expected_docstrings[0]),
+            results[0]['docstring'],
+        )
+        self.assertEqual(0, results[0]['start_col'])
+        self.assertEqual(0, results[0]['end_col'])
+        self.assertEqual(1, results[0]['start_lineno'])
+        self.assertEqual(4, results[0]['end_lineno'])
+
+        self.assertEqual(
+            '\n'.join(expected_docstrings[1]),
+            results[1]['docstring'],
+        )
+        self.assertEqual(0, results[1]['start_col'])
+        self.assertEqual(10, results[1]['end_col'])
+        self.assertEqual(6, results[1]['start_lineno'])
+        self.assertEqual(8, results[1]['end_lineno'])
+
+        self.assertEqual(
+            '\n'.join(expected_docstrings[2]),
+            results[2]['docstring'],
+        )
+        self.assertEqual(3, results[2]['start_col'])
+        self.assertEqual(3, results[2]['end_col'])
+        self.assertEqual(7, results[2]['start_lineno'])
+        self.assertEqual(8, results[2]['end_lineno'])