From b94cab1f053e5706bf7e9b24cb5763acee5a4253 Mon Sep 17 00:00:00 2001 From: David Vo Date: Tue, 5 Sep 2023 20:07:43 +1000 Subject: [PATCH 1/8] csv_overrides: Add stronger type hints --- cursorless-talon/src/csv_overrides.py | 133 +++++++++++++------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 4c51b7c3a4..3e359bbc29 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -1,8 +1,9 @@ import csv -from collections.abc import Container +from collections.abc import Container, Mapping +from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Callable, Optional from talon import Context, Module, actions, app, fs @@ -33,15 +34,16 @@ def init_csv_and_watch_changes( filename: str, - default_values: dict[str, dict[str, str]], - extra_ignored_values: Optional[list[str]] = None, + default_values: Mapping[str, Mapping[str, str]], + extra_ignored_values: Container[str] = (), + *, allow_unknown_values: bool = False, default_list_name: Optional[str] = None, headers: list[str] = [SPOKEN_FORM_HEADER, CURSORLESS_IDENTIFIER_HEADER], ctx: Context = Context(), no_update_file: bool = False, - pluralize_lists: Optional[list[str]] = [], -): + pluralize_lists: Container[str] = (), +) -> Callable[[], None]: """ Initialize a cursorless settings csv, creating it if necessary, and watch for changes to the csv. Talon lists will be generated based on the keys of @@ -60,27 +62,25 @@ def init_csv_and_watch_changes( `cursorles-settings` dir default_values (dict[str, dict]): The default values for the lists to be customized in the given csv - extra_ignored_values list[str]: Don't throw an exception if any of + extra_ignored_values: Don't throw an exception if any of these appear as values; just ignore them and don't add them to any list allow_unknown_values bool: If unknown values appear, just put them in the list default_list_name Optional[str]: If unknown values are allowed, put any unknown values in this list - no_update_file Optional[bool]: Set this to `TRUE` to indicate that we should + no_update_file: Set this to `TRUE` to indicate that we should not update the csv. This is used generally in case there was an issue coming up with the default set of values so we don't want to persist those to disk pluralize_lists: Create plural version of given lists """ - if extra_ignored_values is None: - extra_ignored_values = [] - file_path = get_full_path(filename) super_default_values = get_super_values(default_values) + assert allow_unknown_values == (default_list_name is not None) file_path.parent.mkdir(parents=True, exist_ok=True) check_for_duplicates(filename, default_values) create_default_vocabulary_dicts(default_values, pluralize_lists) - def on_watch(path, flags): + def on_watch(path: str, flags) -> None: if file_path.match(path): current_values, has_errors = read_file( file_path, @@ -93,7 +93,6 @@ def on_watch(path, flags): default_values, current_values, extra_ignored_values, - allow_unknown_values, default_list_name, pluralize_lists, ctx, @@ -114,7 +113,6 @@ def on_watch(path, flags): default_values, current_values, extra_ignored_values, - allow_unknown_values, default_list_name, pluralize_lists, ctx, @@ -126,22 +124,31 @@ def on_watch(path, flags): default_values, super_default_values, extra_ignored_values, - allow_unknown_values, default_list_name, pluralize_lists, ctx, ) - def unsubscribe(): + def unsubscribe() -> None: fs.unwatch(str(file_path.parent), on_watch) return unsubscribe -def check_for_duplicates(filename, default_values): +@dataclass +class ListValue: + key: str + value: str + #: The list name. + list: str + + +def check_for_duplicates( + filename: str, default_values: Mapping[str, Mapping[str, str]] +): results_map = {} - for list_name, dict in default_values.items(): - for key, value in dict.items(): + for list_name, values in default_values.items(): + for key, value in values.items(): if value in results_map: existing_list_name = results_map[value]["list"] warning = f"WARNING ({filename}): Value `{value}` duplicated between lists '{existing_list_name}' and '{list_name}'" @@ -149,12 +156,12 @@ def check_for_duplicates(filename, default_values): app.notify(warning) -def is_removed(value: str): +def is_removed(value: str) -> bool: return value.startswith("-") def create_default_vocabulary_dicts( - default_values: dict[str, dict], pluralize_lists: list[str] + default_values: Mapping[str, Mapping[str, str]], pluralize_lists: Container[str] ): default_values_updated = {} for key, value in default_values.items(): @@ -169,41 +176,38 @@ def create_default_vocabulary_dicts( def update_dicts( - default_values: dict[str, dict], - current_values: dict, - extra_ignored_values: list[str], - allow_unknown_values: bool, + default_values: Mapping[str, Mapping[str, str]], + current_values: dict[str, str], + extra_ignored_values: Container[str], default_list_name: Optional[str], - pluralize_lists: list[str], + pluralize_lists: Container[str], ctx: Context, -): +) -> None: # Create map with all default values - results_map = {} - for list_name, dict in default_values.items(): - for key, value in dict.items(): - results_map[value] = {"key": key, "value": value, "list": list_name} + results_map: dict[str, ListValue] = {} + for list_name, values in default_values.items(): + for key, value in values.items(): + results_map[value] = ListValue(key=key, value=value, list=list_name) # Update result with current values for key, value in current_values.items(): try: - results_map[value]["key"] = key + results_map[value].key = key except KeyError: if value in extra_ignored_values: pass - elif allow_unknown_values: - results_map[value] = { - "key": key, - "value": value, - "list": default_list_name, - } + elif default_list_name is not None: + results_map[value] = ListValue( + key=key, value=value, list=default_list_name + ) else: raise # Convert result map back to result list - results = {res["list"]: {} for res in results_map.values()} + results: dict[str, dict[str, str]] = {res.list: {} for res in results_map.values()} for obj in results_map.values(): - value = obj["value"] - key = obj["key"] + value = obj.value + key = obj.key if not is_removed(key): for k in key.split("|"): if value == "pasteFromClipboard" and k.endswith(" to"): @@ -214,7 +218,7 @@ def update_dicts( # cursorless before this change would have "paste to" as # their spoken form and so would need to say "paste to to". k = k[:-3] - results[obj["list"]][k.strip()] = value + results[obj.list][k.strip()] = value # Assign result to talon context list assign_lists_to_context(ctx, results, pluralize_lists) @@ -222,25 +226,25 @@ def update_dicts( def assign_lists_to_context( ctx: Context, - results: dict, - pluralize_lists: list[str], + results: dict[str, dict[str, str]], + pluralize_lists: Container[str], ): - for list_name, dict in results.items(): + for list_name, values in results.items(): list_singular_name = get_cursorless_list_name(list_name) - ctx.lists[list_singular_name] = dict + ctx.lists[list_singular_name] = values if list_name in pluralize_lists: list_plural_name = f"{list_singular_name}_plural" - ctx.lists[list_plural_name] = {pluralize(k): v for k, v in dict.items()} + ctx.lists[list_plural_name] = {pluralize(k): v for k, v in values.items()} def update_file( path: Path, headers: list[str], default_values: dict[str, str], - extra_ignored_values: list[str], + extra_ignored_values: Container[str], allow_unknown_values: bool, no_update_file: bool, -): +) -> dict[str, str]: current_values, has_errors = read_file( path, headers, @@ -250,7 +254,7 @@ def update_file( ) current_identifiers = current_values.values() - missing = {} + missing: dict[str, str] = {} for key, value in default_values.items(): if value not in current_identifiers: missing[key] = value @@ -263,16 +267,17 @@ def update_file( ) else: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + missing_items = sorted(missing.items()) lines = [ f"# {timestamp} - New entries automatically added by cursorless", - *[create_line(key, missing[key]) for key in sorted(missing)], + *[create_line(key, value) for key, value in missing_items], ] with open(path, "a") as f: f.write("\n\n" + "\n".join(lines)) print(f"New cursorless features added to {path.name}") - for key in sorted(missing): - print(f"{key}: {missing[key]}") + for key, value in missing_items: + print(f"{key}: {value}") print( "See release notes for more info: " "https://github.com/cursorless-dev/cursorless/blob/main/CHANGELOG.md" @@ -282,18 +287,18 @@ def update_file( return current_values -def create_line(*cells: str): +def create_line(*cells: str) -> str: return ", ".join(cells) -def create_file(path: Path, headers: list[str], default_values: dict): - lines = [create_line(key, default_values[key]) for key in sorted(default_values)] +def create_file(path: Path, headers: list[str], default_values: dict[str, str]) -> None: + lines = [create_line(key, value) for key, value in sorted(default_values.items())] lines.insert(0, create_line(*headers)) lines.append("") path.write_text("\n".join(lines)) -def csv_error(path: Path, index: int, message: str, value: str): +def csv_error(path: Path, index: int, message: str, value: str) -> None: """Check that an expected condition is true Note that we try to continue reading in this case so cursorless doesn't get bricked @@ -310,16 +315,15 @@ def read_file( path: Path, headers: list[str], default_identifiers: Container[str], - extra_ignored_values: list[str], + extra_ignored_values: Container[str], allow_unknown_values: bool, -): +) -> tuple[dict[str, str], bool]: with open(path) as csv_file: # Use `skipinitialspace` to allow spaces before quote. `, "a,b"` csv_reader = csv.reader(csv_file, skipinitialspace=True) rows = list(csv_reader) - result = {} - used_identifiers = [] + result: dict[str, str] = {} has_errors = False seen_headers = False @@ -359,13 +363,12 @@ def read_file( csv_error(path, i, "Unknown identifier", value) continue - if value in used_identifiers: + if value in result: has_errors = True csv_error(path, i, "Duplicate identifier", value) continue result[key] = value - used_identifiers.append(value) if has_errors: app.notify("Cursorless settings error; see log") @@ -373,7 +376,7 @@ def read_file( return result, has_errors -def get_full_path(filename: str): +def get_full_path(filename: str) -> Path: if not filename.endswith(".csv"): filename = f"{filename}.csv" @@ -386,7 +389,7 @@ def get_full_path(filename: str): return (settings_directory / filename).resolve() -def get_super_values(values: dict[str, dict[str, str]]): +def get_super_values(values: Mapping[str, Mapping[str, str]]) -> dict[str, str]: result: dict[str, str] = {} for value_dict in values.values(): result.update(value_dict) From 7bea8286af93179d1028edea481dbe96f7ed3678 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 05:49:40 +0100 Subject: [PATCH 2/8] fix --- cursorless-talon/src/csv_overrides.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 191d6abaf9..0cd0e18620 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Callable, Iterable, Optional, TypedDict +from typing import Callable, Container, Iterable, Optional, TypedDict from talon import Context, Module, actions, app, fs, settings @@ -404,7 +404,7 @@ def csv_error(path: Path, index: int, message: str, value: str) -> None: def read_file( path: Path, headers: list[str], - default_identifiers: list[str], + default_identifiers: Container[str], extra_ignored_values: list[str], extra_allowed_values: list[str], allow_unknown_values: bool, From e7dd74285535282baeafc7b39ec228dc386fd260 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 05:55:00 +0100 Subject: [PATCH 3/8] more cleanup --- cursorless-talon/src/csv_overrides.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 0cd0e18620..7c5078b83e 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -48,6 +48,12 @@ class SpokenFormEntry: spoken_forms: list[str] +class ResultsListEntry(TypedDict): + spoken: str + id: str + list: str + + def csv_get_ctx(): return ctx @@ -196,10 +202,7 @@ def unsubscribe() -> None: return unsubscribe -def check_for_duplicates( - filename: str, - default_values: dict[str, dict[str, str]], -): +def check_for_duplicates(filename: str, default_values: ListToSpokenForms): results_map = {} for list_name, values in default_values.items(): for key, value in values.items(): @@ -217,7 +220,7 @@ def is_removed(value: str) -> bool: def create_default_vocabulary_dicts( - default_values: dict[str, dict[str, str]], + default_values: ListToSpokenForms, pluralize_lists: list[str], ): default_values_updated = {} @@ -281,12 +284,6 @@ def update_dicts( handle_new_values(spoken_form_entries) -class ResultsListEntry(TypedDict): - spoken: str - id: str - list: str - - def generate_spoken_forms(results_list: Iterable[ResultsListEntry]): for obj in results_list: id = obj["id"] From 2ff6f6b9deb4a71c1efc644cd3c89e34d3a69dee Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 06:06:59 +0100 Subject: [PATCH 4/8] Fix --- cursorless-talon/src/csv_overrides.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 7c5078b83e..978794b07e 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -132,7 +132,6 @@ def init_csv_and_watch_changes( return lambda: None super_default_values = get_super_values(default_values) - assert allow_unknown_values == (default_list_name is not None) file_path.parent.mkdir(parents=True, exist_ok=True) From cf8e77138c5ef1e3b6530b4d6e51b310f9c20b19 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 06:08:23 +0100 Subject: [PATCH 5/8] Fix --- cursorless-talon/src/csv_overrides.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 978794b07e..9a16a7a0cb 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -411,6 +411,7 @@ def read_file( rows = list(csv_reader) result: dict[str, str] = {} + used_identifiers: set[str] = set() has_errors = False seen_headers = False @@ -451,12 +452,13 @@ def read_file( csv_error(path, i, "Unknown identifier", value) continue - if value in result: + if value in used_identifiers: has_errors = True csv_error(path, i, "Duplicate identifier", value) continue result[key] = value + used_identifiers.add(value) if has_errors: app.notify("Cursorless settings error; see log") From 7d54dad7d9329e8440ac7dd40f569ee1b8109d77 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 06:13:14 +0100 Subject: [PATCH 6/8] Added more types --- cursorless-talon/src/csv_overrides.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index 9a16a7a0cb..b693fb9afa 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -138,7 +138,7 @@ def init_csv_and_watch_changes( check_for_duplicates(filename, default_values) create_default_vocabulary_dicts(default_values, pluralize_lists) - def on_watch(path: str, flags) -> None: + def on_watch(path: str, _flags) -> None: if file_path.match(path): current_values, has_errors = read_file( path=file_path, @@ -224,7 +224,7 @@ def create_default_vocabulary_dicts( ): default_values_updated = {} for key, value in default_values.items(): - updated_dict = {} + updated_dict: dict[str, str] = {} for key2, value2 in value.items(): # Enable deactivated(prefixed with a `-`) items active_key = key2[1:] if key2.startswith("-") else key2 @@ -481,7 +481,7 @@ def get_full_path(filename: str) -> Path: return (settings_directory / filename).resolve() -def get_super_values(values: ListToSpokenForms): +def get_super_values(values: ListToSpokenForms) -> dict[str, str]: result: dict[str, str] = {} for value_dict in values.values(): result.update(value_dict) From d446bd860328e72127fca9249c75c8d6f9647a30 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 06:43:41 +0100 Subject: [PATCH 7/8] More type updates --- cursorless-talon/src/csv_overrides.py | 74 ++++++++++++++++----------- cursorless-talon/src/spoken_forms.py | 19 ++++--- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/cursorless-talon/src/csv_overrides.py b/cursorless-talon/src/csv_overrides.py index b693fb9afa..57d78448d5 100644 --- a/cursorless-talon/src/csv_overrides.py +++ b/cursorless-talon/src/csv_overrides.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Callable, Container, Iterable, Optional, TypedDict +from typing import Callable, Container, Iterable, Optional, Sequence, TypedDict from talon import Context, Module, actions, app, fs, settings @@ -65,16 +65,16 @@ def csv_get_normalized_ctx(): def init_csv_and_watch_changes( filename: str, default_values: ListToSpokenForms, - handle_new_values: Optional[Callable[[list[SpokenFormEntry]], None]] = None, + handle_new_values: Optional[Callable[[Sequence[SpokenFormEntry]], None]] = None, *, - extra_ignored_values: list[str] = [], - extra_allowed_values: Optional[list[str]] = None, + extra_ignored_values: Optional[Sequence[str]] = None, + extra_allowed_values: Optional[Sequence[str]] = None, allow_unknown_values: bool = False, deprecated: bool = False, default_list_name: Optional[str] = None, - headers: list[str] = [SPOKEN_FORM_HEADER, CURSORLESS_IDENTIFIER_HEADER], + headers: Optional[Sequence[str]] = None, no_update_file: bool = False, - pluralize_lists: list[str] = [], + pluralize_lists: Optional[Sequence[str]] = None, ) -> Callable[[], None]: """ Initialize a cursorless settings csv, creating it if necessary, and watch @@ -96,21 +96,21 @@ def init_csv_and_watch_changes( `cursorles-settings` dir default_values (ListToSpokenForms): The default values for the lists to be customized in the given csv - handle_new_values (Optional[Callable[[list[SpokenFormEntry]], None]]): A + handle_new_values (Optional[Callable[[Sequence[SpokenFormEntry]], None]]): A callback to be called when the lists are updated - extra_ignored_values (list[str]): Don't throw an exception if + extra_ignored_values (Optional[Sequence[str]]): Don't throw an exception if any of these appear as values; just ignore them and don't add them to any list allow_unknown_values (bool): If unknown values appear, just put them in the list default_list_name (Optional[str]): If unknown values are allowed, put any unknown values in this list - headers (list[str]): The headers to use for the csv + headers (Optional[Sequence[str]]): The headers to use for the csv no_update_file (bool): Set this to `True` to indicate that we should not update the csv. This is used generally in case there was an issue coming up with the default set of values so we don't want to persist those to disk - pluralize_lists (list[str]): Create plural version of given lists + pluralize_lists (Optional[Sequence[str]]): Create plural version of given lists """ # Don't allow both `extra_allowed_values` and `allow_unknown_values` assert not (extra_allowed_values and allow_unknown_values) @@ -121,8 +121,14 @@ def init_csv_and_watch_changes( (extra_allowed_values or allow_unknown_values) and not default_list_name ) + if headers is None: + headers = (SPOKEN_FORM_HEADER, CURSORLESS_IDENTIFIER_HEADER) + if extra_ignored_values is None: + extra_ignored_values = [] if extra_allowed_values is None: extra_allowed_values = [] + if pluralize_lists is None: + pluralize_lists = [] file_path = get_full_path(filename) is_file = file_path.is_file() @@ -220,7 +226,7 @@ def is_removed(value: str) -> bool: def create_default_vocabulary_dicts( default_values: ListToSpokenForms, - pluralize_lists: list[str], + pluralize_lists: Sequence[str], ): default_values_updated = {} for key, value in default_values.items(): @@ -237,13 +243,13 @@ def create_default_vocabulary_dicts( def update_dicts( default_values: ListToSpokenForms, current_values: dict[str, str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], + extra_ignored_values: Sequence[str], + extra_allowed_values: Sequence[str], allow_unknown_values: bool, default_list_name: str | None, - pluralize_lists: list[str], - handle_new_values: Callable[[list[SpokenFormEntry]], None] | None, -): + pluralize_lists: Sequence[str], + handle_new_values: Callable[[Sequence[SpokenFormEntry]], None] | None, +) -> None: # Create map with all default values results_map: dict[str, ResultsListEntry] = {} for list_name, values in default_values.items(): @@ -283,7 +289,9 @@ def update_dicts( handle_new_values(spoken_form_entries) -def generate_spoken_forms(results_list: Iterable[ResultsListEntry]): +def generate_spoken_forms( + results_list: Iterable[ResultsListEntry], +) -> Iterable[SpokenFormEntry]: for obj in results_list: id = obj["id"] spoken = obj["spoken"] @@ -311,8 +319,8 @@ def generate_spoken_forms(results_list: Iterable[ResultsListEntry]): def assign_lists_to_context( ctx: Context, lists: ListToSpokenForms, - pluralize_lists: list[str], -): + pluralize_lists: Sequence[str], +) -> None: for list_name, values in lists.items(): list_singular_name = get_cursorless_list_name(list_name) ctx.lists[list_singular_name] = values @@ -323,10 +331,10 @@ def assign_lists_to_context( def update_file( path: Path, - headers: list[str], + headers: Sequence[str], default_values: dict[str, str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], + extra_ignored_values: Sequence[str], + extra_allowed_values: Sequence[str], allow_unknown_values: bool, no_update_file: bool, ) -> dict[str, str]: @@ -373,17 +381,21 @@ def update_file( return current_values -def create_line(*cells: str) -> str: - return ", ".join(cells) - - -def create_file(path: Path, headers: list[str], default_values: dict[str, str]) -> None: +def create_file( + path: Path, + headers: Sequence[str], + default_values: dict[str, str], +) -> None: lines = [create_line(key, value) for key, value in sorted(default_values.items())] lines.insert(0, create_line(*headers)) lines.append("") path.write_text("\n".join(lines)) +def create_line(*cells: str) -> str: + return ", ".join(cells) + + def csv_error(path: Path, index: int, message: str, value: str) -> None: """Check that an expected condition is true @@ -399,10 +411,10 @@ def csv_error(path: Path, index: int, message: str, value: str) -> None: def read_file( path: Path, - headers: list[str], + headers: Sequence[str], default_identifiers: Container[str], - extra_ignored_values: list[str], - extra_allowed_values: list[str], + extra_ignored_values: Sequence[str], + extra_allowed_values: Sequence[str], allow_unknown_values: bool, ) -> tuple[dict[str, str], bool]: with open(path) as csv_file: @@ -424,7 +436,7 @@ def read_file( if not seen_headers: seen_headers = True - if row != headers: + if row != list(headers): has_errors = True csv_error(path, i, "Malformed header", create_line(*row)) print(f"Expected '{create_line(*headers)}'") diff --git a/cursorless-talon/src/spoken_forms.py b/cursorless-talon/src/spoken_forms.py index 92dc5a8bc2..cbb2a2a0ba 100644 --- a/cursorless-talon/src/spoken_forms.py +++ b/cursorless-talon/src/spoken_forms.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from typing import Callable, Concatenate +from typing import Callable, Concatenate, Sequence from talon import app, cron, fs, registry @@ -26,9 +26,14 @@ def auto_construct_defaults[**P, R]( spoken_forms: dict[str, ListToSpokenForms], - handle_new_values: Callable[[str, list[SpokenFormEntry]], None], + handle_new_values: Callable[[str, Sequence[SpokenFormEntry]], None], f: Callable[ - Concatenate[str, ListToSpokenForms, Callable[[list[SpokenFormEntry]], None], P], + Concatenate[ + str, + ListToSpokenForms, + Callable[[Sequence[SpokenFormEntry]], None], + P, + ], R, ], ): @@ -94,7 +99,7 @@ def update(): initialized = False # Maps from csv name to list of SpokenFormEntry - custom_spoken_forms: dict[str, list[SpokenFormEntry]] = {} + custom_spoken_forms: dict[str, Sequence[SpokenFormEntry]] = {} spoken_forms_output = SpokenFormsOutput() spoken_forms_output.init() graphemes_talon_list = get_graphemes_talon_list() @@ -116,7 +121,7 @@ def update_spoken_forms_output(): ] ) - def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): + def handle_new_values(csv_name: str, values: Sequence[SpokenFormEntry]): custom_spoken_forms[csv_name] = values if initialized: # On first run, we just do one update at the end, so we suppress @@ -163,13 +168,13 @@ def handle_new_values(csv_name: str, values: list[SpokenFormEntry]): ), handle_csv( "experimental/actions_custom.csv", - headers=[SPOKEN_FORM_HEADER, "VSCode command"], + headers=(SPOKEN_FORM_HEADER, "VSCode command"), allow_unknown_values=True, default_list_name="custom_action", ), handle_csv( "experimental/regex_scope_types.csv", - headers=[SPOKEN_FORM_HEADER, "Regex"], + headers=(SPOKEN_FORM_HEADER, "Regex"), allow_unknown_values=True, default_list_name="custom_regex_scope_type", pluralize_lists=["custom_regex_scope_type"], From d9265ce6ba46230e29b293a248fecff915ea4760 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 8 Mar 2026 06:52:34 +0100 Subject: [PATCH 8/8] Update snip test --- .../src/test/fixtures/communitySnippets.fixture.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts index bd3f585bb3..e1995c1106 100644 --- a/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts +++ b/packages/cursorless-engine/src/test/fixtures/communitySnippets.fixture.ts @@ -22,7 +22,13 @@ const snippetAfterAction: ActionDescriptor = { snippets: [ { type: "custom", - body: "```\n$0\n```", + languages: [ + "javascript", + "typescript", + "javascriptreact", + "typescriptreact", + ], + body: 'import * as $0 from "$0";', }, ], }, @@ -34,5 +40,5 @@ const snippetAfterAction: ActionDescriptor = { * Talon tests by relying on our recorded test fixtures alone. */ export const communitySnippetsSpokenFormsFixture = [ - spokenFormTest("snip code after air", snippetAfterAction, undefined), + spokenFormTest("snip import star after air", snippetAfterAction, undefined), ];