From 52e408c250fd3df665f5432a09251a45c698250d Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Wed, 28 Oct 2026 12:06:20 -0400 Subject: [PATCH 1/8] fix: match patch releases for minor Python overrides --- rsconnect/environment.py | 3 ++- rsconnect/validation.py | 4 +--- tests/test_environment.py | 2 +- tests/test_main.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/rsconnect/environment.py b/rsconnect/environment.py index 85bd1037..4836ef8e 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -159,7 +159,8 @@ def create_python_environment( "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - python_version_requirement = f"=={override_python_version}" + suffix = ".*" if len(override_python_version.split(".")) == 2 else "" + python_version_requirement = f"=={override_python_version}{suffix}" # with cli_feedback("Inspecting Python environment"): environment = cls._get_python_env_info(module_file, python, requirements_file=requirements_file) diff --git a/rsconnect/validation.py b/rsconnect/validation.py index 6cac906b..ff1e5d9a 100644 --- a/rsconnect/validation.py +++ b/rsconnect/validation.py @@ -430,10 +430,8 @@ class PythonVersionParamType(click.ParamType): def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]): try: parts = list(map(int, value.split("."))) - if len(parts) == 3: + if len(parts) in (2, 3): return value - elif len(parts) == 2: - return value + ".0" else: raise ValueError except (AttributeError, ValueError): diff --git a/tests/test_environment.py b/tests/test_environment.py index cbf364b9..ac252cab 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -453,7 +453,7 @@ def test_override_python_version(self): "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - assert result.python_version_requirement == "==3.8" + assert result.python_version_requirement == "==3.8.*" def test_python_interpreter(self): current_python_version = ".".join((str(v) for v in sys.version_info[:3])) diff --git a/tests/test_main.py b/tests/test_main.py index 94a59c2f..a2a6c60c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -12,6 +12,7 @@ import httpretty import pytest from click.testing import CliRunner +from packaging.specifiers import SpecifierSet from rsconnect import VERSION from rsconnect.api import RSConnectClient, RSConnectServer @@ -2154,6 +2155,34 @@ def test_no_package_json(self, tmp_path): class TestWriteManifestFramework: + @pytest.mark.parametrize( + "version, expected_requirement, matching, nonmatching", + [ + ("3.11", "==3.11.*", "3.11.15", "3.12.0"), + ("3.11.0", "==3.11.0", "3.11.0", "3.11.15"), + ("3.11.15", "==3.11.15", "3.11.15", "3.11.14"), + ], + ) + def test_override_python_version(self, tmp_path, version, expected_requirement, matching, nonmatching): + (tmp_path / "app.py").write_text("app = object()\n") + (tmp_path / "requirements.txt").write_text("flask\n") + + result = CliRunner().invoke( + cli, + [ + "write-manifest", "api", "--entrypoint", "app:app", "--exclude-renv", + "--override-python-version", version, str(tmp_path), + ], + ) + assert result.exit_code == 0, result.output + + manifest = json.loads((tmp_path / "manifest.json").read_text()) + assert manifest["python"]["version"] == version + requirement = manifest["environment"]["python"]["requires"] + assert requirement == expected_requirement + assert matching in SpecifierSet(requirement) + assert nonmatching not in SpecifierSet(requirement) + @pytest.mark.parametrize("existing_requirements", [False, True]) def test_force_generate_requirements(self, tmp_path, existing_requirements): (tmp_path / "app.py").write_text("app = object()\n") From a3a7fcb67a1927e773a65dbdff965d7e0f754bdd Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Wed, 28 Oct 2026 12:46:01 -0400 Subject: [PATCH 2/8] style: format Python override regression test --- tests/test_main.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index a2a6c60c..4851bbff 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2170,8 +2170,14 @@ def test_override_python_version(self, tmp_path, version, expected_requirement, result = CliRunner().invoke( cli, [ - "write-manifest", "api", "--entrypoint", "app:app", "--exclude-renv", - "--override-python-version", version, str(tmp_path), + "write-manifest", + "api", + "--entrypoint", + "app:app", + "--exclude-renv", + "--override-python-version", + version, + str(tmp_path), ], ) assert result.exit_code == 0, result.output From 644a4ac0a0ad0f0ac15c04441bd73f534a1c9d09 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Thu, 29 Oct 2026 12:56:02 -0400 Subject: [PATCH 3/8] test: exercise Python overrides against Connect --- tests/test_main.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 4d52da0f..356fc6d1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1622,6 +1622,38 @@ def test_deploy_api(self): result = runner.invoke(cli, args) assert result.exit_code == 0, result.output + @pytest.mark.parametrize("override_kind", ["minor", "patch"]) + def test_deploy_api_with_python_override(self, tmp_path, override_kind): + require_connect_version("2025.03.0") + client = RSConnectClient(RSConnectServer(require_connect(), require_api_key())) + installations = client.python_settings()["installations"] + installed = {item["version"] for item in installations} + candidates = [] + for item in installations: + version = item["version"] + match = re.fullmatch(r"(3)\.(\d+)\.(\d+)", version) + if not match or item.get("publishable") is False: + continue + major, minor, patch = map(int, match.groups()) + if minor >= 8 and patch > 0 and f"{major}.{minor}.0" not in installed: + candidates.append((major, minor, patch)) + + if not candidates: + pytest.skip("Connect needs a publishable Python >= 3.8 with a nonzero patch and no matching x.y.0") + + major, minor, patch = max(candidates) + version = f"{major}.{minor}" + if override_kind == "patch": + version += f".{patch}" + + source = get_api_path("flask") + shutil.copy(join(source, "app.py"), tmp_path) + shutil.copy(join(source, "requirements.txt"), tmp_path) + args = self.create_deploy_args("api", str(tmp_path)) + args.extend(["--entrypoint", "app:app", "--exclude-renv", "--override-python-version", version]) + result = CliRunner().invoke(cli, args) + assert result.exit_code == 0, result.output + def test_deploy_api_fail_verify(self): target = optional_target(get_api_path("flask-bad")) runner = CliRunner() From 20e01b06b7d388a3ba950f103d9b20485e3a2d82 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Thu, 29 Oct 2026 13:04:13 -0400 Subject: [PATCH 4/8] fix: require patch versions for Python overrides --- docs/deploying.md | 2 ++ rsconnect/environment.py | 3 +-- rsconnect/validation.py | 8 ++++++-- tests/test_environment.py | 4 ++-- tests/test_main.py | 24 +++++++++++++++--------- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/deploying.md b/docs/deploying.md index 7e5291ad..8f0c4372 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -232,6 +232,8 @@ rsconnect deploy api --override-python-version 3.11.5 my-api/ will deploy the content in `my-api` while requesting that Connect use Python version 3.11.5. +The `--override-python-version` option requires a full version, including the +patch component; for example, use `3.11.5` rather than `3.11`. > **Note** > The packages and package versions listed in `requirements.txt` must be diff --git a/rsconnect/environment.py b/rsconnect/environment.py index 0243ae03..f9dad6d3 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -157,8 +157,7 @@ def create_python_environment( "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - suffix = ".*" if len(override_python_version.split(".")) == 2 else "" - python_version_requirement = f"=={override_python_version}{suffix}" + python_version_requirement = f"=={override_python_version}" # with cli_feedback("Inspecting Python environment"): environment = cls._get_python_env_info(module_file, python, requirements_file=requirements_file) diff --git a/rsconnect/validation.py b/rsconnect/validation.py index ff1e5d9a..7a6400de 100644 --- a/rsconnect/validation.py +++ b/rsconnect/validation.py @@ -430,12 +430,16 @@ class PythonVersionParamType(click.ParamType): def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]): try: parts = list(map(int, value.split("."))) - if len(parts) in (2, 3): + if len(parts) == 3: return value else: raise ValueError except (AttributeError, ValueError): - self.fail(f"{value!r} is not a valid python version; expected 3.x or 3.x.y", param, ctx) + self.fail( + f"{value!r} is not a valid python version; expected major.minor.patch (for example, 3.11.15)", + param, + ctx, + ) PYTHON_VERSION = PythonVersionParamType() diff --git a/tests/test_environment.py b/tests/test_environment.py index 23de8687..dd1ddfb2 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -504,13 +504,13 @@ def test_override_python_version(self): assert result.python_version_requirement is None with mock.patch.object(rsconnect.environment.logger, "warning") as mock_warning: - result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version="3.8") + result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version="3.8.1") assert mock_warning.call_count == 1 mock_warning.assert_called_once_with( "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - assert result.python_version_requirement == "==3.8.*" + assert result.python_version_requirement == "==3.8.1" def test_python_interpreter(self): current_python_version = ".".join((str(v) for v in sys.version_info[:3])) diff --git a/tests/test_main.py b/tests/test_main.py index 356fc6d1..e33c0413 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1622,12 +1622,10 @@ def test_deploy_api(self): result = runner.invoke(cli, args) assert result.exit_code == 0, result.output - @pytest.mark.parametrize("override_kind", ["minor", "patch"]) - def test_deploy_api_with_python_override(self, tmp_path, override_kind): + def test_deploy_api_with_python_override(self, tmp_path): require_connect_version("2025.03.0") client = RSConnectClient(RSConnectServer(require_connect(), require_api_key())) installations = client.python_settings()["installations"] - installed = {item["version"] for item in installations} candidates = [] for item in installations: version = item["version"] @@ -1635,16 +1633,14 @@ def test_deploy_api_with_python_override(self, tmp_path, override_kind): if not match or item.get("publishable") is False: continue major, minor, patch = map(int, match.groups()) - if minor >= 8 and patch > 0 and f"{major}.{minor}.0" not in installed: + if minor >= 8 and patch > 0: candidates.append((major, minor, patch)) if not candidates: - pytest.skip("Connect needs a publishable Python >= 3.8 with a nonzero patch and no matching x.y.0") + pytest.skip("Connect needs a publishable Python >= 3.8 with a nonzero patch") major, minor, patch = max(candidates) - version = f"{major}.{minor}" - if override_kind == "patch": - version += f".{patch}" + version = f"{major}.{minor}.{patch}" source = get_api_path("flask") shutil.copy(join(source, "app.py"), tmp_path) @@ -1654,6 +1650,11 @@ def test_deploy_api_with_python_override(self, tmp_path, override_kind): result = CliRunner().invoke(cli, args) assert result.exit_code == 0, result.output + def test_deploy_api_rejects_minor_python_override(self, tmp_path): + result = CliRunner().invoke(cli, ["deploy", "api", "--override-python-version", "3.11", str(tmp_path)]) + assert result.exit_code != 0 + assert "expected major.minor.patch" in result.output + def test_deploy_api_fail_verify(self): target = optional_target(get_api_path("flask-bad")) runner = CliRunner() @@ -2279,7 +2280,6 @@ class TestWriteManifestFramework: @pytest.mark.parametrize( "version, expected_requirement, matching, nonmatching", [ - ("3.11", "==3.11.*", "3.11.15", "3.12.0"), ("3.11.0", "==3.11.0", "3.11.0", "3.11.15"), ("3.11.15", "==3.11.15", "3.11.15", "3.11.14"), ], @@ -2310,6 +2310,12 @@ def test_override_python_version(self, tmp_path, version, expected_requirement, assert matching in SpecifierSet(requirement) assert nonmatching not in SpecifierSet(requirement) + def test_minor_python_override_is_rejected(self, tmp_path): + result = CliRunner().invoke(cli, ["write-manifest", "api", "--override-python-version", "3.11", str(tmp_path)]) + assert result.exit_code != 0 + assert "expected major.minor.patch" in result.output + assert not (tmp_path / "manifest.json").exists() + def test_dangling_symlink_reports_clear_error(self, tmp_path): (tmp_path / "app.py").write_text("app = object()\n") (tmp_path / "requirements.txt").write_text("flask\n") From 4157300c32fac23a88001c57a1658160d65f9224 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Thu, 29 Oct 2026 13:25:00 -0400 Subject: [PATCH 5/8] fix: support minor-only Python version overrides --- docs/deploying.md | 7 +++++-- rsconnect/environment.py | 11 +++++++++-- rsconnect/validation.py | 4 ++-- tests/test_environment.py | 8 ++++++++ tests/test_main.py | 30 ++++++++++++++++++------------ 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/docs/deploying.md b/docs/deploying.md index 8f0c4372..7a0be04e 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -232,8 +232,11 @@ rsconnect deploy api --override-python-version 3.11.5 my-api/ will deploy the content in `my-api` while requesting that Connect use Python version 3.11.5. -The `--override-python-version` option requires a full version, including the -patch component; for example, use `3.11.5` rather than `3.11`. +On modern Connect versions, a major and minor version such as `3.11` requests +the latest available patch release in that series. The manifest records the +local Python version separately for compatibility. Older Connect versions that +do not support version constraints use that local version instead, so specify a +full version such as `3.11.5` when deploying to those servers. > **Note** > The packages and package versions listed in `requirements.txt` must be diff --git a/rsconnect/environment.py b/rsconnect/environment.py index f9dad6d3..89730bad 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -157,13 +157,20 @@ def create_python_environment( "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - python_version_requirement = f"=={override_python_version}" + suffix = ".*" if len(override_python_version.split(".")) == 2 else "" + if suffix: + logger.warning( + "Older Posit Connect versions without Python version constraint support will use the " + "local Python version instead of the minor-only override. Specify a full patch version " + "for those servers." + ) + python_version_requirement = f"=={override_python_version}{suffix}" # with cli_feedback("Inspecting Python environment"): environment = cls._get_python_env_info(module_file, python, requirements_file=requirements_file) environment.python_version_requirement = python_version_requirement - if override_python_version: + if override_python_version and len(override_python_version.split(".")) == 3: # Retaing backward compatibility with old Connect versions # that didn't support environment.python.requires environment.python = override_python_version diff --git a/rsconnect/validation.py b/rsconnect/validation.py index 7a6400de..f8689789 100644 --- a/rsconnect/validation.py +++ b/rsconnect/validation.py @@ -430,13 +430,13 @@ class PythonVersionParamType(click.ParamType): def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]): try: parts = list(map(int, value.split("."))) - if len(parts) == 3: + if len(parts) in (2, 3): return value else: raise ValueError except (AttributeError, ValueError): self.fail( - f"{value!r} is not a valid python version; expected major.minor.patch (for example, 3.11.15)", + f"{value!r} is not a valid python version; expected major.minor or major.minor.patch", param, ctx, ) diff --git a/tests/test_environment.py b/tests/test_environment.py index dd1ddfb2..f7498e6f 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -512,6 +512,14 @@ def test_override_python_version(self): ) assert result.python_version_requirement == "==3.8.1" + def test_minor_python_override_warns_about_legacy_connect(self): + with mock.patch.object(rsconnect.environment.logger, "warning") as mock_warning: + result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version="3.8") + + assert result.python == ".".join(str(part) for part in sys.version_info[:3]) + assert result.python_version_requirement == "==3.8.*" + assert any("local Python version instead" in call.args[0] for call in mock_warning.call_args_list) + def test_python_interpreter(self): current_python_version = ".".join((str(v) for v in sys.version_info[:3])) diff --git a/tests/test_main.py b/tests/test_main.py index e33c0413..3f0739f9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1622,7 +1622,8 @@ def test_deploy_api(self): result = runner.invoke(cli, args) assert result.exit_code == 0, result.output - def test_deploy_api_with_python_override(self, tmp_path): + @pytest.mark.parametrize("minor_only", [False, True]) + def test_deploy_api_with_python_override(self, tmp_path, minor_only): require_connect_version("2025.03.0") client = RSConnectClient(RSConnectServer(require_connect(), require_api_key())) installations = client.python_settings()["installations"] @@ -1640,21 +1641,24 @@ def test_deploy_api_with_python_override(self, tmp_path): pytest.skip("Connect needs a publishable Python >= 3.8 with a nonzero patch") major, minor, patch = max(candidates) - version = f"{major}.{minor}.{patch}" + version = f"{major}.{minor}" if minor_only else f"{major}.{minor}.{patch}" source = get_api_path("flask") - shutil.copy(join(source, "app.py"), tmp_path) + (tmp_path / "app.py").write_text( + "import sys\n" + "from flask import Flask\n" + f"assert sys.version_info[:3] == ({major}, {minor}, {patch}), sys.version\n" + "app = Flask(__name__)\n" + "@app.route('/ping')\n" + "def ping():\n" + " return 'ok'\n" + ) shutil.copy(join(source, "requirements.txt"), tmp_path) args = self.create_deploy_args("api", str(tmp_path)) args.extend(["--entrypoint", "app:app", "--exclude-renv", "--override-python-version", version]) result = CliRunner().invoke(cli, args) assert result.exit_code == 0, result.output - def test_deploy_api_rejects_minor_python_override(self, tmp_path): - result = CliRunner().invoke(cli, ["deploy", "api", "--override-python-version", "3.11", str(tmp_path)]) - assert result.exit_code != 0 - assert "expected major.minor.patch" in result.output - def test_deploy_api_fail_verify(self): target = optional_target(get_api_path("flask-bad")) runner = CliRunner() @@ -2280,6 +2284,7 @@ class TestWriteManifestFramework: @pytest.mark.parametrize( "version, expected_requirement, matching, nonmatching", [ + ("3.11", "==3.11.*", "3.11.15", "3.12.0"), ("3.11.0", "==3.11.0", "3.11.0", "3.11.15"), ("3.11.15", "==3.11.15", "3.11.15", "3.11.14"), ], @@ -2304,16 +2309,17 @@ def test_override_python_version(self, tmp_path, version, expected_requirement, assert result.exit_code == 0, result.output manifest = json.loads((tmp_path / "manifest.json").read_text()) - assert manifest["python"]["version"] == version + expected_version = ".".join(str(part) for part in sys.version_info[:3]) if version == "3.11" else version + assert manifest["python"]["version"] == expected_version requirement = manifest["environment"]["python"]["requires"] assert requirement == expected_requirement assert matching in SpecifierSet(requirement) assert nonmatching not in SpecifierSet(requirement) - def test_minor_python_override_is_rejected(self, tmp_path): - result = CliRunner().invoke(cli, ["write-manifest", "api", "--override-python-version", "3.11", str(tmp_path)]) + def test_invalid_python_override_is_rejected(self, tmp_path): + result = CliRunner().invoke(cli, ["write-manifest", "api", "--override-python-version", "3", str(tmp_path)]) assert result.exit_code != 0 - assert "expected major.minor.patch" in result.output + assert "expected major.minor or major.minor.patch" in result.output assert not (tmp_path / "manifest.json").exists() def test_dangling_symlink_reports_clear_error(self, tmp_path): From 979a270881275ab3c67f1658f90e3986c91680a1 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Wed, 4 Nov 2026 13:52:00 -0500 Subject: [PATCH 6/8] refactor: simplify Python override version checks --- rsconnect/environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsconnect/environment.py b/rsconnect/environment.py index 89730bad..bcc8d936 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -157,7 +157,7 @@ def create_python_environment( "The --override-python-version option is deprecated, " "please use a .python-version file to force a specific interpreter version." ) - suffix = ".*" if len(override_python_version.split(".")) == 2 else "" + suffix = ".*" if override_python_version.count(".") == 1 else "" if suffix: logger.warning( "Older Posit Connect versions without Python version constraint support will use the " @@ -170,7 +170,7 @@ def create_python_environment( environment = cls._get_python_env_info(module_file, python, requirements_file=requirements_file) environment.python_version_requirement = python_version_requirement - if override_python_version and len(override_python_version.split(".")) == 3: + if override_python_version and override_python_version.count(".") == 2: # Retaing backward compatibility with old Connect versions # that didn't support environment.python.requires environment.python = override_python_version From 5105cb219051960490345a617060c2c4d670c173 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Fri, 6 Nov 2026 12:18:30 -0500 Subject: [PATCH 7/8] fix: clarify Connect version boundary for Python overrides --- docs/deploying.md | 10 +++++----- rsconnect/environment.py | 2 +- tests/test_environment.py | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/deploying.md b/docs/deploying.md index dea7e81e..9f2d62ac 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -243,11 +243,11 @@ rsconnect deploy api --override-python-version 3.11.5 my-api/ will deploy the content in `my-api` while requesting that Connect use Python version 3.11.5. -On modern Connect versions, a major and minor version such as `3.11` requests -the latest available patch release in that series. The manifest records the -local Python version separately for compatibility. Older Connect versions that -do not support version constraints use that local version instead, so specify a -full version such as `3.11.5` when deploying to those servers. +On Connect 2025.03.0 and later, a major and minor version such as `3.11` +requests the latest available patch release in that series. The manifest records +the local Python version separately for compatibility. Connect versions before +2025.03.0 use that local version instead, so specify a full version such as +`3.11.5` when deploying to those servers. > **Note** > The packages and package versions listed in `requirements.txt` must be diff --git a/rsconnect/environment.py b/rsconnect/environment.py index 2d1b80e7..f1c2e39e 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -154,7 +154,7 @@ def create_python_environment( suffix = ".*" if override_python_version.count(".") == 1 else "" if suffix: logger.warning( - "Older Posit Connect versions without Python version constraint support will use the " + "Posit Connect versions before 2025.03.0 will use the " "local Python version instead of the minor-only override. Specify a full patch version " "for those servers." ) diff --git a/tests/test_environment.py b/tests/test_environment.py index 7f7a1f97..585367d3 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1067,7 +1067,10 @@ def test_minor_python_override_warns_about_legacy_connect(self): assert result.python == ".".join(str(part) for part in sys.version_info[:3]) assert result.python_version_requirement == "==3.8.*" - assert any("local Python version instead" in call.args[0] for call in mock_warning.call_args_list) + assert any( + "Posit Connect versions before 2025.03.0 will use the local Python version instead" in call.args[0] + for call in mock_warning.call_args_list + ) def test_python_interpreter(self): current_python_version = ".".join((str(v) for v in sys.version_info[:3])) From 454476cd047b39bbf54afb4b31f4e856734560dd Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Fri, 6 Nov 2026 12:29:03 -0500 Subject: [PATCH 8/8] fix: document Connect requirement for minor Python overrides Drop the runtime warning about pre-2025.03.0 Connect servers, which printed even when targeting newer servers. Document the requirement in the --override-python-version help and the changelog instead. Co-Authored-By: Claude Opus 5.5 --- docs/CHANGELOG.md | 4 ++++ rsconnect/environment.py | 6 ------ rsconnect/main.py | 40 +++++++++++++++++++++++++++++++-------- tests/test_environment.py | 8 ++++---- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 930d09ea..8abce0b2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `--override-python-version` with a major and minor version, such as `3.11`, + requests the latest Python patch release in that series instead of `3.11.0`. + This requires Posit Connect 2025.03.0 or later; older servers use the local + Python version, so pass a full version such as `3.11.5` for those. (#901) - Inspects activated or project virtual environments, including with `uv tool install`. Global installations now inspect activated environments. `rsconnect preflight` checks the same environment's Python version. Use `--python` to override selection. (#889) diff --git a/rsconnect/environment.py b/rsconnect/environment.py index f1c2e39e..fe71bd1d 100644 --- a/rsconnect/environment.py +++ b/rsconnect/environment.py @@ -152,12 +152,6 @@ def create_python_environment( "please use a .python-version file to force a specific interpreter version." ) suffix = ".*" if override_python_version.count(".") == 1 else "" - if suffix: - logger.warning( - "Posit Connect versions before 2025.03.0 will use the " - "local Python version instead of the minor-only override. Specify a full patch version " - "for those servers." - ) python_version_requirement = f"=={override_python_version}{suffix}" # with cli_feedback("Inspecting Python environment"): diff --git a/rsconnect/main.py b/rsconnect/main.py index e52863a8..2c1b3f7c 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -2144,7 +2144,10 @@ def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -2338,7 +2341,10 @@ def deploy_notebook( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -3035,7 +3041,10 @@ def deploy_git( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -3521,7 +3530,10 @@ def generate_deploy_python( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -3899,7 +3911,10 @@ def write_manifest(): @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -4019,7 +4034,10 @@ def write_manifest_notebook( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -4175,7 +4193,10 @@ def write_manifest_voila( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", @@ -4597,7 +4618,10 @@ def generate_write_manifest_python( @click.option( "--override-python-version", type=validation.PYTHON_VERSION, - help=("An optional python version to use instead of the version from the detected environment."), + help=( + "Python version Connect should use instead of the version from the detected environment, as " + "major.minor.patch or major.minor. major.minor requires Posit Connect 2025.03.0 or later." + ), ) @click.option( "--force-generate", diff --git a/tests/test_environment.py b/tests/test_environment.py index 585367d3..7c44b0df 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1061,15 +1061,15 @@ def test_override_python_version(self): ) assert result.python_version_requirement == "==3.8.1" - def test_minor_python_override_warns_about_legacy_connect(self): + def test_minor_python_override(self): with mock.patch.object(rsconnect.environment.logger, "warning") as mock_warning: result = Environment.create_python_environment(get_dir("pip1-no-version"), override_python_version="3.8") assert result.python == ".".join(str(part) for part in sys.version_info[:3]) assert result.python_version_requirement == "==3.8.*" - assert any( - "Posit Connect versions before 2025.03.0 will use the local Python version instead" in call.args[0] - for call in mock_warning.call_args_list + mock_warning.assert_called_once_with( + "The --override-python-version option is deprecated, " + "please use a .python-version file to force a specific interpreter version." ) def test_python_interpreter(self):