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
37 changes: 34 additions & 3 deletions src/pyfly/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,42 @@ def _run_with_uvicorn_reload(app_path: str, host: str, port: int, reload_dirs: l


def _read_port_from_config() -> int | None:
"""Read the application port from pyfly.yaml if available.
"""Resolve the application port the way the application itself does.

Spring ``server.port`` parity: reads ``pyfly.server.port`` (the former
``pyfly.web.port`` key was removed in v26.06.102).
Spring ``server.port`` parity: ``pyfly.server.port`` (the former ``pyfly.web.port``
key was removed in v26.06.102), resolved with the same precedence as ``Config``:

1. ``PYFLY_SERVER_PORT`` in the environment — the relaxed-binding override, and
also what ``-D server.port=…`` becomes after ``_build_launch_env``.
2. The merged configuration for the active profiles (``pyfly.yaml`` plus
``pyfly-{profile}.yaml`` overlays, in ``config/`` or the project root).
3. A raw read of ``pyfly.yaml`` if the loader cannot run.

Before, only step 3 existed: the CLI bound the base port while the application,
which reads the same key through ``Config``, believed the overridden one. The
``-D server.port=9000`` example in the ``--define`` help text was therefore inert.
"""
env_port = os.environ.get("PYFLY_SERVER_PORT")
if env_port is not None and env_port.strip():
try:
return int(env_port)
except ValueError:
raise click.BadParameter(
f"PYFLY_SERVER_PORT must be an integer, got {env_port!r}",
param_hint="PYFLY_SERVER_PORT / -D server.port",
) from None

profiles = [p.strip() for p in os.environ.get("PYFLY_PROFILES_ACTIVE", "").split(",") if p.strip()]
try:
from pyfly.core.config import Config

merged = Config.from_sources(Path.cwd(), active_profiles=profiles or None, load_defaults=False)
port = merged.get("pyfly.server.port")
if port is not None:
return int(port)
except Exception: # noqa: BLE001 - the loader is best effort here; fall back to the raw file
pass

import yaml

config_path = Path("pyfly.yaml")
Expand Down
68 changes: 68 additions & 0 deletions tests/cli/test_run_launch_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,71 @@ def test_bad_define_raises(self) -> None:

with pytest.raises(click.BadParameter):
_build_launch_env((), ("noequals",), (), debug=False)


class TestReadPortFromConfig:
"""``pyfly run`` must resolve the port the way the application does.

The raw ``pyfly.yaml`` read ignored the relaxed-binding override
``PYFLY_SERVER_PORT`` (which is also what ``-D server.port=…`` becomes) and the
profile overlays, so the CLI bound the base port while the app believed another.
"""

@pytest.fixture(autouse=True)
def _clean_env(self, monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: # type: ignore[no-untyped-def]
monkeypatch.delenv("PYFLY_SERVER_PORT", raising=False)
monkeypatch.delenv("PYFLY_PROFILES_ACTIVE", raising=False)
monkeypatch.chdir(tmp_path)

def test_no_config_file_means_no_port(self) -> None:
from pyfly.cli.run import _read_port_from_config

assert _read_port_from_config() is None

def test_reads_server_port_from_pyfly_yaml(self, tmp_path) -> None: # type: ignore[no-untyped-def]
from pyfly.cli.run import _read_port_from_config

(tmp_path / "pyfly.yaml").write_text("pyfly:\n server:\n port: 8085\n")
assert _read_port_from_config() == 8085

def test_env_override_wins_over_yaml(self, tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[no-untyped-def]
from pyfly.cli.run import _read_port_from_config

(tmp_path / "pyfly.yaml").write_text("pyfly:\n server:\n port: 8085\n")
monkeypatch.setenv("PYFLY_SERVER_PORT", "8090")
assert _read_port_from_config() == 8090

def test_env_override_works_without_yaml(self, monkeypatch: pytest.MonkeyPatch) -> None:
from pyfly.cli.run import _read_port_from_config

monkeypatch.setenv("PYFLY_SERVER_PORT", "8090")
assert _read_port_from_config() == 8090

def test_define_flag_reaches_the_port(self, tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[no-untyped-def]
"""``-D server.port=9000`` is what the CLI help advertises; it must bind 9000."""
import os

from pyfly.cli.run import _read_port_from_config

(tmp_path / "pyfly.yaml").write_text("pyfly:\n server:\n port: 8085\n")
for key, value in _build_launch_env((), ("server.port=9000",), (), debug=False).items():
monkeypatch.setenv(key, value)
assert os.environ["PYFLY_SERVER_PORT"] == "9000"
assert _read_port_from_config() == 9000

def test_profile_overlay_wins_over_base(self, tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: # type: ignore[no-untyped-def]
from pyfly.cli.run import _read_port_from_config

(tmp_path / "pyfly.yaml").write_text("pyfly:\n server:\n port: 8085\n")
(tmp_path / "pyfly-dev.yaml").write_text("pyfly:\n server:\n port: 8095\n")
monkeypatch.setenv("PYFLY_PROFILES_ACTIVE", "dev")
assert _read_port_from_config() == 8095

def test_bad_env_value_is_a_usage_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
import click

from pyfly.cli.run import _read_port_from_config

monkeypatch.setenv("PYFLY_SERVER_PORT", "eighty")
with pytest.raises(click.BadParameter):
_read_port_from_config()
Loading