Skip to content

Commit 5d73bdb

Browse files
committed
feat(build): match local Python version by default
flash build/deploy now resolve the app Python version from the local interpreter (sys.version_info) when no --python-version override and no per-resource python_version is declared, instead of a hardcoded 3.12 fallback. An unsupported local interpreter raises an actionable error. The build prints the resolved version and its source. Extracted from #330 (which is stale/corrupted and built on the reverted #346); the 'add 3.13' half of #330 already landed on main, so only the match-local behavior is carried here. - manifest.py: _reconcile_python_version falls back to local interpreter - build.py: _python_version_source() + build-time 'targeting Python X' line - docs: flash-build/flash-deploy/deploy-guide describe parity-by-default - tests: local-match, unsupported-local, override/declaration precedence
1 parent ffd4cb6 commit 5d73bdb

7 files changed

Lines changed: 154 additions & 24 deletions

File tree

docs/Flash_Deploy_Guide.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ This guide walks through deploying a Flash application from local development to
1212

1313
### Python version selection
1414

15-
Flash apps ship as a single tarball, so every resource in an app shares one Python version. The worker runtime defaults to 3.12 (the version torch is pre-installed for in the GPU base image). Select a different version in two ways:
15+
Flash apps ship as a single tarball, so every resource in an app shares one Python version. By default, `flash build` and `flash deploy` target the Python version you're running flash from — if you're on 3.11 locally, your deploy runs on 3.11; on 3.13, it runs on 3.13. The build prints the resolved version and its source. If your local interpreter is outside the supported set (`3.10``3.13`), the build fails with an actionable error. Override the default in two ways:
1616

17-
- **Per-resource declaration**: set `python_version="3.11"` on any resource config — all resources in the same app must agree or leave it unset.
18-
- **App-level override**: pass `--python-version 3.11` to `flash build` or `flash deploy`. The override wins over per-resource values that are unset and must match any that are set.
17+
- **Per-resource declaration**: set `python_version="3.11"` on any resource config — all resources in the same app must agree or leave it unset. For projects shared across a team or CI, declaring it explicitly makes the deploy result identical regardless of who runs `flash build` and which interpreter they have.
18+
- **App-level override**: pass `--python-version 3.11` to `flash build` or `flash deploy`. The override sets the target when no resource declares one and must match any that are set (a conflict raises).
1919

2020
| Version | Status | GPU cold-start | Notes |
2121
|---------|--------|----------------|-------|

src/runpod_flash/cli/commands/build.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,28 @@ def _resolve_pip_python_version(manifest: dict) -> str | None:
212212
return max(versions)
213213

214214

215+
def _python_version_source(override: str | None, resources_dict: dict) -> str:
216+
"""Return a human-readable source string for the resolved Python version.
217+
218+
Used at build time to surface where the resolved version came from:
219+
explicit override, per-resource declaration, or local interpreter match.
220+
"""
221+
if override:
222+
return "--python-version override"
223+
declared = {
224+
name: r["python_version"]
225+
for name, r in resources_dict.items()
226+
if r.get("python_version")
227+
}
228+
if declared:
229+
# All declared values are identical at this point — reconcile would
230+
# have raised otherwise — so report the lexicographically-first
231+
# resource name for stability.
232+
name = next(iter(sorted(declared)))
233+
return f"declared on resource {name}"
234+
return "matched local interpreter"
235+
236+
215237
def run_build(
216238
project_dir: Path,
217239
app_name: str,
@@ -288,6 +310,10 @@ def run_build(
288310
python_version=manifest_python_version_override,
289311
)
290312
manifest = manifest_builder.build()
313+
console.print(
314+
f"[dim]targeting Python {manifest_builder.python_version} "
315+
f"({_python_version_source(manifest_python_version_override, manifest.get('resources', {}))})[/dim]"
316+
)
291317
manifest["source_fingerprint"] = compute_source_fingerprint(
292318
project_dir, files
293319
)

src/runpod_flash/cli/commands/build_utils/manifest.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Dict, List, Optional
99

1010
from runpod_flash.core.resources.constants import (
11-
DEFAULT_PYTHON_VERSION,
11+
SUPPORTED_PYTHON_VERSIONS,
1212
validate_python_version,
1313
)
1414

@@ -284,18 +284,23 @@ def _extract_config_properties(config: Dict[str, Any], resource_config) -> None:
284284
def _reconcile_python_version(
285285
self, resources_dict: Dict[str, Dict[str, Any]]
286286
) -> str:
287-
"""Pick one Python version for the app from per-resource declarations.
287+
"""Pick one Python version for the app.
288288
289289
Flash apps ship as a single tarball, so every resource must target the
290290
same Python ABI. Resolution order:
291291
1. Explicit override passed to ManifestBuilder (validated)
292292
2. Exactly one distinct ``python_version`` declared across resources
293-
3. ``DEFAULT_PYTHON_VERSION`` when no resource declares one
293+
3. The local interpreter (``sys.version_info``) — the user's
294+
environment is the source of truth when nothing else is declared.
295+
296+
There is no fallback to a hardcoded default. A local interpreter
297+
outside ``SUPPORTED_PYTHON_VERSIONS`` raises an actionable error.
294298
295299
Raises:
296-
ValueError: When resources declare conflicting ``python_version``
297-
values, or when the override conflicts with a resource's
298-
explicit declaration.
300+
ValueError: when resources declare conflicting ``python_version``
301+
values; when the override conflicts with a resource's explicit
302+
declaration; or when the local interpreter is unsupported and
303+
no override or declaration was provided.
299304
"""
300305
per_resource: Dict[str, str] = {
301306
name: r["python_version"]
@@ -329,14 +334,24 @@ def _reconcile_python_version(
329334
raise ValueError(
330335
"Flash apps require one python_version across all resources "
331336
f"(found {sorted(distinct)}): {details}. Set python_version to the "
332-
"same value on every resource, or omit it to use the default "
333-
f"({DEFAULT_PYTHON_VERSION})."
337+
"same value on every resource, pass --python-version, or run "
338+
"flash from a single-version interpreter."
334339
)
335340

336341
if distinct:
337342
return validate_python_version(next(iter(distinct)))
338343

339-
return DEFAULT_PYTHON_VERSION
344+
# Match the user's local interpreter — parity, not policy.
345+
local = f"{sys.version_info.major}.{sys.version_info.minor}"
346+
if local not in SUPPORTED_PYTHON_VERSIONS:
347+
supported = ", ".join(SUPPORTED_PYTHON_VERSIONS)
348+
raise ValueError(
349+
f"Local Python {local} is not supported by Flash workers "
350+
f"(supported: {supported}). Pass --python-version, declare "
351+
f"python_version on a resource config, or run flash from a "
352+
f"supported interpreter."
353+
)
354+
return local
340355

341356
def build(self) -> Dict[str, Any]:
342357
"""Build the manifest dictionary.

src/runpod_flash/cli/docs/flash-build.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ flash build [OPTIONS]
2828
- `--no-deps`: Skip transitive dependencies during pip install (default: false)
2929
- `--output, -o`: Custom archive name (default: artifact.tar.gz)
3030
- `--exclude`: Comma-separated packages to exclude (e.g., 'torch,torchvision')
31-
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, or `3.12`). Overrides per-resource `python_version`. Default: value declared on resource configs, or 3.12 if none set.
31+
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, `3.12`, or `3.13`). Overrides the local-interpreter default; must match any per-resource `python_version` declarations (conflicting declarations raise). By default, `flash build` targets the Python version you're running flash from.
3232

3333
To launch a local preview environment, use `flash deploy --preview` instead.
3434

@@ -68,7 +68,7 @@ After `flash build` completes:
6868
Flash automatically handles cross-platform builds, ensuring compatibility with Runpod's Linux x86_64 serverless infrastructure:
6969

7070
- **Automatic Platform Targeting**: Dependencies are always installed for Linux x86_64, regardless of your build platform (macOS, Windows, or Linux)
71-
- **Python Version**: Targets Python 3.12 for wheel ABI selection regardless of local interpreter
71+
- **Python Version**: Targets the resolved Python version (your local interpreter by default, or whatever `--python-version` / per-resource `python_version` selects) for wheel ABI selection
7272
- **Binary Wheel Enforcement**: Only pre-built binary wheels are used, preventing platform-specific compilation issues
7373

7474
This means you can safely build on macOS ARM64, Windows, or any platform, and the deployment will work correctly on Runpod.
@@ -181,8 +181,8 @@ ls .flash/.build/my-project/
181181
If a package doesn't have pre-built Linux x86_64 wheels:
182182

183183
1. **Install standard pip**: `python -m ensurepip --upgrade` -- standard pip has better manylinux compatibility than uv pip
184-
2. **Check package availability**: Visit PyPI and verify the package has Linux wheels for Python 3.12
185-
3. **Python 3.12**: All flash workers run Python 3.12. Ensure packages are available for this version.
184+
2. **Check package availability**: Visit PyPI and verify the package has Linux wheels for your target Python version (`3.10`, `3.11`, `3.12`, or `3.13`)
185+
3. **Match interpreter**: Flash builds default to your local Python version. If a wheel is missing for that version, either pick a different `--python-version` or upgrade/downgrade the package.
186186
4. **Pure-Python packages**: These work regardless, as they don't require platform-specific builds
187187

188188
## Managing Deployment Size

src/runpod_flash/cli/docs/flash-deploy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ flash deploy [OPTIONS]
8888
- `--exclude`: Comma-separated packages to exclude (e.g., 'torch,torchvision')
8989
- `--output, -o`: Custom archive name (default: artifact.tar.gz)
9090
- `--preview`: Build and launch local preview environment instead of deploying
91-
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, or `3.12`). Overrides per-resource `python_version`.
91+
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, `3.12`, or `3.13`). Overrides the local-interpreter default; must match any per-resource `python_version` declarations (conflicting declarations raise). By default, `flash deploy` targets the Python version you're running flash from.
9292

9393
## Examples
9494

tests/unit/cli/commands/build_utils/test_manifest.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -910,13 +910,13 @@ def test_manifest_includes_python_version():
910910
)
911911
]
912912

913-
builder = ManifestBuilder("test_app", functions)
913+
# Explicit override avoids depending on the runner's local interpreter,
914+
# which is the new resolution default after AE-2827.
915+
builder = ManifestBuilder("test_app", functions, python_version="3.12")
914916
manifest = builder.build()
915917

916918
assert "python_version" in manifest
917-
from runpod_flash.core.resources.constants import DEFAULT_PYTHON_VERSION
918-
919-
assert manifest["python_version"] == DEFAULT_PYTHON_VERSION
919+
assert manifest["python_version"] == "3.12"
920920

921921

922922
def test_manifest_uses_explicit_python_version():
@@ -966,13 +966,21 @@ class TestReconcilePythonVersion:
966966
def _builder(self, python_version: Optional[str] = None) -> ManifestBuilder:
967967
return ManifestBuilder("test_app", [], python_version=python_version)
968968

969-
def test_no_resources_declare_version_uses_default(self):
970-
from runpod_flash.core.resources.constants import DEFAULT_PYTHON_VERSION
969+
def test_no_resources_no_override_uses_local_interpreter(self, monkeypatch):
970+
"""With no override and no declaration, reconcile reads sys.version_info."""
971+
972+
class _StubVersionInfo:
973+
pass
974+
975+
info = _StubVersionInfo()
976+
info.major = 3
977+
info.minor = 11
978+
monkeypatch.setattr(sys, "version_info", info)
971979

972980
resolved = self._builder()._reconcile_python_version(
973981
_make_resources_dict(gpu=None, cpu=None)
974982
)
975-
assert resolved == DEFAULT_PYTHON_VERSION
983+
assert resolved == "3.11"
976984

977985
def test_single_declared_version_wins(self):
978986
resolved = self._builder()._reconcile_python_version(
@@ -1025,3 +1033,62 @@ def test_reconcile_python_version_accepts_3_13_declaration(self):
10251033
_make_resources_dict(gpu="3.13")
10261034
)
10271035
assert resolved == "3.13"
1036+
1037+
1038+
class TestReconcileLocalInterpreter:
1039+
"""Tests for AE-2827 match-local default in _reconcile_python_version."""
1040+
1041+
def _builder(self, python_version: Optional[str] = None) -> ManifestBuilder:
1042+
return ManifestBuilder("test_app", [], python_version=python_version)
1043+
1044+
def _stub_version_info(self, monkeypatch, major: int, minor: int):
1045+
class _StubVersionInfo:
1046+
pass
1047+
1048+
info = _StubVersionInfo()
1049+
info.major = major
1050+
info.minor = minor
1051+
monkeypatch.setattr(sys, "version_info", info)
1052+
1053+
@pytest.mark.parametrize(
1054+
"major,minor,expected",
1055+
[
1056+
(3, 10, "3.10"),
1057+
(3, 11, "3.11"),
1058+
(3, 12, "3.12"),
1059+
(3, 13, "3.13"),
1060+
],
1061+
)
1062+
def test_resolves_to_local_when_no_override_or_declaration(
1063+
self, monkeypatch, major, minor, expected
1064+
):
1065+
self._stub_version_info(monkeypatch, major, minor)
1066+
resolved = self._builder()._reconcile_python_version(
1067+
_make_resources_dict(gpu=None, cpu=None)
1068+
)
1069+
assert resolved == expected
1070+
1071+
@pytest.mark.parametrize("major,minor", [(3, 9), (3, 14)])
1072+
def test_raises_when_local_is_unsupported(self, monkeypatch, major, minor):
1073+
self._stub_version_info(monkeypatch, major, minor)
1074+
with pytest.raises(ValueError, match="Local Python") as excinfo:
1075+
self._builder()._reconcile_python_version(
1076+
_make_resources_dict(gpu=None, cpu=None)
1077+
)
1078+
message = str(excinfo.value)
1079+
assert f"{major}.{minor}" in message
1080+
assert "--python-version" in message
1081+
1082+
def test_local_does_not_shadow_override(self, monkeypatch):
1083+
self._stub_version_info(monkeypatch, 3, 10)
1084+
resolved = self._builder("3.12")._reconcile_python_version(
1085+
_make_resources_dict(gpu=None, cpu=None)
1086+
)
1087+
assert resolved == "3.12"
1088+
1089+
def test_local_does_not_shadow_declaration(self, monkeypatch):
1090+
self._stub_version_info(monkeypatch, 3, 13)
1091+
resolved = self._builder()._reconcile_python_version(
1092+
_make_resources_dict(gpu="3.11", cpu=None)
1093+
)
1094+
assert resolved == "3.11"

tests/unit/cli/commands/test_build_helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from runpod_flash.cli.commands.build import (
99
_bundle_runpod_flash,
1010
_find_runpod_flash,
11+
_python_version_source,
1112
_remove_runpod_flash_from_requirements,
1213
)
1314

@@ -191,3 +192,24 @@ def test_find_spec_returns_spec_without_origin(self, tmp_path):
191192

192193
# No flash repo in tmp_path, so returns None
193194
assert result is None
195+
196+
197+
class TestPythonVersionSource:
198+
"""Tests for _python_version_source (build-time provenance string)."""
199+
200+
def test_override_takes_precedence(self):
201+
assert (
202+
_python_version_source("3.12", {"gpu": {"python_version": "3.11"}})
203+
== "--python-version override"
204+
)
205+
206+
def test_declared_resource_reported_by_first_name(self):
207+
resources = {
208+
"zeta": {"python_version": "3.11"},
209+
"alpha": {"python_version": "3.11"},
210+
}
211+
assert _python_version_source(None, resources) == "declared on resource alpha"
212+
213+
def test_no_override_or_declaration_reports_local_match(self):
214+
resources = {"gpu": {}, "cpu": {"python_version": None}}
215+
assert _python_version_source(None, resources) == "matched local interpreter"

0 commit comments

Comments
 (0)