fastled 1.4.45__py3-none-any.whl → 1.4.47__py3-none-any.whl

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.
fastled/__version__.py CHANGED
@@ -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.47"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
fastled/app.py CHANGED
@@ -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)
fastled/client_server.py CHANGED
@@ -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:
fastled/parse_args.py CHANGED
@@ -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)
fastled/print_filter.py CHANGED
@@ -1,52 +1,52 @@
1
- import re
2
- from abc import ABC, abstractmethod
3
- from enum import Enum
4
-
5
-
6
- class PrintFilter(ABC):
7
- """Abstract base class for filtering text output."""
8
-
9
- def __init__(self, echo: bool = True) -> None:
10
- self.echo = echo
11
-
12
- @abstractmethod
13
- def filter(self, text: str) -> str:
14
- """Filter the text according to implementation-specific rules."""
15
- pass
16
-
17
- def print(self, text: str | bytes) -> str:
18
- """Prints the text to the console after filtering."""
19
- if isinstance(text, bytes):
20
- text = text.decode("utf-8")
21
- text = self.filter(text)
22
- if self.echo:
23
- print(text, end="")
24
- return text
25
-
26
-
27
- def _handle_ino_cpp(line: str) -> str:
28
- if ".ino.cpp" in line[0:30]:
29
- # Extract the filename without path and extension
30
- match = re.search(r"src/([^/]+)\.ino\.cpp", line)
31
- if match:
32
- filename = match.group(1)
33
- # Replace with examples/Filename/Filename.ino format
34
- line = line.replace(
35
- f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
36
- )
37
- else:
38
- # Fall back to simple extension replacement if regex doesn't match
39
- line = line.replace(".ino.cpp", ".ino")
40
- return line
41
-
42
-
43
- class PrintFilterDefault(PrintFilter):
44
- """Provides default filtering for FastLED output."""
45
-
46
- def filter(self, text: str) -> str:
47
- return text
48
-
49
-
50
- class CompileOrLink(Enum):
51
- COMPILE = "compile"
52
- LINK = "link"
1
+ import re
2
+ from abc import ABC, abstractmethod
3
+ from enum import Enum
4
+
5
+
6
+ class PrintFilter(ABC):
7
+ """Abstract base class for filtering text output."""
8
+
9
+ def __init__(self, echo: bool = True) -> None:
10
+ self.echo = echo
11
+
12
+ @abstractmethod
13
+ def filter(self, text: str) -> str:
14
+ """Filter the text according to implementation-specific rules."""
15
+ pass
16
+
17
+ def print(self, text: str | bytes) -> str:
18
+ """Prints the text to the console after filtering."""
19
+ if isinstance(text, bytes):
20
+ text = text.decode("utf-8")
21
+ text = self.filter(text)
22
+ if self.echo:
23
+ print(text, end="")
24
+ return text
25
+
26
+
27
+ def _handle_ino_cpp(line: str) -> str:
28
+ if ".ino.cpp" in line[0:30]:
29
+ # Extract the filename without path and extension
30
+ match = re.search(r"src/([^/]+)\.ino\.cpp", line)
31
+ if match:
32
+ filename = match.group(1)
33
+ # Replace with examples/Filename/Filename.ino format
34
+ line = line.replace(
35
+ f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
36
+ )
37
+ else:
38
+ # Fall back to simple extension replacement if regex doesn't match
39
+ line = line.replace(".ino.cpp", ".ino")
40
+ return line
41
+
42
+
43
+ class PrintFilterDefault(PrintFilter):
44
+ """Provides default filtering for FastLED output."""
45
+
46
+ def filter(self, text: str) -> str:
47
+ return text
48
+
49
+
50
+ class CompileOrLink(Enum):
51
+ COMPILE = "compile"
52
+ LINK = "link"
@@ -101,13 +101,11 @@ def select_sketch_directory(
101
101
  print(f"\nUsing sketch directory: {sketch_directories[0]}")
102
102
  return str(sketch_directories[0])
103
103
  elif len(sketch_directories) > 1:
104
- # On first scan with >4 options, don't prompt - return None to signal ambiguity
104
+ # First scan with >4 directories: return None (too many to auto-select)
105
105
  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
106
  return None
109
107
 
110
- # Otherwise, prompt user to disambiguate
108
+ # Prompt user to disambiguate
111
109
  result = _disambiguate_user_choice(
112
110
  sketch_directories,
113
111
  option_to_str=lambda x: str(x),
fastled/sketch.py CHANGED
@@ -100,3 +100,103 @@ def looks_like_sketch_directory(directory: Path | str | None, quick=False) -> bo
100
100
  if platformini_file:
101
101
  return True
102
102
  return False
103
+
104
+
105
+ def find_sketch_by_partial_name(
106
+ partial_name: str, search_dir: Path | None = None
107
+ ) -> Path | None:
108
+ """
109
+ Find a sketch directory by partial name match.
110
+
111
+ Args:
112
+ partial_name: Partial name to match against sketch directories
113
+ search_dir: Directory to search in (defaults to current directory)
114
+
115
+ Returns:
116
+ Path to the matching sketch directory, or None if no unique match found
117
+
118
+ Raises:
119
+ ValueError: If multiple matches are found with no clear best match, or no matches found
120
+ """
121
+ if search_dir is None:
122
+ search_dir = Path(".")
123
+
124
+ # First, find all sketch directories
125
+ sketch_directories = find_sketch_directories(search_dir)
126
+
127
+ # Normalize the partial name to use forward slashes for cross-platform matching
128
+ partial_name_normalized = partial_name.replace("\\", "/").lower()
129
+
130
+ # Get the set of characters in the partial name for similarity check
131
+ partial_chars = set(partial_name_normalized)
132
+
133
+ # Find matches where the partial name appears in the path
134
+ matches = []
135
+ for sketch_dir in sketch_directories:
136
+ # Normalize the sketch directory path to use forward slashes
137
+ sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
138
+
139
+ # Character similarity check: at least 50% of partial name chars must be in target
140
+ target_chars = set(sketch_str_normalized)
141
+ matching_chars = partial_chars & target_chars
142
+ similarity = (
143
+ len(matching_chars) / len(partial_chars) if len(partial_chars) > 0 else 0
144
+ )
145
+
146
+ # Check if partial_name matches the directory name or any part of the path
147
+ # AND has sufficient character similarity
148
+ if partial_name_normalized in sketch_str_normalized and similarity >= 0.5:
149
+ matches.append(sketch_dir)
150
+
151
+ if len(matches) == 0:
152
+ # Check if this is a total mismatch (low character similarity with all sketches)
153
+ all_low_similarity = True
154
+ for sketch_dir in sketch_directories:
155
+ sketch_str_normalized = str(sketch_dir).replace("\\", "/").lower()
156
+ target_chars = set(sketch_str_normalized)
157
+ matching_chars = partial_chars & target_chars
158
+ similarity = (
159
+ len(matching_chars) / len(partial_chars)
160
+ if len(partial_chars) > 0
161
+ else 0
162
+ )
163
+ if similarity > 0.5:
164
+ all_low_similarity = False
165
+ break
166
+
167
+ if all_low_similarity and len(sketch_directories) > 0:
168
+ # List all available sketches
169
+ sketches_str = "\n ".join(str(s) for s in sketch_directories)
170
+ raise ValueError(
171
+ f"'{partial_name}' does not look like any of the available sketches.\n\n"
172
+ f"Available sketches:\n {sketches_str}"
173
+ )
174
+ else:
175
+ raise ValueError(f"No sketch directory found matching '{partial_name}'")
176
+ elif len(matches) == 1:
177
+ return matches[0]
178
+ else:
179
+ # Multiple matches - try to find the best match
180
+ # Best match criteria: exact match of the final directory name
181
+ exact_matches = []
182
+ for match in matches:
183
+ # Get the final directory name
184
+ final_dir_name = match.name.lower()
185
+ if final_dir_name == partial_name_normalized:
186
+ exact_matches.append(match)
187
+
188
+ if len(exact_matches) == 1:
189
+ # Found exactly one exact match - this is the best match
190
+ return exact_matches[0]
191
+ elif len(exact_matches) > 1:
192
+ # Multiple exact matches - still ambiguous
193
+ matches_str = "\n ".join(str(m) for m in exact_matches)
194
+ raise ValueError(
195
+ f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
196
+ )
197
+ else:
198
+ # No exact match - ambiguous partial matches
199
+ matches_str = "\n ".join(str(m) for m in matches)
200
+ raise ValueError(
201
+ f"Multiple sketch directories found matching '{partial_name}':\n {matches_str}"
202
+ )
fastled/version.py CHANGED
@@ -1,41 +1,41 @@
1
- from concurrent.futures import Future, ThreadPoolExecutor
2
-
3
- import httpx
4
-
5
- from fastled.__version__ import __version_url_latest__
6
-
7
-
8
- def _fetch_version() -> str | Exception:
9
- """
10
- Helper function to fetch the latest version from the GitHub repository.
11
- """
12
- try:
13
- response = httpx.get(__version_url_latest__)
14
- response.raise_for_status()
15
- # Extract the version string from the response text
16
- version_line = response.text.split("__version__ = ")[1].split('"')[1]
17
- return version_line
18
- except Exception as e:
19
- return e
20
-
21
-
22
- def get_latest_version() -> Future[str | Exception]:
23
- """
24
- Fetch the latest version from the GitHub repository.
25
- Returns a future that will resolve with the version string or an exception.
26
- """
27
- executor = ThreadPoolExecutor()
28
- return executor.submit(_fetch_version)
29
-
30
-
31
- def unit_test() -> None:
32
- future = get_latest_version()
33
- latest_version = future.result() # Wait for the future to complete
34
- if isinstance(latest_version, Exception):
35
- print(f"Error fetching latest version: {latest_version}")
36
- else:
37
- print(f"Latest version: {latest_version}")
38
-
39
-
40
- if __name__ == "__main__":
41
- unit_test()
1
+ from concurrent.futures import Future, ThreadPoolExecutor
2
+
3
+ import httpx
4
+
5
+ from fastled.__version__ import __version_url_latest__
6
+
7
+
8
+ def _fetch_version() -> str | Exception:
9
+ """
10
+ Helper function to fetch the latest version from the GitHub repository.
11
+ """
12
+ try:
13
+ response = httpx.get(__version_url_latest__)
14
+ response.raise_for_status()
15
+ # Extract the version string from the response text
16
+ version_line = response.text.split("__version__ = ")[1].split('"')[1]
17
+ return version_line
18
+ except Exception as e:
19
+ return e
20
+
21
+
22
+ def get_latest_version() -> Future[str | Exception]:
23
+ """
24
+ Fetch the latest version from the GitHub repository.
25
+ Returns a future that will resolve with the version string or an exception.
26
+ """
27
+ executor = ThreadPoolExecutor()
28
+ return executor.submit(_fetch_version)
29
+
30
+
31
+ def unit_test() -> None:
32
+ future = get_latest_version()
33
+ latest_version = future.result() # Wait for the future to complete
34
+ if isinstance(latest_version, Exception):
35
+ print(f"Error fetching latest version: {latest_version}")
36
+ else:
37
+ print(f"Latest version: {latest_version}")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ unit_test()
fastled/web_compile.py CHANGED
@@ -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(