From f06e5530f4bc22cb32beec2572a71da76a6ce1dd Mon Sep 17 00:00:00 2001 From: Pablo Caro Revuelta Date: Fri, 10 Jul 2026 11:37:36 +0200 Subject: [PATCH 1/2] fix(project): align bootstrapped extension deps with the runner (LITE-33583) Bootstrapping an extension produced an incoherent runner/connect-eaas-core pair, so the generated project could not run without manual fixes. The runner image was selected by the CLI's own major version (26, stale versus the real runner line 43) while the pyproject template hardcoded a connect-eaas-core ">=30" floor, letting `poetry update` resolve a version the runner image does not support. Select the latest published runner instead of filtering by the CLI major, and derive the connect-eaas-core specifier from that runner's PyPI requires_dist so the pair is always coherent and cannot drift again. The template now pins connect-eaas-core to the derived specifier, aligns python to the runner line (>=3.9,<3.13), and refreshes the stale dev-dependencies (pytest 8, flake8 >=6, pytest-asyncio >=0.23; drops the dead mock marker). --- .../plugins/project/extension/constants.py | 1 + .../cli/plugins/project/extension/helpers.py | 5 +- .../${project_slug}/pyproject.toml.j2 | 25 +++--- .../cli/plugins/project/extension/utils.py | 32 +++++-- .../plugins/project/test_extension_helpers.py | 37 +++++++- tests/plugins/project/test_extension_utils.py | 88 +++++++++++++------ 6 files changed, 139 insertions(+), 49 deletions(-) diff --git a/connect/cli/plugins/project/extension/constants.py b/connect/cli/plugins/project/extension/constants.py index a2557d9c..b67a0306 100644 --- a/connect/cli/plugins/project/extension/constants.py +++ b/connect/cli/plugins/project/extension/constants.py @@ -2,6 +2,7 @@ PYPI_EXTENSION_RUNNER_URL = 'https://pypi.org/pypi/connect-extension-runner/json' +PYPI_EXTENSION_RUNNER_RELEASE_URL = 'https://pypi.org/pypi/connect-extension-runner/{version}/json' PRE_COMMIT_HOOK = """#! /bin/sh diff --git a/connect/cli/plugins/project/extension/helpers.py b/connect/cli/plugins/project/extension/helpers.py index 2d98837b..3d7c25a6 100644 --- a/connect/cli/plugins/project/extension/helpers.py +++ b/connect/cli/plugins/project/extension/helpers.py @@ -12,6 +12,7 @@ from connect.cli.core.terminal import console from connect.cli.plugins.project.extension.utils import ( get_event_definitions, + get_pypi_runner_eaas_core_version, get_pypi_runner_version, get_pypi_runner_version_by_connect_version, initialize_git_repository, @@ -79,11 +80,13 @@ def bootstrap_extension_project( # noqa: CCR001 if not answers: raise ClickException('Aborted by user input') + runner_version = get_pypi_runner_version() ctx = { 'statuses_by_event': statuses_by_event[answers['extension_type']], 'interactive': [], 'current_major_version': get_version().split('.')[0], - 'runner_version': get_pypi_runner_version(), + 'runner_version': runner_version, + 'connect_eaas_core_version': get_pypi_runner_eaas_core_version(runner_version), } for var, answer in answers.items(): diff --git a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 index 85784b39..f95e0298 100644 --- a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 +++ b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 @@ -24,27 +24,26 @@ readme = "./README.md" {%- endif %} [tool.poetry.dependencies] -python = ">=3.8,<4" -connect-eaas-core = ">=30" +python = ">=3.9,<3.13" +connect-eaas-core = "{{ connect_eaas_core_version }}" [tool.poetry.dev-dependencies] -pytest = ">=6.1.2,<8" -pytest-cov = ">=2.10.1,<5" +pytest = ">=8,<9" +pytest-cov = ">=2.10.1,<7" pytest-mock = "^3.3.1" -mock = { version = "^4.0.3", markers = "python_version < '3.8'" } -coverage = {extras = ["toml"], version = ">=5.3,<7"} -flake8 = ">=3.8,<6" -flake8-bugbear = ">=20,<23" +coverage = {extras = ["toml"], version = ">=5.3,<8"} +flake8 = ">=6" +flake8-bugbear = ">=24" flake8-cognitive-complexity = "^0.1" -flake8-commas = "~2.0" +flake8-commas = "~4" flake8-future-import = "~0.4" flake8-import-order = "~0.18" -flake8-broken-line = ">=0.3,<0.7" -flake8-comprehensions = "^3.3.1" +flake8-broken-line = ">=1.0" +flake8-comprehensions = "^3.10" flake8-debugger = "^4.0.0" -flake8-eradicate = "^1.0.0" +flake8-eradicate = "^1.3.0" flake8-string-format = "^0.3.0" -pytest-asyncio = "^0.15.1" +pytest-asyncio = ">=0.23" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/connect/cli/plugins/project/extension/utils.py b/connect/cli/plugins/project/extension/utils.py index 7a582484..0b9cb129 100644 --- a/connect/cli/plugins/project/extension/utils.py +++ b/connect/cli/plugins/project/extension/utils.py @@ -5,15 +5,15 @@ import requests from click import ClickException from connect.client import ClientError +from packaging.requirements import Requirement -from connect.cli import get_version from connect.cli.core.utils import ( get_connect_version, get_last_version_by_major, - sort_and_filter_tags, ) from connect.cli.plugins.project.extension.constants import ( PRE_COMMIT_HOOK, + PYPI_EXTENSION_RUNNER_RELEASE_URL, PYPI_EXTENSION_RUNNER_URL, ) @@ -31,11 +31,29 @@ def get_pypi_runner_version(): raise ClickException( f'We can not retrieve the current connect-extension-runner version from {PYPI_EXTENSION_RUNNER_URL}.', ) - content = res.json() - tags = sort_and_filter_tags(content['releases'], get_version().split('.', 1)[0]) - if tags: - return tags.popitem()[0] - return content['info']['version'] + return res.json()['info']['version'] + + +def get_pypi_runner_eaas_core_version(runner_version): + """Return the connect-eaas-core version specifier required by the given runner. + + The bootstrapped extension must pin the same connect-eaas-core the runner image + ships, otherwise `poetry update` resolves an incompatible version and the + extension fails to run. + """ + url = PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version=runner_version) + res = requests.get(url) + if res.status_code != 200: + raise ClickException( + f'We can not retrieve the connect-extension-runner {runner_version} metadata from {url}.', + ) + for dist in res.json()['info'].get('requires_dist') or []: + requirement = Requirement(dist) + if requirement.name == 'connect-eaas-core': + return str(requirement.specifier) + raise ClickException( + f'connect-extension-runner {runner_version} does not declare a connect-eaas-core dependency.', + ) def get_pypi_runner_version_by_connect_version(): diff --git a/tests/plugins/project/test_extension_helpers.py b/tests/plugins/project/test_extension_helpers.py index a8e285f0..c4bc5171 100644 --- a/tests/plugins/project/test_extension_helpers.py +++ b/tests/plugins/project/test_extension_helpers.py @@ -49,6 +49,10 @@ def test_bootstrap_extension_project_background( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -127,6 +131,8 @@ def test_bootstrap_extension_project_background( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { 'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication", @@ -224,6 +230,10 @@ def test_bootstrap_extension_project_interactive( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -296,6 +306,8 @@ def test_bootstrap_extension_project_interactive( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { 'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication", @@ -377,6 +389,10 @@ def test_bootstrap_extension_project_multiaccount( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -527,6 +543,10 @@ def test_bootstrap_extension_project_webapp( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( @@ -688,6 +708,10 @@ def test_bootstrap_extension_project_tfnapp( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( @@ -893,7 +917,6 @@ def test_bootstrap_extension_project_if_destination_exists(mocker): def test_bump_runner_version(mocker, mocked_responses, capsys): - mocker.patch('connect.cli.plugins.project.extension.utils.get_version', return_value='0.5') mocked_responses.add( 'GET', DEFAULT_ENDPOINT, @@ -1045,6 +1068,10 @@ def test_validate_extension_project(mocker, faker, mocked_responses, config_vend 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -1114,6 +1141,10 @@ def test_validate_extension_project_error_exit( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', side_effect=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -1184,6 +1215,10 @@ def test_bootstrap_extension_project_corrupted_load_answers( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( diff --git a/tests/plugins/project/test_extension_utils.py b/tests/plugins/project/test_extension_utils.py index ffaa2322..2678e8d3 100644 --- a/tests/plugins/project/test_extension_utils.py +++ b/tests/plugins/project/test_extension_utils.py @@ -2,7 +2,10 @@ from click import ClickException from connect.client import ClientError -from connect.cli.plugins.project.extension.constants import PYPI_EXTENSION_RUNNER_URL +from connect.cli.plugins.project.extension.constants import ( + PYPI_EXTENSION_RUNNER_RELEASE_URL, + PYPI_EXTENSION_RUNNER_URL, +) from connect.cli.plugins.project.extension.utils import ( check_eventsapp_feature_not_selected, check_extension_events_applicable, @@ -14,37 +17,32 @@ get_default_application_types, get_event_definitions, get_interactive_events, + get_pypi_runner_eaas_core_version, get_pypi_runner_version, ) -def test_get_pypi_runner_version(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_version(mocked_responses): + """The bootstrap must pin the latest published runner, not one tied to the CLI major. + + The CLI major (26) diverged from the runner line (43), so filtering runner tags by + CLI major returned a stale, incompatible image. Confidence: a fresh extension always + gets the current runner. + """ mocked_responses.add( method='GET', url=PYPI_EXTENSION_RUNNER_URL, json={ - 'releases': { - '26.0': {}, - '25.1': {}, - '25.0': {}, - '24.5': {}, - }, + 'info': {'version': '43.0'}, + 'releases': {'43.0': {}, '42.0': {}, '26.44': {}}, }, status=200, ) - assert get_pypi_runner_version() == '25.1' + assert get_pypi_runner_version() == '43.0' -def test_get_pypi_runner_version_http_error(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_version_http_error(mocked_responses): mocked_responses.add( method='GET', url=PYPI_EXTENSION_RUNNER_URL, @@ -56,22 +54,58 @@ def test_get_pypi_runner_version_http_error(mocker, mocked_responses): assert 'We can not retrieve the current connect-extension-runner version' in str(ve.value) -def test_get_pypi_runner_version_no_releases(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_eaas_core_version(mocked_responses): + """The extension must pin the connect-eaas-core the runner image ships. + + If the generated pyproject allows a newer connect-eaas-core than the runner supports, + `poetry update` resolves an incompatible version and the extension fails to run. + Confidence: the runner↔eaas-core pair is always coherent, so the extension boots. + """ mocked_responses.add( method='GET', - url=PYPI_EXTENSION_RUNNER_URL, + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), status=200, json={ - 'info': {'version': '26.0'}, - 'releases': {}, + 'info': { + 'requires_dist': [ + 'logzio-python-handler>=3.1', + 'connect-eaas-core<38,>=37.4', + ], + }, }, ) - assert get_pypi_runner_version() == '26.0' + assert get_pypi_runner_eaas_core_version('43.0') == '<38,>=37.4' + + +def test_get_pypi_runner_eaas_core_version_http_error(mocked_responses): + mocked_responses.add( + method='GET', + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), + status=500, + ) + with pytest.raises(ClickException) as ve: + get_pypi_runner_eaas_core_version('43.0') + + assert 'We can not retrieve the connect-extension-runner 43.0 metadata' in str(ve.value) + + +def test_get_pypi_runner_eaas_core_version_missing_dependency(mocked_responses): + """A runner that omits connect-eaas-core must fail loudly, not silently mis-pin. + + Confidence: we never generate a pyproject with an empty/invalid eaas-core constraint. + """ + mocked_responses.add( + method='GET', + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), + status=200, + json={'info': {'requires_dist': ['logzio-python-handler>=3.1']}}, + ) + + with pytest.raises(ClickException) as ve: + get_pypi_runner_eaas_core_version('43.0') + + assert 'does not declare a connect-eaas-core dependency' in str(ve.value) def test_get_event_definitions_client_error(mocker): From 0513e0e66a2f87b1039a128c82e1590eae300d71 Mon Sep 17 00:00:00 2001 From: Pablo Caro Revuelta Date: Wed, 15 Jul 2026 14:46:26 +0200 Subject: [PATCH 2/2] fix(project): derive bootstrapped python range from the runner (LITE-33583) Address review feedback on the bootstrap dependency alignment: the generated project's `python` constraint was still a hardcoded snapshot of runner 43's range, recreating for python the same drift this branch removes for connect-eaas-core. Fold both pins into a single lookup: get_pypi_runner_requirements() returns the (connect-eaas-core, python) specifiers from the runner's PyPI release metadata (requires_dist + requires_python), failing loudly when either is missing, and the template now renders both derived values. Also raise test fidelity: the bootstrap tests now mock the specifier form the code actually emits ('<38,>=37.4') instead of a hand-reordered variant, and assert both the connect-eaas-core and python pins in the rendered pyproject.toml. --- .../cli/plugins/project/extension/helpers.py | 6 ++- .../${project_slug}/pyproject.toml.j2 | 2 +- .../cli/plugins/project/extension/utils.py | 20 ++++++--- .../plugins/project/test_extension_helpers.py | 38 ++++++++-------- tests/plugins/project/test_extension_utils.py | 44 ++++++++++++++----- 5 files changed, 71 insertions(+), 39 deletions(-) diff --git a/connect/cli/plugins/project/extension/helpers.py b/connect/cli/plugins/project/extension/helpers.py index 3d7c25a6..9bb9618f 100644 --- a/connect/cli/plugins/project/extension/helpers.py +++ b/connect/cli/plugins/project/extension/helpers.py @@ -12,7 +12,7 @@ from connect.cli.core.terminal import console from connect.cli.plugins.project.extension.utils import ( get_event_definitions, - get_pypi_runner_eaas_core_version, + get_pypi_runner_requirements, get_pypi_runner_version, get_pypi_runner_version_by_connect_version, initialize_git_repository, @@ -81,12 +81,14 @@ def bootstrap_extension_project( # noqa: CCR001 raise ClickException('Aborted by user input') runner_version = get_pypi_runner_version() + connect_eaas_core_version, python_version = get_pypi_runner_requirements(runner_version) ctx = { 'statuses_by_event': statuses_by_event[answers['extension_type']], 'interactive': [], 'current_major_version': get_version().split('.')[0], 'runner_version': runner_version, - 'connect_eaas_core_version': get_pypi_runner_eaas_core_version(runner_version), + 'connect_eaas_core_version': connect_eaas_core_version, + 'python_version': python_version, } for var, answer in answers.items(): diff --git a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 index f95e0298..45b36f03 100644 --- a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 +++ b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 @@ -24,7 +24,7 @@ readme = "./README.md" {%- endif %} [tool.poetry.dependencies] -python = ">=3.9,<3.13" +python = "{{ python_version }}" connect-eaas-core = "{{ connect_eaas_core_version }}" [tool.poetry.dev-dependencies] diff --git a/connect/cli/plugins/project/extension/utils.py b/connect/cli/plugins/project/extension/utils.py index 0b9cb129..93dd9a0b 100644 --- a/connect/cli/plugins/project/extension/utils.py +++ b/connect/cli/plugins/project/extension/utils.py @@ -34,12 +34,12 @@ def get_pypi_runner_version(): return res.json()['info']['version'] -def get_pypi_runner_eaas_core_version(runner_version): - """Return the connect-eaas-core version specifier required by the given runner. +def get_pypi_runner_requirements(runner_version): + """Return the (connect-eaas-core, python) version specifiers required by the given runner. - The bootstrapped extension must pin the same connect-eaas-core the runner image - ships, otherwise `poetry update` resolves an incompatible version and the - extension fails to run. + The bootstrapped extension must pin the same connect-eaas-core and python range + the runner image ships, otherwise `poetry update` resolves an incompatible + version and the extension fails to run. """ url = PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version=runner_version) res = requests.get(url) @@ -47,10 +47,16 @@ def get_pypi_runner_eaas_core_version(runner_version): raise ClickException( f'We can not retrieve the connect-extension-runner {runner_version} metadata from {url}.', ) - for dist in res.json()['info'].get('requires_dist') or []: + info = res.json()['info'] + requires_python = info.get('requires_python') + if not requires_python: + raise ClickException( + f'connect-extension-runner {runner_version} does not declare a python requirement.', + ) + for dist in info.get('requires_dist') or []: requirement = Requirement(dist) if requirement.name == 'connect-eaas-core': - return str(requirement.specifier) + return str(requirement.specifier), requires_python raise ClickException( f'connect-extension-runner {runner_version} does not declare a connect-eaas-core dependency.', ) diff --git a/tests/plugins/project/test_extension_helpers.py b/tests/plugins/project/test_extension_helpers.py index c4bc5171..2b925d0b 100644 --- a/tests/plugins/project/test_extension_helpers.py +++ b/tests/plugins/project/test_extension_helpers.py @@ -50,8 +50,8 @@ def test_bootstrap_extension_project_background( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_vendor.load(config_dir='/tmp') @@ -131,7 +131,8 @@ def test_bootstrap_extension_project_background( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) - assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '<38,>=37.4' + assert pyproject_toml['tool']['poetry']['dependencies']['python'] == '<3.13,>=3.9' ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { @@ -231,8 +232,8 @@ def test_bootstrap_extension_project_interactive( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_vendor.load(config_dir='/tmp') @@ -306,7 +307,8 @@ def test_bootstrap_extension_project_interactive( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) - assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '<38,>=37.4' + assert pyproject_toml['tool']['poetry']['dependencies']['python'] == '<3.13,>=3.9' ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { @@ -390,8 +392,8 @@ def test_bootstrap_extension_project_multiaccount( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_vendor.load(config_dir='/tmp') @@ -544,8 +546,8 @@ def test_bootstrap_extension_project_webapp( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_provider.load(config_dir='/tmp') @@ -709,8 +711,8 @@ def test_bootstrap_extension_project_tfnapp( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_provider.load(config_dir='/tmp') @@ -1069,8 +1071,8 @@ def test_validate_extension_project(mocker, faker, mocked_responses, config_vend return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_vendor.load(config_dir='/tmp') @@ -1142,8 +1144,8 @@ def test_validate_extension_project_error_exit( side_effect=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_vendor.load(config_dir='/tmp') @@ -1216,8 +1218,8 @@ def test_bootstrap_extension_project_corrupted_load_answers( return_value=runner_version, ) mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', - return_value='>=37.4,<38', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_requirements', + return_value=('<38,>=37.4', '<3.13,>=3.9'), ) config_provider.load(config_dir='/tmp') diff --git a/tests/plugins/project/test_extension_utils.py b/tests/plugins/project/test_extension_utils.py index 2678e8d3..b4917e78 100644 --- a/tests/plugins/project/test_extension_utils.py +++ b/tests/plugins/project/test_extension_utils.py @@ -17,7 +17,7 @@ get_default_application_types, get_event_definitions, get_interactive_events, - get_pypi_runner_eaas_core_version, + get_pypi_runner_requirements, get_pypi_runner_version, ) @@ -54,12 +54,13 @@ def test_get_pypi_runner_version_http_error(mocked_responses): assert 'We can not retrieve the current connect-extension-runner version' in str(ve.value) -def test_get_pypi_runner_eaas_core_version(mocked_responses): - """The extension must pin the connect-eaas-core the runner image ships. +def test_get_pypi_runner_requirements(mocked_responses): + """The extension must pin the connect-eaas-core and python range the runner ships. - If the generated pyproject allows a newer connect-eaas-core than the runner supports, + If the generated pyproject allows versions the runner does not support, `poetry update` resolves an incompatible version and the extension fails to run. - Confidence: the runner↔eaas-core pair is always coherent, so the extension boots. + Confidence: the runner↔eaas-core↔python triple is always coherent, so the + extension boots. """ mocked_responses.add( method='GET', @@ -67,6 +68,7 @@ def test_get_pypi_runner_eaas_core_version(mocked_responses): status=200, json={ 'info': { + 'requires_python': '<3.13,>=3.9', 'requires_dist': [ 'logzio-python-handler>=3.1', 'connect-eaas-core<38,>=37.4', @@ -75,22 +77,37 @@ def test_get_pypi_runner_eaas_core_version(mocked_responses): }, ) - assert get_pypi_runner_eaas_core_version('43.0') == '<38,>=37.4' + assert get_pypi_runner_requirements('43.0') == ('<38,>=37.4', '<3.13,>=3.9') -def test_get_pypi_runner_eaas_core_version_http_error(mocked_responses): +def test_get_pypi_runner_requirements_http_error(mocked_responses): mocked_responses.add( method='GET', url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), status=500, ) with pytest.raises(ClickException) as ve: - get_pypi_runner_eaas_core_version('43.0') + get_pypi_runner_requirements('43.0') assert 'We can not retrieve the connect-extension-runner 43.0 metadata' in str(ve.value) -def test_get_pypi_runner_eaas_core_version_missing_dependency(mocked_responses): +def test_get_pypi_runner_requirements_missing_python(mocked_responses): + """A runner that omits requires_python must fail loudly, not render an empty pin.""" + mocked_responses.add( + method='GET', + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), + status=200, + json={'info': {'requires_dist': ['connect-eaas-core<38,>=37.4']}}, + ) + + with pytest.raises(ClickException) as ve: + get_pypi_runner_requirements('43.0') + + assert 'does not declare a python requirement' in str(ve.value) + + +def test_get_pypi_runner_requirements_missing_dependency(mocked_responses): """A runner that omits connect-eaas-core must fail loudly, not silently mis-pin. Confidence: we never generate a pyproject with an empty/invalid eaas-core constraint. @@ -99,11 +116,16 @@ def test_get_pypi_runner_eaas_core_version_missing_dependency(mocked_responses): method='GET', url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), status=200, - json={'info': {'requires_dist': ['logzio-python-handler>=3.1']}}, + json={ + 'info': { + 'requires_python': '<3.13,>=3.9', + 'requires_dist': ['logzio-python-handler>=3.1'], + }, + }, ) with pytest.raises(ClickException) as ve: - get_pypi_runner_eaas_core_version('43.0') + get_pypi_runner_requirements('43.0') assert 'does not declare a connect-eaas-core dependency' in str(ve.value)