flet-cli 1.1.0.dev1__py3-none-any.whl → 1.1.0.dev3__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.
@@ -1,7 +1,7 @@
1
1
  import os
2
2
 
3
3
 
4
- def get_hook_dirs():
4
+ def get_hook_dirs() -> list[str]:
5
5
  """
6
6
  Return directories containing PyInstaller hook modules for `flet-cli`.
7
7
  """
@@ -1 +1,15 @@
1
1
  temp_bin_dir = None
2
+
3
+ verbose = 0
4
+ """
5
+ Number of `-v` flags `flet pack` runs with.
6
+ """
7
+
8
+
9
+ def log_progress(*args) -> None:
10
+ """
11
+ Print a progress message of `flet pack`, shown with `-v` only.
12
+ """
13
+
14
+ if verbose > 0:
15
+ print(*args)
@@ -0,0 +1,177 @@
1
+ import ast
2
+ import importlib.metadata
3
+ import re
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ _PACKAGE_URI = re.compile(rb"package:([a-z][a-z0-9_]*)/")
10
+
11
+ # Directories that hold no app code, skipped when looking for the app's imports.
12
+ _SKIPPED_DIRS = {"__pycache__", "build", "dist", "node_modules", "site-packages"}
13
+
14
+
15
+ @dataclass
16
+ class InstalledExtension:
17
+ """
18
+ A Flet extension installed in the current environment.
19
+ """
20
+
21
+ package: str
22
+ """
23
+ Name of the extension's Flutter package.
24
+ """
25
+
26
+ distribution: Optional[str]
27
+ """
28
+ Name of the distribution that installed it, if known.
29
+ """
30
+
31
+ modules: set[str]
32
+ """
33
+ Top-level Python modules the extension provides.
34
+ """
35
+
36
+
37
+ def get_client_extensions(snapshot_path: str) -> Optional[set[str]]:
38
+ """
39
+ Return the Flutter packages compiled into a prebuilt Flet client.
40
+
41
+ Release builds keep the `package:<name>/` URI of every Dart library in the
42
+ app's AOT snapshot: `data/app.so` on Windows, `lib/libapp.so` on Linux and
43
+ `Contents/Frameworks/App.framework/App` in a macOS app bundle.
44
+
45
+ Args:
46
+ snapshot_path: Path to the client's AOT snapshot.
47
+
48
+ Returns:
49
+ Names of the packages, or `None` when the snapshot is missing or names no
50
+ `flet` library, so the client's extensions can't be told.
51
+ """
52
+
53
+ path = Path(snapshot_path)
54
+ if not path.is_file():
55
+ return None
56
+ packages = {m.decode() for m in _PACKAGE_URI.findall(path.read_bytes())}
57
+ return packages if "flet" in packages else None
58
+
59
+
60
+ def find_installed_extensions() -> list[InstalledExtension]:
61
+ """
62
+ Return the Flet extensions installed in the current environment.
63
+
64
+ An extension is a distribution that installs a `flutter/<package>/pubspec.yaml`
65
+ file. Editable installs list no such file in their `RECORD`, so a
66
+ `flutter/<package>` folder on `sys.path` that no distribution owns is reported
67
+ too when the same `sys.path` entry has a Python module named `<package>`.
68
+ """
69
+
70
+ extensions: dict[str, InstalledExtension] = {}
71
+ for dist in importlib.metadata.distributions():
72
+ files = dist.files or []
73
+ packages = {
74
+ f.parts[1]
75
+ for f in files
76
+ if len(f.parts) == 3
77
+ and f.parts[0] == "flutter"
78
+ and f.parts[2] == "pubspec.yaml"
79
+ }
80
+ if not packages:
81
+ continue
82
+ modules = {
83
+ Path(f.parts[0]).stem
84
+ for f in files
85
+ if f.parts[0] not in ("flutter", "..", "__pycache__")
86
+ and not f.parts[0].endswith((".dist-info", ".data", ".pth"))
87
+ and (len(f.parts) > 1 or f.suffix == ".py")
88
+ }
89
+ for package in packages:
90
+ extensions.setdefault(
91
+ package,
92
+ InstalledExtension(package, dist.metadata["Name"], modules),
93
+ )
94
+
95
+ for entry in sys.path:
96
+ entry_dir = Path(entry or ".")
97
+ if not (entry_dir / "flutter").is_dir():
98
+ continue
99
+ for pubspec in sorted((entry_dir / "flutter").glob("*/pubspec.yaml")):
100
+ package = pubspec.parent.name
101
+ if (entry_dir / package).is_dir() or (
102
+ entry_dir / f"{package}.py"
103
+ ).is_file():
104
+ extensions.setdefault(
105
+ package, InstalledExtension(package, None, {package})
106
+ )
107
+
108
+ return sorted(extensions.values(), key=lambda e: e.package)
109
+
110
+
111
+ def find_imported_modules(app_dir: Path) -> set[str]:
112
+ """
113
+ Return the top-level modules the app's Python files import.
114
+
115
+ Virtual environments, hidden folders, build output and symlinked folders in
116
+ `app_dir` are skipped, as are files that can't be read or don't parse.
117
+
118
+ Args:
119
+ app_dir: Directory of the app's main script.
120
+ """
121
+
122
+ modules: set[str] = set()
123
+ for path in _iter_app_files(app_dir):
124
+ try:
125
+ tree = ast.parse(path.read_bytes(), filename=str(path))
126
+ except (OSError, SyntaxError, ValueError):
127
+ continue
128
+ for node in ast.walk(tree):
129
+ if isinstance(node, ast.Import):
130
+ modules.update(alias.name.split(".")[0] for alias in node.names)
131
+ elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
132
+ modules.add(node.module.split(".")[0])
133
+ return modules
134
+
135
+
136
+ def find_missing_extensions(
137
+ app_dir: Path, client_extensions: set[str]
138
+ ) -> list[InstalledExtension]:
139
+ """
140
+ Return the installed extensions the app imports but the client lacks.
141
+
142
+ Args:
143
+ app_dir: Directory of the app's main script.
144
+ client_extensions: Flutter packages compiled into the client, as
145
+ returned by `get_client_extensions()`.
146
+ """
147
+
148
+ candidates = [
149
+ e for e in find_installed_extensions() if e.package not in client_extensions
150
+ ]
151
+ if not candidates:
152
+ return []
153
+ imported = find_imported_modules(app_dir)
154
+ return [e for e in candidates if e.modules & imported]
155
+
156
+
157
+ def _iter_app_files(directory: Path):
158
+ """
159
+ Yield the Python files of an app folder, recursively.
160
+ """
161
+
162
+ try:
163
+ paths = sorted(directory.iterdir())
164
+ except OSError:
165
+ return
166
+ for path in paths:
167
+ if path.is_dir():
168
+ if (
169
+ path.is_symlink()
170
+ or path.name.startswith(".")
171
+ or path.name in _SKIPPED_DIRS
172
+ or (path / "pyvenv.cfg").exists()
173
+ ):
174
+ continue
175
+ yield from _iter_app_files(path)
176
+ elif path.suffix == ".py":
177
+ yield path
@@ -0,0 +1,3 @@
1
+ from flet.utils import is_windows
2
+
3
+ excludedimports = [] if is_windows() else ["flet_desktop.win_taskbar"]
@@ -5,14 +5,16 @@ import shutil
5
5
  import subprocess
6
6
  import tarfile
7
7
  from pathlib import Path
8
+ from typing import Any, Optional
8
9
 
9
10
  from PyInstaller.building.icon import normalize_icon_type
10
11
 
12
+ import flet_cli.__pyinstaller.config as hook_config
11
13
  from flet.utils import safe_tar_extractall
12
14
  from flet_cli.__pyinstaller.utils import normalize_tar_entry
13
15
 
14
16
 
15
- def unpack_app_bundle(tar_path):
17
+ def unpack_app_bundle(tar_path: str) -> str:
16
18
  """
17
19
  Extract a macOS app bundle archive and remove the source tarball.
18
20
 
@@ -32,7 +34,7 @@ def unpack_app_bundle(tar_path):
32
34
  return os.path.join(bin_dir, "Flet.app")
33
35
 
34
36
 
35
- def update_flet_view_icon(app_path, icon_path):
37
+ def update_flet_view_icon(app_path: str, icon_path: str) -> None:
36
38
  """
37
39
  Replace app icon in a macOS app bundle and update Info.plist metadata.
38
40
 
@@ -41,7 +43,7 @@ def update_flet_view_icon(app_path, icon_path):
41
43
  icon_path: Path to source icon file.
42
44
  """
43
45
 
44
- print("Updating Flet View icon", app_path, icon_path)
46
+ hook_config.log_progress("Updating Flet View icon", app_path, icon_path)
45
47
 
46
48
  icon_file = "AppIcon.icns"
47
49
 
@@ -51,7 +53,7 @@ def update_flet_view_icon(app_path, icon_path):
51
53
  )
52
54
 
53
55
  # patch icon
54
- print("Copying icons from", normalized_icon_path)
56
+ hook_config.log_progress("Copying icons from", normalized_icon_path)
55
57
  shutil.copy(
56
58
  normalized_icon_path,
57
59
  os.path.join(app_path, "Contents", "Resources", icon_file),
@@ -65,12 +67,12 @@ def update_flet_view_icon(app_path, icon_path):
65
67
 
66
68
 
67
69
  def update_flet_view_version_info(
68
- app_path,
69
- bundle_id,
70
- product_name,
71
- product_version,
72
- copyright,
73
- ):
70
+ app_path: str,
71
+ bundle_id: Optional[str],
72
+ product_name: Optional[str],
73
+ product_version: Optional[str],
74
+ copyright: Optional[str],
75
+ ) -> str:
74
76
  """
75
77
  Update selected Info.plist metadata fields in a macOS app bundle.
76
78
 
@@ -85,7 +87,7 @@ def update_flet_view_version_info(
85
87
  Final app bundle path (may change when app bundle is renamed).
86
88
  """
87
89
 
88
- print("Updating Flet View plist", app_path)
90
+ hook_config.log_progress("Updating Flet View plist", app_path)
89
91
 
90
92
  pl = __load_info_plist(app_path)
91
93
 
@@ -109,7 +111,7 @@ def update_flet_view_version_info(
109
111
  return app_path
110
112
 
111
113
 
112
- def assemble_app_bundle(app_path, tar_path):
114
+ def assemble_app_bundle(app_path: str, tar_path: str) -> None:
113
115
  """
114
116
  Code-sign a macOS app bundle, package it as tar.gz, and remove unpacked bundle.
115
117
 
@@ -122,7 +124,7 @@ def assemble_app_bundle(app_path, tar_path):
122
124
  """
123
125
 
124
126
  # sign app bundle
125
- print(f"Signing file {app_path}")
127
+ hook_config.log_progress(f"Signing file {app_path}")
126
128
  cmd_args = [
127
129
  "codesign",
128
130
  "-s",
@@ -162,7 +164,7 @@ def assemble_app_bundle(app_path, tar_path):
162
164
  shutil.rmtree(app_path, ignore_errors=True)
163
165
 
164
166
 
165
- def __load_info_plist(app_path):
167
+ def __load_info_plist(app_path: str) -> dict[str, Any]:
166
168
  """
167
169
  Load Info.plist content from a macOS app bundle.
168
170
  """
@@ -171,7 +173,7 @@ def __load_info_plist(app_path):
171
173
  return plistlib.load(fp)
172
174
 
173
175
 
174
- def __save_info_plist(app_path, pl):
176
+ def __save_info_plist(app_path: str, pl: dict[str, Any]) -> None:
175
177
  """
176
178
  Save Info.plist content to a macOS app bundle.
177
179
 
@@ -184,7 +186,7 @@ def __save_info_plist(app_path, pl):
184
186
  plistlib.dump(pl, fp)
185
187
 
186
188
 
187
- def __get_plist_path(app_path):
189
+ def __get_plist_path(app_path: str) -> str:
188
190
  """
189
191
  Return path to `Info.plist` inside a macOS app bundle.
190
192
  """
@@ -3,12 +3,13 @@ import tarfile
3
3
  import tempfile
4
4
  import uuid
5
5
  from pathlib import Path
6
+ from typing import Optional
6
7
 
7
8
  from flet.utils import copy_tree
8
9
  from flet_desktop import ensure_client_cached
9
10
 
10
11
 
11
- def get_flet_bin_path():
12
+ def get_flet_bin_path() -> Optional[str]:
12
13
  """
13
14
  Return path to Flet desktop binaries, if available.
14
15
 
@@ -52,7 +53,7 @@ def normalize_tar_entry(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
52
53
  return tarinfo
53
54
 
54
55
 
55
- def copy_flet_bin():
56
+ def copy_flet_bin() -> Optional[str]:
56
57
  """
57
58
  Copy packaged Flet desktop binaries into a temporary directory.
58
59
 
@@ -1,26 +1,34 @@
1
1
  import os
2
+ import shutil
2
3
  import tempfile
3
4
  import uuid
4
5
  from pathlib import Path
6
+ from typing import Optional
5
7
 
6
8
  import pefile
7
9
  from packaging import version
8
- from PyInstaller.building.icon import normalize_icon_type
9
10
  from PyInstaller.compat import win32api
10
11
  from PyInstaller.utils.win32 import versioninfo
11
12
  from PyInstaller.utils.win32.icon import IconFile, normalize_icon_type
12
13
 
14
+ import flet_cli.__pyinstaller.config as hook_config
13
15
 
14
- def update_flet_view_icon(exe_path, icon_path):
16
+
17
+ def update_flet_view_icon(exe_path: str, icon_path: str) -> None:
15
18
  """
16
19
  Replace icon resources in a Windows executable.
17
20
 
21
+ When the icon is an `.ico` file, or an image converted to one, it is also
22
+ written to `data/app_icon.ico` next to the executable: Windows shows the icon
23
+ of an app without package identity on its notifications from an image file,
24
+ not from the executable's resources.
25
+
18
26
  Args:
19
27
  exe_path: Path to target executable file.
20
28
  icon_path: Path to source icon file or executable resource.
21
29
  """
22
30
 
23
- print("Updating Flet View icon", exe_path, icon_path)
31
+ hook_config.log_progress("Updating Flet View icon", exe_path, icon_path)
24
32
 
25
33
  RT_ICON = 3
26
34
  RT_GROUP_ICON = 14
@@ -29,7 +37,7 @@ def update_flet_view_icon(exe_path, icon_path):
29
37
  icon_path, ("exe", "ico"), "ico", os.getcwd()
30
38
  )
31
39
  icon = IconFile(normalized_icon_path)
32
- print("Copying icons from", normalized_icon_path)
40
+ hook_config.log_progress("Copying icons from", normalized_icon_path)
33
41
 
34
42
  hdst = win32api.BeginUpdateResource(exe_path, 0)
35
43
 
@@ -40,23 +48,34 @@ def update_flet_view_icon(exe_path, icon_path):
40
48
  data = icon.grp_icon_dir()
41
49
  data = data + icon.grp_icondir_entries(iconid)
42
50
  win32api.UpdateResource(hdst, RT_GROUP_ICON, i, data, 1033)
43
- print("Writing RT_GROUP_ICON %d resource with %d bytes", i, len(data))
51
+ hook_config.log_progress(
52
+ f"Writing RT_GROUP_ICON {i} resource with {len(data)} bytes"
53
+ )
44
54
  for data in icon.images:
45
55
  win32api.UpdateResource(hdst, RT_ICON, iconid, data, 1033)
46
- print("Writing RT_ICON %d resource with %d bytes", iconid, len(data))
56
+ hook_config.log_progress(
57
+ f"Writing RT_ICON {iconid} resource with {len(data)} bytes"
58
+ )
47
59
  iconid = iconid + 1
48
60
 
49
61
  win32api.EndUpdateResource(hdst, 0)
50
62
 
63
+ # Copy the icon only when normalize_icon_type() produced an .ico file.
64
+ if Path(normalized_icon_path).suffix.lower() == ".ico":
65
+ icon_file = Path(exe_path).parent / "data" / "app_icon.ico"
66
+ icon_file.parent.mkdir(parents=True, exist_ok=True)
67
+ shutil.copyfile(normalized_icon_path, icon_file)
68
+ hook_config.log_progress("Writing", icon_file)
69
+
51
70
 
52
71
  def update_flet_view_version_info(
53
- exe_path,
54
- product_name,
55
- file_description,
56
- product_version,
57
- file_version,
58
- company_name,
59
- copyright,
72
+ exe_path: str,
73
+ product_name: Optional[str],
74
+ file_description: Optional[str],
75
+ product_version: Optional[str],
76
+ file_version: Optional[str],
77
+ company_name: Optional[str],
78
+ copyright: Optional[str],
60
79
  ) -> str:
61
80
  """
62
81
  Update VERSIONINFO resources in a Windows executable.
@@ -74,7 +93,7 @@ def update_flet_view_version_info(
74
93
  Path to a temporary text file containing serialized version info.
75
94
  """
76
95
 
77
- print("Updating Flet View version info", exe_path)
96
+ hook_config.log_progress("Updating Flet View version info", exe_path)
78
97
 
79
98
  # load versioninfo from exe
80
99
  if versioninfo.read_version_info_from_executable:
@@ -109,6 +109,7 @@ class Command(BaseBuildCommand):
109
109
  self.update_flutter_dependencies()
110
110
  self.customize_icons()
111
111
  self.customize_splash_images()
112
+ self.copy_android_resources()
112
113
  self.run_flutter()
113
114
  self.copy_build_output()
114
115
  if self.target_platform == "macos":
@@ -178,14 +179,14 @@ class Command(BaseBuildCommand):
178
179
 
179
180
  if (
180
181
  self.target_platform in ("apk", "aab")
181
- and self.template_data["options"]["target_arch"]
182
+ and self.template_data["_options"]["target_arch"]
182
183
  ):
183
184
  args.extend(
184
185
  [
185
186
  "--target-platform",
186
187
  ",".join(
187
188
  flutter_target_platforms(
188
- self.template_data["options"]["target_arch"]
189
+ self.template_data["_options"]["target_arch"]
189
190
  )
190
191
  ),
191
192
  ]
@@ -575,7 +576,7 @@ class Command(BaseBuildCommand):
575
576
  # The authoritative check reads the built app's Info.plist; this
576
577
  # one catches the common case — the key not configured at all —
577
578
  # before the build.
578
- if not self.template_data["options"]["info_plist"].get(
579
+ if not self.template_data["_options"]["info_plist"].get(
579
580
  "LSApplicationCategoryType"
580
581
  ):
581
582
  self.cleanup(