Repository: creisle/markdown_refdocs

Issue:
Error generating reference docs for MAVIS
Encountered this error generating reference docs from the [mavis](https://github.com/bcgsc/mavis) package
```
Traceback (most recent call last):
  File "/Users/creisle/git/markdown_refdocs/venv/bin/markdown_refdocs", line 11, in <module>
    load_entry_point('markdown-refdocs', 'console_scripts', 'markdown_refdocs')()
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/main.py", line 508, in command_interface
    namespace_headers=args.namespace_headers,
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/main.py", line 461, in extract_to_markdown
    fh.write(module_to_markdown(parsed, relative_mapping))
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/markdown.py", line 175, in module_to_markdown
    md.append(class_to_markdown(cls, types_links=types_links))
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/markdown.py", line 127, in class_to_markdown
    + ' '.join([create_type_link(t, types_links) for t in parsed['inherits']]),
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/markdown.py", line 127, in <listcomp>
    + ' '.join([create_type_link(t, types_links) for t in parsed['inherits']]),
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/markdown.py", line 44, in create_type_link
    type_link, has_link = _create_type_link(type_name, types_links)
  File "/Users/creisle/git/markdown_refdocs/markdown_refdocs/markdown.py", line 9, in _create_type_link
    m = re.match(r'^Optional\[(.*)\]$', type_name)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/re.py", line 173, in match
    return _compile(pattern, flags).match(string)
TypeError: expected string or bytes-like object
```

Discussion:

Patch:
diff --git a/README.md b/README.md
index 77f9a6f..5a36484 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Markdown Refdocs
 
-![build](https://github.com/creisle/markdown_refdocs/workflows/build/badge.svg?branch=master) [![PyPi](https://img.shields.io/pypi/v/markdown_refdocs.svg)](https://pypi.org/project/markdown-refdocs) [![codecov](https://codecov.io/gh/creisle/markdown_refdocs/branch/master/graph/badge.svg)](https://codecov.io/gh/creisle/markdown_refdocs)
+![build](https://github.com/creisle/markdown_refdocs/workflows/build/badge.svg) [![PyPi](https://img.shields.io/pypi/v/markdown_refdocs.svg)](https://pypi.org/project/markdown-refdocs) [![codecov](https://codecov.io/gh/creisle/markdown_refdocs/branch/master/graph/badge.svg)](https://codecov.io/gh/creisle/markdown_refdocs)
 
 Extracts docstings and type annotations from a python package to generate reference documentation in markdown.
 See an example of this at: https://creisle.github.io/markdown_refdocs/
diff --git a/markdown_refdocs/links.py b/markdown_refdocs/links.py
index 0dfcc6f..691b43b 100644
--- a/markdown_refdocs/links.py
+++ b/markdown_refdocs/links.py
@@ -8,6 +8,9 @@ from .types import ParsedModule
 def create_types_mapping(modules: Dict[str, ParsedModule]) -> Dict[str, str]:
     """
     Creates mapping of type name to links
+
+    Note:
+        This only links within a package
     """
     simple_mapping: Dict[str, Optional[str]] = {}
     qualified_mapping: Dict[str, Optional[str]] = {}
diff --git a/markdown_refdocs/markdown.py b/markdown_refdocs/markdown.py
index 2651c6d..fc28fd9 100644
--- a/markdown_refdocs/markdown.py
+++ b/markdown_refdocs/markdown.py
@@ -1,50 +1,28 @@
 import re
-from itertools import permutations
-from typing import Dict, Tuple
-
-from .types import ParsedClass, ParsedFunction, ParsedModule, ParsedVariable
-
-
-def _create_type_link(type_name: str, types_links: Dict[str, str] = {}) -> Tuple[str, bool]:
-    m = re.match(r'^Optional\[(.*)\]$', type_name)
-    if m:
-        inner_link, contains_link = _create_type_link(m.group(1), types_links)
-        if contains_link:
-            return f'Optional\\[{inner_link}\\]', True
-    if type_name in types_links:
-        return f'[{type_name}]({types_links[type_name]})', True
-
-    for typing_type in ['Dict', 'List', 'Iterable', 'Set', 'Optional']:
-        for link_type in types_links:
-            if f'{typing_type}[{link_type}]' == type_name:
-                inner_link, contains_link = _create_type_link(link_type, types_links)
-                if contains_link:
-                    return f'{typing_type}\\[{inner_link}\\]', True
-
-    for typing_type in ['str', 'Tuple', 'int']:
-        for link_type in types_links:
-            if f'Dict[{typing_type}, {link_type}]' == type_name:
-                inner_link, contains_link = _create_type_link(link_type, types_links)
-                if contains_link:
-                    return f'Dict\\[`{typing_type}`, {inner_link}\\]', True
-
-    for typing_type in ['Union', 'Tuple']:
-        for first, second in permutations(types_links.keys(), 2):
-            double_link = f'{typing_type}[{first}, {second}]'
-            if double_link == type_name:
-                first_link, contains_link = _create_type_link(first, types_links)
-                second_link, contains_other_link = _create_type_link(second, types_links)
-                if contains_link or contains_other_link:
-                    return f'{typing_type}\\[{first_link}, {second_link}\\]', True
-
-    return type_name, False
+from typing import Dict, List
+
+from .types import ParsedClass, ParsedFunction, ParsedModule, ParsedVariable, ADMONITIONS
 
 
 def create_type_link(type_name: str, types_links: Dict[str, str] = {}) -> str:
-    type_link, has_link = _create_type_link(type_name, types_links)
-    if not has_link:
-        return f'`{type_link}`'
-    return type_link
+    tokens = re.split(r'(\[|\]|\s|,)', str(type_name))
+
+    linked = []
+    contains_link = False
+    for token in tokens:
+        if token in types_links:
+            contains_link = True
+            linked.append(f'[{token}]({types_links[token]})')
+        elif token in ['str', 'int', 'bool', 'float']:
+            linked.append(f'`{token}`')
+        elif token in [']', '[']:
+            linked.append(f'\\{token}')
+        else:
+            linked.append(token)
+
+    if not contains_link:
+        return f'`{type_name}`'
+    return ''.join(linked)
 
 
 def argument_md(
@@ -74,6 +52,17 @@ def argument_md(
     return md
 
 
+def admonitions_to_markdown(parsed: Dict[str, List[str]]) -> str:
+    md: List[str] = []
+    for admonition in ADMONITIONS:
+        if parsed.get(admonition, []):
+            md.append(f'!!! {admonition}')
+            for line in parsed[admonition]:
+                md.append(f'\t{line}')
+            md.append('')
+    return '\n'.join(md)
+
+
 def function_to_markdown(
     parsed: ParsedFunction, heading_level: int = 2, types_links: Dict[str, str] = {}
 ) -> str:
@@ -113,6 +102,10 @@ def function_to_markdown(
             md.append(f'```python\n{example}\n```\n')
         md.append('')
 
+    admon_md = admonitions_to_markdown(parsed)
+    if admon_md:
+        md.append(admon_md)
+
     return '\n'.join(md)
 
 
@@ -138,6 +131,10 @@ def class_to_markdown(parsed: ParsedClass, types_links: Dict[str, str] = {}) ->
             md.append(argument_md(types_links=types_links, **attr))
         md.append('')
 
+    admon_md = admonitions_to_markdown(parsed)
+    if admon_md:
+        md.append(admon_md)
+
     if parsed.get('functions', ''):
         for func in parsed['functions']:
             md.append(function_to_markdown(func, heading_level=3, types_links=types_links))
diff --git a/markdown_refdocs/parsers.py b/markdown_refdocs/parsers.py
index 3ce944e..2ab1d7f 100644
--- a/markdown_refdocs/parsers.py
+++ b/markdown_refdocs/parsers.py
@@ -1,7 +1,7 @@
 import re
 from typing import Any, Dict
 
-from .types import ParsedDocstring, ParsedParameter, ParsedReturn
+from .types import ParsedDocstring, ParsedParameter, ParsedReturn, ADMONITIONS
 
 
 def left_align_block(block: str) -> str:
@@ -9,7 +9,7 @@ def left_align_block(block: str) -> str:
     if not lines:
         return block
 
-    indent = re.match(r'(\s*).*', lines[0]).group(1)
+    indent = re.match(r'(\s*).*', lines[0]).group(1)  # type: ignore
     content = '\n'.join([line[len(indent) :] for line in lines])
     return content
 
@@ -21,7 +21,7 @@ def parse_google_docstring(
     parses a google-style docsting into a dictionary of the various sections
     """
     state = None
-    tags = ['args', 'returns', 'raises', 'note', 'desc', 'example', 'attributes']
+    tags = ['args', 'returns', 'raises', 'note', 'desc', 'example', 'attributes', 'warning']
     content: Dict[str, Any] = {tag: [] for tag in tags}
 
     docstring = (docstring if docstring else '').strip()
@@ -66,7 +66,7 @@ def parse_google_docstring(
         parsed_args = []
         for i, arg in enumerate(content[tag]):
             try:
-                name, _, arg_type, arg_desc = re.match(
+                name, _, arg_type, arg_desc = re.match(  # type: ignore
                     r'^(\w+)(\s+\(([^)]+)\))?:\s*(.*)$', arg
                 ).groups()
                 parsed_args.append(
@@ -88,11 +88,11 @@ def parse_google_docstring(
             result[tag] = parsed_args
 
     for i, line in enumerate(content['raises']):
-        _, arg_type, arg_desc = re.match(r'^(([^:]+):)?\s*(.*)$', line).groups()
+        _, arg_type, arg_desc = re.match(r'^(([^:]+):)?\s*(.*)$', line).groups()  # type: ignore
         result['raises'].append(ParsedReturn({'type': arg_type, 'description': arg_desc}))
 
     for i, line in enumerate(content['returns']):
-        _, arg_type, arg_desc = re.match(r'^(([^:]+):)?\s*(.*)$', line).groups()
+        _, arg_type, arg_desc = re.match(r'^(([^:]+):)?\s*(.*)$', line).groups()  # type: ignore
         if result['returns']:
             result['returns']['description'] += ' ' + line
         else:
@@ -102,9 +102,12 @@ def parse_google_docstring(
             result['returns'] = ParsedReturn({'type': arg_type, 'description': arg_desc})
 
     result['description'] = '\n'.join(content['desc']).strip()
-    result['note'] = '\n'.join(content['note'])
 
     for i, example in enumerate(content['example']):
         result['examples'].append(left_align_block('\n'.join(example)))
 
+    for admon in ADMONITIONS:
+        if content.get(admon, []):
+            result[admon] = content[admon]
+
     return result
diff --git a/markdown_refdocs/types.py b/markdown_refdocs/types.py
index 537adb4..cb70fe0 100644
--- a/markdown_refdocs/types.py
+++ b/markdown_refdocs/types.py
@@ -63,3 +63,6 @@ class ParsedModule(Parsed):
     classes: List[ParsedClass]
     functions: List[ParsedFunction]
     variables: List[ParsedVariable]
+
+
+ADMONITIONS = ['warning', 'note', 'info', 'bug', 'tip', 'question', 'failure', 'danger', 'quote']
diff --git a/mkdocs.yml b/mkdocs.yml
index 164f786..3546cad 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,6 +9,7 @@ nav:
       - markdown_refdocs.links: reference/markdown_refdocs/links.md
 markdown_extensions:
   - codehilite
+  - admonition
 theme:
   name: "material"
 repo_url: https://github.com/creisle/markdown_refdocs.git


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 9600efd..bd0ca72 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -4,7 +4,7 @@ from unittest.mock import mock_open, patch
 
 from markdown_refdocs.main import command_interface, parse_module_file
 from markdown_refdocs.markdown import module_to_markdown
-from markdown_refdocs.types import ParsedModule, ParsedVariable
+from markdown_refdocs.types import ParsedVariable
 
 
 class TestParseModuleFile:
@@ -284,7 +284,6 @@ say something
                     'hidden': False,
                     'functions': [],
                     'examples': [],
-                    'note': '',
                     'attributes': [
                         {
                             'hidden': False,
@@ -550,10 +549,9 @@ class SomeType(TypedDict):
                     'description': '',
                     'hidden': False,
                     'examples': [],
-                    'note': '',
                     'attributes': [
-                        {'name': 'name', 'type': 'str', 'source_code': 'name: str',},
-                        {'name': 'parent', 'type': 'str', 'source_code': 'parent: str',},
+                        {'name': 'name', 'type': 'str', 'source_code': 'name: str'},
+                        {'name': 'parent', 'type': 'str', 'source_code': 'parent: str'},
                         {'name': 'grandparent', 'type': 'str', 'source_code': 'grandparent: str',},
                     ],
                     'functions': [],
@@ -603,7 +601,6 @@ class SomeType(TypedDict):
                     'name': 'SomeType',
                     'description': '',
                     'inherits': ['TypedDict'],
-                    'note': '',
                     'examples': [],
                     'hidden': False,
                     'functions': [],
diff --git a/tests/test_markdown.py b/tests/test_markdown.py
index 57d1253..0c059b7 100644
--- a/tests/test_markdown.py
+++ b/tests/test_markdown.py
@@ -75,3 +75,15 @@ class TestCreateTypeLink:
         types = {'type': link}
         md = create_type_link('List[type]', types)
         assert md == f'List\\[[type]({link})\\]'
+
+    def test_linking_as_dict_target(self):
+        link = './path/to/other/#class-someclass'
+        types = {'type': link}
+        md = create_type_link('Dict[str, type]', types)
+        assert md == f'Dict\\[`str`, [type]({link})\\]'
+
+    def test_tuple_of_links(self):
+        link = './path/to/other/#class-someclass'
+        types = {'type': link}
+        md = create_type_link('Tuple[type, type]', types)
+        assert md == f'Tuple\\[[type]({link}), [type]({link})\\]'