Source code for opend6_tools.character.monsterbook

"""
This creates a Python Module with creature or character definitions.
The module will be a CLI application with a number of subcommands.

-   ``python creature_module.py display`` will display the spells in RST format.
    This is used by the publication process.

-   ``python creature_module.py debug 'name'`` will write debugging output for a spell.

..  autofunction:: build_app

..  autofunction:: make_character_doctest

..  autofunction::  parse_param_name

..  autoclass:: Format
    :members:

"""

import doctest
from pathlib import Path
import subprocess
from typing import cast, Literal, Annotated
import string
import sys

import typer

from .features import (
    Character,
    Creature,
    Agility,
    Intellect,
    Coordination,
    Acumen,
    Physique,
    Charisma,
    CharacterDict,
    CharacterBudget,
)
from .output import Format, FORMAT_OPTIONS, detail, parse_param_name, CharacterWriter
from .workbook import debug


[docs] def build_app( book: dict[str, Character | Creature], book_attr_name: str = "characters", *, rich_markup_mode: Literal["rich", "markdown"] | None = "rich", ) -> typer.Typer: characters_app = typer.Typer( help="Work with this collection of Characters (or Creatures).", rich_markup_mode=rich_markup_mode, ) @characters_app.command(name="display") def display_command( format: Annotated[FORMAT_OPTIONS, typer.Option(case_sensitive=False)] = "TABLE", ): """Write details of all Character or Creature definitions. The default format is a markdown table that can be displayed in Jupyter Lab. """ detail(book, Format[format]) @characters_app.command(name="debug") def debug_command( names: Annotated[ list[str] | None, typer.Argument(help="Character name (or number)") ] = None, check: bool = True, ): """Print debugging information for a specific definition to STDOUT""" debug(book, names) @characters_app.command(name="test") def test_command(make: bool = False, verbose: bool = False): # pragma: no cover """Run the doctest examples, using the __test__ global. If the --make option is present, it writes a suggested __test__ definition. """ module = sys.modules["__main__"] print(f"Testing {Path(cast(str, module.__file__)).relative_to(Path.cwd())}") if make: make_character_doctest(book, book_attr_name) sys.exit() if not hasattr(module, "__test__"): print( "No __test__ found. If present, it must be **before** the build_app(). The --make option can be used to create a template." ) sys.exit(2) else: failures, tests = doctest.testmod(module, verbose=verbose) sys.exit(failures) @characters_app.command(name="blank") def blank_sheet_command( format: Annotated[ FORMAT_OPTIONS, typer.Option(case_sensitive=False) ] = "PLAYER", ): """ Print a blank character sheet in the desired format. The default format is HTML that can be run through **xhtml2pdf**. """ blank = Character( agility=Agility(), intellect=Intellect(), coordination=Coordination(), acumen=Acumen(), physique=Physique(), charisma=Charisma(), ) form = Format[format] detail(blank, form) @characters_app.command(name="pdf") def pdf_sheet_command( format: Annotated[FORMAT_OPTIONS, typer.Option(case_sensitive=False)] = "TABLE", names: Annotated[ list[str] | None, typer.Argument(help="Character names") ] = None, ): """ Create a PDF character sheet. There are several choices of formatting pipelines. - RST ("TABLE" format) -> **rst2html** -> **xhtml2pdf**. - HTML ("PLAYER" format) -> **xhtml2pdf**. - LATEX ("LATEX" format) -> **pdflatex**. """ def sanitize(filename: str) -> str: for char in string.punctuation + string.whitespace: filename = filename.replace(char, "_") while "__" in filename: filename = filename.replace("__", "_") return filename form = Format[format] writer_class: type[CharacterWriter] = form.value w = writer_class() all_chars: CharacterDict match book: case Character() | Creature() as one_char: all_chars = {one_char.name: one_char} case list(list_char): all_chars = {c.name: c for c in list_char} case dict(dict_char): all_chars = dict_char for name in names or [""]: target_name = parse_param_name(all_chars, name) if not target_name: # pragma: no cover sys.exit(f"Can't match {name!r} in {list(all_chars.keys())!r}") character = all_chars[target_name] if not character.name: filename = sanitize(target_name) else: filename = sanitize(character.name) match form: case Format.TABLE: step_0 = (Path.cwd() / filename).with_suffix(".rst") step_1 = Path(step_0).with_suffix(".html") step_2 = Path(step_1).with_suffix(".pdf") print(f"Creating {step_0}") print(f"rst2html {step_0} {step_1}") print(f"xhtml2pdf {step_1} {step_2}") step_0.write_text(w.report(character)) subprocess.run( ["rst2html", str(step_0), "--output", str(step_1)], check=True ) subprocess.run(["xhtml2pdf", str(step_1), str(step_2)], check=True) case Format.PLAYER: step_0 = (Path.cwd() / filename).with_suffix(".html") step_1 = Path(step_0).with_suffix(".pdf") print(f"Creating {step_0}") print(f"xhtml2pdf {step_0} {step_1}") step_0.write_text(w.report(character)) subprocess.run(["xhtml2pdf", str(step_0), str(step_1)], check=True) case Format.LATEX: step_0 = (Path.cwd() / filename).with_suffix(".tex") print(f"Creating {step_0}") print(f"pdflatex {step_0}") step_0.write_text(w.report(character)) subprocess.run(["pdflatex", str(step_0)], check=True) case _: # pragma: no cover sys.exit(f"not a valid format choice: {format!r}") return characters_app
[docs] def make_character_doctest( character_book: dict[str, Character | Creature], book_attr_name: str = "characters", budget: CharacterBudget = CharacterBudget.NO_BUDGET, ) -> None: """ Given a book of Characters or Creatures, write a ``__test__`` definition, suitable for doctest. :param character_book: The book with a list of characters or creatures. :param book_attr_name: The attribute name for the book. :param budget: A CharacterBudget against which to test the character or creature. """ expected = { "Attributes": "{budget.attributes} out of {budget.attributes}", "Skills": "{budget.skills} out of {budget.skills}", "Options": "{budget.options} out of {budget.options}", } print("__test__ = {") for slot, character in enumerate(character_book.values()): print( f' "{character.name}": ">>> {book_attr_name}[{slot}].budget_check({budget})\\n{expected!r}",' ) print("}")