prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Vocabulary functions do not strip whitespace from CSV
  I had the misfortune the last few days to believe that the vocabulary csv files were not operating correctly.
  
  Apparently whitespace is not stripped around fields. So "test, test2" leads a row which is parsed, but not turned into a functioning command.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `code/user_settings.py`
  Content:
  ```
  1 from talon import Module, fs, Context
  2 import os
  3 import csv
  4 from pathlib import Path
  5 from typing import Dict, List, Tuple
  6 import threading
  7 
  8 
  9 # NOTE: This method requires this module to be one folder below the top-level
  10 #   knausj folder.
  11 SETTINGS_DIR = Path(__file__).parents[1] / "settings"
  12 
  13 if not SETTINGS_DIR.is_dir():
  14     os.mkdir(SETTINGS_DIR)
  15 
  16 mod = Module()
  17 ctx = Context()
  18 
  19 
  20 def _load_csv_dict(
  21     file_name: str, headers=Tuple[str, str], default: Dict[str, str] = {}
  22 ) -> Dict[str, str]:
  23     """Load a word mapping from a CSV file. If it doesn't exist, create it."""
  24     assert file_name.endswith(".csv")
  25     path = SETTINGS_DIR / file_name
  26 
  27     # Create the file if it doesn't exist
  28     if not SETTINGS_DIR.is_dir():
  29         os.mkdir(SETTINGS_DIR)
  30     if not path.is_file():
  31         with open(path, "w", encoding="utf-8") as file:
  32             writer = csv.writer(file)
  33             writer.writerow(headers)
  34             for key, value in default.items():
  35                 writer.writerow([key] if key == value else [value, key])
  36 
  37     # Now read from disk
  38     with open(path, "r", encoding="utf-8") as file:
  39         rows = list(csv.reader(file))
  40 
  41     mapping = {}
  42     if len(rows) >= 2:
  43         actual_headers = rows[0]
  44         if not actual_headers == list(headers):
  45             print(
  46                 f'"{file_name}": Malformed headers - {actual_headers}.'
  47                 + f" Should be {list(headers)}. Ignoring row."
  48             )
  49         for row in rows[1:]:
  50             if len(row) == 0:
  51                 # Windows newlines are sometimes read as empty rows. :champagne:
  52                 continue
  53             if len(row) == 1:
  54                 mapping[row[0]] = row[0]
  55             else:
  56                 mapping[row[1]] = row[0]
  57                 if len(row) > 2:
  58                     print(
  59                         f'"{file_name}": More than two values in row: {row}.'
  60                         + " Ignoring the extras."
  61                     )
  62     return mapping
  63 
  64 
  65 _mapped_lists = {}
  66 _settings_lock = threading.Lock()
  67 _word_map_params = None
  68 
  69 
  70 def _update_list(list_name: str, *csv_params):
  71     """Update list with `list_name` from a csv on disk.
  72 
  73     `csv_params` will be passed to `_load_csv_dict`.
  74 
  75     """
  76     global ctx
  77     ctx.lists[list_name] = _load_csv_dict(*csv_params)
  78 
  79 
  80 def _update_word_map(*csv_params):
  81     """Update `dictate.word_map` from disk.
  82 
  83     `csv_params` will be passed to `_load_csv_dict`.
  84 
  85     """
  86     global ctx
  87     ctx.settings["dictate.word_map"] = _load_csv_dict(*csv_params)
  88 
  89 
  90 def _update_lists(*_):
  91     """Update all CSV lists from disk."""
  92     print("Updating CSV lists...")
  93     with _settings_lock:
  94         for list_name, csv_params in _mapped_lists.items():
  95             try:
  96                 _update_list(list_name, *csv_params)
  97             except Exception as e:
  98                 print(f'Error loading list "{list_name}": {e}')
  99         # Special case - `dictate.word_map` isn't a list.
  100         if _word_map_params:
  101             try:
  102                 _update_word_map(*_word_map_params)
  103             except Exception as e:
  104                 print(f'Error updating "dictate.word_map": {e}')
  105 
  106 
  107 def bind_list_to_csv(
  108     list_name: str,
  109     csv_name: str,
  110     csv_headers: Tuple[str, str],
  111     default_values: Dict[str, str] = {},
  112 ) -> None:
  113     """Register a Talon list that should be updated from a CSV on disk.
  114 
  115     The CSV file will be created automatically in the "settings" dir if it
  116     doesn't exist. This directory can be tracked independently to
  117     `knausj_talon`, allowing the user to specify things like private vocab
  118     separately.
  119 
  120     Note the list must be declared separately.
  121 
  122     """
  123     global _mapped_lists
  124     with _settings_lock:
  125         _update_list(list_name, csv_name, csv_headers, default_values)
  126         # If there were no errors, we can register it permanently.
  127         _mapped_lists[list_name] = (csv_name, csv_headers, default_values)
  128 
  129 
  130 def bind_word_map_to_csv(
  131     csv_name: str, csv_headers: Tuple[str, str], default_values: Dict[str, str] = {}
  132 ) -> None:
  133     """Like `bind_list_to_csv`, but for the `dictate.word_map` setting.
  134 
  135     Since it is a setting, not a list, it has to be handled separately.
  136 
  137     """
  138     global _word_map_params
  139     # TODO: Maybe a generic system for binding the dicts to settings? Only
  140     #   implement if it's needed.
  141     with _settings_lock:
  142         _update_word_map(csv_name, csv_headers, default_values)
  143         # If there were no errors, we can register it permanently.
  144         _word_map_params = (csv_name, csv_headers, default_values)
  145 
  146 
  147 fs.watch(str(SETTINGS_DIR), _update_lists)
  148
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/code/user_settings.py b/code/user_settings.py
  --- a/code/user_settings.py
  +++ b/code/user_settings.py
  @@ -51,14 +51,17 @@
                   # Windows newlines are sometimes read as empty rows. :champagne:
                   continue
               if len(row) == 1:
  -                mapping[row[0]] = row[0]
  +                output = spoken_form = row[0]
               else:
  -                mapping[row[1]] = row[0]
  +                output, spoken_form = row[:2]
                   if len(row) > 2:
                       print(
                           f'"{file_name}": More than two values in row: {row}.'
                           + " Ignoring the extras."
                       )
  +            # Leading/trailing whitespace in spoken form can prevent recognition.
  +            spoken_form = spoken_form.strip()
  +            mapping[spoken_form] = output
       return mapping
  
verification_info: {"golden_diff": "diff --git a/code/user_settings.py b/code/user_settings.py\n--- a/code/user_settings.py\n+++ b/code/user_settings.py\n@@ -51,14 +51,17 @@\n                 # Windows newlines are sometimes read as empty rows. :champagne:\n                 continue\n             if len(row) == 1:\n-                mapping[row[0]] = row[0]\n+                output = spoken_form = row[0]\n             else:\n-                mapping[row[1]] = row[0]\n+                output, spoken_form = row[:2]\n                 if len(row) > 2:\n                     print(\n                         f'\"{file_name}\": More than two values in row: {row}.'\n                         + \" Ignoring the extras.\"\n                     )\n+            # Leading/trailing whitespace in spoken form can prevent recognition.\n+            spoken_form = spoken_form.strip()\n+            mapping[spoken_form] = output\n     return mapping\n", "issue": "Vocabulary functions do not strip whitespace from CSV\nI had the misfortune the last few days to believe that the vocabulary csv files were not operating correctly.\r\n\r\nApparently whitespace is not stripped around fields. So \"test, test2\" leads a row which is parsed, but not turned into a functioning command.\n", "before_files": [{"content": "from talon import Module, fs, Context\nimport os\nimport csv\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\nimport threading\n\n\n# NOTE: This method requires this module to be one folder below the top-level\n#   knausj folder.\nSETTINGS_DIR = Path(__file__).parents[1] / \"settings\"\n\nif not SETTINGS_DIR.is_dir():\n    os.mkdir(SETTINGS_DIR)\n\nmod = Module()\nctx = Context()\n\n\ndef _load_csv_dict(\n    file_name: str, headers=Tuple[str, str], default: Dict[str, str] = {}\n) -> Dict[str, str]:\n    \"\"\"Load a word mapping from a CSV file. If it doesn't exist, create it.\"\"\"\n    assert file_name.endswith(\".csv\")\n    path = SETTINGS_DIR / file_name\n\n    # Create the file if it doesn't exist\n    if not SETTINGS_DIR.is_dir():\n        os.mkdir(SETTINGS_DIR)\n    if not path.is_file():\n        with open(path, \"w\", encoding=\"utf-8\") as file:\n            writer = csv.writer(file)\n            writer.writerow(headers)\n            for key, value in default.items():\n                writer.writerow([key] if key == value else [value, key])\n\n    # Now read from disk\n    with open(path, \"r\", encoding=\"utf-8\") as file:\n        rows = list(csv.reader(file))\n\n    mapping = {}\n    if len(rows) >= 2:\n        actual_headers = rows[0]\n        if not actual_headers == list(headers):\n            print(\n                f'\"{file_name}\": Malformed headers - {actual_headers}.'\n                + f\" Should be {list(headers)}. Ignoring row.\"\n            )\n        for row in rows[1:]:\n            if len(row) == 0:\n                # Windows newlines are sometimes read as empty rows. :champagne:\n                continue\n            if len(row) == 1:\n                mapping[row[0]] = row[0]\n            else:\n                mapping[row[1]] = row[0]\n                if len(row) > 2:\n                    print(\n                        f'\"{file_name}\": More than two values in row: {row}.'\n                        + \" Ignoring the extras.\"\n                    )\n    return mapping\n\n\n_mapped_lists = {}\n_settings_lock = threading.Lock()\n_word_map_params = None\n\n\ndef _update_list(list_name: str, *csv_params):\n    \"\"\"Update list with `list_name` from a csv on disk.\n\n    `csv_params` will be passed to `_load_csv_dict`.\n\n    \"\"\"\n    global ctx\n    ctx.lists[list_name] = _load_csv_dict(*csv_params)\n\n\ndef _update_word_map(*csv_params):\n    \"\"\"Update `dictate.word_map` from disk.\n\n    `csv_params` will be passed to `_load_csv_dict`.\n\n    \"\"\"\n    global ctx\n    ctx.settings[\"dictate.word_map\"] = _load_csv_dict(*csv_params)\n\n\ndef _update_lists(*_):\n    \"\"\"Update all CSV lists from disk.\"\"\"\n    print(\"Updating CSV lists...\")\n    with _settings_lock:\n        for list_name, csv_params in _mapped_lists.items():\n            try:\n                _update_list(list_name, *csv_params)\n            except Exception as e:\n                print(f'Error loading list \"{list_name}\": {e}')\n        # Special case - `dictate.word_map` isn't a list.\n        if _word_map_params:\n            try:\n                _update_word_map(*_word_map_params)\n            except Exception as e:\n                print(f'Error updating \"dictate.word_map\": {e}')\n\n\ndef bind_list_to_csv(\n    list_name: str,\n    csv_name: str,\n    csv_headers: Tuple[str, str],\n    default_values: Dict[str, str] = {},\n) -> None:\n    \"\"\"Register a Talon list that should be updated from a CSV on disk.\n\n    The CSV file will be created automatically in the \"settings\" dir if it\n    doesn't exist. This directory can be tracked independently to\n    `knausj_talon`, allowing the user to specify things like private vocab\n    separately.\n\n    Note the list must be declared separately.\n\n    \"\"\"\n    global _mapped_lists\n    with _settings_lock:\n        _update_list(list_name, csv_name, csv_headers, default_values)\n        # If there were no errors, we can register it permanently.\n        _mapped_lists[list_name] = (csv_name, csv_headers, default_values)\n\n\ndef bind_word_map_to_csv(\n    csv_name: str, csv_headers: Tuple[str, str], default_values: Dict[str, str] = {}\n) -> None:\n    \"\"\"Like `bind_list_to_csv`, but for the `dictate.word_map` setting.\n\n    Since it is a setting, not a list, it has to be handled separately.\n\n    \"\"\"\n    global _word_map_params\n    # TODO: Maybe a generic system for binding the dicts to settings? Only\n    #   implement if it's needed.\n    with _settings_lock:\n        _update_word_map(csv_name, csv_headers, default_values)\n        # If there were no errors, we can register it permanently.\n        _word_map_params = (csv_name, csv_headers, default_values)\n\n\nfs.watch(str(SETTINGS_DIR), _update_lists)\n", "path": "code/user_settings.py"}], "after_files": [{"content": "from talon import Module, fs, Context\nimport os\nimport csv\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\nimport threading\n\n\n# NOTE: This method requires this module to be one folder below the top-level\n#   knausj folder.\nSETTINGS_DIR = Path(__file__).parents[1] / \"settings\"\n\nif not SETTINGS_DIR.is_dir():\n    os.mkdir(SETTINGS_DIR)\n\nmod = Module()\nctx = Context()\n\n\ndef _load_csv_dict(\n    file_name: str, headers=Tuple[str, str], default: Dict[str, str] = {}\n) -> Dict[str, str]:\n    \"\"\"Load a word mapping from a CSV file. If it doesn't exist, create it.\"\"\"\n    assert file_name.endswith(\".csv\")\n    path = SETTINGS_DIR / file_name\n\n    # Create the file if it doesn't exist\n    if not SETTINGS_DIR.is_dir():\n        os.mkdir(SETTINGS_DIR)\n    if not path.is_file():\n        with open(path, \"w\", encoding=\"utf-8\") as file:\n            writer = csv.writer(file)\n            writer.writerow(headers)\n            for key, value in default.items():\n                writer.writerow([key] if key == value else [value, key])\n\n    # Now read from disk\n    with open(path, \"r\", encoding=\"utf-8\") as file:\n        rows = list(csv.reader(file))\n\n    mapping = {}\n    if len(rows) >= 2:\n        actual_headers = rows[0]\n        if not actual_headers == list(headers):\n            print(\n                f'\"{file_name}\": Malformed headers - {actual_headers}.'\n                + f\" Should be {list(headers)}. Ignoring row.\"\n            )\n        for row in rows[1:]:\n            if len(row) == 0:\n                # Windows newlines are sometimes read as empty rows. :champagne:\n                continue\n            if len(row) == 1:\n                output = spoken_form = row[0]\n            else:\n                output, spoken_form = row[:2]\n                if len(row) > 2:\n                    print(\n                        f'\"{file_name}\": More than two values in row: {row}.'\n                        + \" Ignoring the extras.\"\n                    )\n            # Leading/trailing whitespace in spoken form can prevent recognition.\n            spoken_form = spoken_form.strip()\n            mapping[spoken_form] = output\n    return mapping\n\n\n_mapped_lists = {}\n_settings_lock = threading.Lock()\n_word_map_params = None\n\n\ndef _update_list(list_name: str, *csv_params):\n    \"\"\"Update list with `list_name` from a csv on disk.\n\n    `csv_params` will be passed to `_load_csv_dict`.\n\n    \"\"\"\n    global ctx\n    ctx.lists[list_name] = _load_csv_dict(*csv_params)\n\n\ndef _update_word_map(*csv_params):\n    \"\"\"Update `dictate.word_map` from disk.\n\n    `csv_params` will be passed to `_load_csv_dict`.\n\n    \"\"\"\n    global ctx\n    ctx.settings[\"dictate.word_map\"] = _load_csv_dict(*csv_params)\n\n\ndef _update_lists(*_):\n    \"\"\"Update all CSV lists from disk.\"\"\"\n    print(\"Updating CSV lists...\")\n    with _settings_lock:\n        for list_name, csv_params in _mapped_lists.items():\n            try:\n                _update_list(list_name, *csv_params)\n            except Exception as e:\n                print(f'Error loading list \"{list_name}\": {e}')\n        # Special case - `dictate.word_map` isn't a list.\n        if _word_map_params:\n            try:\n                _update_word_map(*_word_map_params)\n            except Exception as e:\n                print(f'Error updating \"dictate.word_map\": {e}')\n\n\ndef bind_list_to_csv(\n    list_name: str,\n    csv_name: str,\n    csv_headers: Tuple[str, str],\n    default_values: Dict[str, str] = {},\n) -> None:\n    \"\"\"Register a Talon list that should be updated from a CSV on disk.\n\n    The CSV file will be created automatically in the \"settings\" dir if it\n    doesn't exist. This directory can be tracked independently to\n    `knausj_talon`, allowing the user to specify things like private vocab\n    separately.\n\n    Note the list must be declared separately.\n\n    \"\"\"\n    global _mapped_lists\n    with _settings_lock:\n        _update_list(list_name, csv_name, csv_headers, default_values)\n        # If there were no errors, we can register it permanently.\n        _mapped_lists[list_name] = (csv_name, csv_headers, default_values)\n\n\ndef bind_word_map_to_csv(\n    csv_name: str, csv_headers: Tuple[str, str], default_values: Dict[str, str] = {}\n) -> None:\n    \"\"\"Like `bind_list_to_csv`, but for the `dictate.word_map` setting.\n\n    Since it is a setting, not a list, it has to be handled separately.\n\n    \"\"\"\n    global _word_map_params\n    # TODO: Maybe a generic system for binding the dicts to settings? Only\n    #   implement if it's needed.\n    with _settings_lock:\n        _update_word_map(csv_name, csv_headers, default_values)\n        # If there were no errors, we can register it permanently.\n        _word_map_params = (csv_name, csv_headers, default_values)\n\n\nfs.watch(str(SETTINGS_DIR), _update_lists)\n", "path": "code/user_settings.py"}]}