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.
- {fastled-1.4.45 → fastled-1.4.49}/PKG-INFO +2 -1
- {fastled-1.4.45 → fastled-1.4.49}/pyproject.toml +1 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__version__.py +1 -1
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/app.py +1 -1
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/client_server.py +9 -1
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/parse_args.py +21 -6
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/select_sketch_directory.py +42 -4
- fastled-1.4.49/src/fastled/sketch.py +219 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/web_compile.py +9 -7
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/PKG-INFO +2 -1
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/SOURCES.txt +1 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/requires.txt +1 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_select_sketch_directory.py +60 -0
- fastled-1.4.49/tests/unit/test_sketch_partial_match.py +145 -0
- fastled-1.4.45/src/fastled/sketch.py +0 -102
- {fastled-1.4.45 → fastled-1.4.49}/.aiderignore +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.claude/settings.local.json +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.cursorrules +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.dockerignore +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/build_multi_docker_image.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/build_webpage.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/lint.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/publish_release.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/template_build_docker_image.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_build_exe.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_macos.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_ubuntu.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.github/workflows/test_win.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.gitignore +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.pylintrc +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.vscode/launch.json +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.vscode/settings.json +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/.vscode/tasks.json +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/DEBUGGER.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/Dockerfile +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/EXTENDED_INO.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/FAQ.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/LICENSE +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/MANIFEST.in +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/README.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/RELEASE.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/TODO.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/build_exe.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/build_local_docker.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/build_site.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/chrome_vscode_bridge_design_task.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/clean +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/compiler/debug.sh +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/compiler/run.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/demo/100dots.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/demo/demo_threejs.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/demo/micdemo.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/demo/mp3upload.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/docker-compose.yml +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/entrypoint.sh +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/install +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/install_linux.sh +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/lint +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/requirements.docker.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/requirements.testing.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/setup.cfg +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/setup.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__init__.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/__main__.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/args.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/example.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/localhost-key.pem +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/assets/localhost.pem +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli_test.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/cli_test_interactive.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/compile_server.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/compile_server_impl.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/docker_manager.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/emoji_util.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/filewatcher.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/find_good_connection.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/header_dump.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/__init__.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/examples_manager.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/extension_manager.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/main.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/project_detection.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/test_install.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/install/vscode_config.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/interruptible_http.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/keyboard.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/keyz.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/live_client.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/open_browser.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/paths.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/chrome_extension_downloader.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/playwright_browser.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/playwright/resize_tracking.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/print_filter.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/project_init.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/server_flask.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/server_start.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/settings.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/site/build.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/site/examples.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/spinner.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/string_diff.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/test/can_run_local_docker_tests.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/test/examples.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/types.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/util.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/version.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled/zip_files.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/dependency_links.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/entry_points.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/src/fastled.egg-info/top_level.txt +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/task.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/task2.md +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/test +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_build_examples.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_examples.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_libcompile.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/integration/test_playwright_integration.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/html/index.html +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_api.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_bad_ino.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_banner_string.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_cli.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_cli_no_platformio.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_compile_server.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_debug_fetch_source_files.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_docker_linux_on_windows.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_embedded_data.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_filechanger.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_flask_headers.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_header_dump.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_http_server.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad/bad.ino +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/embedded/wasm.ino +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_ino/wasm/wasm.ino +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_manual_api_invocation.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_no_platformio_compile.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_project_init.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_server_and_client_seperatly.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_session_compile.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_string_diff.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_string_diff_comprehensive.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_version.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/tests/unit/test_webcompile.py +0 -0
- {fastled-1.4.45 → fastled-1.4.49}/upload_package.sh +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
382
|
-
|
|
383
|
-
if
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
441
|
-
|
|
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=
|
|
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,
|
|
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.
|
|
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
|
|
@@ -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()
|