diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
index 28d6548..1526ca6 100644
--- a/.github/workflows/pr-labeler.yml
+++ b/.github/workflows/pr-labeler.yml
@@ -12,7 +12,7 @@ jobs:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/pr-labeler-file-path.yml
# workaround for problem: https://github.com/wesnoth/wesnoth/commit/958c82d0867568057caaf58356502ec8c87d8366
- sync-labels: ""
+ sync-labels: false
- uses: TimonVS/pr-labeler-action@v3
with:
configuration-path: .github/pr-labeler-branch-name.yml
diff --git a/README.md b/README.md
index 9370673..73ffc38 100644
--- a/README.md
+++ b/README.md
@@ -171,11 +171,15 @@ lazydocs [OPTIONS] PATHS...
* `--output-path TEXT`: The output path for the creation of the markdown files. Set this to `stdout` to print all markdown to stdout. [default: ./docs/]
* `--src-base-url TEXT`: The base repo link used as prefix for all source links. Should also include the branch name.
+* `--url-line-prefix TEXT`: Line prefix for git repository line url anchors #{prefix}line. If None provided, defaults to Github style notation.
* `--overview-file TEXT`: Filename of overview file. If not provided, no API overview file will be generated.
* `--remove-package-prefix / --no-remove-package-prefix`: If `True`, the package prefix will be removed from all functions and methods. [default: True]
* `--ignored-modules TEXT`: A list of modules that should be ignored. [default: ]
* `--watermark / --no-watermark`: If `True`, add a watermark with a timestamp to bottom of the markdown files. [default: True]
* `--validate / --no-validate`: If `True`, validate the docstrings via pydocstyle. Requires pydocstyle to be installed. [default: False]
+* `--output-format TEXT`: The output format for the creation of the markdown files. This may be 'md' or 'mdx'. Defaults to md.
+* `--private-modules / --no-private-modules`: If `True`, includes modules with "_" prefix. [default: False]
+* `--toc / --no-toc`: If `True`, includes table of contents in generated module markdown files. [default: False]
* `--install-completion`: Install completion for the current shell.
* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
* `--help`: Show this message and exit.
diff --git a/docs/lazydocs.generation.md b/docs/lazydocs.generation.md
index 1b21093..1f2d163 100644
--- a/docs/lazydocs.generation.md
+++ b/docs/lazydocs.generation.md
@@ -1,14 +1,28 @@
-
+
# module `lazydocs.generation`
-Main module for markdown generation.
+Main module for markdown generation.
+
+
+## Table of Contents
+- [`MarkdownGenerator`](./lazydocs.generation.md#class-markdowngenerator)
+ - [`__init__`](./lazydocs.generation.md#constructor-__init__)
+ - [`class2md`](./lazydocs.generation.md#method-class2md)
+ - [`func2md`](./lazydocs.generation.md#method-func2md)
+ - [`import2md`](./lazydocs.generation.md#method-import2md)
+ - [`module2md`](./lazydocs.generation.md#method-module2md)
+ - [`overview2md`](./lazydocs.generation.md#method-overview2md)
+ - [`toc2md`](./lazydocs.generation.md#method-toc2md)
+- [`generate_docs`](./lazydocs.generation.md#function-generate_docs)
+- [`to_md_file`](./lazydocs.generation.md#function-to_md_file)
+
---
-
+
## function `to_md_file`
@@ -18,26 +32,28 @@ to_md_file(
filename: str,
out_path: str = '.',
watermark: bool = True,
- disable_markdownlint: bool = True
+ disable_markdownlint: bool = True,
+ is_mdx: bool = False
) → None
```
-Creates an API docs file from a provided text.
-
+Creates an API docs file from a provided text.
**Args:**
-
- - `markdown_str` (str): Markdown string with line breaks to write to file.
- - `filename` (str): Filename without the .md
- - `watermark` (bool): If `True`, add a watermark with a timestamp to bottom of the markdown files.
- - `disable_markdownlint` (bool): If `True`, an inline tag is added to disable markdownlint for this file.
- - `out_path` (str): The output directory
+
+- `markdown_str` (str): Markdown string with line breaks to write to file.
+- `filename` (str): Filename without the .md
+- `out_path` (str): The output directory.
+- `watermark` (bool): If `True`, add a watermark with a timestamp to bottom of the markdown files.
+- `disable_markdownlint` (bool): If `True`, an inline tag is added to disable markdownlint for this file.
+- `is_mdx` (bool, optional): JSX support. Default to False.
+
---
-
+
## function `generate_docs`
@@ -49,173 +65,225 @@ generate_docs(
src_base_url: Optional[str] = None,
remove_package_prefix: bool = False,
ignored_modules: Optional[List[str]] = None,
+ output_format: Optional[str] = None,
overview_file: Optional[str] = None,
watermark: bool = True,
- validate: bool = False
+ validate: bool = False,
+ private_modules: bool = False,
+ include_toc: bool = False,
+ url_line_prefix: Optional[str] = None
) → None
```
-Generates markdown documentation for provided paths based on Google-style docstrings.
-
+Generates markdown documentation for provided paths based on Google-style docstrings.
**Args:**
-
- - `paths`: Selected paths or import name for markdown generation.
- - `output_path`: The output path for the creation of the markdown files. Set this to `stdout` to print all markdown to stdout.
- - `src_root_path`: The root folder name containing all the sources. Fallback to git repo root.
- - `src_base_url`: The base url of the github link. Should include branch name. All source links are generated with this prefix.
- - `remove_package_prefix`: If `True`, the package prefix will be removed from all functions and methods.
- - `ignored_modules`: A list of modules that should be ignored.
- - `overview_file`: Filename of overview file. If not provided, no overview file will be generated.
- - `watermark`: If `True`, add a watermark with a timestamp to bottom of the markdown files.
- - `validate`: If `True`, validate the docstrings via pydocstyle. Requires pydocstyle to be installed.
+
+- `paths`: Selected paths or import name for markdown generation.
+- `output_path`: The output path for the creation of the markdown files. Set this to `stdout` to print all markdown to stdout.
+- `src_root_path`: The root folder name containing all the sources. Fallback to git repo root.
+- `src_base_url`: The base url of the github link. Should include branch name. All source links are generated with this prefix.
+- `remove_package_prefix`: If `True`, the package prefix will be removed from all functions and methods.
+- `ignored_modules`: A list of modules that should be ignored.
+- `output_format`: Markdown file extension and format.
+- `overview_file`: Filename of overview file. If not provided, no overview file will be generated.
+- `watermark`: If `True`, add a watermark with a timestamp to bottom of the markdown files.
+- `validate`: If `True`, validate the docstrings via pydocstyle. Requires pydocstyle to be installed.
+- `private_modules`: If `True`, includes modules with `_` prefix.
+- `url_line_prefix: Line prefix for git repository line url anchors. Default`: None - github "L".
+
---
-
+
## class `MarkdownGenerator`
-Markdown generator class.
+Markdown generator class.
-
-### method `__init__`
+
+
+### constructor `__init__`
```python
-__init__(
+MarkdownGenerator(
src_root_path: Optional[str] = None,
src_base_url: Optional[str] = None,
- remove_package_prefix: bool = False
+ remove_package_prefix: bool = False,
+ url_line_prefix: Optional[str] = None
)
```
-Initializes the markdown API generator.
-
+Initializes the markdown API generator.
**Args:**
-
- - `src_root_path`: The root folder name containing all the sources.
- - `src_base_url`: The base github link. Should include branch name. All source links are generated with this prefix.
- - `remove_package_prefix`: If `True`, the package prefix will be removed from all functions and methods.
+
+- `src_root_path`: The root folder name containing all the sources.
+- `src_base_url`: The base github link. Should include branch name.
+ All source links are generated with this prefix.
+- `remove_package_prefix`: If `True`, the package prefix will be removed from all functions and methods.
+- `url_line_prefix: Line prefix for git repository line url anchors. Default`: None - github "L".
+
---
-
+
### method `class2md`
```python
-class2md(cls: Any, depth: int = 2) → str
+class2md(cls: Any, depth: int = 2, is_mdx: bool = False) → str
```
-Takes a class and creates markdown text to document its methods and variables.
-
+Takes a class and creates markdown text to document its methods and variables.
**Args:**
-
- - `cls` (class): Selected class for markdown generation.
- - `depth` (int, optional): Number of # to append to function name. Defaults to 2.
+- `cls` (class): Selected class for markdown generation.
+- `depth` (int, optional): Number of # to append to function name. Defaults to 2.
+- `is_mdx` (bool, optional): JSX support. Default to False.
**Returns:**
-
- - `str`: Markdown documentation for selected class.
+
+- `str`: Markdown documentation for selected class.
+
---
-
+
### method `func2md`
```python
-func2md(func: Callable, clsname: str = '', depth: int = 3) → str
+func2md(
+ func: Callable,
+ clsname: str = '',
+ depth: int = 3,
+ is_mdx: bool = False
+) → str
```
-Takes a function (or method) and generates markdown docs.
-
+Takes a function (or method) and generates markdown docs.
**Args:**
-
- - `func` (Callable): Selected function (or method) for markdown generation.
- - `clsname` (str, optional): Class name to prepend to funcname. Defaults to "".
- - `depth` (int, optional): Number of # to append to class name. Defaults to 3.
+- `func` (Callable): Selected function (or method) for markdown generation.
+- `clsname` (str, optional): Class name to prepend to funcname. Defaults to "".
+- `depth` (int, optional): Number of # to append to class name. Defaults to 3.
+- `is_mdx` (bool, optional): JSX support. Default to False.
**Returns:**
-
- - `str`: Markdown documentation for selected function.
+
+- `str`: Markdown documentation for selected function.
+
---
-
+
### method `import2md`
```python
-import2md(obj: Any, depth: int = 1) → str
+import2md(
+ obj: Any,
+ depth: int = 1,
+ is_mdx: bool = False,
+ include_toc: bool = False
+) → str
```
-Generates markdown documentation for a selected object/import.
-
+Generates markdown documentation for a selected object/import.
**Args:**
-
- - `obj` (Any): Selcted object for markdown docs generation.
- - `depth` (int, optional): Number of # to append before heading. Defaults to 1.
+- `obj` (Any): Selcted object for markdown docs generation.
+- `depth` (int, optional): Number of # to append before heading. Defaults to 1.
+- `is_mdx` (bool, optional): JSX support. Default to False.
+- `include_toc` (bool, Optional): Include table of contents for module file. Defaults to False.
**Returns:**
-
- - `str`: Markdown documentation of selected object.
+
+- `str`: Markdown documentation of selected object.
+
---
-
+
### method `module2md`
```python
-module2md(module: module, depth: int = 1) → str
+module2md(
+ module: module,
+ depth: int = 1,
+ is_mdx: bool = False,
+ include_toc: bool = False
+) → str
```
-Takes an imported module object and create a Markdown string containing functions and classes.
-
+Takes an imported module object and create a Markdown string containing functions and classes.
**Args:**
-
- - `module` (types.ModuleType): Selected module for markdown generation.
- - `depth` (int, optional): Number of # to append before module heading. Defaults to 1.
+- `module` (types.ModuleType): Selected module for markdown generation.
+- `depth` (int, optional): Number of # to append before module heading. Defaults to 1.
+- `is_mdx` (bool, optional): JSX support. Default to False.
+- `include_toc` (bool, optional): Include table of contents in module file. Defaults to False.
**Returns:**
-
- - `str`: Markdown documentation for selected module.
+
+- `str`: Markdown documentation for selected module.
+
---
-
+
### method `overview2md`
```python
-overview2md() → str
+overview2md(is_mdx: bool = False) → str
+```
+
+Generates a documentation overview file based on the generated docs.
+
+
+**Args:**
+
+- `is_mdx` (bool, optional): JSX support. Default to False.
+
+
+**Returns:**
+
+- `str`: Markdown documentation of overview file.
+
+
+---
+
+
+
+### method `toc2md`
+
+```python
+toc2md(module: module = None, is_mdx: bool = False) → str
```
-Generates a documentation overview file based on the generated docs.
+Generates table of contents for imported object.
+
diff --git a/src/lazydocs/_about.py b/src/lazydocs/_about.py
index a7b66d7..1092556 100644
--- a/src/lazydocs/_about.py
+++ b/src/lazydocs/_about.py
@@ -1,5 +1,5 @@
"""Information about this library. This file will automatically changed."""
-__version__ = "0.5.1"
+__version__ = "0.6.0"
# __author__
# __email__
diff --git a/src/lazydocs/_cli.py b/src/lazydocs/_cli.py
index ac139cd..47c0e36 100644
--- a/src/lazydocs/_cli.py
+++ b/src/lazydocs/_cli.py
@@ -45,8 +45,19 @@ def generate(
output_format: Optional[str] = typer.Option(
None,
help="The output format for the creation of the markdown files. This may be 'md' or 'mdx'. Defaults to md.",
- )
-
+ ),
+ private_modules: bool = typer.Option(
+ False,
+ help="If `True`, all packages starting with `_` will be included.",
+ ),
+ toc: bool = typer.Option(
+ False,
+ help="Include table of contents in module file. Defaults to False.",
+ ),
+ url_line_prefix: Optional[str] = typer.Option(
+ None,
+ help="Line prefix for git repository line url anchors #{prefix}line. If none provided, defaults to Github style notation.",
+ ),
) -> None:
"""Generates markdown documentation for your Python project based on Google-style docstrings."""
@@ -61,6 +72,9 @@ def generate(
overview_file=overview_file,
watermark=watermark,
validate=validate,
+ private_modules=private_modules,
+ include_toc=toc,
+ url_line_prefix=url_line_prefix,
)
except Exception as ex:
typer.echo(str(ex))
diff --git a/src/lazydocs/generation.py b/src/lazydocs/generation.py
index 8196665..e773445 100755
--- a/src/lazydocs/generation.py
+++ b/src/lazydocs/generation.py
@@ -9,20 +9,33 @@
import re
import subprocess
import types
+from dataclasses import dataclass, is_dataclass
+from enum import Enum
from pydoc import locate
from typing import Any, Callable, Dict, List, Optional
+from urllib.parse import quote
_RE_BLOCKSTART_LIST = re.compile(
- r"(Args:|Arg:|Arguments:|Parameters:|Kwargs:|Attributes:|Returns:|Yields:|Kwargs:|Raises:).{0,2}$",
+ r"^(Args:|Arg:|Arguments:|Parameters:|Kwargs:|Attributes:|Returns:|Yields:|Kwargs:|Raises:).{0,2}$",
re.IGNORECASE,
)
-_RE_BLOCKSTART_TEXT = re.compile(r"(Examples:|Example:|Todo:).{0,2}$", re.IGNORECASE)
+_RE_BLOCKSTART_TEXT = re.compile(
+ r"^(Example[s]?:|Todo:|Reference[s]?:).{0,2}$",
+ re.IGNORECASE
+)
+
+# https://github.com/orgs/community/discussions/16925
+# https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
+_RE_ADMONITION_TEXT = re.compile(
+ r"^(?:\[\!?)?(NOTE|TIP|IMPORTANT|WARNING|CAUTION)s?[\]:][^:]?[ ]*(.*)$",
+ re.IGNORECASE
+)
-_RE_QUOTE_TEXT = re.compile(r"(Notes:|Note:).{0,2}$", re.IGNORECASE)
+_RE_TYPED_ARGSTART = re.compile(r"^([\w\[\]_]{1,}?)[ ]*?\((.*?)\):[ ]+(.{2,})", re.IGNORECASE)
+_RE_ARGSTART = re.compile(r"^(.+):[ ]+(.{2,})$", re.IGNORECASE)
-_RE_TYPED_ARGSTART = re.compile(r"([\w\[\]_]{1,}?)\s*?\((.*?)\):(.{2,})", re.IGNORECASE)
-_RE_ARGSTART = re.compile(r"(.{1,}?):(.{2,})", re.IGNORECASE)
+_RE_CODE_TEXT = re.compile(r"^```[\w\-\.]*[ ]*$", re.IGNORECASE)
_IGNORE_GENERATION_INSTRUCTION = "lazydocs: ignore"
@@ -51,8 +64,13 @@
"""
+_TOC_TEMPLATE = """
+## Table of Contents
+{toc}
+"""
+
_CLASS_TEMPLATE = """
-{section} class `{header}`
+{section} {kind} `{header}`
{doc}
{init}
{variables}
@@ -63,6 +81,7 @@
_MODULE_TEMPLATE = """
{section} module `{header}`
{doc}
+{toc}
{global_vars}
{functions}
{classes}
@@ -125,7 +144,10 @@ def _get_function_signature(
if owner_class:
name_parts.append(owner_class.__name__)
if hasattr(function, "__name__"):
- name_parts.append(function.__name__)
+ if function.__name__ == "__init__":
+ name_parts.append(_get_class_that_defined_method(function).__name__)
+ else:
+ name_parts.append(function.__name__)
else:
name_parts.append(type(function).__name__)
name_parts.append("__call__")
@@ -211,16 +233,17 @@ def to_md_file(
Args:
markdown_str (str): Markdown string with line breaks to write to file.
filename (str): Filename without the .md
+ out_path (str): The output directory.
watermark (bool): If `True`, add a watermark with a timestamp to bottom of the markdown files.
disable_markdownlint (bool): If `True`, an inline tag is added to disable markdownlint for this file.
- out_path (str): The output directory
+ is_mdx (bool, optional): JSX support. Default to False.
"""
if not markdown_str:
# Dont write empty files
return
md_file = filename
-
+
if is_mdx:
if not filename.endswith(".mdx"):
md_file = filename + ".mdx"
@@ -237,7 +260,7 @@ def to_md_file(
)
print("Writing {}.".format(md_file))
- with open(os.path.join(out_path, md_file), "w", encoding="utf-8") as f:
+ with open(os.path.join(out_path, md_file), "w", encoding="utf-8", newline="\n") as f:
f.write(markdown_str)
@@ -275,12 +298,19 @@ def _get_class_that_defined_method(meth: Any) -> Any:
mod = inspect.getmodule(meth)
if mod is None:
return None
- cls = getattr(
- inspect.getmodule(meth),
- meth.__qualname__.split(".", 1)[0].rsplit(".", 1)[0],
- )
- if isinstance(cls, type):
- return cls
+ try:
+ cls = getattr(
+ inspect.getmodule(meth),
+ meth.__qualname__.split(".", 1)[0].rsplit(".", 1)[0],
+ )
+ except AttributeError:
+ # workaround for AttributeError("module '' has no attribute '__create_fn__'")
+ for obj in meth.__globals__.values():
+ if is_dataclass(obj):
+ return obj
+ else:
+ if isinstance(cls, type):
+ return cls
return getattr(meth, "__objclass__", None) # handle special descriptor objects
@@ -298,9 +328,9 @@ def _is_object_ignored(obj: Any) -> bool:
return False
-def _is_module_ignored(module_name: str, ignored_modules: List[str]) -> bool:
+def _is_module_ignored(module_name: str, ignored_modules: List[str], private_modules: bool = False) -> bool:
"""Checks if a given module is ignored."""
- if module_name.split(".")[-1].startswith("_"):
+ if module_name.split(".")[-1].startswith("_") and module_name[1] != "_" and not private_modules:
return True
for ignored_module in ignored_modules:
@@ -360,106 +390,239 @@ def _doc2md(obj: Any) -> str:
# doc = getdoc(func) or ""
doc = _get_docstring(obj)
+ padding = 0
blockindent = 0
- argindent = 1
+ argindent = 0
out = []
arg_list = False
- literal_block = False
+ section_block = False
+ block_exit = False
md_code_snippet = False
- quote_block = False
+ admonition_block = None
+ literal_block = None
+ doctest_block = None
+ prev_blank_line_count = 0
+ offset = 0
- for line in doc.split("\n"):
- indent = len(line) - len(line.lstrip())
- if not md_code_snippet and not literal_block:
- line = line.lstrip()
+ @dataclass
+ class SectionBlock():
+ line_index: int
+ indent: int
+ offset: int
- if line.startswith(">>>"):
- # support for doctest
- line = line.replace(">>>", "```") + "```"
+ def _get_section_offset(lines: list, start_index: int, blockindent: int):
+ """Determine base padding offset for section.
- if (
- _RE_BLOCKSTART_LIST.match(line)
- or _RE_BLOCKSTART_TEXT.match(line)
- or _RE_QUOTE_TEXT.match(line)
- ):
- # start of a new block
- blockindent = indent
-
- if quote_block:
- quote_block = False
+ Args:
+ lines (list): Line lists.
+ start_index (int): Index of lines to start parsing.
+ blockindent (int): Reference block indent of section.
- if literal_block:
- # break literal block
- out.append("```\n")
- literal_block = False
+ Returns:
+ int: Padding offset.
+ """
+ offset = []
+ try:
+ for line in lines[start_index:]:
+ indent = len(line) - len(line.lstrip())
+ if not line.strip():
+ continue
+ if indent <= blockindent:
+ return -min(offset) if offset else 0
+ if indent > blockindent:
+ offset.append(indent - blockindent)
+ except IndexError:
+ return 0
+ return -min(offset) if offset else 0
+
+ def _lines_isvalid(lines: list, start_index: int, blockindent: int,
+ allow_same_level: bool = False,
+ require_next_is_blank: bool = False,
+ max_blank: int = None):
+ """Determine following lines fit section rules.
- out.append("\n\n**{}**\n".format(line.strip()))
+ Args:
+ lines (list): Line lists.
+ start_index (int): Index of lines to start parsing.
+ blockindent (int): Reference block indent of section.
+ allow_same_level (bool, optional): Allow line indent as blockindent. Defaults to False.
+ require_next_is_blank (bool, optional): Require first parsed line to be blank. Defaults to False.
+ max_blank (int, optional): Max number of allowable continuous blank lines in section. Defaults to None.
- arg_list = bool(_RE_BLOCKSTART_LIST.match(line))
+ Returns:
+ bool: Validity of tested lines.
+ """
+ prev_blank = 0
+ try:
+ for index, line in enumerate(lines[start_index:]):
+ indent = len(line) - len(line.lstrip())
+ line = line.strip()
+ if require_next_is_blank and index == 0 and line:
+ return False
+ if line:
+ prev_blank = 0
+ if indent <= blockindent:
+ if allow_same_level and indent == blockindent:
+ return True
+ return False
+ return True
+ if max_blank is not None:
+ if not line:
+ prev_blank += 1
+ if prev_blank > max_blank:
+ return False
+ except IndexError:
+ pass
+ return False
- if _RE_QUOTE_TEXT.match(line):
- quote_block = True
- out.append("\n>")
- elif line.strip().startswith("```"):
- # Code snippet is used
- if md_code_snippet:
- md_code_snippet = False
- else:
+ docstring = doc.split("\n")
+ for line_indx, line in enumerate(docstring):
+ indent = len(line) - len(line.lstrip())
+ line = line.lstrip()
+ offset = 0
+
+ # Exit condition for args and section blocks
+ if (any([arg_list, section_block])
+ and all([indent <= blockindent,
+ prev_blank_line_count,
+ line])):
+ arg_list = False if arg_list else arg_list
+ section_block = False if section_block else section_block
+ blockindent = 0
+
+ admonition_result = _RE_ADMONITION_TEXT.match(line)
+ blockstart_result = _RE_BLOCKSTART_LIST.match(line)
+ blocktext_result = _RE_BLOCKSTART_TEXT.match(line)
+
+ if admonition_result and not (md_code_snippet or admonition_block):
+ # Admonition block entry condition
+ admonition_block = SectionBlock(
+ line_indx, indent, _get_section_offset(docstring,
+ line_indx + 1,
+ indent))
+ line = "[!{}] {}".format(admonition_result.group(1).upper(),
+ admonition_result.group(2))
+
+ # Entry conditions and block offsets
+ if _RE_CODE_TEXT.match(line):
+ # Code block, detect "```"
+ md_code_snippet = not md_code_snippet
+ elif line.startswith(">>>") and not doctest_block:
+ # Doctest Entry condition
+ line = "```python\n" + line
+ if _lines_isvalid(docstring, line_indx + 1, indent, True, False, 1):
+ doctest_block = SectionBlock(line_indx, indent, 0)
md_code_snippet = True
+ else:
+ line = line + "\n```"
+ elif doctest_block and \
+ not _lines_isvalid(docstring, line_indx + 1, doctest_block.indent,
+ True, False, 1):
+ # Doctest block Exit Condition
+ offset = doctest_block.indent - indent
+ line = " " * (indent - doctest_block.indent +
+ doctest_block.offset) + line + "\n```"
+ block_exit = True
+ elif line.endswith("::") and not (literal_block) and \
+ _lines_isvalid(docstring, line_indx + 1, indent, False, True, None):
+ # Literal Block Entry Conditions
+ literal_block = SectionBlock(
+ line_indx, indent,
+ _get_section_offset(docstring, line_indx + 1, indent))
+ line = line.replace("::", "") if line.startswith(
+ "::") else line.replace("::", ":")
+ md_code_snippet = True
+ elif literal_block:
+ if line_indx == literal_block.line_index + 1 and not line:
+ # Literal block post entry
+ line = "```" + line
+ indent = literal_block.indent
+ elif not _lines_isvalid(docstring, line_indx + 1, literal_block.indent,
+ False, False, None):
+ # Literal block exit condition
+ offset += literal_block.indent - indent
+ line = " " * (indent - literal_block.indent +
+ literal_block.offset) + line + "\n```"
+ block_exit = True
+ elif line:
+ offset += literal_block.offset
+
+ # Admonition block processing and exit condition
+ if admonition_block:
+ if md_code_snippet:
+ if literal_block:
+ padding = max(indent - literal_block.indent, 0)
+ elif doctest_block:
+ padding = max(indent - doctest_block.indent, 0)
+ else:
+ padding = max(indent - admonition_block.indent
+ + admonition_block.offset, 0)
+ line = " " * (padding + offset) + line
+ offset = admonition_block.indent - indent
+ line = "> {}".format(line.replace("\n", "\n> "))
+ if not _lines_isvalid(docstring, line_indx + 1, admonition_block.indent,
+ False, False, None):
+ admonition_block = None
+
+ if (blockstart_result or blocktext_result):
+ # start of a new block
+ blockindent = indent
+ arg_list = bool(blockstart_result)
+ section_block = bool(blocktext_result)
- out.append(line)
- elif line.strip().endswith("::"):
- # Literal Block Support: https://docutils.sourceforge.io/docs/user/rst/quickref.html#literal-blocks
- literal_block = True
- out.append(line.replace("::", ":\n```"))
- elif quote_block:
- out.append(line.strip())
- elif line.strip().startswith("-"):
- # Allow bullet lists
- out.append("\n" + (" " * indent) + line)
- elif indent > blockindent:
+ if prev_blank_line_count <= 1:
+ out.append("\n")
+ out.append("**{}**\n".format(line.strip()))
+ elif indent > blockindent and (arg_list or section_block):
if arg_list and not literal_block and _RE_TYPED_ARGSTART.match(line):
# start of new argument
out.append(
- "\n"
- + " " * blockindent
- + " - "
+ "- "
+ _RE_TYPED_ARGSTART.sub(r"`\1` (\2): \3", line)
)
argindent = indent
elif arg_list and not literal_block and _RE_ARGSTART.match(line):
# start of an exception-type block
out.append(
- "\n"
- + " " * blockindent
- + " - "
+ "- "
+ _RE_ARGSTART.sub(r"`\1`: \2", line)
)
argindent = indent
elif indent > argindent:
# attach docs text of argument
# * (blockindent + 2)
- out.append(" " + line)
+ padding = max(indent - argindent + offset, 0)
+ out.append(" " * padding
+ + line.replace("\n",
+ "\n" + " " * padding))
else:
- out.append(line)
+ padding = max(indent - blockindent + offset, 0)
+ out.append(line.replace("\n",
+ "\n" + " " * padding))
+ elif line:
+ padding = max(indent - blockindent + offset, 0)
+ out.append(" " * padding
+ + line.replace("\n",
+ "\n" + " " * padding))
else:
- if line.strip() and literal_block:
- # indent has changed, if not empty line, break literal block
- line = "```\n" + line
- literal_block = False
out.append(line)
- if md_code_snippet:
- out.append("\n")
- elif not line and not quote_block:
- out.append("\n\n")
- elif not line and quote_block:
- out.append("\n>")
- else:
- out.append(" ")
+ out.append("\n")
- return "".join(out)
+ if block_exit:
+ block_exit = False
+ if md_code_snippet:
+ md_code_snippet = False
+ if literal_block:
+ literal_block = None
+ elif doctest_block:
+ doctest_block = None
+ if line.lstrip():
+ prev_blank_line_count = 0
+ else:
+ prev_blank_line_count += 1
+ return "".join(out)
class MarkdownGenerator(object):
"""Markdown generator class."""
@@ -469,6 +632,7 @@ def __init__(
src_root_path: Optional[str] = None,
src_base_url: Optional[str] = None,
remove_package_prefix: bool = False,
+ url_line_prefix: Optional[str] = None,
):
"""Initializes the markdown API generator.
@@ -477,10 +641,12 @@ def __init__(
src_base_url: The base github link. Should include branch name.
All source links are generated with this prefix.
remove_package_prefix: If `True`, the package prefix will be removed from all functions and methods.
+ url_line_prefix: Line prefix for git repository line url anchors. Default: None - github "L".
"""
self.src_root_path = src_root_path
self.src_base_url = src_base_url
self.remove_package_prefix = remove_package_prefix
+ self.url_line_prefix = url_line_prefix
self.generated_objects: List[Dict] = []
@@ -522,14 +688,20 @@ def _get_src_path(self, obj: Any, append_base: bool = True) -> str:
relative_path = os.path.relpath(path, src_root_path)
lineno = _get_line_no(obj)
- lineno_hashtag = "" if lineno is None else "#L{}".format(lineno)
+ if self.url_line_prefix is None:
+ lineno_hashtag = "" if lineno is None else "#L{}".format(lineno)
+ else:
+ lineno_hashtag = "" if lineno is None else "#{}{}".format(
+ self.url_line_prefix,
+ lineno
+ )
# add line hash
relative_path = relative_path + lineno_hashtag
if append_base and self.src_base_url:
relative_path = os.path.join(self.src_base_url, relative_path)
- return relative_path
+ return quote("/".join(relative_path.split("\\")), safe=":/#")
def func2md(self, func: Callable, clsname: str = "", depth: int = 3, is_mdx: bool = False) -> str:
"""Takes a function (or method) and generates markdown docs.
@@ -538,6 +710,7 @@ def func2md(self, func: Callable, clsname: str = "", depth: int = 3, is_mdx: boo
func (Callable): Selected function (or method) for markdown generation.
clsname (str, optional): Class name to prepend to funcname. Defaults to "".
depth (int, optional): Number of # to append to class name. Defaults to 3.
+ is_mdx (bool, optional): JSX support. Default to False.
Returns:
str: Markdown documentation for selected function.
@@ -589,7 +762,7 @@ def func2md(self, func: Callable, clsname: str = "", depth: int = 3, is_mdx: boo
func_type = "function"
else:
# function of a class
- func_type = "method"
+ func_type = "constructor" if escfuncname == "__init__" else "method"
self.generated_objects.append(
{
@@ -614,7 +787,7 @@ def func2md(self, func: Callable, clsname: str = "", depth: int = 3, is_mdx: boo
if path:
if is_mdx:
markdown = _MDX_SOURCE_BADGE_TEMPLATE.format(path=path) + markdown
- else:
+ else:
markdown = _SOURCE_BADGE_TEMPLATE.format(path=path) + markdown
return markdown
@@ -625,6 +798,7 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
Args:
cls (class): Selected class for markdown generation.
depth (int, optional): Number of # to append to function name. Defaults to 2.
+ is_mdx (bool, optional): JSX support. Default to False.
Returns:
str: Markdown documentation for selected class.
@@ -634,6 +808,7 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
return ""
section = "#" * depth
+ sectionheader = "#" * (depth + 1)
subsection = "#" * (depth + 2)
clsname = cls.__name__
modname = cls.__module__
@@ -641,6 +816,27 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
path = self._get_src_path(cls)
doc = _doc2md(cls)
summary = _get_doc_summary(cls)
+ variables = []
+
+ # Handle different kinds of classes
+ if issubclass(cls, Enum):
+ kind = cls.__base__.__name__
+ if kind != "Enum":
+ kind = "enum[%s]" % (kind)
+ else:
+ kind = kind.lower()
+ variables.append(
+ "%s symbols\n" % (sectionheader)
+ )
+ elif is_dataclass(cls):
+ kind = "dataclass"
+ variables.append(
+ "%s attributes\n" % (sectionheader)
+ )
+ elif issubclass(cls, Exception):
+ kind = "exception"
+ else:
+ kind = "class"
self.generated_objects.append(
{
@@ -648,7 +844,7 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
"name": header,
"full_name": header,
"module": modname,
- "anchor_tag": _get_anchor_tag("class-" + header),
+ "anchor_tag": _get_anchor_tag("%s-%s" % (kind, header)),
"description": summary,
}
)
@@ -666,21 +862,31 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
# this happens if __init__ is outside the repo
init = ""
- variables = []
for name, obj in inspect.getmembers(
cls, lambda a: not (inspect.isroutine(a) or inspect.ismethod(a))
):
- if not name.startswith("_") and type(obj) == property:
- comments = _doc2md(obj) or inspect.getcomments(obj)
- comments = "\n\n%s" % comments if comments else ""
- property_name = f"{clsname}.{name}"
+ if not name.startswith("_"):
+ full_name = f"{clsname}.{name}"
if self.remove_package_prefix:
- property_name = name
- variables.append(
- _SEPARATOR
- + "\n%s property %s%s\n"
- % (subsection, property_name, comments)
- )
+ full_name = name
+ if isinstance(obj, property):
+ comments = _doc2md(obj) or inspect.getcomments(obj)
+ comments = "\n\n%s" % comments if comments else ""
+ variables.append(
+ _SEPARATOR
+ + "\n%s property %s%s\n"
+ % (subsection, full_name, comments)
+ )
+ elif isinstance(obj, Enum):
+ variables.append(
+ "- **%s** = %s\n" % (full_name, obj.value)
+ )
+ elif name == "__dataclass_fields__":
+ for name, field in sorted((obj).items()):
+ variables.append(
+ "- ```%s``` (%s)\n" % (name,
+ field.type.__name__)
+ )
handlers = []
for name, obj in inspect.getmembers(cls, inspect.ismethoddescriptor):
@@ -717,6 +923,7 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
markdown = _CLASS_TEMPLATE.format(
section=section,
+ kind=kind,
header=header,
doc=doc if doc else "",
init=init,
@@ -733,12 +940,14 @@ def class2md(self, cls: Any, depth: int = 2, is_mdx: bool = False) -> str:
return markdown
- def module2md(self, module: types.ModuleType, depth: int = 1, is_mdx: bool = False) -> str:
+ def module2md(self, module: types.ModuleType, depth: int = 1, is_mdx: bool = False, include_toc: bool = False) -> str:
"""Takes an imported module object and create a Markdown string containing functions and classes.
Args:
module (types.ModuleType): Selected module for markdown generation.
depth (int, optional): Number of # to append before module heading. Defaults to 1.
+ is_mdx (bool, optional): JSX support. Default to False.
+ include_toc (bool, optional): Include table of contents in module file. Defaults to False.
Returns:
str: Markdown documentation for selected module.
@@ -814,10 +1023,13 @@ def module2md(self, module: types.ModuleType, depth: int = 1, is_mdx: bool = Fal
new_list = ["\n**Global Variables**", "---------------", *variables]
variables = new_list
+ toc = self.toc2md(module=module, is_mdx=is_mdx) if include_toc else ""
+
markdown = _MODULE_TEMPLATE.format(
header=modname,
section="#" * depth,
doc=doc,
+ toc=toc,
global_vars="\n".join(variables) if variables else "",
functions="\n".join(functions) if functions else "",
classes="".join(classes) if classes else "",
@@ -831,12 +1043,14 @@ def module2md(self, module: types.ModuleType, depth: int = 1, is_mdx: bool = Fal
return markdown
- def import2md(self, obj: Any, depth: int = 1, is_mdx: bool = False) -> str:
+ def import2md(self, obj: Any, depth: int = 1, is_mdx: bool = False, include_toc: bool = False) -> str:
"""Generates markdown documentation for a selected object/import.
Args:
obj (Any): Selcted object for markdown docs generation.
depth (int, optional): Number of # to append before heading. Defaults to 1.
+ is_mdx (bool, optional): JSX support. Default to False.
+ include_toc(bool, Optional): Include table of contents for module file. Defaults to False.
Returns:
str: Markdown documentation of selected object.
@@ -844,7 +1058,7 @@ def import2md(self, obj: Any, depth: int = 1, is_mdx: bool = False) -> str:
if inspect.isclass(obj):
return self.class2md(obj, depth=depth, is_mdx=is_mdx)
elif isinstance(obj, types.ModuleType):
- return self.module2md(obj, depth=depth, is_mdx=is_mdx)
+ return self.module2md(obj, depth=depth, is_mdx=is_mdx, include_toc=include_toc)
elif callable(obj):
return self.func2md(obj, depth=depth, is_mdx=is_mdx)
else:
@@ -852,7 +1066,14 @@ def import2md(self, obj: Any, depth: int = 1, is_mdx: bool = False) -> str:
return ""
def overview2md(self, is_mdx: bool = False) -> str:
- """Generates a documentation overview file based on the generated docs."""
+ """Generates a documentation overview file based on the generated docs.
+
+ Args:
+ is_mdx (bool, optional): JSX support. Default to False.
+
+ Returns:
+ str: Markdown documentation of overview file.
+ """
entries_md = ""
for obj in list(
@@ -913,6 +1134,26 @@ def overview2md(self, is_mdx: bool = False) -> str:
modules=modules_md, classes=classes_md, functions=functions_md
)
+ def toc2md(self, module: types.ModuleType = None, is_mdx: bool = False) -> str:
+ """Generates table of contents for imported object."""
+ toc = []
+ for obj in self.generated_objects:
+ if module and (module.__name__ != obj["module"] or obj["type"] == "module"):
+ continue
+ # module_name = obj["module"].split(".")[-1]
+ full_name = obj["full_name"]
+ name = obj["name"]
+ if is_mdx:
+ link = "./" + obj["module"] + ".mdx#" + obj["anchor_tag"]
+ else:
+ link = "./" + obj["module"] + ".md#" + obj["anchor_tag"]
+ line = f"- [`{name}`]({link})"
+ depth = max(len(full_name.split(".")) - 1, 0)
+ if depth:
+ line = "\t" * depth + line
+ toc.append(line)
+ return _TOC_TEMPLATE.format(toc="\n".join(toc))
+
def generate_docs(
paths: List[str],
@@ -925,6 +1166,9 @@ def generate_docs(
overview_file: Optional[str] = None,
watermark: bool = True,
validate: bool = False,
+ private_modules: bool = False,
+ include_toc: bool = False,
+ url_line_prefix: Optional[str] = None,
) -> None:
"""Generates markdown documentation for provided paths based on Google-style docstrings.
@@ -935,9 +1179,12 @@ def generate_docs(
src_base_url: The base url of the github link. Should include branch name. All source links are generated with this prefix.
remove_package_prefix: If `True`, the package prefix will be removed from all functions and methods.
ignored_modules: A list of modules that should be ignored.
+ output_format: Markdown file extension and format.
overview_file: Filename of overview file. If not provided, no overview file will be generated.
watermark: If `True`, add a watermark with a timestamp to bottom of the markdown files.
validate: If `True`, validate the docstrings via pydocstyle. Requires pydocstyle to be installed.
+ private_modules: If `True`, includes modules with `_` prefix.
+ url_line_prefix: Line prefix for git repository line url anchors. Default: None - github "L".
"""
stdout_mode = output_path.lower() == "stdout"
@@ -978,6 +1225,7 @@ def generate_docs(
src_root_path=src_root_path,
src_base_url=src_base_url,
remove_package_prefix=remove_package_prefix,
+ url_line_prefix=url_line_prefix,
)
pydocstyle_cmd = "pydocstyle --convention=google --add-ignore=D100,D101,D102,D103,D104,D105,D107,D202"
@@ -992,15 +1240,19 @@ def generate_docs(
# Generate one file for every discovered module
for loader, module_name, _ in pkgutil.walk_packages([path]):
- if _is_module_ignored(module_name, ignored_modules):
+ if _is_module_ignored(module_name, ignored_modules, private_modules):
# Add module to ignore list, so submodule will also be ignored
ignored_modules.append(module_name)
continue
-
try:
- mod_spec = loader.find_spec(module_name)
- mod = importlib.util.module_from_spec(mod_spec)
- module_md = generator.module2md(mod, is_mdx=is_mdx)
+ try:
+ mod_spec = importlib.util.spec_from_loader(module_name, loader)
+ mod = importlib.util.module_from_spec(mod_spec)
+ mod_spec.loader.exec_module(mod)
+ except AttributeError:
+ # For older python version compatibility
+ mod = loader.find_module(module_name).load_module(module_name) # type: ignore
+ module_md = generator.module2md(mod, is_mdx=is_mdx, include_toc=include_toc)
if not module_md:
# Module md is empty -> ignore module and all submodules
# Add module to ignore list, so submodule will also be ignored
@@ -1039,7 +1291,7 @@ def generate_docs(
spec.loader.exec_module(mod) # type: ignore
if mod:
- module_md = generator.module2md(mod, is_mdx=is_mdx)
+ module_md = generator.module2md(mod, is_mdx=is_mdx, include_toc=include_toc)
if stdout_mode:
print(module_md)
else:
@@ -1071,15 +1323,20 @@ def generate_docs(
path=obj.__path__, # type: ignore
prefix=obj.__name__ + ".", # type: ignore
):
- if _is_module_ignored(module_name, ignored_modules):
+ if _is_module_ignored(module_name, ignored_modules, private_modules):
# Add module to ignore list, so submodule will also be ignored
ignored_modules.append(module_name)
continue
try:
- mod_spec = loader.find_spec(module_name)
- mod = importlib.util.module_from_spec(mod_spec)
- module_md = generator.module2md(mod, is_mdx=is_mdx)
+ try:
+ mod_spec = importlib.util.spec_from_loader(module_name, loader)
+ mod = importlib.util.module_from_spec(mod_spec)
+ mod_spec.loader.exec_module(mod)
+ except AttributeError:
+ # For older python version compatibility
+ mod = loader.find_module(module_name).load_module(module_name) # type: ignore
+ module_md = generator.module2md(mod, is_mdx=is_mdx, include_toc=include_toc)
if not module_md:
# Module MD is empty -> ignore module and all submodules
@@ -1132,5 +1389,5 @@ def generate_docs(
# Write mkdocs pages file
print("Writing mkdocs .pages file.")
# TODO: generate navigation items to fix problem with naming
- with open(os.path.join(output_path, ".pages"), "w") as f:
+ with open(os.path.join(output_path, ".pages"), "w", encoding="utf-8", newline="\n") as f:
f.write(_MKDOCS_PAGES_TEMPLATE.format(overview_file=overview_file))