milieux.cli.doc

  1from dataclasses import dataclass, field
  2from pathlib import Path
  3import time
  4from typing import Literal, Union
  5
  6from fancy_dataclass import ArgparseDataclass, CLIDataclass
  7import pdoc.render
  8import pdoc.web
  9
 10from milieux import logger
 11from milieux.distro import get_packages
 12
 13
 14DocFormat = Literal['markdown', 'google', 'numpy', 'restructuredtext']
 15
 16
 17@dataclass
 18class PkgArgs(ArgparseDataclass):
 19    """Specify which packages to build docs for."""
 20    packages: list[str] = field(
 21        default_factory=list,
 22        metadata={'nargs': '+', 'args': ['-p', '--packages'], 'help': 'list of packages'}
 23    )
 24    requirements: list[str] = field(
 25        default_factory=list,
 26        metadata={'nargs': '+', 'args': ['-r', '--requirements'], 'help': 'requirements file(s) containing packages'}
 27    )
 28    distros: list[str] = field(
 29        default_factory=list,
 30        metadata={'nargs': '+', 'args': ['-d', '--distros'], 'help': 'existing distro name(s) to include'}
 31    )
 32
 33    @property
 34    def all_packages(self) -> list[str]:
 35        """Gets a list of all packages."""
 36        return get_packages(self.packages, self.requirements, self.distros)
 37
 38
 39@dataclass
 40class RenderArgs(ArgparseDataclass):
 41    """Customize rendering of docs."""
 42    # TODO: enable a way to vary the arguments per package
 43    docformat: DocFormat = field(
 44        default='markdown',
 45        metadata={'help': 'docstring format', 'default_help': True}
 46    )
 47
 48    def configure(self) -> None:
 49        """Configures the global pdoc render settings."""
 50        pdoc.render.configure(docformat=self.docformat)
 51
 52
 53@dataclass
 54class DocBuild(CLIDataclass, command_name='build'):
 55    """Build API documentation."""
 56    output_dir: Path = field(
 57        metadata={
 58            'args': ['-o', '--output-dir'],
 59            'help': 'save output documentation to this directory'
 60        }
 61    )
 62    pkg_args: PkgArgs = field(
 63        default_factory=PkgArgs,
 64        metadata={'group': 'package arguments'}
 65    )
 66    render_args: RenderArgs = field(
 67        default_factory=RenderArgs,
 68        metadata={'group': 'rendering arguments'}
 69    )
 70
 71    def run(self) -> None:
 72        self.render_args.configure()
 73        start = time.perf_counter()
 74        logger.info(f'Building documentation to {self.output_dir}...')
 75        pdoc.pdoc(*self.pkg_args.all_packages, output_directory=self.output_dir)
 76        elapsed = time.perf_counter() - start
 77        logger.info(f'Build docs in {elapsed:.3g} sec')
 78
 79
 80@dataclass
 81class DocServe(CLIDataclass, command_name='serve'):
 82    """Serve API documentation."""
 83    host: str = field(
 84        default='localhost',
 85        metadata={'help': 'host on which to run HTTP server', 'default_help': True}
 86    )
 87    port: int = field(
 88        default=8080,
 89        metadata={'help': 'port on which to run HTTP serve', 'default_help': True}
 90    )
 91    no_browser: bool = field(
 92        default=False,
 93        metadata={'help': 'do not open a browser after web server has started'}
 94    )
 95    pkg_args: PkgArgs = field(
 96        default_factory=PkgArgs,
 97        metadata={'group': 'package arguments'}
 98    )
 99    render_args: RenderArgs = field(
100        default_factory=RenderArgs,
101        metadata={'group': 'rendering arguments'}
102    )
103
104    def run(self) -> None:
105        self.render_args.configure()
106        logger.info('Serving documentation...')
107        try:
108            httpd = pdoc.web.DocServer((self.host, self.port), self.pkg_args.all_packages)
109        except OSError as e:
110            raise OSError(f'Cannot start web server on {self.host}:{self.port}: {e}') from e
111        with httpd:
112            url = f'http://{self.host}:{httpd.server_port}'
113            logger.info(f'Server ready at {url}')
114            if not self.no_browser:
115                pdoc.web.open_browser(url)
116            try:
117                httpd.serve_forever()
118            except KeyboardInterrupt:
119                httpd.server_close()
120                return
121
122
123@dataclass
124class DocCmd(CLIDataclass, command_name='doc'):
125    """Generate API documentation."""
126    subcommand: Union[
127        DocBuild,
128        DocServe,
129    ] = field(metadata={'subcommand': True})
DocFormat = typing.Literal['markdown', 'google', 'numpy', 'restructuredtext']
@dataclass
class PkgArgs(fancy_dataclass.cli.ArgparseDataclass):
18@dataclass
19class PkgArgs(ArgparseDataclass):
20    """Specify which packages to build docs for."""
21    packages: list[str] = field(
22        default_factory=list,
23        metadata={'nargs': '+', 'args': ['-p', '--packages'], 'help': 'list of packages'}
24    )
25    requirements: list[str] = field(
26        default_factory=list,
27        metadata={'nargs': '+', 'args': ['-r', '--requirements'], 'help': 'requirements file(s) containing packages'}
28    )
29    distros: list[str] = field(
30        default_factory=list,
31        metadata={'nargs': '+', 'args': ['-d', '--distros'], 'help': 'existing distro name(s) to include'}
32    )
33
34    @property
35    def all_packages(self) -> list[str]:
36        """Gets a list of all packages."""
37        return get_packages(self.packages, self.requirements, self.distros)

Specify which packages to build docs for.

PkgArgs( packages: list[str] = <factory>, requirements: list[str] = <factory>, distros: list[str] = <factory>)
packages: list[str]
requirements: list[str]
distros: list[str]
all_packages: list[str]
34    @property
35    def all_packages(self) -> list[str]:
36        """Gets a list of all packages."""
37        return get_packages(self.packages, self.requirements, self.distros)

Gets a list of all packages.

subcommand_field_name: ClassVar[Optional[str]] = None
subcommand_dest_name: ClassVar[str] = '_subcommand_PkgArgs'
@dataclass
class RenderArgs(fancy_dataclass.cli.ArgparseDataclass):
40@dataclass
41class RenderArgs(ArgparseDataclass):
42    """Customize rendering of docs."""
43    # TODO: enable a way to vary the arguments per package
44    docformat: DocFormat = field(
45        default='markdown',
46        metadata={'help': 'docstring format', 'default_help': True}
47    )
48
49    def configure(self) -> None:
50        """Configures the global pdoc render settings."""
51        pdoc.render.configure(docformat=self.docformat)

Customize rendering of docs.

RenderArgs( docformat: Literal['markdown', 'google', 'numpy', 'restructuredtext'] = 'markdown')
docformat: Literal['markdown', 'google', 'numpy', 'restructuredtext'] = 'markdown'
def configure(self) -> None:
49    def configure(self) -> None:
50        """Configures the global pdoc render settings."""
51        pdoc.render.configure(docformat=self.docformat)

Configures the global pdoc render settings.

subcommand_field_name: ClassVar[Optional[str]] = None
subcommand_dest_name: ClassVar[str] = '_subcommand_RenderArgs'
@dataclass
class DocBuild(fancy_dataclass.cli.CLIDataclass):
54@dataclass
55class DocBuild(CLIDataclass, command_name='build'):
56    """Build API documentation."""
57    output_dir: Path = field(
58        metadata={
59            'args': ['-o', '--output-dir'],
60            'help': 'save output documentation to this directory'
61        }
62    )
63    pkg_args: PkgArgs = field(
64        default_factory=PkgArgs,
65        metadata={'group': 'package arguments'}
66    )
67    render_args: RenderArgs = field(
68        default_factory=RenderArgs,
69        metadata={'group': 'rendering arguments'}
70    )
71
72    def run(self) -> None:
73        self.render_args.configure()
74        start = time.perf_counter()
75        logger.info(f'Building documentation to {self.output_dir}...')
76        pdoc.pdoc(*self.pkg_args.all_packages, output_directory=self.output_dir)
77        elapsed = time.perf_counter() - start
78        logger.info(f'Build docs in {elapsed:.3g} sec')

Build API documentation.

DocBuild( output_dir: pathlib.Path, pkg_args: PkgArgs = <factory>, render_args: RenderArgs = <factory>)
output_dir: pathlib.Path
pkg_args: PkgArgs
render_args: RenderArgs
def run(self) -> None:
72    def run(self) -> None:
73        self.render_args.configure()
74        start = time.perf_counter()
75        logger.info(f'Building documentation to {self.output_dir}...')
76        pdoc.pdoc(*self.pkg_args.all_packages, output_directory=self.output_dir)
77        elapsed = time.perf_counter() - start
78        logger.info(f'Build docs in {elapsed:.3g} sec')

Runs the main body of the program.

Subclasses should implement this to provide custom behavior.

If the class has a subcommand defined, and it is an instance of CLIDataclass, the default implementation of run will be to call the subcommand's own implementation.

subcommand_field_name: ClassVar[Optional[str]] = None
subcommand_dest_name: ClassVar[str] = '_subcommand_DocBuild'
@dataclass
class DocServe(fancy_dataclass.cli.CLIDataclass):
 81@dataclass
 82class DocServe(CLIDataclass, command_name='serve'):
 83    """Serve API documentation."""
 84    host: str = field(
 85        default='localhost',
 86        metadata={'help': 'host on which to run HTTP server', 'default_help': True}
 87    )
 88    port: int = field(
 89        default=8080,
 90        metadata={'help': 'port on which to run HTTP serve', 'default_help': True}
 91    )
 92    no_browser: bool = field(
 93        default=False,
 94        metadata={'help': 'do not open a browser after web server has started'}
 95    )
 96    pkg_args: PkgArgs = field(
 97        default_factory=PkgArgs,
 98        metadata={'group': 'package arguments'}
 99    )
100    render_args: RenderArgs = field(
101        default_factory=RenderArgs,
102        metadata={'group': 'rendering arguments'}
103    )
104
105    def run(self) -> None:
106        self.render_args.configure()
107        logger.info('Serving documentation...')
108        try:
109            httpd = pdoc.web.DocServer((self.host, self.port), self.pkg_args.all_packages)
110        except OSError as e:
111            raise OSError(f'Cannot start web server on {self.host}:{self.port}: {e}') from e
112        with httpd:
113            url = f'http://{self.host}:{httpd.server_port}'
114            logger.info(f'Server ready at {url}')
115            if not self.no_browser:
116                pdoc.web.open_browser(url)
117            try:
118                httpd.serve_forever()
119            except KeyboardInterrupt:
120                httpd.server_close()
121                return

Serve API documentation.

DocServe( host: str = 'localhost', port: int = 8080, no_browser: bool = False, pkg_args: PkgArgs = <factory>, render_args: RenderArgs = <factory>)
host: str = 'localhost'
port: int = 8080
no_browser: bool = False
pkg_args: PkgArgs
render_args: RenderArgs
def run(self) -> None:
105    def run(self) -> None:
106        self.render_args.configure()
107        logger.info('Serving documentation...')
108        try:
109            httpd = pdoc.web.DocServer((self.host, self.port), self.pkg_args.all_packages)
110        except OSError as e:
111            raise OSError(f'Cannot start web server on {self.host}:{self.port}: {e}') from e
112        with httpd:
113            url = f'http://{self.host}:{httpd.server_port}'
114            logger.info(f'Server ready at {url}')
115            if not self.no_browser:
116                pdoc.web.open_browser(url)
117            try:
118                httpd.serve_forever()
119            except KeyboardInterrupt:
120                httpd.server_close()
121                return

Runs the main body of the program.

Subclasses should implement this to provide custom behavior.

If the class has a subcommand defined, and it is an instance of CLIDataclass, the default implementation of run will be to call the subcommand's own implementation.

subcommand_field_name: ClassVar[Optional[str]] = None
subcommand_dest_name: ClassVar[str] = '_subcommand_DocServe'
@dataclass
class DocCmd(fancy_dataclass.cli.CLIDataclass):
124@dataclass
125class DocCmd(CLIDataclass, command_name='doc'):
126    """Generate API documentation."""
127    subcommand: Union[
128        DocBuild,
129        DocServe,
130    ] = field(metadata={'subcommand': True})

Generate API documentation.

DocCmd( subcommand: Union[DocBuild, DocServe])
subcommand: Union[DocBuild, DocServe]
subcommand_field_name: ClassVar[Optional[str]] = 'subcommand'
subcommand_dest_name: ClassVar[str] = '_subcommand_DocCmd'