From 945e9425ba3e3759641e2697779d0d9025aa6719 Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Fri, 4 Dec 2020 22:03:17 +0300 Subject: [PATCH 1/6] Introduce CardBuilder to make cards --- AutoDefineAddon/autodefine.py | 236 +++++++++++++++++------------ AutoDefineAddon/tests/tests_api.py | 69 +++++++++ 2 files changed, 211 insertions(+), 94 deletions(-) create mode 100644 AutoDefineAddon/tests/tests_api.py diff --git a/AutoDefineAddon/autodefine.py b/AutoDefineAddon/autodefine.py index f83ff93..f19d643 100644 --- a/AutoDefineAddon/autodefine.py +++ b/AutoDefineAddon/autodefine.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # AutoDefine Anki Add-on # Auto-defines words, optionally adding pronunciation and images. # @@ -21,7 +23,10 @@ from urllib.error import URLError from xml.etree import ElementTree as ET -from .libs import webbrowser +import sys +sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") + +import webbrowser # --------------------------------- SETTINGS --------------------------------- @@ -148,7 +153,7 @@ def _focus_zero_field(editor): editor.web.eval("focusField(%d);" % 0) -def get_preferred_valid_entries(editor, word): +def get_preferred_valid_entries(word): collegiate_url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ urllib.parse.quote_plus(word) + "?key=" + MERRIAM_WEBSTER_API_KEY medical_url = "https://www.dictionaryapi.com/api/references/medical/v2/xml/" + \ @@ -174,7 +179,7 @@ def get_preferred_valid_entries(editor, word): potential = " Potential matches: " + ", ".join(potential_unified) tooltip("No entry found in Merriam-Webster dictionary for word '%s'.%s" % (word, potential if entries.potential else "")) - _focus_zero_field(editor) + return entries.valid @@ -221,6 +226,8 @@ def get_entries_from_api(word, url): return etree.findall("entry") except URLError: return [] + except ValueError: + return [] except (ET.ParseError, RemoteDisconnected): showInfo("Couldn't parse API response for word '%s'. " "Please submit an issue to the AutoDefine GitHub (a web browser window will open)." % word) @@ -243,77 +250,54 @@ def _get_word(editor): word = clean_html(word).strip() return word - -def _get_definition(editor, - force_pronounce=False, - force_definition=False, - force_phonetic_transcription=False): - validate_settings() - word = _get_word(editor) - if word == "": - tooltip("AutoDefine: No text found in note fields.") - return - valid_entries = get_preferred_valid_entries(editor, word) - - insert_queue = {} - - # Add Vocal Pronunciation - if (not force_definition and not force_phonetic_transcription and PRONUNCIATION_FIELD > -1) or force_pronounce: - # Parse all unique pronunciations, and convert them to URLs as per http://goo.gl/nL0vte - all_sounds = [] - for entry in valid_entries: - for wav in entry.findall("sound/wav"): - raw_wav = wav.text - # API-specific URL conversions - if raw_wav[:3] == "bix": - mid_url = "bix" - elif raw_wav[:2] == "gg": - mid_url = "gg" - elif raw_wav[:1].isdigit(): - mid_url = "number" +class Card(): + entries = [] + fields = ["", "", "", ""] + def parse(self, entries) -> list: + pass + + def getField(self, id): + # FIXME: out of range + return self.fields[id] + + def serialize(self, editor): + if editor.note: + i = 0 + for field in self.fields: + if field.find("wav") != -1: + editor.note.fields[i] = editor.urlToLink(field) else: - mid_url = raw_wav[:1] - wav_url = "http://media.merriam-webster.com/soundc11/" + mid_url + "/" + raw_wav + editor.note.fields[i] = field + editor.loadNote() + i += 1 - all_sounds.append(editor.urlToLink(wav_url).strip()) +class CardBuilder: + _card = Card() + def __init__(self, word): + self._card.fields[0] = word - # We want to make this a non-duplicate list, so that we only get unique sound files. - all_sounds = list(dict.fromkeys(all_sounds)) - - final_pronounce_index = PRONUNCIATION_FIELD - fields = mw.col.models.fieldNames(editor.note.model()) - for field in fields: - if '🔊' in field: - final_pronounce_index = fields.index(field) - break - - to_print = ''.join(all_sounds) + def addDefinition(self): + self._card.fields[DEFINITION_FIELD] = "" - _add_to_insert_queue(insert_queue, to_print, final_pronounce_index) + def addTranscription(self): + self._card.fields[PHONETIC_TRANSCRIPTION_FIELD] = "" - # Add Phonetic Transcription - if (not force_definition and not force_pronounce and PHONETIC_TRANSCRIPTION_FIELD > -1) or \ - force_phonetic_transcription: + def addPronunciation(self): + self._card.fields[PRONUNCIATION_FIELD] = "" - # extract phonetic transcriptions for each entry and label them by part of speech - all_transcriptions = [] - for entry in valid_entries: - if entry.find("pr") is not None: - phonetic_transcription = entry.find("pr").text + def getCard(self) -> Card: + return self._card - part_of_speech = entry.find("fl").text - part_of_speech = _abbreviate_part_of_speech(part_of_speech) +class CollegiateCardBuilder(CardBuilder): + def __init__(self, word): + super().__init__(word) + self._card.entries = get_preferred_valid_entries(word) - row = f'{part_of_speech} \\{phonetic_transcription}\\' - all_transcriptions.append(row) - - to_print = "
".join(all_transcriptions) - - _add_to_insert_queue(insert_queue, to_print, PHONETIC_TRANSCRIPTION_FIELD) - - # Add Definition - definition_array = [] - if (not force_pronounce and not force_phonetic_transcription and DEFINITION_FIELD > -1) or force_definition: + def addDefinition(self): + valid_entries = self._card.entries + insert_queue = {} + # Add Definition + definition_array = [] # Extract the type of word this is for entry in valid_entries: this_def = entry.find("def") @@ -386,28 +370,104 @@ def _get_definition(editor, last_functional_label = definition.tail to_return += to_print - # final cleanup of tag bs - to_return = to_return.replace(". ; ", ". ") # as first definition after "n. " or "v. " - to_return = to_return.replace("\n; ", "\n") # as first definition after newline - _add_to_insert_queue(insert_queue, to_return, DEFINITION_FIELD) + # final cleanup of tag bs + to_return = to_return.replace(". ; ", ". ") # as first definition after "n. " or "v. " + to_return = to_return.replace("\n; ", "\n") # as first definition after newline + self._card.fields[DEFINITION_FIELD] = to_return - # Insert each queue into the considered field - for field_index in insert_queue.keys(): - insert_into_field(editor, insert_queue[field_index], field_index) + def addTranscription(self): + valid_entries = self._card.entries + # extract phonetic transcriptions for each entry and label them by part of speech + all_transcriptions = [] + for entry in valid_entries: + if entry.find("pr") is not None: + phonetic_transcription = entry.find("pr").text + + part_of_speech = entry.find("fl").text + part_of_speech = _abbreviate_part_of_speech(part_of_speech) + + row = f'{part_of_speech} \\{phonetic_transcription}\\' + all_transcriptions.append(row) + + to_print = "
".join(all_transcriptions) + + self._card.fields[PHONETIC_TRANSCRIPTION_FIELD] = to_print + + def addPronunciation(self): + valid_entries = self._card.entries + # Parse all unique pronunciations, and convert them to URLs as per http://goo.gl/nL0vte + all_sounds = [] + for entry in valid_entries: + for wav in entry.findall("sound/wav"): + raw_wav = wav.text + # API-specific URL conversions + if raw_wav[:3] == "bix": + mid_url = "bix" + elif raw_wav[:2] == "gg": + mid_url = "gg" + elif raw_wav[:1].isdigit(): + mid_url = "number" + else: + mid_url = raw_wav[:1] + wav_url = "http://media.merriam-webster.com/soundc11/" + mid_url + "/" + raw_wav + # FIXME: self.editor.urlToLink( + all_sounds.append(wav_url.strip()) + + # We want to make this a non-duplicate list, so that we only get unique sound files. + all_sounds = list(dict.fromkeys(all_sounds)) + # FIXME: does not look like a right place + final_pronounce_index = PRONUNCIATION_FIELD + if mw and False: + fields = mw.col.models.fieldNames(editor.note.model()) + for field in fields: + if '🔊' in field: + final_pronounce_index = fields.index(field) + break + + to_print = ''.join(all_sounds) + + self._card.fields[PRONUNCIATION_FIELD] = to_print + +def _get_definition(editor, + force_pronounce=False, + force_definition=False, + force_phonetic_transcription=False): + # FIXME: comment to work on test + #validate_settings() + word = _get_word(editor) + if word == "": + tooltip("AutoDefine: No text found in note fields.") + return + + insert_queue = {} + + cardBuilder = None + if PREFERRED_DICTIONARY == "COLLEGIATE": + cardBuilder = CollegiateCardBuilder(word) + # Add Vocal Pronunciation + if (not force_definition and not force_phonetic_transcription and PRONUNCIATION_FIELD > -1) or force_pronounce: + cardBuilder.addPronunciation() + + # Add Phonetic Transcription + if (not force_definition and not force_pronounce and PHONETIC_TRANSCRIPTION_FIELD > -1) or \ + force_phonetic_transcription: + cardBuilder.addTranscription() + + + # Add Definition + if (not force_pronounce and not force_phonetic_transcription and DEFINITION_FIELD > -1) or force_definition: + cardBuilder.addDefinition() + + # Insert each queue into the considered field + card = cardBuilder.getCard() + card.serialize(editor) if OPEN_IMAGES_IN_BROWSER: webbrowser.open("https://www.google.com/search?q= " + word + "&safe=off&tbm=isch&tbs=isz:lt,islt:xga", 0, False) _focus_zero_field(editor) -def _add_to_insert_queue(insert_queue, to_print, field_index): - if field_index not in insert_queue.keys(): - insert_queue[field_index] = to_print - else: - insert_queue[field_index] += "
" + to_print - - def _abbreviate_part_of_speech(part_of_speech): if part_of_speech in PART_OF_SPEECH_ABBREVIATION.keys(): part_of_speech = PART_OF_SPEECH_ABBREVIATION[part_of_speech] @@ -415,19 +475,6 @@ def _abbreviate_part_of_speech(part_of_speech): return part_of_speech -def insert_into_field(editor, text, field_id, overwrite=False): - if len(editor.note.fields) <= field_id: - tooltip("AutoDefine: Tried to insert '%s' into user-configured field number %d (0-indexed), but note type only " - "has %d fields. Use a different note type with %d or more fields, or change the index in the " - "Add-on configuration." % (text, field_id, len(editor.note.fields), field_id + 1), period=10000) - return - if overwrite: - editor.note.fields[field_id] = text - else: - editor.note.fields[field_id] += text - editor.loadNote() - - # via https://stackoverflow.com/a/12982689 def clean_html(raw_html): return re.sub(re.compile('<.*?>'), '', raw_html).replace(" ", " ") @@ -482,7 +529,7 @@ def setup_buttons(buttons, editor): addHook("setupEditorButtons", setup_buttons) - +''' if getattr(mw.addonManager, "getConfig", None): config = mw.addonManager.getConfig(__name__) if '1 required' in config and 'MERRIAM_WEBSTER_API_KEY' in config['1 required']: @@ -520,3 +567,4 @@ def setup_buttons(buttons, editor): PRONOUNCE_ONLY_SHORTCUT = shortcuts['3 PRONOUNCE_ONLY_SHORTCUT'] if '4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT' in shortcuts: PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = shortcuts['4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT'] +''' diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py new file mode 100644 index 0000000..5b87ea7 --- /dev/null +++ b/AutoDefineAddon/tests/tests_api.py @@ -0,0 +1,69 @@ +import pytest + +import sys +import os + +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath + '/../') + +import urllib.request +import autodefine + +def test_empty_entries_if_url_empty(): + assert autodefine.get_entries_from_api("word", "") == [] + +def test_non_empty_entries_if_url_correct(): + word = "word" + KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ + urllib.parse.quote_plus(word) + "?key=" + KEY + assert autodefine.get_entries_from_api("word", url) != [] + +class MockNote: + fields = ["", "", "", "", "WA"] + +class MockEditor: + note = MockNote() + web = False + + def loadNote(self): + print("LoadNote") + + def urlToLink(self, url): + return url + +autodefine.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" +autodefine.DEFINITION_FIELD = 1 +autodefine.PRONUNCIATION_FIELD = 2 +autodefine.PHONETIC_TRANSCRIPTION_FIELD = 3 + +autodefine.PREFERRED_DICTIONARY = "COLLEGIATE" + +def test_can_serialize_xml_card(): + word = "test" + cardBuilder = autodefine.CollegiateCardBuilder(word) + cardBuilder.addDefinition() + cardBuilder.addPronunciation() + cardBuilder.addTranscription() + card = cardBuilder.getCard() + + editor = MockEditor() + card.serialize(editor) + + assert word == editor.note.fields[0] + assert editor.note.fields[autodefine.DEFINITION_FIELD].find("to put to test or proof") != -1 + assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[autodefine.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 + +def test_can_get_definition(): + editor = MockEditor() + word = "insert" + editor.note.fields[0] = word + autodefine._get_definition(editor, True, True, True) + + assert editor.note.fields[0] == word + assert editor.note.fields[autodefine.DEFINITION_FIELD].find("to put or thrust") != -1 + assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("insert") != -1 + assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[autodefine.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsÉ™rt") != -1 + From 478d223db1c4074d8fb145128cce33b46633834d Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Fri, 4 Dec 2020 23:25:51 +0300 Subject: [PATCH 2/6] Extract settings and card builder into libs --- AutoDefineAddon/autodefine.py | 325 +--------------------------- AutoDefineAddon/libs/cardbuilder.py | 297 +++++++++++++++++++++++++ AutoDefineAddon/libs/config.py | 37 ++++ AutoDefineAddon/tests/tests_api.py | 35 +-- 4 files changed, 363 insertions(+), 331 deletions(-) create mode 100644 AutoDefineAddon/libs/cardbuilder.py create mode 100644 AutoDefineAddon/libs/config.py diff --git a/AutoDefineAddon/autodefine.py b/AutoDefineAddon/autodefine.py index f19d643..c8e97a9 100644 --- a/AutoDefineAddon/autodefine.py +++ b/AutoDefineAddon/autodefine.py @@ -26,46 +26,11 @@ import sys sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") +import config import webbrowser - +import cardbuilder # --------------------------------- SETTINGS --------------------------------- -# Get your unique API key by signing up at http://www.dictionaryapi.com/ -MERRIAM_WEBSTER_API_KEY = "YOUR_KEY_HERE" - -# Index of field to insert definitions into (use -1 to turn off) -DEFINITION_FIELD = 1 - -# Ignore archaic/obsolete definitions? -IGNORE_ARCHAIC = True - -# Get your unique API key by signing up at http://www.dictionaryapi.com/ -MERRIAM_WEBSTER_MEDICAL_API_KEY = "YOUR_KEY_HERE" - -# Open a browser tab with an image search for the same word? -OPEN_IMAGES_IN_BROWSER = False - -# Which dictionary should AutoDefine prefer to get definitions from? Available options are COLLEGIATE and MEDICAL. -PREFERRED_DICTIONARY = "COLLEGIATE" - -# Index of field to insert pronunciations into (use -1 to turn off) -PRONUNCIATION_FIELD = 0 - -# Index of field to insert phonetic transcription into (use -1 to turn off) -PHONETIC_TRANSCRIPTION_FIELD = -1 - -# Index of field to insert pronunciations into (use -1 to turn off) -DEDICATED_INDIVIDUAL_BUTTONS = False - -PRIMARY_SHORTCUT = "ctrl+alt+e" - -DEFINE_ONLY_SHORTCUT = "" - -PRONOUNCE_ONLY_SHORTCUT = "" - -PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = "" - -PART_OF_SPEECH_ABBREVIATION = {"verb": "v.", "noun": "n.", "adverb": "adv.", "adjective": "adj."} # Collegiate Dictionary API XML documentation: http://goo.gl/LuD83A @@ -119,20 +84,20 @@ def get_definition_force_phonetic_transcription(editor): def validate_settings(): # ideally, we wouldn't have to force people to individually register, but the API limit is just 1000 calls/day. - if PREFERRED_DICTIONARY != "COLLEGIATE" and PREFERRED_DICTIONARY != "MEDICAL": + if config.PREFERRED_DICTIONARY != "COLLEGIATE" and config.PREFERRED_DICTIONARY != "MEDICAL": message = "Setting PREFERRED_DICTIONARY must be set to either COLLEGIATE or MEDICAL. Current setting: '%s'" \ - % PREFERRED_DICTIONARY + % config.PREFERRED_DICTIONARY showInfo(message) return - if PREFERRED_DICTIONARY == "MEDICAL" and MERRIAM_WEBSTER_MEDICAL_API_KEY == "YOUR_KEY_HERE": + if config.PREFERRED_DICTIONARY == "MEDICAL" and config.MERRIAM_WEBSTER_MEDICAL_API_KEY == "YOUR_KEY_HERE": message = "The preferred dictionary was set to MEDICAL, but no API key was provided.\n" \ "Please register for one at www.dictionaryapi.com." showInfo(message) webbrowser.open("https://www.dictionaryapi.com/", 0, False) return - if MERRIAM_WEBSTER_API_KEY == "YOUR_KEY_HERE": + if config.MERRIAM_WEBSTER_API_KEY == "YOUR_KEY_HERE": message = "AutoDefine requires use of Merriam-Webster's Collegiate Dictionary with Audio API. " \ "To get functionality working:\n" \ "1. Go to www.dictionaryapi.com and sign up for an account, requesting access to " \ @@ -144,98 +109,12 @@ def validate_settings(): return -ValidAndPotentialEntries = namedtuple('Entries', ['valid', 'potential']) - - def _focus_zero_field(editor): # no idea why, but sometimes web seems to be unavailable if editor.web: editor.web.eval("focusField(%d);" % 0) -def get_preferred_valid_entries(word): - collegiate_url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ - urllib.parse.quote_plus(word) + "?key=" + MERRIAM_WEBSTER_API_KEY - medical_url = "https://www.dictionaryapi.com/api/references/medical/v2/xml/" + \ - urllib.parse.quote_plus(word) + "?key=" + MERRIAM_WEBSTER_MEDICAL_API_KEY - all_collegiate_entries = get_entries_from_api(word, collegiate_url) - all_medical_entries = get_entries_from_api(word, medical_url) - - potential_unified = set() - if PREFERRED_DICTIONARY == "COLLEGIATE": - entries = filter_entries_lower_and_potential(word, all_collegiate_entries) - potential_unified |= entries.potential - if not entries.valid: - entries = filter_entries_lower_and_potential(word, all_medical_entries) - potential_unified |= entries.potential - else: - entries = filter_entries_lower_and_potential(word, all_medical_entries) - potential_unified |= entries.potential - if not entries.valid: - entries = filter_entries_lower_and_potential(word, all_collegiate_entries) - potential_unified |= entries.potential - - if not entries.valid: - potential = " Potential matches: " + ", ".join(potential_unified) - tooltip("No entry found in Merriam-Webster dictionary for word '%s'.%s" % - (word, potential if entries.potential else "")) - - return entries.valid - - -def filter_entries_lower_and_potential(word, all_entries): - valid_entries = extract_valid_entries(word, all_entries) - maybe_entries = set() - if not valid_entries: - valid_entries = extract_valid_entries(word, all_entries, True) - if not valid_entries: - for entry in all_entries: - maybe_entries.add(re.sub(r'\[\d+\]$', "", entry.attrib["id"])) - return ValidAndPotentialEntries(valid_entries, maybe_entries) - - -def extract_valid_entries(word, all_entries, lower=False): - valid_entries = [] - for entry in all_entries: - if lower: - if entry.attrib["id"][:len(word) + 1].lower() == word.lower() + "[" \ - or entry.attrib["id"].lower() == word.lower(): - valid_entries.append(entry) - else: - if entry.attrib["id"][:len(word) + 1] == word + "[" \ - or entry.attrib["id"] == word: - valid_entries.append(entry) - return valid_entries - - -def get_entries_from_api(word, url): - if "YOUR_KEY_HERE" in url: - return [] - try: - req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0)' - ' Gecko/20100101 Firefox/62.0'}) - returned = urllib.request.urlopen(req).read() - if "Invalid API key" in returned.decode("UTF-8"): - showInfo("API key '%s' is invalid. Please double-check you are using the key labeled \"Key (Dictionary)\". " - "A web browser with the web page that lists your keys will open." % url.split("?key=")[1]) - webbrowser.open("https://www.dictionaryapi.com/account/my-keys.htm") - return [] - if "Results not found" in returned.decode("UTF-8"): - return [] - etree = ET.fromstring(returned) - return etree.findall("entry") - except URLError: - return [] - except ValueError: - return [] - except (ET.ParseError, RemoteDisconnected): - showInfo("Couldn't parse API response for word '%s'. " - "Please submit an issue to the AutoDefine GitHub (a web browser window will open)." % word) - webbrowser.open("https://github.com/z1lc/AutoDefine/issues/new?title=Parse error for word '%s'" - "&body=Anki Version: %s%%0APlatform: %s %s%%0AURL: %s%%0AStack Trace: %s" - % (word, version, platform.system(), platform.release(), url, traceback.format_exc()), 0, False) - - def _get_word(editor): word = "" maybe_web = editor.web @@ -250,191 +129,12 @@ def _get_word(editor): word = clean_html(word).strip() return word -class Card(): - entries = [] - fields = ["", "", "", ""] - def parse(self, entries) -> list: - pass - - def getField(self, id): - # FIXME: out of range - return self.fields[id] - - def serialize(self, editor): - if editor.note: - i = 0 - for field in self.fields: - if field.find("wav") != -1: - editor.note.fields[i] = editor.urlToLink(field) - else: - editor.note.fields[i] = field - editor.loadNote() - i += 1 - -class CardBuilder: - _card = Card() - def __init__(self, word): - self._card.fields[0] = word - - def addDefinition(self): - self._card.fields[DEFINITION_FIELD] = "" - - def addTranscription(self): - self._card.fields[PHONETIC_TRANSCRIPTION_FIELD] = "" - - def addPronunciation(self): - self._card.fields[PRONUNCIATION_FIELD] = "" - - def getCard(self) -> Card: - return self._card - -class CollegiateCardBuilder(CardBuilder): - def __init__(self, word): - super().__init__(word) - self._card.entries = get_preferred_valid_entries(word) - - def addDefinition(self): - valid_entries = self._card.entries - insert_queue = {} - # Add Definition - definition_array = [] - # Extract the type of word this is - for entry in valid_entries: - this_def = entry.find("def") - if entry.find("fl") is None: - continue - fl = entry.find("fl").text - fl = _abbreviate_part_of_speech(fl) - - this_def.tail = "" + fl + "" # save the functional label (noun/verb/etc) in the tail - - # the tag will contain the word 'obsolete' if the term is not in use anymore. However, for some - # reason, the tag precedes the
that it is associated with instead of being a child. We need to - # associate it here so that later we can either remove or keep it regardless. - previous_was_ssl = False - for child in this_def: - # this is a kind of poor way of going about things, but the ElementTree API - # doesn't seem to offer an alternative. - if child.text == "obsolete" and child.tag == "ssl": - previous_was_ssl = True - if previous_was_ssl and child.tag == "dt": - child.tail = "obsolete" - previous_was_ssl = False - - definition_array.append(this_def) - - to_return = "" - for definition in definition_array: - last_functional_label = "" - medical_api_def = definition.findall("./sensb/sens/dt") - # sometimes there's not a definition directly (dt) but just a usage example (un): - if len(medical_api_def) == 1 and not medical_api_def[0].text: - medical_api_def = definition.findall("./sensb/sens/dt/un") - for dtTag in (definition.findall("dt") + medical_api_def): - - if dtTag.tail == "obsolete": - dtTag.tail = "" # take away the tail word so that when printing it does not show up. - if IGNORE_ARCHAIC: - continue - - # We don't really care for 'verbal illustrations' or 'usage notes', - # even though they are occasionally useful. - for usageNote in dtTag.findall("un"): - dtTag.remove(usageNote) - for verbalIllustration in dtTag.findall("vi"): - dtTag.remove(verbalIllustration) - - # Directional cross reference doesn't make sense for us - for dxTag in dtTag.findall("dx"): - for dxtTag in dxTag.findall("dxt"): - for dxnTag in dxtTag.findall("dxn"): - dxtTag.remove(dxnTag) - - # extract raw XML from
...
- to_print = ET.tostring(dtTag, "", "xml").strip().decode("utf-8") - # attempt to remove 'synonymous cross reference tag' and replace with semicolon - to_print = to_print.replace("", "; ") - # attempt to remove 'Directional cross reference tag' and replace with semicolon - to_print = to_print.replace("", "; ") - # remove all other XML tags - to_print = re.sub('<[^>]*>', '', to_print) - # remove all colons, since they are usually useless and have been replaced with semicolons above - to_print = re.sub(':', '', to_print) - # erase space between semicolon and previous word, if exists, and strip any extraneous whitespace - to_print = to_print.replace(" ; ", "; ").strip() - to_print += "\n
" - - # add verb/noun/adjective - if last_functional_label != definition.tail: - to_print = definition.tail + " " + to_print - last_functional_label = definition.tail - to_return += to_print - - # final cleanup of tag bs - to_return = to_return.replace(". ; ", ". ") # as first definition after "n. " or "v. " - to_return = to_return.replace("\n; ", "\n") # as first definition after newline - self._card.fields[DEFINITION_FIELD] = to_return - - def addTranscription(self): - valid_entries = self._card.entries - # extract phonetic transcriptions for each entry and label them by part of speech - all_transcriptions = [] - for entry in valid_entries: - if entry.find("pr") is not None: - phonetic_transcription = entry.find("pr").text - - part_of_speech = entry.find("fl").text - part_of_speech = _abbreviate_part_of_speech(part_of_speech) - - row = f'{part_of_speech} \\{phonetic_transcription}\\' - all_transcriptions.append(row) - - to_print = "
".join(all_transcriptions) - - self._card.fields[PHONETIC_TRANSCRIPTION_FIELD] = to_print - - def addPronunciation(self): - valid_entries = self._card.entries - # Parse all unique pronunciations, and convert them to URLs as per http://goo.gl/nL0vte - all_sounds = [] - for entry in valid_entries: - for wav in entry.findall("sound/wav"): - raw_wav = wav.text - # API-specific URL conversions - if raw_wav[:3] == "bix": - mid_url = "bix" - elif raw_wav[:2] == "gg": - mid_url = "gg" - elif raw_wav[:1].isdigit(): - mid_url = "number" - else: - mid_url = raw_wav[:1] - wav_url = "http://media.merriam-webster.com/soundc11/" + mid_url + "/" + raw_wav - # FIXME: self.editor.urlToLink( - all_sounds.append(wav_url.strip()) - - # We want to make this a non-duplicate list, so that we only get unique sound files. - all_sounds = list(dict.fromkeys(all_sounds)) - # FIXME: does not look like a right place - final_pronounce_index = PRONUNCIATION_FIELD - if mw and False: - fields = mw.col.models.fieldNames(editor.note.model()) - for field in fields: - if '🔊' in field: - final_pronounce_index = fields.index(field) - break - - to_print = ''.join(all_sounds) - - self._card.fields[PRONUNCIATION_FIELD] = to_print - def _get_definition(editor, force_pronounce=False, force_definition=False, force_phonetic_transcription=False): - # FIXME: comment to work on test - #validate_settings() + validate_settings() word = _get_word(editor) if word == "": tooltip("AutoDefine: No text found in note fields.") @@ -443,8 +143,8 @@ def _get_definition(editor, insert_queue = {} cardBuilder = None - if PREFERRED_DICTIONARY == "COLLEGIATE": - cardBuilder = CollegiateCardBuilder(word) + if config.PREFERRED_DICTIONARY == "COLLEGIATE" or config.PREFERRED_DICTIONARY == "MEDICAL": + cardBuilder = cardbuilder.CollegiateCardBuilder(word) # Add Vocal Pronunciation if (not force_definition and not force_phonetic_transcription and PRONUNCIATION_FIELD > -1) or force_pronounce: cardBuilder.addPronunciation() @@ -462,17 +162,12 @@ def _get_definition(editor, # Insert each queue into the considered field card = cardBuilder.getCard() card.serialize(editor) - if OPEN_IMAGES_IN_BROWSER: + if config.OPEN_IMAGES_IN_BROWSER: webbrowser.open("https://www.google.com/search?q= " + word + "&safe=off&tbm=isch&tbs=isz:lt,islt:xga", 0, False) _focus_zero_field(editor) -def _abbreviate_part_of_speech(part_of_speech): - if part_of_speech in PART_OF_SPEECH_ABBREVIATION.keys(): - part_of_speech = PART_OF_SPEECH_ABBREVIATION[part_of_speech] - - return part_of_speech # via https://stackoverflow.com/a/12982689 diff --git a/AutoDefineAddon/libs/cardbuilder.py b/AutoDefineAddon/libs/cardbuilder.py new file mode 100644 index 0000000..c61be36 --- /dev/null +++ b/AutoDefineAddon/libs/cardbuilder.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# AutoDefine Anki Add-on +# Auto-defines words, optionally adding pronunciation and images. +# +# Copyright (c) 2014 - 2019 Robert Sanek robertsanek.com rsanek@gmail.com +# https://github.com/z1lc/AutoDefine Licensed under GPL v2 + +import os +from collections import namedtuple + +import platform +import re +import traceback +import urllib.error +import urllib.parse +import urllib.request +from anki import version +from anki.hooks import addHook +from aqt import mw +from aqt.utils import showInfo, tooltip +from http.client import RemoteDisconnected +from urllib.error import URLError +from xml.etree import ElementTree as ET + +import sys +sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") + +import webbrowser +import config + +def _abbreviate_part_of_speech(part_of_speech): + if part_of_speech in config.PART_OF_SPEECH_ABBREVIATION.keys(): + part_of_speech = config.PART_OF_SPEECH_ABBREVIATION[part_of_speech] + + return part_of_speech + + +def get_preferred_valid_entries(word): + collegiate_url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ + urllib.parse.quote_plus(word) + "?key=" + config.MERRIAM_WEBSTER_API_KEY + medical_url = "https://www.dictionaryapi.com/api/references/medical/v2/xml/" + \ + urllib.parse.quote_plus(word) + "?key=" + config.MERRIAM_WEBSTER_MEDICAL_API_KEY + all_collegiate_entries = get_entries_from_api(word, collegiate_url) + all_medical_entries = get_entries_from_api(word, medical_url) + + potential_unified = set() + if config.PREFERRED_DICTIONARY == "COLLEGIATE": + entries = filter_entries_lower_and_potential(word, all_collegiate_entries) + potential_unified |= entries.potential + if not entries.valid: + entries = filter_entries_lower_and_potential(word, all_medical_entries) + potential_unified |= entries.potential + else: + entries = filter_entries_lower_and_potential(word, all_medical_entries) + potential_unified |= entries.potential + if not entries.valid: + entries = filter_entries_lower_and_potential(word, all_collegiate_entries) + potential_unified |= entries.potential + + if not entries.valid: + potential = " Potential matches: " + ", ".join(potential_unified) + tooltip("No entry found in Merriam-Webster dictionary for word '%s'.%s" % + (word, potential if entries.potential else "")) + + return entries.valid + + +def filter_entries_lower_and_potential(word, all_entries): + valid_entries = extract_valid_entries(word, all_entries) + maybe_entries = set() + if not valid_entries: + valid_entries = extract_valid_entries(word, all_entries, True) + if not valid_entries: + for entry in all_entries: + maybe_entries.add(re.sub(r'\[\d+\]$', "", entry.attrib["id"])) + return ValidAndPotentialEntries(valid_entries, maybe_entries) + + +def extract_valid_entries(word, all_entries, lower=False): + valid_entries = [] + for entry in all_entries: + if lower: + if entry.attrib["id"][:len(word) + 1].lower() == word.lower() + "[" \ + or entry.attrib["id"].lower() == word.lower(): + valid_entries.append(entry) + else: + if entry.attrib["id"][:len(word) + 1] == word + "[" \ + or entry.attrib["id"] == word: + valid_entries.append(entry) + return valid_entries + + +ValidAndPotentialEntries = namedtuple('Entries', ['valid', 'potential']) + + +def get_entries_from_api(word, url): + if "YOUR_KEY_HERE" in url: + return [] + try: + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0)' + ' Gecko/20100101 Firefox/62.0'}) + returned = urllib.request.urlopen(req).read() + if "Invalid API key" in returned.decode("UTF-8"): + showInfo("API key '%s' is invalid. Please double-check you are using the key labeled \"Key (Dictionary)\". " + "A web browser with the web page that lists your keys will open." % url.split("?key=")[1]) + webbrowser.open("https://www.dictionaryapi.com/account/my-keys.htm") + return [] + if "Results not found" in returned.decode("UTF-8"): + return [] + etree = ET.fromstring(returned) + return etree.findall("entry") + except URLError: + return [] + except ValueError: + return [] + except (ET.ParseError, RemoteDisconnected): + showInfo("Couldn't parse API response for word '%s'. " + "Please submit an issue to the AutoDefine GitHub (a web browser window will open)." % word) + webbrowser.open("https://github.com/z1lc/AutoDefine/issues/new?title=Parse error for word '%s'" + "&body=Anki Version: %s%%0APlatform: %s %s%%0AURL: %s%%0AStack Trace: %s" + % (word, version, platform.system(), platform.release(), url, traceback.format_exc()), 0, False) +class Card(): + entries = [] + fields = ["", "", "", ""] + def parse(self, entries) -> list: + pass + + def getField(self, id): + # FIXME: out of range + return self.fields[id] + + def serialize(self, editor): + if editor.note: + i = 0 + for field in self.fields: + if field.find("wav") != -1: + editor.note.fields[i] = editor.urlToLink(field) + else: + editor.note.fields[i] = field + editor.loadNote() + i += 1 + +class CardBuilder: + _card = Card() + def __init__(self, word): + self._card.fields[0] = word + + def addDefinition(self): + self._card.fields[config.DEFINITION_FIELD] = "" + + def addTranscription(self): + self._card.fields[config.PHONETIC_TRANSCRIPTION_FIELD] = "" + + def addPronunciation(self): + self._card.fields[config.PRONUNCIATION_FIELD] = "" + + def getCard(self) -> Card: + return self._card + +class CollegiateCardBuilder(CardBuilder): + def __init__(self, word): + super().__init__(word) + self._card.entries = get_preferred_valid_entries(word) + + def addDefinition(self): + valid_entries = self._card.entries + insert_queue = {} + # Add Definition + definition_array = [] + # Extract the type of word this is + for entry in valid_entries: + this_def = entry.find("def") + if entry.find("fl") is None: + continue + fl = entry.find("fl").text + fl = _abbreviate_part_of_speech(fl) + + this_def.tail = "" + fl + "" # save the functional label (noun/verb/etc) in the tail + + # the tag will contain the word 'obsolete' if the term is not in use anymore. However, for some + # reason, the tag precedes the
that it is associated with instead of being a child. We need to + # associate it here so that later we can either remove or keep it regardless. + previous_was_ssl = False + for child in this_def: + # this is a kind of poor way of going about things, but the ElementTree API + # doesn't seem to offer an alternative. + if child.text == "obsolete" and child.tag == "ssl": + previous_was_ssl = True + if previous_was_ssl and child.tag == "dt": + child.tail = "obsolete" + previous_was_ssl = False + + definition_array.append(this_def) + + to_return = "" + for definition in definition_array: + last_functional_label = "" + medical_api_def = definition.findall("./sensb/sens/dt") + # sometimes there's not a definition directly (dt) but just a usage example (un): + if len(medical_api_def) == 1 and not medical_api_def[0].text: + medical_api_def = definition.findall("./sensb/sens/dt/un") + for dtTag in (definition.findall("dt") + medical_api_def): + + if dtTag.tail == "obsolete": + dtTag.tail = "" # take away the tail word so that when printing it does not show up. + if IGNORE_ARCHAIC: + continue + + # We don't really care for 'verbal illustrations' or 'usage notes', + # even though they are occasionally useful. + for usageNote in dtTag.findall("un"): + dtTag.remove(usageNote) + for verbalIllustration in dtTag.findall("vi"): + dtTag.remove(verbalIllustration) + + # Directional cross reference doesn't make sense for us + for dxTag in dtTag.findall("dx"): + for dxtTag in dxTag.findall("dxt"): + for dxnTag in dxtTag.findall("dxn"): + dxtTag.remove(dxnTag) + + # extract raw XML from
...
+ to_print = ET.tostring(dtTag, "", "xml").strip().decode("utf-8") + # attempt to remove 'synonymous cross reference tag' and replace with semicolon + to_print = to_print.replace("", "; ") + # attempt to remove 'Directional cross reference tag' and replace with semicolon + to_print = to_print.replace("", "; ") + # remove all other XML tags + to_print = re.sub('<[^>]*>', '', to_print) + # remove all colons, since they are usually useless and have been replaced with semicolons above + to_print = re.sub(':', '', to_print) + # erase space between semicolon and previous word, if exists, and strip any extraneous whitespace + to_print = to_print.replace(" ; ", "; ").strip() + to_print += "\n
" + + # add verb/noun/adjective + if last_functional_label != definition.tail: + to_print = definition.tail + " " + to_print + last_functional_label = definition.tail + to_return += to_print + + # final cleanup of tag bs + to_return = to_return.replace(". ; ", ". ") # as first definition after "n. " or "v. " + to_return = to_return.replace("\n; ", "\n") # as first definition after newline + self._card.fields[config.DEFINITION_FIELD] = to_return + + def addTranscription(self): + valid_entries = self._card.entries + # extract phonetic transcriptions for each entry and label them by part of speech + all_transcriptions = [] + for entry in valid_entries: + if entry.find("pr") is not None: + phonetic_transcription = entry.find("pr").text + + part_of_speech = entry.find("fl").text + part_of_speech = _abbreviate_part_of_speech(part_of_speech) + + row = f'{part_of_speech} \\{phonetic_transcription}\\' + all_transcriptions.append(row) + + to_print = "
".join(all_transcriptions) + + self._card.fields[config.PHONETIC_TRANSCRIPTION_FIELD] = to_print + + def addPronunciation(self): + valid_entries = self._card.entries + # Parse all unique pronunciations, and convert them to URLs as per http://goo.gl/nL0vte + all_sounds = [] + for entry in valid_entries: + for wav in entry.findall("sound/wav"): + raw_wav = wav.text + # API-specific URL conversions + if raw_wav[:3] == "bix": + mid_url = "bix" + elif raw_wav[:2] == "gg": + mid_url = "gg" + elif raw_wav[:1].isdigit(): + mid_url = "number" + else: + mid_url = raw_wav[:1] + wav_url = "http://media.merriam-webster.com/soundc11/" + mid_url + "/" + raw_wav + all_sounds.append(wav_url.strip()) + + # We want to make this a non-duplicate list, so that we only get unique sound files. + all_sounds = list(dict.fromkeys(all_sounds)) + final_pronounce_index = config.PRONUNCIATION_FIELD + if mw and False: + fields = mw.col.models.fieldNames(editor.note.model()) + for field in fields: + if '🔊' in field: + final_pronounce_index = fields.index(field) + break + + to_print = ''.join(all_sounds) + + self._card.fields[config.PRONUNCIATION_FIELD] = to_print diff --git a/AutoDefineAddon/libs/config.py b/AutoDefineAddon/libs/config.py new file mode 100644 index 0000000..4de0730 --- /dev/null +++ b/AutoDefineAddon/libs/config.py @@ -0,0 +1,37 @@ +# Get your unique API key by signing up at http://www.dictionaryapi.com/ +MERRIAM_WEBSTER_API_KEY = "YOUR_KEY_HERE" + +MERRIAM_WEBSTER_SPANISH_API_KEY = "YOUR_KEY_HERE" +# Index of field to insert definitions into (use -1 to turn off) +DEFINITION_FIELD = 1 + +# Ignore archaic/obsolete definitions? +IGNORE_ARCHAIC = True + +# Get your unique API key by signing up at http://www.dictionaryapi.com/ +MERRIAM_WEBSTER_MEDICAL_API_KEY = "YOUR_KEY_HERE" + +# Open a browser tab with an image search for the same word? +OPEN_IMAGES_IN_BROWSER = False + +# Which dictionary should AutoDefine prefer to get definitions from? Available options are COLLEGIATE and MEDICAL. +PREFERRED_DICTIONARY = "COLLEGIATE" + +# Index of field to insert pronunciations into (use -1 to turn off) +PRONUNCIATION_FIELD = 0 + +# Index of field to insert phonetic transcription into (use -1 to turn off) +PHONETIC_TRANSCRIPTION_FIELD = -1 + +# Index of field to insert pronunciations into (use -1 to turn off) +DEDICATED_INDIVIDUAL_BUTTONS = False + +PRIMARY_SHORTCUT = "ctrl+alt+e" + +DEFINE_ONLY_SHORTCUT = "" + +PRONOUNCE_ONLY_SHORTCUT = "" + +PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = "" + +PART_OF_SPEECH_ABBREVIATION = {"verb": "v.", "noun": "n.", "adverb": "adv.", "adjective": "adj."} diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py index 5b87ea7..a341006 100644 --- a/AutoDefineAddon/tests/tests_api.py +++ b/AutoDefineAddon/tests/tests_api.py @@ -5,19 +5,23 @@ myPath = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, myPath + '/../') +sys.path.insert(0, myPath + '/../libs') import urllib.request +import config +import cardbuilder import autodefine + def test_empty_entries_if_url_empty(): - assert autodefine.get_entries_from_api("word", "") == [] + assert cardbuilder.get_entries_from_api("word", "") == [] def test_non_empty_entries_if_url_correct(): word = "word" KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ urllib.parse.quote_plus(word) + "?key=" + KEY - assert autodefine.get_entries_from_api("word", url) != [] + assert cardbuilder.get_entries_from_api("word", url) != [] class MockNote: fields = ["", "", "", "", "WA"] @@ -32,16 +36,16 @@ def loadNote(self): def urlToLink(self, url): return url -autodefine.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" -autodefine.DEFINITION_FIELD = 1 -autodefine.PRONUNCIATION_FIELD = 2 -autodefine.PHONETIC_TRANSCRIPTION_FIELD = 3 +config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" +config.DEFINITION_FIELD = 1 +config.PRONUNCIATION_FIELD = 2 +config.PHONETIC_TRANSCRIPTION_FIELD = 3 -autodefine.PREFERRED_DICTIONARY = "COLLEGIATE" +config.PREFERRED_DICTIONARY = "COLLEGIATE" def test_can_serialize_xml_card(): word = "test" - cardBuilder = autodefine.CollegiateCardBuilder(word) + cardBuilder = cardbuilder.CollegiateCardBuilder(word) cardBuilder.addDefinition() cardBuilder.addPronunciation() cardBuilder.addTranscription() @@ -51,9 +55,9 @@ def test_can_serialize_xml_card(): card.serialize(editor) assert word == editor.note.fields[0] - assert editor.note.fields[autodefine.DEFINITION_FIELD].find("to put to test or proof") != -1 - assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[autodefine.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 + assert editor.note.fields[config.DEFINITION_FIELD].find("to put to test or proof") != -1 + assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 def test_can_get_definition(): editor = MockEditor() @@ -62,8 +66,7 @@ def test_can_get_definition(): autodefine._get_definition(editor, True, True, True) assert editor.note.fields[0] == word - assert editor.note.fields[autodefine.DEFINITION_FIELD].find("to put or thrust") != -1 - assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("insert") != -1 - assert editor.note.fields[autodefine.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[autodefine.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsÉ™rt") != -1 - + assert editor.note.fields[config.DEFINITION_FIELD].find("to put or thrust") != -1 + assert editor.note.fields[config.PRONUNCIATION_FIELD].find("insert") != -1 + assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsÉ™rt") != -1 From 135caac090b575d8747795f86bf6d2948577228b Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Sun, 6 Dec 2020 19:45:57 +0300 Subject: [PATCH 3/6] Add Spanish dictionary --- AutoDefineAddon/autodefine.py | 9 +++--- AutoDefineAddon/libs/cardbuilder.py | 49 +++++++++++++++++++++++++++-- AutoDefineAddon/tests/tests_api.py | 33 +++++++++++++++---- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/AutoDefineAddon/autodefine.py b/AutoDefineAddon/autodefine.py index c8e97a9..af34332 100644 --- a/AutoDefineAddon/autodefine.py +++ b/AutoDefineAddon/autodefine.py @@ -84,8 +84,8 @@ def get_definition_force_phonetic_transcription(editor): def validate_settings(): # ideally, we wouldn't have to force people to individually register, but the API limit is just 1000 calls/day. - if config.PREFERRED_DICTIONARY != "COLLEGIATE" and config.PREFERRED_DICTIONARY != "MEDICAL": - message = "Setting PREFERRED_DICTIONARY must be set to either COLLEGIATE or MEDICAL. Current setting: '%s'" \ + if not config.PREFERRED_DICTIONARY in ["COLLEGIATE", "MEDICAL", "SPANISH"]: + message = "Setting PREFERRED_DICTIONARY must be set to COLLEGIATE, MEDICAL or SPANISH. Current setting: '%s'" \ % config.PREFERRED_DICTIONARY showInfo(message) return @@ -145,6 +145,8 @@ def _get_definition(editor, cardBuilder = None if config.PREFERRED_DICTIONARY == "COLLEGIATE" or config.PREFERRED_DICTIONARY == "MEDICAL": cardBuilder = cardbuilder.CollegiateCardBuilder(word) + elif config.PREFERRED_DICTIONARY == "SPANISH": + cardBuilder = cardbuilder.SpanishCardBuilder(word) # Add Vocal Pronunciation if (not force_definition and not force_phonetic_transcription and PRONUNCIATION_FIELD > -1) or force_pronounce: cardBuilder.addPronunciation() @@ -159,7 +161,6 @@ def _get_definition(editor, if (not force_pronounce and not force_phonetic_transcription and DEFINITION_FIELD > -1) or force_definition: cardBuilder.addDefinition() - # Insert each queue into the considered field card = cardBuilder.getCard() card.serialize(editor) if config.OPEN_IMAGES_IN_BROWSER: @@ -168,8 +169,6 @@ def _get_definition(editor, _focus_zero_field(editor) - - # via https://stackoverflow.com/a/12982689 def clean_html(raw_html): return re.sub(re.compile('<.*?>'), '', raw_html).replace(" ", " ") diff --git a/AutoDefineAddon/libs/cardbuilder.py b/AutoDefineAddon/libs/cardbuilder.py index c61be36..8b287af 100644 --- a/AutoDefineAddon/libs/cardbuilder.py +++ b/AutoDefineAddon/libs/cardbuilder.py @@ -22,6 +22,7 @@ from http.client import RemoteDisconnected from urllib.error import URLError from xml.etree import ElementTree as ET +import json import sys sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") @@ -108,8 +109,12 @@ def get_entries_from_api(word, url): return [] if "Results not found" in returned.decode("UTF-8"): return [] - etree = ET.fromstring(returned) - return etree.findall("entry") + if url.find("xml") != -1: + etree = ET.fromstring(returned) + return etree.findall("entry") + elif url.find("json") != -1: + content = json.loads(returned) + return content except URLError: return [] except ValueError: @@ -295,3 +300,43 @@ def addPronunciation(self): to_print = ''.join(all_sounds) self._card.fields[config.PRONUNCIATION_FIELD] = to_print + +class SpanishCardBuilder(CardBuilder): + def __init__(self, word): + super().__init__(word) + url = "https://dictionaryapi.com/api/v3/references/spanish/json/" + word + "?key=" + config.MERRIAM_WEBSTER_SPANISH_API_KEY + entries = get_entries_from_api(word, url) + if entries: + self._card.entries = entries[0] + else: + self._card.entries = [] + + def addDefinition(self): + entries = self._card.entries + if entries: + definitions = [] + sseq_list = entries["def"][0]["sseq"] + for sseq in sseq_list: + dt = sseq[0][1]["dt"] + for elem in dt: + if elem[0] == "text": + definition = elem[1].replace("{bc}", "") \ + .replace("{a_link|", "") \ + .replace("}", "") \ + .replace("{sx|", "") \ + .replace("|", "") \ + .strip() + definitions.append(definition) + text = "" + for definition in definitions: + text += definition + '\n
' + self._card.fields[config.DEFINITION_FIELD] = text + + + def addPronunciation(self): + entries = self._card.entries + if entries: + pronunciations = [] + audio = entries["hwi"]["prs"][0]["sound"]["audio"] + if audio: + self._card.fields[config.PRONUNCIATION_FIELD] = "https://media.merriam-webster.com/audio/prons/es/me/wav/" + audio[0] + "/" + audio + ".wav" diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py index a341006..55cfa46 100644 --- a/AutoDefineAddon/tests/tests_api.py +++ b/AutoDefineAddon/tests/tests_api.py @@ -12,7 +12,6 @@ import cardbuilder import autodefine - def test_empty_entries_if_url_empty(): assert cardbuilder.get_entries_from_api("word", "") == [] @@ -24,26 +23,25 @@ def test_non_empty_entries_if_url_correct(): assert cardbuilder.get_entries_from_api("word", url) != [] class MockNote: - fields = ["", "", "", "", "WA"] + fields = ["", "", "", "", ""] class MockEditor: note = MockNote() web = False def loadNote(self): - print("LoadNote") + pass def urlToLink(self, url): return url -config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" config.DEFINITION_FIELD = 1 config.PRONUNCIATION_FIELD = 2 config.PHONETIC_TRANSCRIPTION_FIELD = 3 -config.PREFERRED_DICTIONARY = "COLLEGIATE" - def test_can_serialize_xml_card(): + config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + config.PREFERRED_DICTIONARY = "COLLEGIATE" word = "test" cardBuilder = cardbuilder.CollegiateCardBuilder(word) cardBuilder.addDefinition() @@ -60,13 +58,36 @@ def test_can_serialize_xml_card(): assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 def test_can_get_definition(): + config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + config.PREFERRED_DICTIONARY = "COLLEGIATE" editor = MockEditor() word = "insert" editor.note.fields[0] = word autodefine._get_definition(editor, True, True, True) + print(editor.note.fields) + assert editor.note.fields[0] == word assert editor.note.fields[config.DEFINITION_FIELD].find("to put or thrust") != -1 assert editor.note.fields[config.PRONUNCIATION_FIELD].find("insert") != -1 assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsərt") != -1 + +def test_can_serialize_json_spanish_card(): + config.MERRIAM_WEBSTER_SPANISH_API_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" + config.PREFERRED_DICTIONARY = "SPANISH" + word = "entender" + cardBuilder = cardbuilder.SpanishCardBuilder(word) + cardBuilder.addDefinition() + cardBuilder.addPronunciation() + cardBuilder.addTranscription() + card = cardBuilder.getCard() + + editor = MockEditor() + card.serialize(editor) + + assert word == editor.note.fields[0] + assert editor.note.fields[config.DEFINITION_FIELD].find("understand") != -1 + assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[config.PRONUNCIATION_FIELD].find("enten") != -1 + assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD] == "" From d09c694bc9954a6ca1724732f7a9dfbbdc79fa31 Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Sun, 6 Dec 2020 19:55:24 +0300 Subject: [PATCH 4/6] Rename config -> settings and fixes after test in anki --- AutoDefineAddon/autodefine.py | 80 +++++++++---------- AutoDefineAddon/libs/cardbuilder.py | 37 ++++----- .../libs/{config.py => settings.py} | 0 AutoDefineAddon/tests/tests_api.py | 42 +++++----- 4 files changed, 75 insertions(+), 84 deletions(-) rename AutoDefineAddon/libs/{config.py => settings.py} (100%) diff --git a/AutoDefineAddon/autodefine.py b/AutoDefineAddon/autodefine.py index af34332..c6fa7a7 100644 --- a/AutoDefineAddon/autodefine.py +++ b/AutoDefineAddon/autodefine.py @@ -23,12 +23,9 @@ from urllib.error import URLError from xml.etree import ElementTree as ET -import sys -sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") - -import config -import webbrowser -import cardbuilder +from .libs import settings +from .libs import webbrowser +from .libs import cardbuilder # --------------------------------- SETTINGS --------------------------------- @@ -84,20 +81,20 @@ def get_definition_force_phonetic_transcription(editor): def validate_settings(): # ideally, we wouldn't have to force people to individually register, but the API limit is just 1000 calls/day. - if not config.PREFERRED_DICTIONARY in ["COLLEGIATE", "MEDICAL", "SPANISH"]: + if not settings.PREFERRED_DICTIONARY in ["COLLEGIATE", "MEDICAL", "SPANISH"]: message = "Setting PREFERRED_DICTIONARY must be set to COLLEGIATE, MEDICAL or SPANISH. Current setting: '%s'" \ - % config.PREFERRED_DICTIONARY + % settings.PREFERRED_DICTIONARY showInfo(message) return - if config.PREFERRED_DICTIONARY == "MEDICAL" and config.MERRIAM_WEBSTER_MEDICAL_API_KEY == "YOUR_KEY_HERE": + if settings.PREFERRED_DICTIONARY == "MEDICAL" and settings.MERRIAM_WEBSTER_MEDICAL_API_KEY == "YOUR_KEY_HERE": message = "The preferred dictionary was set to MEDICAL, but no API key was provided.\n" \ "Please register for one at www.dictionaryapi.com." showInfo(message) webbrowser.open("https://www.dictionaryapi.com/", 0, False) return - if config.MERRIAM_WEBSTER_API_KEY == "YOUR_KEY_HERE": + if settings.MERRIAM_WEBSTER_API_KEY == "YOUR_KEY_HERE": message = "AutoDefine requires use of Merriam-Webster's Collegiate Dictionary with Audio API. " \ "To get functionality working:\n" \ "1. Go to www.dictionaryapi.com and sign up for an account, requesting access to " \ @@ -143,27 +140,27 @@ def _get_definition(editor, insert_queue = {} cardBuilder = None - if config.PREFERRED_DICTIONARY == "COLLEGIATE" or config.PREFERRED_DICTIONARY == "MEDICAL": + if settings.PREFERRED_DICTIONARY == "COLLEGIATE" or settings.PREFERRED_DICTIONARY == "MEDICAL": cardBuilder = cardbuilder.CollegiateCardBuilder(word) - elif config.PREFERRED_DICTIONARY == "SPANISH": + elif settings.PREFERRED_DICTIONARY == "SPANISH": cardBuilder = cardbuilder.SpanishCardBuilder(word) # Add Vocal Pronunciation - if (not force_definition and not force_phonetic_transcription and PRONUNCIATION_FIELD > -1) or force_pronounce: + if (not force_definition and not force_phonetic_transcription and settings.PRONUNCIATION_FIELD > -1) or force_pronounce: cardBuilder.addPronunciation() # Add Phonetic Transcription - if (not force_definition and not force_pronounce and PHONETIC_TRANSCRIPTION_FIELD > -1) or \ + if (not force_definition and not force_pronounce and settings.PHONETIC_TRANSCRIPTION_FIELD > -1) or \ force_phonetic_transcription: cardBuilder.addTranscription() # Add Definition - if (not force_pronounce and not force_phonetic_transcription and DEFINITION_FIELD > -1) or force_definition: + if (not force_pronounce and not force_phonetic_transcription and settings.DEFINITION_FIELD > -1) or force_definition: cardBuilder.addDefinition() card = cardBuilder.getCard() card.serialize(editor) - if config.OPEN_IMAGES_IN_BROWSER: + if settings.OPEN_IMAGES_IN_BROWSER: webbrowser.open("https://www.google.com/search?q= " + word + "&safe=off&tbm=isch&tbs=isz:lt,islt:xga", 0, False) _focus_zero_field(editor) @@ -179,55 +176,53 @@ def setup_buttons(buttons, editor): cmd="AD", func=get_definition, tip="AutoDefine Word (%s)" % - ("no shortcut" if PRIMARY_SHORTCUT == "" else PRIMARY_SHORTCUT), + ("no shortcut" if settings.PRIMARY_SHORTCUT == "" else settings.PRIMARY_SHORTCUT), toggleable=False, label="", - keys=PRIMARY_SHORTCUT, + keys=settings.PRIMARY_SHORTCUT, disables=False) define_button = editor.addButton(icon="", cmd="D", func=get_definition_force_definition, tip="AutoDefine: Definition only (%s)" % - ("no shortcut" if DEFINE_ONLY_SHORTCUT == "" else DEFINE_ONLY_SHORTCUT), + ("no shortcut" if settings.DEFINE_ONLY_SHORTCUT == "" else settings.DEFINE_ONLY_SHORTCUT), toggleable=False, label="", - keys=DEFINE_ONLY_SHORTCUT, + keys=settings.DEFINE_ONLY_SHORTCUT, disables=False) pronounce_button = editor.addButton(icon="", cmd="P", func=get_definition_force_pronunciation, tip="AutoDefine: Pronunciation only (%s)" % ("no shortcut" - if PRONOUNCE_ONLY_SHORTCUT == "" - else PRONOUNCE_ONLY_SHORTCUT), + if settings.PRONOUNCE_ONLY_SHORTCUT == "" + else settings.PRONOUNCE_ONLY_SHORTCUT), toggleable=False, label="", - keys=PRONOUNCE_ONLY_SHORTCUT, + keys=settings.PRONOUNCE_ONLY_SHORTCUT, disables=False) phonetic_transcription_button = editor.addButton(icon="", cmd="ə", func=get_definition_force_phonetic_transcription, tip="AutoDefine: Phonetic Transcription only (%s)" % ("no shortcut" - if PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT == "" - else PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT), + if settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT == "" + else settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT), toggleable=False, label="", - keys=PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT, + keys=settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT, disables=False) buttons.append(both_button) - if DEDICATED_INDIVIDUAL_BUTTONS: + if settings.DEDICATED_INDIVIDUAL_BUTTONS: buttons.append(define_button) buttons.append(pronounce_button) buttons.append(phonetic_transcription_button) return buttons - addHook("setupEditorButtons", setup_buttons) -''' if getattr(mw.addonManager, "getConfig", None): config = mw.addonManager.getConfig(__name__) if '1 required' in config and 'MERRIAM_WEBSTER_API_KEY' in config['1 required']: - MERRIAM_WEBSTER_API_KEY = config['1 required']['MERRIAM_WEBSTER_API_KEY'] + settings.MERRIAM_WEBSTER_API_KEY = config['1 required']['MERRIAM_WEBSTER_API_KEY'] else: showInfo("AutoDefine: The schema of the configuration has changed in a backwards-incompatible way.\n" "Please remove and re-download the AutoDefine Add-on.") @@ -235,30 +230,29 @@ def setup_buttons(buttons, editor): if '2 extra' in config: extra = config['2 extra'] if 'DEDICATED_INDIVIDUAL_BUTTONS' in extra: - DEDICATED_INDIVIDUAL_BUTTONS = extra['DEDICATED_INDIVIDUAL_BUTTONS'] + settings.DEDICATED_INDIVIDUAL_BUTTONS = extra['DEDICATED_INDIVIDUAL_BUTTONS'] if 'DEFINITION_FIELD' in extra: - DEFINITION_FIELD = extra['DEFINITION_FIELD'] + settings.DEFINITION_FIELD = extra['DEFINITION_FIELD'] if 'IGNORE_ARCHAIC' in extra: - IGNORE_ARCHAIC = extra['IGNORE_ARCHAIC'] + settings.IGNORE_ARCHAIC = extra['IGNORE_ARCHAIC'] if 'MERRIAM_WEBSTER_MEDICAL_API_KEY' in extra: - MERRIAM_WEBSTER_MEDICAL_API_KEY = extra['MERRIAM_WEBSTER_MEDICAL_API_KEY'] + settings.MERRIAM_WEBSTER_MEDICAL_API_KEY = extra['MERRIAM_WEBSTER_MEDICAL_API_KEY'] if 'OPEN_IMAGES_IN_BROWSER' in extra: - OPEN_IMAGES_IN_BROWSER = extra['OPEN_IMAGES_IN_BROWSER'] + settings.OPEN_IMAGES_IN_BROWSER = extra['OPEN_IMAGES_IN_BROWSER'] if 'PREFERRED_DICTIONARY' in extra: - PREFERRED_DICTIONARY = extra['PREFERRED_DICTIONARY'] + settings.PREFERRED_DICTIONARY = extra['PREFERRED_DICTIONARY'] if 'PRONUNCIATION_FIELD' in extra: - PRONUNCIATION_FIELD = extra['PRONUNCIATION_FIELD'] + settings.PRONUNCIATION_FIELD = extra['PRONUNCIATION_FIELD'] if 'PHONETIC_TRANSCRIPTION_FIELD' in extra: - PHONETIC_TRANSCRIPTION_FIELD = extra['PHONETIC_TRANSCRIPTION_FIELD'] + settings.PHONETIC_TRANSCRIPTION_FIELD = extra['PHONETIC_TRANSCRIPTION_FIELD'] if '3 shortcuts' in config: shortcuts = config['3 shortcuts'] if '1 PRIMARY_SHORTCUT' in shortcuts: - PRIMARY_SHORTCUT = shortcuts['1 PRIMARY_SHORTCUT'] + settings.PRIMARY_SHORTCUT = shortcuts['1 PRIMARY_SHORTCUT'] if '2 DEFINE_ONLY_SHORTCUT' in shortcuts: - DEFINE_ONLY_SHORTCUT = shortcuts['2 DEFINE_ONLY_SHORTCUT'] + settings.DEFINE_ONLY_SHORTCUT = shortcuts['2 DEFINE_ONLY_SHORTCUT'] if '3 PRONOUNCE_ONLY_SHORTCUT' in shortcuts: - PRONOUNCE_ONLY_SHORTCUT = shortcuts['3 PRONOUNCE_ONLY_SHORTCUT'] + settings.PRONOUNCE_ONLY_SHORTCUT = shortcuts['3 PRONOUNCE_ONLY_SHORTCUT'] if '4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT' in shortcuts: - PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = shortcuts['4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT'] -''' + settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = shortcuts['4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT'] diff --git a/AutoDefineAddon/libs/cardbuilder.py b/AutoDefineAddon/libs/cardbuilder.py index 8b287af..0b165a6 100644 --- a/AutoDefineAddon/libs/cardbuilder.py +++ b/AutoDefineAddon/libs/cardbuilder.py @@ -24,29 +24,26 @@ from xml.etree import ElementTree as ET import json -import sys -sys.path.append("/home/artem/workspace/AutoDefine/AutoDefineAddon/libs") - -import webbrowser -import config +from . import settings +from . import webbrowser def _abbreviate_part_of_speech(part_of_speech): - if part_of_speech in config.PART_OF_SPEECH_ABBREVIATION.keys(): - part_of_speech = config.PART_OF_SPEECH_ABBREVIATION[part_of_speech] + if part_of_speech in settings.PART_OF_SPEECH_ABBREVIATION.keys(): + part_of_speech = settings.PART_OF_SPEECH_ABBREVIATION[part_of_speech] return part_of_speech def get_preferred_valid_entries(word): collegiate_url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ - urllib.parse.quote_plus(word) + "?key=" + config.MERRIAM_WEBSTER_API_KEY + urllib.parse.quote_plus(word) + "?key=" + settings.MERRIAM_WEBSTER_API_KEY medical_url = "https://www.dictionaryapi.com/api/references/medical/v2/xml/" + \ - urllib.parse.quote_plus(word) + "?key=" + config.MERRIAM_WEBSTER_MEDICAL_API_KEY + urllib.parse.quote_plus(word) + "?key=" + settings.MERRIAM_WEBSTER_MEDICAL_API_KEY all_collegiate_entries = get_entries_from_api(word, collegiate_url) all_medical_entries = get_entries_from_api(word, medical_url) potential_unified = set() - if config.PREFERRED_DICTIONARY == "COLLEGIATE": + if settings.PREFERRED_DICTIONARY == "COLLEGIATE": entries = filter_entries_lower_and_potential(word, all_collegiate_entries) potential_unified |= entries.potential if not entries.valid: @@ -152,13 +149,13 @@ def __init__(self, word): self._card.fields[0] = word def addDefinition(self): - self._card.fields[config.DEFINITION_FIELD] = "" + self._card.fields[settings.DEFINITION_FIELD] = "" def addTranscription(self): - self._card.fields[config.PHONETIC_TRANSCRIPTION_FIELD] = "" + self._card.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] = "" def addPronunciation(self): - self._card.fields[config.PRONUNCIATION_FIELD] = "" + self._card.fields[settings.PRONUNCIATION_FIELD] = "" def getCard(self) -> Card: return self._card @@ -248,7 +245,7 @@ def addDefinition(self): # final cleanup of tag bs to_return = to_return.replace(". ; ", ". ") # as first definition after "n. " or "v. " to_return = to_return.replace("\n; ", "\n") # as first definition after newline - self._card.fields[config.DEFINITION_FIELD] = to_return + self._card.fields[settings.DEFINITION_FIELD] = to_return def addTranscription(self): valid_entries = self._card.entries @@ -266,7 +263,7 @@ def addTranscription(self): to_print = "
".join(all_transcriptions) - self._card.fields[config.PHONETIC_TRANSCRIPTION_FIELD] = to_print + self._card.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] = to_print def addPronunciation(self): valid_entries = self._card.entries @@ -289,7 +286,7 @@ def addPronunciation(self): # We want to make this a non-duplicate list, so that we only get unique sound files. all_sounds = list(dict.fromkeys(all_sounds)) - final_pronounce_index = config.PRONUNCIATION_FIELD + final_pronounce_index = settings.PRONUNCIATION_FIELD if mw and False: fields = mw.col.models.fieldNames(editor.note.model()) for field in fields: @@ -299,12 +296,12 @@ def addPronunciation(self): to_print = ''.join(all_sounds) - self._card.fields[config.PRONUNCIATION_FIELD] = to_print + self._card.fields[settings.PRONUNCIATION_FIELD] = to_print class SpanishCardBuilder(CardBuilder): def __init__(self, word): super().__init__(word) - url = "https://dictionaryapi.com/api/v3/references/spanish/json/" + word + "?key=" + config.MERRIAM_WEBSTER_SPANISH_API_KEY + url = "https://dictionaryapi.com/api/v3/references/spanish/json/" + word + "?key=" + settings.MERRIAM_WEBSTER_SPANISH_API_KEY entries = get_entries_from_api(word, url) if entries: self._card.entries = entries[0] @@ -330,7 +327,7 @@ def addDefinition(self): text = "" for definition in definitions: text += definition + '\n
' - self._card.fields[config.DEFINITION_FIELD] = text + self._card.fields[settings.DEFINITION_FIELD] = text def addPronunciation(self): @@ -339,4 +336,4 @@ def addPronunciation(self): pronunciations = [] audio = entries["hwi"]["prs"][0]["sound"]["audio"] if audio: - self._card.fields[config.PRONUNCIATION_FIELD] = "https://media.merriam-webster.com/audio/prons/es/me/wav/" + audio[0] + "/" + audio + ".wav" + self._card.fields[settings.PRONUNCIATION_FIELD] = "https://media.merriam-webster.com/audio/prons/es/me/wav/" + audio[0] + "/" + audio + ".wav" diff --git a/AutoDefineAddon/libs/config.py b/AutoDefineAddon/libs/settings.py similarity index 100% rename from AutoDefineAddon/libs/config.py rename to AutoDefineAddon/libs/settings.py diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py index 55cfa46..530ccae 100644 --- a/AutoDefineAddon/tests/tests_api.py +++ b/AutoDefineAddon/tests/tests_api.py @@ -8,7 +8,7 @@ sys.path.insert(0, myPath + '/../libs') import urllib.request -import config +import settings import cardbuilder import autodefine @@ -35,13 +35,13 @@ def loadNote(self): def urlToLink(self, url): return url -config.DEFINITION_FIELD = 1 -config.PRONUNCIATION_FIELD = 2 -config.PHONETIC_TRANSCRIPTION_FIELD = 3 +settings.DEFINITION_FIELD = 1 +settings.PRONUNCIATION_FIELD = 2 +settings.PHONETIC_TRANSCRIPTION_FIELD = 3 def test_can_serialize_xml_card(): - config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" - config.PREFERRED_DICTIONARY = "COLLEGIATE" + settings.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + settings.PREFERRED_DICTIONARY = "COLLEGIATE" word = "test" cardBuilder = cardbuilder.CollegiateCardBuilder(word) cardBuilder.addDefinition() @@ -53,13 +53,13 @@ def test_can_serialize_xml_card(): card.serialize(editor) assert word == editor.note.fields[0] - assert editor.note.fields[config.DEFINITION_FIELD].find("to put to test or proof") != -1 - assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 + assert editor.note.fields[settings.DEFINITION_FIELD].find("to put to test or proof") != -1 + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 def test_can_get_definition(): - config.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" - config.PREFERRED_DICTIONARY = "COLLEGIATE" + settings.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + settings.PREFERRED_DICTIONARY = "COLLEGIATE" editor = MockEditor() word = "insert" editor.note.fields[0] = word @@ -68,14 +68,14 @@ def test_can_get_definition(): print(editor.note.fields) assert editor.note.fields[0] == word - assert editor.note.fields[config.DEFINITION_FIELD].find("to put or thrust") != -1 - assert editor.note.fields[config.PRONUNCIATION_FIELD].find("insert") != -1 - assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsərt") != -1 + assert editor.note.fields[settings.DEFINITION_FIELD].find("to put or thrust") != -1 + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("insert") != -1 + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsərt") != -1 def test_can_serialize_json_spanish_card(): - config.MERRIAM_WEBSTER_SPANISH_API_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" - config.PREFERRED_DICTIONARY = "SPANISH" + settings.MERRIAM_WEBSTER_SPANISH_API_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" + settings.PREFERRED_DICTIONARY = "SPANISH" word = "entender" cardBuilder = cardbuilder.SpanishCardBuilder(word) cardBuilder.addDefinition() @@ -87,7 +87,7 @@ def test_can_serialize_json_spanish_card(): card.serialize(editor) assert word == editor.note.fields[0] - assert editor.note.fields[config.DEFINITION_FIELD].find("understand") != -1 - assert editor.note.fields[config.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[config.PRONUNCIATION_FIELD].find("enten") != -1 - assert editor.note.fields[config.PHONETIC_TRANSCRIPTION_FIELD] == "" + assert editor.note.fields[settings.DEFINITION_FIELD].find("understand") != -1 + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("enten") != -1 + assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] == "" From bbea38b49b1106a0a3c77d26520575dde28e4c29 Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Mon, 7 Dec 2020 23:08:21 +0300 Subject: [PATCH 5/6] Various fixes to make it working in anki --- AutoDefineAddon/autodefine.py | 94 ++++++++++++++--------------- AutoDefineAddon/config.json | 2 +- AutoDefineAddon/config.md | 2 +- AutoDefineAddon/libs/cardbuilder.py | 84 +++++++++++++++++--------- AutoDefineAddon/libs/settings.py | 4 +- AutoDefineAddon/tests/tests_api.py | 48 ++++++++++++--- 6 files changed, 149 insertions(+), 85 deletions(-) diff --git a/AutoDefineAddon/autodefine.py b/AutoDefineAddon/autodefine.py index c6fa7a7..ac9e84e 100644 --- a/AutoDefineAddon/autodefine.py +++ b/AutoDefineAddon/autodefine.py @@ -23,12 +23,13 @@ from urllib.error import URLError from xml.etree import ElementTree as ET -from .libs import settings -from .libs import webbrowser -from .libs import cardbuilder -# --------------------------------- SETTINGS --------------------------------- - - +import sys +import os +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath + '/libs') +import settings +import webbrowser +import cardbuilder # Collegiate Dictionary API XML documentation: http://goo.gl/LuD83A # Medical Dictionary API XML documentation: https://goo.gl/akvkbB @@ -149,11 +150,9 @@ def _get_definition(editor, cardBuilder.addPronunciation() # Add Phonetic Transcription - if (not force_definition and not force_pronounce and settings.PHONETIC_TRANSCRIPTION_FIELD > -1) or \ - force_phonetic_transcription: + if (not force_definition and not force_pronounce and settings.PHONETIC_TRANSCRIPTION_FIELD > -1) or force_phonetic_transcription: cardBuilder.addTranscription() - # Add Definition if (not force_pronounce and not force_phonetic_transcription and settings.DEFINITION_FIELD > -1) or force_definition: cardBuilder.addDefinition() @@ -218,41 +217,42 @@ def setup_buttons(buttons, editor): buttons.append(phonetic_transcription_button) return buttons -addHook("setupEditorButtons", setup_buttons) -if getattr(mw.addonManager, "getConfig", None): - config = mw.addonManager.getConfig(__name__) - if '1 required' in config and 'MERRIAM_WEBSTER_API_KEY' in config['1 required']: - settings.MERRIAM_WEBSTER_API_KEY = config['1 required']['MERRIAM_WEBSTER_API_KEY'] - else: - showInfo("AutoDefine: The schema of the configuration has changed in a backwards-incompatible way.\n" - "Please remove and re-download the AutoDefine Add-on.") - - if '2 extra' in config: - extra = config['2 extra'] - if 'DEDICATED_INDIVIDUAL_BUTTONS' in extra: - settings.DEDICATED_INDIVIDUAL_BUTTONS = extra['DEDICATED_INDIVIDUAL_BUTTONS'] - if 'DEFINITION_FIELD' in extra: - settings.DEFINITION_FIELD = extra['DEFINITION_FIELD'] - if 'IGNORE_ARCHAIC' in extra: - settings.IGNORE_ARCHAIC = extra['IGNORE_ARCHAIC'] - if 'MERRIAM_WEBSTER_MEDICAL_API_KEY' in extra: - settings.MERRIAM_WEBSTER_MEDICAL_API_KEY = extra['MERRIAM_WEBSTER_MEDICAL_API_KEY'] - if 'OPEN_IMAGES_IN_BROWSER' in extra: - settings.OPEN_IMAGES_IN_BROWSER = extra['OPEN_IMAGES_IN_BROWSER'] - if 'PREFERRED_DICTIONARY' in extra: - settings.PREFERRED_DICTIONARY = extra['PREFERRED_DICTIONARY'] - if 'PRONUNCIATION_FIELD' in extra: - settings.PRONUNCIATION_FIELD = extra['PRONUNCIATION_FIELD'] - if 'PHONETIC_TRANSCRIPTION_FIELD' in extra: - settings.PHONETIC_TRANSCRIPTION_FIELD = extra['PHONETIC_TRANSCRIPTION_FIELD'] - - if '3 shortcuts' in config: - shortcuts = config['3 shortcuts'] - if '1 PRIMARY_SHORTCUT' in shortcuts: - settings.PRIMARY_SHORTCUT = shortcuts['1 PRIMARY_SHORTCUT'] - if '2 DEFINE_ONLY_SHORTCUT' in shortcuts: - settings.DEFINE_ONLY_SHORTCUT = shortcuts['2 DEFINE_ONLY_SHORTCUT'] - if '3 PRONOUNCE_ONLY_SHORTCUT' in shortcuts: - settings.PRONOUNCE_ONLY_SHORTCUT = shortcuts['3 PRONOUNCE_ONLY_SHORTCUT'] - if '4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT' in shortcuts: - settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = shortcuts['4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT'] +if not settings.TEST_MODE: + addHook("setupEditorButtons", setup_buttons) + if getattr(mw.addonManager, "getConfig", None): + config = mw.addonManager.getConfig(__name__) + if '1 required' in config and 'MERRIAM_WEBSTER_API_KEY' in config['1 required']: + settings.MERRIAM_WEBSTER_API_KEY = config['1 required']['MERRIAM_WEBSTER_API_KEY'] + else: + showInfo("AutoDefine: The schema of the configuration has changed in a backwards-incompatible way.\n" + "Please remove and re-download the AutoDefine Add-on.") + + if '2 extra' in config: + extra = config['2 extra'] + if 'DEDICATED_INDIVIDUAL_BUTTONS' in extra: + settings.DEDICATED_INDIVIDUAL_BUTTONS = extra['DEDICATED_INDIVIDUAL_BUTTONS'] + if 'DEFINITION_FIELD' in extra: + settings.DEFINITION_FIELD = extra['DEFINITION_FIELD'] + if 'IGNORE_ARCHAIC' in extra: + settings.IGNORE_ARCHAIC = extra['IGNORE_ARCHAIC'] + if 'MERRIAM_WEBSTER_MEDICAL_API_KEY' in extra: + settings.MERRIAM_WEBSTER_MEDICAL_API_KEY = extra['MERRIAM_WEBSTER_MEDICAL_API_KEY'] + if 'OPEN_IMAGES_IN_BROWSER' in extra: + settings.OPEN_IMAGES_IN_BROWSER = extra['OPEN_IMAGES_IN_BROWSER'] + if 'PREFERRED_DICTIONARY' in extra: + settings.PREFERRED_DICTIONARY = extra['PREFERRED_DICTIONARY'] + if 'PRONUNCIATION_FIELD' in extra: + settings.PRONUNCIATION_FIELD = extra['PRONUNCIATION_FIELD'] + if 'PHONETIC_TRANSCRIPTION_FIELD' in extra: + settings.PHONETIC_TRANSCRIPTION_FIELD = extra['PHONETIC_TRANSCRIPTION_FIELD'] + + if '3 shortcuts' in config: + shortcuts = config['3 shortcuts'] + if '1 PRIMARY_SHORTCUT' in shortcuts: + settings.PRIMARY_SHORTCUT = shortcuts['1 PRIMARY_SHORTCUT'] + if '2 DEFINE_ONLY_SHORTCUT' in shortcuts: + settings.DEFINE_ONLY_SHORTCUT = shortcuts['2 DEFINE_ONLY_SHORTCUT'] + if '3 PRONOUNCE_ONLY_SHORTCUT' in shortcuts: + settings.PRONOUNCE_ONLY_SHORTCUT = shortcuts['3 PRONOUNCE_ONLY_SHORTCUT'] + if '4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT' in shortcuts: + settings.PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = shortcuts['4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT'] diff --git a/AutoDefineAddon/config.json b/AutoDefineAddon/config.json index 2101091..9853f82 100644 --- a/AutoDefineAddon/config.json +++ b/AutoDefineAddon/config.json @@ -18,4 +18,4 @@ "3 PRONOUNCE_ONLY_SHORTCUT": "", "4 PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT": "" } -} \ No newline at end of file +} diff --git a/AutoDefineAddon/config.md b/AutoDefineAddon/config.md index 18ef280..e1d1d41 100644 --- a/AutoDefineAddon/config.md +++ b/AutoDefineAddon/config.md @@ -10,4 +10,4 @@ * `PRIMARY_SHORTCUT`: Keyboard shortcut to run default AutoDefine. * `DEFINE_ONLY_SHORTCUT`: Keyboard shortcut for definition-only button (must enable `DEDICATED_INDIVIDUAL_BUTTONS`). * `PRONOUNCE_ONLY_SHORTCUT`: Keyboard shortcut for pronunciation-only button (must enable `DEDICATED_INDIVIDUAL_BUTTONS`). -* `PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT`: Keyboard shortcut for phonetic-transcription-only button (must enable `DEDICATED_INDIVIDUAL_BUTTONS`). \ No newline at end of file +* `PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT`: Keyboard shortcut for phonetic-transcription-only button (must enable `DEDICATED_INDIVIDUAL_BUTTONS`). diff --git a/AutoDefineAddon/libs/cardbuilder.py b/AutoDefineAddon/libs/cardbuilder.py index 0b165a6..6d35a58 100644 --- a/AutoDefineAddon/libs/cardbuilder.py +++ b/AutoDefineAddon/libs/cardbuilder.py @@ -15,17 +15,19 @@ import urllib.error import urllib.parse import urllib.request -from anki import version -from anki.hooks import addHook from aqt import mw -from aqt.utils import showInfo, tooltip from http.client import RemoteDisconnected from urllib.error import URLError from xml.etree import ElementTree as ET import json -from . import settings -from . import webbrowser +import sys + +myPath = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, myPath) + +import settings +import webbrowser def _abbreviate_part_of_speech(part_of_speech): if part_of_speech in settings.PART_OF_SPEECH_ABBREVIATION.keys(): @@ -301,39 +303,65 @@ def addPronunciation(self): class SpanishCardBuilder(CardBuilder): def __init__(self, word): super().__init__(word) - url = "https://dictionaryapi.com/api/v3/references/spanish/json/" + word + "?key=" + settings.MERRIAM_WEBSTER_SPANISH_API_KEY + url = "https://dictionaryapi.com/api/v3/references/spanish/json/" + word + "?key=" + settings.MERRIAM_WEBSTER_API_KEY entries = get_entries_from_api(word, url) if entries: - self._card.entries = entries[0] + self._card.entries = entries else: self._card.entries = [] def addDefinition(self): entries = self._card.entries if entries: - definitions = [] - sseq_list = entries["def"][0]["sseq"] - for sseq in sseq_list: - dt = sseq[0][1]["dt"] - for elem in dt: - if elem[0] == "text": - definition = elem[1].replace("{bc}", "") \ - .replace("{a_link|", "") \ - .replace("}", "") \ - .replace("{sx|", "") \ - .replace("|", "") \ - .strip() - definitions.append(definition) text = "" - for definition in definitions: - text += definition + '\n
' + for entry in entries: + word_id = entry["meta"]["id"] + # word_id must contain word we are looking + # otherwise this is a definition for another word + word = self._card.fields[0] + if word_id.find(word) == -1: + continue + fl = entry["fl"] + if fl: + fl = _abbreviate_part_of_speech(fl) + text += "" + fl + "\n
" + + definitions = [] + sseq_list = entry["def"][0]["sseq"] + if sseq_list: + for sseq in sseq_list: + dt = sseq[0][1]["dt"] + for elem in dt: + if elem[0] == "text": + definition = elem[1] + if definition.find("sx") != -1: + definition = definition.replace("{sx|", "syn:") \ + .replace("}", ",", 1) + + definition = definition.replace("{bc}", "") \ + .replace("{a_link|", "") \ + .replace("}", "") \ + .replace("{sx|", "") \ + .replace("|", "") \ + .strip() + definitions.append(definition) + for definition in definitions: + text += definition + '\n
' + self._card.fields[settings.DEFINITION_FIELD] = text def addPronunciation(self): - entries = self._card.entries - if entries: - pronunciations = [] - audio = entries["hwi"]["prs"][0]["sound"]["audio"] - if audio: - self._card.fields[settings.PRONUNCIATION_FIELD] = "https://media.merriam-webster.com/audio/prons/es/me/wav/" + audio[0] + "/" + audio + ".wav" + # assumption is that it is enough to have + # pronunciation from the first entry with pronunciation available + isFound = False + for entry in self._card.entries: + if "hwi" in entry and "prs" in entry["hwi"]: + prs = entry["hwi"]["prs"][0] + if "sound" in prs: + audio = prs["sound"]["audio"] + self._card.fields[settings.PRONUNCIATION_FIELD] = "https://media.merriam-webster.com/audio/prons/es/me/wav/" + audio[0] + "/" + audio + ".wav" + isFound = True + break + if not isFound: + self._card.fields[settings.PRONUNCIATION_FIELD] = "not available :(" diff --git a/AutoDefineAddon/libs/settings.py b/AutoDefineAddon/libs/settings.py index 4de0730..f2300e4 100644 --- a/AutoDefineAddon/libs/settings.py +++ b/AutoDefineAddon/libs/settings.py @@ -1,7 +1,6 @@ # Get your unique API key by signing up at http://www.dictionaryapi.com/ MERRIAM_WEBSTER_API_KEY = "YOUR_KEY_HERE" -MERRIAM_WEBSTER_SPANISH_API_KEY = "YOUR_KEY_HERE" # Index of field to insert definitions into (use -1 to turn off) DEFINITION_FIELD = 1 @@ -35,3 +34,6 @@ PHONETIC_TRANSCRIPTION_ONLY_SHORTCUT = "" PART_OF_SPEECH_ABBREVIATION = {"verb": "v.", "noun": "n.", "adverb": "adv.", "adjective": "adj."} + +# supposed to be set in tests only +TEST_MODE = False diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py index 530ccae..44a58a9 100644 --- a/AutoDefineAddon/tests/tests_api.py +++ b/AutoDefineAddon/tests/tests_api.py @@ -9,6 +9,15 @@ import urllib.request import settings + +settings.TEST_MODE = True +settings.DEFINITION_FIELD = 1 +settings.PRONUNCIATION_FIELD = 2 +settings.PHONETIC_TRANSCRIPTION_FIELD = 3 + +COLLEGIATE_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" +SPANISH_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" + import cardbuilder import autodefine @@ -35,12 +44,9 @@ def loadNote(self): def urlToLink(self, url): return url -settings.DEFINITION_FIELD = 1 -settings.PRONUNCIATION_FIELD = 2 -settings.PHONETIC_TRANSCRIPTION_FIELD = 3 def test_can_serialize_xml_card(): - settings.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY settings.PREFERRED_DICTIONARY = "COLLEGIATE" word = "test" cardBuilder = cardbuilder.CollegiateCardBuilder(word) @@ -58,7 +64,7 @@ def test_can_serialize_xml_card(): assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 def test_can_get_definition(): - settings.MERRIAM_WEBSTER_API_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" + settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY settings.PREFERRED_DICTIONARY = "COLLEGIATE" editor = MockEditor() word = "insert" @@ -66,7 +72,6 @@ def test_can_get_definition(): autodefine._get_definition(editor, True, True, True) print(editor.note.fields) - assert editor.note.fields[0] == word assert editor.note.fields[settings.DEFINITION_FIELD].find("to put or thrust") != -1 assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("insert") != -1 @@ -74,7 +79,7 @@ def test_can_get_definition(): assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsərt") != -1 def test_can_serialize_json_spanish_card(): - settings.MERRIAM_WEBSTER_SPANISH_API_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" + settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY settings.PREFERRED_DICTIONARY = "SPANISH" word = "entender" cardBuilder = cardbuilder.SpanishCardBuilder(word) @@ -88,6 +93,35 @@ def test_can_serialize_json_spanish_card(): assert word == editor.note.fields[0] assert editor.note.fields[settings.DEFINITION_FIELD].find("understand") != -1 + assert editor.note.fields[settings.DEFINITION_FIELD].find("verb") != -1 assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("enten") != -1 assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] == "" + +def test_can_get_pronunciation_hombre(): + settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY + settings.PREFERRED_DICTIONARY = "SPANISH" + word = "hombre" + cardBuilder = cardbuilder.SpanishCardBuilder(word) + cardBuilder.addPronunciation() + card = cardBuilder.getCard() + + editor = MockEditor() + card.serialize(editor) + + assert word == editor.note.fields[0] + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("not available :(") != -1 + +def test_can_get_pronunciation_alto(): + settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY + settings.PREFERRED_DICTIONARY = "SPANISH" + word = "alto" + cardBuilder = cardbuilder.SpanishCardBuilder(word) + cardBuilder.addPronunciation() + card = cardBuilder.getCard() + + editor = MockEditor() + card.serialize(editor) + + assert word == editor.note.fields[0] + assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("alto") != -1 From e8b34a64ad7de0b1423c7768b4a8019acb665cc7 Mon Sep 17 00:00:00 2001 From: ArtemSkrebkov Date: Thu, 10 Dec 2020 22:10:54 +0300 Subject: [PATCH 6/6] Refactor tests --- AutoDefineAddon/tests/tests_api.py | 121 +++++++++++++++-------------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/AutoDefineAddon/tests/tests_api.py b/AutoDefineAddon/tests/tests_api.py index 44a58a9..dfa59c9 100644 --- a/AutoDefineAddon/tests/tests_api.py +++ b/AutoDefineAddon/tests/tests_api.py @@ -9,14 +9,16 @@ import urllib.request import settings +import re settings.TEST_MODE = True settings.DEFINITION_FIELD = 1 settings.PRONUNCIATION_FIELD = 2 settings.PHONETIC_TRANSCRIPTION_FIELD = 3 -COLLEGIATE_KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" -SPANISH_KEY = "01c4cc4d-6f84-41e2-9ae0-10cfd5e6277e" +# put your keys to run tests +COLLEGIATE_KEY = "YOUR_KEY" +SPANISH_KEY = "YOUR_KEY" import cardbuilder import autodefine @@ -26,9 +28,8 @@ def test_empty_entries_if_url_empty(): def test_non_empty_entries_if_url_correct(): word = "word" - KEY = "47f165c1-346b-410a-b25c-a3611ac762cc" url = "http://www.dictionaryapi.com/api/v1/references/collegiate/xml/" + \ - urllib.parse.quote_plus(word) + "?key=" + KEY + urllib.parse.quote_plus(word) + "?key=" + COLLEGIATE_KEY assert cardbuilder.get_entries_from_api("word", url) != [] class MockNote: @@ -44,25 +45,6 @@ def loadNote(self): def urlToLink(self, url): return url - -def test_can_serialize_xml_card(): - settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY - settings.PREFERRED_DICTIONARY = "COLLEGIATE" - word = "test" - cardBuilder = cardbuilder.CollegiateCardBuilder(word) - cardBuilder.addDefinition() - cardBuilder.addPronunciation() - cardBuilder.addTranscription() - card = cardBuilder.getCard() - - editor = MockEditor() - card.serialize(editor) - - assert word == editor.note.fields[0] - assert editor.note.fields[settings.DEFINITION_FIELD].find("to put to test or proof") != -1 - assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("test") != -1 - def test_can_get_definition(): settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY settings.PREFERRED_DICTIONARY = "COLLEGIATE" @@ -71,57 +53,82 @@ def test_can_get_definition(): editor.note.fields[0] = word autodefine._get_definition(editor, True, True, True) - print(editor.note.fields) assert editor.note.fields[0] == word assert editor.note.fields[settings.DEFINITION_FIELD].find("to put or thrust") != -1 assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("insert") != -1 assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD].find("in-ˈsərt") != -1 -def test_can_serialize_json_spanish_card(): - settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY - settings.PREFERRED_DICTIONARY = "SPANISH" - word = "entender" - cardBuilder = cardbuilder.SpanishCardBuilder(word) - cardBuilder.addDefinition() - cardBuilder.addPronunciation() - cardBuilder.addTranscription() - card = cardBuilder.getCard() + +class MockCardBuilder(cardbuilder.CardBuilder): + def __init__(self, word): + super().__init__(word) + + def addDefinition(self): + self._card.fields[settings.DEFINITION_FIELD] = "definition" + + def addTranscription(self): + self._card.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] = "transcription" + + def addPronunciation(self): + self._card.fields[settings.PRONUNCIATION_FIELD] = "pronunciation" + +def test_can_serialize_card(): + word = "word" + card_builder = MockCardBuilder(word) + card = card_builder.getCard() + card_builder.addDefinition() + card_builder.addTranscription() + card_builder.addPronunciation() editor = MockEditor() card.serialize(editor) assert word == editor.note.fields[0] - assert editor.note.fields[settings.DEFINITION_FIELD].find("understand") != -1 - assert editor.note.fields[settings.DEFINITION_FIELD].find("verb") != -1 - assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("wav") != -1 - assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("enten") != -1 - assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] == "" - -def test_can_get_pronunciation_hombre(): - settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY - settings.PREFERRED_DICTIONARY = "SPANISH" - word = "hombre" - cardBuilder = cardbuilder.SpanishCardBuilder(word) - cardBuilder.addPronunciation() - card = cardBuilder.getCard() + assert editor.note.fields[settings.DEFINITION_FIELD] == "definition" + assert editor.note.fields[settings.PRONUNCIATION_FIELD] == "pronunciation" + assert editor.note.fields[settings.PHONETIC_TRANSCRIPTION_FIELD] == "transcription" + + +@pytest.mark.parametrize("dictionary, word, expected_output", [("SPANISH", "entender", "understand"), + ("COLLEGIATE", "insert", "to put or thrust")]) +def test_can_get_definition(dictionary, word, expected_output): + settings.PREFERRED_DICTIONARY = dictionary + card_builder = None + if dictionary == "SPANISH": + settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY + card_builder = cardbuilder.SpanishCardBuilder(word) + else: + settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY + card_builder = cardbuilder.CollegiateCardBuilder(word) + card_builder.addDefinition() + card = card_builder.getCard() editor = MockEditor() card.serialize(editor) - assert word == editor.note.fields[0] - assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("not available :(") != -1 + assert editor.note.fields[settings.DEFINITION_FIELD].find(expected_output) != -1 + -def test_can_get_pronunciation_alto(): - settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY - settings.PREFERRED_DICTIONARY = "SPANISH" - word = "alto" - cardBuilder = cardbuilder.SpanishCardBuilder(word) - cardBuilder.addPronunciation() - card = cardBuilder.getCard() +@pytest.mark.parametrize("dictionary, word, expected_output", [("SPANISH", "hombre", r"not available"), + ("SPANISH", "alto", "^http.+?alto.+?wav$"), + ("SPANISH", "entender", "^http.+?enten.+?wav$"), + ("COLLEGIATE", "insert", "^http.+?insert.+?wav$"), + ]) +def test_can_get_pronunciation(dictionary, word, expected_output): + settings.PREFERRED_DICTIONARY = dictionary + card_builder = None + if dictionary == "SPANISH": + settings.MERRIAM_WEBSTER_API_KEY = SPANISH_KEY + card_builder = cardbuilder.SpanishCardBuilder(word) + else: + settings.MERRIAM_WEBSTER_API_KEY = COLLEGIATE_KEY + card_builder = cardbuilder.CollegiateCardBuilder(word) + + card_builder.addPronunciation() + card = card_builder.getCard() editor = MockEditor() card.serialize(editor) - assert word == editor.note.fields[0] - assert editor.note.fields[settings.PRONUNCIATION_FIELD].find("alto") != -1 + assert re.match(expected_output, editor.note.fields[settings.PRONUNCIATION_FIELD])