From 74e774941f70dcaadac19e5a30225c99b8aaa33d Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 8 May 2026 12:22:41 +0100 Subject: [PATCH 1/6] Tests to reproduce unresolved version behaviour --- tests/core_tests/test_pipelineconfig_utils.py | 56 +++++++++++++++++++ tests/integration_tests/pip_install.py | 17 ++++++ .../pip_install_bootstrap.py | 15 +++++ 3 files changed, 88 insertions(+) diff --git a/tests/core_tests/test_pipelineconfig_utils.py b/tests/core_tests/test_pipelineconfig_utils.py index b8ee79ff9f..cca5816bd8 100644 --- a/tests/core_tests/test_pipelineconfig_utils.py +++ b/tests/core_tests/test_pipelineconfig_utils.py @@ -359,3 +359,59 @@ def test_get_sgtk_module_path(self): self.assertEqual(sgtk.get_sgtk_module_path(), python_path) self.assertEqual(sgtk.get_sgtk_module_path(), tank.get_sgtk_module_path()) + + +class TestGetCurrentlyRunningApiVersion(ShotgunTestBase): + """ + Tests get_currently_running_api_version, including the importlib.metadata + fallback used when info.yml is absent (e.g. flat pip install layout). + """ + + @mock.patch("tank.pipelineconfig_utils._get_version_from_manifest") + def test_returns_manifest_version_when_present(self, manifest_mock): + """ + When info.yml is present, its version is returned and the dist-metadata + fallback is not consulted. + """ + manifest_mock.return_value = "v1.2.3" + with mock.patch("importlib.metadata.version") as dist_mock: + self.assertEqual( + pipelineconfig_utils.get_currently_running_api_version(), + "v1.2.3", + ) + dist_mock.assert_not_called() + + @mock.patch("tank.pipelineconfig_utils._get_version_from_manifest") + def test_falls_back_to_dist_metadata_when_manifest_missing(self, manifest_mock): + """ + Pip install layout: info.yml is absent so the manifest yields "unknown", + and the function falls back to the installed sgtk distribution version, + re-adding the 'v' prefix that PEP 440 normalization strips. + """ + manifest_mock.return_value = "unknown" + with mock.patch( + "importlib.metadata.version", return_value="0.23.8" + ) as dist_mock: + self.assertEqual( + pipelineconfig_utils.get_currently_running_api_version(), + "v0.23.8", + ) + dist_mock.assert_called_once_with("sgtk") + + @mock.patch("tank.pipelineconfig_utils._get_version_from_manifest") + def test_returns_unknown_when_manifest_and_dist_metadata_missing( + self, manifest_mock + ): + """ + Neither info.yml nor an installed sgtk distribution available: preserve + the original "unknown" contract instead of raising. + """ + manifest_mock.return_value = "unknown" + with mock.patch( + "importlib.metadata.version", + side_effect=Exception("PackageNotFoundError"), + ): + self.assertEqual( + pipelineconfig_utils.get_currently_running_api_version(), + "unknown", + ) diff --git a/tests/integration_tests/pip_install.py b/tests/integration_tests/pip_install.py index e73ccd14c0..6a6d65f70e 100644 --- a/tests/integration_tests/pip_install.py +++ b/tests/integration_tests/pip_install.py @@ -60,6 +60,23 @@ def test_pip_install_and_import(self): ] ) + # Under the flat pip layout info.yml is absent; the version must + # come from the installed sgtk distribution metadata instead of + # falling through to "unknown". + version = subprocess.check_output( # nosec B603 + [ + python, + "-c", + "import sgtk; print(sgtk.get_currently_running_api_version())", + ], + text=True, + ).strip() + self.assertNotEqual(version, "unknown") + self.assertTrue( + version.startswith("v"), + "expected vX.Y.Z, got %r" % version, + ) + if __name__ == "__main__": unittest.main(failfast=True, verbosity=2) diff --git a/tests/integration_tests/pip_install_bootstrap.py b/tests/integration_tests/pip_install_bootstrap.py index a522de5ade..d3555337b5 100644 --- a/tests/integration_tests/pip_install_bootstrap.py +++ b/tests/integration_tests/pip_install_bootstrap.py @@ -100,6 +100,21 @@ def test_boostrap_engine(self): engine = manager.bootstrap_engine("tk-shell", self.asset) self.assertEqual(engine.name, "tk-shell") + def test_get_currently_running_api_version_in_simulated_pip_layout(self): + """ + The simulated layout has no info.yml. The function must not raise; it + should return a non-empty string. The exact value depends on whether + sgtk is also pip-installed in the test runner's Python — importlib's + distribution metadata is read from the runtime Python's site-packages, + not from the sys.path-prepended simulated copy. + """ + self.__clean_sgtk_modules() + import sgtk + + result = sgtk.get_currently_running_api_version() + self.assertIsInstance(result, str) + self.assertTrue(result, "version must be non-empty") + if __name__ == "__main__": unittest.main(failfast=True, verbosity=2) From 2e254c37d042ca86d295a6f3786ea8f8ba0596e2 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 8 May 2026 13:00:27 +0100 Subject: [PATCH 2/6] Fixed unkown version issue for pip installed tk-core --- python/tank/pipelineconfig_utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/python/tank/pipelineconfig_utils.py b/python/tank/pipelineconfig_utils.py index 7a5543dce3..e9ec01dd04 100644 --- a/python/tank/pipelineconfig_utils.py +++ b/python/tank/pipelineconfig_utils.py @@ -439,7 +439,17 @@ def get_currently_running_api_version(): info_yml_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..", "info.yml") ) - return _get_version_from_manifest(info_yml_path) + version = _get_version_from_manifest(info_yml_path) + if version == "unknown": + # In a pip install the flat site-packages layout has no info.yml. + # Fall back to the installed distribution metadata; PEP 440 strips the + # leading 'v', so re-add it to match the info.yml convention. + try: + from importlib.metadata import version as _dist_version + version = "v" + _dist_version("sgtk") + except Exception: + pass + return version def get_core_api_version(core_install_root): From 9b37fd02a537335afa409866e5dd7dcade8a8814 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 8 May 2026 13:51:27 +0100 Subject: [PATCH 3/6] SG-42277 Call get_currently_running_api_version via submodule in integration tests The function is defined in tank.pipelineconfig_utils and is not re-exported at the top of the tank/sgtk module, so sgtk.get_currently_running_api_version raises AttributeError. Call it via sgtk.pipelineconfig_utils instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration_tests/pip_install.py | 2 +- tests/integration_tests/pip_install_bootstrap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/pip_install.py b/tests/integration_tests/pip_install.py index 6a6d65f70e..15f56af70e 100644 --- a/tests/integration_tests/pip_install.py +++ b/tests/integration_tests/pip_install.py @@ -67,7 +67,7 @@ def test_pip_install_and_import(self): [ python, "-c", - "import sgtk; print(sgtk.get_currently_running_api_version())", + "import sgtk; print(sgtk.pipelineconfig_utils.get_currently_running_api_version())", ], text=True, ).strip() diff --git a/tests/integration_tests/pip_install_bootstrap.py b/tests/integration_tests/pip_install_bootstrap.py index d3555337b5..3e9f442b51 100644 --- a/tests/integration_tests/pip_install_bootstrap.py +++ b/tests/integration_tests/pip_install_bootstrap.py @@ -111,7 +111,7 @@ def test_get_currently_running_api_version_in_simulated_pip_layout(self): self.__clean_sgtk_modules() import sgtk - result = sgtk.get_currently_running_api_version() + result = sgtk.pipelineconfig_utils.get_currently_running_api_version() self.assertIsInstance(result, str) self.assertTrue(result, "version must be non-empty") From 36d954884c357e05a4bb352b61c6eae8990831e4 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 8 May 2026 14:03:00 +0100 Subject: [PATCH 4/6] SG-42277 Strip PYTHONPATH for venv subprocess in pip_install integration test The integration test runner prepends the source tree's python/ directory to PYTHONPATH, which the test's subprocess inherits. This shadows the venv's pip-installed sgtk with the source tree, causing _get_version_from_manifest to pick up the source repo's info.yml (version: "HEAD") instead of exercising the importlib.metadata fallback. Drop PYTHONPATH from the subprocess env so the venv's site-packages takes precedence. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration_tests/pip_install.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests/pip_install.py b/tests/integration_tests/pip_install.py index 15f56af70e..d4874c17ba 100644 --- a/tests/integration_tests/pip_install.py +++ b/tests/integration_tests/pip_install.py @@ -62,7 +62,13 @@ def test_pip_install_and_import(self): # Under the flat pip layout info.yml is absent; the version must # come from the installed sgtk distribution metadata instead of - # falling through to "unknown". + # falling through to "unknown". Strip PYTHONPATH from the + # subprocess env — the integration runner sets it to the source + # tree, which would otherwise shadow the venv's pip-installed + # sgtk and let _get_version_from_manifest pick up the source + # repo's info.yml. + subprocess_env = dict(os.environ) + subprocess_env.pop("PYTHONPATH", None) version = subprocess.check_output( # nosec B603 [ python, @@ -70,6 +76,7 @@ def test_pip_install_and_import(self): "import sgtk; print(sgtk.pipelineconfig_utils.get_currently_running_api_version())", ], text=True, + env=subprocess_env, ).strip() self.assertNotEqual(version, "unknown") self.assertTrue( From e0db8bcb27582eea70baefe5c21f293c2afa332e Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 8 May 2026 14:11:37 +0100 Subject: [PATCH 5/6] Linter --- python/tank/pipelineconfig_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/tank/pipelineconfig_utils.py b/python/tank/pipelineconfig_utils.py index e9ec01dd04..5f0d0657e6 100644 --- a/python/tank/pipelineconfig_utils.py +++ b/python/tank/pipelineconfig_utils.py @@ -446,6 +446,7 @@ def get_currently_running_api_version(): # leading 'v', so re-add it to match the info.yml convention. try: from importlib.metadata import version as _dist_version + version = "v" + _dist_version("sgtk") except Exception: pass From b3dd9fdb7bb0ccdacda69d508b02ff34aad6a2f6 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Mon, 11 May 2026 16:23:31 +0100 Subject: [PATCH 6/6] Code review feedback addressed --- python/tank/pipelineconfig_utils.py | 33 +++++++++---------- tests/core_tests/test_pipelineconfig_utils.py | 9 ++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/python/tank/pipelineconfig_utils.py b/python/tank/pipelineconfig_utils.py index 5f0d0657e6..834236377e 100644 --- a/python/tank/pipelineconfig_utils.py +++ b/python/tank/pipelineconfig_utils.py @@ -13,6 +13,7 @@ across storages, configurations etc. """ +import importlib.metadata import os from tank_vendor import yaml @@ -440,17 +441,15 @@ def get_currently_running_api_version(): os.path.join(os.path.dirname(__file__), "..", "..", "info.yml") ) version = _get_version_from_manifest(info_yml_path) - if version == "unknown": - # In a pip install the flat site-packages layout has no info.yml. - # Fall back to the installed distribution metadata; PEP 440 strips the - # leading 'v', so re-add it to match the info.yml convention. - try: - from importlib.metadata import version as _dist_version - - version = "v" + _dist_version("sgtk") - except Exception: - pass - return version + if version is not None: + return version + # In a pip install the flat site-packages layout has no info.yml. + # Fall back to the installed distribution metadata; PEP 440 strips the + # leading 'v', so re-add it to match the info.yml convention. + try: + return "v" + importlib.metadata.version("sgtk") + except importlib.metadata.PackageNotFoundError: + return "unknown" def get_core_api_version(core_install_root): @@ -466,7 +465,8 @@ def get_core_api_version(core_install_root): """ # now try to get to the info.yml file to get the version number info_yml_path = os.path.join(core_install_root, "install", "core", "info.yml") - return _get_version_from_manifest(info_yml_path) + version = _get_version_from_manifest(info_yml_path) + return "unknown" if version is None else version def _get_version_from_manifest(info_yml_path): @@ -475,15 +475,14 @@ def _get_version_from_manifest(info_yml_path): Returns the version given a manifest. :param info_yml_path: path to manifest file. - :returns: Always a string, 'unknown' if data cannot be found + :returns: Version string, or None if data cannot be found. """ try: data = yaml_cache.g_yaml_cache.get(info_yml_path, deepcopy_data=False) or {} - data = str(data.get("version", "unknown")) + version = data.get("version") + return str(version) if version is not None else None except Exception: - data = "unknown" - - return data + return None def _get_core_descriptor_file(pipeline_config_path): diff --git a/tests/core_tests/test_pipelineconfig_utils.py b/tests/core_tests/test_pipelineconfig_utils.py index cca5816bd8..3752e79003 100644 --- a/tests/core_tests/test_pipelineconfig_utils.py +++ b/tests/core_tests/test_pipelineconfig_utils.py @@ -8,6 +8,7 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. +import importlib.metadata import os import inspect import sys @@ -384,11 +385,11 @@ def test_returns_manifest_version_when_present(self, manifest_mock): @mock.patch("tank.pipelineconfig_utils._get_version_from_manifest") def test_falls_back_to_dist_metadata_when_manifest_missing(self, manifest_mock): """ - Pip install layout: info.yml is absent so the manifest yields "unknown", + Pip install layout: info.yml is absent so the manifest yields None, and the function falls back to the installed sgtk distribution version, re-adding the 'v' prefix that PEP 440 normalization strips. """ - manifest_mock.return_value = "unknown" + manifest_mock.return_value = None with mock.patch( "importlib.metadata.version", return_value="0.23.8" ) as dist_mock: @@ -406,10 +407,10 @@ def test_returns_unknown_when_manifest_and_dist_metadata_missing( Neither info.yml nor an installed sgtk distribution available: preserve the original "unknown" contract instead of raising. """ - manifest_mock.return_value = "unknown" + manifest_mock.return_value = None with mock.patch( "importlib.metadata.version", - side_effect=Exception("PackageNotFoundError"), + side_effect=importlib.metadata.PackageNotFoundError("sgtk"), ): self.assertEqual( pipelineconfig_utils.get_currently_running_api_version(),