Added a fallback PDF routine that uses pdfrw package instead of pdftk utility.

This commit is contained in:
Mark Wolfman
2018-10-17 02:53:11 -05:00
parent 3c3d4be218
commit 3368a435cb
10 changed files with 258 additions and 113 deletions
+17 -13
View File
@@ -28,24 +28,28 @@ Installation
.. _f-strings: https://www.python.org/dev/peps/pep-0498/ .. _f-strings: https://www.python.org/dev/peps/pep-0498/
External dependencies Optional External dependencies
===================== ==============================
* You will need **pdftk** installed to generate the sheets in PDF format. * You may use **pdftk** to generate the sheets in PDF format.
* You will need **pdflatex** installed to generate the PDF spell pages (optional). * You will need **pdflatex** installed to generate the PDF spell pages (optional).
.. note:: If **pdftk** is available, it will be used for pdf generation. If not,
a fallback python library (pdfrw) will be used. This has some
limitations:
Different linux distributions have different names for packages. While - Produces v1.3 PDF files
pdftk is available in Debian and derivatives as **pdftk**, the package - Not able to flatten PDF forms
is not available in some RPM distributions, such as Fedora and CentOS. - Will produce separate character-sheets, spell-lists and spell-books.
One alternative would be to build your PC sheets using docker.
Different linux distributions have different names for packages. While
pdftk is available in Debian and derivatives as **pdftk**, the package
is not available in some RPM distributions, such as Fedora and CentOS.
One alternative would be to build your PC sheets using docker.
.. note:: If the ``pdflatex`` command is available on your system, spellcasters
will include a spellbook with descriptions of each spell known. If
If the ``pdflatex`` command is available on your system, not, then this feature will be skipped.
spellcasters will include a spellbook with descriptions of each
spell known. If not, then this feature will be skipped.
Usage Usage
===== =====
+1 -1
View File
@@ -100,7 +100,7 @@ class Character():
@property @property
def speed(self): def speed(self):
return self.race.speed return getattr(self.race, 'speed', 30)
def set_attrs(self, **attrs): def set_attrs(self, **attrs):
"""Bulk setting of attributes. Useful for loading a character from a """Bulk setting of attributes. Useful for loading a character from a
+210 -98
View File
@@ -10,6 +10,7 @@ import warnings
import re import re
from fdfgen import forge_fdf from fdfgen import forge_fdf
import pdfrw
from dungeonsheets import character, exceptions from dungeonsheets import character, exceptions
from dungeonsheets.stats import mod_str from dungeonsheets.stats import mod_str
@@ -18,6 +19,11 @@ from dungeonsheets.stats import mod_str
"""Program to take character definitions and build a PDF of the """Program to take character definitions and build a PDF of the
character sheet.""" character sheet."""
CHECKBOX_ON = 'Yes'
CHECKBOX_OFF = 'Off'
PDFTK_CMD = 'pdftk'
def text_box(string): def text_box(string):
"""Format a string for displaying in a text box.""" """Format a string for displaying in a text box."""
# Remove line breaks # Remove line breaks
@@ -112,27 +118,27 @@ def create_spellbook_pdf(character, basename):
def create_spells_pdf(character, basename, flatten=False): def create_spells_pdf(character, basename, flatten=False):
class_level = (character.class_name + ' ' + str(character.level)) class_level = (character.class_name + ' ' + str(character.level))
spell_level = lambda x : (x or '') spell_level = lambda x : (x or '')
fields = [ fields = {
('Spellcasting Class 2', class_level), 'Spellcasting Class 2': class_level,
("SpellcastingAbility 2", character.spellcasting_ability.capitalize()), 'SpellcastingAbility 2': character.spellcasting_ability.capitalize(),
('SpellSaveDC 2', character.spell_save_dc), 'SpellSaveDC 2': character.spell_save_dc,
('SpellAtkBonus 2', mod_str(character.spell_attack_bonus)), 'SpellAtkBonus 2': mod_str(character.spell_attack_bonus),
# Number of spell slots # Number of spell slots
('SlotsTotal 19', spell_level(character.spell_slots(1))), 'SlotsTotal 19': spell_level(character.spell_slots(1)),
('SlotsTotal 20', spell_level(character.spell_slots(2))), 'SlotsTotal 20': spell_level(character.spell_slots(2)),
('SlotsTotal 21', spell_level(character.spell_slots(3))), 'SlotsTotal 21': spell_level(character.spell_slots(3)),
('SlotsTotal 22', spell_level(character.spell_slots(4))), 'SlotsTotal 22': spell_level(character.spell_slots(4)),
('SlotsTotal 23', spell_level(character.spell_slots(5))), 'SlotsTotal 23': spell_level(character.spell_slots(5)),
('SlotsTotal 24', spell_level(character.spell_slots(6))), 'SlotsTotal 24': spell_level(character.spell_slots(6)),
('SlotsTotal 25', spell_level(character.spell_slots(7))), 'SlotsTotal 25': spell_level(character.spell_slots(7)),
('SlotsTotal 26', spell_level(character.spell_slots(8))), 'SlotsTotal 26': spell_level(character.spell_slots(8)),
('SlotsTotal 27', spell_level(character.spell_slots(9))), 'SlotsTotal 27': spell_level(character.spell_slots(9)),
] }
# Cantrips # Cantrips
cantrip_fields = (f'Spells 10{i}' for i in (14, 16, 17, 18, 19, 20, 21, 22)) 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) cantrips = (spl for spl in character.spells if spl.level == 0)
for spell, field_name in zip(cantrips, cantrip_fields): for spell, field_name in zip(cantrips, cantrip_fields):
fields.append((field_name, str(spell))) fields[field_name] = str(spell)
# Spells for each level # Spells for each level
field_numbers = { field_numbers = {
1: (1015, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, ), 1: (1015, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, ),
@@ -161,9 +167,9 @@ def create_spells_pdf(character, basename, flatten=False):
field_names = tuple(f'Spells {i}' for i in field_numbers[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]) 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): for spell, field, chk_field in zip(spells, field_names, prep_names):
fields.append((field, str(spell))) fields[field] = str(spell)
is_prepared = any([isinstance(spell, Spl) for Spl in character.spells_prepared]) is_prepared = any([isinstance(spell, Spl) for Spl in character.spells_prepared])
fields.append((chk_field, is_prepared)) fields[chk_field] = CHECKBOX_ON if is_prepared else CHECKBOX_OFF
# # Uncomment to post field names instead: # # Uncomment to post field names instead:
# for field in field_names: # for field in field_names:
# fields.append((field, field)) # fields.append((field, field))
@@ -175,77 +181,77 @@ def create_spells_pdf(character, basename, flatten=False):
def create_character_pdf(character, basename, flatten=False): def create_character_pdf(character, basename, flatten=False):
# Prepare the list of fields # Prepare the list of fields
class_level = (character.class_name + ' ' + str(character.level)) class_level = f"{character.class_name} {character.level}"
fields = [ fields = {
# Character description # Character description
('CharacterName', character.name), 'CharacterName': character.name,
('ClassLevel', class_level), 'ClassLevel': class_level,
('Background', character.background), 'Background': character.background,
('PlayerName', character.player_name), 'PlayerName': character.player_name,
('Race ', str(character.race)), 'Race ': str(character.race),
('Alignment', character.alignment), 'Alignment': character.alignment,
('XP', character.xp), 'XP': str(character.xp),
# Abilities # Abilities
('ProfBonus', mod_str(character.proficiency_bonus)), 'ProfBonus': mod_str(character.proficiency_bonus),
('STRmod', str(character.strength.value)), 'STRmod': str(character.strength.value),
('STR', mod_str(character.strength.modifier)), 'STR': mod_str(character.strength.modifier),
('DEXmod ', character.dexterity.value), 'DEXmod ': str(character.dexterity.value),
('DEX', mod_str(character.dexterity.modifier)), 'DEX': mod_str(character.dexterity.modifier),
('CONmod', character.constitution.value), 'CONmod': str(character.constitution.value),
('CON', mod_str(character.constitution.modifier)), 'CON': mod_str(character.constitution.modifier),
('INTmod', character.intelligence.value), 'INTmod': str(character.intelligence.value),
('INT', mod_str(character.intelligence.modifier)), 'INT': mod_str(character.intelligence.modifier),
('WISmod', character.wisdom.value), 'WISmod': str(character.wisdom.value),
('WIS', mod_str(character.wisdom.modifier)), 'WIS': mod_str(character.wisdom.modifier),
('CHamod', character.charisma.value), 'CHamod': str(character.charisma.value),
('CHA', mod_str(character.charisma.modifier)), 'CHA': mod_str(character.charisma.modifier),
('AC', character.armor_class), 'AC': str(character.armor_class),
('Initiative', mod_str(character.dexterity.modifier)), 'Initiative': mod_str(character.dexterity.modifier),
('Speed', character.speed), 'Speed': str(character.speed),
('Passive', 10 + character.perception), 'Passive': 10 + character.perception,
# Saving throws (proficiencies handled later) # Saving throws (proficiencies handled later)
('ST Strength', mod_str(character.strength.saving_throw)), 'ST Strength': mod_str(character.strength.saving_throw),
('ST Dexterity', mod_str(character.dexterity.saving_throw)), 'ST Dexterity': mod_str(character.dexterity.saving_throw),
('ST Constitution', mod_str(character.constitution.saving_throw)), 'ST Constitution': mod_str(character.constitution.saving_throw),
('ST Intelligence', mod_str(character.intelligence.saving_throw)), 'ST Intelligence': mod_str(character.intelligence.saving_throw),
('ST Wisdom', mod_str(character.wisdom.saving_throw)), 'ST Wisdom': mod_str(character.wisdom.saving_throw),
('ST Charisma', mod_str(character.charisma.saving_throw)), 'ST Charisma': mod_str(character.charisma.saving_throw),
# Skills (proficiencies handled below) # Skills (proficiencies handled below)
('Acrobatics', mod_str(character.acrobatics)), 'Acrobatics': mod_str(character.acrobatics),
('Animal', mod_str(character.animal_handling)), 'Animal': mod_str(character.animal_handling),
('Arcana', mod_str(character.arcana)), 'Arcana': mod_str(character.arcana),
('Athletics', mod_str(character.athletics)), 'Athletics': mod_str(character.athletics),
('Deception ', mod_str(character.deception)), 'Deception ': mod_str(character.deception),
('History ', mod_str(character.history)), 'History ': mod_str(character.history),
('Insight', mod_str(character.insight)), 'Insight': mod_str(character.insight),
('Intimidation', mod_str(character.intimidation)), 'Intimidation': mod_str(character.intimidation),
('Investigation ', mod_str(character.investigation)), 'Investigation ': mod_str(character.investigation),
('Medicine', mod_str(character.medicine)), 'Medicine': mod_str(character.medicine),
('Nature', mod_str(character.nature)), 'Nature': mod_str(character.nature),
('Perception ', mod_str(character.perception)), 'Perception ': mod_str(character.perception),
('Performance', mod_str(character.performance)), 'Performance': mod_str(character.performance),
('Persuasion', mod_str(character.persuasian)), 'Persuasion': mod_str(character.persuasian),
('Religion', mod_str(character.religion)), 'Religion': mod_str(character.religion),
('SleightofHand', mod_str(character.sleight_of_hand)), 'SleightofHand': mod_str(character.sleight_of_hand),
('Stealth ', mod_str(character.stealth)), 'Stealth ': mod_str(character.stealth),
('Survival', mod_str(character.survival)), 'Survival': mod_str(character.survival),
# Hit points # Hit points
('HDTotal', character.hit_dice), 'HDTotal': character.hit_dice,
('HPMax', character.hp_max), 'HPMax': str(character.hp_max),
# Personality traits and other features # Personality traits and other features
('PersonalityTraits ', text_box(character.personality_traits)), 'PersonalityTraits ': text_box(character.personality_traits),
('Ideals', text_box(character.ideals)), 'Ideals': text_box(character.ideals),
('Bonds', text_box(character.bonds)), 'Bonds': text_box(character.bonds),
('Flaws', text_box(character.flaws)), 'Flaws': text_box(character.flaws),
('Features and Traits', text_box(character.features_and_traits)), 'Features and Traits': text_box(character.features_and_traits),
# Inventory # Inventory
('CP', character.cp), 'CP': character.cp,
('SP', character.sp), 'SP': character.sp,
('EP', character.ep), 'EP': character.ep,
('GP', character.gp), 'GP': character.gp,
('PP', character.pp), 'PP': character.pp,
('Equipment', text_box(character.equipment)), 'Equipment': text_box(character.equipment),
] }
# Check boxes for proficiencies # Check boxes for proficiencies
ST_boxes = { ST_boxes = {
'strength': 'Check Box 11', 'strength': 'Check Box 11',
@@ -256,7 +262,8 @@ def create_character_pdf(character, basename, flatten=False):
'charisma': 'Check Box 22', 'charisma': 'Check Box 22',
} }
for ability in character.saving_throw_proficiencies: for ability in character.saving_throw_proficiencies:
fields.append((ST_boxes[ability], 'Yes')) fields[ST_boxes[ability]] = CHECKBOX_ON
# Add skill proficiencies
skill_boxes = { skill_boxes = {
'acrobatics': 'Check Box 23', 'acrobatics': 'Check Box 23',
'animal_handling': 'Check Box 24', 'animal_handling': 'Check Box 24',
@@ -277,10 +284,9 @@ def create_character_pdf(character, basename, flatten=False):
'stealth': 'Check Box 39', 'stealth': 'Check Box 39',
'survival': 'Check Box 40', 'survival': 'Check Box 40',
} }
# Add skill proficienies
for skill in character.skill_proficiencies: for skill in character.skill_proficiencies:
try: try:
fields.append((skill_boxes[skill.replace(' ', '_').lower()], 'Yes')) fields[skill_boxes[skill.replace(' ', '_').lower()]] = CHECKBOX_ON
except KeyError: except KeyError:
raise KeyError(f"Unknown skill: '{skill}'") raise KeyError(f"Unknown skill: '{skill}'")
# Add weapons # Add weapons
@@ -289,27 +295,104 @@ def create_character_pdf(character, basename, flatten=False):
('Wpn Name 3', 'Wpn3 AtkBonus ', 'Wpn3 Damage '),] ('Wpn Name 3', 'Wpn3 AtkBonus ', 'Wpn3 Damage '),]
for _fields, weapon in zip(weapon_fields, character.weapons): for _fields, weapon in zip(weapon_fields, character.weapons):
name_field, atk_field, dmg_field = _fields name_field, atk_field, dmg_field = _fields
fields.append((name_field, weapon.name)) fields[name_field] = weapon.name
fields.append((atk_field, mod_str(weapon.attack_bonus))) fields[atk_field] = str(weapon.attack_bonus)
fields.append((dmg_field, f'{weapon.damage} {weapon.damage_type}')) fields[dmg_field] = f'{weapon.damage} {weapon.damage_type}'
# Other attack information # Other attack information
attack_str = f'Armor: {character.armor}' attack_str = f'Armor: {character.armor}'
attack_str += f'Shield: {character.shield}\n\n' attack_str += f'Shield: {character.shield}\n\n'
attack_str += character.attacks_and_spellcasting attack_str += character.attacks_and_spellcasting
fields.append(('AttacksSpellcasting', text_box(attack_str))) fields['AttacksSpellcasting'] = text_box(attack_str)
# Other proficiencies and languages # Other proficiencies and languages
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text) prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages) prof_text += "\n\nLanguages:\n" + text_box(character.languages)
fields.append(('ProficienciesLang', prof_text)) fields['ProficienciesLang'] = prof_text
# Prepare the actual PDF # Prepare the actual PDF
dirname = os.path.dirname(os.path.abspath(__file__)) dirname = os.path.dirname(os.path.abspath(__file__))
src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf') src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf')
return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def make_pdf(fields, src_pdf, basename, flatten=False): 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:
result = _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 # Create the actual FDF file
fdfname = basename + '.fdf' fdfname = basename + '.fdf'
fdf = forge_fdf("", fields, [], [], []) fdf = forge_fdf("", fields, [], [], [])
fdf_file = open(fdfname, "wb") fdf_file = open(fdfname, "wb")
fdf_file.write(fdf) fdf_file.write(fdf)
@@ -317,7 +400,7 @@ def make_pdf(fields, src_pdf, basename, flatten=False):
# Build the final flattened PDF documents # Build the final flattened PDF documents
dest_pdf = basename + '.pdf' dest_pdf = basename + '.pdf'
popenargs = [ popenargs = [
'pdftk', src_pdf, 'fill_form', fdfname, 'output', dest_pdf, PDFTK_CMD, src_pdf, 'fill_form', fdfname, 'output', dest_pdf,
] ]
if flatten: if flatten:
popenargs.append('flatten') popenargs.append('flatten')
@@ -327,6 +410,7 @@ def make_pdf(fields, src_pdf, basename, flatten=False):
def make_sheet(character_file, flatten=False): def make_sheet(character_file, flatten=False):
"""Prepare a PDF character sheet from the given character file. """Prepare a PDF character sheet from the given character file.
Parameters Parameters
@@ -343,7 +427,9 @@ def make_sheet(character_file, flatten=False):
# Set the fields in the FDF # Set the fields in the FDF
char_base = os.path.splitext(character_file)[0] + '_char' char_base = os.path.splitext(character_file)[0] + '_char'
sheets = [char_base + '.pdf'] sheets = [char_base + '.pdf']
create_character_pdf(character=char, basename=char_base, flatten=flatten) pages = []
char_pdf = create_character_pdf(character=char, basename=char_base, flatten=flatten)
pages.append(char_pdf)
if char.is_spellcaster: if char.is_spellcaster:
# Create spell sheet # Create spell sheet
spell_base = os.path.splitext(character_file)[0] + '_spells' spell_base = os.path.splitext(character_file)[0] + '_spells'
@@ -360,11 +446,32 @@ def make_sheet(character_file, flatten=False):
sheets.append(spellbook_base + '.pdf') sheets.append(spellbook_base + '.pdf')
# Combine sheets into final pdf # Combine sheets into final pdf
final_pdf = os.path.splitext(character_file)[0] + '.pdf' final_pdf = os.path.splitext(character_file)[0] + '.pdf'
popenargs = ('pdftk', *sheets, 'cat', 'output', final_pdf) merge_pdfs(sheets, final_pdf, clean_up=True)
subprocess.call(popenargs)
# Remove temporary files def merge_pdfs(src_filenames, dest_filename, clean_up=False):
for sheet in sheets: """Merge several PDF files into a single final file.
os.remove(sheet)
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)
def main(): def main():
@@ -375,7 +482,12 @@ def main():
help="Python file with character definition") help="Python file with character definition")
parser.add_argument('--editable', '-e', action="store_true", parser.add_argument('--editable', '-e', action="store_true",
help="Keep the PDF fields in place once processed.") help="Keep the PDF fields in place once processed.")
parser.add_argument('--debug', '-d', action="store_true",
help="Provide verbose logging for debugging purposes.")
args = parser.parse_args() args = parser.parse_args()
# Prepare logging if necessary
if args.debug:
logging.basicConfig(level=logging.DEBUG)
# Process the requested files # Process the requested files
if args.filename is None: if args.filename is None:
filenames = [f for f in os.listdir('.') if os.path.splitext(f)[1] == '.py'] filenames = [f for f in os.listdir('.') if os.path.splitext(f)[1] == '.py']
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -45,7 +45,7 @@ equipment = (
# List of known spells # List of known spells
spells = ('blindness deafness', 'burning hands', 'detect magic', spells = ('blindness deafness', 'burning hands', 'detect magic',
'falsee life', 'mage armor', 'mage hand', 'magic missile', 'false life', 'mage armor', 'mage hand', 'magic missile',
'prestidigitation', 'ray of frost', 'ray of sickness', 'shield', 'prestidigitation', 'ray of frost', 'ray of sickness', 'shield',
'shocking grasp', 'sleep',) 'shocking grasp', 'sleep',)
# Which spells have been prepared (not including cantrips) # Which spells have been prepared (not including cantrips)
+1
View File
@@ -4,3 +4,4 @@ npyscreen
jinja2 jinja2
pytest pytest
sphinx sphinx
pdfrw
+9
View File
@@ -153,3 +153,12 @@ class TestCharacter(TestCase):
# Try passing an Armor object directly # Try passing an Armor object directly
char.wield_shield(Shield) char.wield_shield(Shield)
self.assertEqual(char.armor_class, 15) self.assertEqual(char.armor_class, 15)
def test_speed(self):
# Check that the speed pulls from the character's race
char = Character(race='halfling')
self.assertEqual(char.speed, 25)
# Check that a character with no race defaults to 30 feet
char = Character()
char.race = None
self.assertEqual(char.speed, 30)
+19
View File
@@ -11,3 +11,22 @@ class CharacterFileTestCase(unittest.TestCase):
charfile = CHARFILE charfile = CHARFILE
result = make_sheets.load_character_file(charfile) result = make_sheets.load_character_file(charfile)
self.assertEqual(result['strength'], 8) self.assertEqual(result['strength'], 8)
class PdfOutputTeestCase(unittest.TestCase):
basename = 'clara'
def tearDown(self):
temp_files = [f'{self.basename}.pdf']
for f in temp_files:
if os.path.exists(f):
os.remove(f)
def test_file_created(self):
# Check that a file is created once the function is run
pdf_name = f'{self.basename}.pdf'
# self.assertFalse(os.path.exists(pdf_name), f'{pdf_name} already exists.')
char = character.Character(name='Clara')
char.saving_throw_proficiencies = ['strength']
make_sheets.create_character_pdf(character=char, basename=self.basename)
self.assertTrue(os.path.exists(pdf_name), f'{pdf_name} not created.')