-
Notifications
You must be signed in to change notification settings - Fork 599
Open
Description
This is continuation of work in #5468, #5556.
Tree-shaking of icon is needed for mega-sets like Material Symbols.
Without tree-shaking that set adds around 30 MB to an application bundle!
The idea:
- During
flet buildscan Python program codebase to find all occurrences of icon values of enums inherited fromflet.IconData, for exampleft.Icons.ABCorft.CupertinoIcons.ALARM. - In
flet buildtemplate re-writegetIconDatamethod on Dart side which returnsIconDatafrom a hard-coded list of icons found on a previous step. For example:
switch(iconCode) {
case 0x0001: return flet.Icons.ABC;
case 0x0002: return flet.CupertinoIcons.ALARM;
default: return null;
}- As no
materialIconsorcupertinoIconslists referenced Dart compiler will do icon-shaking and leave only icons used in a switch.
Vibe-coded prototype of Icon scanner
import ast
import importlib
from pathlib import Path
from types import ModuleType
from typing import Optional, Set, Tuple
def resolve_enum_reference(
module: ModuleType, class_name: str, member_name: str
) -> Optional[object]:
try:
enum_class = getattr(module, class_name, None)
if enum_class and hasattr(enum_class, member_name):
return getattr(enum_class, member_name)
except Exception:
pass
return None
def find_icon_usages(source_code: str) -> Set[Tuple[str, str]]:
used_icons = set()
class IconVisitor(ast.NodeVisitor):
def visit_Attribute(self, node: ast.Attribute):
full_chain = []
current = node
while isinstance(current, ast.Attribute):
full_chain.append(current.attr)
current = current.value
if isinstance(current, ast.Name):
full_chain.append(current.id)
full_chain = list(reversed(full_chain))
if len(full_chain) == 3 and full_chain[0] == "ft":
enum_class = full_chain[1]
icon_name = full_chain[2]
used_icons.add((enum_class, icon_name))
self.generic_visit(node)
tree = ast.parse(source_code)
IconVisitor().visit(tree)
return used_icons
def scan_project_icons(root_dir: Path) -> Set[Tuple[str, str]]:
all_used_icons = set()
for file in root_dir.rglob("*.py"):
try:
source = file.read_text(encoding="utf-8")
found = find_icon_usages(source)
if found:
print(f"๐ {file.relative_to(root_dir)}: {found}")
all_used_icons.update(found)
except Exception as e:
print(f"โ ๏ธ Error reading {file}: {e}")
return all_used_icons
def print_resolved_icons(
used: Set[Tuple[str, str]],
enum_module: str,
icon_base_class_path: str,
):
base_mod_path, base_class_name = icon_base_class_path.rsplit(".", 1)
base_mod = importlib.import_module(base_mod_path)
IconDataBase = getattr(base_mod, base_class_name)
enum_mod = importlib.import_module(enum_module)
print("\nโ
Resolved icon enum instances with metadata:")
for class_name, member_name in sorted(used):
try:
enum_class = getattr(enum_mod, class_name)
if not issubclass(enum_class, IconDataBase):
continue # Skip non-icon enums
icon_member = getattr(enum_class, member_name, None)
if icon_member is not None:
value = hex(icon_member.value)
package = getattr(enum_class, "_package_name", "unknown")
klass = getattr(enum_class, "_class_name", "unknown")
print(
f"{class_name}.{member_name} = {value} "
f"(package={package}, class={klass})"
)
except Exception as e:
print(f"โ ๏ธ Error resolving {class_name}.{member_name}: {e}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 4:
print(
"Usage: python scan_icon_usage.py <path_to_project> <enum_module> <base_class_path>"
)
print(
"Example: python scan_icon_usage.py ./myapp flet.icons flet.icons.IconData"
)
sys.exit(1)
project_path = Path(sys.argv[1])
enum_module = sys.argv[2] # e.g. "flet.icons"
icon_base_class_path = sys.argv[3] # e.g. "flet.icons.IconData"
if not project_path.exists():
print(f"โ Path does not exist: {project_path}")
sys.exit(1)
sys.path.insert(0, str(project_path.resolve()))
used = scan_project_icons(project_path)
print_resolved_icons(used, enum_module, icon_base_class_path)Metadata
Metadata
Assignees
Labels
No labels