diff --git a/.github/scripts/common.sh b/.github/scripts/common.sh index 108f08da7..4cf19a9bd 100644 --- a/.github/scripts/common.sh +++ b/.github/scripts/common.sh @@ -1,4 +1,5 @@ -function patch_python_package_versions() { +function patch_python_package_versions() ( + set -euo pipefail cd "$SDK_PYTHON" || exit 1 # Install dependencies @@ -6,11 +7,16 @@ function patch_python_package_versions() { # Update package versions in version.py and pyproject.toml files for pkg in flet flet-cli flet-desktop flet-web; do - sed -i -e "s/version = \"\"/version = \"$PYPI_VER\"/g" packages/$pkg/src/${pkg//-/_}/version.py + sed -i -e "s/flet_version = \"\"/flet_version = \"$PYPI_VER\"/g" packages/$pkg/src/${pkg//-/_}/version.py uv version --package "$pkg" "$PYPI_VER" echo "Patched version for $pkg to $PYPI_VER" done -} + + # Get Flutter version from .fvmrc and set it in version.py + FLUTTER_VERSION="$( uv run "$SCRIPTS/read_fvmrc.py" "${ROOT}/.fvmrc" )" + sed -i -e "s/flutter_version = \"\"/flutter_version = \"$FLUTTER_VERSION\"/g" packages/flet/src/flet/version.py + echo "Patched Flutter SDK version to $FLUTTER_VERSION" +) update_flet_wheel_deps() { diff --git a/.github/scripts/read_fvmrc.py b/.github/scripts/read_fvmrc.py new file mode 100644 index 000000000..75d8421c2 --- /dev/null +++ b/.github/scripts/read_fvmrc.py @@ -0,0 +1,25 @@ +""" +Reads the Flutter version from a .fvmrc file and prints it. + +Usage: + uv run read_fvmrc.py +""" + +import json +import sys + + +def main() -> None: + try: + with open(sys.argv[1], encoding="utf-8") as f: + v = json.load(f)["flutter"].strip() + if not v: + raise ValueError("Empty or missing 'flutter' value") + print(v) + except Exception as e: + print(f"Error parsing {sys.argv[1]!r}: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70b0b6474..d4156271b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,7 @@ env: ROOT: "${{ github.workspace }}" SDK_PYTHON: "${{ github.workspace }}/sdk/python" SCRIPTS: "${{ github.workspace }}/.github/scripts" - UV_PYTHON: "3.12" - PYODIDE_VERSION: "0.27.7" + UV_PYTHON: "3.12.7" jobs: # ============ @@ -159,10 +158,11 @@ jobs: - name: Prepare env and patch versions shell: bash + working-directory: ${{ env.SDK_PYTHON }} run: | source "$SCRIPTS/common.sh" patch_python_package_versions - patch_toml_versions "${SDK_PYTHON}/packages/flet-desktop/pyproject.toml" "$PYPI_VER" + patch_toml_versions "packages/flet-desktop/pyproject.toml" "$PYPI_VER" - name: Build Flutter Windows client env: @@ -497,6 +497,21 @@ jobs: path: '.fvmrc' cache: true + - name: Patch Python and Flutter versions + shell: bash + working-directory: ${{ env.SDK_PYTHON }} + run: | + source "$SCRIPTS/common.sh" + patch_python_package_versions + + - name: Get Pyodide version + shell: bash + working-directory: ${{ env.SDK_PYTHON }} + run: | + PYODIDE_VERSION="$( uv run python -c 'import flet.version; print(flet.version.pyodide_version)' )" + echo "PYODIDE_VERSION=$PYODIDE_VERSION" >> "$GITHUB_ENV" + echo "Pyodide version: $PYODIDE_VERSION" + - name: Build Web client shell: bash working-directory: client @@ -530,8 +545,6 @@ jobs: shell: bash working-directory: ${{ env.SDK_PYTHON }} run: | - source "$SCRIPTS/common.sh" - patch_python_package_versions uv build --package flet-web --wheel rm -rf "packages/flet-web/src/flet_web/web" @@ -639,6 +652,7 @@ jobs: - name: Build Python packages shell: bash + working-directory: ${{ env.SDK_PYTHON }} run: | source "$SCRIPTS/common.sh" patch_python_package_versions diff --git a/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py index dc93f704d..701fc3841 100644 --- a/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py +++ b/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py @@ -53,7 +53,7 @@ class PlotlyChart(ft.Container): def init(self): _require_plotly() self.alignment = ft.Alignment.CENTER - self.__img = ft.Image(fit=ft.BoxFit.FILL) + self.__img = ft.Image(src="", fit=ft.BoxFit.FILL) self.content = self.__img def before_update(self): diff --git a/sdk/python/packages/flet-cli/src/flet_cli/cli.py b/sdk/python/packages/flet-cli/src/flet_cli/cli.py index 7dff34ba3..1a7b3e1a8 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/cli.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/cli.py @@ -64,16 +64,20 @@ def set_default_subparser( def get_parser() -> argparse.ArgumentParser: """Construct and return the CLI argument parser.""" - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter + ) # add version flag parser.add_argument( "--version", "-V", action="version", - version=flet.version.version - if flet.version.version - else flet.version.from_git(), + version=( + f"Flet: {flet.version.flet_version}\n" + f"Flutter: {flet.version.flutter_version}\n" + f"Pyodide: {flet.version.pyodide_version}" + ), ) sp = parser.add_subparsers(dest="command") diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/base.py index edcef6274..7a3e96cd7 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/base.py @@ -4,6 +4,39 @@ from flet_cli.commands.options import Option, verbose_option +class CustomArgumentDefaultsHelpFormatter(argparse.HelpFormatter): + """ + An argparse help formatter that appends default values to help text + selectively. + + Defaults are added only when they are informative and not already + present in the help string. Noisy or redundant defaults (such as + None, empty lists, booleans for flag arguments, or suppressed values) + are omitted. + """ + + def _get_help_string(self, action: argparse.Action) -> str: + help_text = action.help or "" + default = action.default + + # skip appending a default + if ( + default is None + or default == [] + or isinstance(default, bool) # store_true / store_false flags + or default is argparse.SUPPRESS + or any(token in help_text for token in ("%(default)", "(default:")) + ): + return help_text + + # only add defaults for optionals or for nargs implying optional values + defaulting_nargs = (argparse.OPTIONAL, argparse.ZERO_OR_MORE) + if action.option_strings or action.nargs in defaulting_nargs: + help_text += " (default: %(default)s)" + + return help_text + + class BaseCommand: """A CLI subcommand""" @@ -41,7 +74,7 @@ def register_to( name, description=help_text, help=help_text, - # formatter_class=PdmFormatter, + formatter_class=CustomArgumentDefaultsHelpFormatter, **kwargs, ) command = cls(parser) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index 02744a14f..c7ad4e1e4 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -31,7 +31,6 @@ ) from flet_cli.utils.pyproject_toml import load_pyproject_toml -PYODIDE_ROOT_URL = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full" DEFAULT_TEMPLATE_URL = "gh:flet-dev/flet-build-template" @@ -544,7 +543,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "--android-signing-key-alias", dest="android_signing_key_alias", default="upload", - help="Android signing key alias (default: upload)", + help="Android signing key alias", ) parser.add_argument( "--build-number", @@ -1016,11 +1015,7 @@ def create_flutter_project(self, second_pass=False): "tool.flet.template.ref" ) if not template_ref: - template_ref = ( - version.Version(flet.version.version).base_version - if flet.version.version - else flet.version.from_git() - ) + template_ref = version.Version(flet.version.flet_version).base_version hash.update(template_ref) template_dir = self.options.template_dir or self.get_pyproject( @@ -1622,12 +1617,7 @@ def package_python_app(self): hash.update(reqs_txt_contents) package_args.extend(["-r", "-r", "-r", str(requirements_txt)]) else: - flet_version = ( - flet.version.version - if flet.version.version - else flet.version.from_git() - ) - package_args.extend(["-r", f"flet=={flet_version}"]) + package_args.extend(["-r", f"flet=={flet.version.flet_version}"]) # site-packages variable if self.package_platform != "Pyodide": diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py index c70760e17..acbc99bc8 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py @@ -70,13 +70,13 @@ def handle(self, options: argparse.Namespace) -> None: template_data = { "template_name": options.template, - "flet_version": flet.version.version, + "flet_version": flet.version.flet_version, "sep": os.sep, } template_ref = options.template_ref - if not template_ref and flet.version.version: - template_ref = version.Version(flet.version.version).base_version + if not template_ref: + template_ref = version.Version(flet.version.flet_version).base_version out_dir = Path(options.output_directory).resolve() template_data["out_dir"] = out_dir.name diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/devices.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/devices.py index c491e95ce..747a9ee54 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/devices.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/devices.py @@ -34,7 +34,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: type=int, default=10, dest="device_timeout", - help="Time (in seconds) to wait for devices to attach (default: 10).", + help="Time (in seconds) to wait for devices to attach", ) parser.add_argument( "--device-connection", @@ -42,8 +42,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: choices=["both", "attached", "wireless"], default="both", dest="device_connection", - help="Filter devices by connection type: attached (USB) or wireless " - "(default: both).", + help="Filter devices by connection type: attached (USB) or wireless", ) super().add_arguments(parser) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/doctor.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/doctor.py index da5548430..2dc8f89ef 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/doctor.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/doctor.py @@ -1,5 +1,4 @@ import argparse -import os import platform import sys @@ -21,43 +20,20 @@ def handle(self, options: argparse.Namespace) -> None: """Handle the 'doctor' command.""" verbose = options.verbose - # Step-by-step checks (No need to store results) - self.check_flet_version() - self.check_python_version() - self.check_os_info() - - # Extra details in verbose mode - if verbose: - self.check_virtual_env() - - def check_flet_version(self) -> None: - """Check and print Flet version.""" - with console.status("[bold white]Checking Flet version..."): - flet_version = flet.version.version or "Unknown" - console.print(f"[green]✔ Flet Version:[/green] {flet_version}") - - def check_python_version(self) -> None: - """Check and print Python version.""" - with console.status("[bold white]Checking Python version..."): - console.print(f"[green]✔ Python Version:[/green] {sys.version}") - - def check_os_info(self) -> None: - """Check and print OS information.""" - with console.status("[bold white]Checking OS information..."): - os_info = f"{platform.system()} {platform.release()} ({platform.version()})" - console.print(f"[green]✔ Operating System:[/green] {os_info}") - - def check_virtual_env(self) -> None: - """Check if a Python virtual environment is active.""" - with console.status("[bold white]Checking Python virtual environment..."): - venv = os.getenv("VIRTUAL_ENV") - conda_env = os.getenv("CONDA_PREFIX") - - if venv: - console.print(f"[green]✔ Virtual Environment active:[/green] {venv}") - elif conda_env: - console.print(f"[green]✔ Conda Environment active:[/green] {conda_env}") - else: - console.print( - "[yellow]⚠ No virtual environment or Conda detected[/yellow]" - ) + os_name = platform.system() + if os_name == "Darwin": + os_name = "macOS" + os_version = platform.mac_ver()[0] + else: + os_version = platform.release() + + arch = platform.machine() + console.print( + f"Flet {flet.version.flet_version} on {os_name} {os_version} ({arch})" + if arch + else f"Flet {flet.version.flet_version} on {os_name} {os_version}" + ) + + console.print(f"Python {platform.python_version()} ({sys.executable})") + + # TODO: output Flutter version, if installed diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/flutter_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/flutter_base.py index d718b378d..ce07c6c84 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/flutter_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/flutter_base.py @@ -14,17 +14,13 @@ from rich.style import Style from rich.theme import Theme +import flet.version import flet_cli.utils.processes as processes from flet.utils import cleanup_path, is_windows from flet.utils.platform_utils import get_bool_env_var from flet_cli.commands.base import BaseCommand from flet_cli.utils.flutter import get_flutter_dir, install_flutter -PYODIDE_ROOT_URL = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full" -DEFAULT_TEMPLATE_URL = "gh:flet-dev/flet-build-template" - -MINIMAL_FLUTTER_VERSION = version.Version("3.38.3") - no_rich_output = get_bool_env_var("FLET_CLI_NO_RICH_OUTPUT") error_style = Style(color="red", bold=True) @@ -51,6 +47,7 @@ def __init__(self, parser: argparse.ArgumentParser) -> None: self.emojis = {} self.dart_exe = None self.flutter_exe = None + self.required_flutter_version: Optional[version.Version] = None self.verbose = False self.require_android_sdk = False self.skip_flutter_doctor = get_bool_env_var("FLET_CLI_SKIP_FLUTTER_DOCTOR") @@ -99,6 +96,14 @@ def handle(self, options: argparse.Namespace) -> None: def initialize_command(self): assert self.options + self.required_flutter_version = version.Version(flet.version.FLUTTER_VERSION) + if self.required_flutter_version == version.Version("0"): + self.cleanup( + 1, + "Unable to determine the required Flutter SDK version. " + "If in a source checkout, ensure a valid `.fvmrc` file exists.", + ) + self.emojis = { "checkmark": "[green]OK[/]" if self.no_rich_output else "✅", "loading": "" if self.no_rich_output else "⏳", @@ -125,9 +130,8 @@ def initialize_command(self): style=warning_style, ) prompt = ( - "Flutter SDK " - f"{MINIMAL_FLUTTER_VERSION} is required. It will be installed now. " - "Proceed? [y/n] " + f"Flutter SDK {self.required_flutter_version} is required. " + f"It will be installed now. Proceed? [y/n] " ) if not self._prompt_input(prompt): @@ -155,6 +159,7 @@ def initialize_command(self): self.install_android_sdk() def flutter_version_valid(self): + assert self.required_flutter_version version_results = self.run( [ self.flutter_exe, @@ -172,20 +177,21 @@ def flutter_version_valid(self): # validate installed Flutter version return ( - flutter_version.major == MINIMAL_FLUTTER_VERSION.major - and flutter_version.minor == MINIMAL_FLUTTER_VERSION.minor + flutter_version.major == self.required_flutter_version.major + and flutter_version.minor == self.required_flutter_version.minor ) else: console.log(1, "Failed to validate Flutter version.") return False def install_flutter(self): + assert self.required_flutter_version self.update_status( - f"[bold blue]Installing Flutter {MINIMAL_FLUTTER_VERSION}..." + f"[bold blue]Installing Flutter {self.required_flutter_version}..." ) flutter_dir = install_flutter( - str(MINIMAL_FLUTTER_VERSION), self.log_stdout, progress=self.progress + str(self.required_flutter_version), self.log_stdout, progress=self.progress ) ext = ".bat" if platform.system() == "Windows" else "" self.flutter_exe = os.path.join(flutter_dir, "bin", f"flutter{ext}") @@ -225,8 +231,8 @@ def install_flutter(self): if self.verbose > 0: console.log( - f"Flutter {MINIMAL_FLUTTER_VERSION} installed " - f"{self.emojis['checkmark']}" + f"Flutter {self.required_flutter_version} " + f"installed {self.emojis['checkmark']}" ) def install_jdk(self): @@ -304,7 +310,8 @@ def _prompt_input(self, prompt: str) -> bool: self.live.start() def find_flutter_batch(self, exe_filename: str): - install_dir = get_flutter_dir(str(MINIMAL_FLUTTER_VERSION)) + assert self.required_flutter_version + install_dir = get_flutter_dir(str(self.required_flutter_version)) ext = ".bat" if is_windows() else "" batch_path = os.path.join(install_dir, "bin", f"{exe_filename}{ext}") if os.path.exists(batch_path): diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/pack.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/pack.py index 6e779dcd7..ec2b50de7 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/pack.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/pack.py @@ -47,7 +47,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "--distpath", dest="distpath", default="dist", - help="Directory where the packaged app will be placed (default: ./dist)", + help="Directory where the packaged app will be placed", ) parser.add_argument( "--add-data", @@ -150,7 +150,7 @@ def handle(self, options: argparse.Namespace) -> None: ensure_flet_desktop_package_installed() - is_dir_not_empty = lambda dir: os.path.isdir(dir) and len(os.listdir(dir)) != 0 + is_dir_not_empty = lambda dir: os.path.isdir(dir) and len(os.listdir(dir)) != 0 # noqa: E731 # delete "build" directory build_dir = os.path.join(os.getcwd(), "build") @@ -161,7 +161,7 @@ def handle(self, options: argparse.Namespace) -> None: delete_dir_prompt = input( 'Do you want to delete "build" directory? (y/n) ' ) - if not delete_dir_prompt.lower() == "n": + if delete_dir_prompt.lower() != "n": shutil.rmtree(build_dir, ignore_errors=True) else: print('Failing... "build" directory must be empty to proceed.') @@ -179,13 +179,15 @@ def handle(self, options: argparse.Namespace) -> None: shutil.rmtree(dist_dir, ignore_errors=True) else: delete_dir_prompt = input( - f'Do you want to delete "{os.path.basename(dist_dir)}" directory? (y/n) ' + f'Do you want to delete "{os.path.basename(dist_dir)}" ' + f"directory? (y/n) " ) - if not delete_dir_prompt.lower() == "n": + if delete_dir_prompt.lower() != "n": shutil.rmtree(dist_dir, ignore_errors=True) else: print( - f'Failing... DISTPATH "{os.path.basename(dist_dir)}" directory must be empty to proceed.' + f'Failing... DISTPATH "{os.path.basename(dist_dir)}" directory ' + f"must be empty to proceed." ) exit(1) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py index fd9f05e38..dc3877150 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py @@ -6,7 +6,7 @@ import tempfile from pathlib import Path -from flet.controls.types import WebRenderer +from flet.controls.types import RouteUrlStrategy, WebRenderer from flet.utils import copy_tree, is_within_directory, random_string from flet_cli.commands.base import BaseCommand from flet_cli.utils.project_dependencies import ( @@ -50,8 +50,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "--distpath", dest="distpath", default="dist", - help="Directory where the published web app " - "should be placed (default: ./dist)", + help="Directory where the published web app should be placed", ) parser.add_argument( "--app-name", @@ -216,7 +215,7 @@ def handle(self, options: argparse.Namespace) -> None: print(f"{reqs_filename} dependencies: {deps}") if len(deps) == 0: - deps = [f"flet=={flet.version.version}"] + deps = [f"flet=={flet.version.flet_version}"] temp_reqs_txt = Path(tempfile.gettempdir()).joinpath(random_string(10)) with open(temp_reqs_txt, "w", encoding="utf-8") as f: @@ -305,7 +304,7 @@ def filter_tar(tarinfo: tarfile.TarInfo): or get_pyproject("tool.flet.web.renderer") or "auto" ), - route_url_strategy=str( + route_url_strategy=RouteUrlStrategy( options.route_url_strategy or get_pyproject("tool.flet.web.route_url_strategy") or "path" diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py index 6f6491b45..d67efb6fa 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py @@ -36,7 +36,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: type=str, nargs="?", default=".", - help="Path to the Python script that starts your Flet app (default: .)", + help="Path to the Python script that starts your Flet app", ) parser.add_argument( "-p", diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/serve.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/serve.py index d5e3af1cb..1b82b0c06 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/serve.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/serve.py @@ -34,7 +34,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "web_root", type=str, nargs="?", - help="Directory to serve (default: ./build/web)", + help="Directory to serve", default="./build/web", ) parser.add_argument( @@ -43,7 +43,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: type=int, default=8000, help="Port number to serve the files on. Use this to customize the port if " - "the default is already in use or needs to be changed (default: 8000)", + "the default is already in use or needs to be changed", ) def handle(self, options: argparse.Namespace) -> None: @@ -65,7 +65,8 @@ def handler(*args, **kwargs): try: with socketserver.TCPServer(("", options.port), handler) as httpd: console.print( - f"Serving [green]{directory}[/green] at [cyan]http://localhost:{options.port}[/cyan] (Press Ctrl+C to stop)\n" + f"Serving [green]{directory}[/green] at [cyan]" + f"http://localhost:{options.port}[/cyan] (Press Ctrl+C to stop)\n" ) httpd.serve_forever() except KeyboardInterrupt: diff --git a/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py b/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py index a61fe145a..626832f87 100644 --- a/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py +++ b/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py @@ -211,7 +211,7 @@ def __download_flet_client(file_name): if not ver: import flet.version - ver = flet.version.version or flet.version.from_git() + ver = flet.version.flet_version temp_arch = Path(tempfile.gettempdir()).joinpath(file_name) flet_url = f"https://github.com/flet-dev/flet/releases/download/v{ver}/{file_name}" logger.info(f"Downloading Flet v{ver} from {flet_url} to {temp_arch}") diff --git a/sdk/python/packages/flet-map/src/flet_map/types.py b/sdk/python/packages/flet-map/src/flet_map/types.py index bcb7d1f7c..053dbbbfc 100644 --- a/sdk/python/packages/flet-map/src/flet_map/types.py +++ b/sdk/python/packages/flet-map/src/flet_map/types.py @@ -185,7 +185,7 @@ class DashedStrokePattern(StrokePattern): values must be strictly positive. 'Units' refers to pixels, unless the pattern has been scaled due to the - use of [`pattern_fit`][(c).] [`PatternFit.SCALE_UP`][flet.PatternFit.SCALE_UP]. + [`pattern_fit`][(c).] being [`PatternFit.SCALE_UP`][flet_map.]. If more than two items are specified, then each segments will alternate/iterate through the values. diff --git a/sdk/python/packages/flet-web/src/flet_web/__init__.py b/sdk/python/packages/flet-web/src/flet_web/__init__.py index 3beb3023a..16cf8f519 100644 --- a/sdk/python/packages/flet-web/src/flet_web/__init__.py +++ b/sdk/python/packages/flet-web/src/flet_web/__init__.py @@ -13,4 +13,9 @@ def get_package_web_dir(): return web_root_dir or str(Path(__file__).parent.joinpath("web")) -__all__ = ["patch_font_manifest_json", "patch_index_html", "patch_manifest_json"] +__all__ = [ + "get_package_web_dir", + "patch_font_manifest_json", + "patch_index_html", + "patch_manifest_json", +] diff --git a/sdk/python/packages/flet/docs/extras/macros/cli_to_md.py b/sdk/python/packages/flet/docs/extras/macros/cli_to_md.py index c9d2489c3..1d0d1f3b0 100644 --- a/sdk/python/packages/flet/docs/extras/macros/cli_to_md.py +++ b/sdk/python/packages/flet/docs/extras/macros/cli_to_md.py @@ -110,7 +110,7 @@ def _format_default(a: argparse.Action) -> Optional[str]: return f"`{a.default}`" if a.default not in (None, 0) else None if getattr(a, "const", None) is not None and a.default == a.const: return None - if a.default not in (None, ""): + if a.default not in (None, "", []): return f"`{a.default}`" return None diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index ae213addd..376029578 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -576,6 +576,7 @@ ) from flet.pubsub.pubsub_client import PubSubClient from flet.pubsub.pubsub_hub import PubSubHub +from flet.version import flet_version as __version__ __all__ = [ "Accelerometer", @@ -1047,6 +1048,7 @@ "WindowEventType", "WindowResizeEdge", "WindowsDeviceInfo", + "__version__", "alignment", "app", "app_async", diff --git a/sdk/python/packages/flet/src/flet/auth/authorization_service.py b/sdk/python/packages/flet/src/flet/auth/authorization_service.py index 0f15c80ca..d49590e41 100644 --- a/sdk/python/packages/flet/src/flet/auth/authorization_service.py +++ b/sdk/python/packages/flet/src/flet/auth/authorization_service.py @@ -11,7 +11,7 @@ from flet.auth.oauth_provider import OAuthProvider from flet.auth.oauth_token import OAuthToken from flet.auth.user import User -from flet.version import version +from flet.version import flet_version class AuthorizationService(Authorization): @@ -157,5 +157,5 @@ async def __get_user(self): def __get_default_headers(self): return { - "User-Agent": f"Flet/{version}", + "User-Agent": f"Flet/{flet_version}", } diff --git a/sdk/python/packages/flet/src/flet/utils/pip.py b/sdk/python/packages/flet/src/flet/utils/pip.py index d41a49415..7cc10f3a2 100644 --- a/sdk/python/packages/flet/src/flet/utils/pip.py +++ b/sdk/python/packages/flet/src/flet/utils/pip.py @@ -5,7 +5,7 @@ def install_flet_package(name: str): - print(f"Installing {name} {flet.version.version} package...", end="") + print(f"Installing {name} {flet.version.flet_version} package...", end="") retcode = subprocess.call( [ sys.executable, @@ -14,45 +14,46 @@ def install_flet_package(name: str): "install", "-q", "--disable-pip-version-check", - f"{name}=={flet.version.version}", + f"{name}=={flet.version.flet_version}", ] ) if retcode == 0: print("OK") else: print( - f'Unable to upgrade "{name}" package to version {flet.version.version}. Please use "pip install \'flet[all]=={flet.version.version}\' --upgrade" command to upgrade Flet.' + f'Unable to upgrade "{name}" package to version ' + f"{flet.version.flet_version}. Please use " + f"\"pip install 'flet[all]=={flet.version.flet_version}' --upgrade\" " + f"command to upgrade Flet." ) exit(1) def ensure_flet_desktop_package_installed(): try: - import flet_desktop.version - import flet.version + import flet_desktop.version if ( flet_desktop.version.version - and flet_desktop.version.version != flet.version.version + and flet_desktop.version.version != flet.version.flet_version ): raise RuntimeError("flet-desktop version mismatch") - except: + except Exception: install_flet_package("flet-desktop") def ensure_flet_web_package_installed(): try: - import flet_web.version - import flet.version + import flet_web.version if ( flet_web.version.version - and flet_web.version.version != flet.version.version + and flet_web.version.version != flet.version.flet_version ): raise RuntimeError("flet-web version mismatch") - except: + except Exception: install_flet_package("flet-web") @@ -63,8 +64,8 @@ def ensure_flet_cli_package_installed(): if ( flet_cli.version.version - and flet_cli.version.version != flet.version.version + and flet_cli.version.version != flet.version.flet_version ): raise RuntimeError("flet-cli version mismatch") - except: + except Exception: install_flet_package("flet-cli") diff --git a/sdk/python/packages/flet/src/flet/version.py b/sdk/python/packages/flet/src/flet/version.py index 1796700f5..42454323e 100644 --- a/sdk/python/packages/flet/src/flet/version.py +++ b/sdk/python/packages/flet/src/flet/version.py @@ -1,57 +1,79 @@ """Provide the current Flet version.""" -import os +import json import subprocess as sp from pathlib import Path +from typing import Optional -import flet from flet.utils import is_mobile, is_windows, which -DEFAULT_VERSION = "0.1.0" +__all__ = [ + "find_repo_root", + "flet_version", + "flutter_version", + "from_git", + "pyodide_version", +] -# will be replaced by CI -version = "" +# set by CI +flet_version = "" +""" +The Flet version in use. +This value is set explicitly in CI for released packages. When running from +source and no version is provided, it is derived from the nearest Git tag +when available. +""" -def from_git(): +# set by CI +flutter_version = "" +""" +The Flutter SDK version used when building the flet client or packaging +apps with [`flet build`](https://docs.flet.dev/cli/flet-build/). + +This value is set explicitly in CI for released packages. When running from +source and no version is provided, it is resolved from the repository's +`.fvmrc` file when available. +""" + +PYODIDE_VERSION = "0.27.7" +""" +The Pyodide version being used when packaging +with [`flet build web`](https://docs.flet.dev/cli/flet-build/). +""" + + +def from_git() -> Optional[str]: """Try to get the version from Git tags.""" - working = Path().absolute() + repo_root = find_repo_root(Path(__file__).resolve().parent) + if not repo_root: + return None + + git_cmd = "git.exe" if is_windows() else "git" + if not which(git_cmd): + return None + try: - version_file_path = Path(flet.__file__).absolute().parent / "version.py" - repo_root = find_repo_root(version_file_path.parent) + result = sp.run( + [git_cmd, "describe", "--abbrev=0"], + cwd=repo_root, + capture_output=True, + text=True, + check=True, + ) + tag = result.stdout.strip() + return tag[1:] if tag.startswith("v") else tag + + except sp.CalledProcessError as e: + # Git is present but no tags / not a valid repo state + print(f"Error getting Git version: {e}") + except OSError as e: + print(f"Error running Git: {e}") - if repo_root: - os.chdir(repo_root) - in_repo = ( - which("git.exe" if is_windows() else "git") - and sp.run( - ["git", "status"], - capture_output=True, - text=True, - ).returncode - == 0 - ) - - if in_repo: - try: - git_p = sp.run( - ["git", "describe", "--abbrev=0"], - capture_output=True, - text=True, - check=True, # Raise an exception for non-zero exit codes - ) - tag = git_p.stdout.strip() - return tag[1:] if tag.startswith("v") else tag - except sp.CalledProcessError as e: - print(f"Error getting Git version: {e}") - except FileNotFoundError: - print("Git command not found.") - finally: - os.chdir(working) return None -def find_repo_root(start_path: Path) -> Path | None: +def find_repo_root(start_path: Path) -> Optional[Path]: """Find the root directory of the Git repository containing the start path.""" current_path = start_path.resolve() while current_path != current_path.parent: @@ -61,14 +83,59 @@ def find_repo_root(start_path: Path) -> Path | None: return None -if not version and not is_mobile(): - # Only try to get the version from Git if the pre-set version is empty - # This is more likely to happen in a development/source environment - version = from_git() or DEFAULT_VERSION # Fallback to a default if Git fails +def get_flet_version() -> str: + """Return the Flet version, falling back to Git or a default if needed.""" + + # If the version is already set (e.g., replaced by CI), use it + if flet_version: + return flet_version + + # Only try to get the version from Git if the pre-set version is empty. + # This is more likely to happen in a development/source environment. + if not is_mobile(): + git_version = from_git() + if git_version: + return git_version # Use Git version if available + + # If 'flet_version' is still empty after the above (e.g., in a built package + # where CI didn't replace it), fall back to the default version. + # CI replacement is the standard way for packaged versions. + return "0.1.0" + + +def get_flutter_version() -> str: + """ + Return the Flutter SDK version. + + Uses `flutter_version` when set (CI/release builds); otherwise resolves it + from `.fvmrc` in a local development checkout. + """ + + # If the version is already set (e.g., replaced by CI), use it + if flutter_version: + return flutter_version + + if not is_mobile(): + repo_root = find_repo_root(Path(__file__).resolve().parent) + if repo_root: + fvmrc_path = repo_root / ".fvmrc" + try: + v = json.loads(fvmrc_path.read_text(encoding="utf-8"))[ + "flutter" + ].strip() + if not v: + raise ValueError("Empty or missing 'flutter' value") + return v + except Exception as e: + print(f"Error parsing {fvmrc_path!r}: {e}") + + # If 'flutter_version' is still empty after the above (e.g., in a built package + # where CI didn't replace it), fall back to the below default. + # CI replacement is the standard way for packaged versions. + return "0" + -# If 'version' is still empty after the above (e.g., in a built package -# where CI didn't replace it), it might be appropriate to have another -# default or a way to set it during the build process. However, the -# CI replacement is the standard way for packaged versions. -if not version: - version = DEFAULT_VERSION # Final fallback +flutter_version = get_flutter_version() +pyodide_version = PYODIDE_VERSION +flet_version = get_flet_version() +__version__ = flet_version diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 0fe97e8f4..22036b847 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -111,6 +111,8 @@ pydocstyle = { convention = 'google' } isort = { known-first-party = [ "flet", "flet_cli", + "flet_desktop", + "flet_web", "flet_ads", "flet_audio", "flet_audio_recorder",