From 0abf28ac31968f9b34b0f921fba0890bb3cd7861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:05:58 +0100 Subject: [PATCH 1/7] Add --port option to textual console. --- docs/guide/devtools.md | 8 ++++++++ src/textual/app.py | 2 ++ src/textual/cli/cli.py | 25 ++++++++++++++++++++++--- src/textual/devtools/client.py | 19 ++++++++++++++++--- src/textual/devtools/server.py | 7 +++++-- tests/devtools/test_devtools_client.py | 4 ++-- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index dd0f168b8e..f5bd6d686f 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -90,6 +90,14 @@ Multiple groups may be excluded, for example to exclude everything except warnin textual console -x SYSTEM -x EVENT -x DEBUG -x INFO ``` +### Custom port + +You can use the option `--port` to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default: + +```bash +textual console --port 7342 +``` + ## Textual log diff --git a/src/textual/app.py b/src/textual/app.py index 1399d97f57..50077524ab 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -58,6 +58,8 @@ from rich.segment import Segment, Segments from rich.traceback import Traceback +from textual.devtools.client import get_port_for_devtools + from . import Logger, LogGroup, LogVerbosity, actions, constants, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index c4d1da0a19..cde7280b0f 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -2,6 +2,8 @@ import sys +from textual.devtools.client import DEVTOOLS_PORT_ENVIRON_VARIABLE + try: import click except ImportError: @@ -21,14 +23,27 @@ def run(): @run.command(help="Run the Textual Devtools console.") +@click.option( + "--port", + "port", + type=int, + default=None, + metavar="PORT", + help="Port to use for the development mode console", +) @click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True) @click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True) -def console(verbose: bool, exclude: list[str]) -> None: +def console(port: int | None, verbose: bool, exclude: list[str]) -> None: """Launch the textual console.""" + import os + from rich.console import Console from textual.devtools.server import _run_devtools + if port is not None: + os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) + console = Console() console.clear() console.show_cursor(False) @@ -86,7 +101,9 @@ def _post_run_warnings() -> None: metavar="DELAY", help="Take screenshot after DELAY seconds", ) -def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) -> None: +def run_app( + import_name: str, dev: bool, port: int | None, press: str, screenshot: int | None +) -> None: """Run a Textual app. The code to run may be given as a path (ending with .py) or as a Python @@ -107,7 +124,6 @@ def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) -> in quotes: textual run "foo.py arg --option" - """ import os @@ -131,6 +147,9 @@ def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) -> console.print(str(error)) sys.exit(1) + if port is not None: + os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) + press_keys = press.split(",") if press else None async def run_press_keys(pilot: Pilot) -> None: diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index 3c8592ae61..fc9a268b67 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -3,6 +3,7 @@ import asyncio import inspect import json +import os import pickle from asyncio import Queue, QueueFull, Task from io import StringIO @@ -17,11 +18,20 @@ from .._log import LogGroup, LogVerbosity -DEVTOOLS_PORT = 8081 +DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" +DEFAULT_DEVTOOLS_PORT = 8081 WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 +def get_port_for_devtools() -> int: + """Get the port to run the devtools on from the environment or the default.""" + try: + return int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) + except (KeyError, ValueError): + return DEFAULT_DEVTOOLS_PORT + + class DevtoolsLog(NamedTuple): """A devtools log message. @@ -88,10 +98,13 @@ class DevtoolsClient: Args: host: The host the devtools server is running on, defaults to "127.0.0.1" - port: The port the devtools server is accessed via, defaults to 8081 + port: The port the devtools server is accessed via, + retrieved by `get_port_for_devtools` by default. """ - def __init__(self, host: str = "127.0.0.1", port: int = DEVTOOLS_PORT) -> None: + def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None: + if port is None: + port = get_port_for_devtools() self.url: str = f"ws://{host}:{port}" self.session: aiohttp.ClientSession | None = None self.log_queue_task: Task | None = None diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index 4228ca6f53..daa2fcac4f 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -8,7 +8,7 @@ from aiohttp.web_routedef import get from aiohttp.web_ws import WebSocketResponse -from textual.devtools.client import DEVTOOLS_PORT +from textual.devtools.client import get_port_for_devtools from textual.devtools.service import DevtoolsService DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2 @@ -46,7 +46,10 @@ def noop_print(_: str) -> None: try: run_app( - app, port=DEVTOOLS_PORT, print=noop_print, loop=asyncio.get_event_loop() + app, + port=get_port_for_devtools(), + print=noop_print, + loop=asyncio.get_event_loop(), ) except OSError: from rich import print diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index 88088bff06..47afa280e8 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -10,7 +10,7 @@ from rich.panel import Panel from tests.utilities.render import wait_for_predicate -from textual.devtools.client import DevtoolsClient +from textual.devtools.client import DEFAULT_DEVTOOLS_PORT, DevtoolsClient from textual.devtools.redirect_output import DevtoolsLog CALLER_LINENO = 123 @@ -21,7 +21,7 @@ def test_devtools_client_initialize_defaults(): devtools = DevtoolsClient() - assert devtools.url == "ws://127.0.0.1:8081" + assert devtools.url == f"ws://127.0.0.1:{DEFAULT_DEVTOOLS_PORT}" async def test_devtools_client_is_connected(devtools): From 33fb463d29070faca794f3431dd26c581ba32e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:12:58 +0100 Subject: [PATCH 2/7] Changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2124f2dd3c..1dd5d81cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - `Widget.scroll_to_center` now scrolls the widget to the center of the screen https://github.com/Textualize/textual/pull/2255 +- option `--port` to the command `textual console` to specify which port the console should connect to https://github.com/Textualize/textual/pull/2258 ## [0.19.1] - 2023-04-10 From 626191199de21907c4137a236807473511a5e917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Wed, 12 Apr 2023 16:18:22 +0100 Subject: [PATCH 3/7] Address review feedback. --- docs/guide/devtools.md | 6 ++++++ src/textual/app.py | 2 -- src/textual/cli/cli.py | 18 +++++++++++++----- src/textual/constants.py | 13 ++++++++++++- src/textual/devtools/client.py | 12 +----------- tests/devtools/test_devtools_client.py | 3 ++- .../snapshot_apps/scroll_to_center.py | 2 +- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index f5bd6d686f..4fbffe6cbf 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -98,6 +98,12 @@ You can use the option `--port` to specify a custom port to run the console on, textual console --port 7342 ``` +Then, use the command `run` with the same `--port` option: + +```bash +textual run --dev --port 7342 my_app.py +``` + ## Textual log diff --git a/src/textual/app.py b/src/textual/app.py index 50077524ab..1399d97f57 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -58,8 +58,6 @@ from rich.segment import Segment, Segments from rich.traceback import Traceback -from textual.devtools.client import get_port_for_devtools - from . import Logger, LogGroup, LogVerbosity, actions, constants, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index cde7280b0f..c1186f99d7 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -2,7 +2,7 @@ import sys -from textual.devtools.client import DEVTOOLS_PORT_ENVIRON_VARIABLE +from ..constants import DEFAULT_DEVTOOLS_PORT, DEVTOOLS_PORT_ENVIRON_VARIABLE try: import click @@ -29,7 +29,7 @@ def run(): type=int, default=None, metavar="PORT", - help="Port to use for the development mode console", + help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.", ) @click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True) @click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True) @@ -93,6 +93,14 @@ def _post_run_warnings() -> None: ) @click.argument("import_name", metavar="FILE or FILE:APP") @click.option("--dev", "dev", help="Enable development mode", is_flag=True) +@click.option( + "--port", + "port", + type=int, + default=None, + metavar="PORT", + help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.", +) @click.option("--press", "press", help="Comma separated keys to simulate press") @click.option( "--screenshot", @@ -132,6 +140,9 @@ def run_app( from textual.features import parse_features + if port is not None: + os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) + features = set(parse_features(os.environ.get("TEXTUAL", ""))) if dev: features.add("debug") @@ -147,9 +158,6 @@ def run_app( console.print(str(error)) sys.exit(1) - if port is not None: - os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) - press_keys = press.split(",") if press else None async def run_press_keys(pilot: Pilot) -> None: diff --git a/src/textual/constants.py b/src/textual/constants.py index f3762b33d6..191a282162 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -15,6 +15,9 @@ get_environ = os.environ.get +DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" +DEFAULT_DEVTOOLS_PORT = 8081 + def get_environ_bool(name: str) -> bool: """Check an environment variable switch. @@ -25,10 +28,18 @@ def get_environ_bool(name: str) -> bool: Returns: `True` if the env var is "1", otherwise `False`. """ - has_environ = os.environ.get(name) == "1" + has_environ = get_environ(name) == "1" return has_environ +def get_port_for_devtools() -> int: + """Get the port to run the devtools on from the environment or the default.""" + try: + return int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) + except (KeyError, ValueError): + return DEFAULT_DEVTOOLS_PORT + + BORDERS = list(BORDER_CHARS) DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG") diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index fc9a268b67..83126e5cd7 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -3,7 +3,6 @@ import asyncio import inspect import json -import os import pickle from asyncio import Queue, QueueFull, Task from io import StringIO @@ -17,21 +16,12 @@ from rich.segment import Segment from .._log import LogGroup, LogVerbosity +from ..constants import get_port_for_devtools -DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" -DEFAULT_DEVTOOLS_PORT = 8081 WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 -def get_port_for_devtools() -> int: - """Get the port to run the devtools on from the environment or the default.""" - try: - return int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) - except (KeyError, ValueError): - return DEFAULT_DEVTOOLS_PORT - - class DevtoolsLog(NamedTuple): """A devtools log message. diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index 47afa280e8..b67f558ba4 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -10,7 +10,8 @@ from rich.panel import Panel from tests.utilities.render import wait_for_predicate -from textual.devtools.client import DEFAULT_DEVTOOLS_PORT, DevtoolsClient +from textual.constants import DEFAULT_DEVTOOLS_PORT +from textual.devtools.client import DevtoolsClient from textual.devtools.redirect_output import DevtoolsLog CALLER_LINENO = 123 diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py index fdd616cb6d..ffb692465e 100644 --- a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py +++ b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py @@ -18,7 +18,7 @@ class MyApp(App[None]): def compose(self) -> ComposeResult: with VerticalScroll(): - yield Label(("SPAM\n" * 25)[:-1]) + yield Label(("SPAM\n" * 205)[:-1]) with VerticalScroll(): yield Label(("SPAM\n" * 53)[:-1]) with VerticalScroll(id="vertical"): From d17568876cdbe180452a8409b8536c33163e5815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Wed, 12 Apr 2023 16:38:36 +0100 Subject: [PATCH 4/7] Mark unpredictable test as xfail. This test gets an xfail mark until #2254 is open. --- tests/snapshot_tests/test_snapshots.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 35a0cebc2c..e8bde50c72 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -418,6 +418,9 @@ def test_scroll_visible(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"]) +@pytest.mark.xfail( + reason="Unpredictable while https://github.com/Textualize/textual/issues/2254 is open." +) def test_scroll_to_center(snap_compare): # READ THIS IF THIS TEST FAILS: # While https://github.com/Textualize/textual/issues/2254 is open, the snapshot From e6307bb0117d945a48d11e9ec18934ba3614173c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 13 Apr 2023 10:46:10 +0100 Subject: [PATCH 5/7] Make DEVTOOLS_PORT a constant. Related review: https://github.com/Textualize/textual/pull/2258\#discussion_r1165210395 --- src/textual/constants.py | 20 +++++++++----------- src/textual/devtools/client.py | 7 +++---- src/textual/devtools/server.py | 4 ++-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/textual/constants.py b/src/textual/constants.py index 191a282162..506feaf578 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -15,9 +15,6 @@ get_environ = os.environ.get -DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" -DEFAULT_DEVTOOLS_PORT = 8081 - def get_environ_bool(name: str) -> bool: """Check an environment variable switch. @@ -32,14 +29,6 @@ def get_environ_bool(name: str) -> bool: return has_environ -def get_port_for_devtools() -> int: - """Get the port to run the devtools on from the environment or the default.""" - try: - return int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) - except (KeyError, ValueError): - return DEFAULT_DEVTOOLS_PORT - - BORDERS = list(BORDER_CHARS) DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG") @@ -48,3 +37,12 @@ def get_port_for_devtools() -> int: LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None) """A last resort log file that appends all logs, when devtools isn't working.""" + + +DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" +DEFAULT_DEVTOOLS_PORT = 8081 +try: + _devtools_port = int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) +except (KeyError, ValueError): + _devtools_port = DEFAULT_DEVTOOLS_PORT +DEVTOOLS_PORT: Final[int] = _devtools_port diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index 83126e5cd7..2982d07feb 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -16,7 +16,7 @@ from rich.segment import Segment from .._log import LogGroup, LogVerbosity -from ..constants import get_port_for_devtools +from ..constants import DEVTOOLS_PORT WEBSOCKET_CONNECT_TIMEOUT = 3 LOG_QUEUE_MAXSIZE = 512 @@ -88,13 +88,12 @@ class DevtoolsClient: Args: host: The host the devtools server is running on, defaults to "127.0.0.1" - port: The port the devtools server is accessed via, - retrieved by `get_port_for_devtools` by default. + port: The port the devtools server is accessed via, `DEVTOOLS_PORT` by default. """ def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None: if port is None: - port = get_port_for_devtools() + port = DEVTOOLS_PORT self.url: str = f"ws://{host}:{port}" self.session: aiohttp.ClientSession | None = None self.log_queue_task: Task | None = None diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index daa2fcac4f..4e7f66814b 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -8,7 +8,7 @@ from aiohttp.web_routedef import get from aiohttp.web_ws import WebSocketResponse -from textual.devtools.client import get_port_for_devtools +from textual.devtools.client import DEVTOOLS_PORT from textual.devtools.service import DevtoolsService DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2 @@ -47,7 +47,7 @@ def noop_print(_: str) -> None: try: run_app( app, - port=get_port_for_devtools(), + port=DEVTOOLS_PORT, print=noop_print, loop=asyncio.get_event_loop(), ) From fa05e169e307187974b13879b88aba7b1a474e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 13 Apr 2023 11:10:36 +0100 Subject: [PATCH 6/7] Factor logic into function. Related review: https://github.com/Textualize/textual/pull/2258\#discussion_r1165298259 --- src/textual/constants.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/textual/constants.py b/src/textual/constants.py index 506feaf578..e406016192 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -6,6 +6,7 @@ from __future__ import annotations import os +from typing import overload from typing_extensions import Final @@ -29,6 +30,24 @@ def get_environ_bool(name: str) -> bool: return has_environ +def get_environ_int(name: str, default: int) -> int: + """Retrieves an integer environment variable. + + Args: + name: Name of environment variable. + default: The value to use if the value is not set, or set to something other + than a valid integer. + + Returns: + The integer associated with the environment variable if it's set to a valid int + or the default value otherwise. + """ + try: + return int(get_environ(name, default)) + except ValueError: + return default + + BORDERS = list(BORDER_CHARS) DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG") @@ -39,10 +58,11 @@ def get_environ_bool(name: str) -> bool: """A last resort log file that appends all logs, when devtools isn't working.""" -DEVTOOLS_PORT_ENVIRON_VARIABLE = "TEXTUAL_CONSOLE_PORT" -DEFAULT_DEVTOOLS_PORT = 8081 -try: - _devtools_port = int(os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE]) -except (KeyError, ValueError): - _devtools_port = DEFAULT_DEVTOOLS_PORT -DEVTOOLS_PORT: Final[int] = _devtools_port +DEVTOOLS_PORT_ENVIRON_VARIABLE: Final[str] = "TEXTUAL_CONSOLE_PORT" +"""The name of the environment variable that sets the port for the devtools.""" +DEFAULT_DEVTOOLS_PORT: Final[int] = 8081 +"""The default port to use for the devtools.""" +DEVTOOLS_PORT: Final[int] = get_environ_int( + DEVTOOLS_PORT_ENVIRON_VARIABLE, DEFAULT_DEVTOOLS_PORT +) +"""Constant with the port that the devtools will connect to.""" From 4bf6c18674c9d03645034b4d535aab37f4b19c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 13 Apr 2023 11:11:37 +0100 Subject: [PATCH 7/7] Remove dead import. --- src/textual/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/constants.py b/src/textual/constants.py index e406016192..c73fe31af1 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -6,7 +6,6 @@ from __future__ import annotations import os -from typing import overload from typing_extensions import Final