commit d3dbe07158d943b39df87a0c1b64c4c9f1418781
parent 8930bdee2e7de948cd34438370fb67527ae92afa
Author: amin <dev@aminmesbah.com>
Date: Tue, 16 May 2017 10:47:34 +0000
Package and upload to pypi. Update readme.
FossilOrigin-Name: 5099710aa471842544ca25a3fbc7aec1757c3afc37e43702ac759676103ee137
Diffstat:
14 files changed, 555 insertions(+), 419 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,9 @@
-.cache/*
-__pycache__/*
*.json
*.swp
+
+__pycache__/
+.cache/
+perf_tests/
+build/
+dist/
+Stoichiograph.egg-info/
diff --git a/MANIFEST.in b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst
+include LICENSE
diff --git a/Makefile b/Makefile
@@ -1,21 +1,32 @@
DATADIR = data/
DATE = `date +%Y-%m-%d`
+clean:
+ rm -rf build/
+ rm -rf dist/
+ rm -rf Stoichiograph.egg-info/
+
init:
pip install -r dev_requirements.txt
-test:
- # To run individual tests, use "py.test -k the_test_path"
- py.test tests.py
-
lint:
- flake8 *.py --max-line-length 90
-
-watch-log:
- tail -f debug.log
+ flake8 --max-line-length 90 stoichiograph/*.py tests/*.py setup.py
loc:
cloc --by-file --include-lang=Python .
+package: clean lint
+ python setup.py sdist bdist_wheel
+
+test:
+ # To run individual tests, use "py.test -k the_test_path"
+ py.test tests
+
todo:
- grep -FR --ignore-case --binary-file=without-match todo *.py
+ grep -FR --ignore-case --binary-file=without-match todo *.py stoichiograph/ tests/
+
+upload: package
+ twine upload dist/*
+
+watch-log:
+ tail -f debug.log
diff --git a/README.rst b/README.rst
@@ -1,13 +1,25 @@
Stoichiograph - The Elemental Speller
=====================================
-Spell words with elemental symbols from the periodic table ("He", "Cu", etc).
+Spell words with elemental symbols from the periodic table ("He", "Cu", etc). I
+made this when I was bored in Chemistry class. I wrote about the process of
+making it `here`_.
.. figure:: https://cloud.githubusercontent.com/assets/5744114/21043177/7c3efe8c-bdaa-11e6-9c1a-22db4de6bb2f.png
:alt: A list of four words and their elemental spellings
Some words and their elemental spellings
+.. _here: https://www.amin.space/blog/2017/5/elemental_speller/
+
+
+Installation
+------------
+
+.. code-block::
+
+ $ pip install stoichiograph
+
Usage
-----
@@ -37,3 +49,24 @@ Usage
-t, --tuples display spellings as tuples
-v, --verbose print a detailed log
-V, --version print version info and exit
+
+
+Graph Export
+------------
+
+Stoichiograph builds a graph to find a word's elemental spellings. Use the
+`--export-graph` option to output dot code that `graphviz`_ can use to generate
+an image of the graph.
+
+.. code-block:: bash
+
+ $ stoichiograph --export-graph flashbacks | dot -Tpng -o word_graph.png
+
+.. figure:: https://cloud.githubusercontent.com/assets/5744114/26102406/abf1a33a-39e9-11e7-8bdb-fef168e8e0cf.png
+ :alt: The file output by the above command
+
+ A visualization of the directed acyclic graph of elemental spellings for
+ 'flashbacks'.
+
+
+.. _Graphviz: http://www.graphviz.org/Home.php
diff --git a/conftest.py b/conftest.py
@@ -1,51 +0,0 @@
-from collections import defaultdict
-import pytest
-from speller import Graph, Node
-
-
-@pytest.fixture()
-def test_graph():
- """Return a `speller.Graph` object of the word 'because'."""
- test_graph = Graph()
-
- test_graph._parents_of = defaultdict(
- set,
- {
- Node(value='c', position=2): {Node(value='be', position=0)},
- Node(value='au', position=3): {Node(value='c', position=2)},
- Node(value='s', position=5): {
- Node(value='au', position=3),
- Node(value='u', position=4)
- },
- Node(value='se', position=5): {
- Node(value='au', position=3),
- Node(value='u', position=4)
- },
- None: {Node(value='se', position=5)},
- Node(value='ca', position=2): {Node(value='be', position=0)},
- Node(value='u', position=4): {Node(value='ca', position=2)}
- }
- )
-
- test_graph._children_of = defaultdict(
- set,
- {
- None: {Node(value='be', position=0), Node(value='b', position=0)},
- Node(value='be', position=0): {
- Node(value='ca', position=2),
- Node(value='c', position=2)
- },
- Node(value='c', position=2): {Node(value='au', position=3)},
- Node(value='au', position=3): {
- Node(value='se', position=5),
- Node(value='s', position=5)
- },
- Node(value='ca', position=2): {Node(value='u', position=4)},
- Node(value='u', position=4): {
- Node(value='se', position=5),
- Node(value='s', position=5)
- }
- }
- )
-
- return test_graph
diff --git a/setup.py b/setup.py
@@ -0,0 +1,72 @@
+import io
+import os
+import re
+from setuptools import setup
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+def read(*names, **kwargs):
+ with io.open(
+ os.path.join(here, *names),
+ encoding=kwargs.get('encoding', 'utf8')
+ ) as fp:
+ return fp.read()
+
+
+def find_version(*file_paths):
+ version_file = read(*file_paths)
+ version_match = re.search(
+ r"^__version__ = ['\"]([^'\"]*)['\"]",
+ version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError('Unable to find version string.')
+
+
+def readme():
+ with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
+ return f.read()
+
+
+setup(
+ name='Stoichiograph',
+ version=find_version('stoichiograph', '__init__.py'),
+ description=(
+ 'Spell words with elemental symbols from the periodic table ("He", "Cu", etc).'
+ ),
+ long_description=readme(),
+ url='https://github.com/mesbahamin/stoichiograph',
+ author='Amin Mesbah',
+ author_email='mesbahamin@gmail.com',
+ license='MIT',
+ # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Console',
+ 'Intended Audience :: Education',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: End Users/Desktop',
+ 'Topic :: Scientific/Engineering :: Chemistry',
+ 'Topic :: Text Processing :: Filters',
+ 'Topic :: Utilities',
+ 'License :: OSI Approved :: MIT License',
+ 'Natural Language :: English',
+ 'Operating System :: Microsoft :: Windows :: Windows 7',
+ 'Operating System :: POSIX :: Linux',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ ],
+ keywords='command-line chemistry words fun combinatorics spelling',
+ packages=['stoichiograph'],
+ include_package_data=True,
+ entry_points={
+ 'console_scripts': [
+ 'stoichiograph = stoichiograph.stoichiograph:main',
+ ],
+ },
+)
diff --git a/stoichiograph.py b/stoichiograph.py
@@ -1,147 +0,0 @@
-#!/usr/bin/python3
-
-import argparse
-import collections
-import json
-import logging
-import pathlib
-
-import speller
-
-__title__ = 'stoichiograph'
-__author__ = 'Amin Mesbah'
-__version__ = '0.0.1'
-__description__ = 'Spell words with elemental symbols from the periodic table.'
-
-
-def get_args():
- parser = argparse.ArgumentParser(
- prog=__title__,
- description=__description__
- )
- parser.add_argument(
- 'words',
- help='word(s) for which to find elemental spellings',
- type=str,
- nargs='*'
- )
- parser.add_argument(
- '-b', '--batch-file',
- help='text file containing one word per line'
- )
- parser.add_argument(
- '-c', '--clobber', action='store_true',
- help='overwrite output file if it exists'
- )
- parser.add_argument(
- '--debug', action='store_true',
- help='print debug log'
- )
- parser.add_argument(
- '--list-elements', action='store_true',
- help='print list of elemental symbols and exit'
- )
- parser.add_argument(
- '--export-graph', action='store_true',
- help='export graph of first word as dot code'
- )
- parser.add_argument(
- '-o', '--output-file',
- help='path of output json file'
- )
- parser.add_argument(
- '-s', '--sort', action='store_true',
- help='sort words by length'
- )
- parser.add_argument(
- '-t', '--tuples', action='store_true',
- help='display spellings as tuples'
- )
- parser.add_argument(
- '-v', '--verbose', action='store_true',
- help='print a detailed log'
- )
- parser.add_argument(
- '-V', '--version', action='store_true',
- help='print version info and exit'
- )
-
- return parser.parse_args()
-
-
-def main():
- args = get_args()
-
- if args.version:
- print('{} {}'.format(__title__, __version__))
- raise SystemExit
-
- if args.list_elements:
- print('{} Elements:'.format(len(speller.ELEMENTS)))
- print(sorted(list(speller.ELEMENTS)))
- raise SystemExit
-
- if args.debug:
- CONSOLE_LOG_LEVEL = logging.DEBUG
- elif args.verbose:
- CONSOLE_LOG_LEVEL = logging.INFO
- else:
- CONSOLE_LOG_LEVEL = logging.WARNING
-
- logging.basicConfig(level=CONSOLE_LOG_LEVEL)
- logging.debug('{} {}'.format(__title__, __version__))
-
- SORT_WORDS = args.sort
- TUPLES = args.tuples
-
- if args.output_file:
- OUTPUT_FILE = pathlib.Path(args.output_file)
- CLOBBER = args.clobber
-
- if not CLOBBER and OUTPUT_FILE.exists():
- logging.warning(
- "{} exists. To overwrite, use '--clobber'.".format(OUTPUT_FILE)
- )
- raise SystemExit
-
- if args.batch_file:
- words_file = pathlib.Path(args.batch_file)
- with words_file.open('r') as f:
- dictionary = f.readlines()
-
- # TODO(amin): Handle punctuation, apostraphies, etc.
- words = [word.rstrip('\n') for word in dictionary if "'" not in word]
- else:
- words = args.words
-
- if SORT_WORDS:
- words.sort(key=len, reverse=True)
-
- if args.export_graph and words:
- g = speller.Graph()
- speller.build_spelling_graph(words[0], g)
- print(g.export())
- raise SystemExit
-
- spellable = collections.OrderedDict()
-
- for word in words:
- if TUPLES:
- spellings = speller.spell(word)
- else:
- spellings = [''.join(s) for s in speller.spell(word)]
-
- if spellings:
- spellable[word] = spellings
- for spelling in spellings:
- print(spelling)
-
- if args.output_file:
- with OUTPUT_FILE.open('w') as f:
- json.dump(spellable, f, indent=4, sort_keys=False)
-
- logging.debug('Done!')
-
-
-if __name__ == '__main__':
- main()
diff --git a/stoichiograph/__init__.py b/stoichiograph/__init__.py
@@ -0,0 +1,9 @@
+__title__ = 'stoichiograph'
+__version__ = '0.1.2'
+__license__ = 'MIT'
+__author__ = 'Amin Mesbah'
+__email__ = 'mesbahamin@gmail.com'
+__description__ = (
+ 'Spell words with elemental symbols from the periodic table '
+ '("He", "Cu", etc).'
+)
diff --git a/stoichiograph/__main__.py b/stoichiograph/__main__.py
@@ -0,0 +1,4 @@
+from stoichiograph.stoichiograph import main
+
+if __name__ == '__main__':
+ main()
diff --git a/speller.py b/stoichiograph/speller.py
diff --git a/stoichiograph/stoichiograph.py b/stoichiograph/stoichiograph.py
@@ -0,0 +1,147 @@
+#!/usr/bin/python3
+
+import argparse
+import collections
+import json
+import logging
+import pathlib
+
+from stoichiograph import speller
+
+__title__ = 'stoichiograph'
+__author__ = 'Amin Mesbah'
+__version__ = '0.0.1'
+__description__ = 'Spell words with elemental symbols from the periodic table.'
+
+
+def get_args():
+ parser = argparse.ArgumentParser(
+ prog=__title__,
+ description=__description__
+ )
+ parser.add_argument(
+ 'words',
+ help='word(s) for which to find elemental spellings',
+ type=str,
+ nargs='*'
+ )
+ parser.add_argument(
+ '-b', '--batch-file',
+ help='text file containing one word per line'
+ )
+ parser.add_argument(
+ '-c', '--clobber', action='store_true',
+ help='overwrite output file if it exists'
+ )
+ parser.add_argument(
+ '--debug', action='store_true',
+ help='print debug log'
+ )
+ parser.add_argument(
+ '--list-elements', action='store_true',
+ help='print list of elemental symbols and exit'
+ )
+ parser.add_argument(
+ '--export-graph', action='store_true',
+ help='export graph of first word as dot code'
+ )
+ parser.add_argument(
+ '-o', '--output-file',
+ help='path of output json file'
+ )
+ parser.add_argument(
+ '-s', '--sort', action='store_true',
+ help='sort words by length'
+ )
+ parser.add_argument(
+ '-t', '--tuples', action='store_true',
+ help='display spellings as tuples'
+ )
+ parser.add_argument(
+ '-v', '--verbose', action='store_true',
+ help='print a detailed log'
+ )
+ parser.add_argument(
+ '-V', '--version', action='store_true',
+ help='print version info and exit'
+ )
+
+ return parser.parse_args()
+
+
+def main():
+ args = get_args()
+
+ if args.version:
+ print('{} {}'.format(__title__, __version__))
+ raise SystemExit
+
+ if args.list_elements:
+ print('{} Elements:'.format(len(speller.ELEMENTS)))
+ print(sorted(list(speller.ELEMENTS)))
+ raise SystemExit
+
+ if args.debug:
+ CONSOLE_LOG_LEVEL = logging.DEBUG
+ elif args.verbose:
+ CONSOLE_LOG_LEVEL = logging.INFO
+ else:
+ CONSOLE_LOG_LEVEL = logging.WARNING
+
+ logging.basicConfig(level=CONSOLE_LOG_LEVEL)
+ logging.debug('{} {}'.format(__title__, __version__))
+
+ SORT_WORDS = args.sort
+ TUPLES = args.tuples
+
+ if args.output_file:
+ OUTPUT_FILE = pathlib.Path(args.output_file)
+ CLOBBER = args.clobber
+
+ if not CLOBBER and OUTPUT_FILE.exists():
+ logging.warning(
+ "{} exists. To overwrite, use '--clobber'.".format(OUTPUT_FILE)
+ )
+ raise SystemExit
+
+ if args.batch_file:
+ words_file = pathlib.Path(args.batch_file)
+ with words_file.open('r') as f:
+ dictionary = f.readlines()
+
+ # TODO(amin): Handle punctuation, apostraphies, etc.
+ words = [word.rstrip('\n') for word in dictionary if "'" not in word]
+ else:
+ words = args.words
+
+ if SORT_WORDS:
+ words.sort(key=len, reverse=True)
+
+ if args.export_graph and words:
+ g = speller.Graph()
+ speller.build_spelling_graph(words[0], g)
+ print(g.export())
+ raise SystemExit
+
+ spellable = collections.OrderedDict()
+
+ for word in words:
+ if TUPLES:
+ spellings = speller.spell(word)
+ else:
+ spellings = [''.join(s) for s in speller.spell(word)]
+
+ if spellings:
+ spellable[word] = spellings
+ for spelling in spellings:
+ print(spelling)
+
+ if args.output_file:
+ with OUTPUT_FILE.open('w') as f:
+ json.dump(spellable, f, indent=4, sort_keys=False)
+
+ logging.debug('Done!')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests.py b/tests.py
@@ -1,209 +0,0 @@
-from collections import defaultdict
-import speller
-from speller import Node
-
-ELEMENTS = {
- 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al',
- 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe',
- 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y',
- 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb',
- 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd',
- 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir',
- 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac',
- 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No',
- 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl',
- 'Mc', 'Lv', 'Ts', 'Og'
-}
-
-
-def test_verify_data():
- """Assert that the set of elements in `speller.py` matches this
- canonical set.
- """
- assert speller.ELEMENTS == ELEMENTS
-
-
-def test_elemental_spelling():
- """Assert that we get the expected results when spelling various
- inputs.
- """
- assert speller.spell('amputation') == [
- ('Am', 'Pu', 'Ta', 'Ti', 'O', 'N'),
- ('Am', 'P', 'U', 'Ta', 'Ti', 'O', 'N')
- ]
- assert speller.spell('') == []
- assert speller.spell('o') == [('O',)]
-
-
-def test_find_all_paths():
- """Make simple graph with some branches, and assert that we find all
- the paths from the first node to the last.
- """
- parents_to_children = {
- 'a': {'b'},
- 'b': {'c'},
- 'c': {'d'},
- 'd': {'e', 'y', 'z'},
- 'e': {'f', 'x'},
- 'f': {'g', 'x'},
- 'g': {'h'},
- 'h': {'i'},
- 'x': {'y'},
- 'y': {'z'},
- }
-
- assert set(speller.find_all_paths(parents_to_children, 'a', 'z')) == set([
- ('a', 'b', 'c', 'd', 'z'),
- ('a', 'b', 'c', 'd', 'y', 'z'),
- ('a', 'b', 'c', 'd', 'e', 'x', 'y', 'z'),
- ('a', 'b', 'c', 'd', 'e', 'f', 'x', 'y', 'z'),
- ])
-
-
-def test_build_spelling_graph():
- """Make a `speller.Graph` object, then build it with a word and
- assert that it contains the proper node relationships.
- """
- g = speller.Graph()
- speller.build_spelling_graph('because', g)
-
- assert g._parents_of == defaultdict(
- set,
- {
- Node(value='c', position=2): {Node(value='be', position=0)},
- Node(value='au', position=3): {Node(value='c', position=2)},
- Node(value='s', position=5): {
- Node(value='au', position=3),
- Node(value='u', position=4)
- },
- Node(value='se', position=5): {
- Node(value='au', position=3),
- Node(value='u', position=4)
- },
- None: {Node(value='se', position=5)},
- Node(value='ca', position=2): {Node(value='be', position=0)},
- Node(value='u', position=4): {Node(value='ca', position=2)}
- }
- )
-
- assert g._children_of == defaultdict(
- set,
- {
- None: {Node(value='be', position=0), Node(value='b', position=0)},
- Node(value='be', position=0): {
- Node(value='ca', position=2),
- Node(value='c', position=2)
- },
- Node(value='c', position=2): {Node(value='au', position=3)},
- Node(value='au', position=3): {
- Node(value='se', position=5),
- Node(value='s', position=5)
- },
- Node(value='ca', position=2): {Node(value='u', position=4)},
- Node(value='u', position=4): {
- Node(value='se', position=5),
- Node(value='s', position=5)
- }
- }
- )
-
-
-class TestGraph:
- """Tests for the methods of the `speller.Graph` class."""
-
- def test_firsts(self, test_graph):
- """Assert that the graph properly identifies its first nodes."""
- assert test_graph.firsts() == {Node('be', 0), Node('b', 0)}
-
- def test_lasts(self, test_graph):
- """Assert that the graph properly identifies its last nodes."""
- assert test_graph.lasts() == {Node('se', 5)}
-
- def test_add_edge(self, test_graph):
- """Add an edge to the graph."""
- parent = Node('te', 0)
- child = Node('st', 2)
- test_graph.add_edge(parent, child)
- assert test_graph._children_of[parent] == {child}
- assert test_graph._parents_of[child] == {parent}
-
- def test_add_edge_with_no_parent(self, test_graph):
- """Add an edge with no parent to the graph. Assert that 'None'
- isn't added to `_parents_of[child]`.
- """
- parent = None
- child = Node('a', 0)
- test_graph.add_edge(parent, child)
- assert child in test_graph._children_of[parent]
- assert None not in test_graph._parents_of[child]
-
- def test_add_edge_with_no_child(self, test_graph):
- """Add an edge with no child to the graph. Assert that `None`
- isn't added to `_children_of[parent]`.
- """
- parent = Node('z', 25)
- child = None
- test_graph.add_edge(parent, child)
- assert None not in test_graph._children_of[parent]
- assert parent in test_graph._parents_of[child]
-
- def test_nodes(self, test_graph):
- """Assert that the graph properly lists its nodes."""
- assert set(test_graph.nodes(connected_only=True)) == set([
- Node(value='be', position=0),
- Node(value='c', position=2),
- Node(value='ca', position=2),
- Node(value='au', position=3),
- Node(value='u', position=4),
- Node(value='s', position=5),
- Node(value='se', position=5),
- ])
- assert set(test_graph.nodes(connected_only=False)) == set([
- Node(value='b', position=0),
- Node(value='be', position=0),
- Node(value='c', position=2),
- Node(value='ca', position=2),
- Node(value='au', position=3),
- Node(value='u', position=4),
- Node(value='s', position=5),
- Node(value='se', position=5),
- ])
-
- def test_edges(self, test_graph):
- """Assert that the graph properly lists its edges."""
- assert set(test_graph.edges()) == set([
- (None, Node(value='b', position=0)),
- (None, Node(value='be', position=0)),
- (Node(value='be', position=0), Node(value='c', position=2)),
- (Node(value='be', position=0), Node(value='ca', position=2)),
- (Node(value='c', position=2), Node(value='au', position=3)),
- (Node(value='au', position=3), Node(value='s', position=5)),
- (Node(value='au', position=3), Node(value='se', position=5)),
- (Node(value='ca', position=2), Node(value='u', position=4)),
- (Node(value='u', position=4), Node(value='s', position=5)),
- (Node(value='u', position=4), Node(value='se', position=5))
- ])
-
- def test_export(self, test_graph):
- """Assert that the graph exports the proper dot code."""
- assert test_graph.export() == (
- """digraph G {
- graph [rankdir=LR];
- node [width=0.75 shape=circle];
- "Node(value='au', position=3)" -> "Node(value='s', position=5)";
- "Node(value='au', position=3)" -> "Node(value='se', position=5)";
- "Node(value='be', position=0)" -> "Node(value='c', position=2)";
- "Node(value='be', position=0)" -> "Node(value='ca', position=2)";
- "Node(value='c', position=2)" -> "Node(value='au', position=3)";
- "Node(value='ca', position=2)" -> "Node(value='u', position=4)";
- "Node(value='u', position=4)" -> "Node(value='s', position=5)";
- "Node(value='u', position=4)" -> "Node(value='se', position=5)";
- "Node(value='au', position=3)" [label="Au"];
- "Node(value='be', position=0)" [label="Be"];
- "Node(value='c', position=2)" [label="C"];
- "Node(value='ca', position=2)" [label="Ca"];
- "Node(value='s', position=5)" [label="S"];
- "Node(value='se', position=5)" [label="Se"];
- "Node(value='u', position=4)" [label="U"];
-}"""
- )
diff --git a/tests/conftest.py b/tests/conftest.py
@@ -0,0 +1,51 @@
+from collections import defaultdict
+import pytest
+from stoichiograph.speller import Graph, Node
+
+
+@pytest.fixture()
+def test_graph():
+ """Return a `speller.Graph` object of the word 'because'."""
+ test_graph = Graph()
+
+ test_graph._parents_of = defaultdict(
+ set,
+ {
+ Node(value='c', position=2): {Node(value='be', position=0)},
+ Node(value='au', position=3): {Node(value='c', position=2)},
+ Node(value='s', position=5): {
+ Node(value='au', position=3),
+ Node(value='u', position=4)
+ },
+ Node(value='se', position=5): {
+ Node(value='au', position=3),
+ Node(value='u', position=4)
+ },
+ None: {Node(value='se', position=5)},
+ Node(value='ca', position=2): {Node(value='be', position=0)},
+ Node(value='u', position=4): {Node(value='ca', position=2)}
+ }
+ )
+
+ test_graph._children_of = defaultdict(
+ set,
+ {
+ None: {Node(value='be', position=0), Node(value='b', position=0)},
+ Node(value='be', position=0): {
+ Node(value='ca', position=2),
+ Node(value='c', position=2)
+ },
+ Node(value='c', position=2): {Node(value='au', position=3)},
+ Node(value='au', position=3): {
+ Node(value='se', position=5),
+ Node(value='s', position=5)
+ },
+ Node(value='ca', position=2): {Node(value='u', position=4)},
+ Node(value='u', position=4): {
+ Node(value='se', position=5),
+ Node(value='s', position=5)
+ }
+ }
+ )
+
+ return test_graph
diff --git a/tests/test_speller.py b/tests/test_speller.py
@@ -0,0 +1,209 @@
+from collections import defaultdict
+from stoichiograph import speller
+from stoichiograph.speller import Node
+
+ELEMENTS = {
+ 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na', 'Mg', 'Al',
+ 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe',
+ 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y',
+ 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb',
+ 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd',
+ 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir',
+ 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac',
+ 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No',
+ 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fl',
+ 'Mc', 'Lv', 'Ts', 'Og'
+}
+
+
+def test_verify_data():
+ """Assert that the set of elements in `speller.py` matches this
+ canonical set.
+ """
+ assert speller.ELEMENTS == ELEMENTS
+
+
+def test_elemental_spelling():
+ """Assert that we get the expected results when spelling various
+ inputs.
+ """
+ assert speller.spell('amputation') == [
+ ('Am', 'Pu', 'Ta', 'Ti', 'O', 'N'),
+ ('Am', 'P', 'U', 'Ta', 'Ti', 'O', 'N')
+ ]
+ assert speller.spell('') == []
+ assert speller.spell('o') == [('O',)]
+
+
+def test_find_all_paths():
+ """Make simple graph with some branches, and assert that we find all
+ the paths from the first node to the last.
+ """
+ parents_to_children = {
+ 'a': {'b'},
+ 'b': {'c'},
+ 'c': {'d'},
+ 'd': {'e', 'y', 'z'},
+ 'e': {'f', 'x'},
+ 'f': {'g', 'x'},
+ 'g': {'h'},
+ 'h': {'i'},
+ 'x': {'y'},
+ 'y': {'z'},
+ }
+
+ assert set(speller.find_all_paths(parents_to_children, 'a', 'z')) == set([
+ ('a', 'b', 'c', 'd', 'z'),
+ ('a', 'b', 'c', 'd', 'y', 'z'),
+ ('a', 'b', 'c', 'd', 'e', 'x', 'y', 'z'),
+ ('a', 'b', 'c', 'd', 'e', 'f', 'x', 'y', 'z'),
+ ])
+
+
+def test_build_spelling_graph():
+ """Make a `speller.Graph` object, then build it with a word and
+ assert that it contains the proper node relationships.
+ """
+ g = speller.Graph()
+ speller.build_spelling_graph('because', g)
+
+ assert g._parents_of == defaultdict(
+ set,
+ {
+ Node(value='c', position=2): {Node(value='be', position=0)},
+ Node(value='au', position=3): {Node(value='c', position=2)},
+ Node(value='s', position=5): {
+ Node(value='au', position=3),
+ Node(value='u', position=4)
+ },
+ Node(value='se', position=5): {
+ Node(value='au', position=3),
+ Node(value='u', position=4)
+ },
+ None: {Node(value='se', position=5)},
+ Node(value='ca', position=2): {Node(value='be', position=0)},
+ Node(value='u', position=4): {Node(value='ca', position=2)}
+ }
+ )
+
+ assert g._children_of == defaultdict(
+ set,
+ {
+ None: {Node(value='be', position=0), Node(value='b', position=0)},
+ Node(value='be', position=0): {
+ Node(value='ca', position=2),
+ Node(value='c', position=2)
+ },
+ Node(value='c', position=2): {Node(value='au', position=3)},
+ Node(value='au', position=3): {
+ Node(value='se', position=5),
+ Node(value='s', position=5)
+ },
+ Node(value='ca', position=2): {Node(value='u', position=4)},
+ Node(value='u', position=4): {
+ Node(value='se', position=5),
+ Node(value='s', position=5)
+ }
+ }
+ )
+
+
+class TestGraph:
+ """Tests for the methods of the `speller.Graph` class."""
+
+ def test_firsts(self, test_graph):
+ """Assert that the graph properly identifies its first nodes."""
+ assert test_graph.firsts() == {Node('be', 0), Node('b', 0)}
+
+ def test_lasts(self, test_graph):
+ """Assert that the graph properly identifies its last nodes."""
+ assert test_graph.lasts() == {Node('se', 5)}
+
+ def test_add_edge(self, test_graph):
+ """Add an edge to the graph."""
+ parent = Node('te', 0)
+ child = Node('st', 2)
+ test_graph.add_edge(parent, child)
+ assert test_graph._children_of[parent] == {child}
+ assert test_graph._parents_of[child] == {parent}
+
+ def test_add_edge_with_no_parent(self, test_graph):
+ """Add an edge with no parent to the graph. Assert that 'None'
+ isn't added to `_parents_of[child]`.
+ """
+ parent = None
+ child = Node('a', 0)
+ test_graph.add_edge(parent, child)
+ assert child in test_graph._children_of[parent]
+ assert None not in test_graph._parents_of[child]
+
+ def test_add_edge_with_no_child(self, test_graph):
+ """Add an edge with no child to the graph. Assert that `None`
+ isn't added to `_children_of[parent]`.
+ """
+ parent = Node('z', 25)
+ child = None
+ test_graph.add_edge(parent, child)
+ assert None not in test_graph._children_of[parent]
+ assert parent in test_graph._parents_of[child]
+
+ def test_nodes(self, test_graph):
+ """Assert that the graph properly lists its nodes."""
+ assert set(test_graph.nodes(connected_only=True)) == set([
+ Node(value='be', position=0),
+ Node(value='c', position=2),
+ Node(value='ca', position=2),
+ Node(value='au', position=3),
+ Node(value='u', position=4),
+ Node(value='s', position=5),
+ Node(value='se', position=5),
+ ])
+ assert set(test_graph.nodes(connected_only=False)) == set([
+ Node(value='b', position=0),
+ Node(value='be', position=0),
+ Node(value='c', position=2),
+ Node(value='ca', position=2),
+ Node(value='au', position=3),
+ Node(value='u', position=4),
+ Node(value='s', position=5),
+ Node(value='se', position=5),
+ ])
+
+ def test_edges(self, test_graph):
+ """Assert that the graph properly lists its edges."""
+ assert set(test_graph.edges()) == set([
+ (None, Node(value='b', position=0)),
+ (None, Node(value='be', position=0)),
+ (Node(value='be', position=0), Node(value='c', position=2)),
+ (Node(value='be', position=0), Node(value='ca', position=2)),
+ (Node(value='c', position=2), Node(value='au', position=3)),
+ (Node(value='au', position=3), Node(value='s', position=5)),
+ (Node(value='au', position=3), Node(value='se', position=5)),
+ (Node(value='ca', position=2), Node(value='u', position=4)),
+ (Node(value='u', position=4), Node(value='s', position=5)),
+ (Node(value='u', position=4), Node(value='se', position=5))
+ ])
+
+ def test_export(self, test_graph):
+ """Assert that the graph exports the proper dot code."""
+ assert test_graph.export() == (
+ """digraph G {
+ graph [rankdir=LR];
+ node [width=0.75 shape=circle];
+ "Node(value='au', position=3)" -> "Node(value='s', position=5)";
+ "Node(value='au', position=3)" -> "Node(value='se', position=5)";
+ "Node(value='be', position=0)" -> "Node(value='c', position=2)";
+ "Node(value='be', position=0)" -> "Node(value='ca', position=2)";
+ "Node(value='c', position=2)" -> "Node(value='au', position=3)";
+ "Node(value='ca', position=2)" -> "Node(value='u', position=4)";
+ "Node(value='u', position=4)" -> "Node(value='s', position=5)";
+ "Node(value='u', position=4)" -> "Node(value='se', position=5)";
+ "Node(value='au', position=3)" [label="Au"];
+ "Node(value='be', position=0)" [label="Be"];
+ "Node(value='c', position=2)" [label="C"];
+ "Node(value='ca', position=2)" [label="Ca"];
+ "Node(value='s', position=5)" [label="S"];
+ "Node(value='se', position=5)" [label="Se"];
+ "Node(value='u', position=4)" [label="U"];
+}"""
+ )