From 6f1f7ac1f7274ed089806636cb89657f3435e2c3 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Sat, 14 Feb 2015 21:42:38 -0800 Subject: [PATCH 1/6] Switch from unittest module to nose for testing --- .coveragerc | 20 + .editorconfig | 3 + .gitignore | 8 + README.md | 19 +- YouVersion Suggest.alfredworkflow | Bin 23914 -> 23930 bytes tests/__init__.py | 11 - tests/test_pep8.py | 2 +- tests/test_search_book.py | 5 + tests/test_search_main.py | 10 + tests/test_search_xml.py | 3 +- yv_suggest/bible/books.json | 660 +++++++++++++++--------------- yv_suggest/bible/versions.json | 72 ++-- yv_suggest/search.py | 12 +- 13 files changed, 430 insertions(+), 395 deletions(-) create mode 100644 .coveragerc delete mode 100644 tests/__init__.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..0d20cb7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,20 @@ +# Configuration for coverage.py (https://pypi.python.org/pypi/coverage) + +# Enable branch coverage +[run] +branch = True + +[report] + +# Regexes for lines to exclude from consideration +exclude_lines = + # Allow source to designate lines/branches to be skipped + pragma: no cover + + # Ignore non-runnable code + if __name__ == .__main__.: + +# Only check coverage for source files +include = + yv_suggest/open.py + yv_suggest/search.py diff --git a/.editorconfig b/.editorconfig index c88ed16..9329484 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ indent_size = 4 [*.md] trim_trailing_whitespace = false + +[*.json] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 9ab76a1..0c1bc0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# OS-specific files .DS_Store + +# Python bytecode *.pyc __pycache__ + +# Test coverage +.coverage +coverage.xml +cover diff --git a/README.md b/README.md index ef5f1d9..9286148 100644 --- a/README.md +++ b/README.md @@ -27,25 +27,28 @@ suggestions matching your query. ## Testing If you are contributing to the project and would like to run the included unit -tests, run the following command in the project directory: +tests, run the `nosetests` command within the project directory. ``` -python -m unittest tests +nosetests ``` -### Running an individual test case +### Viewing test coverage -If you wish to run a single test case, reference the module name like so: +To view the test coverage report, run `nosetests` with the `--with-coverage` and +`--cover-erase` options. ``` -python -m unittest tests.test_search_book +nosetests --with-coverage --cover-erase ``` ### Requirements for running tests -Note that running these unit tests requires Python 2.7. Running these tests also -requires the `pep8` module, which you can install using `pip`: +Note that all scripts apart of the workflow require Python 2.7 (Python 3 is not +supported). Running these unit tests requires `nose`, `coverage`, and `pep8` to +be installed. If you do not have these packages installed already, you can +install them via `pip`: ``` -pip install pep8 +pip install nose coverage pep8 ``` diff --git a/YouVersion Suggest.alfredworkflow b/YouVersion Suggest.alfredworkflow index f2f8049ba2fa7c78ab0cc80300f261b7bafc8de4..fdc04b92da382ecc067f991b22522684f7d57d4d 100644 GIT binary patch delta 1314 zcmaF0i}BYkM&1B#W)?065Gc>}naC@{K2h#&h>GRii3U>j?BPCc+ie9qWtbTl`UM#n z1Q=u(k}{KWQuUMa^RtWfvWoNbLPIzin0>$B32`vJ8&X=q&A`a=m63sgMFeQ#)EU11 zw;cr9-v8m!PUqXo{N-_*Lenw_O=X33!ziOPWuUis7?d+UQCch_~U3cPQ#<80p-``$#?{3bCzdHjnud+)WEBXD! zch}d?6{jL>ep?x_*j6`KOcXHRVt6oQ&ZUcN0bSdfp6cBy`hG(0uq(I3XXC~~Pea>B zbGPgNo-<47(HxFBOXW*Les^q@zgXzvvPL>-dE|9orzcF0?AZ45OC&kecM9G+znMK% zx%78WO^Albqz)!0E)K!wiRtruvLj4BT~lyV6dkG zS8vuj@%v8eUYle3gO^v;+rxfAeVW>@+Q7F(Syv<~(pIjTAoJ3J|DNg0|MT9i>oKvq z!hVl)LI2`ePp%)hFvn9b`VxcBCbcJ9f?qnAzkJoibW-GJ?&F*BaP$aP3Nq15zRCk8XrpZQAwU`%=Bj$Ec~Ra+6O7tqk}+Q6hfD^^<{r zRH9$;3tcifRUldLsN3k0GxG#S>HZl)S66x&D~2hipX!*vc1YNLL1Tc2YSn7CX~NbE zq|Ps0Rw^8wRI8@cu&1f=?TqcFpSJC2m>b?Go>5;K!1Hcz=$=SEIa6;#uFTYr&4rG$ zIW^Y@Y!~os_0cl$Ecz8%6fq-6>V;w9=a*me&J}sAI>M}=oojXZd$7;?z?WWrJ3q{o zZ=AvPt+?`Zt&h7`wNDh!@{?RIVlMUQ{A<-x@bU@^N?pVMtM#?x?9FdvIW!eqy3Q}= z{3C5B5#nEeDX{FO+G|;VQyq0_fTDHsEyd5Kf4)127^;d)P*|GG$MLzDEADIUmOq0eW0Ae!7wSz$L-$>yPG{gUIH)!2{Xt{-oh+dU-Iov$QuKUtd)A+mhX@O z59lqJ~N!+lzdFu4%%RGlUUYpI^x1#r=% zi-Wh+-eK|C58>$vT#tc=2rYp1Inwy2iRfbGE*VQfE7- zvex-^(lmpfu6Ye{jK2h%7OzmOa*9mxew*{Qb#MAjx%qO>QtwU9XVGVtw7k1{AAIAxnpWfr|mzEO$Q!hRnRQe7%C4%;J*J5TFsdZzem(Dzcr92H6Mz-Zm}0 delta 1292 zcmeyhi}BSiM&1B#W)?065J(DBoyaT0E`E1aNYsYg6Ah&5zpN>9Rd7$7%*)KcP|wf6 zAiyBQkd&E}ld7MTpPyZB6Wr%euA z&hI%<-}C3|l266;Rx{7c-DL9LV0QfE#rM8^p8Rm>%C>ORo%`#jMQv@|Ww+O++Ia3C zE6I(g#p7MuE_I8)&~(brOmEV(ynDg#!p~@j`|i;^_VOF_q7LevW2|;H{QG9^c6+{` zU5lRhGw%Pix9^p}-5+snycZ*`9$9oRWUGsDgWZt@{0H^eW@a2n-RxeUs%+Yxb4|N4 zh&qS{-t4AnGbAB`gorlU{*HOvD|JJbm9Cen_Sohi zBPk>M`rP{!+7V5+{rLD-oKCg8a;$!Z-3P9~NU!f2AKc%+n)%;OdfpkQ_bK-KnAz;U zWre6S_to`X*|^A0q|!M^F?<#hJ{&VZ{cEcP)hYFoG~Fm$gE zf4$Y?xtf+4y;lrln!Vhw)&FPu;a`>)vT)n&9jU5;n_dh4ZjG8d<$&_N;OZrhcZId> zW|bD$<@^5v=k^5;6T3ned!3buR-M{%aRS?GWA~>CX9G{)S=smWM%kw8MkXqGkEcb{ zyK_F$ZoA%>8Q{%(i2InUWX0hr?(;q#j6T{P`249sebaKE)wjxzvF5soZ3-&W-ZEi- znxebd1f}(pjSkIN*J$=&S^n1z?G|%>J=UmL>zjP5tY>@FHlbBtmgd*!&zYWJ_vrS` zU@qIY?@KO9s@tFVaZ&GNy!k|CVU3@fT*YR z7~`j2csb(?W9Oya0#6=!7}d_$Sh&0MYTzB_w<~@YODBKkuRX?Ro%<{ImozB5fwBe% zgGBBO*9lLn0=j`bo5_2b_{ za_lp1o1eOMt#Q{IqaEy%R(F0#yZK3{()F~iiBV$KT;(Xft`>)&Z^0a1%U?dQQQLcM zSLrm-ZQWbLudO|i^}oify^?uyJ&Qhb@P^x)H?l17WRi}Z9PFpa?z-W22uQ5jFNT*H zSc*)(6|Kb#EJh~B#>nuYmLZc3V>Cgf1xU&kB?N$FhA@K!Ff14tfJ}xZjnjdJ4y3Rt zOD!tS%+G_BH>?cIz>or&F!@r99Ap0Ek1_hj(y`udy(;|29C#TRvUC_2xKPY8_hVql d%uCDHE6B+#E(r|*8sWNOa&W97+wN$PjR1LMJhT7+ diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 26887c4..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -from tests.test_search_book import * -from tests.test_search_chapter import * -from tests.test_search_verse import * -from tests.test_search_version import * -from tests.test_search_incomplete import * -from tests.test_search_xml import * -from tests.test_search_main import * -from tests.test_open import * -from tests.test_pep8 import * diff --git a/tests/test_pep8.py b/tests/test_pep8.py index fdb0f6f..b4f2eec 100644 --- a/tests/test_pep8.py +++ b/tests/test_pep8.py @@ -21,7 +21,7 @@ def test_test_pep8_compliant(self): style_guide = pep8.StyleGuide(quiet=True) result = style_guide.check_files(glob.iglob('tests/*.py')) self.assertEqual(result.total_errors, 0, - 'Unit tests not pep8-compliant') + 'Unit tests are not pep8-compliant') if __name__ == '__main__': unittest.main() diff --git a/tests/test_search_book.py b/tests/test_search_book.py index 5549fc8..3db6cc1 100644 --- a/tests/test_search_book.py +++ b/tests/test_search_book.py @@ -74,5 +74,10 @@ def test_nonexistent(self): results = yvs.get_result_list('jesus') self.assertEqual(len(results), 0) + def test_empty(self): + """should not match empty input""" + results = yvs.get_result_list('') + self.assertEqual(len(results), 0) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_search_main.py b/tests/test_search_main.py index 95fc488..5c965fd 100644 --- a/tests/test_search_main.py +++ b/tests/test_search_main.py @@ -7,6 +7,7 @@ from StringIO import StringIO import inspect import sys +import os class SearchMainTestCase(unittest.TestCase): @@ -52,5 +53,14 @@ def test_query_param(self): default_query_str = spec.defaults[0] self.assertEqual(default_query_str, '{query}') + def test_source_only(self): + """should run script from source only""" + yvs.sys.argv[0] = yvs.__file__ + del yvs.__file__ + results = yvs.get_result_list('e') + self.assertEqual(len(results), 6) + yvs.__file__ = yvs.sys.argv[0] + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_search_xml.py b/tests/test_search_xml.py index 9f3e28f..3252073 100644 --- a/tests/test_search_xml.py +++ b/tests/test_search_xml.py @@ -23,7 +23,8 @@ def test_structure(self): result = results[0] xml = yvs.get_result_list_xml(results) root = ET.fromstring(xml) - self.assertEqual(root.tag, 'items', 'root element incorrectly named') + self.assertEqual(root.tag, 'items', + 'root element must be named ') item = root.find('item') self.assertIsNotNone(item, ' element is missing') self.assertEqual(item.get('uid'), result['uid']) diff --git a/yv_suggest/bible/books.json b/yv_suggest/bible/books.json index 5e4eb71..2e333f5 100644 --- a/yv_suggest/bible/books.json +++ b/yv_suggest/bible/books.json @@ -1,332 +1,332 @@ [ - { - "name": "Genesis", - "id": "gen", - "chapters": 50 - }, - { - "name": "Exodus", - "id": "exo", - "chapters": 40 - }, - { - "name": "Leviticus", - "id": "lev", - "chapters": 27 - }, - { - "name": "Numbers", - "id": "num", - "chapters": 36 - }, - { - "name": "Deuteronomy", - "id": "deu", - "chapters": 34 - }, - { - "name": "Joshua", - "id": "jos", - "chapters": 24 - }, - { - "name": "Judges", - "id": "jdg", - "chapters": 21 - }, - { - "name": "Ruth", - "id": "rut", - "chapters": 4 - }, - { - "name": "1 Samuel", - "id": "1sa", - "chapters": 31 - }, - { - "name": "2 Samuel", - "id": "2sa", - "chapters": 24 - }, - { - "name": "1 Kings", - "id": "1ki", - "chapters": 22 - }, - { - "name": "2 Kings", - "id": "2ki", - "chapters": 25 - }, - { - "name": "1 Chronicles", - "id": "1ch", - "chapters": 29 - }, - { - "name": "2 Chronicles", - "id": "2ch", - "chapters": 36 - }, - { - "name": "Ezra", - "id": "ezr", - "chapters": 10 - }, - { - "name": "Nehemiah", - "id": "neh", - "chapters": 13 - }, - { - "name": "Esther", - "id": "est", - "chapters": 10 - }, - { - "name": "Job", - "id": "job", - "chapters": 42 - }, - { - "name": "Psalm", - "id": "psa", - "chapters": 150 - }, - { - "name": "Proverbs", - "id": "pro", - "chapters": 31 - }, - { - "name": "Ecclesiastes", - "id": "ecc", - "chapters": 12 - }, - { - "name": "Song of Songs", - "id": "sng", - "chapters": 8 - }, - { - "name": "Isaiah", - "id": "isa", - "chapters": 66 - }, - { - "name": "Jeremiah", - "id": "jer", - "chapters": 52 - }, - { - "name": "Lamentations", - "id": "lam", - "chapters": 5 - }, - { - "name": "Ezekiel", - "id": "ezk", - "chapters": 48 - }, - { - "name": "Daniel", - "id": "dan", - "chapters": 12 - }, - { - "name": "Hosea", - "id": "hos", - "chapters": 14 - }, - { - "name": "Joel", - "id": "jol", - "chapters": 3 - }, - { - "name": "Amos", - "id": "amo", - "chapters": 9 - }, - { - "name": "Obadiah", - "id": "oba", - "chapters": 1 - }, - { - "name": "Jonah", - "id": "jon", - "chapters": 4 - }, - { - "name": "Micah", - "id": "mic", - "chapters": 7 - }, - { - "name": "Nahum", - "id": "nah", - "chapters": 3 - }, - { - "name": "Habakkuk", - "id": "hab", - "chapters": 3 - }, - { - "name": "Zephaniah", - "id": "zep", - "chapters": 3 - }, - { - "name": "Haggai", - "id": "hag", - "chapters": 2 - }, - { - "name": "Zechariah", - "id": "zec", - "chapters": 14 - }, - { - "name": "Malachi", - "id": "mal", - "chapters": 4 - }, - { - "name": "Matthew", - "id": "mat", - "chapters": 28 - }, - { - "name": "Mark", - "id": "mrk", - "chapters": 16 - }, - { - "name": "Luke", - "id": "luk", - "chapters": 24 - }, - { - "name": "John", - "id": "jhn", - "chapters": 21 - }, - { - "name": "Acts", - "id": "act", - "chapters": 28 - }, - { - "name": "Romans", - "id": "rom", - "chapters": 16 - }, - { - "name": "1 Corinthians", - "id": "1co", - "chapters": 16 - }, - { - "name": "2 Corinthians", - "id": "2co", - "chapters": 13 - }, - { - "name": "Galatians", - "id": "gal", - "chapters": 6 - }, - { - "name": "Ephesians", - "id": "eph", - "chapters": 6 - }, - { - "name": "Philippians", - "id": "php", - "chapters": 4 - }, - { - "name": "Colossians", - "id": "col", - "chapters": 4 - }, - { - "name": "1 Thessalonians", - "id": "1th", - "chapters": 5 - }, - { - "name": "2 Thessalonians", - "id": "2th", - "chapters": 3 - }, - { - "name": "1 Timothy", - "id": "1ti", - "chapters": 6 - }, - { - "name": "2 Timothy", - "id": "2ti", - "chapters": 4 - }, - { - "name": "Titus", - "id": "tit", - "chapters": 3 - }, - { - "name": "Philemon", - "id": "phm", - "chapters": 1 - }, - { - "name": "Hebrews", - "id": "heb", - "chapters": 13 - }, - { - "name": "James", - "id": "jas", - "chapters": 5 - }, - { - "name": "1 Peter", - "id": "1pe", - "chapters": 5 - }, - { - "name": "2 Peter", - "id": "2pe", - "chapters": 3 - }, - { - "name": "1 John", - "id": "1jn", - "chapters": 5 - }, - { - "name": "2 John", - "id": "2jn", - "chapters": 1 - }, - { - "name": "3 John", - "id": "3jn", - "chapters": 1 - }, - { - "name": "Jude", - "id": "jud", - "chapters": 1 - }, - { - "name": "Revelation", - "id": "rev", - "chapters": 22 - } + { + "name": "Genesis", + "id": "gen", + "chapters": 50 + }, + { + "name": "Exodus", + "id": "exo", + "chapters": 40 + }, + { + "name": "Leviticus", + "id": "lev", + "chapters": 27 + }, + { + "name": "Numbers", + "id": "num", + "chapters": 36 + }, + { + "name": "Deuteronomy", + "id": "deu", + "chapters": 34 + }, + { + "name": "Joshua", + "id": "jos", + "chapters": 24 + }, + { + "name": "Judges", + "id": "jdg", + "chapters": 21 + }, + { + "name": "Ruth", + "id": "rut", + "chapters": 4 + }, + { + "name": "1 Samuel", + "id": "1sa", + "chapters": 31 + }, + { + "name": "2 Samuel", + "id": "2sa", + "chapters": 24 + }, + { + "name": "1 Kings", + "id": "1ki", + "chapters": 22 + }, + { + "name": "2 Kings", + "id": "2ki", + "chapters": 25 + }, + { + "name": "1 Chronicles", + "id": "1ch", + "chapters": 29 + }, + { + "name": "2 Chronicles", + "id": "2ch", + "chapters": 36 + }, + { + "name": "Ezra", + "id": "ezr", + "chapters": 10 + }, + { + "name": "Nehemiah", + "id": "neh", + "chapters": 13 + }, + { + "name": "Esther", + "id": "est", + "chapters": 10 + }, + { + "name": "Job", + "id": "job", + "chapters": 42 + }, + { + "name": "Psalm", + "id": "psa", + "chapters": 150 + }, + { + "name": "Proverbs", + "id": "pro", + "chapters": 31 + }, + { + "name": "Ecclesiastes", + "id": "ecc", + "chapters": 12 + }, + { + "name": "Song of Songs", + "id": "sng", + "chapters": 8 + }, + { + "name": "Isaiah", + "id": "isa", + "chapters": 66 + }, + { + "name": "Jeremiah", + "id": "jer", + "chapters": 52 + }, + { + "name": "Lamentations", + "id": "lam", + "chapters": 5 + }, + { + "name": "Ezekiel", + "id": "ezk", + "chapters": 48 + }, + { + "name": "Daniel", + "id": "dan", + "chapters": 12 + }, + { + "name": "Hosea", + "id": "hos", + "chapters": 14 + }, + { + "name": "Joel", + "id": "jol", + "chapters": 3 + }, + { + "name": "Amos", + "id": "amo", + "chapters": 9 + }, + { + "name": "Obadiah", + "id": "oba", + "chapters": 1 + }, + { + "name": "Jonah", + "id": "jon", + "chapters": 4 + }, + { + "name": "Micah", + "id": "mic", + "chapters": 7 + }, + { + "name": "Nahum", + "id": "nah", + "chapters": 3 + }, + { + "name": "Habakkuk", + "id": "hab", + "chapters": 3 + }, + { + "name": "Zephaniah", + "id": "zep", + "chapters": 3 + }, + { + "name": "Haggai", + "id": "hag", + "chapters": 2 + }, + { + "name": "Zechariah", + "id": "zec", + "chapters": 14 + }, + { + "name": "Malachi", + "id": "mal", + "chapters": 4 + }, + { + "name": "Matthew", + "id": "mat", + "chapters": 28 + }, + { + "name": "Mark", + "id": "mrk", + "chapters": 16 + }, + { + "name": "Luke", + "id": "luk", + "chapters": 24 + }, + { + "name": "John", + "id": "jhn", + "chapters": 21 + }, + { + "name": "Acts", + "id": "act", + "chapters": 28 + }, + { + "name": "Romans", + "id": "rom", + "chapters": 16 + }, + { + "name": "1 Corinthians", + "id": "1co", + "chapters": 16 + }, + { + "name": "2 Corinthians", + "id": "2co", + "chapters": 13 + }, + { + "name": "Galatians", + "id": "gal", + "chapters": 6 + }, + { + "name": "Ephesians", + "id": "eph", + "chapters": 6 + }, + { + "name": "Philippians", + "id": "php", + "chapters": 4 + }, + { + "name": "Colossians", + "id": "col", + "chapters": 4 + }, + { + "name": "1 Thessalonians", + "id": "1th", + "chapters": 5 + }, + { + "name": "2 Thessalonians", + "id": "2th", + "chapters": 3 + }, + { + "name": "1 Timothy", + "id": "1ti", + "chapters": 6 + }, + { + "name": "2 Timothy", + "id": "2ti", + "chapters": 4 + }, + { + "name": "Titus", + "id": "tit", + "chapters": 3 + }, + { + "name": "Philemon", + "id": "phm", + "chapters": 1 + }, + { + "name": "Hebrews", + "id": "heb", + "chapters": 13 + }, + { + "name": "James", + "id": "jas", + "chapters": 5 + }, + { + "name": "1 Peter", + "id": "1pe", + "chapters": 5 + }, + { + "name": "2 Peter", + "id": "2pe", + "chapters": 3 + }, + { + "name": "1 John", + "id": "1jn", + "chapters": 5 + }, + { + "name": "2 John", + "id": "2jn", + "chapters": 1 + }, + { + "name": "3 John", + "id": "3jn", + "chapters": 1 + }, + { + "name": "Jude", + "id": "jud", + "chapters": 1 + }, + { + "name": "Revelation", + "id": "rev", + "chapters": 22 + } ] diff --git a/yv_suggest/bible/versions.json b/yv_suggest/bible/versions.json index 309c95b..17e2657 100644 --- a/yv_suggest/bible/versions.json +++ b/yv_suggest/bible/versions.json @@ -1,38 +1,38 @@ [ - "AMP", - "ASV", - "BOOKS", - "CEB", - "CEV", - "CEV", - "CEVUK", - "CPDV", - "DARBY", - "DRA", - "ESV", - "ERV", - "GNB", - "GNBDC", - "GNBDK", - "GNT", - "GNTD", - "GWT", - "HCSB", - "ISR98", - "KJV", - "LEB", - "MSG", - "NIV", - "NIVUK", - "NLT", - "NET", - "NKJV", - "NCV", - "NASB", - "NABRE", - "NIRV", - "OJB", - "RV1885", - "TLV", - "WEB" + "AMP", + "ASV", + "BOOKS", + "CEB", + "CEV", + "CEV", + "CEVUK", + "CPDV", + "DARBY", + "DRA", + "ESV", + "ERV", + "GNB", + "GNBDC", + "GNBDK", + "GNT", + "GNTD", + "GWT", + "HCSB", + "ISR98", + "KJV", + "LEB", + "MSG", + "NIV", + "NIVUK", + "NLT", + "NET", + "NKJV", + "NCV", + "NASB", + "NABRE", + "NIRV", + "OJB", + "RV1885", + "TLV", + "WEB" ] diff --git a/yv_suggest/search.py b/yv_suggest/search.py index 25db52b..a7c13b9 100644 --- a/yv_suggest/search.py +++ b/yv_suggest/search.py @@ -14,20 +14,16 @@ def get_package_path(): if '__file__' in globals(): package_path = os.path.dirname(os.path.realpath(__file__)) - elif os.path.exists(sys.argv[0]): - package_path = os.path.dirname(os.path.realpath(sys.argv[0])) else: - package_path = '.' + package_path = os.path.dirname(os.path.realpath(sys.argv[0])) return package_path -package_path = get_package_path() - # Loads list of Bible books from file def get_books(): - books_path = os.path.join(package_path, 'bible', 'books.json') + books_path = os.path.join(get_package_path(), 'bible', 'books.json') with open(books_path, 'r') as file: books = tuple(json.load(file)) @@ -37,7 +33,7 @@ def get_books(): # Loads list of Bible versions from file def get_versions(): - versions_path = os.path.join(package_path, 'bible', 'versions.json') + versions_path = os.path.join(get_package_path(), 'bible', 'versions.json') with open(versions_path, 'r') as file: versions = tuple(json.load(file)) @@ -77,7 +73,7 @@ def guess_version(partial_version): version_guess = default_version # Attempt to guess the version used for version in versions: - if version.startswith(partial_version): + if version.startswith(partial_version): # pragma: no cover version_guess = version break From 28b1a932002d07d11ce165481dae463c66c95300 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Sun, 15 Feb 2015 17:47:09 -0800 Subject: [PATCH 2/6] Rewrite tests to use nose instead of unittest --- tests/test_open.py | 36 ++++--- tests/test_pep8.py | 30 ++---- tests/test_search_book.py | 160 ++++++++++++++++---------------- tests/test_search_chapter.py | 68 +++++++------- tests/test_search_incomplete.py | 36 ++++--- tests/test_search_main.py | 107 +++++++++++---------- tests/test_search_verse.py | 81 ++++++++-------- tests/test_search_version.py | 89 +++++++++--------- tests/test_search_xml.py | 67 +++++++------ 9 files changed, 323 insertions(+), 351 deletions(-) diff --git a/tests/test_open.py b/tests/test_open.py index d9aa27d..c036ee8 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.open as yvs import inspect @@ -13,26 +13,22 @@ def open(self, url): self.url = url -class OpenTestCase(unittest.TestCase): - """test the handling of Bible reference URLs""" +def test_url(): + """should build correct URL to Bible reference""" + url = yvs.get_ref_url('esv/jhn.3.16') + nose.assert_equal(url, 'https://www.bible.com/bible/esv/jhn.3.16') - def test_url(self): - """should build correct URL to Bible reference""" - url = yvs.get_ref_url('esv/jhn.3.16') - self.assertEqual(url, 'https://www.bible.com/bible/esv/jhn.3.16') - def test_query_param(self): - """should use received query parameter as default ref ID""" - spec = inspect.getargspec(yvs.main) - default_query_str = spec.defaults[0] - self.assertEqual(default_query_str, '{query}') +def test_query_param(): + """should use received query parameter as default ref ID""" + spec = inspect.getargspec(yvs.main) + default_query_str = spec.defaults[0] + nose.assert_equal(default_query_str, '{query}') - def test_url_open(self): - """should attempt to open URL using webbrowser module""" - mock = WebbrowserMock() - yvs.webbrowser = mock - yvs.main('nlt/jhn.3.17') - self.assertEqual(mock.url, 'https://www.bible.com/bible/nlt/jhn.3.17') -if __name__ == '__main__': - unittest.main() +def test_url_open(): + """should attempt to open URL using webbrowser module""" + mock = WebbrowserMock() + yvs.webbrowser = mock + yvs.main('nlt/jhn.3.17') + nose.assert_equal(mock.url, 'https://www.bible.com/bible/nlt/jhn.3.17') diff --git a/tests/test_pep8.py b/tests/test_pep8.py index b4f2eec..8fdb9a8 100644 --- a/tests/test_pep8.py +++ b/tests/test_pep8.py @@ -1,27 +1,15 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import pep8 import glob -import os -class TestPep8(unittest.TestCase): - """test all python source files for pep8 compliance""" - - def test_source_pep8_compliant(self): - """source files should comply with pep8""" - style_guide = pep8.StyleGuide(quiet=True) - result = style_guide.check_files(glob.iglob('yv_suggest/*.py')) - self.assertEqual(result.total_errors, 0, - 'Source files are not pep8-compliant') - - def test_test_pep8_compliant(self): - """unit tests should comply with pep8""" - style_guide = pep8.StyleGuide(quiet=True) - result = style_guide.check_files(glob.iglob('tests/*.py')) - self.assertEqual(result.total_errors, 0, - 'Unit tests are not pep8-compliant') - -if __name__ == '__main__': - unittest.main() +def test_source_compliance(): + """source files should comply with pep8""" + style_guide = pep8.StyleGuide(quiet=True) + files = glob.iglob('*/*.py') + for file in files: + result = style_guide.check_files((file,)) + msg = '{file} is not pep8-compliant'.format(file=file) + yield nose.assert_equal, result.total_errors, 0, msg diff --git a/tests/test_search_book.py b/tests/test_search_book.py index 3db6cc1..feab3b9 100644 --- a/tests/test_search_book.py +++ b/tests/test_search_book.py @@ -1,83 +1,87 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs -class SearchBookTestCase(unittest.TestCase): - """test the searching of Bible books""" - - def test_partial(self): - """should match books by partial name""" - results = yvs.get_result_list('luk') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Luke') - - def test_case(self): - """should match books irrespective of case""" - query_str = 'Matthew' - results = yvs.get_result_list(query_str) - results_lower = yvs.get_result_list(query_str.lower()) - results_upper = yvs.get_result_list(query_str.upper()) - self.assertEqual(len(results), 1) - self.assertListEqual(results_lower, results) - self.assertListEqual(results_upper, results) - - def test_whitespace(self): - """should match books irrespective of surrounding whitespace""" - results = yvs.get_result_list(' romans ') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Romans') - - def test_partial_ambiguous(self): - """should match books by ambiguous partial name""" - results = yvs.get_result_list('r') - self.assertEqual(len(results), 3) - self.assertEqual(results[0]['title'], 'Ruth') - self.assertEqual(results[1]['title'], 'Romans') - self.assertEqual(results[2]['title'], 'Revelation') - - def test_multiple_words(self): - """should match books with names comprised of multiple words""" - results = yvs.get_result_list('song of songs') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Song of Songs') - - def test_numbered_partial(self): - """should match numbered books by partial numbered name""" - results = yvs.get_result_list('1 cor') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], '1 Corinthians') - - def test_numbered_whitespace(self): - """should match numbered books irrespective of extra whitespace""" - results = yvs.get_result_list('1 cor') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], '1 Corinthians') - - def test_nonnumbered_partial(self): - """should match numbered books by partial non-numbered name""" - results = yvs.get_result_list('john') - self.assertEqual(len(results), 4) - self.assertEqual(results[0]['title'], 'John') - self.assertEqual(results[1]['title'], '1 John') - self.assertEqual(results[2]['title'], '2 John') - self.assertEqual(results[3]['title'], '3 John') - - def test_id(self): - """should use correct ID for books""" - results = yvs.get_result_list('philippians') - self.assertEqual(results[0]['uid'], 'niv/php.1') - - def test_nonexistent(self): - """should not match nonexistent books""" - results = yvs.get_result_list('jesus') - self.assertEqual(len(results), 0) - - def test_empty(self): - """should not match empty input""" - results = yvs.get_result_list('') - self.assertEqual(len(results), 0) - -if __name__ == '__main__': - unittest.main() +def test_partial(): + """should match books by partial name""" + results = yvs.get_result_list('luk') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Luke') + + +def test_case(): + """should match books irrespective of case""" + query_str = 'Matthew' + results = yvs.get_result_list(query_str) + results_lower = yvs.get_result_list(query_str.lower()) + results_upper = yvs.get_result_list(query_str.upper()) + nose.assert_equal(len(results), 1) + nose.assert_list_equal(results_lower, results) + nose.assert_list_equal(results_upper, results) + + +def test_whitespace(): + """should match books irrespective of surrounding whitespace""" + results = yvs.get_result_list(' romans ') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Romans') + + +def test_partial_ambiguous(): + """should match books by ambiguous partial name""" + results = yvs.get_result_list('r') + nose.assert_equal(len(results), 3) + nose.assert_equal(results[0]['title'], 'Ruth') + nose.assert_equal(results[1]['title'], 'Romans') + nose.assert_equal(results[2]['title'], 'Revelation') + + +def test_multiple_words(): + """should match books with names comprised of multiple words""" + results = yvs.get_result_list('song of songs') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Song of Songs') + + +def test_numbered_partial(): + """should match numbered books by partial numbered name""" + results = yvs.get_result_list('1 cor') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians') + + +def test_numbered_whitespace(): + """should match numbered books irrespective of extra whitespace""" + results = yvs.get_result_list('1 cor') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians') + + +def test_nonnumbered_partial(): + """should match numbered books by partial non-numbered name""" + results = yvs.get_result_list('john') + nose.assert_equal(len(results), 4) + nose.assert_equal(results[0]['title'], 'John') + nose.assert_equal(results[1]['title'], '1 John') + nose.assert_equal(results[2]['title'], '2 John') + nose.assert_equal(results[3]['title'], '3 John') + + +def test_id(): + """should use correct ID for books""" + results = yvs.get_result_list('philippians') + nose.assert_equal(results[0]['uid'], 'niv/php.1') + + +def test_nonexistent(): + """should not match nonexistent books""" + results = yvs.get_result_list('jesus') + nose.assert_equal(len(results), 0) + + +def test_empty(): + """should not match empty input""" + results = yvs.get_result_list('') + nose.assert_equal(len(results), 0) diff --git a/tests/test_search_chapter.py b/tests/test_search_chapter.py index 850259d..ac91dce 100644 --- a/tests/test_search_chapter.py +++ b/tests/test_search_chapter.py @@ -1,40 +1,38 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs -class SearchChapterTestCase(unittest.TestCase): - """test the searching of Bible chapters""" - - def test_basic(self): - """should match chapters""" - results = yvs.get_result_list('matthew 5') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Matthew 5') - - def test_ambiguous(self): - """should match chapters by ambiguous book name""" - results = yvs.get_result_list('a 3') - self.assertEqual(len(results), 2) - self.assertEqual(results[0]['title'], 'Amos 3') - self.assertEqual(results[1]['title'], 'Acts 3') - - def test_whitespace(self): - """should match chapters irrespective of surrounding whitespace""" - results = yvs.get_result_list('1 peter 5') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], '1 Peter 5') - - def test_id(self): - """should use correct ID for chapters""" - results = yvs.get_result_list('luke 4') - self.assertEqual(results[0]['uid'], 'niv/luk.4') - - def test_nonexistent(self): - """should not match nonexistent chapters""" - results = yvs.get_result_list('psalm 160') - self.assertEqual(len(results), 0) - -if __name__ == '__main__': - unittest.main() +def test_basic(): + """should match chapters""" + results = yvs.get_result_list('matthew 5') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Matthew 5') + + +def test_ambiguous(): + """should match chapters by ambiguous book name""" + results = yvs.get_result_list('a 3') + nose.assert_equal(len(results), 2) + nose.assert_equal(results[0]['title'], 'Amos 3') + nose.assert_equal(results[1]['title'], 'Acts 3') + + +def test_whitespace(): + """should match chapters irrespective of surrounding whitespace""" + results = yvs.get_result_list('1 peter 5') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Peter 5') + + +def test_id(): + """should use correct ID for chapters""" + results = yvs.get_result_list('luke 4') + nose.assert_equal(results[0]['uid'], 'niv/luk.4') + + +def test_nonexistent(): + """should not match nonexistent chapters""" + results = yvs.get_result_list('psalm 160') + nose.assert_equal(len(results), 0) diff --git a/tests/test_search_incomplete.py b/tests/test_search_incomplete.py index 09c50b0..e4bf7db 100644 --- a/tests/test_search_incomplete.py +++ b/tests/test_search_incomplete.py @@ -1,29 +1,25 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs -class SearchIncompleteTestCase(unittest.TestCase): - """test the searching of incomplete Bible references""" +def test_incomplete_verse(): + """should treat incomplete verse reference as chapter reference""" + results = yvs.get_result_list('psalm 19:') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Psalm 19') - def test_incomplete_verse(self): - """should treat incomplete verse reference as chapter reference""" - results = yvs.get_result_list('psalm 19:') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Psalm 19') - def test_incomplete_dot_verse(self): - """should treat incomplete .verse reference as chapter reference""" - results = yvs.get_result_list('psalm 19.') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Psalm 19') +def test_incomplete_dot_verse(): + """should treat incomplete .verse reference as chapter reference""" + results = yvs.get_result_list('psalm 19.') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Psalm 19') - def test_incomplete_verse_range(self): - """should treat incomplete verse ranges as single-verse references""" - results = yvs.get_result_list('psalm 19.7-') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Psalm 19.7') -if __name__ == '__main__': - unittest.main() +def test_incomplete_verse_range(): + """should treat incomplete verse ranges as single-verse references""" + results = yvs.get_result_list('psalm 19.7-') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Psalm 19.7') diff --git a/tests/test_search_main.py b/tests/test_search_main.py index 5c965fd..cd431f1 100644 --- a/tests/test_search_main.py +++ b/tests/test_search_main.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs from xml.etree import ElementTree as ET from contextlib import contextmanager @@ -10,57 +10,54 @@ import os -class SearchMainTestCase(unittest.TestCase): - """test the main search function called by the workflow""" - - @contextmanager - def redirect_stdout(self): - """temporarily redirect stdout to new output stream""" - original_stdout = sys.stdout - out = StringIO() - try: - sys.stdout = out - yield out - finally: - sys.stdout = original_stdout - - def test_output(self): - """should output result list XML""" - query_str = 'genesis 50:20' - with self.redirect_stdout() as out: - yvs.main(query_str) - output = out.getvalue().strip() - results = yvs.get_result_list(query_str) - xml = yvs.get_result_list_xml(results).strip() - self.assertEqual(output, xml) - - def test_null_result(self): - """should output "No Results" XML item for empty result lists""" - query_str = 'nothing' - with self.redirect_stdout() as out: - yvs.main(query_str) - xml = out.getvalue().strip() - root = ET.fromstring(xml) - item = root.find('item') - self.assertIsNotNone(item, ' element is missing') - self.assertEqual(item.get('valid'), 'no') - title = item.find('title') - self.assertEqual(title.text, 'No Results') - - def test_query_param(self): - """should use typed Alfred query as default query string""" - spec = inspect.getargspec(yvs.main) - default_query_str = spec.defaults[0] - self.assertEqual(default_query_str, '{query}') - - def test_source_only(self): - """should run script from source only""" - yvs.sys.argv[0] = yvs.__file__ - del yvs.__file__ - results = yvs.get_result_list('e') - self.assertEqual(len(results), 6) - yvs.__file__ = yvs.sys.argv[0] - - -if __name__ == '__main__': - unittest.main() +@contextmanager +def redirect_stdout(): + """temporarily redirect stdout to new output stream""" + original_stdout = sys.stdout + out = StringIO() + try: + sys.stdout = out + yield out + finally: + sys.stdout = original_stdout + + +def test_output(): + """should output result list XML""" + query_str = 'genesis 50:20' + with redirect_stdout() as out: + yvs.main(query_str) + output = out.getvalue().strip() + results = yvs.get_result_list(query_str) + xml = yvs.get_result_list_xml(results).strip() + nose.assert_equal(output, xml) + + +def test_null_result(): + """should output "No Results" XML item for empty result lists""" + query_str = 'nothing' + with redirect_stdout() as out: + yvs.main(query_str) + xml = out.getvalue().strip() + root = ET.fromstring(xml) + item = root.find('item') + nose.assert_is_not_none(item, ' element is missing') + nose.assert_equal(item.get('valid'), 'no') + title = item.find('title') + nose.assert_equal(title.text, 'No Results') + + +def test_query_param(): + """should use typed Alfred query as default query string""" + spec = inspect.getargspec(yvs.main) + default_query_str = spec.defaults[0] + nose.assert_equal(default_query_str, '{query}') + + +def test_source_only(): + """should run script from source only""" + yvs.sys.argv[0] = yvs.__file__ + del yvs.__file__ + results = yvs.get_result_list('e') + nose.assert_equal(len(results), 6) + yvs.__file__ = yvs.sys.argv[0] diff --git a/tests/test_search_verse.py b/tests/test_search_verse.py index d6c406d..a18fbef 100644 --- a/tests/test_search_verse.py +++ b/tests/test_search_verse.py @@ -1,46 +1,45 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs -class SearchVerseTestCase(unittest.TestCase): - """test the searching of Bible verses""" - - def test_basic(self): - """should match verses""" - results = yvs.get_result_list('luke 4:8') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Luke 4:8') - - def test_ambiguous(self): - """should match verses by ambiguous book reference""" - results = yvs.get_result_list('a 3:2') - self.assertEqual(len(results), 2) - self.assertEqual(results[0]['title'], 'Amos 3:2') - self.assertEqual(results[1]['title'], 'Acts 3:2') - - def test_dot(self): - """should match verses preceded by dot (instead of colon)""" - results = yvs.get_result_list('luke 4.8') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], 'Luke 4.8') - - def test_range(self): - """should match verse ranges""" - results = yvs.get_result_list('1 cor 13.4-7') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['title'], '1 Corinthians 13.4-7') - - def test_id(self): - """should use correct ID for verses""" - results = yvs.get_result_list('luke 4:8') - self.assertEqual(results[0]['uid'], 'niv/luk.4.8') - - def test_range_id(self): - """should use correct ID for verse ranges""" - results = yvs.get_result_list('1 cor 13.4-7') - self.assertEqual(results[0]['uid'], 'niv/1co.13.4-7') - -if __name__ == '__main__': - unittest.main() +def test_basic(): + """should match verses""" + results = yvs.get_result_list('luke 4:8') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Luke 4:8') + + +def test_ambiguous(): + """should match verses by ambiguous book reference""" + results = yvs.get_result_list('a 3:2') + nose.assert_equal(len(results), 2) + nose.assert_equal(results[0]['title'], 'Amos 3:2') + nose.assert_equal(results[1]['title'], 'Acts 3:2') + + +def test_dot(): + """should match verses preceded by dot (instead of colon)""" + results = yvs.get_result_list('luke 4.8') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Luke 4.8') + + +def test_range(): + """should match verse ranges""" + results = yvs.get_result_list('1 cor 13.4-7') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians 13.4-7') + + +def test_id(): + """should use correct ID for verses""" + results = yvs.get_result_list('luke 4:8') + nose.assert_equal(results[0]['uid'], 'niv/luk.4.8') + + +def test_range_id(): + """should use correct ID for verse ranges""" + results = yvs.get_result_list('1 cor 13.4-7') + nose.assert_equal(results[0]['uid'], 'niv/1co.13.4-7') diff --git a/tests/test_search_version.py b/tests/test_search_version.py index 062ca50..677ab60 100644 --- a/tests/test_search_version.py +++ b/tests/test_search_version.py @@ -1,50 +1,49 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs -class SearchVersionTestCase(unittest.TestCase): - """test the searching of Bible references in a specific version""" - - def test_numbered(self): - """should match versions ending in number by partial name""" - results = yvs.get_result_list('luke 4:8 rv1') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['subtitle'], 'RV1885') - - def test_case(self): - """should match versions irrespective of case""" - query = 'e 4:8 esv' - results = yvs.get_result_list(query) - results_lower = yvs.get_result_list(query.lower()) - results_upper = yvs.get_result_list(query.upper()) - self.assertEqual(len(results), 6) - self.assertListEqual(results_lower, results) - self.assertListEqual(results_upper, results) - - def test_whitespace(self): - """should match versions irrespective of surrounding whitespace""" - results = yvs.get_result_list('1 peter 5:7 esv') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['subtitle'], 'ESV') - - def test_partial(self): - """should match versions by partial name""" - results = yvs.get_result_list('luke 4:8 e') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['subtitle'], 'ESV') - - def test_partial_ambiguous(self): - """should match versions by ambiguous partial name""" - results = yvs.get_result_list('luke 4:8 a') - self.assertEqual(len(results), 1) - self.assertEqual(results[0]['subtitle'], 'AMP') - - def test_id(self): - """should use correct ID for versions""" - results = yvs.get_result_list('malachi 3:2 esv') - self.assertEqual(results[0]['uid'], 'esv/mal.3.2') - -if __name__ == '__main__': - unittest.main() +def test_numbered(): + """should match versions ending in number by partial name""" + results = yvs.get_result_list('luke 4:8 rv1') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['subtitle'], 'RV1885') + + +def test_case(): + """should match versions irrespective of case""" + query = 'e 4:8 esv' + results = yvs.get_result_list(query) + results_lower = yvs.get_result_list(query.lower()) + results_upper = yvs.get_result_list(query.upper()) + nose.assert_equal(len(results), 6) + nose.assert_list_equal(results_lower, results) + nose.assert_list_equal(results_upper, results) + + +def test_whitespace(): + """should match versions irrespective of surrounding whitespace""" + results = yvs.get_result_list('1 peter 5:7 esv') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['subtitle'], 'ESV') + + +def test_partial(): + """should match versions by partial name""" + results = yvs.get_result_list('luke 4:8 e') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['subtitle'], 'ESV') + + +def test_partial_ambiguous(): + """should match versions by ambiguous partial name""" + results = yvs.get_result_list('luke 4:8 a') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['subtitle'], 'AMP') + + +def test_id(): + """should use correct ID for versions""" + results = yvs.get_result_list('malachi 3:2 esv') + nose.assert_equal(results[0]['uid'], 'esv/mal.3.2') diff --git a/tests/test_search_xml.py b/tests/test_search_xml.py index 3252073..458ce80 100644 --- a/tests/test_search_xml.py +++ b/tests/test_search_xml.py @@ -1,44 +1,39 @@ #!/usr/bin/env python -import unittest +import nose.tools as nose import yv_suggest.search as yvs from xml.etree import ElementTree as ET -class SearchXmlTestCase(unittest.TestCase): - """test the integrity of the result list XML""" +def test_validity(): + """should return syntactically-valid XML""" + results = yvs.get_result_list('john 3:16') + xml = yvs.get_result_list_xml(results) + try: + nose.assert_is_instance(ET.fromstring(xml), ET.Element) + except ET.ParseError: + ().fail('result list XML is not valid') - def test_validity(self): - """should be valid XML""" - results = yvs.get_result_list('john 3:16') - xml = yvs.get_result_list_xml(results) - try: - self.assertIsInstance(ET.fromstring(xml), ET.Element) - except ET.ParseError: - self.fail('result list XML is not valid') - def test_structure(self): - """should contain necessary elements/attributes/values""" - results = yvs.get_result_list('matthew 6:34') - result = results[0] - xml = yvs.get_result_list_xml(results) - root = ET.fromstring(xml) - self.assertEqual(root.tag, 'items', - 'root element must be named ') - item = root.find('item') - self.assertIsNotNone(item, ' element is missing') - self.assertEqual(item.get('uid'), result['uid']) - self.assertEqual(item.get('arg'), result['arg']) - self.assertEqual(item.get('valid'), 'yes') - title = item.find('title') - self.assertIsNotNone(title, ' element is missing') - self.assertEqual(title.text, result['title']) - subtitle = item.find('subtitle') - self.assertIsNotNone(subtitle, '<subtitle> element is missing') - self.assertEqual(subtitle.text, result['subtitle']) - icon = item.find('icon') - self.assertIsNotNone(icon, '<icon> element is missing') - self.assertEqual(icon.text, 'icon.png') - -if __name__ == '__main__': - unittest.main() +def test_structure(): + """XML should match result list""" + results = yvs.get_result_list('matthew 6:34') + result = results[0] + xml = yvs.get_result_list_xml(results) + root = ET.fromstring(xml) + nose.assert_equal(root.tag, 'items', + 'root element must be named <items>') + item = root.find('item') + nose.assert_is_not_none(item, '<item> element is missing') + nose.assert_equal(item.get('uid'), result['uid']) + nose.assert_equal(item.get('arg'), result['arg']) + nose.assert_equal(item.get('valid'), 'yes') + title = item.find('title') + nose.assert_is_not_none(title, '<title> element is missing') + nose.assert_equal(title.text, result['title']) + subtitle = item.find('subtitle') + nose.assert_is_not_none(subtitle, '<subtitle> element is missing') + nose.assert_equal(subtitle.text, result['subtitle']) + icon = item.find('icon') + nose.assert_is_not_none(icon, '<icon> element is missing') + nose.assert_equal(icon.text, 'icon.png') From c60b88c3c5599e11adffa9532dc3461cca5fd617 Mon Sep 17 00:00:00 2001 From: Caleb Evans <caleb@calebevans.me> Date: Sun, 15 Feb 2015 23:03:07 -0800 Subject: [PATCH 3/6] Add support for shorthand notation --- .editorconfig | 6 +++--- README.md | 4 +++- YouVersion Suggest.alfredworkflow | Bin 23930 -> 23970 bytes tests/test_pep8.py | 2 +- tests/test_shorthand.py | 27 +++++++++++++++++++++++++++ yv_suggest/search.py | 27 +++++++++++++++------------ 6 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 tests/test_shorthand.py diff --git a/.editorconfig b/.editorconfig index 9329484..e61c96e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,8 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 -[*.md] -trim_trailing_whitespace = false - [*.json] indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md index 9286148..a3a69fc 100644 --- a/README.md +++ b/README.md @@ -50,5 +50,7 @@ be installed. If you do not have these packages installed already, you can install them via `pip`: ``` -pip install nose coverage pep8 +pip install nose +pip install coverage +pip install pep8 ``` diff --git a/YouVersion Suggest.alfredworkflow b/YouVersion Suggest.alfredworkflow index fdc04b92da382ecc067f991b22522684f7d57d4d..977082579135fbb4e87c7d25d0e708f0b73aa724 100644 GIT binary patch delta 3592 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 OlT=+G2J>400000}PTB(i delta 3544 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 S11;>^lYCtu2FqIj0000_V8rSG diff --git a/tests/test_pep8.py b/tests/test_pep8.py index 8fdb9a8..d3228ba 100644 --- a/tests/test_pep8.py +++ b/tests/test_pep8.py @@ -11,5 +11,5 @@ def test_source_compliance(): files = glob.iglob('*/*.py') for file in files: result = style_guide.check_files((file,)) - msg = '{file} is not pep8-compliant'.format(file=file) + msg = '{} is not pep8-compliant'.format(file) yield nose.assert_equal, result.total_errors, 0, msg diff --git a/tests/test_shorthand.py b/tests/test_shorthand.py new file mode 100644 index 0000000..a3c6c99 --- /dev/null +++ b/tests/test_shorthand.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import nose.tools as nose +import yv_suggest.search as yvs + + +def test_book(): + """should recognize shorthand book syntax""" + results = yvs.get_result_list('1c') + nose.assert_equal(len(results), 2) + nose.assert_equal(results[0]['title'], '1 Chronicles') + nose.assert_equal(results[1]['title'], '1 Corinthians') + + +def test_chapter(): + """should recognize shorthand chapter syntax""" + results = yvs.get_result_list('1co13') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians 13') + + +def test_version(): + """should recognize shorthand version syntax""" + results = yvs.get_result_list('1co13esv') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians 13') + nose.assert_equal(results[0]['subtitle'], 'ESV') diff --git a/yv_suggest/search.py b/yv_suggest/search.py index a7c13b9..61d0338 100644 --- a/yv_suggest/search.py +++ b/yv_suggest/search.py @@ -3,7 +3,6 @@ import json import re -import os import os.path import sys from xml.etree import ElementTree as ET @@ -44,7 +43,7 @@ def get_versions(): default_version = 'NIV' # Pattern for parsing any bible reference -ref_patt = '^{book}(?: {ch}(?:{sep}{v}{v_end}?)?{version}?)?$'.format( +ref_patt = '^{book}(?:{ch}(?:{sep}{v}{v_end}?)?{version}?)?$'.format( # Book name (including preceding number, if amu) book='((?:\d )?[a-z ]+)', # Chapter number @@ -87,19 +86,18 @@ def get_result_list_xml(results): for result in results: # Create <item> element for result with appropriate attributes - item = ET.Element('item') - item.set('uid', result['uid']) - item.set('arg', result.get('arg', '')) - item.set('valid', result.get('valid', 'yes')) - root.append(item) + item = ET.SubElement(root, 'item', { + 'uid': result['uid'], + 'arg': result.get('arg', ''), + 'valid': result.get('valid', 'yes') + }) # Create appropriate child elements of <item> element - title = ET.Element('title') + title = ET.SubElement(item, 'title') title.text = result['title'] - subtitle = ET.Element('subtitle') + subtitle = ET.SubElement(item, 'subtitle') subtitle.text = result['subtitle'] - icon = ET.Element('icon') + icon = ET.SubElement(item, 'icon') icon.text = 'icon.png' - item.extend((title, subtitle, icon)) return ET.tostring(root) @@ -115,6 +113,11 @@ def format_query_str(query_str): # Remove tokens at end of incomplete references query_str = re.sub('[\-\.\:]$', '', query_str) + # Parse shorthand book name notation + query_str = re.sub('^(\d)(?=[a-z])', '\\1 ', query_str) + # Parse shorthand version notation + query_str = re.sub('(?<=\d)([a-z]+\d*)$', ' \\1', query_str) + return query_str @@ -131,7 +134,7 @@ def get_query_object(query_str): query = {} # Parse partial book name if given - query['book'] = ref_matches.group(1) + query['book'] = ref_matches.group(1).rstrip() # Parse chapter if given if ref_matches.group(2): From fbfe0235433f9f9d2b861c4333bee798c017f58b Mon Sep 17 00:00:00 2001 From: Caleb Evans <caleb@calebevans.me> Date: Mon, 16 Feb 2015 11:54:09 -0800 Subject: [PATCH 4/6] Improve various sections of tests and codebase --- tests/test_pep8.py | 6 +-- tests/test_search_book.py | 6 --- tests/test_search_incomplete.py | 2 +- tests/test_search_invalid.py | 22 ++++++++ ..._shorthand.py => test_search_shorthand.py} | 11 ++-- tests/test_search_verse.py | 15 ++++-- yv_suggest/search.py | 53 +++++++++---------- 7 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 tests/test_search_invalid.py rename tests/{test_shorthand.py => test_search_shorthand.py} (65%) diff --git a/tests/test_pep8.py b/tests/test_pep8.py index d3228ba..031d1bd 100644 --- a/tests/test_pep8.py +++ b/tests/test_pep8.py @@ -7,9 +7,9 @@ def test_source_compliance(): """source files should comply with pep8""" - style_guide = pep8.StyleGuide(quiet=True) files = glob.iglob('*/*.py') for file in files: - result = style_guide.check_files((file,)) + style_guide = pep8.StyleGuide(quiet=True) + total_errors = style_guide.input_file(file) msg = '{} is not pep8-compliant'.format(file) - yield nose.assert_equal, result.total_errors, 0, msg + yield nose.assert_equal, total_errors, 0, msg diff --git a/tests/test_search_book.py b/tests/test_search_book.py index feab3b9..456085a 100644 --- a/tests/test_search_book.py +++ b/tests/test_search_book.py @@ -79,9 +79,3 @@ def test_nonexistent(): """should not match nonexistent books""" results = yvs.get_result_list('jesus') nose.assert_equal(len(results), 0) - - -def test_empty(): - """should not match empty input""" - results = yvs.get_result_list('') - nose.assert_equal(len(results), 0) diff --git a/tests/test_search_incomplete.py b/tests/test_search_incomplete.py index e4bf7db..1f691f2 100644 --- a/tests/test_search_incomplete.py +++ b/tests/test_search_incomplete.py @@ -22,4 +22,4 @@ def test_incomplete_verse_range(): """should treat incomplete verse ranges as single-verse references""" results = yvs.get_result_list('psalm 19.7-') nose.assert_equal(len(results), 1) - nose.assert_equal(results[0]['title'], 'Psalm 19.7') + nose.assert_equal(results[0]['title'], 'Psalm 19:7') diff --git a/tests/test_search_invalid.py b/tests/test_search_invalid.py new file mode 100644 index 0000000..d99aaf4 --- /dev/null +++ b/tests/test_search_invalid.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import nose.tools as nose +import yv_suggest.search as yvs + + +def test_empty(): + """should not match empty input""" + results = yvs.get_result_list('') + nose.assert_equal(len(results), 0) + + +def test_invalid(): + """should not match invalid input""" + results = yvs.get_result_list('!!!') + nose.assert_equal(len(results), 0) + + +def test_invalid_xml(): + """should not match input containing XML reserved characters""" + results = yvs.get_result_list('<input>') + nose.assert_equal(len(results), 0) diff --git a/tests/test_shorthand.py b/tests/test_search_shorthand.py similarity index 65% rename from tests/test_shorthand.py rename to tests/test_search_shorthand.py index a3c6c99..0f2e449 100644 --- a/tests/test_shorthand.py +++ b/tests/test_search_shorthand.py @@ -6,22 +6,21 @@ def test_book(): """should recognize shorthand book syntax""" - results = yvs.get_result_list('1c') - nose.assert_equal(len(results), 2) - nose.assert_equal(results[0]['title'], '1 Chronicles') - nose.assert_equal(results[1]['title'], '1 Corinthians') + results = yvs.get_result_list('1co') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], '1 Corinthians') def test_chapter(): """should recognize shorthand chapter syntax""" - results = yvs.get_result_list('1co13') + results = yvs.get_result_list('1 co13') nose.assert_equal(len(results), 1) nose.assert_equal(results[0]['title'], '1 Corinthians 13') def test_version(): """should recognize shorthand version syntax""" - results = yvs.get_result_list('1co13esv') + results = yvs.get_result_list('1 co 13esv') nose.assert_equal(len(results), 1) nose.assert_equal(results[0]['title'], '1 Corinthians 13') nose.assert_equal(results[0]['subtitle'], 'ESV') diff --git a/tests/test_search_verse.py b/tests/test_search_verse.py index a18fbef..e8b2901 100644 --- a/tests/test_search_verse.py +++ b/tests/test_search_verse.py @@ -19,18 +19,25 @@ def test_ambiguous(): nose.assert_equal(results[1]['title'], 'Acts 3:2') -def test_dot(): - """should match verses preceded by dot (instead of colon)""" +def test_dot_separator(): + """should match verses preceded by dot""" results = yvs.get_result_list('luke 4.8') nose.assert_equal(len(results), 1) - nose.assert_equal(results[0]['title'], 'Luke 4.8') + nose.assert_equal(results[0]['title'], 'Luke 4:8') + + +def test_space_separator(): + """should match verses preceded by space""" + results = yvs.get_result_list('luke 4 8') + nose.assert_equal(len(results), 1) + nose.assert_equal(results[0]['title'], 'Luke 4:8') def test_range(): """should match verse ranges""" results = yvs.get_result_list('1 cor 13.4-7') nose.assert_equal(len(results), 1) - nose.assert_equal(results[0]['title'], '1 Corinthians 13.4-7') + nose.assert_equal(results[0]['title'], '1 Corinthians 13:4-7') def test_id(): diff --git a/yv_suggest/search.py b/yv_suggest/search.py index 61d0338..d956cbb 100644 --- a/yv_suggest/search.py +++ b/yv_suggest/search.py @@ -49,7 +49,7 @@ def get_versions(): # Chapter number ch='(\d+)', # Chapter-verse separator - sep='(\:|\.)', + sep='(?:[\:\. ])', # Verse number v='(\d+)', # End verse for a verse range @@ -69,7 +69,7 @@ def guess_version(partial_version): version_guess = partial_version else: # Use a predetermined version by default - version_guess = default_version + version_guess = None # Attempt to guess the version used for version in versions: if version.startswith(partial_version): # pragma: no cover @@ -113,10 +113,8 @@ def format_query_str(query_str): # Remove tokens at end of incomplete references query_str = re.sub('[\-\.\:]$', '', query_str) - # Parse shorthand book name notation - query_str = re.sub('^(\d)(?=[a-z])', '\\1 ', query_str) - # Parse shorthand version notation - query_str = re.sub('(?<=\d)([a-z]+\d*)$', ' \\1', query_str) + # Parse shorthand book name and chapter/verse notation + query_str = re.sub('(\d)(?=[a-z])', '\\1 ', query_str) return query_str @@ -137,24 +135,24 @@ def get_query_object(query_str): query['book'] = ref_matches.group(1).rstrip() # Parse chapter if given - if ref_matches.group(2): - query['chapter'] = int(ref_matches.group(2)) - - # Store separator used to separate chapter from verse number - if ref_matches.group(3): - query['separator'] = ref_matches.group(3) + chapter_match = ref_matches.group(2) + if chapter_match: + query['chapter'] = int(chapter_match) # Parse verse if given - if ref_matches.group(4): - query['verse'] = int(ref_matches.group(4)) + verse_match = ref_matches.group(3) + if verse_match: + query['verse'] = int(verse_match) # Parse verse range if given - if ref_matches.group(5): - query['verse_end'] = int(ref_matches.group(5)) + verse_range_match = ref_matches.group(4) + if verse_range_match: + query['verse_end'] = int(verse_range_match) # Parse version if given - if ref_matches.group(6): - query['version'] = ref_matches.group(6).lstrip() + version_match = ref_matches.group(5) + if version_match: + query['version'] = version_match.lstrip() return query @@ -188,13 +186,15 @@ def get_result_list(query_str): # Filter book list to match query matching_books = get_matching_books(query) + version_guess = None if 'version' in query: # Guess version if possible - matched_version = guess_version(query['version']) - else: - # Otherwise, use default version - matched_version = default_version + version_guess = guess_version(query['version']) + + if not version_guess: + # Use default version if version could not be guessed + version_guess = default_version # Build results list from books that matched the query for book in matching_books: @@ -220,9 +220,8 @@ def get_result_list(query_str): # Find verse if given result['uid'] += '.{verse}'.format( verse=query['verse']) - result['title'] += '{sep}{verse}'.format( - verse=query['verse'], - sep=query['separator']) + result['title'] += ':{verse}'.format( + verse=query['verse']) if 'verse_end' in query: @@ -241,10 +240,10 @@ def get_result_list(query_str): if 'uid' in result: result['uid'] = '{version}/{uid}'.format( - version=matched_version.lower(), + version=version_guess.lower(), uid=result['uid']) result['arg'] = result['uid'] - result['subtitle'] = matched_version + result['subtitle'] = version_guess results.append(result) return results From 7e4e4aee93e70151f156518b8483ab70f616f4ac Mon Sep 17 00:00:00 2001 From: Caleb Evans <caleb@calebevans.me> Date: Mon, 16 Feb 2015 14:38:58 -0800 Subject: [PATCH 5/6] Update README documentation; tweak project files --- .coveragerc | 2 +- .gitignore | 11 ++++++----- README.md | 37 +++++++++++++++++++++---------------- yv_suggest/search.py | 3 +-- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0d20cb7..f552851 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ # Configuration for coverage.py (https://pypi.python.org/pypi/coverage) -# Enable branch coverage [run] +# Enable branch coverage branch = True [report] diff --git a/.gitignore b/.gitignore index 0c1bc0b..9ceac38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ -# OS-specific files +# Files generated by OS .DS_Store # Python bytecode -*.pyc -__pycache__ +__pycache__/ +*.py[cod] -# Test coverage +# Unit test / coverage reports +cover/ .coverage coverage.xml -cover +nosetests.xml diff --git a/README.md b/README.md index a3a69fc..7ce07d8 100644 --- a/README.md +++ b/README.md @@ -20,37 +20,42 @@ suggestions matching your query. * `luke` => Luke * `eph 3` => Ephesians 3 -* `1 t 3 e` => 1 Thessalonians 3 (ESV), 1 Timothy 3 (ESV) +* `1t3e` => 1 Thessalonians 3 (ESV), 1 Timothy 3 (ESV) * `mat 6:34 nlt` => Matthew 6:34 (NLT) * `1 co 13.4-7` => 1 Corinthians 13.4-7 ## Testing -If you are contributing to the project and would like to run the included unit -tests, run the `nosetests` command within the project directory. +### Requirements for running tests + +Running these unit tests requires Python 2.7, as well as the following packages +to be installed: + +* nose +* coverage +* pep8 + +If you do not have these packages installed already, you can install them via +`pip`: ``` -nosetests +sudo pip install nose coverage pep8 ``` -### Viewing test coverage +### Running tests -To view the test coverage report, run `nosetests` with the `--with-coverage` and -`--cover-erase` options. +To run the included unit tests, run the `nosetests` command within the project +directory. ``` -nosetests --with-coverage --cover-erase +nosetests ``` -### Requirements for running tests +### Viewing test coverage -Note that all scripts apart of the workflow require Python 2.7 (Python 3 is not -supported). Running these unit tests requires `nose`, `coverage`, and `pep8` to -be installed. If you do not have these packages installed already, you can -install them via `pip`: +To view the test coverage report, run `nosetests` with the `--with-coverage` and +`--cover-erase` flags. ``` -pip install nose -pip install coverage -pip install pep8 +nosetests --with-coverage --cover-erase ``` diff --git a/yv_suggest/search.py b/yv_suggest/search.py index d956cbb..eda6c65 100644 --- a/yv_suggest/search.py +++ b/yv_suggest/search.py @@ -68,9 +68,8 @@ def guess_version(partial_version): if partial_version in versions: version_guess = partial_version else: - # Use a predetermined version by default - version_guess = None # Attempt to guess the version used + version_guess = None for version in versions: if version.startswith(partial_version): # pragma: no cover version_guess = version From f22b5e0a005c39ba092715682ff43911658e1e0c Mon Sep 17 00:00:00 2001 From: Caleb Evans <caleb@calebevans.me> Date: Mon, 16 Feb 2015 14:40:24 -0800 Subject: [PATCH 6/6] Update workflow file in preparation for v1.5 --- YouVersion Suggest.alfredworkflow | Bin 23970 -> 23936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/YouVersion Suggest.alfredworkflow b/YouVersion Suggest.alfredworkflow index 977082579135fbb4e87c7d25d0e708f0b73aa724..ae356b86cc95383a1a5e5c271c50af1e41dd3096 100644 GIT binary patch delta 3607 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 delta 3642 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 M0RRB=TL1t60H7x0e*gdg