5
0

Adds better method control, and overall stability. Adds valid_reference and valid_translation methods.

This commit is contained in:
Llewellyn van der Merwe 2023-11-25 14:13:51 +02:00
parent 07f1c95d83
commit 8901087217
Signed by: Llewellyn
GPG Key ID: A9201372263741E7
7 changed files with 405 additions and 171 deletions

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name="getbible",
version="1.0.2",
version="1.0.3",
author="Llewellyn van der Merwe",
author_email="getbible@vdm.io",
description="A Python package to retrieving Bible references with ease.",

View File

@ -1,14 +1,16 @@
import os
import re
import json
import requests
import threading
import time
from datetime import datetime, timedelta
from typing import Dict, Optional, Union
from getbible import GetBibleReference
class GetBible:
def __init__(self, repo_path="https://api.getbible.net", version='v2'):
def __init__(self, repo_path: str = "https://api.getbible.net", version: str = 'v2') -> None:
"""
Initialize the GetBible class.
@ -24,49 +26,11 @@ class GetBible:
self.__books_cache = {}
self.__chapters_cache = {}
self.__start_cache_reset_thread()
self.__pattern = re.compile(r'^[a-zA-Z0-9]{1,30}$')
# Determine if the repository path is a URL
self.__repo_path_url = self.__repo_path.startswith("http://") or self.__repo_path.startswith("https://")
def __start_cache_reset_thread(self):
"""
Start a background thread to reset the cache monthly.
This method creates and starts a daemon thread that runs the cache reset function
every month.
"""
reset_thread = threading.Thread(target=self.__reset_cache_monthly)
reset_thread.daemon = True # Daemonize thread
reset_thread.start()
def __reset_cache_monthly(self):
"""
Periodically clears the cache on the first day of each month.
This method runs in a background thread and calculates the time until the start
of the next month. It sleeps until that time and then clears the cache.
"""
while True:
time_to_sleep = self.__calculate_time_until_next_month()
time.sleep(time_to_sleep)
self.__chapters_cache.clear()
print(f"Cache cleared on {datetime.now()}")
def __calculate_time_until_next_month(self):
"""
Calculate the seconds until the start of the next month.
Determines how many seconds are left until the first day of the next month
from the current time. This duration is used by the cache reset thread to
sleep until the cache needs to be cleared.
:return: Number of seconds until the start of the next month.
"""
now = datetime.now()
# Calculate the first day of the next month
first_of_next_month = (now.replace(day=1) + timedelta(days=32)).replace(day=1)
return (first_of_next_month - now).total_seconds()
def select(self, reference, abbreviation='kjv'):
def select(self, reference: str, abbreviation: Optional[str] = 'kjv') -> Dict[str, Union[Dict, str]]:
"""
Select and return Bible verses based on the reference and abbreviation.
@ -81,13 +45,13 @@ class GetBible:
try:
reference = self.__get.ref(ref, abbreviation)
except ValueError:
raise ValueError(f"Invalid reference format.")
raise ValueError(f"Invalid reference '{ref}'.")
self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result)
return result
def scripture(self, reference, abbreviation='kjv'):
def scripture(self, reference: str, abbreviation: Optional[str] = 'kjv') -> str:
"""
Select and return Bible verses based on the reference and abbreviation.
@ -98,7 +62,72 @@ class GetBible:
return json.dumps(self.select(reference, abbreviation))
def __set_verse(self, abbreviation, book, chapter, verses, result):
def valid_reference(self, reference: str, abbreviation: Optional[str] = 'kjv') -> bool:
"""
Validate a scripture reference and check its presence in the cache.
:param reference: Scripture reference string.
:param abbreviation: Optional translation code.
:return: True if valid and present, False otherwise.
"""
return self.__get.valid(reference, abbreviation)
def valid_translation(self, abbreviation: str) -> bool:
"""
Check if the given translation is valid.
:param abbreviation: The abbreviation of the Bible translation to check.
:return: True if the translation is available, False otherwise.
"""
if self.__pattern.match(abbreviation):
path = self.__generate_path(abbreviation, "books.json")
# Check if the translation is already in the cache
if abbreviation not in self.__books_cache:
self.__books_cache[abbreviation] = self.__fetch_data(path)
# Return True if the translation is available, False otherwise
return self.__books_cache[abbreviation] is not None
return False
def __start_cache_reset_thread(self) -> None:
"""
Start a background thread to reset the cache monthly.
This method creates and starts a daemon thread that runs the cache reset function
every month.
"""
reset_thread = threading.Thread(target=self.__reset_cache_monthly)
reset_thread.daemon = True # Daemonize thread
reset_thread.start()
def __reset_cache_monthly(self) -> None:
"""
Periodically clears the cache on the first day of each month.
This method runs in a background thread and calculates the time until the start
of the next month. It sleeps until that time and then clears the cache.
"""
while True:
time_to_sleep = self.__calculate_time_until_next_month()
time.sleep(time_to_sleep)
self.__chapters_cache.clear()
print(f"Cache cleared on {datetime.now()}")
def __calculate_time_until_next_month(self) -> float:
"""
Calculate the seconds until the start of the next month.
Determines how many seconds are left until the first day of the next month
from the current time. This duration is used by the cache reset thread to
sleep until the cache needs to be cleared.
:return: Number of seconds until the start of the next month.
"""
now = datetime.now()
# Calculate the first day of the next month
first_of_next_month = (now.replace(day=1) + timedelta(days=32)).replace(day=1)
return (first_of_next_month - now).total_seconds()
def __set_verse(self, abbreviation: str, book: int, chapter: int, verses: list, result: Dict) -> None:
"""
Set verse information into the result JSON.
:param abbreviation: Bible translation abbreviation.
@ -131,20 +160,18 @@ class GetBible:
result[cache_key] = {key: chapter_data[key] for key in chapter_data if key != "verses"}
result[cache_key]["verses"] = [verse_info]
def __check_translation(self, abbreviation):
def __check_translation(self, abbreviation: str) -> None:
"""
Check if the given translation is available.
Check if the given translation is available and raises an exception if not found.
:param abbreviation: The abbreviation of the Bible translation to check.
:raises FileNotFoundError: If the translation is not found.
"""
path = self.__generate_path(abbreviation, "books.json")
if abbreviation not in self.__books_cache:
self.__books_cache[abbreviation] = self.__fetch_data(path)
if self.__books_cache[abbreviation] is None:
raise FileNotFoundError(f"Translation ({abbreviation}) not found in this API.")
# Use valid_translation to check if the translation is available
if not self.valid_translation(abbreviation):
raise FileNotFoundError(f"Translation ({abbreviation}) not found in this API.")
def __generate_path(self, abbreviation, file_name):
def __generate_path(self, abbreviation: str, file_name: str) -> str:
"""
Generate the path or URL for a given file.
@ -157,7 +184,7 @@ class GetBible:
else:
return os.path.join(self.__repo_path, self.__repo_version, abbreviation, file_name)
def __fetch_data(self, path):
def __fetch_data(self, path: str) -> Optional[Dict]:
"""
Fetch data from either a URL or a local file path.
@ -177,7 +204,7 @@ class GetBible:
else:
return None
def __retrieve_chapter_data(self, abbreviation, book, chapter):
def __retrieve_chapter_data(self, abbreviation: str, book: int, chapter: int) -> Dict:
"""
Retrieve chapter data for a given book and chapter.

View File

@ -1,14 +1,26 @@
from .getbible_reference_trie import GetBibleReferenceTrie
import os
from typing import Any, List, Optional
class GetBibleBookNumber:
def __init__(self):
def __init__(self) -> None:
"""
Initialize the GetBibleBookNumber class.
Sets up the class by loading all translation tries from the data directory.
"""
self._tries = {}
self._data_path = os.path.join(os.path.dirname(__file__), 'data')
self._load_all_translations()
self.__load_all_translations()
def _load_translation(self, filename):
def __load_translation(self, filename: str) -> None:
"""
Load a translation trie from a specified file.
:param filename: The name of the file to load.
:raises IOError: If there is an error loading the file.
"""
trie = GetBibleReferenceTrie()
translation_code = filename.split('.')[0]
try:
@ -17,27 +29,38 @@ class GetBibleBookNumber:
raise IOError(f"Error loading translation {translation_code}: {e}")
self._tries[translation_code] = trie
def _load_all_translations(self):
def __load_all_translations(self) -> None:
"""
Load all translation tries from the data directory.
"""
for filename in os.listdir(self._data_path):
if filename.endswith('.json'):
self._load_translation(filename)
self.__load_translation(filename)
def number(self, reference, translation_code=None, fallback_translations=None):
# Default to 'kjv' if no translation code is provided
def number(self, reference: str, translation_code: Optional[str] = None,
fallback_translations: Optional[List[str]] = None) -> Optional[int]:
"""
Get the book number based on a reference and translation code.
:param reference: The reference to search for.
:param translation_code: The code for the translation to use.
:param fallback_translations: A list of fallback translations to use if necessary.
:return: The book number as an integer if found, None otherwise.
"""
if not translation_code or translation_code not in self._tries:
translation_code = 'kjv'
translation = self._tries.get(translation_code)
result = translation.search(reference) if translation else None
if result:
return result
if result and result.isdigit():
return int(result)
# If 'kjv' is not the original choice, try it next
if translation_code != 'kjv':
translation = self._tries.get('kjv')
result = translation.search(reference) if translation else None
if result:
return result
if result and result.isdigit():
return int(result)
# Fallback to other translations
if fallback_translations is None:
@ -46,12 +69,19 @@ class GetBibleBookNumber:
for code in fallback_translations:
translation = self._tries.get(code)
result = translation.search(reference) if translation else None
if result:
return result
if result and result.isdigit():
return int(result)
return None
def dump(self, translation_code, filename):
def dump(self, translation_code: str, filename: str) -> None:
"""
Dump the trie data for a specific translation to a file.
:param translation_code: The code for the translation.
:param filename: The name of the file to dump to.
:raises ValueError: If no data is available for the specified translation.
"""
if translation_code in self._tries:
self._tries[translation_code].dump(filename)
else:

View File

@ -1,6 +1,7 @@
import re
from getbible import GetBibleBookNumber
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
@ -11,60 +12,156 @@ class BookReference:
class GetBibleReference:
def __init__(self):
self.__get_book = GetBibleBookNumber()
self.__pattern = re.compile(r'^[\w\s,:-]{1,50}$', re.UNICODE)
self.cache = {}
self.cache_limit = 5000
def ref(self, reference, translation_code=None):
# Split at the first colon to separate book from verses, defaulting to chapter 1, verse 1 if not present
book_chapter, verses_portion = reference.split(':', 1) if ':' in reference else (reference, '1')
# Try to extract the chapter number from the book_chapter part
def ref(self, reference: str, translation_code: Optional[str] = None) -> BookReference:
"""
Fetch the BookReference from cache or create it if not present.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: BookReference object.
:raises ValueError: If reference is invalid.
"""
sanitized_ref = self.__sanitize(reference)
if not sanitized_ref:
raise ValueError(f"Invalid reference '{reference}'.")
if sanitized_ref not in self.cache:
book_ref = self.__book_reference(reference, translation_code)
if book_ref is None:
raise ValueError(f"Invalid reference '{reference}'.")
self.__manage_local_cache(sanitized_ref, book_ref)
return self.cache[sanitized_ref]
def valid(self, reference: str, translation_code: Optional[str] = None) -> bool:
"""
Validate a scripture reference and check its presence in the cache.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: True if valid and present, False otherwise.
"""
sanitized_ref = self.__sanitize(reference)
if sanitized_ref is None:
return False
if sanitized_ref not in self.cache:
book_ref = self.__book_reference(reference, translation_code)
self.__manage_local_cache(sanitized_ref, book_ref)
return self.cache[sanitized_ref] is not None
def __sanitize(self, reference: str) -> Optional[str]:
"""
Sanitize a scripture reference by validating and escaping it.
:param reference: The scripture reference to sanitize.
:return: Sanitized reference or None if invalid.
"""
if self.__pattern.match(reference):
return re.escape(reference)
return None
def __book_reference(self, reference: str, translation_code: Optional[str] = None) -> Optional[BookReference]:
"""
Create a BookReference object from a scripture reference.
:param reference: Scripture reference string.
:param translation_code: Optional translation code.
:return: BookReference object or None if invalid.
"""
try:
book_chapter, verses_portion = self.__split_reference(reference)
book_name = self.__extract_book_name(book_chapter)
book_number = self.__get_book_number(book_name, translation_code)
if not book_number:
return None
verses_arr = self.__get_verses_numbers(verses_portion)
chapter_number = self.__extract_chapter(book_chapter)
return BookReference(book=int(book_number), chapter=chapter_number, verses=verses_arr)
except Exception:
return None
def __split_reference(self, reference: str) -> Tuple[str, str]:
"""
Split a scripture reference into book chapter and verses portion.
:param reference: Scripture reference string.
:return: Tuple of book chapter and verses portion.
"""
return reference.split(':', 1) if ':' in reference else (reference, '1')
def __extract_chapter(self, book_chapter: str) -> int:
"""
Extract the chapter number from the book chapter part.
:param book_chapter: Book chapter part of the reference.
:return: Extracted chapter number.
"""
chapter_match = re.search(r'\d+$', book_chapter)
if chapter_match:
# If a chapter number is found, extract it and the book name
chapter_number = int(chapter_match.group())
book_name = book_chapter[:chapter_match.start()].strip()
else:
# If no chapter number is found, default to chapter 1
chapter_number = 1
book_name = book_chapter.strip()
# Retrieve the book number
book_number = self.__get_book_number(book_name, translation_code)
if not book_number:
raise ValueError(f"Book number for '{book_name}' could not be found.")
# Extract verses
verses_arr = self.__get_verses_numbers(verses_portion.strip())
# We return a dataclass (needs Python 3.7+)
return BookReference(book=int(book_number), chapter=chapter_number, verses=verses_arr)
return int(chapter_match.group()) if chapter_match else 1
def __get_verses_numbers(self, verses):
def __extract_book_name(self, book_chapter: str) -> str:
"""
Extract the book name from the book chapter part.
:param book_chapter: Book chapter part of the reference.
:return: Extracted book name.
"""
if book_chapter.isdigit():
# If the entire string is numeric, return it as is
return book_chapter
chapter_match = re.search(r'\d+$', book_chapter)
return book_chapter[:chapter_match.start()].strip() if chapter_match else book_chapter.strip()
def __get_verses_numbers(self, verses: str) -> list:
"""
Convert a verses portion of a reference into a list of verse numbers.
:param verses: Verses portion of the reference.
:return: List of verse numbers.
"""
if not verses:
return [1]
# Process a string of verses into a list
verse_parts = verses.split(',')
verse_list = []
for part in verse_parts:
if '-' in part:
range_parts = part.split('-')
# Ignore if neither start nor end are digits
if len(range_parts) == 2:
start, end = range_parts
if start.isdigit() and end.isdigit():
verse_list.extend(range(int(start), int(end) + 1))
elif start.isdigit():
verse_list.append(int(start))
elif len(range_parts) == 1 and range_parts[0].isdigit():
if all(rp.isdigit() for rp in range_parts):
start, end = sorted(map(int, range_parts))
verse_list.extend(range(start, end + 1))
elif len(range_parts) == 2 and range_parts[0].isdigit() and not range_parts[1]:
verse_list.append(int(range_parts[0]))
elif len(range_parts) == 2 and range_parts[1].isdigit() and not range_parts[0]:
verse_list.append(int(range_parts[1]))
elif part.isdigit():
verse_list.append(int(part))
if not verse_list:
return [1]
return verse_list
return verse_list if verse_list else [1]
def __get_book_number(self, book_name, abbreviation):
# Retrieve the book number given a translation abbreviation and a book name
if re.match(r'^[0-9]+$', book_name):
return book_name
def __get_book_number(self, book_name: str, abbreviation: Optional[str]) -> Optional[int]:
"""
Retrieve the book number given a book name and translation abbreviation.
:param book_name: Name of the book.
:param abbreviation: Translation abbreviation.
:return: Book number or None if not found.
"""
if book_name.isdigit():
return int(book_name)
book_number = self.__get_book.number(book_name, abbreviation)
return book_number
return int(book_number) if book_number is not None else None
def __manage_local_cache(self, key: str, value: Optional[BookReference]):
"""
Manage the insertion and eviction policy for the cache.
:param key: The key to insert into the cache.
:param value: The value to associate with the key.
"""
if len(self.cache) >= self.cache_limit:
self.cache.pop(next(iter(self.cache))) # Evict the oldest cache item
self.cache[key] = value

View File

@ -1,31 +1,52 @@
from .trie_node import TrieNode
import json
import re
from typing import Dict, Optional
class GetBibleReferenceTrie:
def __init__(self):
def __init__(self) -> None:
"""
Initialize the GetBibleReferenceTrie class.
Sets up the Trie data structure for storing and searching book names.
"""
self.root = TrieNode()
# Updated regex to support Unicode characters
self.space_removal_regex = re.compile(r'(\d)\s+(\w)', re.UNICODE)
def _preprocess(self, name):
# Remove all periods
def __preprocess(self, name: str) -> str:
"""
Preprocess a book name by removing periods and spaces between numbers and words.
:param name: The book name to preprocess.
:return: The processed name in lowercase.
"""
processed_name = name.replace('.', '')
# Process the name considering Unicode characters
processed_name = self.space_removal_regex.sub(r'\1\2', processed_name)
return processed_name.lower()
def _insert(self, book_number, names):
def __insert(self, book_number: str, names: [str]) -> None:
"""
Insert a book number with associated names into the Trie.
:param book_number: The book number to insert.
:param names: A list of names associated with the book number.
"""
for name in names:
processed_name = self._preprocess(name)
processed_name = self.__preprocess(name)
node = self.root
for char in processed_name:
node = node.children.setdefault(char, TrieNode())
node.book_number = book_number
def search(self, book_name):
processed_name = self._preprocess(book_name)
def search(self, book_name: str) -> Optional[str]:
"""
Search for a book number based on a book name.
:param book_name: The book name to search for.
:return: The book number if found, None otherwise.
"""
processed_name = self.__preprocess(book_name)
node = self.root
for char in processed_name:
node = node.children.get(char)
@ -33,7 +54,14 @@ class GetBibleReferenceTrie:
return None
return node.book_number if node.book_number else None
def _dump_to_dict(self, node=None, key=''):
def __dump_to_dict(self, node: Optional[TrieNode] = None, key: str = '') -> Dict[str, Dict]:
"""
Convert the Trie into a dictionary representation.
:param node: The current Trie node to process.
:param key: The current key being constructed.
:return: Dictionary representation of the Trie.
"""
if node is None:
node = self.root
@ -42,21 +70,34 @@ class GetBibleReferenceTrie:
result[key] = {'book_number': node.book_number}
for char, child in node.children.items():
result.update(self._dump_to_dict(child, key + char))
result.update(self.__dump_to_dict(child, key + char))
return result
def dump(self, filename):
trie_dict = self._dump_to_dict()
def dump(self, filename: str) -> None:
"""
Dump the Trie data to a JSON file.
:param filename: The filename to dump the data to.
"""
trie_dict = self.__dump_to_dict()
with open(filename, 'w') as file:
json.dump(trie_dict, file, ensure_ascii=False, indent=4)
def load(self, file_path):
def load(self, file_path: str) -> None:
"""
Load the Trie data from a JSON file.
:param file_path: The path of the file to load data from.
:raises IOError: If there is an error opening the file.
:raises ValueError: If there is an error decoding the JSON data.
:raises Exception: If any other error occurs.
"""
try:
with open(file_path, 'r') as file:
data = json.load(file)
for book_number, names in data.items():
self._insert(book_number, names)
self.__insert(book_number, names)
except IOError as e:
raise IOError(f"Error loading file {file_path}: {e}")
except json.JSONDecodeError as e:

View File

@ -8,48 +8,68 @@ class TestGetBibleBookNumber(unittest.TestCase):
self.get_book = GetBibleBookNumber()
def test_valid_reference(self):
self.assertEqual(self.get_book.number('Gen', 'kjv'), '1', "Failed to find 'Gen' in 'kjv' translation")
expected_result = 1
actual_result = self.get_book.number('Gen', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen' in 'kjv' translation")
def test_valid_reference_ch(self):
self.assertEqual(self.get_book.number('创世记', 'cns', ['cnt']), '1',
"Failed to find '创世记' in 'cns' translation with 'cnt' fallback")
self.assertEqual(self.get_book.number('创记', 'cus', ['cut']), '1',
"Failed to find '创记' in 'cus' translation with 'cut' fallback")
expected_result = 1
actual_result = self.get_book.number('创世记', 'cns', ['cnt'])
self.assertEqual(actual_result, expected_result, "Failed to find '创世记' in 'cns' translation with 'cnt' fallback")
def test_valid_reference__ch(self):
expected_result = 1
actual_result = self.get_book.number('创记', 'cus', ['cut'])
self.assertEqual(actual_result, expected_result, "Failed to find '创记' in 'cus' translation with 'cut' fallback")
def test_valid_reference_ch_no_trans(self):
self.assertEqual(self.get_book.number('创世记'), '1', "Failed to find '创世记' in 'none-given' translation")
self.assertEqual(self.get_book.number('创记'), '1', "Failed to find '创记' in 'none-given' translation")
expected_result = 1
actual_result = self.get_book.number('创世记')
self.assertEqual(actual_result, expected_result, "Failed to find '创世记' in 'none-given' translation")
def test_valid_reference__ch_no_trans(self):
expected_result = 1
actual_result = self.get_book.number('创记')
self.assertEqual(actual_result, expected_result, "Failed to find '创记' in 'none-given' translation")
def test_valid_1_john(self):
self.assertEqual(self.get_book.number('1 John', 'kjv'), '62', "Failed to find '1 John' in 'kjv' translation")
expected_result = 62
actual_result = self.get_book.number('1 John', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find '1 John' in 'kjv' translation")
def test_valid_1_peter_ch(self):
self.assertEqual(self.get_book.number('彼得前书', 'cns'), '60',
"Failed to find '彼得前书' in 'cns' translation")
expected_result = 60
actual_result = self.get_book.number('彼得前书', 'cns')
self.assertEqual(actual_result, expected_result, "Failed to find '彼得前书' in 'cns' translation")
def test_valid_first_john(self):
self.assertEqual(self.get_book.number('First John', 'kjv'), '62',
"Failed to find 'First John' in 'kjv' translation")
expected_result = 62
actual_result = self.get_book.number('First John', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find 'First John' in 'kjv' translation")
def test_valid_mismatch_nospace_call(self):
self.assertEqual(self.get_book.number('1Jn', 'aov'), '62',
"Failed to find '1Jn' in 'aov' translation with 'kjv' as fallback translation")
expected_result = 62
actual_result = self.get_book.number('1Jn', 'aov')
self.assertEqual(actual_result, expected_result, "Failed to find '1Jn' in 'aov' translation with 'kjv' as fallback translation")
def test_valid_mismatch_call(self):
self.assertEqual(self.get_book.number('1 John', 'aov'), '62',
"Failed to find '1 John' in 'aov' translation with 'kjv' as fallback translation")
expected_result = 62
actual_result = self.get_book.number('1 John', 'aov')
self.assertEqual(actual_result, expected_result, "Failed to find '1 John' in 'aov' translation with 'kjv' as fallback translation")
def test_invalid_reference(self):
self.assertIsNone(self.get_book.number('NonExistent', 'kjv'),
"Invalid reference 'NonExistent' unexpectedly found in 'kjv'")
actual_result = self.get_book.number('NonExistent', 'kjv')
self.assertIsNone(actual_result, "Invalid reference 'NonExistent' unexpectedly found in 'kjv'")
def test_nonexistent_translation(self):
self.assertEqual(self.get_book.number('Gen', 'nonexistent', ['nonexistent', 'nonexistent']), '1',
"Fallback to 'kjv' did not work for non-existent translation")
expected_result = 1
actual_result = self.get_book.number('Gen', 'nonexistent', ['nonexistent', 'nonexistent'])
self.assertEqual(actual_result, expected_result, "Fallback to 'kjv' did not work for non-existent translation")
def test_fallback_translation(self):
self.assertEqual(self.get_book.number('Gen', 'bad-translation'), '1',
"Fallback to 'kjv' did not work for 'bad-translation'")
expected_result = 1
actual_result = self.get_book.number('Gen', 'bad-translation')
self.assertEqual(actual_result, expected_result, "Fallback to 'kjv' did not work for 'bad-translation'")
if __name__ == '__main__':

View File

@ -8,51 +8,70 @@ class TestGetBibleReference(unittest.TestCase):
self.get = GetBibleReference()
def test_valid_reference(self):
self.assertEqual(self.get.ref('Gen 1:2-7', 'kjv'), BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7]),
"Failed to find 'Gen 1:2-7' book reference.")
expected_result = BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7])
actual_result = self.get.ref('Gen 1:2-7', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen 1:2-7' book reference.")
def test_valid_reference_ch(self):
self.assertEqual(self.get.ref('创世记1:2-7', 'cns'),
BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7]),
"Failed to find '创世记1:2-7' book reference")
self.assertEqual(self.get.ref('创记 1:2-', 'cus'), BookReference(book=1, chapter=1, verses=[2]),
"Failed to find '创记 1:2-' book reference")
expected_result = BookReference(book=1, chapter=1, verses=[2, 3, 4, 5, 6, 7])
actual_result = self.get.ref('创世记1:2-7', 'cns')
self.assertEqual(actual_result, expected_result, "Failed to find '创世记1:2-7' book reference")
def test_valid_reference_missing_verse_ch(self):
expected_result = BookReference(book=1, chapter=1, verses=[2])
actual_result = self.get.ref('创记 1:2-', 'cus')
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:2-' book reference")
def test_valid_reference_missing_verse__ch(self):
expected_result = BookReference(book=1, chapter=1, verses=[5])
actual_result = self.get.ref('创记 1:-5', 'cus')
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:-5' book reference")
def test_valid_reference_ch_no_trans(self):
self.assertEqual(self.get.ref('创世记'), BookReference(book=1, chapter=1, verses=[1]),
"Failed to find '创世记 1:1' book reference")
self.assertEqual(self.get.ref('创记'), BookReference(book=1, chapter=1, verses=[1]),
"Failed to find '创记 1:1' book reference")
actual_result = self.get.ref('创世记')
expected_result = BookReference(book=1, chapter=1, verses=[1])
self.assertEqual(actual_result, expected_result, "Failed to find '创世记 1:1' book reference")
def test_valid_reference_ch_no__trans(self):
expected_result = BookReference(book=1, chapter=1, verses=[1])
actual_result = self.get.ref('创记')
self.assertEqual(actual_result, expected_result, "Failed to find '创记 1:1' book reference")
def test_valid_1_john(self):
self.assertEqual(self.get.ref('1 John', 'kjv'), BookReference(book=62, chapter=1, verses=[1]),
"Failed to find '1 John 1:1' book reference")
expected_result = BookReference(book=62, chapter=1, verses=[1])
actual_result = self.get.ref('1 John', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find '1 John 1:1' book reference")
def test_valid_1_peter_ch(self):
self.assertEqual(self.get.ref('彼得前书', 'cns'), BookReference(book=60, chapter=1, verses=[1]),
"Failed to find '彼得前书 1:1' book reference")
actual_result = self.get.ref('彼得前书', 'cns')
expected_result = BookReference(book=60, chapter=1, verses=[1])
self.assertEqual(actual_result, expected_result, "Failed to find '彼得前书 1:1' book reference")
def test_valid_first_john(self):
self.assertEqual(self.get.ref('First John 3:16,19-21', 'kjv'),
BookReference(book=62, chapter=3, verses=[16, 19, 20, 21]),
"Failed to find 'First John 1:2-7' book reference.")
expected_result = BookReference(book=62, chapter=3, verses=[16, 19, 20, 21])
actual_result = self.get.ref('First John 3:16,19-21', 'kjv')
self.assertEqual(actual_result, expected_result, "Failed to find 'First John 1:2-7' book reference.")
def test_valid_mismatch_nospace_call(self):
self.assertEqual(self.get.ref('1Jn', 'aov'), BookReference(book=62, chapter=1, verses=[1]),
"Failed to find '1Jn 1:1' book reference.")
expected_result = BookReference(book=62, chapter=1, verses=[1])
actual_result = self.get.ref('1Jn', 'aov')
self.assertEqual(actual_result, expected_result, "Failed to find '1Jn 1:1' book reference.")
def test_valid_mismatch_call(self):
self.assertEqual(self.get.ref('1 John 5', 'aov'), BookReference(book=62, chapter=5, verses=[1]),
"Failed to find '1 John 5:1' book reference.")
expected_result = BookReference(book=62, chapter=5, verses=[1])
actual_result = self.get.ref('1 John 5', 'aov')
self.assertEqual(actual_result, expected_result, "Failed to find '1 John 5:1' book reference.")
def test_invalid_reference(self):
with self.assertRaises(ValueError) as context:
expected_exception = "Invalid reference 'NonExistent'."
with self.assertRaises(ValueError) as actual:
self.get.ref('NonExistent', 'kjv')
self.assertEqual(str(context.exception), "Book number for 'NonExistent' could not be found.")
self.assertEqual(str(actual.exception), expected_exception)
def test_nonexistent_translation(self):
self.assertEqual(self.get.ref('Gen', 'nonexistent'), BookReference(book=1, chapter=1, verses=[1]),
"Failed to find 'Gen 1:1' book reference.")
expected_result = BookReference(book=1, chapter=1, verses=[1])
actual_result = self.get.ref('Gen', 'nonexistent')
self.assertEqual(actual_result, expected_result, "Failed to find 'Gen 1:1' book reference.")
if __name__ == '__main__':