Welcome to Unittest2doc

English | 中文

Unittest2doc is a tool for converting Python unit test code into documentation. The unit test section in this documentation is generated using Unittest2doc.

Seeing is believing, let’s learn how to use Unittest2doc through examples in this documentation.

Project repository: https://github.com/Fmajor/unittest2doc

The project structure is:

unittest2doc/            # Project root directory
  src/                   # Source code directory
    unittest2doc/        # Package directory
      __init__.py
      unittest2doc.py
      ...
  sphinx-docs/           # Documentation directory
    source/              # Documentation source files
      conf.py            # Configuration file, import test packages here
      index.rst
      unittests/         # Unittest2doc will generate rst files here
      src/               # API documentation generated by sphinx autosummary
      ...
    build/               # Documentation build directory, run make html to generate this documentation
  tests/                 # Test directory, where we use Unittest2doc
    test_unittest2doc.py # Test file, including a Unittest2doc class and its running examples
    test_pformat_json.py # Here we test a function for structured JSON output
                         # For a structured output function, the best way to test is to display the results and save as documentation
  pyproject.toml
  README.rst
  • In the project root directory, use make unittest

    • Actually executes python -m unittest discover -s tests -p '*.py' -b

    • This is general unit testing, we only care about the test results, not the test process

  • In the project root directory, use make generate-unittest-docs

    • Actually executes unittest2doc -s tests -p '*.py', which directly runs all test files that meet the conditions

    • Each test file can be executed individually, with runtime parameters that allow them to generate RST format sphinx documentation and save to the /sphinx-docs/source/unittests/ directory, ultimately displayed in our documentation

Here we directly show the test files

tests/test_unittest2doc.py

Generated documentation: unittest2doc.unittest2doc.Unittest2Doc

  1import unittest
  2import unittest2doc
  3from unittest2doc import Unittest2Doc, docpprint
  4import time
  5import json
  6import yaml
  7import textwrap
  8from pathlib import Path
  9
 10if 'test Unittest2Doc' and 1:
 11    class Test(unittest.TestCase):
 12        ''' docstring of class, new title
 13            -----------------------------
 14
 15            * Sphinx have already use "=" as title marker
 16            * to form a subtitle, we should use ``-`` as the title marker
 17
 18        '''
 19        def test(s):
 20            a = 1
 21            b = 2
 22            print("# this is a normal unittest.TestCase")
 23            print("# we can use all its assertion methods")
 24            s.assertEqual(a, 1)
 25            s.assertNotEqual(a, b)
 26            s.assertIs(a, 1)
 27            s.assertIsNot(a, b)
 28            s.assertIsNone(None)
 29            s.assertIsNotNone(a)
 30            s.assertTrue(True)
 31            s.assertFalse(False)
 32            
 33        def rst_test_doc(s):
 34            ''' group of tests
 35                --------------
 36
 37                function startswith rst will only provide its docstring to generate docs
 38
 39                we set the ``title_marker`` config to '^', and following tests will be grouped under this title
 40
 41                because rst title markers have these priorities:
 42
 43                * ``=`` (already used by upper Sphinx structure)
 44                * ``-``
 45                * ``^``
 46
 47            '''
 48            unittest2doc.update_config(s, title_marker='^')
 49
 50        #@unittest2doc.stop_after
 51        #@unittest2doc.only
 52        #@unittest2doc.stop
 53        def test_show_variable(s):
 54            """ the title marker is `^` (set in previous function rst_test_doc)
 55
 56                here we test the ``Unittest2Doc.v`` method, to display variables
 57            """
 58            a = 1
 59            b = '2'
 60            c = {
 61                'normal': 'some data',
 62                'secret': 'should be masked',
 63                'subsecret': {
 64                    'good': 1,
 65                    'bad': 0,
 66                    'sub': {
 67                        'good': 1,
 68                        'bad': 0,
 69                    }
 70                }
 71            }
 72            d = [1,2,3]
 73            unittest2doc.v(['a', 'b', 'c', 'd'], locals(), globals(), mask=[
 74                'c.secret',
 75                'c.subsecret.bad',
 76                'c.subsecret.sub.bad',
 77            ])
 78        def test_add_more_doc_0(s):
 79            """ {"open_input":false}
 80
 81                here we close the input block by json setting at first line of docstring
 82
 83                the title marker is still `^` (set in previous function rst_test_doc)
 84
 85            """
 86            pass
 87        def test_add_more_doc_1(s):
 88            """ {"open_output": false}
 89
 90                here we close the output block by json setting at first line of docstring
 91
 92                the title marker is still `^` (set in previous function rst_test_doc)
 93            """
 94            print('here we close the output ')
 95        def test_add_more_doc_2(s):
 96            """ after this, set title level to '-', and the current group is finished
 97            """
 98            unittest2doc.update_config(s, title_marker='-') # set title level to '-' after this test
 99        def test_add_more_doc_3(s):
100            """ this test back to top level (because title_marker is set to '-' at last function)
101            """
102            pass
103        def test_title_marker_for_single_test(s):
104            """ {"title_marker": "^"}
105
106                title marker set by above json is only effective in this function
107
108            """
109            print("# the title_marker ^ is only used in this function, and will not affect other tests")
110            print("# after this test, the title_marker is back to previous '-'")
111        def test_output_as_json(s):
112            """ {"output_highlight": "json"}
113
114                the output is highlighted as ``json``
115
116                the title marker here and below are all the default ``-``
117            """
118            print(json.dumps({"1":1, "2":"2", "3": 3.0, "4":4, "a":[{"1":1, "2":2}, {"3":3, "4":4}]}, indent=2))
119        def test_output_as_yaml(s):
120            """ {"output_highlight": "yaml"}
121
122                the output is highlighted as ``yaml``
123            """
124            # pprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]}, expand_all=True, indent_guides=False)
125            docpprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]})
126        def test_output_as_python(s):
127            """ {"output_highlight": "python"}
128            """
129            # print(pformat_json({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]}))
130            docpprint({1:1, '2':'2', '3': 3.0, '4':4, 'a':[{1:1, 2:2}, {3:3, 4:4}]})
131            from datetime import datetime
132            from collections import OrderedDict
133            d = [
134                  {
135                    'system_tags': [
136                      OrderedDict([('a', 1), ('b', 2), ('c', 3)]),
137                    ],
138                    'date': datetime.now(),
139                  }
140                ]
141            docpprint(d)
142        @unittest2doc.skip
143        def test_skipped(s):
144            raise Exception('this function should be skipped and we should not get this Exception')
145
146        @unittest2doc.expected_failure
147        def test_with_exception(s):
148            """ {"output_processors": ["no_home_folder"]}
149
150                test with exception, the output string will be processed by ``no_home_folder`` processor defined below
151            """
152            raise Exception('expected exception')
153          
154        def test_add_foldable_output(s):
155          """ add extra foldable text at end of the doc page
156          """
157          print("# add some output")
158          unittest2doc.add_foldable_output(
159            s, # must pass self into add_foldable_output
160            name='some python code',
161            highlight='python',
162            output=textwrap.dedent('''
163                  # some code ...
164                  def func(*args, **kwargs):
165                    pass
166                '''
167            )
168          )
169
170          # some nested data
171          data = {
172            'a': 1,
173            'b': 2,
174            'c': 3,
175            'd': {
176              'a': 1,
177              'b': 2,
178              'c': 3,
179            }
180          }
181          print("# add some output")
182
183          unittest2doc.add_foldable_output(
184            s, # must pass self into add_foldable_output
185            name='some yaml data',
186            highlight='yaml',
187            output=yaml.dump(data, indent=2)
188          )
189          print("# add some output")
190        
191        def test_last(s):
192          """ we use decorator above, make sure that this test is the last one """
193          pass
194
195    class Test2(unittest.TestCase):
196        ''' docstring of class
197            ------------------
198
199            in this class, we test the decorator ``@Unittest2Doc.only``
200
201        '''
202        def setUp(s):
203          print("# this setup function is always called at beginning")
204        def tearDown(s):
205          print("# this tearDown function is always called at end")
206
207        @unittest2doc.only
208        def test_only_1(s):
209          """ when you use ``Unittest2Doc.generate_docs()``, this test will be executed
210
211              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
212              
213              Thus the `only` decorator should only be used during your development and testing,
214              e.g., you just want to test one function and want to skip others for speed
215
216          """
217          pass
218
219        def test_other(s):
220          """ when you use ``Unittest2Doc.generate_docs()``, this test will be skipped because of not @unittest2doc.only decorator
221
222              it will be executed anyway if you use ``python -m unittest ...`` framework
223
224          """
225          pass
226
227        @unittest2doc.only
228        def test_only_2(s):
229          """ when you use ``Unittest2Doc.generate_docs()``, this test will be executed
230          """
231          pass
232    
233    class Test3(unittest.TestCase):
234        """ docstring of class
235            ------------------
236
237            in this class, we test the decorator ``@Unittest2Doc.stop``
238
239        """
240        def setUp(s):
241          print("# this setup function is always called at beginning")
242        def tearDown(s):
243          print("# this tearDown function is always called at end")
244
245        def test_3(s):
246          """ this should be the only test when you use ``Unittest2Doc.generate_docs()``
247
248              we have a @unittest2doc.stop decorator at next test
249
250          """
251          pass
252
253        @unittest2doc.stop
254        def test_2(s):
255          """ stop before this test when you use ``Unittest2Doc.generate_docs()``
256
257              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
258
259              Thus the `stop` decorator should only be used during your development and testing,
260              e.g., you just want to test above function and want to skip others for speed
261          
262          """
263          pass
264        
265        def test_1(s):
266          pass
267
268    class Test4(unittest.TestCase):
269        """ docstring of class
270            ------------------
271
272            in this class, we test the decorator ``@Unittest2Doc.stop_after``
273
274        """
275        def setUp(s):
276          print("# this setup function is always called at beginning")
277        def tearDown(s):
278          print("# this tearDown function is always called at end")
279
280        def test_3(s):
281          """ this should be the executed when you use ``Unittest2Doc.generate_docs()`` """
282          pass
283
284        @unittest2doc.stop_after
285        def test_2(s):
286          """ stop after this test when you use ``Unittest2Doc.generate_docs()``
287
288          """
289          pass
290
291        def test_1(s):
292          """ this should be skipped when you use ``Unittest2Doc.generate_docs()``
293
294              Note that if you use ``python -m unittest ...`` framework, all tests will be executed
295
296              Thus the `stop_after` decorator should only be used during your development and testing,
297              e.g., you just want to test above function and want to skip others for speed
298
299          """
300          pass
301    
302    class Test5(unittest.TestCase):
303        """ docstring of class
304            ------------------
305
306            in this class, we test the unittest decorator (not unittest2doc decorator)
307
308        """
309        def setUp(s):
310          print("# this setup function is always called at beginning")
311        def tearDown(s):
312          print("# this tearDown function is always called at end")
313        
314        @unittest.skip
315        def test_skipped(s):
316          raise Exception('this function should be skipped and we should not get this Exception') 
317
318        @unittest.expectedFailure
319        def test_with_exception(s):
320          raise Exception('expected exception')
321
322
323if __name__ == "__main__":
324    def no_home_folder(output):
325        # filter out `${HOME}/*/unittest2doc` to `${PROJECT_ROOT}/unittest2doc`
326        import os
327        home = os.environ.get('HOME')
328        import re
329        pattern = r"{home}/(?:[^/]+/)*?unittest2doc/".format(home=home)
330        replacement = r"${PROJECT_ROOT}/unittest2doc/"
331        output = re.sub(pattern, replacement, output)
332        return output
333    t = Unittest2Doc(
334        testcase=Test(),
335        name='unittest2doc.unittest2doc.Unittest2Doc.basic',
336        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
337        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
338        output_processors=dict(
339          no_home_folder=no_home_folder,
340        )
341    )
342    t.generate_docs()
343
344    t2 = Unittest2Doc(
345        testcase=Test2(),
346        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_only',
347        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
348        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
349    )
350    t2.generate_docs()
351
352    t3 = Unittest2Doc(
353        testcase=Test3(),
354        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_stop',
355        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
356        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
357    )
358    t3.generate_docs()
359
360    t4 = Unittest2Doc(
361        testcase=Test4(),
362        name='unittest2doc.unittest2doc.Unittest2Doc.test_decorator_stop_after',
363        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
364        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
365    )
366    t4.generate_docs()
367
368    t5 = Unittest2Doc(
369        testcase=Test5(),
370        name='unittest2doc.unittest2doc.Unittest2Doc.test_unittest_decorator',
371        ref=':class:`unittest2doc.unittest2doc.Unittest2Doc`',
372        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
373    )
374    t5.generate_docs()

tests/test_pformat_json.py

Generated documentation: unittest2doc.formatter

  1import unittest
  2import sys
  3import os
  4import json
  5from pathlib import Path
  6import unittest2doc
  7from unittest2doc import Unittest2Doc, FLog
  8from unittest2doc.formatter.json_formatter import pformat_json
  9
 10class TestJsonFormatter(unittest.TestCase):
 11    """ Test cases for json_formatter module's pformat_json function
 12    """
 13
 14    def setUp(self):
 15        """ here we also test the FLog class, it is a helper class to print function inputs and outputs """
 16        self.flog = FLog(do_print=True, output_suffix='\n', input_suffix=' # ===>')
 17        self.frun = self.flog.frun
 18    
 19    def test_basic_format(self):
 20        """ {"output_highlight": "python"}
 21        """
 22        # Test basic dictionary formatting
 23        data = {"name": "John", "age": 30, "city": "New York"}
 24        # call pformat_json(data) and print the result
 25        self.frun(pformat_json, data)
 26        
 27        # Test basic list formatting
 28        data = [1, 2, 3, "four", 5.0]
 29        # call pformat_json(data) and print the result
 30        self.frun(pformat_json, data)
 31        
 32        # Test simple value
 33        data = "simple string"
 34        # call pformat_json(data) and print the result
 35        self.frun(pformat_json, data)
 36    
 37    def test_nested_structures(self):
 38        """ {"output_highlight": "python"}
 39        """
 40        # Test nested dictionary and list
 41        data = {
 42            "person": {
 43                "name": "Alice",
 44                "details": {
 45                    "age": 28,
 46                    "occupation": "Engineer"
 47                }
 48            },
 49            "hobbies": ["reading", "hiking", {"sport": "tennis"}]
 50        }
 51        self.frun(pformat_json, data)
 52    
 53    def test_dict_title_comment(self):
 54        """ {"output_highlight": "python"}
 55        """
 56        # Test using string comment for dict title
 57        data = {"key1": "value1", "key2": "value2"}
 58        self.frun(pformat_json, data, comments="Dictionary Title")
 59        
 60        # Test using __dtitle__ special key
 61        data = {"key1": "value1", "key2": "value2"}
 62        comments = {"__dtitle__": "Dictionary Title With Special Key"}
 63        self.frun(pformat_json, data, comments=comments)
 64        
 65        # Test multi-line dict title
 66        comments = {"__dtitle__": "First Line\nSecond Line\nThird Line"}
 67        self.frun(pformat_json, data, comments=comments)
 68    
 69    def test_list_title_comment(self):
 70        """ {"output_highlight": "python"}
 71        """
 72        # Test using string comment for list title
 73        data = ["item1", "item2", "item3"]
 74        self.frun(pformat_json, data, comments="List Title")
 75        
 76        # Test using __ltitle__ special key
 77        comments = {"__ltitle__": "List Title With Special Key"}
 78        self.frun(pformat_json, data, comments=comments)
 79        
 80        # Test list prefix and suffix comments
 81        comments = {
 82            "__ltitle__": "List With Prefix and Suffix",
 83            "__lprefix__": ["Prefix Line 1", "Prefix Line 2"],
 84            "__lsuffix__": ["Suffix Line 1", "Suffix Line 2"]
 85        }
 86        self.frun(pformat_json, data, comments=comments)
 87    
 88    def test_specific_element_comments(self):
 89        """ {"output_highlight": "python"}
 90        """
 91        # Test comments for specific dict keys
 92        data = {"name": "Bob", "age": 45, "city": "Boston"}
 93        comments = {
 94            "name": "Person's name",
 95            "age": "Person's age in years",
 96            "city": "City of residence"
 97        }
 98        self.frun(pformat_json, data, comments=comments)
 99        
100        # Test comments for specific list indices
101        data = ["Python", "Java", "JavaScript", "C++"]
102        comments = {
103            0: "My favorite language",
104            2: "Web development language"
105        }
106        self.frun(pformat_json, data, comments=comments)
107    
108    def test_callable_comments(self):
109        """ {"output_highlight": "python"}
110        """
111        # Test callable comments for dict
112        data = {"price": 129.99, "quantity": 5, "discount": 0.15}
113        
114        def calc_total(key, value):
115            if key == "price":
116                return f"Base price: ${value}"
117            elif key == "quantity":
118                return f"Order quantity of {value} units"
119            elif key == "discount":
120                return f"Discount rate of {int(value*100)}%"
121            return ""
122        
123        comments = {
124            "price": calc_total,
125            "quantity": calc_total,
126            "discount": calc_total
127        }
128        self.frun(pformat_json, data, comments=comments)
129    
130    def test_compact_mode(self):
131        """ {"output_highlight": "python"}
132        """
133        # Test compact mode for dict
134        data = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
135        comments = {
136            "__lcompact__": 30,
137            "a": "First item",
138            "c": "Third item",
139            "e": "Fifth item"
140        }
141        self.frun(pformat_json, data, comments=comments)
142        
143        comments['__lcompact__'] = 25
144        self.frun(pformat_json, data, comments=comments)
145
146        comments['__lcompact__'] = 20
147        self.frun(pformat_json, data, comments=comments)
148
149        comments['__lcompact__'] = 15
150        self.frun(pformat_json, data, comments=comments)
151
152        comments.pop('__lcompact__')
153        self.frun(pformat_json, data, comments=comments)
154
155        self.frun(pformat_json, data, comments=comments, compact=15)
156
157        self.frun(pformat_json, data, comments=comments, compact=20)
158
159        self.frun(pformat_json, data, comments=comments, compact=30)
160
161    
162    def test_recursive_comments(self):
163        """ {"output_highlight": "python"}
164        """
165        # Test __lsub__ for all dict elements
166        data = {
167            "user": {
168                "id": 12345,
169                "name": "Alice Smith",
170                "email": "alice@example.com"
171            },
172            "settings": {
173                "theme": "dark",
174                "notifications": True
175            }
176        }
177        comments = {
178            "__dtitle__": "User Profile",
179            "__lsub__": {
180                "id": "Unique identifier",
181                "name": "Full name",
182                "theme": "UI theme preference"
183            }
184        }
185        self.frun(pformat_json, data, comments=comments)
186        
187        # Test __llist__ and __ldict__ for typed elements
188        data = [
189            {"type": "book", "title": "Python Programming"},
190            [1, 2, 3],
191            {"type": "video", "title": "Advanced Python"}
192        ]
193        comments = {
194            "__llist__": {
195                "__lcompact__": 40
196            },
197            "__ldict__": {
198                "type": "Content type",
199                "title": "Content title"
200            }
201        }
202        self.frun(pformat_json, data, comments=comments)
203    
204    def test_custom_indentation(self):
205        """ {"output_highlight": "python"}
206        """
207        # Test custom indentation
208        data = {
209            "outer": {
210                "middle": {
211                    "inner": "value"
212                }
213            }
214        }
215        # Test with different indent values
216        self.frun(pformat_json, data, indent=2)
217        self.frun(pformat_json, data, indent=4)
218    
219    
220    def test_custom_comment_prefix(self):
221        """ {"output_highlight": "python"}
222        """
223        # Test custom comment prefix
224        data = {"name": "John", "age": 30}
225        comments = {
226            "__dtitle__": "Person Info",
227            "name": "Person's name",
228            "age": "Age in years"
229        }
230        self.frun(pformat_json, data, comments=comments, comment_prefix="// ")
231        
232    def test_different_compact_values(self):
233        """ {"output_highlight": "python"}
234        """
235        # Same data with different __lcompact__ values
236        data = {"item1": 100, "item2": 200, "item3": 300, "item4": 400, "item5": 500}
237        comments = {
238            "item1": "First item comment",
239            "item3": "Third item comment",
240            "item5": "Fifth item comment"
241        }
242        
243        # No compact mode
244        print("\nNo compact mode:")
245        self.frun(pformat_json, data, comments=comments)
246        
247        # Very wide compact mode (essentially same as no compact)
248        print("\nCompact mode (width=100):")
249        comments_wide = comments.copy()
250        comments_wide["__lcompact__"] = 100
251        self.frun(pformat_json, data, comments=comments_wide)
252        
253        # Medium compact mode
254        print("\nCompact mode (width=50):")
255        comments_medium = comments.copy()
256        comments_medium["__lcompact__"] = 50
257        self.frun(pformat_json, data, comments=comments_medium)
258        
259        # Narrow compact mode
260        print("\nCompact mode (width=25):")
261        comments_narrow = comments.copy()
262        comments_narrow["__lcompact__"] = 25
263        self.frun(pformat_json, data, comments=comments_narrow)
264    
265    def test_deeply_nested_structure(self):
266        """ {"output_highlight": "python"}
267        """
268        # Create a deeply nested structure to test indentation handling
269        data = {
270            "level1": {
271                "level2": {
272                    "level3": {
273                        "level4": {
274                            "level5": {
275                                "value": "deeply nested value"
276                            },
277                            "array": [1, 2, [3, 4, [5, 6]]]
278                        }
279                    }
280                }
281            }
282        }
283        
284        comments = {
285            "__dtitle__": "Deep Nesting Test",
286            "__lsub__": {
287                "level1": "First level",
288                "level2": "Second level",
289                "level3": "Third level",
290                "level4": "Fourth level",
291                "level5": "Fifth level",
292                "value": "The final value",
293                "array": "Array of values"
294            }
295        }
296        
297        self.frun(pformat_json, data, comments=comments, debug=True)
298    
299    def test_comprehensive_example(self):
300        """ {"output_highlight": "python"}
301        """
302        # Test case combining multiple features
303        data = {
304            "metadata": {
305                "title": "Comprehensive Example",
306                "version": 1.5,
307                "tags": ["test", "example", "comprehensive"]
308            },
309            "configuration": {
310                "enabled": True,
311                "options": {
312                    "debug": False,
313                    "verbose": True,
314                    "timeout": 30
315                }
316            },
317            "data_points": [
318                {"id": 1, "value": 10.5, "label": "Point A"},
319                {"id": 2, "value": 20.7, "label": "Point B"},
320                {"id": 3, "value": 15.3, "label": "Point C"}
321            ],
322            "statistics": {
323                "count": 3,
324                "average": 15.5,
325                "max": 20.7,
326                "min": 10.5
327            }
328        }
329        
330        # Define comprehensive comments
331        def format_stat(key, value):
332            if key == "average":
333                return f"Average value: {value:.1f}"
334            elif key == "max":
335                return f"Maximum value: {value:.1f}"
336            elif key == "min":
337                return f"Minimum value: {value:.1f}"
338            return str(value)
339        
340        comments = {
341            "__dtitle__": "Complete Feature Demonstration",
342            "__lcompact__": 60,
343            "metadata": {
344                "__dtitle__": "Document Metadata",
345                "title": "The title of this example",
346                "tags": "Keywords for categorization"
347            },
348            "configuration": {
349                "__dtitle__": "System Configuration",
350                "__lcompact__": 40,
351                "options": {
352                    "__dtitle__": "Available Options",
353                    "debug": "Enable debug mode",
354                    "verbose": "Show detailed output",
355                    "timeout": "Operation timeout in seconds"
356                }
357            },
358            "data_points": {
359                "__ltitle__": "Measurement Data",
360                "__lprefix__": ["Array of data point objects", "Each with id, value and label"],
361                "__ldict__": {
362                    "id": "Unique identifier",
363                    "value": "Measurement value",
364                    "label": "Display name"
365                }
366            },
367            "statistics": {
368                "__dtitle__": "Statistical Analysis",
369                "count": "Number of data points",
370                "average": format_stat,
371                "max": format_stat,
372                "min": format_stat
373            }
374        }
375        
376        result = pformat_json(data, comments=comments)
377        print(result)
378
379if __name__ == "__main__":
380    t = unittest2doc.Unittest2Doc(
381        testcase=TestJsonFormatter(),
382        name='unittest2doc.formatter.pformat_json',
383        ref=':func:`unittest2doc.formatter.pformat_json`',
384        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
385    )
386    t.generate_docs()

tests/test_exec_tool.py

Generated documentation: unittest2doc.utils.exec_tool

 1from pathlib import Path
 2import unittest2doc
 3from unittest2doc import Unittest2Doc
 4from unittest2doc.utils.exec_tool import filter_after_comment_by, load_module, collect_module_attr, load_main
 5
 6""" Note
 7This is a special way to run test cases in the ``if __name__ == "__main__"`` block of a test file (src/unittest2doc/utils/exec_tool.py),
 8Using the helper functions from that file.
 9"""
10
11if __name__ == "__main__":
12    test_module = "unittest2doc.utils.exec_tool"
13    module = load_module(test_module)
14    module_globals = collect_module_attr(module, all=True)
15
16    main = load_main(
17             module,
18             code_filter=filter_after_comment_by("Run unittests"),
19             globals=module_globals,
20             add_code_object=True,
21           )
22
23    t = unittest2doc.Unittest2Doc(
24        testcase=main['TestExecTool'](),
25        name='unittest2doc.utils.exec_tool',
26        ref=':mod:`unittest2doc.utils.exec_tool`',
27        doc_root=Path(__file__).absolute().parent.parent / 'sphinx-docs/source/unittests',
28        open_input=False,
29    )
30    t.generate_docs()

Finally, let’s look at the generated documentation

  • API documentation comes from our project source code, generated by sphinx’s autosummary feature, which is not the focus of this article

  • Unit test documentation comes from our test code, please explore the results yourself and compare with the test code

API Documentation and Unittests