Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions python/tank/pipelineconfig_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
across storages, configurations etc.
"""

import importlib.metadata
import os

from tank_vendor import yaml
Expand Down Expand Up @@ -439,7 +440,16 @@ 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 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):
Expand All @@ -455,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):
Expand All @@ -464,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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sorry for my previous comments. When I see the result, I realize that it was not the best strategy.

Can we do something like that instead?

def _get_version_from_manifest_or_pip(info_yml_path: str) -> str:
    """
    Helper method.
    Returns the version given a manifest.

    :param info_yml_path: path to manifest file.
    :returns: Always a string, 'unknown' if data cannot be found
    """
    try:
        data = yaml_cache.g_yaml_cache.get(info_yml_path, deepcopy_data=False) or {}
        return str(data.get("version", "unknown"))
    except Exception:
        pass

    # 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:
        pass

    return "unknown"

And then we replace the 2 calls of the method:

diff --git a/python/tank/pipelineconfig_utils.py b/python/tank/pipelineconfig_utils.py
index 7a5543dc..9f3eb9f1 100644
--- a/python/tank/pipelineconfig_utils.py
+++ b/python/tank/pipelineconfig_utils.py
@@ -439,7 +439,7 @@ 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)
+    return _get_version_from_manifest_or_pip(info_yml_path)
 
 
 def get_core_api_version(core_install_root):
@@ -455,7 +455,7 @@ 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)
+    return _get_version_from_manifest_or_pip(info_yml_path)
 
 
 def _get_version_from_manifest(info_yml_path):



def _get_core_descriptor_file(pipeline_config_path):
Expand Down
57 changes: 57 additions & 0 deletions tests/core_tests/test_pipelineconfig_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -359,3 +360,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 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 = None
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 = None
with mock.patch(
"importlib.metadata.version",
side_effect=importlib.metadata.PackageNotFoundError("sgtk"),
):
self.assertEqual(
pipelineconfig_utils.get_currently_running_api_version(),
"unknown",
)
24 changes: 24 additions & 0 deletions tests/integration_tests/pip_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ 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". 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,
"-c",
"import sgtk; print(sgtk.pipelineconfig_utils.get_currently_running_api_version())",
],
text=True,
env=subprocess_env,
).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)
15 changes: 15 additions & 0 deletions tests/integration_tests/pip_install_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.pipelineconfig_utils.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)