Module botroyale.util.docs
Documentation utility.
This module includes tools to test, create, and open the documentation locally.
Project must be installed from source and with dev requirements, see:
botroyale.guides.contributing
.
To create and open the docs:
botroyale docs --help
Creating the docs
Uses the pdoc3
library to automatically read source code and produce HTML
documentation. Will delete the output folder and recreate docs. Default output
folder is in the usr dir.
Testing the docs
See botroyale.util.code
for more information about testing.
Expand source code Browse git
"""Documentation utility.
This module includes tools to test, create, and open the documentation locally.
Project must be installed from source and with dev requirements, see:
`botroyale.guides.contributing`.
To create and open the docs:
```noformat
botroyale docs --help
```
### Creating the docs
Uses the `pdoc3` library to automatically read source code and produce HTML
documentation. Will delete the output folder and recreate docs. Default output
folder is in the usr dir.
### Testing the docs
See `botroyale.util.code` for more information about testing.
"""
from typing import Optional
import shutil
import os
import traceback
import sys
import warnings
import argparse
from pathlib import Path
from pdoc import Module, Context, tpl_lookup, link_inheritance
from botroyale.util import PROJ_DIR, PACKAGE_DIR, INSTALLED_FROM_SOURCE
from botroyale.util.file import popen_path, file_dump, get_usr_dir
DOCS_DIR = PROJ_DIR / "docs"
TEMPLATE_DIR = DOCS_DIR / "templates"
USER_DIR = "botroyale"
def entry_point_docs(args) -> int:
"""Script entry point to run the documentation utility."""
parser = argparse.ArgumentParser(
description="Generate and view Bot Royale documentation",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="force recreating the docs",
)
parser.add_argument(
"--no-open",
"--no",
action="store_true",
help="don't open the docs",
)
args = parser.parse_args(args)
make_docs(force_remake=args.force)
if not args.no_open:
open_docs()
return 0
def open_docs(
output_dir: Optional[os.PathLike] = None,
force_remake: bool = False,
):
"""Opens the docs in default browser.
Generates the docs if missing or if *force_remake*.
"""
output_dir = _get_output_dir(output_dir)
make_docs(output_dir, force_remake=force_remake)
index_file = _get_index_file(output_dir)
popen_path(index_file)
def docs_exist(output_dir: Optional[os.PathLike] = None) -> bool:
"""Return True if docs exist in *output_dir*."""
output_dir = _get_output_dir(output_dir)
index_file = _get_index_file(output_dir)
return index_file.is_file()
def make_docs(
output_dir: Optional[os.PathLike] = None,
force_remake: bool = False,
):
"""Create the docs if missing or if *force_remake*."""
output_dir = _get_output_dir(output_dir)
if force_remake or not docs_exist(output_dir):
_make_docs(output_dir)
def test_docs() -> bool:
"""If making the docs raises no warnings or exceptions."""
if not INSTALLED_FROM_SOURCE:
print("Cannot create docs unless installed from source.")
return False
issues = []
with warnings.catch_warnings(record=True) as warning_catcher:
try:
make_docs(force_remake=True)
except Exception as e:
issues.append(f" Error: {e}")
issues = [f" Warning: {w.message}" for w in warning_catcher] + issues
if len(issues) > 0:
print(f"Found {len(issues)} issues:")
print("\n".join(issues))
return False
print("Documentation built sucessfully.")
return True
def _get_index_file(output_dir: Optional[os.PathLike]) -> Path:
output_dir = _get_output_dir(output_dir)
return output_dir / "botroyale" / "index.html"
def _get_output_dir(output_dir: Optional[os.PathLike]) -> Path:
return get_usr_dir("docs") if output_dir is None else output_dir
def _make_docs(output_dir: Optional[os.PathLike] = None):
"""Clear and create the docs."""
if not INSTALLED_FROM_SOURCE:
raise EnvironmentError("Cannot create docs unless installed from source.")
output_dir = _get_output_dir(output_dir)
if output_dir.is_dir():
print("Clearing existing docs...")
shutil.rmtree(output_dir)
print("Preparing docs...")
tpl_lookup.directories.insert(0, str(TEMPLATE_DIR))
_copy_assets(output_dir)
print("Building docs...")
doc_root = _get_root_package_doc()
print("Writing new docs...")
_write_html(doc_root, output_dir)
print("Make docs done.")
def _copy_assets(output_dir):
new_package_dir = output_dir / "botroyale"
new_package_dir.mkdir(parents=True, exist_ok=True)
# Copy icon
shutil.copy(
PACKAGE_DIR / "icon.png",
new_package_dir / "icon.png",
)
# Copy preview gif
shutil.copy(
PACKAGE_DIR / "assets" / "preview.gif",
new_package_dir / "preview.gif",
)
def _recursive_files(dir, _top_dir=None):
_top_dir = dir if _top_dir is None else _top_dir
for child in dir.iterdir():
if child.is_dir():
yield from _recursive_files(child, _top_dir)
elif child.is_file():
yield child.relative_to(_top_dir)
def _get_root_package_doc():
try:
_write_guides()
context = Context()
root_package = Module("botroyale", context=context)
link_inheritance(context)
except Exception as e:
print("".join(traceback.format_exception(*sys.exc_info())))
_delete_guides()
raise e
_delete_guides()
return root_package
def _write_guides():
guides_md = PACKAGE_DIR.parent / "docs" / "guides"
guides_py = PACKAGE_DIR / "guides"
if guides_py.is_dir():
warnings.warn(f"Guides subpackage dir already exists: {guides_py}")
guides_py.mkdir(exist_ok=True)
guides = _recursive_files(guides_md)
for path in guides:
path_py = guides_py / path.parent / f"{path.stem}.py"
path_py.parent.mkdir(parents=True, exist_ok=True)
path_md = _windows_compatibility(str(guides_md / path))
include_md = f'""".. include:: {path_md}"""'
file_dump(path_py, include_md)
def _delete_guides():
guides_py = PACKAGE_DIR / "guides"
shutil.rmtree(guides_py)
def _write_html(doc_root, output_dir):
for mod in _recursive_mods(doc_root):
with warnings.catch_warnings(record=True) as warning_catcher:
html = mod.html()
for w in warning_catcher:
warning_str = w.message.args[0]
warnings.warn(warning_str)
file_path = output_dir / _module_relative_path(mod)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_dump(file_path, html)
def _module_relative_path(mod):
full_module_name = mod.name
module_parts = full_module_name.split(".")
module_name = module_parts[-1]
file_path = Path()
for n in module_parts[:-1]:
file_path /= n
if mod.is_package:
file_path /= module_name
file_path /= "index.html"
else:
file_path /= f"{module_name}.html"
return file_path
def _recursive_mods(mod):
yield mod
for submod in mod.submodules():
yield from _recursive_mods(submod)
def _windows_compatibility(s):
"""Multiply backslashes in a string for windows path compatibility."""
return s.replace("\\", "\\\\")
Functions
def docs_exist(output_dir: Optional[os.PathLike] = None) ‑> bool
-
Return True if docs exist in output_dir.
Expand source code Browse git
def docs_exist(output_dir: Optional[os.PathLike] = None) -> bool: """Return True if docs exist in *output_dir*.""" output_dir = _get_output_dir(output_dir) index_file = _get_index_file(output_dir) return index_file.is_file()
def entry_point_docs(args) ‑> int
-
Script entry point to run the documentation utility.
Expand source code Browse git
def entry_point_docs(args) -> int: """Script entry point to run the documentation utility.""" parser = argparse.ArgumentParser( description="Generate and view Bot Royale documentation", ) parser.add_argument( "-f", "--force", action="store_true", help="force recreating the docs", ) parser.add_argument( "--no-open", "--no", action="store_true", help="don't open the docs", ) args = parser.parse_args(args) make_docs(force_remake=args.force) if not args.no_open: open_docs() return 0
def make_docs(output_dir: Optional[os.PathLike] = None, force_remake: bool = False)
-
Create the docs if missing or if force_remake.
Expand source code Browse git
def make_docs( output_dir: Optional[os.PathLike] = None, force_remake: bool = False, ): """Create the docs if missing or if *force_remake*.""" output_dir = _get_output_dir(output_dir) if force_remake or not docs_exist(output_dir): _make_docs(output_dir)
def open_docs(output_dir: Optional[os.PathLike] = None, force_remake: bool = False)
-
Opens the docs in default browser.
Generates the docs if missing or if force_remake.
Expand source code Browse git
def open_docs( output_dir: Optional[os.PathLike] = None, force_remake: bool = False, ): """Opens the docs in default browser. Generates the docs if missing or if *force_remake*. """ output_dir = _get_output_dir(output_dir) make_docs(output_dir, force_remake=force_remake) index_file = _get_index_file(output_dir) popen_path(index_file)
def test_docs() ‑> bool
-
If making the docs raises no warnings or exceptions.
Expand source code Browse git
def test_docs() -> bool: """If making the docs raises no warnings or exceptions.""" if not INSTALLED_FROM_SOURCE: print("Cannot create docs unless installed from source.") return False issues = [] with warnings.catch_warnings(record=True) as warning_catcher: try: make_docs(force_remake=True) except Exception as e: issues.append(f" Error: {e}") issues = [f" Warning: {w.message}" for w in warning_catcher] + issues if len(issues) > 0: print(f"Found {len(issues)} issues:") print("\n".join(issues)) return False print("Documentation built sucessfully.") return True