fastled 1.4.45__tar.gz → 1.4.49__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fastled might be problematic. Click here for more details.

Files changed (151) hide show
  1. {fastled-1.4.45 → fastled-1.4.49}/PKG-INFO +2 -1
  2. {fastled-1.4.45 → fastled-1.4.49}/pyproject.toml +1 -0
  3. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__version__.py +1 -1
  4. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/app.py +1 -1
  5. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/client_server.py +9 -1
  6. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/parse_args.py +21 -6
  7. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/select_sketch_directory.py +42 -4
  8. fastled-1.4.49/src/fastled/sketch.py +219 -0
  9. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/web_compile.py +9 -7
  10. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/PKG-INFO +2 -1
  11. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/SOURCES.txt +1 -0
  12. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/requires.txt +1 -0
  13. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_select_sketch_directory.py +60 -0
  14. fastled-1.4.49/tests/unit/test_sketch_partial_match.py +145 -0
  15. fastled-1.4.45/src/fastled/sketch.py +0 -102
  16. {fastled-1.4.45 → fastled-1.4.49}/.aiderignore +0 -0
  17. {fastled-1.4.45 → fastled-1.4.49}/.claude/settings.local.json +0 -0
  18. {fastled-1.4.45 → fastled-1.4.49}/.cursorrules +0 -0
  19. {fastled-1.4.45 → fastled-1.4.49}/.dockerignore +0 -0
  20. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/build_multi_docker_image.yml +0 -0
  21. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/build_webpage.yml +0 -0
  22. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/lint.yml +0 -0
  23. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/publish_release.yml +0 -0
  24. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/template_build_docker_image.yml +0 -0
  25. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_build_exe.yml +0 -0
  26. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_macos.yml +0 -0
  27. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_ubuntu.yml +0 -0
  28. {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_win.yml +0 -0
  29. {fastled-1.4.45 → fastled-1.4.49}/.gitignore +0 -0
  30. {fastled-1.4.45 → fastled-1.4.49}/.pylintrc +0 -0
  31. {fastled-1.4.45 → fastled-1.4.49}/.vscode/launch.json +0 -0
  32. {fastled-1.4.45 → fastled-1.4.49}/.vscode/settings.json +0 -0
  33. {fastled-1.4.45 → fastled-1.4.49}/.vscode/tasks.json +0 -0
  34. {fastled-1.4.45 → fastled-1.4.49}/DEBUGGER.md +0 -0
  35. {fastled-1.4.45 → fastled-1.4.49}/Dockerfile +0 -0
  36. {fastled-1.4.45 → fastled-1.4.49}/EXTENDED_INO.md +0 -0
  37. {fastled-1.4.45 → fastled-1.4.49}/FAQ.md +0 -0
  38. {fastled-1.4.45 → fastled-1.4.49}/LICENSE +0 -0
  39. {fastled-1.4.45 → fastled-1.4.49}/MANIFEST.in +0 -0
  40. {fastled-1.4.45 → fastled-1.4.49}/README.md +0 -0
  41. {fastled-1.4.45 → fastled-1.4.49}/RELEASE.md +0 -0
  42. {fastled-1.4.45 → fastled-1.4.49}/TODO.md +0 -0
  43. {fastled-1.4.45 → fastled-1.4.49}/build_exe.py +0 -0
  44. {fastled-1.4.45 → fastled-1.4.49}/build_local_docker.py +0 -0
  45. {fastled-1.4.45 → fastled-1.4.49}/build_site.py +0 -0
  46. {fastled-1.4.45 → fastled-1.4.49}/chrome_vscode_bridge_design_task.md +0 -0
  47. {fastled-1.4.45 → fastled-1.4.49}/clean +0 -0
  48. {fastled-1.4.45 → fastled-1.4.49}/compiler/debug.sh +0 -0
  49. {fastled-1.4.45 → fastled-1.4.49}/compiler/run.py +0 -0
  50. {fastled-1.4.45 → fastled-1.4.49}/demo/100dots.html +0 -0
  51. {fastled-1.4.45 → fastled-1.4.49}/demo/demo_threejs.html +0 -0
  52. {fastled-1.4.45 → fastled-1.4.49}/demo/micdemo.html +0 -0
  53. {fastled-1.4.45 → fastled-1.4.49}/demo/mp3upload.html +0 -0
  54. {fastled-1.4.45 → fastled-1.4.49}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
  55. {fastled-1.4.45 → fastled-1.4.49}/docker-compose.yml +0 -0
  56. {fastled-1.4.45 → fastled-1.4.49}/entrypoint.sh +0 -0
  57. {fastled-1.4.45 → fastled-1.4.49}/install +0 -0
  58. {fastled-1.4.45 → fastled-1.4.49}/install_linux.sh +0 -0
  59. {fastled-1.4.45 → fastled-1.4.49}/lint +0 -0
  60. {fastled-1.4.45 → fastled-1.4.49}/requirements.docker.txt +0 -0
  61. {fastled-1.4.45 → fastled-1.4.49}/requirements.testing.txt +0 -0
  62. {fastled-1.4.45 → fastled-1.4.49}/setup.cfg +0 -0
  63. {fastled-1.4.45 → fastled-1.4.49}/setup.py +0 -0
  64. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__init__.py +0 -0
  65. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__main__.py +0 -0
  66. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/args.py +0 -0
  67. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/example.txt +0 -0
  68. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/localhost-key.pem +0 -0
  69. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/localhost.pem +0 -0
  70. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli.py +0 -0
  71. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli_test.py +0 -0
  72. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli_test_interactive.py +0 -0
  73. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/compile_server.py +0 -0
  74. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/compile_server_impl.py +0 -0
  75. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/docker_manager.py +0 -0
  76. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/emoji_util.py +0 -0
  77. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/filewatcher.py +0 -0
  78. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/find_good_connection.py +0 -0
  79. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/header_dump.py +0 -0
  80. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/__init__.py +0 -0
  81. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/examples_manager.py +0 -0
  82. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/extension_manager.py +0 -0
  83. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/main.py +0 -0
  84. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/project_detection.py +0 -0
  85. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/test_install.py +0 -0
  86. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/vscode_config.py +0 -0
  87. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/interruptible_http.py +0 -0
  88. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/keyboard.py +0 -0
  89. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/keyz.py +0 -0
  90. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/live_client.py +0 -0
  91. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/open_browser.py +0 -0
  92. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/paths.py +0 -0
  93. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/chrome_extension_downloader.py +0 -0
  94. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/playwright_browser.py +0 -0
  95. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/resize_tracking.py +0 -0
  96. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/print_filter.py +0 -0
  97. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/project_init.py +0 -0
  98. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/server_flask.py +0 -0
  99. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/server_start.py +0 -0
  100. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/settings.py +0 -0
  101. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/site/build.py +0 -0
  102. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/site/examples.py +0 -0
  103. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/spinner.py +0 -0
  104. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/string_diff.py +0 -0
  105. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  106. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/test/examples.py +0 -0
  107. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/types.py +0 -0
  108. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/util.py +0 -0
  109. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/version.py +0 -0
  110. {fastled-1.4.45 → fastled-1.4.49}/src/fastled/zip_files.py +0 -0
  111. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/dependency_links.txt +0 -0
  112. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/entry_points.txt +0 -0
  113. {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/top_level.txt +0 -0
  114. {fastled-1.4.45 → fastled-1.4.49}/task.md +0 -0
  115. {fastled-1.4.45 → fastled-1.4.49}/task2.md +0 -0
  116. {fastled-1.4.45 → fastled-1.4.49}/test +0 -0
  117. {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_build_examples.py +0 -0
  118. {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_examples.py +0 -0
  119. {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_libcompile.py +0 -0
  120. {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_playwright_integration.py +0 -0
  121. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/html/index.html +0 -0
  122. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_api.py +0 -0
  123. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_bad_ino.py +0 -0
  124. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_banner_string.py +0 -0
  125. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_cli.py +0 -0
  126. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_cli_no_platformio.py +0 -0
  127. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_compile_server.py +0 -0
  128. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_debug_fetch_source_files.py +0 -0
  129. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_docker_linux_on_windows.py +0 -0
  130. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_embedded_data.py +0 -0
  131. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_filechanger.py +0 -0
  132. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_flask_headers.py +0 -0
  133. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_header_dump.py +0 -0
  134. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_http_server.py +0 -0
  135. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad/bad.ino +0 -0
  136. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  137. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  138. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  139. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  140. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  141. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_manual_api_invocation.py +0 -0
  142. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_no_platformio_compile.py +0 -0
  143. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_project_init.py +0 -0
  144. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_server_and_client_seperatly.py +0 -0
  145. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_session_compile.py +0 -0
  146. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_string_diff.py +0 -0
  147. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_string_diff_comprehensive.py +0 -0
  148. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_version.py +0 -0
  149. {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_webcompile.py +0 -0
  150. {fastled-1.4.45 → fastled-1.4.49}/upload_package.sh +0 -0
  151. {fastled-1.4.45 → fastled-1.4.49}/vscode-plugin/readme +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.45
3
+ Version: 1.4.49
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -24,6 +24,7 @@ Requires-Dist: livereload
24
24
  Requires-Dist: disklru>=2.0.4
25
25
  Requires-Dist: playwright>=1.40.0
26
26
  Requires-Dist: websockify>=0.13.0
27
+ Requires-Dist: fasteners>=0.20
27
28
  Dynamic: home-page
28
29
  Dynamic: license-file
29
30
  Dynamic: maintainer
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "disklru>=2.0.4",
28
28
  "playwright>=1.40.0",
29
29
  "websockify>=0.13.0",
30
+ "fasteners>=0.20",
30
31
  ]
31
32
 
32
33
  dynamic = ["version"]
@@ -1,6 +1,6 @@
1
1
  # IMPORTANT! There's a bug in github which will REJECT any version update
2
2
  # that has any other change in the repo. Please bump the version as the
3
3
  # ONLY change in a commit, or else the pypi update and the release will fail.
4
- __version__ = "1.4.45"
4
+ __version__ = "1.4.49"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
@@ -121,7 +121,7 @@ def main() -> int:
121
121
  sketch_list: list[Path] = find_sketch_directories()
122
122
  if sketch_list:
123
123
  maybe_dir: str | None = select_sketch_directory(
124
- sketch_list, cwd_looks_like_fastled_repo
124
+ sketch_list, cwd_looks_like_fastled_repo, is_followup=True
125
125
  )
126
126
  if maybe_dir is not None:
127
127
  directory = Path(maybe_dir)
@@ -452,7 +452,15 @@ def run_client(
452
452
  ) -> tuple[bool, CompileResult]:
453
453
  changed_files = debounced_sketch_watcher.get_all_changes()
454
454
  if changed_files:
455
- print(f"\nChanges detected in {changed_files}")
455
+ # Filter out any fastled_js changes that slipped through
456
+ sketch_changes = [
457
+ f for f in changed_files if "fastled_js" not in Path(f).parts
458
+ ]
459
+ if not sketch_changes:
460
+ # All changes were in fastled_js, ignore them
461
+ return False, last_compiled_result
462
+ print(f"\nChanges detected in {sketch_changes}")
463
+ print("Compiling...")
456
464
  last_hash_value = last_compiled_result.hash_value
457
465
  out = compile_function(last_hash_value=last_hash_value)
458
466
  if not out.success:
@@ -8,6 +8,7 @@ from fastled.project_init import project_init
8
8
  from fastled.select_sketch_directory import select_sketch_directory
9
9
  from fastled.settings import DEFAULT_URL, IMAGE_NAME
10
10
  from fastled.sketch import (
11
+ find_sketch_by_partial_name,
11
12
  find_sketch_directories,
12
13
  looks_like_fastled_repo,
13
14
  looks_like_sketch_directory,
@@ -368,7 +369,7 @@ def parse_args() -> Args:
368
369
  print("Searching for sketch directories...")
369
370
  sketch_directories = find_sketch_directories(maybe_sketch_dir)
370
371
  selected_dir = select_sketch_directory(
371
- sketch_directories, cwd_is_fastled
372
+ sketch_directories, cwd_is_fastled, is_followup=True
372
373
  )
373
374
  if selected_dir:
374
375
  print(f"Using sketch directory: {selected_dir}")
@@ -378,10 +379,24 @@ def parse_args() -> Args:
378
379
  "\nYou either need to specify a sketch directory or run in --server mode."
379
380
  )
380
381
  sys.exit(1)
381
- elif args.directory is not None and os.path.isfile(args.directory):
382
- dir_path = Path(args.directory).parent
383
- if looks_like_sketch_directory(dir_path):
384
- print(f"Using sketch directory: {dir_path}")
385
- args.directory = str(dir_path)
382
+ elif args.directory is not None:
383
+ # Check if directory is a file path
384
+ if os.path.isfile(args.directory):
385
+ dir_path = Path(args.directory).parent
386
+ if looks_like_sketch_directory(dir_path):
387
+ print(f"Using sketch directory: {dir_path}")
388
+ args.directory = str(dir_path)
389
+ # Check if directory exists as a path
390
+ elif not os.path.exists(args.directory):
391
+ # Directory doesn't exist - try partial name matching
392
+ try:
393
+ matched_dir = find_sketch_by_partial_name(args.directory)
394
+ print(
395
+ f"Matched '{args.directory}' to sketch directory: {matched_dir}"
396
+ )
397
+ args.directory = str(matched_dir)
398
+ except ValueError as e:
399
+ print(f"Error: {e}")
400
+ sys.exit(1)
386
401
 
387
402
  return Args.from_namespace(args)
@@ -1,6 +1,8 @@
1
1
  from pathlib import Path
2
2
  from typing import Callable, TypeVar, Union
3
3
 
4
+ from fastled.string_diff import string_diff
5
+
4
6
  T = TypeVar("T")
5
7
 
6
8
 
@@ -82,6 +84,44 @@ def _disambiguate_user_choice(
82
84
  0, # Reset default to first match
83
85
  )
84
86
 
87
+ # Try fuzzy matching as fallback
88
+ # For better fuzzy matching on paths, extract just the last component (basename)
89
+ # to avoid the "examples/" prefix interfering with matching
90
+ from pathlib import Path as PathLib
91
+
92
+ option_basenames = []
93
+ for option in options:
94
+ option_str = option_to_str(option)
95
+ # Extract basename for fuzzy matching
96
+ basename = (
97
+ PathLib(option_str).name
98
+ if "/" in option_str or "\\" in option_str
99
+ else option_str
100
+ )
101
+ option_basenames.append(basename)
102
+
103
+ fuzzy_results = string_diff(user_input, option_basenames)
104
+
105
+ if fuzzy_results:
106
+ # Map fuzzy results back to original options
107
+ fuzzy_matches = []
108
+ for _, matched_basename in fuzzy_results:
109
+ for i, basename in enumerate(option_basenames):
110
+ if basename == matched_basename:
111
+ fuzzy_matches.append(options[i])
112
+ break
113
+
114
+ if len(fuzzy_matches) == 1:
115
+ return fuzzy_matches[0]
116
+ elif len(fuzzy_matches) > 1:
117
+ # Recursive disambiguation with fuzzy matches
118
+ return _disambiguate_user_choice(
119
+ fuzzy_matches,
120
+ option_to_str,
121
+ f"Multiple fuzzy matches for '{user_input}':",
122
+ 0,
123
+ )
124
+
85
125
  # No match found
86
126
  print(f"No match found for '{user_input}'. Please try again.")
87
127
  return _disambiguate_user_choice(options, option_to_str, prompt, default_index)
@@ -101,13 +141,11 @@ def select_sketch_directory(
101
141
  print(f"\nUsing sketch directory: {sketch_directories[0]}")
102
142
  return str(sketch_directories[0])
103
143
  elif len(sketch_directories) > 1:
104
- # On first scan with >4 options, don't prompt - return None to signal ambiguity
144
+ # First scan with >4 directories: return None (too many to auto-select)
105
145
  if not is_followup and len(sketch_directories) > 4:
106
- print(f"\nFound {len(sketch_directories)} sketch directories.")
107
- print("Please specify a sketch directory to avoid ambiguity.")
108
146
  return None
109
147
 
110
- # Otherwise, prompt user to disambiguate
148
+ # Prompt user to disambiguate
111
149
  result = _disambiguate_user_choice(
112
150
  sketch_directories,
113
151
  option_to_str=lambda x: str(x),
@@ -0,0 +1,219 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ _MAX_FILES_SEARCH_LIMIT = 10000
5
+
6
+
7
+ def find_sketch_directories(directory: Path | None = None) -> list[Path]:
8
+ if directory is None:
9
+ directory = Path(".")
10
+ file_count = 0
11
+ sketch_directories: list[Path] = []
12
+ # search all the paths one level deep
13
+ for path in directory.iterdir():
14
+ if path.is_dir():
15
+ dir_name = path.name
16
+ if str(dir_name).startswith("."):
17
+ continue
18
+ file_count += 1
19
+ if file_count > _MAX_FILES_SEARCH_LIMIT:
20
+ print(
21
+ f"More than {_MAX_FILES_SEARCH_LIMIT} files found. Stopping search."
22
+ )
23
+ break
24
+
25
+ if looks_like_sketch_directory(path, quick=True):
26
+ sketch_directories.append(path)
27
+ if dir_name.lower() == "examples":
28
+ # Recursively search examples directory for sketch directories
29
+ def search_examples_recursive(
30
+ examples_path: Path, depth: int = 0, max_depth: int = 3
31
+ ):
32
+ nonlocal file_count
33
+ if depth >= max_depth:
34
+ return
35
+ for example in examples_path.iterdir():
36
+ if example.is_dir():
37
+ if str(example.name).startswith("."):
38
+ continue
39
+ file_count += 1
40
+ if file_count > _MAX_FILES_SEARCH_LIMIT:
41
+ return
42
+ if looks_like_sketch_directory(example, quick=True):
43
+ sketch_directories.append(example)
44
+ else:
45
+ # Keep searching deeper if this isn't a sketch directory
46
+ search_examples_recursive(example, depth + 1, max_depth)
47
+
48
+ search_examples_recursive(path)
49
+ # make relative to cwd
50
+ sketch_directories = [p.relative_to(directory) for p in sketch_directories]
51
+ return sketch_directories
52
+
53
+
54
+ def get_sketch_files(directory: Path) -> list[Path]:
55
+ file_count = 0
56
+ files: list[Path] = []
57
+ for root, dirs, filenames in os.walk(directory):
58
+ # ignore hidden directories
59
+ dirs[:] = [d for d in dirs if not d.startswith(".")]
60
+ # ignore fastled_js directory
61
+ dirs[:] = [d for d in dirs if "fastled_js" not in d]
62
+ # ignore hidden files
63
+ filenames = [f for f in filenames if not f.startswith(".")]
64
+ outer_break = False
65
+ for filename in filenames:
66
+ if "platformio.ini" in filename:
67
+ continue
68
+ file_count += 1
69
+ if file_count > _MAX_FILES_SEARCH_LIMIT:
70
+ print(
71
+ f"More than {_MAX_FILES_SEARCH_LIMIT} files found. Stopping search."
72
+ )
73
+ outer_break = True
74
+ break
75
+ files.append(Path(root) / filename)
76
+ if outer_break:
77
+ break
78
+
79
+ return files
80
+
81
+
82
+ def looks_like_fastled_repo(directory: Path = Path(".")) -> bool:
83
+ libprops = directory / "library.properties"
84
+ if not libprops.exists():
85
+ return False
86
+ txt = libprops.read_text(encoding="utf-8", errors="ignore")
87
+ return "FastLED" in txt
88
+
89
+
90
+ def _lots_and_lots_of_files(directory: Path) -> bool:
91
+ return len(get_sketch_files(directory)) > 100
92
+
93
+
94
+ def looks_like_sketch_directory(directory: Path | str | None, quick=False) -> bool:
95
+ if directory is None:
96
+ return False
97
+ directory = Path(directory)
98
+ if looks_like_fastled_repo(directory):
99
+ print("Directory looks like the FastLED repo")
100
+ return False
101
+
102
+ if not quick:
103
+ if _lots_and_lots_of_files(directory):
104
+ return False
105
+
106
+ # walk the path and if there are over 30 files, return False
107
+ # at the root of the directory there should either be an ino file or a src directory
108
+ # or some cpp files
109
+ # if there is a platformio.ini file, return True
110
+ ino_file_at_root = list(directory.glob("*.ino"))
111
+ if ino_file_at_root:
112
+ return True
113
+ cpp_file_at_root = list(directory.glob("*.cpp"))
114
+ if cpp_file_at_root:
115
+ return True
116
+ platformini_file = list(directory.glob("platformio.ini"))
117
+ if platformini_file:
118
+ return True
119
+ return False
120
+
121
+
122
+ def find_sketch_by_partial_name(
123
+ partial_name: str, search_dir: Path | None = None
124
+ ) -> Path | None:
125
+ """
126
+ Find a sketch directory by partial name match.
127
+
128
+ Args:
129
+ partial_name: Partial name to match against sketch directories
130
+ search_dir: Directory to search in (defaults to current directory)
131
+
132
+ Returns:
133
+ Path to the matching sketch directory, or None if no unique match found
134
+
135
+ Raises:
136
+ ValueError: If multiple matches are found with no clear best match, or no matches found
137
+ """
138
+ if search_dir is None:
139
+ search_dir = Path(".")
140
+
141
+ # First, find all sketch directories
142
+ sketch_directories = find_sketch_directories(search_dir)
143
+
144
+ # Normalize the partial name to use forward slashes for cross-platform matching
145
+ partial_name_normalized = partial_name.replace("\\", "/").lower()
146
+
147
+ # Get the set of characters in the partial name for similarity check
148
+ partial_chars = set(partial_name_normalized)
149
+
150
+ # Find matches where the partial name appears in the path
151
+ matches = []
152
+ for sketch_dir in sketch_directories:
153
+ # Normalize the sketch directory path to use forward slashes
154
+ sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
155
+
156
+ # Character similarity check: at least 50% of partial name chars must be in target
157
+ target_chars = set(sketch_str_normalized)
158
+ matching_chars = partial_chars & target_chars
159
+ similarity = (
160
+ len(matching_chars) / len(partial_chars) if len(partial_chars) > 0 else 0
161
+ )
162
+
163
+ # Check if partial_name matches the directory name or any part of the path
164
+ # AND has sufficient character similarity
165
+ if partial_name_normalized in sketch_str_normalized and similarity >= 0.5:
166
+ matches.append(sketch_dir)
167
+
168
+ if len(matches) == 0:
169
+ # Check if this is a total mismatch (low character similarity with all sketches)
170
+ all_low_similarity = True
171
+ for sketch_dir in sketch_directories:
172
+ sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
173
+ target_chars = set(sketch_str_normalized)
174
+ matching_chars = partial_chars & target_chars
175
+ similarity = (
176
+ len(matching_chars) / len(partial_chars)
177
+ if len(partial_chars) > 0
178
+ else 0
179
+ )
180
+ if similarity > 0.5:
181
+ all_low_similarity = False
182
+ break
183
+
184
+ if all_low_similarity and len(sketch_directories) > 0:
185
+ # List all available sketches
186
+ sketches_str = "\n ".join(str(s) for s in sketch_directories)
187
+ raise ValueError(
188
+ f"'{partial_name}' does not look like any of the available sketches.\n\n"
189
+ f"Available sketches:\n {sketches_str}"
190
+ )
191
+ else:
192
+ raise ValueError(f"No sketch directory found matching '{partial_name}'")
193
+ elif len(matches) == 1:
194
+ return matches[0]
195
+ else:
196
+ # Multiple matches - try to find the best match
197
+ # Best match criteria: exact match of the final directory name
198
+ exact_matches = []
199
+ for match in matches:
200
+ # Get the final directory name
201
+ final_dir_name = match.name.lower()
202
+ if final_dir_name == partial_name_normalized:
203
+ exact_matches.append(match)
204
+
205
+ if len(exact_matches) == 1:
206
+ # Found exactly one exact match - this is the best match
207
+ return exact_matches[0]
208
+ elif len(exact_matches) > 1:
209
+ # Multiple exact matches - still ambiguous
210
+ matches_str = "\n ".join(str(m) for m in exact_matches)
211
+ raise ValueError(
212
+ f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
213
+ )
214
+ else:
215
+ # No exact match - ambiguous partial matches
216
+ matches_str = "\n ".join(str(m) for m in matches)
217
+ raise ValueError(
218
+ f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
219
+ )
@@ -437,21 +437,23 @@ def web_compile(
437
437
  response, zip_result, start_time, zip_time, libfastled_time, sketch_time
438
438
  )
439
439
 
440
- except ConnectionError as e:
441
- _print_banner(str(e))
440
+ except KeyboardInterrupt:
441
+ print("Keyboard interrupt")
442
+ raise
443
+ except (ConnectionError, httpx.RemoteProtocolError, httpx.RequestError) as e:
444
+ # Handle connection and server disconnection issues
445
+ error_msg = f"Server connection error: {e}"
446
+ _print_banner(error_msg)
442
447
  return CompileResult(
443
448
  success=False,
444
- stdout=str(e),
449
+ stdout=error_msg,
445
450
  hash_value=None,
446
451
  zip_bytes=b"",
447
452
  zip_time=zip_time,
448
453
  libfastled_time=libfastled_time,
449
454
  sketch_time=sketch_time,
450
- response_processing_time=0.0, # No response processing in connection error case
455
+ response_processing_time=0.0,
451
456
  )
452
- except KeyboardInterrupt:
453
- print("Keyboard interrupt")
454
- raise
455
457
  except httpx.HTTPError as e:
456
458
  print(f"Error: {e}")
457
459
  return CompileResult(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.45
3
+ Version: 1.4.49
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -24,6 +24,7 @@ Requires-Dist: livereload
24
24
  Requires-Dist: disklru>=2.0.4
25
25
  Requires-Dist: playwright>=1.40.0
26
26
  Requires-Dist: websockify>=0.13.0
27
+ Requires-Dist: fasteners>=0.20
27
28
  Dynamic: home-page
28
29
  Dynamic: license-file
29
30
  Dynamic: maintainer
@@ -133,6 +133,7 @@ tests/unit/test_project_init.py
133
133
  tests/unit/test_select_sketch_directory.py
134
134
  tests/unit/test_server_and_client_seperatly.py
135
135
  tests/unit/test_session_compile.py
136
+ tests/unit/test_sketch_partial_match.py
136
137
  tests/unit/test_string_diff.py
137
138
  tests/unit/test_string_diff_comprehensive.py
138
139
  tests/unit/test_version.py
@@ -12,3 +12,4 @@ livereload
12
12
  disklru>=2.0.4
13
13
  playwright>=1.40.0
14
14
  websockify>=0.13.0
15
+ fasteners>=0.20
@@ -98,6 +98,66 @@ class TestSelectSketchDirectory(unittest.TestCase):
98
98
  # Should be called twice: once for invalid, once for valid
99
99
  self.assertEqual(mock_input.call_count, 2)
100
100
 
101
+ @patch("builtins.input", return_value="beats")
102
+ def test_fuzzy_matching_with_typos(self, mock_input):
103
+ """Fuzzy matching should handle typos like 'beats' -> 'BeatDetection'."""
104
+ sketch_dirs = [
105
+ Path("examples/Audio"),
106
+ Path("examples/BeatDetection"),
107
+ Path("examples/Blink"),
108
+ ]
109
+ result = select_sketch_directory(
110
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
111
+ )
112
+ # Should fuzzy match "beats" to "BeatDetection"
113
+ self.assertEqual(str(Path("examples/BeatDetection")), result)
114
+ mock_input.assert_called_once()
115
+
116
+ @patch("builtins.input", return_value="pacifca")
117
+ def test_fuzzy_matching_with_single_char_typo(self, mock_input):
118
+ """Fuzzy matching should handle single character typos like 'pacifca' -> 'Pacifica'."""
119
+ sketch_dirs = [
120
+ Path("examples/Audio"),
121
+ Path("examples/Pacifica"),
122
+ Path("examples/Blink"),
123
+ ]
124
+ result = select_sketch_directory(
125
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
126
+ )
127
+ # Should fuzzy match "pacifca" to "Pacifica"
128
+ self.assertEqual(str(Path("examples/Pacifica")), result)
129
+ mock_input.assert_called_once()
130
+
131
+ @patch("builtins.input", return_value="demreel")
132
+ def test_fuzzy_matching_missing_char(self, mock_input):
133
+ """Fuzzy matching should handle missing characters like 'demreel' -> 'DemoReel100'."""
134
+ sketch_dirs = [
135
+ Path("examples/Audio"),
136
+ Path("examples/DemoReel100"),
137
+ Path("examples/Blink"),
138
+ ]
139
+ result = select_sketch_directory(
140
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
141
+ )
142
+ # Should fuzzy match "demreel" to "DemoReel100"
143
+ self.assertEqual(str(Path("examples/DemoReel100")), result)
144
+ mock_input.assert_called_once()
145
+
146
+ @patch("builtins.input", return_value="cylom")
147
+ def test_fuzzy_matching_edit_distance(self, mock_input):
148
+ """Fuzzy matching should handle edit distance like 'cylom' -> 'Cylon'."""
149
+ sketch_dirs = [
150
+ Path("examples/Audio"),
151
+ Path("examples/Cylon"),
152
+ Path("examples/Blink"),
153
+ ]
154
+ result = select_sketch_directory(
155
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
156
+ )
157
+ # Should fuzzy match "cylom" to "Cylon"
158
+ self.assertEqual(str(Path("examples/Cylon")), result)
159
+ mock_input.assert_called_once()
160
+
101
161
 
102
162
  if __name__ == "__main__":
103
163
  unittest.main()
@@ -0,0 +1,145 @@
1
+ """
2
+ Unit tests for find_sketch_by_partial_name to ensure correct partial matching behavior.
3
+ """
4
+
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ from fastled.sketch import find_sketch_by_partial_name
10
+
11
+
12
+ class TestFindSketchByPartialName(unittest.TestCase):
13
+ """Test find_sketch_by_partial_name behavior."""
14
+
15
+ @patch("fastled.sketch.find_sketch_directories")
16
+ def test_single_match_returns_path(self, mock_find):
17
+ """Single unique match should return the matching path."""
18
+ mock_find.return_value = [
19
+ Path("examples/sketch1"),
20
+ Path("examples/sketch2"),
21
+ ]
22
+ result = find_sketch_by_partial_name("sketch2")
23
+ self.assertEqual(Path("examples/sketch2"), result)
24
+
25
+ @patch("fastled.sketch.find_sketch_directories")
26
+ def test_no_match_raises_error(self, mock_find):
27
+ """No matches should raise ValueError."""
28
+ mock_find.return_value = [
29
+ Path("examples/sketch1"),
30
+ Path("examples/sketch2"),
31
+ ]
32
+ with self.assertRaises(ValueError) as context:
33
+ find_sketch_by_partial_name("nonexistent")
34
+ self.assertIn("No sketch directory found", str(context.exception))
35
+
36
+ @patch("fastled.sketch.find_sketch_directories")
37
+ def test_multiple_matches_raises_error(self, mock_find):
38
+ """Multiple matches should raise ValueError with list of matches."""
39
+ mock_find.return_value = [
40
+ Path("examples/sketch1"),
41
+ Path("examples/sketch2"),
42
+ Path("more/sketch3"),
43
+ ]
44
+ with self.assertRaises(ValueError) as context:
45
+ find_sketch_by_partial_name("sketch")
46
+ error_msg = str(context.exception)
47
+ self.assertIn("Multiple sketch directories found", error_msg)
48
+ # Check using normalized paths (cross-platform)
49
+ self.assertIn("sketch1", error_msg)
50
+ self.assertIn("sketch2", error_msg)
51
+
52
+ @patch("fastled.sketch.find_sketch_directories")
53
+ def test_case_insensitive_match(self, mock_find):
54
+ """Match should be case insensitive."""
55
+ mock_find.return_value = [
56
+ Path("examples/MySketch"),
57
+ Path("examples/OtherSketch"),
58
+ ]
59
+ result = find_sketch_by_partial_name("mysketch")
60
+ self.assertEqual(Path("examples/MySketch"), result)
61
+
62
+ @patch("fastled.sketch.find_sketch_directories")
63
+ def test_partial_path_match(self, mock_find):
64
+ """Should match partial paths, not just directory names."""
65
+ mock_find.return_value = [
66
+ Path("examples/path/sketch1"),
67
+ Path("examples/other/sketch2"),
68
+ ]
69
+ result = find_sketch_by_partial_name("path/sketch1")
70
+ self.assertEqual(Path("examples/path/sketch1"), result)
71
+
72
+ @patch("fastled.sketch.find_sketch_directories")
73
+ def test_best_match_exact_name(self, mock_find):
74
+ """When multiple partial matches exist, exact directory name match wins."""
75
+ mock_find.return_value = [
76
+ Path("examples/sketch"),
77
+ Path("examples/sketch1"),
78
+ Path("examples/sketch2"),
79
+ ]
80
+ result = find_sketch_by_partial_name("sketch")
81
+ self.assertEqual(Path("examples/sketch"), result)
82
+
83
+ @patch("fastled.sketch.find_sketch_directories")
84
+ def test_no_exact_match_with_multiple_partials_errors(self, mock_find):
85
+ """Multiple partial matches with no exact match should error."""
86
+ mock_find.return_value = [
87
+ Path("examples/sketch1"),
88
+ Path("examples/sketch2"),
89
+ ]
90
+ with self.assertRaises(ValueError) as context:
91
+ find_sketch_by_partial_name("sketch")
92
+ self.assertIn("Multiple sketch directories found", str(context.exception))
93
+
94
+ @patch("fastled.sketch.find_sketch_directories")
95
+ def test_multiple_exact_matches_errors(self, mock_find):
96
+ """Multiple exact matches should error."""
97
+ mock_find.return_value = [
98
+ Path("examples/sketch"),
99
+ Path("more/sketch"),
100
+ ]
101
+ with self.assertRaises(ValueError) as context:
102
+ find_sketch_by_partial_name("sketch")
103
+ self.assertIn("Multiple sketch directories found", str(context.exception))
104
+
105
+ @patch("fastled.sketch.find_sketch_directories")
106
+ def test_low_character_similarity_shows_available_sketches(self, mock_find):
107
+ """When search term has low character similarity, show available sketches."""
108
+ mock_find.return_value = [
109
+ Path("path/to/sketch"),
110
+ Path("examples/MyProject"),
111
+ ]
112
+ with self.assertRaises(ValueError) as context:
113
+ find_sketch_by_partial_name("blah")
114
+ error_msg = str(context.exception)
115
+ self.assertIn("does not look like any of the available sketches", error_msg)
116
+ self.assertIn("Available sketches:", error_msg)
117
+ self.assertIn("sketch", error_msg)
118
+ self.assertIn("MyProject", error_msg)
119
+
120
+ @patch("fastled.sketch.find_sketch_directories")
121
+ def test_substring_match_with_low_similarity_rejected(self, mock_find):
122
+ """Substring match with low character similarity should be rejected."""
123
+ mock_find.return_value = [
124
+ Path("qrs/tuv"),
125
+ ]
126
+ # "ab" has chars {a, b}, "qrs/tuv" has chars {q, r, s, /, t, u, v}
127
+ # No common characters, so similarity = 0/2 = 0% < 50%
128
+ with self.assertRaises(ValueError) as context:
129
+ find_sketch_by_partial_name("ab")
130
+ error_msg = str(context.exception)
131
+ self.assertIn("does not look like any of the available sketches", error_msg)
132
+
133
+ @patch("fastled.sketch.find_sketch_directories")
134
+ def test_sufficient_similarity_allows_match(self, mock_find):
135
+ """Match with sufficient character similarity should succeed."""
136
+ mock_find.return_value = [
137
+ Path("examples/sketch123"),
138
+ ]
139
+ # "sketch" has all chars in "examples/sketch123" (100% similarity)
140
+ result = find_sketch_by_partial_name("sketch")
141
+ self.assertEqual(Path("examples/sketch123"), result)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()