Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 0.86.3

### Improvements

* `flet run` can now pass custom arguments to your app script: everything after a `--` separator is forwarded to the script instead of being parsed by Flet, and arrives there as `sys.argv[1:]` - e.g. `flet run --web main.py -- --dataset big.csv --verbose`. Previously there was no way to do this: the app was always launched as `python -u <script>` with no extra arguments, so a flag meant for the app was consumed by the CLI's own parser and rejected with `flet: error: unrecognized arguments: --verbose`. The arguments are re-applied on every hot reload and work in all run modes (desktop, `--web`, `--ios`, `--android`, and `-m` module invocations). Arguments that don't look like options can be passed without the separator (`flet run main.py big.csv`), and mistyped Flet options are still reported as errors - now with a hint to use `--` when they were meant for the app. See [Passing arguments to your app](https://flet.dev/docs/getting-started/running-app#passing-arguments-to-your-app) by @FeodorFitsner.

### Bug fixes

* Fix a system/edge-swipe back gesture exiting the whole host app instead of navigating back when it lands on an embedded `FletApp` (an app rendered inside another Flet app — e.g. a gallery host running example apps in-process). The embedded app's `WidgetsApp` (`MaterialApp`/`CupertinoApp`) ran the default `NavigationNotification` handler, which reported `SystemNavigator.setFrameworkHandlesBack(false)` for a nested app that couldn't pop (typically a single-view example) and swallowed the notification, so the OS finished the whole activity on back and the host never got to report that it could pop. An embedded page now lets that notification bubble to the host (which re-reports `canHandlePop`) and chains a `ChildBackButtonDispatcher` to the host Router, so a system back propagates to the host and pops the view that embeds it by @FeodorFitsner.
Expand Down
110 changes: 86 additions & 24 deletions sdk/python/packages/flet-cli/src/flet_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import json
import sys
from typing import Optional

import flet.version
import flet_cli.commands.build
Expand Down Expand Up @@ -41,23 +42,25 @@ def _render_version(as_json: bool) -> str:

# Source https://stackoverflow.com/a/26379693
def set_default_subparser(
parser: argparse.ArgumentParser, name: str, args: list = None, index: int = 0
):
parser: argparse.ArgumentParser, name: str, args: list
) -> list:
"""
Set a default subparser when no subparser is provided.
Prepend a default subparser name when the command line doesn't name one.
This should be called after setting up the argument parser but before
`parse_args()`.

Args:
parser: The root argument parser.
name: The name of the default subparser to use.
args: A list of arguments passed to `parse_args()`.
index: Position in `sys.argv` where the default subparser should be
inserted.
args: The command-line arguments, without the program name.

Returns:
`args`, with the default subparser name prepended when applicable.
"""

# exit if help or version flags are present
if any(flag in sys.argv[1:] for flag in {"-h", "--help", "-V", "--version"}):
return
# exit if no arguments, or help or version flags are present
if not args or any(flag in args for flag in {"-h", "--help", "-V", "--version"}):
return args

# all subparser actions
subparser_actions = [
Expand All @@ -72,19 +75,14 @@ def set_default_subparser(
]

# if an existing subparser is provided, skip setting a default
if any(arg in subparser_names for arg in sys.argv[1:]):
return
if any(arg in subparser_names for arg in args):
return args

# if the default subparser doesn't exist, register it in the first subparser action
if (name not in subparser_names) and subparser_actions:
subparser_actions[0].add_parser(name)

# insert the default subparser into the appropriate argument list
if args is None:
if len(sys.argv) > 1:
sys.argv.insert(index, name)
else:
args.insert(index, name)
return [name, *args]


def get_parser() -> argparse.ArgumentParser:
Expand Down Expand Up @@ -132,22 +130,86 @@ def get_parser() -> argparse.ArgumentParser:
except ImportError:
pass

# set "run" as the default subparser
set_default_subparser(parser, name="run", index=1)

return parser


def main():
def split_script_args(argv: list) -> tuple[list, Optional[list]]:
"""
Split a command line at the first bare `--` separator.

Everything after the separator is meant for the app script, not for Flet
itself, so it must be removed before the parser (and `set_default_subparser`)
sees it - otherwise `flet run app.py -- --web` would be read as Flet's own
`--web` option, and `flet app.py -- build` would suppress the default `run`
subcommand.

Args:
argv: Command-line arguments, without the program name.

Returns:
A `(argv, script_args)` tuple, where `script_args` is `None` when no
`--` separator is present.
"""
try:
index = argv.index("--")
except ValueError:
return argv, None
return argv[:index], argv[index + 1 :]


def parse_command_line(argv: list) -> argparse.Namespace:
"""
Parse a Flet command line into the namespace passed to a command handler.

Exits the process with a usage message when the command line is invalid.

Args:
argv: Command-line arguments, without the program name.

Returns:
The parsed arguments. For `flet run`, `script_args` holds the arguments
to forward to the app script.
"""

# pull off the arguments meant for the app script before the parser sees them
argv, script_args = split_script_args(argv)

parser = get_parser()

# "run" is the default subcommand
argv = set_default_subparser(parser, name="run", args=argv)

args, unrecognized = parser.parse_known_args(argv)

if unrecognized:
message = f"unrecognized arguments: {' '.join(unrecognized)}"
if getattr(args, "command", None) == "run":
message += (
"\nIf these are arguments for your app script, put them after "
"a `--` separator, e.g. "
f"`flet run {args.script} -- {' '.join(unrecognized)}`"
)
parser.error(message)

# forward the arguments that followed `--` to the app script
if script_args is not None:
if getattr(args, "command", None) != "run":
parser.error(
"the `--` separator is only supported by `flet run`, to pass "
"the arguments that follow it to your app script"
)
args.script_args = list(args.script_args) + script_args

return args


def main():
# print usage/help if called without arguments
if len(sys.argv) == 1:
parser.print_help(sys.stdout)
get_parser().print_help(sys.stdout)
sys.exit(1)

# parse arguments
args = parser.parse_args()
args = parse_command_line(sys.argv[1:])

# handle `flet --version [--json]` (no subcommand/handler is set)
if getattr(args, "version", False):
Expand Down
13 changes: 12 additions & 1 deletion sdk/python/packages/flet-cli/src/flet_cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
default=".",
help="Path to the Python script that starts your Flet app",
)
parser.add_argument(
"script_args",
type=str,
nargs="*",
default=[],
metavar="args",
help="Arguments to pass to your app script, available there as "
"`sys.argv[1:]`. Separate them from Flet's own options with `--`, "
"e.g. `flet run --web app.py -- --verbose`",
)
parser.add_argument(
"-p",
"--port",
Expand Down Expand Up @@ -287,7 +297,8 @@ def handle(self, options: argparse.Namespace) -> None:
my_event_handler = Handler(
args=[sys.executable, "-u"]
+ ["-m"] * options.module
+ [options.script if options.module else script_path],
+ [options.script if options.module else script_path]
+ list(options.script_args),
watch_directory=options.directory or options.recursive,
script_path=str(script_path),
port=port,
Expand Down
103 changes: 103 additions & 0 deletions sdk/python/packages/flet-cli/tests/test_run_script_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

from flet_cli.cli import parse_command_line


def _parse(*argv):
"""Parse a Flet command line given as separate arguments."""
return parse_command_line(list(argv))


def test_no_script_args():
"""A plain `flet run app.py` passes nothing to the script."""
args = _parse("run", "app.py")

assert args.script == "app.py"
assert args.script_args == []


def test_script_args_after_separator():
"""Arguments after `--` are forwarded to the script, options and all."""
args = _parse("run", "app.py", "--", "--manual", "-x", "1")

assert args.script == "app.py"
assert args.script_args == ["--manual", "-x", "1"]


def test_flet_options_are_not_forwarded():
"""Flet's own options before `--` stay with Flet."""
args = _parse("run", "-w", "app.py", "--", "--web")

assert args.web is True
assert args.script == "app.py"
assert args.script_args == ["--web"]


def test_flet_options_between_script_and_separator():
"""Flet's options may follow the script path and precede `--`."""
args = _parse("run", "app.py", "--web", "--port", "8080", "--", "--manual")

assert args.web is True
assert args.port == 8080
assert args.script_args == ["--manual"]


def test_positional_script_args_need_no_separator():
"""Arguments that don't look like options are forwarded without `--`."""
args = _parse("run", "app.py", "foo", "bar")

assert args.script == "app.py"
assert args.script_args == ["foo", "bar"]


def test_positional_and_separated_script_args_are_combined():
"""Arguments before and after `--` are forwarded in command-line order."""
args = _parse("run", "app.py", "foo", "--", "--manual")

assert args.script_args == ["foo", "--manual"]


def test_script_args_with_module():
"""`-m` module invocations forward script arguments too."""
args = _parse("run", "-m", "my_app.main", "--", "--manual")

assert args.module is True
assert args.script == "my_app.main"
assert args.script_args == ["--manual"]


def test_script_args_with_implicit_run_command():
"""The default `run` subcommand is applied even with a `--` separator."""
args = _parse("app.py", "--", "--manual")

assert args.command == "run"
assert args.script == "app.py"
assert args.script_args == ["--manual"]


def test_script_args_named_like_a_subcommand():
"""A script argument matching a subcommand name doesn't select it."""
args = _parse("app.py", "--", "build", "test")

assert args.command == "run"
assert args.script == "app.py"
assert args.script_args == ["build", "test"]


def test_unknown_flet_option_is_rejected(capsys):
"""A mistyped Flet option still errors instead of reaching the script."""
with pytest.raises(SystemExit):
_parse("run", "app.py", "--wev")

err = capsys.readouterr().err
assert "unrecognized arguments: --wev" in err
# ...and the error suggests the `--` separator
assert "flet run app.py -- --wev" in err


def test_separator_rejected_for_other_commands(capsys):
"""Only `flet run` accepts a `--` separator."""
with pytest.raises(SystemExit):
_parse("build", "apk", "--", "--manual")

assert "only supported by `flet run`" in capsys.readouterr().err
49 changes: 49 additions & 0 deletions website/docs/getting-started/running-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,55 @@ A new browser window/tab will be opened and the app will be using a random TCP p

<Image src="assets/getting-started/counter-app/safari.png" alt="Web" width="45%" caption="Running Flet app as a web app" />

## Passing arguments to your app

Anything after a `--` separator is passed through to your app script instead of being
interpreted by Flet, and shows up there as `sys.argv[1:]`:

<Tabs groupId="uv--pip">
<TabItem value="uv" label="uv">
```bash
uv run flet run --web [script] -- --dataset big.csv --verbose
```
</TabItem>
<TabItem value="pip" label="pip">
```bash
flet run --web [script] -- --dataset big.csv --verbose
```
</TabItem>
</Tabs>

Your app reads them as it would when started with `python`:

```python title="main.py"
import argparse
import sys

import flet as ft

parser = argparse.ArgumentParser()
parser.add_argument("--dataset", default="small.csv")
parser.add_argument("--verbose", action="store_true")
options = parser.parse_args(sys.argv[1:])


def main(page: ft.Page):
page.add(ft.Text(f"Dataset: {options.dataset}"))


ft.run(main)
```

The `--` separator is what keeps your app's arguments from being read as Flet's own -
without it, `flet run main.py --verbose` fails because `--verbose` is not a
[`flet run`](../cli/flet-run.md) option. Arguments that don't start with a `-` can be
passed without a separator, e.g. `flet run main.py big.csv`.

:::note
The same arguments are re-applied every time the app is hot reloaded, and they are
passed through in all run modes: desktop, `--web`, `--ios` and `--android`.
:::

## Watching for changes

By default, Flet will watch the script file that was run and reload the app whenever the contents
Expand Down
Loading