diff --git a/README.rst b/README.rst
index 7ceac6d..c6667a8 100644
--- a/README.rst
+++ b/README.rst
@@ -5,7 +5,11 @@
A tool to create character sheets for Dungeons and Dragons.
.. image:: https://travis-ci.com/canismarko/dungeon-sheets.svg?branch=master
- :target: https://travis-ci.com/canismarko/dungeon-sheets
+ :target: https://travis-ci.com/canismarko/dungeon-sheets
+
+.. image:: https://readthedocs.org/projects/dungeon-sheets/badge/?version=latest
+ :target: https://dungeon-sheets.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
Installation
============
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..298ea9e
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/character_files.html b/docs/character_files.html
new file mode 100644
index 0000000..dfc1202
--- /dev/null
+++ b/docs/character_files.html
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+Character Files
+
+
+
+
+
Character Files
+
+
Dungeonsheets expects one file per character, with a .dnd
+extension. An older python file format is still supported, but not
+recommended for security reasons. Details for this older style are
+provided below.
+
+
Old Python Character Files
+
+
Warning
+
The mechanism described below is insecure since it involves
+directly running python modules. Do not run files that you did not
+write yourself and do not trust completely. Maliciously-crafted
+files can run python code during import time.
+
+
Previous versions of this library used python files that are directly
+imported to produce the appropriate values. The benefit is that this
+allows much more versatile content, but at the expense of allowing
+these files to run un-sanitized python code on your machine. This
+behavior is still supported for now for backwards compatibility
+reasons, but do so at your own risk.
+
An example of this syntax can be found in :ref:`this wizard example<Wizard Example (Python)>`.
+
+
System Message: ERROR/3 (character_files.rst, line 29); backlink
+Unknown interpreted text role "ref".
+
+
+
+
diff --git a/docs/character_files.rst b/docs/character_files.rst
new file mode 100644
index 0000000..f7f46a8
--- /dev/null
+++ b/docs/character_files.rst
@@ -0,0 +1,215 @@
+=================
+ Character Files
+=================
+
+.. warning::
+
+ Character files are python modules that are imported when
+ parsed. **NEVER parse a character file without inspecting it** to
+ verify that there are no unexpected consequences, especially a file
+ from someone you do not trust.
+
+Dungeonsheets expects one file per character, with a ``.py``
+extension. This file is a python module, most likely with a series of
+variables set describing the character. They are roughly grouped into
+sections, which are documented below. Additionally, some
+:ref:`examples` may be useful.
+
+Each character file must contain a line like::
+
+ dungeonsheets_version = "0.4.2"
+
+Without this line, the `makesheets`_ command-line utility will ignore
+the file. This is necessary to avoid importing non-D&D python files.
+
+.. note::
+
+ Some proficiencies, character traits, abilities, etc.\ are the
+ result of the character's race and/or background. These **must
+ still be included** in the character file and will not be
+ automatically added if omitted.
+
+Basic Info
+==========
+
+The character file will contain several basic information values that
+are fairly self-evident. The values for ``character_class``,
+``background``, ``race`` and ``alignment`` must match entries in the
+standard 5e rules, and are case-insensitive. Refer to the D&D
+`player's handbook`_ for more information.
+
+.. code:: python
+
+ name = 'Inara Serradon'
+ character_class = 'wizard'
+ player_name = 'Mark'
+ background = "Acolyte"
+ race = "High-Elf"
+ level = 3
+ alignment = "Chaotic good"
+ xp = 2190
+ hp_max = 16
+
+Ability Scores
+==============
+
+Ability scores are numeric scores for each ability, as described in
+the `player's handbook`_.
+
+.. code:: python
+
+ # Ability Scores
+ strength = 10
+ dexterity = 15
+ constitution = 14
+ intelligence = 16
+ wisdom = 12
+ charisma = 8
+
+Proficiencies and Languages
+===========================
+
+This section may contain entries, one for ``skill_proficiencies`` and
+one for ``languages``. ``skill_proficiencies`` must be an iterable of
+case-insensitive strings matching skills described in the `player's
+handbook`_. Languages is a standard string, since language proficiency
+does not affect other areas of the character.
+
+.. code:: python
+
+ # Proficiencies and languages
+ skill_proficiencies = [
+ 'arcana',
+ 'insight',
+ 'investigation',
+ 'perception',
+ 'religion',
+ ]
+ languages = "Common, Elvish, Draconic, Dwarvish, Goblin."
+
+
+Inventory
+=========
+
+There are five entries for currencies, which must be
+integers. ``weapons`` (iterable of strings), ``armor`` (string) and
+``shield`` (string) must correspond to items available in the
+`player's handbook`_. The ``equipment`` is a string that is rendered
+as-is on the character sheet.
+
+.. todo:: Allow custom weapons and armor to be specified in the
+ character file.
+
+.. warning::
+
+ Not all weapons and armor have been entered into the
+ ``dungeonsheets`` library. If you receive an ``AttributeError``
+ stating the item you entered is not defined despite being listed in
+ the `player's handbook`_, please submit an `issue`_.
+
+.. code:: python
+
+ cp = 950
+ sp = 75
+ ep = 50
+ gp = 120
+ pp = 0
+ weapons = ('shortsword', 'shortbow')
+ armor = 'light leather armor'
+ shield = 'shield'
+ equipment = (
+ """Shortsword, shortbow, 20 arrows, leather armor, thieves’ tools,
+ backpack, bell, 5 candles, crowbar, hammer, 10 pitons, 50 feet of
+ hempen rope, hooded lantern, 2 flasks of oil, 5 days rations,
+ tinderbox, waterskin, crowbar, set of dark common clothes
+ including a hood, pouch.""")
+
+Spells
+======
+
+Two entries are available for spell-casting, and only if the class
+supports spells. Both are lists of case-insensitive strings that must
+correspond to spells described in the `player's handbook`_.
+
+.. todo:: Allow custom spells to be specified in the character file.
+
+.. warning::
+
+ Not all spells have been entered into the ``dungeonsheets``
+ library. If you receive a ``UserWarning`` stating the spell you
+ entered is not defined despite being listed in the `player's
+ handbook`_, please submit an `issue`_.
+
+.. code:: python
+
+ # List of known spells
+ spells = ('blindness deafness', 'burning hands', 'detect magic',
+ 'false life', 'mage armor', 'mage hand', 'magic missile',
+ 'prestidigitation', 'ray of frost', 'ray of sickness', 'shield',
+ 'shocking grasp', 'sleep',)
+ # Which spells have been prepared (not including cantrips)
+ spells_prepared = ('blindness deafness', 'false life', 'mage armor',
+ 'ray of sickness', 'shield', 'sleep',)
+
+
+Personality and Backstory
+=========================
+
+This section contains string that describe the nature and backstory of
+the character. They will be printed as-is on the character
+sheet. Triple-quoted string and parenthesis may make the character's
+source file more readable, but are not required.
+
+.. code:: python
+
+ # Backstory
+ personality_traits = """I use polysyllabic words that convey the impression of
+ erudition. Also, I’ve spent so long in the temple that I have little
+ experience dealing with people on a casual basis."""
+
+ ideals = """Knowledge. The path to power and self-improvement is through
+ knowledge."""
+
+ bonds = """The tome I carry with me is the record of my life’s work so far,
+ and no vault is secure enough to keep it safe."""
+
+ flaws = """I’ll do just about anything to uncover historical secrets that
+ would add to my research."""
+
+ features_and_traits = (
+ """Spellcasting Ability: Intelligence is your spellcasting ability for
+ your spells. The saving throw DC to resist a spell you cast is
+ 13. Your attack bonus when you make an attack with a spell is
+ +5. See the rulebook for rules on casting your spells.
+
+ Arcane Recovery: You can regain some of your magical energy by
+ studying your spellbook. Once per day during a short rest, you can
+ choose to recover expended spell slots with a combined level equal
+ to or less than half your wizard level (rounded up).
+
+ Darkvision: You see in dim light within a 60-foot radius of you as
+ if it were bright light, and in darkness in that radius as if it
+ were dim light. You can’t discern color in darkness, only shades
+ of gray.
+
+ Fey Ancestry: You have advantage on saving throws against being
+ charmed, and magic can’t put you to sleep.
+
+ Trance: Elves don’t need to sleep. They meditate deeply, remaining
+ semiconscious, for 4 hours a day and gain the same benefit a human
+ does from 8 hours of sleep.
+
+ Shelter of the Faithful: As a servant of Oghma, you command the
+ respect of those who share your faith, and you can perform the
+ rites of Oghma. You and your companions can expect to receive free
+ healing and care at a temple, shrine, or other established
+ presence of Oghma’s faith. Those who share your religion will
+ support you (and only you) at a modest lifestyle. You also have
+ ties to the temple of Oghma in Neverwinter, where you have a
+ residence. When you are in Neverwinter, you can call upon the
+ priests there for assistance that won’t endanger them.""")
+
+
+.. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
+
+.. _issue: https://github.com/canismarko/dungeon-sheets/issues
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..fa34e21
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'dungeonsheets'
+copyright = '2018, Mark Wolfman'
+author = 'Mark Wolfman'
+
+# The short X.Y version
+version = ''
+# The full version, including alpha/beta/rc tags
+release = '0.4.2'
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.viewcode',
+ 'sphinx.ext.autosectionlabel',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself. Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'dungeonsheetsdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'dungeonsheets.tex', 'dungeonsheets Documentation',
+ 'Mark Wolfman', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'dungeonsheets', 'dungeonsheets Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'dungeonsheets', 'dungeonsheets Documentation',
+ author, 'dungeonsheets', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+
+# -- Extension configuration -------------------------------------------------
+
+# -- Options for todo extension ----------------------------------------------
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
diff --git a/docs/examples.rst b/docs/examples.rst
new file mode 100644
index 0000000..edfba4d
--- /dev/null
+++ b/docs/examples.rst
@@ -0,0 +1,20 @@
+==========
+ Examples
+==========
+
+.. contents:: :local:
+
+Wizard
+======
+
+.. literalinclude:: ../examples/wizard.py
+
+Warlock
+=======
+
+.. literalinclude:: ../examples/warlock.py
+
+Rogue
+=====
+
+.. literalinclude:: ../examples/rogue.py
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..64e4178
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,26 @@
+.. dungeonsheets documentation master file, created by
+ sphinx-quickstart on Sun Oct 14 18:07:48 2018.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to Dungeonsheets's documentation!
+=========================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ character_files
+ examples
+
+To-Do Tasks
+===========
+.. todolist::
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..27f573b
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py
index 79a92c6..3963614 100644
--- a/dungeonsheets/character.py
+++ b/dungeonsheets/character.py
@@ -106,7 +106,9 @@ class Character():
"""Bulk setting of attributes. Useful for loading a character from a
dictionary."""
for attr, val in attrs.items():
- if attr == 'weapons':
+ if attr == 'dungeonsheets_version':
+ pass # Maybe we'll verify this later?
+ elif attr == 'weapons':
# Treat weapons specially
for weap in val:
self.wield_weapon(weap)
@@ -249,7 +251,7 @@ class Character():
try:
NewWeapon = findattr(weapons, weapon)
except AttributeError:
- raise AttributeError(f'Weapon {weapon} is not defined')
+ raise AttributeError(f'Weapon "{weapon}" is not defined')
weapon_ = NewWeapon()
# Set weapon attributes based on character
if weapon_.is_finesse:
diff --git a/dungeonsheets/exceptions.py b/dungeonsheets/exceptions.py
index db6d580..6a72e38 100644
--- a/dungeonsheets/exceptions.py
+++ b/dungeonsheets/exceptions.py
@@ -1,3 +1,6 @@
+class CharacterFileFormatError(ValueError):
+ """The given file is not a valid Dungeons and Dragons file."""
+
class DiceError(ValueError):
"""Improper formatting for a dice string."""
diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py
index e93d98c..298a60e 100644
--- a/dungeonsheets/make_sheets.py
+++ b/dungeonsheets/make_sheets.py
@@ -7,6 +7,7 @@ import importlib.util
import os
import subprocess
import warnings
+import re
from fdfgen import forge_fdf
@@ -43,6 +44,19 @@ def load_character_file(filename):
module_name, ext = os.path.splitext(fname)
if ext != '.py':
raise ValueError(f"Character definition {filename} is not a python file.")
+ # Check if this file contains the version string
+ version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-4.]+)[\'"]')
+ with open(filename, mode='r') as f:
+ version = None
+ for line in f:
+ match = version_re.match(line)
+ if match:
+ version = match.group(1)
+ break
+ if version is None:
+ # Not a valid DND character file
+ raise exceptions.CharacterFileFormatError(
+ "No ``dungeonsheets_version = `` entry.")
# Import the module to extract the information
spec = importlib.util.spec_from_file_location('module', filename)
module = importlib.util.module_from_spec(spec)
@@ -265,7 +279,10 @@ def create_character_pdf(character, basename, flatten=False):
}
# Add skill proficienies
for skill in character.skill_proficiencies:
- fields.append((skill_boxes[skill.replace(' ', '_')], 'Yes'))
+ try:
+ fields.append((skill_boxes[skill.replace(' ', '_').lower()], 'Yes'))
+ 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 '),
@@ -368,6 +385,11 @@ def main():
print(f"Processing {os.path.splitext(filename)[0]}...", end='')
try:
make_sheet(character_file=filename, flatten=(not args.editable))
+ except exceptions.CharacterFileFormatError as e:
+ # Only raise the failed exception if this file is explicitly given
+ print('invalid')
+ if args.filename:
+ raise
except Exception as e:
print('failed')
raise
diff --git a/examples/rogue.pdf b/examples/rogue.pdf
index 3477fee..35825c8 100644
Binary files a/examples/rogue.pdf and b/examples/rogue.pdf differ
diff --git a/examples/rogue.py b/examples/rogue.py
index 14d8656..a7b4784 100644
--- a/examples/rogue.py
+++ b/examples/rogue.py
@@ -1,3 +1,6 @@
+dungeonsheets_version = "0.4.2"
+
+# Basic information
name = 'Mr. Stabby'
character_class = 'rogue'
player_name = 'Mike'
diff --git a/examples/warlock.pdf b/examples/warlock.pdf
index 319d40b..d96e986 100644
Binary files a/examples/warlock.pdf and b/examples/warlock.pdf differ
diff --git a/examples/warlock.py b/examples/warlock.py
index 0f632ef..1726ee4 100644
--- a/examples/warlock.py
+++ b/examples/warlock.py
@@ -1,3 +1,6 @@
+dungeonsheets_version = "0.4.2"
+
+# Basic information
name = 'Sid Istick'
character_class = 'Warlock'
player_name = 'Mark'
diff --git a/examples/wizard.pdf b/examples/wizard.pdf
index 9c49884..6b6c5de 100644
Binary files a/examples/wizard.pdf and b/examples/wizard.pdf differ
diff --git a/examples/wizard.py b/examples/wizard.py
index 38a3f58..a0cc6d2 100644
--- a/examples/wizard.py
+++ b/examples/wizard.py
@@ -1,3 +1,6 @@
+dungeonsheets_version = "0.4.2"
+
+# Basic information
name = 'Inara Serradon'
character_class = 'wizard'
player_name = 'Mark'
@@ -15,6 +18,8 @@ constitution = 14
intelligence = 16
wisdom = 12
charisma = 8
+
+# Proficiencies and languages
skill_proficiencies = [
'arcana',
'insight',
@@ -22,8 +27,6 @@ skill_proficiencies = [
'perception',
'religion',
]
-
-# Proficiencies and languages
languages = "Common, Elvish, Draconic, Dwarvish, Goblin."
# Inventory
@@ -42,7 +45,7 @@ equipment = (
# List of known spells
spells = ('blindness deafness', 'burning hands', 'detect magic',
- 'false life', 'mage armor', 'mage hand', 'magic missile',
+ 'falsee life', 'mage armor', 'mage hand', 'magic missile',
'prestidigitation', 'ray of frost', 'ray of sickness', 'shield',
'shocking grasp', 'sleep',)
# Which spells have been prepared (not including cantrips)
@@ -68,7 +71,7 @@ features_and_traits = (
your spells. The saving throw DC to resist a spell you cast is
13. Your attack bonus when you make an attack with a spell is
+5. See the rulebook for rules on casting your spells.
-
+
Arcane Recovery: You can regain some of your magical energy by
studying your spellbook. Once per day during a short rest, you can
choose to recover expended spell slots with a combined level equal
diff --git a/requirements.txt b/requirements.txt
index 09dbe45..67bd0b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ fdfgen>=0.16
npyscreen
jinja2
pytest
+sphinx