fastled 1.4.18__py3-none-any.whl → 1.4.20__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 +1 -1
- fastled/args.py +5 -0
- fastled/client_server.py +45 -10
- fastled/filewatcher.py +3 -0
- fastled/parse_args.py +7 -0
- fastled/playwright/playwright_browser.py +5 -0
- fastled/print_filter.py +52 -52
- fastled/string_diff.py +165 -165
- fastled/version.py +41 -41
- fastled/web_compile.py +1 -1
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/METADATA +531 -531
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/RECORD +16 -16
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/WHEEL +0 -0
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/entry_points.txt +0 -0
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.4.18.dist-info → fastled-1.4.20.dist-info}/top_level.txt +0 -0
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.
|
4
|
+
__version__ = "1.4.20"
|
5
5
|
|
6
6
|
__version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
|
fastled/args.py
CHANGED
@@ -16,6 +16,7 @@ class Args:
|
|
16
16
|
app: bool # New flag to trigger Playwright browser with browser download if needed
|
17
17
|
auto_update: bool | None
|
18
18
|
update: bool
|
19
|
+
background_update: bool
|
19
20
|
localhost: bool
|
20
21
|
build: bool
|
21
22
|
server: bool
|
@@ -57,6 +58,9 @@ class Args:
|
|
57
58
|
args.no_auto_updates, bool | None
|
58
59
|
), f"expected bool | None, got {type(args.no_auto_updates)}"
|
59
60
|
assert isinstance(args.update, bool), f"expected bool, got {type(args.update)}"
|
61
|
+
assert isinstance(
|
62
|
+
args.background_update, bool
|
63
|
+
), f"expected bool, got {type(args.background_update)}"
|
60
64
|
assert isinstance(
|
61
65
|
args.localhost, bool
|
62
66
|
), f"expected bool, got {type(args.localhost)}"
|
@@ -88,6 +92,7 @@ class Args:
|
|
88
92
|
app=args.app,
|
89
93
|
auto_update=not args.no_auto_updates,
|
90
94
|
update=args.update,
|
95
|
+
background_update=args.background_update,
|
91
96
|
localhost=args.localhost,
|
92
97
|
build=args.build,
|
93
98
|
server=args.server,
|
fastled/client_server.py
CHANGED
@@ -243,14 +243,35 @@ def _try_make_compile_server(
|
|
243
243
|
return None
|
244
244
|
|
245
245
|
|
246
|
-
def
|
246
|
+
def _background_update_docker_image() -> None:
|
247
|
+
"""Perform docker image update in the background."""
|
248
|
+
try:
|
249
|
+
print("\n🔄 Starting background update of docker image...")
|
250
|
+
docker_manager = DockerManager()
|
251
|
+
updated = docker_manager.validate_or_download_image(
|
252
|
+
image_name=IMAGE_NAME, tag="latest", upgrade=True
|
253
|
+
)
|
254
|
+
if updated:
|
255
|
+
print("✅ Background docker image update completed successfully.")
|
256
|
+
else:
|
257
|
+
print("ℹ️ Docker image was already up to date.")
|
258
|
+
except KeyboardInterrupt:
|
259
|
+
print("⚠️ Background docker image update interrupted by user.")
|
260
|
+
import _thread
|
261
|
+
|
262
|
+
_thread.interrupt_main()
|
263
|
+
except Exception as e:
|
264
|
+
print(f"⚠️ Background docker image update failed: {e}")
|
265
|
+
|
266
|
+
|
267
|
+
def _is_local_host(url: str) -> bool:
|
247
268
|
return (
|
248
|
-
|
249
|
-
or
|
250
|
-
or
|
251
|
-
or
|
252
|
-
or
|
253
|
-
or
|
269
|
+
url.startswith("http://localhost")
|
270
|
+
or url.startswith("http://127.0.0.1")
|
271
|
+
or url.startswith("http://0.0.0.0")
|
272
|
+
or url.startswith("http://[::]")
|
273
|
+
or url.startswith("http://[::1]")
|
274
|
+
or url.startswith("http://[::ffff:127.0.0.1]")
|
254
275
|
)
|
255
276
|
|
256
277
|
|
@@ -268,6 +289,7 @@ def run_client(
|
|
268
289
|
clear: bool = False,
|
269
290
|
no_platformio: bool = False,
|
270
291
|
app: bool = False, # Use app-like browser experience
|
292
|
+
background_update: bool = False,
|
271
293
|
) -> int:
|
272
294
|
has_checked_newer_version_yet = False
|
273
295
|
compile_server: CompileServer | None = None
|
@@ -426,9 +448,20 @@ def run_client(
|
|
426
448
|
)
|
427
449
|
if has_update:
|
428
450
|
print(f"\n🔄 {message}")
|
429
|
-
|
430
|
-
|
431
|
-
|
451
|
+
if background_update:
|
452
|
+
# Start background update in a separate thread
|
453
|
+
update_thread = threading.Thread(
|
454
|
+
target=_background_update_docker_image, daemon=True
|
455
|
+
)
|
456
|
+
update_thread.start()
|
457
|
+
background_update = False
|
458
|
+
else:
|
459
|
+
print(
|
460
|
+
"Run with `fastled -u` to update the docker image to the latest version."
|
461
|
+
)
|
462
|
+
print(
|
463
|
+
"Or use `--background-update` to update automatically in the background after compilation."
|
464
|
+
)
|
432
465
|
except Exception as e:
|
433
466
|
# Don't let Docker check failures interrupt the main flow
|
434
467
|
warnings.warn(f"Failed to check for Docker image updates: {e}")
|
@@ -513,6 +546,7 @@ def run_client_server(args: Args) -> int:
|
|
513
546
|
profile = bool(args.profile)
|
514
547
|
web: str | bool = args.web if isinstance(args.web, str) else bool(args.web)
|
515
548
|
auto_update = bool(args.auto_update)
|
549
|
+
background_update = bool(args.background_update)
|
516
550
|
localhost = bool(args.localhost)
|
517
551
|
directory = args.directory if args.directory else Path(".")
|
518
552
|
just_compile = bool(args.just_compile)
|
@@ -585,6 +619,7 @@ def run_client_server(args: Args) -> int:
|
|
585
619
|
clear=args.clear,
|
586
620
|
no_platformio=no_platformio,
|
587
621
|
app=app,
|
622
|
+
background_update=background_update,
|
588
623
|
)
|
589
624
|
except KeyboardInterrupt:
|
590
625
|
return 1
|
fastled/filewatcher.py
CHANGED
fastled/parse_args.py
CHANGED
@@ -34,6 +34,7 @@ FastLED WASM Compiler - Useful options:
|
|
34
34
|
--quick Build in quick mode (default)
|
35
35
|
--profile Enable profiling the C++ build system
|
36
36
|
--update Update the docker image for the wasm compiler
|
37
|
+
--background-update Update the docker image in the background after compilation
|
37
38
|
--purge Remove all FastLED containers and images
|
38
39
|
--version Show version information
|
39
40
|
--help Show detailed help
|
@@ -41,6 +42,7 @@ Examples:
|
|
41
42
|
fastled (will auto detect the sketch directory and prompt you)
|
42
43
|
fastled my_sketch
|
43
44
|
fastled my_sketch --web (compiles using the web compiler only)
|
45
|
+
fastled my_sketch --background-update (compiles and updates docker image in background)
|
44
46
|
fastled --init Blink (initializes a new sketch directory with the Blink example)
|
45
47
|
fastled --server (runs the compiler server in the current directory)
|
46
48
|
|
@@ -132,6 +134,11 @@ def parse_args() -> Args:
|
|
132
134
|
action="store_true",
|
133
135
|
help="Update the wasm compiler (if necessary) before running",
|
134
136
|
)
|
137
|
+
parser.add_argument(
|
138
|
+
"--background-update",
|
139
|
+
action="store_true",
|
140
|
+
help="Update the docker image in the background after compilation (user doesn't have to wait)",
|
141
|
+
)
|
135
142
|
parser.add_argument(
|
136
143
|
"--localhost",
|
137
144
|
"--local",
|
@@ -562,6 +562,11 @@ class PlaywrightBrowserProxy:
|
|
562
562
|
# Force exit the entire program
|
563
563
|
os._exit(0)
|
564
564
|
|
565
|
+
except KeyboardInterrupt:
|
566
|
+
print("[MAIN] Browser monitor interrupted by user")
|
567
|
+
import _thread
|
568
|
+
|
569
|
+
_thread.interrupt_main()
|
565
570
|
except Exception as e:
|
566
571
|
print(f"[MAIN] Error monitoring browser process: {e}")
|
567
572
|
|
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/string_diff.py
CHANGED
@@ -1,165 +1,165 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
|
3
|
-
from rapidfuzz import fuzz
|
4
|
-
|
5
|
-
|
6
|
-
def _filter_out_obvious_bad_choices(
|
7
|
-
input_str: str, string_list: list[str]
|
8
|
-
) -> list[str]:
|
9
|
-
"""
|
10
|
-
Filter out strings that are too different from the input string.
|
11
|
-
This is a heuristic and may not be perfect.
|
12
|
-
"""
|
13
|
-
if not input_str.strip(): # Handle empty input
|
14
|
-
return string_list
|
15
|
-
|
16
|
-
input_chars = set(input_str.lower())
|
17
|
-
filtered_list = []
|
18
|
-
for s in string_list:
|
19
|
-
# Check if at least half of the input characters are in the string
|
20
|
-
s_chars = set(s.lower())
|
21
|
-
common_chars = input_chars.intersection(s_chars)
|
22
|
-
if len(common_chars) >= len(input_chars) / 2:
|
23
|
-
filtered_list.append(s)
|
24
|
-
return filtered_list
|
25
|
-
|
26
|
-
|
27
|
-
def is_in_order_match(input_str: str, other: str) -> bool:
|
28
|
-
"""
|
29
|
-
Check if the input string is an in-order match for any string in the list.
|
30
|
-
An in-order match means that the characters of the input string appear
|
31
|
-
in the same order in the string from the list, ignoring spaces in the input.
|
32
|
-
"""
|
33
|
-
|
34
|
-
# Remove spaces from input string for matching
|
35
|
-
input_chars = [c.lower() for c in input_str if c != " "]
|
36
|
-
other_chars = [c.lower() for c in other]
|
37
|
-
input_index = 0
|
38
|
-
other_index = 0
|
39
|
-
while input_index < len(input_chars) and other_index < len(other_chars):
|
40
|
-
if input_chars[input_index] == other_chars[other_index]:
|
41
|
-
input_index += 1
|
42
|
-
other_index += 1
|
43
|
-
# If we reached the end of the input string, it means all characters were found in order
|
44
|
-
if input_index == len(input_chars):
|
45
|
-
return True
|
46
|
-
return False
|
47
|
-
|
48
|
-
|
49
|
-
# Returns the min distance strings. If there is a tie, it returns
|
50
|
-
# all the strings that have the same min distance.
|
51
|
-
# Returns a tuple of index and string.
|
52
|
-
def string_diff(
|
53
|
-
input_string: str, string_list: list[str], ignore_case=True
|
54
|
-
) -> list[tuple[float, str]]:
|
55
|
-
|
56
|
-
def normalize(s: str) -> str:
|
57
|
-
return s.lower() if ignore_case else s
|
58
|
-
|
59
|
-
# Handle empty input or empty list
|
60
|
-
if not input_string.strip():
|
61
|
-
# Return all strings with equal distance for empty input
|
62
|
-
return [(i, s) for i, s in enumerate(string_list)]
|
63
|
-
|
64
|
-
if not string_list:
|
65
|
-
return []
|
66
|
-
|
67
|
-
map_string: dict[str, str] = {}
|
68
|
-
|
69
|
-
if ignore_case:
|
70
|
-
map_string = {s.lower(): s for s in string_list}
|
71
|
-
else:
|
72
|
-
map_string = {s: s for s in string_list}
|
73
|
-
|
74
|
-
original_string_list = string_list.copy()
|
75
|
-
if ignore_case:
|
76
|
-
string_list = [s.lower() for s in string_list]
|
77
|
-
input_string = input_string.lower()
|
78
|
-
|
79
|
-
# Check for exact matches, but also check if there are other substring matches
|
80
|
-
exact_matches = [s for s in string_list if s == input_string]
|
81
|
-
substring_matches = [s for s in string_list if input_string in s]
|
82
|
-
|
83
|
-
# If there's an exact match AND other substring matches, return all substring matches
|
84
|
-
# This provides better user experience for partial matching
|
85
|
-
if exact_matches and len(substring_matches) > 1:
|
86
|
-
out: list[tuple[float, str]] = []
|
87
|
-
for i, s in enumerate(substring_matches):
|
88
|
-
s_mapped = map_string.get(s, s)
|
89
|
-
out.append((i, s_mapped))
|
90
|
-
return out
|
91
|
-
|
92
|
-
# If there's only an exact match and no other substring matches, return just the exact match
|
93
|
-
if exact_matches and len(substring_matches) == 1:
|
94
|
-
out: list[tuple[float, str]] = []
|
95
|
-
for i, s in enumerate(exact_matches):
|
96
|
-
s_mapped = map_string.get(s, s)
|
97
|
-
out.append((i, s_mapped))
|
98
|
-
return out
|
99
|
-
|
100
|
-
# Apply set membership filtering for queries with 3+ characters
|
101
|
-
if len(input_string.strip()) >= 3:
|
102
|
-
filtered = _filter_out_obvious_bad_choices(input_string, string_list)
|
103
|
-
if filtered: # Only apply filter if it doesn't eliminate everything
|
104
|
-
string_list = filtered
|
105
|
-
|
106
|
-
# Second filter: exact substring filtering if applicable
|
107
|
-
if substring_matches:
|
108
|
-
string_list = substring_matches
|
109
|
-
# Return all substring matches
|
110
|
-
out: list[tuple[float, str]] = []
|
111
|
-
for i, s in enumerate(string_list):
|
112
|
-
s_mapped = map_string.get(s, s)
|
113
|
-
out.append((i, s_mapped))
|
114
|
-
return out
|
115
|
-
|
116
|
-
# Third filter: in order exact match filtering if applicable.
|
117
|
-
in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
|
118
|
-
if in_order_matches:
|
119
|
-
string_list = in_order_matches
|
120
|
-
|
121
|
-
# Calculate distances
|
122
|
-
distances: list[float] = []
|
123
|
-
for s in string_list:
|
124
|
-
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
125
|
-
distances.append(1.0 / (dist + 1.0))
|
126
|
-
|
127
|
-
# Handle case where no strings remain after filtering
|
128
|
-
if not distances:
|
129
|
-
# Fall back to original list and calculate distances
|
130
|
-
string_list = original_string_list.copy()
|
131
|
-
if ignore_case:
|
132
|
-
string_list = [s.lower() for s in string_list]
|
133
|
-
|
134
|
-
distances = []
|
135
|
-
for s in string_list:
|
136
|
-
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
137
|
-
distances.append(1.0 / (dist + 1.0))
|
138
|
-
|
139
|
-
min_distance = min(distances)
|
140
|
-
out: list[tuple[float, str]] = []
|
141
|
-
for i, d in enumerate(distances):
|
142
|
-
if d == min_distance:
|
143
|
-
s = string_list[i]
|
144
|
-
s_mapped = map_string.get(s, s)
|
145
|
-
out.append((i, s_mapped))
|
146
|
-
|
147
|
-
return out
|
148
|
-
|
149
|
-
|
150
|
-
def string_diff_paths(
|
151
|
-
input_string: str | Path, path_list: list[Path], ignore_case=True
|
152
|
-
) -> list[tuple[float, Path]]:
|
153
|
-
# Normalize path separators to forward slashes for consistent comparison
|
154
|
-
string_list = [str(p).replace("\\", "/") for p in path_list]
|
155
|
-
input_str = str(input_string).replace("\\", "/")
|
156
|
-
|
157
|
-
tmp = string_diff(input_str, string_list, ignore_case)
|
158
|
-
out: list[tuple[float, Path]] = []
|
159
|
-
for i, j in tmp:
|
160
|
-
# Find the original path that matches the normalized result
|
161
|
-
for idx, orig_path in enumerate(path_list):
|
162
|
-
if str(orig_path).replace("\\", "/") == j:
|
163
|
-
out.append((i, orig_path))
|
164
|
-
break
|
165
|
-
return out
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from rapidfuzz import fuzz
|
4
|
+
|
5
|
+
|
6
|
+
def _filter_out_obvious_bad_choices(
|
7
|
+
input_str: str, string_list: list[str]
|
8
|
+
) -> list[str]:
|
9
|
+
"""
|
10
|
+
Filter out strings that are too different from the input string.
|
11
|
+
This is a heuristic and may not be perfect.
|
12
|
+
"""
|
13
|
+
if not input_str.strip(): # Handle empty input
|
14
|
+
return string_list
|
15
|
+
|
16
|
+
input_chars = set(input_str.lower())
|
17
|
+
filtered_list = []
|
18
|
+
for s in string_list:
|
19
|
+
# Check if at least half of the input characters are in the string
|
20
|
+
s_chars = set(s.lower())
|
21
|
+
common_chars = input_chars.intersection(s_chars)
|
22
|
+
if len(common_chars) >= len(input_chars) / 2:
|
23
|
+
filtered_list.append(s)
|
24
|
+
return filtered_list
|
25
|
+
|
26
|
+
|
27
|
+
def is_in_order_match(input_str: str, other: str) -> bool:
|
28
|
+
"""
|
29
|
+
Check if the input string is an in-order match for any string in the list.
|
30
|
+
An in-order match means that the characters of the input string appear
|
31
|
+
in the same order in the string from the list, ignoring spaces in the input.
|
32
|
+
"""
|
33
|
+
|
34
|
+
# Remove spaces from input string for matching
|
35
|
+
input_chars = [c.lower() for c in input_str if c != " "]
|
36
|
+
other_chars = [c.lower() for c in other]
|
37
|
+
input_index = 0
|
38
|
+
other_index = 0
|
39
|
+
while input_index < len(input_chars) and other_index < len(other_chars):
|
40
|
+
if input_chars[input_index] == other_chars[other_index]:
|
41
|
+
input_index += 1
|
42
|
+
other_index += 1
|
43
|
+
# If we reached the end of the input string, it means all characters were found in order
|
44
|
+
if input_index == len(input_chars):
|
45
|
+
return True
|
46
|
+
return False
|
47
|
+
|
48
|
+
|
49
|
+
# Returns the min distance strings. If there is a tie, it returns
|
50
|
+
# all the strings that have the same min distance.
|
51
|
+
# Returns a tuple of index and string.
|
52
|
+
def string_diff(
|
53
|
+
input_string: str, string_list: list[str], ignore_case=True
|
54
|
+
) -> list[tuple[float, str]]:
|
55
|
+
|
56
|
+
def normalize(s: str) -> str:
|
57
|
+
return s.lower() if ignore_case else s
|
58
|
+
|
59
|
+
# Handle empty input or empty list
|
60
|
+
if not input_string.strip():
|
61
|
+
# Return all strings with equal distance for empty input
|
62
|
+
return [(i, s) for i, s in enumerate(string_list)]
|
63
|
+
|
64
|
+
if not string_list:
|
65
|
+
return []
|
66
|
+
|
67
|
+
map_string: dict[str, str] = {}
|
68
|
+
|
69
|
+
if ignore_case:
|
70
|
+
map_string = {s.lower(): s for s in string_list}
|
71
|
+
else:
|
72
|
+
map_string = {s: s for s in string_list}
|
73
|
+
|
74
|
+
original_string_list = string_list.copy()
|
75
|
+
if ignore_case:
|
76
|
+
string_list = [s.lower() for s in string_list]
|
77
|
+
input_string = input_string.lower()
|
78
|
+
|
79
|
+
# Check for exact matches, but also check if there are other substring matches
|
80
|
+
exact_matches = [s for s in string_list if s == input_string]
|
81
|
+
substring_matches = [s for s in string_list if input_string in s]
|
82
|
+
|
83
|
+
# If there's an exact match AND other substring matches, return all substring matches
|
84
|
+
# This provides better user experience for partial matching
|
85
|
+
if exact_matches and len(substring_matches) > 1:
|
86
|
+
out: list[tuple[float, str]] = []
|
87
|
+
for i, s in enumerate(substring_matches):
|
88
|
+
s_mapped = map_string.get(s, s)
|
89
|
+
out.append((i, s_mapped))
|
90
|
+
return out
|
91
|
+
|
92
|
+
# If there's only an exact match and no other substring matches, return just the exact match
|
93
|
+
if exact_matches and len(substring_matches) == 1:
|
94
|
+
out: list[tuple[float, str]] = []
|
95
|
+
for i, s in enumerate(exact_matches):
|
96
|
+
s_mapped = map_string.get(s, s)
|
97
|
+
out.append((i, s_mapped))
|
98
|
+
return out
|
99
|
+
|
100
|
+
# Apply set membership filtering for queries with 3+ characters
|
101
|
+
if len(input_string.strip()) >= 3:
|
102
|
+
filtered = _filter_out_obvious_bad_choices(input_string, string_list)
|
103
|
+
if filtered: # Only apply filter if it doesn't eliminate everything
|
104
|
+
string_list = filtered
|
105
|
+
|
106
|
+
# Second filter: exact substring filtering if applicable
|
107
|
+
if substring_matches:
|
108
|
+
string_list = substring_matches
|
109
|
+
# Return all substring matches
|
110
|
+
out: list[tuple[float, str]] = []
|
111
|
+
for i, s in enumerate(string_list):
|
112
|
+
s_mapped = map_string.get(s, s)
|
113
|
+
out.append((i, s_mapped))
|
114
|
+
return out
|
115
|
+
|
116
|
+
# Third filter: in order exact match filtering if applicable.
|
117
|
+
in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
|
118
|
+
if in_order_matches:
|
119
|
+
string_list = in_order_matches
|
120
|
+
|
121
|
+
# Calculate distances
|
122
|
+
distances: list[float] = []
|
123
|
+
for s in string_list:
|
124
|
+
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
125
|
+
distances.append(1.0 / (dist + 1.0))
|
126
|
+
|
127
|
+
# Handle case where no strings remain after filtering
|
128
|
+
if not distances:
|
129
|
+
# Fall back to original list and calculate distances
|
130
|
+
string_list = original_string_list.copy()
|
131
|
+
if ignore_case:
|
132
|
+
string_list = [s.lower() for s in string_list]
|
133
|
+
|
134
|
+
distances = []
|
135
|
+
for s in string_list:
|
136
|
+
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
137
|
+
distances.append(1.0 / (dist + 1.0))
|
138
|
+
|
139
|
+
min_distance = min(distances)
|
140
|
+
out: list[tuple[float, str]] = []
|
141
|
+
for i, d in enumerate(distances):
|
142
|
+
if d == min_distance:
|
143
|
+
s = string_list[i]
|
144
|
+
s_mapped = map_string.get(s, s)
|
145
|
+
out.append((i, s_mapped))
|
146
|
+
|
147
|
+
return out
|
148
|
+
|
149
|
+
|
150
|
+
def string_diff_paths(
|
151
|
+
input_string: str | Path, path_list: list[Path], ignore_case=True
|
152
|
+
) -> list[tuple[float, Path]]:
|
153
|
+
# Normalize path separators to forward slashes for consistent comparison
|
154
|
+
string_list = [str(p).replace("\\", "/") for p in path_list]
|
155
|
+
input_str = str(input_string).replace("\\", "/")
|
156
|
+
|
157
|
+
tmp = string_diff(input_str, string_list, ignore_case)
|
158
|
+
out: list[tuple[float, Path]] = []
|
159
|
+
for i, j in tmp:
|
160
|
+
# Find the original path that matches the normalized result
|
161
|
+
for idx, orig_path in enumerate(path_list):
|
162
|
+
if str(orig_path).replace("\\", "/") == j:
|
163
|
+
out.append((i, orig_path))
|
164
|
+
break
|
165
|
+
return out
|