Files
dungeon-sheets/dungeonsheets/make_sheets.py
T
Mark Wolfman 30369ce1d4 Ran flake8 and black linters, and other cleanup-related fixes.
Project now passes flake8 and black linter (also including more rst
cleanup). Moved latex related things to dedicated ``latex.py`` module,
and removed the ``makesheets -dF`` call from travis.
2021-04-16 11:10:17 -05:00

967 lines
29 KiB
Python
Executable File

#!/usr/bin/env python
import logging
import argparse
import os
import subprocess
import warnings
import re
from pathlib import Path
from multiprocessing import Pool, cpu_count
from itertools import product
from fdfgen import forge_fdf
import pdfrw
from jinja2 import Environment, PackageLoader
from dungeonsheets import character as _char
from dungeonsheets import exceptions, readers, latex
from dungeonsheets.stats import mod_str
log = logging.getLogger(__name__)
"""Program to take character definitions and build a PDF of the
character sheet."""
bold_re = re.compile(r"\*\*([^*]+)\*\*")
it_re = re.compile(r"\*([^*]+)\*")
verb_re = re.compile(r"``([^`]+)``")
heading_re = re.compile(r"^[ \t\r\f\v]*(.+)\n\s*([-=\^]+)$", flags=re.MULTILINE)
# What defines a list in reST:
# - a blank line
# - one or more of the following
# - "- [a-z]"
# - an additional line of text (if multi-line bullets)
# - a blank line
# - a blank line
# - a non-list line or end of file
# list_re = re.compile('^[ \t\r\f\v]*\n((?:\s*[-*+]\s+[^\n]*\n)+)', flags=re.MULTILINE)
list_re = re.compile(
r"^[ \t\r\f\v]*\n" # A blank line
r"((?:\s*[-*+]\s+[^\n]*\n)+)" # The first line of each list item
"",
flags=re.MULTILINE,
)
# What defines a list item in reST:
# - a line starting with "- " then some text
# - zero or more lines starting with anything other than "- "
list_item_re = re.compile(r"^\s*[-*+]\s+", flags=re.MULTILINE)
def _parse_rst_lists(rst):
"""Read lists in reST and iterate.
Yields
======
list_rst : str
The matching reST list found in the input text
list_items : list
A python list of the items found in the reST list.
"""
for match in list_re.finditer(rst):
list_rst = match.group(0)
# Separate the list items
list_items = list_item_re.split(match.group(1))
# Clean up separated list items
list_items = list_items[1:] # First item is an empty string
list_items = [item.replace("\n", " ").strip() for item in list_items]
yield list_rst, list_items
def _parse_rst_headings(rst):
"""Read headings in reST and iterate.
Yields
======
heading_rst : str
The matching reST heading found in the input text.
heading : str
The text of the heading with underlining removed.
level : int
How deep the heading is: 0 is top-level, 1 is next level down,
etc.
"""
heading_levels = {
"=": 0,
"-": 1,
"^": 2,
}
for match in heading_re.finditer(rst):
heading_rst = match.group(0)
heading, underline = match.groups()
# Check for valid heading
if len(underline) < len(heading):
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
continue
if len(set(underline)) > 1:
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
continue
# Valid heading, so determine how many levels deep it is
level = heading_levels[underline[0]]
yield heading_rst, heading, level
jinja_env = Environment(
loader=PackageLoader("dungeonsheets", "forms"),
block_start_string="[%",
block_end_string="%]",
variable_start_string="[[",
variable_end_string="]]",
)
jinja_env.filters["rst_to_latex"] = latex.rst_to_latex
jinja_env.filters["mod_str"] = mod_str
CHECKBOX_ON = "Yes"
CHECKBOX_OFF = "Off"
PDFTK_CMD = "pdftk"
def text_box(string):
"""Format a string for displaying in a text box."""
# remove multiple whitespace without removing linebreaks
new_string = " ".join(string.replace("\n", r"\m").split())
# Remove *single* line breaks, swap *multi* line breaks to single (fdf: \r)
new_string = (
new_string.replace(r"\m \m", r"\r").replace(r"\m\m", r"\r").replace(r"\m", " ")
)
return new_string
def create_druid_shapes_pdf(
character, basename, keep_temp_files=False, use_dnd_decorations=False
):
template = jinja_env.get_template("druid_shapes_template.tex")
return latex.create_latex_pdf(
character,
basename,
template,
keep_temp_files=keep_temp_files,
use_dnd_decorations=use_dnd_decorations,
)
def create_infusions_pdf(
character, basename, keep_temp_files=False, use_dnd_decorations=False
):
template = jinja_env.get_template("infusions_template.tex")
return latex.create_latex_pdf(
character,
basename,
template,
keep_temp_files=keep_temp_files,
use_dnd_decorations=use_dnd_decorations,
)
def create_spellbook_pdf(
character, basename, keep_temp_files=False, use_dnd_decorations=False
):
template = jinja_env.get_template("spellbook_template.tex")
return latex.create_latex_pdf(
character,
basename,
template,
keep_temp_files=keep_temp_files,
use_dnd_decorations=use_dnd_decorations,
)
def create_features_pdf(
character, basename, keep_temp_files=False, use_dnd_decorations=False
):
template = jinja_env.get_template("features_template.tex")
return latex.create_latex_pdf(
character,
basename,
template,
keep_temp_files=keep_temp_files,
use_dnd_decorations=use_dnd_decorations,
)
def create_spells_pdf(character, basename, flatten=False):
classes_and_levels = " / ".join(
[c.name + " " + str(c.level) for c in character.spellcasting_classes]
)
abilities = " / ".join(
[c.spellcasting_ability.upper()[:3] for c in character.spellcasting_classes]
)
DCs = " / ".join(
[str(character.spell_save_dc(c)) for c in character.spellcasting_classes]
)
bonuses = " / ".join(
[
mod_str(character.spell_attack_bonus(c))
for c in character.spellcasting_classes
]
)
def spell_level(x):
return x or 0
fields = {
"Spellcasting Class 2": classes_and_levels,
"SpellcastingAbility 2": abilities,
"SpellSaveDC 2": DCs,
"SpellAtkBonus 2": bonuses,
# Number of spell slots
"SlotsTotal 19": spell_level(character.spell_slots(1)),
"SlotsTotal 20": spell_level(character.spell_slots(2)),
"SlotsTotal 21": spell_level(character.spell_slots(3)),
"SlotsTotal 22": spell_level(character.spell_slots(4)),
"SlotsTotal 23": spell_level(character.spell_slots(5)),
"SlotsTotal 24": spell_level(character.spell_slots(6)),
"SlotsTotal 25": spell_level(character.spell_slots(7)),
"SlotsTotal 26": spell_level(character.spell_slots(8)),
"SlotsTotal 27": spell_level(character.spell_slots(9)),
}
# Cantrips
cantrip_fields = (f"Spells 10{i}" for i in (14, 16, 17, 18, 19, 20, 21, 22))
cantrips = (spl for spl in character.spells if spl.level == 0)
for spell, field_name in zip(cantrips, cantrip_fields):
fields[field_name] = str(spell)
# Spells for each level
field_numbers = {
1: (
1015,
1023,
1024,
1025,
1026,
1027,
1028,
1029,
1030,
1031,
1032,
1033,
),
2: (
1046,
1034,
1035,
1036,
1037,
1038,
1039,
1040,
1041,
1042,
1043,
1044,
1045,
),
3: (
1048,
1047,
1049,
1050,
1051,
1052,
1053,
1054,
1055,
1056,
1057,
1058,
1059,
),
4: (
1061,
1060,
1062,
1063,
1064,
1065,
1066,
1067,
1068,
1069,
1070,
1071,
1072,
),
5: (
1074,
1073,
1075,
1076,
1077,
1078,
1079,
1080,
1081,
),
6: (
1083,
1082,
1084,
1085,
1086,
1087,
1088,
1089,
1090,
),
7: (
1092,
1091,
1093,
1094,
1095,
1096,
1097,
1098,
1099,
),
8: (
10101,
10100,
10102,
10103,
10104,
10105,
10106,
),
9: (10108, 10107, 10109, 101010, 101011, 101012, 101013),
}
prep_numbers = {
1: (
251,
309,
3010,
3011,
3012,
3013,
3014,
3015,
3016,
3017,
3018,
3019,
),
2: (
313,
310,
3020,
3021,
3022,
3023,
3024,
3025,
3026,
3027,
3028,
3029,
3030,
),
3: (
315,
314,
3031,
3032,
3033,
3034,
3035,
3036,
3037,
3038,
3039,
3040,
3041,
),
4: (
317,
316,
3042,
3043,
3044,
3045,
3046,
3047,
3048,
3049,
3050,
3051,
3052,
),
5: (
319,
318,
3053,
3054,
3055,
3056,
3057,
3058,
3059,
),
6: (
321,
320,
3060,
3061,
3062,
3063,
3064,
3065,
3066,
),
7: (
323,
322,
3067,
3068,
3069,
3070,
3071,
3072,
3073,
),
8: (
325,
324,
3074,
3075,
3076,
3077,
3078,
),
9: (
327,
326,
3079,
3080,
3081,
3082,
3083,
),
}
for level in field_numbers.keys():
spells = tuple(spl for spl in character.spells if spl.level == level)
field_names = tuple(f"Spells {i}" for i in field_numbers[level])
prep_names = tuple(f"Check Box {i}" for i in prep_numbers[level])
for spell, field, chk_field in zip(spells, field_names, prep_names):
fields[field] = str(spell)
is_prepared = any([spell == Spl for Spl in character.spells_prepared])
fields[chk_field] = CHECKBOX_ON if is_prepared else CHECKBOX_OFF
# # Uncomment to post field names instead:
# for field in field_names:
# fields.append((field, field))
# Make the actual pdf
dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/")
src_pdf = os.path.join(dirname, "blank-spell-sheet-default.pdf")
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def create_character_pdf(character, basename, flatten=False):
# Prepare the list of fields
fields = {
# Character description
"CharacterName": character.name,
"ClassLevel": character.classes_and_levels,
"Background": str(character.background),
"PlayerName": character.player_name,
"Race ": str(character.race),
"Alignment": character.alignment,
"XP": str(character.xp),
"Inspiration": str("Yes" if character.inspiration else "No"),
# Abilities
"ProfBonus": mod_str(character.proficiency_bonus),
"STRmod": str(character.strength.value),
"STR": mod_str(character.strength.modifier),
"DEXmod ": str(character.dexterity.value),
"DEX": mod_str(character.dexterity.modifier),
"CONmod": str(character.constitution.value),
"CON": mod_str(character.constitution.modifier),
"INTmod": str(character.intelligence.value),
"INT": mod_str(character.intelligence.modifier),
"WISmod": str(character.wisdom.value),
"WIS": mod_str(character.wisdom.modifier),
"CHamod": str(character.charisma.value),
"CHA": mod_str(character.charisma.modifier),
"AC": str(character.armor_class),
"Initiative": str(character.initiative),
"Speed": str(character.speed),
"Passive": 10 + character.perception,
# Saving throws (proficiencies handled later)
"ST Strength": mod_str(character.strength.saving_throw),
"ST Dexterity": mod_str(character.dexterity.saving_throw),
"ST Constitution": mod_str(character.constitution.saving_throw),
"ST Intelligence": mod_str(character.intelligence.saving_throw),
"ST Wisdom": mod_str(character.wisdom.saving_throw),
"ST Charisma": mod_str(character.charisma.saving_throw),
# Skills (proficiencies handled below)
"Acrobatics": mod_str(character.acrobatics),
"Animal": mod_str(character.animal_handling),
"Arcana": mod_str(character.arcana),
"Athletics": mod_str(character.athletics),
"Deception ": mod_str(character.deception),
"History ": mod_str(character.history),
"Insight": mod_str(character.insight),
"Intimidation": mod_str(character.intimidation),
"Investigation ": mod_str(character.investigation),
"Medicine": mod_str(character.medicine),
"Nature": mod_str(character.nature),
"Perception ": mod_str(character.perception),
"Performance": mod_str(character.performance),
"Persuasion": mod_str(character.persuasion),
"Religion": mod_str(character.religion),
"SleightofHand": mod_str(character.sleight_of_hand),
"Stealth ": mod_str(character.stealth),
"Survival": mod_str(character.survival),
# Hit points
"HDTotal": character.hit_dice,
"HPMax": str(character.hp_max),
# Personality traits and other features
"PersonalityTraits ": text_box(character.personality_traits),
"Ideals": text_box(character.ideals),
"Bonds": text_box(character.bonds),
"Flaws": text_box(character.flaws),
"Features and Traits": text_box(
character.features_text + character.features_and_traits
),
# Inventory
"CP": character.cp,
"SP": character.sp,
"EP": character.ep,
"GP": character.gp,
"PP": character.pp,
"Equipment": text_box(character.magic_items_text + character.equipment),
}
# Check boxes for proficiencies
ST_boxes = {
"strength": "Check Box 11",
"dexterity": "Check Box 18",
"constitution": "Check Box 19",
"intelligence": "Check Box 20",
"wisdom": "Check Box 21",
"charisma": "Check Box 22",
}
for ability in character.saving_throw_proficiencies:
fields[ST_boxes[ability]] = CHECKBOX_ON
# Add skill proficiencies
skill_boxes = {
"acrobatics": "Check Box 23",
"animal_handling": "Check Box 24",
"arcana": "Check Box 25",
"athletics": "Check Box 26",
"deception": "Check Box 27",
"history": "Check Box 28",
"insight": "Check Box 29",
"intimidation": "Check Box 30",
"investigation": "Check Box 31",
"medicine": "Check Box 32",
"nature": "Check Box 33",
"perception": "Check Box 34",
"performance": "Check Box 35",
"persuasion": "Check Box 36",
"religion": "Check Box 37",
"sleight_of_hand": "Check Box 38",
"stealth": "Check Box 39",
"survival": "Check Box 40",
}
for skill in character.skill_proficiencies:
try:
fields[skill_boxes[skill.replace(" ", "_").lower()]] = CHECKBOX_ON
except KeyError:
raise KeyError(f"Unknown skill: '{skill}'")
# Add weapons
weapon_fields = [
("Wpn Name", "Wpn1 AtkBonus", "Wpn1 Damage"),
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
]
if len(character.weapons) == 0:
character.wield_weapon("unarmed")
for _fields, weapon in zip(weapon_fields, character.weapons):
name_field, atk_field, dmg_field = _fields
fields[name_field] = weapon.name
fields[atk_field] = "{:+d}".format(weapon.attack_modifier)
fields[dmg_field] = f"{weapon.damage}/{weapon.damage_type}"
# Other attack information
attack = []
if character.armor:
attack.append(f"Armor: {character.armor}")
if character.shield:
attack.append(f"Shield: {character.shield}")
attack.append(character.attacks_and_spellcasting)
attack_str = "\n\n".join(attack)
fields["AttacksSpellcasting"] = text_box(attack_str)
# Other proficiencies and languages
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages)
fields["ProficienciesLang"] = prof_text
# Prepare the actual PDF
dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/")
src_pdf = os.path.join(dirname, "blank-character-sheet-default.pdf")
return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False):
"""Create a new PDF by applying fields to a src PDF document.
Parameters
==========
fields :
Data to fill into the form. The keys are field names in the PDF
form, and the values will be entered as the value in the PDF.
src_pdf :
Path to the PDF that will serve as the template.
basename :
The path of the destination PDF without the file extensions. The
resulting pdf will be {basename}.pdf
flatten :
If truthy, the PDF will be collapsed so it is no longer
editable.
"""
try:
_make_pdf_pdftk(fields, src_pdf, basename, flatten)
except FileNotFoundError:
# pdftk could not run, so alert the user and use pdfrw
warnings.warn(
f"Could not run `{PDFTK_CMD}`, using fallback; forcing `--editable`.",
RuntimeWarning,
)
_make_pdf_pdfrw(fields, src_pdf, basename, flatten)
def _make_pdf_pdfrw(fields: dict, src_pdf: str, basename: str, flatten: bool = False):
"""Backup make_pdf function in case pdftk is not available."""
template = pdfrw.PdfReader(src_pdf)
# Different types of PDF fields
BUTTON = "/Btn"
# Names for entries in PDF annotation list
# DEFAULT_VALUE = "/DV"
# APPEARANCE = "/MK"
FIELD = "/T"
# PROPS = "/P"
TYPE = "/FT"
# FLAGS = "/Ff"
# SUBTYPE = "/Subtype"
# ALL_KEYS = [
# "/DV",
# "/F",
# "/FT",
# "/Ff",
# "/MK",
# "/P",
# "/Rect",
# "/Subtype",
# "/T",
# "/Type",
# ]
annots = template.pages[0]["/Annots"]
# Update each annotation if it's in the requested dictionary
for annot in annots:
this_field = annot[FIELD][1:-1]
# Check if the field has a new value passed
if this_field in fields.keys():
val = fields[this_field]
# Convert integers to strings
if isinstance(val, int):
val = str(val)
log.debug(
f"Set field '{this_field}' "
f"({annot[TYPE]}) "
f"to `{val}` ({val.__class__}) "
f"in file '{basename}.pdf'"
)
# Prepare a PDF dictionary based on the fields properties
if annot[TYPE] == BUTTON:
# Radio buttons require special appearance streams
if val == CHECKBOX_ON:
val = bytes(val, "utf-8")
pdf_dict = pdfrw.PdfDict(V=val, AS=val)
else:
continue
else:
# All other widget types
pdf_dict = pdfrw.PdfDict(V=val)
annot.update(pdf_dict)
else:
log.debug(f"Skipping unused field '{this_field}' in file '{basename}.pdf'")
# Now write the PDF to the new pdf file
pdfrw.PdfWriter().write(f"{basename}.pdf", template)
def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
"""More robust way to make a PDF, but has a hard dependency."""
# Create the actual FDF file
fdfname = basename + ".fdf"
fdf = forge_fdf("", fields, [], [], [])
fdf_file = open(fdfname, "wb")
fdf_file.write(fdf)
fdf_file.close()
# Build the final flattened PDF documents
dest_pdf = basename + ".pdf"
popenargs = [
PDFTK_CMD,
src_pdf,
"fill_form",
fdfname,
"output",
dest_pdf,
]
if flatten:
popenargs.append("flatten")
subprocess.call(popenargs)
# Clean up temporary files
os.remove(fdfname)
def make_sheet(
character_file, character=None, flatten=False, fancy_decorations=False, debug=False
):
"""Prepare a PDF character sheet from the given character file.
Parameters
----------
character_file : str
File (.py) to load character from. Will save PDF using same name
character : Character, optional
If provided, will not load from the character file, just use
file for PDF name
flatten : bool, optional
If true, the resulting PDF will look better and won't be
fillable form.
fancy_decorations : bool, optional
Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
debug : bool, optional
Provide extra info and preserve temporary files.
"""
if character is None:
character = _char.Character.load(character_file)
# Set the fields in the FDF
char_base = os.path.splitext(character_file)[0] + "_char"
sheets = [char_base + ".pdf"]
pages = []
char_pdf = create_character_pdf(
character=character, basename=char_base, flatten=flatten
)
pages.append(char_pdf)
if character.is_spellcaster:
# Create spell sheet
spell_base = "{:s}_spells".format(os.path.splitext(character_file)[0])
create_spells_pdf(character=character, basename=spell_base, flatten=flatten)
sheets.append(spell_base + ".pdf")
if len(character.features) > 0:
feat_base = "{:s}_feats".format(os.path.splitext(character_file)[0])
try:
create_features_pdf(
character=character,
basename=feat_base,
keep_temp_files=debug,
use_dnd_decorations=fancy_decorations,
)
except exceptions.LatexNotFoundError:
log.warning(
"``pdflatex`` not available. Skipping features book "
f"for {character.name}"
)
else:
sheets.append(feat_base + ".pdf")
if character.is_spellcaster:
# Create spell book
spellbook_base = os.path.splitext(character_file)[0] + "_spellbook"
try:
create_spellbook_pdf(
character=character,
basename=spellbook_base,
keep_temp_files=debug,
use_dnd_decorations=fancy_decorations,
)
except exceptions.LatexNotFoundError:
log.warning(
f"``pdflatex`` not available. Skipping spellbook for {character.name}"
)
else:
sheets.append(spellbook_base + ".pdf")
# Create a list of Artificer infusions
infusions = getattr(character, "infusions", [])
if len(infusions) > 0:
infusions_base = os.path.splitext(character_file)[0] + "_infusions"
try:
create_infusions_pdf(
character=character,
basename=infusions_base,
keep_temp_files=debug,
use_dnd_decorations=fancy_decorations,
)
except exceptions.LatexNotFoundError:
log.warning(
"``pdflatex`` not available. Skipping infusions list "
f"for {character.name}"
)
else:
sheets.append(infusions_base + ".pdf")
# Create a list of Druid wild_shapes
wild_shapes = getattr(character, "wild_shapes", [])
if len(wild_shapes) > 0:
shapes_base = os.path.splitext(character_file)[0] + "_wild_shapes"
try:
create_druid_shapes_pdf(
character=character,
basename=shapes_base,
keep_temp_files=debug,
use_dnd_decorations=fancy_decorations,
)
except exceptions.LatexNotFoundError:
log.warning(
"``pdflatex`` not available. Skipping wild shapes list "
f"for {character.name}"
)
else:
sheets.append(shapes_base + ".pdf")
# Combine sheets into final pdf
final_pdf = os.path.splitext(character_file)[0] + ".pdf"
merge_pdfs(sheets, final_pdf, clean_up=True)
def merge_pdfs(src_filenames, dest_filename, clean_up=False):
"""Merge several PDF files into a single final file.
src_filenames
Iterable of source PDF file paths to use.
dest_filename
Path to requested PDF filename, will be overwritten if it
exists.
clean_up : optional
If truthy, the ``src_filenames`` will be deleted once the
``dest_filename`` has been created.
"""
popenargs = (PDFTK_CMD, *src_filenames, "cat", "output", dest_filename)
try:
subprocess.call(popenargs)
except FileNotFoundError:
warnings.warn(
f"Could not run `{PDFTK_CMD}`; skipping file concatenation.", RuntimeWarning
)
else:
# Remove temporary files
if clean_up:
for sheet in src_filenames:
os.remove(sheet)
load_character_file = readers.read_character_file
def _build(filename, args) -> int:
basename = filename.stem
print(f"Processing {basename}...")
try:
make_sheet(
character_file=filename,
flatten=(not args.editable),
debug=args.debug,
fancy_decorations=args.fancy_decorations,
)
except exceptions.CharacterFileFormatError:
# Only raise the failed exception if this file is explicitly given
print(f"invalid {basename}")
if args.filename:
raise
except Exception:
print(f"{basename} failed")
raise
else:
print(f"{basename} done")
return 1
def main():
# Prepare an argument parser
parser = argparse.ArgumentParser(
description="Prepare Dungeons and Dragons character sheets as PDFs"
)
parser.add_argument(
"filename",
type=str,
nargs="*",
help="File with character definition, or directory containing such files",
)
parser.add_argument(
"--editable",
"-e",
action="store_true",
help="Keep the PDF fields in place once processed",
)
parser.add_argument(
"--recursive",
"-r",
action="store_true",
help="Descend into subfolders looking for character files",
)
parser.add_argument(
"--fancy-decorations",
"--fancy",
"-F",
action="store_true",
help=(
"Render extra pages using fancy decorations "
"(experimental, requires https://github.com/rpgtex/DND-5e-LaTeX-Template)"
),
)
parser.add_argument(
"--debug",
"-d",
action="store_true",
help="Provide verbose logging for debugging purposes.",
)
args = parser.parse_args()
# Prepare logging if necessary
if args.debug:
logging.basicConfig(level=logging.DEBUG)
# Build the true list of filenames
input_filenames = args.filename
known_extensions = readers.readers_by_extension.keys()
if input_filenames == []:
input_filenames = [Path()]
else:
input_filenames = [Path(f) for f in input_filenames]
def get_char_files(fpath, parse_dirs=False):
valid_files = []
if fpath.is_dir() and parse_dirs:
for f in fpath.iterdir():
valid_files.extend(get_char_files(f, parse_dirs=args.recursive))
elif fpath.suffix in known_extensions:
valid_files.append(fpath)
return valid_files
temp_filenames = []
for fpath in input_filenames:
temp_filenames.extend(get_char_files(fpath, parse_dirs=True))
# IMPORANT:
# Check that the files are valid dungeonsheets files without importing them
filenames = []
version_re = re.compile(
r"^dungeonsheets_version = [\'\"](?P<version>[0-9.]+)[\'\"]\s*$", re.MULTILINE
)
for fpath in temp_filenames:
with open(fpath, mode="r") as fp:
if version_re.search(fp.read()) or fpath.suffix != ".py":
filenames.append(fpath)
# Process the requested files
if args.debug:
for filename in filenames:
_build(filename, args)
else:
with Pool(cpu_count()) as p:
p.starmap(_build, product(filenames, [args]))
if __name__ == "__main__":
main()