diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index f8ac52933c..ba6219fd1e 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -7,6 +7,9 @@ and when the change was applied given the delay between changes being submitted and the time they were reviewed and merged. --- +* 2024-09-07 Removed `get_list_from_csv` from `user_settings.py`. Please + use the new `track_csv_list` decorator, which leverages Talon's + `talon.watch` API for robustness on Talon launch. * 2024-09-07 If you've updated `community` since 2024-08-31, you may need to replace `host:` with `hostname:` in the header of `core/system_paths-.talon-list` due to an issue with diff --git a/apps/emacs/emacs_commands.py b/apps/emacs/emacs_commands.py index 9618b7c388..12fd817285 100644 --- a/apps/emacs/emacs_commands.py +++ b/apps/emacs/emacs_commands.py @@ -33,10 +33,9 @@ def emacs_command_short_form(command_name: str) -> Optional[str]: return emacs_commands.get(command_name, Command(command_name)).short -def load_csv(): - filepath = Path(__file__).parents[0] / "emacs_commands.csv" - with resource.open(filepath) as f: - rows = list(csv.reader(f)) +@resource.watch("emacs_commands.csv") +def load_commands(f): + rows = list(csv.reader(f)) # Check headers assert rows[0] == ["Command", " Key binding", " Short form", " Spoken form"] @@ -46,7 +45,7 @@ def load_csv(): continue if len(row) > 4: print( - f'"{filepath}": More than four values in row: {row}. ' + f"emacs_commands.csv: More than four values in row: {row}. " + " Ignoring the extras" ) name, keys, short, spoken = ( @@ -70,7 +69,3 @@ def load_csv(): if c.spoken: command_list[c.spoken] = c.name ctx.lists["self.emacs_command"] = command_list - - -# TODO: register on change to file! -app.register("ready", load_csv) diff --git a/core/abbreviate/abbreviate.py b/core/abbreviate/abbreviate.py index 2544dbf93c..cb43d5c192 100644 --- a/core/abbreviate/abbreviate.py +++ b/core/abbreviate/abbreviate.py @@ -2,12 +2,13 @@ from talon import Context, Module -from ..user_settings import get_list_from_csv +from ..user_settings import track_csv_list mod = Module() +ctx = Context() mod.list("abbreviation", desc="Common abbreviation") - +abbreviations_list = {} abbreviations = { "J peg": "jpg", "abbreviate": "abbr", @@ -447,24 +448,26 @@ "work in progress": "wip", } -# This variable is also considered exported for the create_spoken_forms module -abbreviations_list = get_list_from_csv( - "abbreviations.csv", - headers=("Abbreviation", "Spoken Form"), - default=abbreviations, + +@track_csv_list( + "abbreviations.csv", headers=("Abbreviation", "Spoken Form"), default=abbreviations ) +def on_abbreviations(values): + global abbreviations_list -# Matches letters and spaces, as currently, Talon doesn't accept other characters in spoken forms. -PATTERN = re.compile(r"^[a-zA-Z ]+$") -abbreviation_values = { - v: v for v in abbreviations_list.values() if PATTERN.match(v) is not None -} + # note: abbreviations_list is imported by the create_spoken_forms module + abbreviations_list = values -# Allows the abbreviated/short form to be used as spoken phrase. eg "brief app" -> app -abbreviations_list_with_values = { - **abbreviation_values, - **abbreviations_list, -} + # Matches letters and spaces, as currently, Talon doesn't accept other characters in spoken forms. + PATTERN = re.compile(r"^[a-zA-Z ]+$") + abbreviation_values = { + v: v for v in abbreviations_list.values() if PATTERN.match(v) is not None + } -ctx = Context() -ctx.lists["user.abbreviation"] = abbreviations_list_with_values + # Allows the abbreviated/short form to be used as spoken phrase. eg "brief app" -> app + abbreviations_list_with_values = { + **{v: v for v in abbreviation_values.values()}, + **abbreviations_list, + } + + ctx.lists["user.abbreviation"] = abbreviations_list_with_values diff --git a/core/create_spoken_forms.py b/core/create_spoken_forms.py index 02fe77b098..508e8a93ff 100644 --- a/core/create_spoken_forms.py +++ b/core/create_spoken_forms.py @@ -6,34 +6,58 @@ from talon import Module, actions -from .abbreviate.abbreviate import abbreviations_list -from .file_extension.file_extension import file_extensions from .keys.keys import symbol_key_words from .numbers.numbers import digits_map, scales, teens, tens +from .user_settings import track_csv_list mod = Module() - DEFAULT_MINIMUM_TERM_LENGTH = 2 EXPLODE_MAX_LEN = 3 FANCY_REGULAR_EXPRESSION = r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+" -FILE_EXTENSIONS_REGEX = "|".join( - re.escape(file_extension.strip()) + "$" - for file_extension in file_extensions.values() -) SYMBOLS_REGEX = "|".join(re.escape(symbol) for symbol in set(symbol_key_words.values())) -REGEX_NO_SYMBOLS = re.compile( - "|".join( - [ - FANCY_REGULAR_EXPRESSION, - FILE_EXTENSIONS_REGEX, - ] +FILE_EXTENSIONS_REGEX = r"^\b$" +file_extensions = {} + + +def update_regex(): + global REGEX_NO_SYMBOLS + global REGEX_WITH_SYMBOLS + REGEX_NO_SYMBOLS = re.compile( + "|".join( + [ + FANCY_REGULAR_EXPRESSION, + FILE_EXTENSIONS_REGEX, + ] + ) + ) + REGEX_WITH_SYMBOLS = re.compile( + "|".join([FANCY_REGULAR_EXPRESSION, FILE_EXTENSIONS_REGEX, SYMBOLS_REGEX]) + ) + + +update_regex() + + +@track_csv_list("file_extensions.csv", headers=("File extension", "Name")) +def on_extensions(values): + global FILE_EXTENSIONS_REGEX + global file_extensions + file_extensions = values + FILE_EXTENSIONS_REGEX = "|".join( + re.escape(file_extension.strip()) + "$" for file_extension in values.values() ) -) + update_regex() + + +abbreviations_list = {} + + +@track_csv_list("abbreviations.csv", headers=("Abbreviation", "Spoken Form")) +def on_abbreviations(values): + global abbreviations_list + abbreviations_list = values -REGEX_WITH_SYMBOLS = re.compile( - "|".join([FANCY_REGULAR_EXPRESSION, FILE_EXTENSIONS_REGEX, SYMBOLS_REGEX]) -) REVERSE_PRONUNCIATION_MAP = { **{str(value): key for key, value in digits_map.items()}, diff --git a/core/file_extension/file_extension.py b/core/file_extension/file_extension.py index 04a1c23ca2..1a75e76c42 100644 --- a/core/file_extension/file_extension.py +++ b/core/file_extension/file_extension.py @@ -1,6 +1,6 @@ from talon import Context, Module -from ..user_settings import get_list_from_csv +from ..user_settings import track_csv_list mod = Module() mod.list("file_extension", desc="A file extension, such as .py") @@ -55,11 +55,13 @@ "dot log": ".log", } -file_extensions = get_list_from_csv( +ctx = Context() + + +@track_csv_list( "file_extensions.csv", headers=("File extension", "Name"), default=_file_extensions_defaults, ) - -ctx = Context() -ctx.lists["self.file_extension"] = file_extensions +def on_update(values): + ctx.lists["self.file_extension"] = values diff --git a/core/keys/arrow_key.talon-list b/core/keys/arrow_key.talon-list new file mode 100644 index 0000000000..42bc4779b8 --- /dev/null +++ b/core/keys/arrow_key.talon-list @@ -0,0 +1,6 @@ +list: user.arrow_key +- +down: down +left: left +right: right +up: up diff --git a/core/keys/function_key.talon-list b/core/keys/function_key.talon-list new file mode 100644 index 0000000000..936eec4729 --- /dev/null +++ b/core/keys/function_key.talon-list @@ -0,0 +1,27 @@ +list: user.function_key +- +f one: f1 +f two: f2 +f three: f3 +f four: f4 +f five: f5 +f six: f6 +f seven: f7 +f eight: f8 +f nine: f9 +f ten: f10 +f eleven: f11 +f twelve: f12 +f thirteen: f13 +f fourteen: f14 +f fifteen: f15 +f sixteen: f16 +f seventeen: f17 +f eighteen: f18 +f nineteen: f19 +f twenty: f20 +# these f keys are not supported by all platforms (eg Mac) and are disabled by default +#f twenty one: f21 +#f twenty two: f22 +#f twenty three: f23 +#f twenty four: f24 diff --git a/core/keys/keypad_key.talon-list b/core/keys/keypad_key.talon-list new file mode 100644 index 0000000000..cf4e251222 --- /dev/null +++ b/core/keys/keypad_key.talon-list @@ -0,0 +1,19 @@ +list: user.keypad_key +- +key pad zero: keypad_0 +key pad one: keypad_1 +key pad two: keypad_2 +key pad three: keypad_3 +key pad four: keypad_4 +key pad five: keypad_5 +key pad six: keypad_6 +key pad seven: keypad_7 +key pad eight: keypad_8 +key pad nine: keypad_9 +key pad point: keypad_decimal +key pad plus: keypad_plus +key pad minus: keypad_minus +key pad star: keypad_multiply +key pad slash: keypad_divide +key pad equals: keypad_equals +key pad clear: keypad_clear diff --git a/core/keys/keys.py b/core/keys/keys.py index 831a9f68c9..59855b4e97 100644 --- a/core/keys/keys.py +++ b/core/keys/keys.py @@ -1,11 +1,4 @@ -from talon import Context, Module, app - -from ..user_settings import get_list_from_csv - -# used for number keys & function keys respectively -digits = "zero one two three four five six seven eight nine".split() -f_digits = "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty".split() - +from talon import Context, Module, actions, app mod = Module() mod.list("letter", desc="The spoken phonetic alphabet") @@ -15,6 +8,7 @@ mod.list("modifier_key", desc="All modifier keys") mod.list("function_key", desc="All function keys") mod.list("special_key", desc="All special keys") +mod.list("keypad_key", desc="All keypad keys") mod.list("punctuation", desc="words for inserting punctuation into text") @@ -42,6 +36,12 @@ def number_key(m) -> str: return m.number_key +@mod.capture(rule="{self.keypad_key}") +def keypad_key(m) -> str: + "One keypad key" + return m.keypad_key + + @mod.capture(rule="{self.letter}") def letter(m) -> str: "One letter key" @@ -74,7 +74,7 @@ def any_alphanumeric_key(m) -> str: @mod.capture( rule="( | | " - "| | | )" + "| | | | )" ) def unmodified_key(m) -> str: "A single key with no modifiers" @@ -104,17 +104,6 @@ def letters(m) -> str: ctx = Context() -modifier_keys = { - # If you find 'alt' is often misrecognized, try using 'alter'. - "alt": "alt", #'alter': 'alt', - "control": "ctrl", #'troll': 'ctrl', - "shift": "shift", #'sky': 'shift', - "super": "super", -} -if app.platform == "mac": - modifier_keys["command"] = "cmd" - modifier_keys["option"] = "alt" -ctx.lists["self.modifier_key"] = modifier_keys # `punctuation_words` is for words you want available BOTH in dictation and as key names in command mode. # `symbol_key_words` is for key names that should be available in command mode, but NOT during dictation. @@ -212,42 +201,3 @@ def letters(m) -> str: symbol_key_words.update(punctuation_words) ctx.lists["self.punctuation"] = punctuation_words ctx.lists["self.symbol_key"] = symbol_key_words -ctx.lists["self.number_key"] = {name: str(i) for i, name in enumerate(digits)} -ctx.lists["self.arrow_key"] = { - "down": "down", - "left": "left", - "right": "right", - "up": "up", -} - -simple_keys = [ - "end", - "enter", - "escape", - "home", - "insert", - "pagedown", - "pageup", - "space", - "tab", -] - -alternate_keys = { - "wipe": "backspace", - "delete": "backspace", - #'junk': 'backspace', - "forward delete": "delete", - "page up": "pageup", - "page down": "pagedown", -} -# mac apparently doesn't have the menu key. -if app.platform in ("windows", "linux"): - alternate_keys["menu key"] = "menu" - alternate_keys["print screen"] = "printscr" - -special_keys = {k: k for k in simple_keys} -special_keys.update(alternate_keys) -ctx.lists["self.special_key"] = special_keys -ctx.lists["self.function_key"] = { - f"F {name}": f"f{i}" for i, name in enumerate(f_digits, start=1) -} diff --git a/core/keys/letter.talon-list b/core/keys/letter.talon-list index 3dc3418e33..824a2939fc 100644 --- a/core/keys/letter.talon-list +++ b/core/keys/letter.talon-list @@ -1,5 +1,7 @@ list: user.letter - +# for common alternative spoken forms for letters, visit +# https://talon.wiki/quickstart/improving_recognition_accuracy/#collected-alternatives-to-the-default-alphabet air: a bat: b cap: c diff --git a/core/keys/mac/modifier_key.talon-list b/core/keys/mac/modifier_key.talon-list new file mode 100644 index 0000000000..589235d5b9 --- /dev/null +++ b/core/keys/mac/modifier_key.talon-list @@ -0,0 +1,9 @@ +list: user.modifier_key +os: mac +- +alt: alt +control: ctrl +shift: shift +super: cmd +command: cmd +option: alt diff --git a/core/keys/mac/special_key.talon-list b/core/keys/mac/special_key.talon-list new file mode 100644 index 0000000000..b2626176f3 --- /dev/null +++ b/core/keys/mac/special_key.talon-list @@ -0,0 +1,14 @@ +list: user.special_key +os: mac +- +end: end +home: home +minus: minus +enter: enter +page down: pagedown +page up: pageup +escape: escape +tab: tab +wipe: backspace +delete: backspace +forward delete: delete diff --git a/core/keys/number_key.talon-list b/core/keys/number_key.talon-list new file mode 100644 index 0000000000..b83e24b2a8 --- /dev/null +++ b/core/keys/number_key.talon-list @@ -0,0 +1,12 @@ +list: user.number_key +- +zero: 0 +one: 1 +two: 2 +three: 3 +four: 4 +five: 5 +six: 6 +seven: 7 +eight: 8 +nine: 9 diff --git a/core/keys/win/modifier_key.talon-list b/core/keys/win/modifier_key.talon-list new file mode 100644 index 0000000000..4ab343a343 --- /dev/null +++ b/core/keys/win/modifier_key.talon-list @@ -0,0 +1,11 @@ +list: user.modifier_key +os: windows +os: linux +- +alt: alt +control: ctrl +shift: shift +# super is the windows key +super: super +command: ctrl +option: alt diff --git a/core/keys/win/special_key.talon-list b/core/keys/win/special_key.talon-list new file mode 100644 index 0000000000..88b0841254 --- /dev/null +++ b/core/keys/win/special_key.talon-list @@ -0,0 +1,17 @@ +list: user.special_key +os: windows +os: linux +- +end: end +home: home +minus: minus +enter: enter +page down: pagedown +page up: pageup +escape: escape +tab: tab +wipe: backspace +delete: backspace +forward delete: delete +menu key: menu +print screen: printscr diff --git a/core/snippets/snippets/elseIfStatement.snippet b/core/snippets/snippets/elseIfStatement.snippet index f9405931ad..6aec0a370d 100644 --- a/core/snippets/snippets/elseIfStatement.snippet +++ b/core/snippets/snippets/elseIfStatement.snippet @@ -20,3 +20,9 @@ language: python elif $1: $0 --- + +language: lua +- +elseif $1 then + $0 +--- diff --git a/core/snippets/snippets/elseStatement.snippet b/core/snippets/snippets/elseStatement.snippet index 6c976bdd25..50d00379bb 100644 --- a/core/snippets/snippets/elseStatement.snippet +++ b/core/snippets/snippets/elseStatement.snippet @@ -18,3 +18,9 @@ language: python else: $0 --- + +language: lua +- +else + $0 +--- diff --git a/core/snippets/snippets/ifStatement.snippet b/core/snippets/snippets/ifStatement.snippet index d41a6ba3a2..9d2cdf0fe6 100644 --- a/core/snippets/snippets/ifStatement.snippet +++ b/core/snippets/snippets/ifStatement.snippet @@ -20,3 +20,10 @@ language: python if $1: $0 --- + +language: lua +- +if $1 then + $0 +end +--- diff --git a/core/snippets/snippets/lua.snippet b/core/snippets/snippets/lua.snippet new file mode 100644 index 0000000000..b4be8de438 --- /dev/null +++ b/core/snippets/snippets/lua.snippet @@ -0,0 +1,21 @@ +language: lua +--- + +name: forInIPairs +phrase: for eye pairs +insertionScope: statement +$1.insertionFormatter: SNAKE_CASE +- +for _, $1 in ipairs($2) do + $0 +end +--- + +name: forInPairs +phrase: for pairs +insertionScope: statement +- +for ${1:k}, ${2:v} in pairs($3) do + $0 +end +--- diff --git a/core/snippets/snippets/ternary.snippet b/core/snippets/snippets/ternary.snippet index c7639a8665..20641f9ca2 100644 --- a/core/snippets/snippets/ternary.snippet +++ b/core/snippets/snippets/ternary.snippet @@ -11,3 +11,8 @@ language: python - $1 if $2 else $0 --- + +language: lua +- +$1 and $2 or $0 +--- diff --git a/core/user_settings.py b/core/user_settings.py index 2630f2c518..6e08cfe035 100644 --- a/core/user_settings.py +++ b/core/user_settings.py @@ -1,35 +1,23 @@ import csv import os from pathlib import Path +from typing import IO, Callable from talon import resource # NOTE: This method requires this module to be one folder below the top-level # community/knausj folder. SETTINGS_DIR = Path(__file__).parents[1] / "settings" +SETTINGS_DIR.mkdir(exist_ok=True) -if not SETTINGS_DIR.is_dir(): - os.mkdir(SETTINGS_DIR) +CallbackT = Callable[[dict[str, str]], None] +DecoratorT = Callable[[CallbackT], CallbackT] -def get_list_from_csv( - filename: str, headers: tuple[str, str], default: dict[str, str] = {} -): - """Retrieves list from CSV""" - path = SETTINGS_DIR / filename - assert filename.endswith(".csv") - - if not path.is_file(): - with open(path, "w", encoding="utf-8", newline="") as file: - writer = csv.writer(file) - writer.writerow(headers) - for key, value in default.items(): - writer.writerow([key] if key == value else [value, key]) - - # Now read via resource to take advantage of talon's - # ability to reload this script for us when the resource changes - with resource.open(str(path), "r") as f: - rows = list(csv.reader(f)) +def read_csv_list( + f: IO, headers: tuple[str, str], is_spoken_form_first: bool = False +) -> dict[str, str]: + rows = list(csv.reader(f)) # print(str(rows)) mapping = {} @@ -37,7 +25,7 @@ def get_list_from_csv( actual_headers = rows[0] if not actual_headers == list(headers): print( - f'"{filename}": Malformed headers - {actual_headers}.' + f'"{f.name}": Malformed headers - {actual_headers}.' + f" Should be {list(headers)}. Ignoring row." ) for row in rows[1:]: @@ -47,10 +35,14 @@ def get_list_from_csv( if len(row) == 1: output = spoken_form = row[0] else: - output, spoken_form = row[:2] + if is_spoken_form_first: + spoken_form, output = row[:2] + else: + output, spoken_form = row[:2] + if len(row) > 2: print( - f'"{filename}": More than two values in row: {row}.' + f'"{f.name}": More than two values in row: {row}.' + " Ignoring the extras." ) # Leading/trailing whitespace in spoken form can prevent recognition. @@ -60,6 +52,44 @@ def get_list_from_csv( return mapping +def write_csv_defaults( + path: Path, + headers: tuple[str, str], + default: dict[str, str] = None, + is_spoken_form_first: bool = False, +) -> None: + if not path.is_file() and default is not None: + with open(path, "w", encoding="utf-8") as file: + writer = csv.writer(file) + writer.writerow(headers) + for key, value in default.items(): + if key == value: + writer.writerow([key]) + elif is_spoken_form_first: + writer.writerow([key, value]) + else: + writer.writerow([value, key]) + + +def track_csv_list( + filename: str, + headers: tuple[str, str], + default: dict[str, str] = None, + is_spoken_form_first: bool = False, +) -> DecoratorT: + assert filename.endswith(".csv") + path = SETTINGS_DIR / filename + write_csv_defaults(path, headers, default, is_spoken_form_first) + + def decorator(fn: CallbackT) -> CallbackT: + @resource.watch(str(path)) + def on_update(f): + data = read_csv_list(f, headers, is_spoken_form_first) + fn(data) + + return decorator + + def append_to_csv(filename: str, rows: dict[str, str]): path = SETTINGS_DIR / filename assert filename.endswith(".csv") diff --git a/core/vocabulary/vocabulary.py b/core/vocabulary/vocabulary.py index 2b49703ce2..3792138ed1 100644 --- a/core/vocabulary/vocabulary.py +++ b/core/vocabulary/vocabulary.py @@ -6,7 +6,7 @@ from talon import Context, Module, actions from talon.grammar import Phrase -from ..user_settings import append_to_csv, get_list_from_csv +from ..user_settings import append_to_csv, track_csv_list mod = Module() ctx = Context() @@ -43,22 +43,7 @@ # This is the opposite ordering to words_to_replace.csv (the latter has the target word first) } _word_map_defaults.update({word.lower(): word for word in _capitalize_defaults}) - - -# phrases_to_replace is a spoken form -> written form map, used by our -# implementation of `dictate.replace_words` (at bottom of file) to rewrite words -# and phrases Talon recognized. This does not change the priority with which -# Talon recognizes particular phrases over others. -phrases_to_replace = get_list_from_csv( - "words_to_replace.csv", - headers=("Replacement", "Original"), - default=_word_map_defaults, -) - -# "dictate.word_map" is used by Talon's built-in default implementation of -# `dictate.replace_words`, but supports only single-word replacements. -# Multi-word phrases are ignored. -ctx.settings["dictate.word_map"] = phrases_to_replace +phrases_to_replace = {} class PhraseReplacer: @@ -70,7 +55,10 @@ class PhraseReplacer: - phrase_dict: dictionary mapping recognized/spoken forms to written forms """ - def __init__(self, phrase_dict: dict[str, str]): + def __init__(self): + self.phrase_index = {} + + def update(self, phrase_dict: dict[str, str]): # Index phrases by first word, then number of subsequent words n_next phrase_index = dict() for spoken_form, written_form in phrase_dict.items(): @@ -120,7 +108,8 @@ def replace_string(self, text: str) -> str: # Unit tests for PhraseReplacer -rep = PhraseReplacer( +rep = PhraseReplacer() +rep.update( { "this": "foo", "that": "bar", @@ -136,7 +125,27 @@ def replace_string(self, text: str) -> str: assert rep.replace_string("try this is too") == "try stopping early too" assert rep.replace_string("this is a tricky one") == "stopping early a tricky one" -phrase_replacer = PhraseReplacer(phrases_to_replace) +phrase_replacer = PhraseReplacer() + + +# phrases_to_replace is a spoken form -> written form map, used by our +# implementation of `dictate.replace_words` (at bottom of file) to rewrite words +# and phrases Talon recognized. This does not change the priority with which +# Talon recognizes particular phrases over others. +@track_csv_list( + "words_to_replace.csv", + headers=("Replacement", "Original"), + default=_word_map_defaults, +) +def on_word_map(values): + global phrases_to_replace + phrases_to_replace = values + phrase_replacer.update(values) + + # "dictate.word_map" is used by Talon's built-in default implementation of + # `dictate.replace_words`, but supports only single-word replacements. + # Multi-word phrases are ignored. + ctx.settings["dictate.word_map"] = values @ctx.action_class("dictate") diff --git a/test/stubs/talon/__init__.py b/test/stubs/talon/__init__.py index 35501260c4..f7001e4910 100644 --- a/test/stubs/talon/__init__.py +++ b/test/stubs/talon/__init__.py @@ -184,6 +184,9 @@ class Resource: def open(self, path: str, mode: str = "r"): return open(path, mode, encoding="utf-8") + def watch(self, path: str): + return lambda f: f + class App: """ diff --git a/test/test_create_spoken_forms.py b/test/test_create_spoken_forms.py index b921be0cc2..609c01b17d 100644 --- a/test/test_create_spoken_forms.py +++ b/test/test_create_spoken_forms.py @@ -4,9 +4,38 @@ # Only include this when we're running tests import itertools + from typing import IO, Callable from talon import actions + import core.abbreviate + import core.user_settings + + # we need to replace the track_csv_list decorator for unit tests. + CallbackT = Callable[[dict[str, str]], None] + DecoratorT = Callable[[CallbackT], CallbackT] + + def track_csv_list_test( + filename: str, + headers: tuple[str, str], + default: dict[str, str] = None, + is_spoken_form_first: bool = False, + ) -> DecoratorT: + def decorator(fn: CallbackT) -> CallbackT: + extensions = { + "dot see sharp": ".cs", + } + abbreviations = {"source": "src", "whats app": "WhatsApp"} + if filename == "abbreviations.csv": + fn(abbreviations) + elif filename == "file_extensions.csv": + fn(extensions) + + return decorator + + # replace track_csv_list before importing create_spoken_forms + core.user_settings.track_csv_list = track_csv_list_test + import core.create_spoken_forms def test_excludes_words(): @@ -43,6 +72,7 @@ def test_expands_file_extensions(): assert "hi dot see sharp" in result def test_expands_abbreviations(): + result = actions.user.create_spoken_forms("src", None, 0, True) assert "source" in result