stoichiograph

Spell words with elemental symbols from the periodic table.
git clone git://git.amin.space/stoichiograph.git
Log | Files | Refs | LICENSE

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:
M.gitignore | 9+++++++--
AMANIFEST.in | 2++
MMakefile | 29++++++++++++++++++++---------
MREADME.rst | 35++++++++++++++++++++++++++++++++++-
Dconftest.py | 51---------------------------------------------------
Asetup.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dstoichiograph.py | 147-------------------------------------------------------------------------------
Astoichiograph/__init__.py | 9+++++++++
Astoichiograph/__main__.py | 4++++
Rspeller.py -> stoichiograph/speller.py | 0
Astoichiograph/stoichiograph.py | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtests.py | 209-------------------------------------------------------------------------------
Atests/conftest.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/test_speller.py | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"]; +}""" + )