fastled 1.4.42__py3-none-any.whl → 1.4.44__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.42"
4
+ __version__ = "1.4.44"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
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"
fastled/project_init.py CHANGED
@@ -22,20 +22,27 @@ def get_examples(host: str | None = None) -> list[str]:
22
22
 
23
23
 
24
24
  def _prompt_for_example() -> str:
25
+ from fastled.select_sketch_directory import _disambiguate_user_choice
26
+
25
27
  examples = get_examples()
26
- while True:
27
- print("Available examples:")
28
- for i, example in enumerate(examples):
29
- print(f" [{i+1}]: {example}")
30
- answer = input("Enter the example number or name: ").strip()
31
- if answer.isdigit():
32
- example_num = int(answer) - 1
33
- if example_num < 0 or example_num >= len(examples):
34
- print("Invalid example number")
35
- continue
36
- return examples[example_num]
37
- elif answer in examples:
38
- return answer
28
+
29
+ # Find default example index (prefer DEFAULT_EXAMPLE if it exists)
30
+ default_index = 0
31
+ if DEFAULT_EXAMPLE in examples:
32
+ default_index = examples.index(DEFAULT_EXAMPLE)
33
+
34
+ result = _disambiguate_user_choice(
35
+ examples,
36
+ option_to_str=lambda x: x,
37
+ prompt="Available examples:",
38
+ default_index=default_index,
39
+ )
40
+
41
+ if result is None:
42
+ # Fallback to DEFAULT_EXAMPLE if user cancelled
43
+ return DEFAULT_EXAMPLE
44
+
45
+ return result
39
46
 
40
47
 
41
48
  class DownloadThread(threading.Thread):
@@ -1,8 +1,90 @@
1
1
  from pathlib import Path
2
+ from typing import Callable, TypeVar, Union
2
3
 
3
- from rapidfuzz import fuzz
4
+ T = TypeVar("T")
4
5
 
5
- from fastled.string_diff import string_diff_paths
6
+
7
+ def _disambiguate_user_choice(
8
+ options: list[T],
9
+ option_to_str: Callable[[T], str] = str,
10
+ prompt: str = "Multiple matches found. Please choose:",
11
+ default_index: int = 0,
12
+ ) -> Union[T, None]:
13
+ """
14
+ Present multiple options to the user with a default selection.
15
+
16
+ Args:
17
+ options: List of options to choose from
18
+ option_to_str: Function to convert option to display string
19
+ prompt: Prompt message to show user
20
+ default_index: Index of the default option (0-based)
21
+
22
+ Returns:
23
+ Selected option or None if cancelled
24
+ """
25
+ if not options:
26
+ return None
27
+
28
+ if len(options) == 1:
29
+ return options[0]
30
+
31
+ # Ensure default_index is valid
32
+ if default_index < 0 or default_index >= len(options):
33
+ default_index = 0
34
+
35
+ print(f"\n{prompt}")
36
+ for i, option in enumerate(options):
37
+ option_str = option_to_str(option)
38
+ if i == default_index:
39
+ print(f" [{i+1}]: [{option_str}]") # Default option shown in brackets
40
+ else:
41
+ print(f" [{i+1}]: {option_str}")
42
+
43
+ default_option_str = option_to_str(options[default_index])
44
+ user_input = input(
45
+ f"\nEnter number or name (default: [{default_option_str}]): "
46
+ ).strip()
47
+
48
+ # Handle empty input - select default
49
+ if not user_input:
50
+ return options[default_index]
51
+
52
+ # Try to parse as number
53
+ try:
54
+ index = int(user_input) - 1
55
+ if 0 <= index < len(options):
56
+ return options[index]
57
+ except ValueError:
58
+ pass
59
+
60
+ # Try to match by name (case insensitive)
61
+ user_input_lower = user_input.lower()
62
+ for option in options:
63
+ option_str = option_to_str(option).lower()
64
+ if option_str == user_input_lower:
65
+ return option
66
+
67
+ # Try partial match
68
+ matches = []
69
+ for option in options:
70
+ option_str = option_to_str(option)
71
+ if user_input_lower in option_str.lower():
72
+ matches.append(option)
73
+
74
+ if len(matches) == 1:
75
+ return matches[0]
76
+ elif len(matches) > 1:
77
+ # Recursive disambiguation with the filtered matches
78
+ return _disambiguate_user_choice(
79
+ matches,
80
+ option_to_str,
81
+ f"Multiple partial matches for '{user_input}':",
82
+ 0, # Reset default to first match
83
+ )
84
+
85
+ # No match found
86
+ print(f"No match found for '{user_input}'. Please try again.")
87
+ return _disambiguate_user_choice(options, option_to_str, prompt, default_index)
6
88
 
7
89
 
8
90
  def select_sketch_directory(
@@ -19,49 +101,21 @@ def select_sketch_directory(
19
101
  print(f"\nUsing sketch directory: {sketch_directories[0]}")
20
102
  return str(sketch_directories[0])
21
103
  elif len(sketch_directories) > 1:
22
- print("\nMultiple Directories found, choose one:")
23
- for i, sketch_dir in enumerate(sketch_directories):
24
- print(f" [{i+1}]: {sketch_dir}")
25
- which = input(
26
- "\nPlease specify a sketch directory\nYou can enter a number or type a fuzzy search: "
27
- ).strip()
28
- try:
29
- index = int(which) - 1
30
- return str(sketch_directories[index])
31
- except (ValueError, IndexError):
32
- inputs = [p for p in sketch_directories]
33
-
34
- if is_followup:
35
- # On follow-up, find the closest match by fuzzy distance
36
- distances = []
37
- for path in inputs:
38
- path_str = str(path).replace("\\", "/")
39
- dist = fuzz.token_sort_ratio(which.lower(), path_str.lower())
40
- distances.append((dist, path))
41
-
42
- # Get the best distance and return the closest match(es)
43
- best_distance = max(distances, key=lambda x: x[0])[0]
44
- best_matches = [
45
- path for dist, path in distances if dist == best_distance
46
- ]
47
-
48
- if len(best_matches) == 1:
49
- example = best_matches[0]
50
- return str(example)
51
- else:
52
- # If still multiple matches with same distance, recurse again
53
- return select_sketch_directory(
54
- best_matches, cwd_is_fastled, is_followup=True
55
- )
56
- else:
57
- # First call - use original fuzzy matching (allows ambiguity)
58
- top_hits: list[tuple[float, Path]] = string_diff_paths(which, inputs)
59
- if len(top_hits) == 1:
60
- example = top_hits[0][1]
61
- return str(example)
62
- else:
63
- # Recursive call with is_followup=True for more precise matching
64
- return select_sketch_directory(
65
- [p for _, p in top_hits], cwd_is_fastled, is_followup=True
66
- )
104
+ if is_followup:
105
+ # Only disambiguate on follow-up calls
106
+ result = _disambiguate_user_choice(
107
+ sketch_directories,
108
+ option_to_str=lambda x: str(x),
109
+ prompt="Multiple Directories found, choose one:",
110
+ default_index=0,
111
+ )
112
+
113
+ if result is None:
114
+ return None
115
+
116
+ return str(result)
117
+ else:
118
+ # On first call, use the first directory automatically
119
+ print(f"\nUsing sketch directory: {sketch_directories[0]}")
120
+ return str(sketch_directories[0])
67
121
  return None
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()