From 8901087217f164b585f2ff77d965ba28be2176db Mon Sep 17 00:00:00 2001 From: Llewellyn van der Merwe Date: Sat, 25 Nov 2023 14:13:51 +0200 Subject: [PATCH] Adds better method control, and overall stability. Adds valid_reference and valid_translation methods. --- setup.py | 2 +- src/getbible/getbible.py | 121 +++++++++------- src/getbible/getbible_book_number.py | 58 ++++++-- src/getbible/getbible_reference.py | 177 ++++++++++++++++++------ src/getbible/getbible_reference_trie.py | 71 ++++++++-- tests/test_getbible_book_number.py | 64 ++++++--- tests/test_getbible_reference.py | 71 ++++++---- 7 files changed, 399 insertions(+), 165 deletions(-) diff --git a/setup.py b/setup.py index d3a5641..d160fa2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ 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.", diff --git a/src/getbible/getbible.py b/src/getbible/getbible.py index cd2be06..58cc3c5 100644 --- a/src/getbible/getbible.py +++ b/src/getbible/getbible.py @@ -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,10 +26,69 @@ def __init__(self, repo_path="https://api.getbible.net", version='v2'): 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): + 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. + + :param reference: The Bible reference (e.g., John 3:16). + :param abbreviation: The abbreviation for the Bible translation. + :return: dictionary of the selected Bible verses. + """ + self.__check_translation(abbreviation) + result = {} + references = reference.split(';') + for ref in references: + try: + reference = self.__get.ref(ref, abbreviation) + except ValueError: + raise ValueError(f"Invalid reference '{ref}'.") + + self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result) + + return result + + def scripture(self, reference: str, abbreviation: Optional[str] = 'kjv') -> str: + """ + Select and return Bible verses based on the reference and abbreviation. + + :param reference: The Bible reference (e.g., John 3:16). + :param abbreviation: The abbreviation for the Bible translation. + :return: JSON string of the selected Bible verses. + """ + + return json.dumps(self.select(reference, abbreviation)) + + 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. @@ -38,7 +99,7 @@ def __start_cache_reset_thread(self): reset_thread.daemon = True # Daemonize thread reset_thread.start() - def __reset_cache_monthly(self): + def __reset_cache_monthly(self) -> None: """ Periodically clears the cache on the first day of each month. @@ -51,7 +112,7 @@ def __reset_cache_monthly(self): self.__chapters_cache.clear() print(f"Cache cleared on {datetime.now()}") - def __calculate_time_until_next_month(self): + def __calculate_time_until_next_month(self) -> float: """ Calculate the seconds until the start of the next month. @@ -66,39 +127,7 @@ def __calculate_time_until_next_month(self): 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'): - """ - Select and return Bible verses based on the reference and abbreviation. - - :param reference: The Bible reference (e.g., John 3:16). - :param abbreviation: The abbreviation for the Bible translation. - :return: dictionary of the selected Bible verses. - """ - self.__check_translation(abbreviation) - result = {} - references = reference.split(';') - for ref in references: - try: - reference = self.__get.ref(ref, abbreviation) - except ValueError: - raise ValueError(f"Invalid reference format.") - - self.__set_verse(abbreviation, reference.book, reference.chapter, reference.verses, result) - - return result - - def scripture(self, reference, abbreviation='kjv'): - """ - Select and return Bible verses based on the reference and abbreviation. - - :param reference: The Bible reference (e.g., John 3:16). - :param abbreviation: The abbreviation for the Bible translation. - :return: JSON string of the selected Bible verses. - """ - - return json.dumps(self.select(reference, abbreviation)) - - def __set_verse(self, abbreviation, book, chapter, verses, result): + 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 @@ def __set_verse(self, abbreviation, book, chapter, verses, result): 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 @@ def __generate_path(self, abbreviation, file_name): 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 @@ def __fetch_data(self, path): 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. diff --git a/src/getbible/getbible_book_number.py b/src/getbible/getbible_book_number.py index 697e139..7dfae17 100644 --- a/src/getbible/getbible_book_number.py +++ b/src/getbible/getbible_book_number.py @@ -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: str) -> None: + """ + Load a translation trie from a specified file. - def _load_translation(self, filename): + :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 @@ def _load_translation(self, filename): 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 @@ def number(self, reference, translation_code=None, fallback_translations=None): 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: diff --git a/src/getbible/getbible_reference.py b/src/getbible/getbible_reference.py index 8dd729d..7cc316a 100644 --- a/src/getbible/getbible_reference.py +++ b/src/getbible/getbible_reference.py @@ -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: 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) + return int(chapter_match.group()) if chapter_match else 1 + + 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 - 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 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) - - def __get_verses_numbers(self, verses): + 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 diff --git a/src/getbible/getbible_reference_trie.py b/src/getbible/getbible_reference_trie.py index 9b1e5dc..323a962 100644 --- a/src/getbible/getbible_reference_trie.py +++ b/src/getbible/getbible_reference_trie.py @@ -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 @@ def search(self, book_name): 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 @@ def _dump_to_dict(self, node=None, key=''): 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: diff --git a/tests/test_getbible_book_number.py b/tests/test_getbible_book_number.py index b9801c8..83e8808 100644 --- a/tests/test_getbible_book_number.py +++ b/tests/test_getbible_book_number.py @@ -8,48 +8,68 @@ def setUp(self): 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__': diff --git a/tests/test_getbible_reference.py b/tests/test_getbible_reference.py index 55ed1db..330d812 100644 --- a/tests/test_getbible_reference.py +++ b/tests/test_getbible_reference.py @@ -8,51 +8,70 @@ def setUp(self): 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__':