Skip to content

Commit cd2178c

Browse files
authored
feat: print adaptor and runtime version on start (#259)
Signed-off-by: Jericho Tolentino <68654047+jericht@users.noreply.github.com>
1 parent f0964bd commit cd2178c

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

src/openjd/adaptor_runtime/_entrypoint.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import contextlib
6+
import importlib.metadata
67
import logging
78
import os
89
import signal
@@ -46,6 +47,7 @@
4647
ConditionalFormatter,
4748
)
4849
from .adaptors import SemanticVersion
50+
from ._version import version as _RUNTIME_VERSION
4951

5052
if TYPE_CHECKING: # pragma: no cover
5153
from .adaptors.configuration import AdaptorConfiguration
@@ -242,6 +244,53 @@ def _get_version_info(self) -> _VersionInfo:
242244
).integration_data_interface_version,
243245
)
244246

247+
def _get_adaptor_package_version(self) -> str:
248+
"""
249+
Attempts to determine the package version of the adaptor using the runtime.
250+
251+
Uses importlib.metadata to find the distribution that owns the adaptor's module
252+
file. Returns "UNKNOWN" if the version cannot be determined.
253+
"""
254+
adaptor_module_name = self.adaptor_class.__module__
255+
256+
# Convert module path to a file path to match against distribution records.
257+
# e.g. "deadline.max_adaptor.MaxAdaptor.adaptor" -> "deadline/max_adaptor/MaxAdaptor/adaptor.py"
258+
module_file_path = adaptor_module_name.replace(".", "/") + ".py"
259+
260+
# Find distributions that provide the top-level package, then check which one
261+
# owns the adaptor's module file. packages_distributions() requires Python 3.11+.
262+
_packages_distributions = getattr(importlib.metadata, "packages_distributions", None)
263+
if _packages_distributions is not None:
264+
top_level_package = adaptor_module_name.split(".")[0]
265+
pkg_to_dists = _packages_distributions()
266+
candidate_dists = []
267+
for name in pkg_to_dists.get(top_level_package, []):
268+
try:
269+
candidate_dists.append(importlib.metadata.distribution(name))
270+
except importlib.metadata.PackageNotFoundError:
271+
continue
272+
273+
for dist in candidate_dists:
274+
if dist.files:
275+
for f in dist.files:
276+
if str(f) == module_file_path or str(f).endswith("/" + module_file_path):
277+
return dist.version
278+
279+
# Fallback: walk up module path looking for a _version module in sys.modules
280+
parts = adaptor_module_name.split(".")
281+
for i in range(len(parts) - 1, 0, -1):
282+
candidate_package = ".".join(parts[:i])
283+
version_module_name = f"{candidate_package}._version"
284+
version_module = sys.modules.get(version_module_name)
285+
if version_module and hasattr(version_module, "version"):
286+
return version_module.version
287+
288+
_logger.warning(
289+
f"Could not determine package version for adaptor '{self.adaptor_class.__name__}'. "
290+
f"Looked up module: '{adaptor_module_name}'"
291+
)
292+
return "UNKNOWN"
293+
245294
def _get_integration_data(self, parsed_args: Namespace) -> _IntegrationData:
246295
return _IntegrationData(
247296
init_data=parsed_args.init_data if hasattr(parsed_args, "init_data") else {},
@@ -273,6 +322,11 @@ def start(
273322
)
274323
)
275324

325+
# Log package versions at startup
326+
_logger.info(f"openjd-adaptor-runtime version: {_RUNTIME_VERSION}")
327+
adaptor_version = self._get_adaptor_package_version()
328+
_logger.info(f"{self.adaptor_class.__name__} version: {adaptor_version}")
329+
276330
interface_version_info = self._get_version_info()
277331

278332
if parsed_args.command == "is-compatible":

test/openjd/adaptor_runtime/unit/test_entrypoint.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import os
88
import signal
9+
import sys
910
from pathlib import Path
1011
from typing import Optional
1112
from unittest.mock import ANY, MagicMock, Mock, PropertyMock, mock_open, patch
@@ -117,6 +118,25 @@ def test_version_info(
117118
"MockAdaptor Data Interface Version": "1.5",
118119
}
119120

121+
def test_logs_versions_at_startup(self):
122+
"""Verifies that both the runtime and adaptor versions are logged during start()."""
123+
# GIVEN
124+
entrypoint = EntryPoint(FakeAdaptor)
125+
126+
with (
127+
patch.object(entrypoint, "_get_adaptor_package_version", return_value="1.2.3"),
128+
patch.object(runtime_entrypoint.sys, "argv", ["Adaptor", "run"]),
129+
patch.object(runtime_entrypoint, "_logger") as mock_logger,
130+
):
131+
# WHEN
132+
entrypoint.start()
133+
134+
# THEN
135+
mock_logger.info.assert_any_call(
136+
f"openjd-adaptor-runtime version: {runtime_entrypoint._RUNTIME_VERSION}"
137+
)
138+
mock_logger.info.assert_any_call("FakeAdaptor version: 1.2.3")
139+
120140
@pytest.mark.parametrize("integration_version", ["1.4", "1.5"])
121141
def test_is_compatible(
122142
self,
@@ -904,3 +924,166 @@ def test_raises_on_nonvalid_parsed_data_type(self):
904924

905925
# THEN
906926
assert raised_err.match(f"Expected loaded data to be a dict, but got {type(input)}")
927+
928+
929+
@pytest.mark.skipif(
930+
sys.version_info < (3, 10),
931+
reason="packages_distributions requires Python 3.10+",
932+
)
933+
class TestGetAdaptorPackageVersion:
934+
"""
935+
Tests for the EntryPoint._get_adaptor_package_version method
936+
"""
937+
938+
def test_returns_version_from_distribution_file_match(self, mock_adaptor_cls: MagicMock):
939+
"""When the adaptor module file is found in a distribution's recorded files, return its version."""
940+
# GIVEN
941+
mock_adaptor_cls.__module__ = "deadline.max_adaptor.MaxAdaptor.adaptor"
942+
entrypoint = EntryPoint(mock_adaptor_cls)
943+
944+
mock_dist = MagicMock()
945+
mock_dist.version = "0.3.3"
946+
mock_dist.files = [
947+
MagicMock(__str__=lambda self: "deadline/max_adaptor/MaxAdaptor/adaptor.py"),
948+
MagicMock(__str__=lambda self: "deadline/max_adaptor/__init__.py"),
949+
]
950+
951+
def fake_distribution(name):
952+
if name == "deadline-cloud-for-3ds-max":
953+
return mock_dist
954+
raise runtime_entrypoint.importlib.metadata.PackageNotFoundError(name)
955+
956+
with (
957+
patch(
958+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.packages_distributions",
959+
return_value={"deadline": ["deadline-cloud-for-3ds-max", "deadline"]},
960+
),
961+
patch(
962+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.distribution",
963+
side_effect=fake_distribution,
964+
),
965+
):
966+
# WHEN
967+
result = entrypoint._get_adaptor_package_version()
968+
969+
# THEN
970+
assert result == "0.3.3"
971+
972+
def test_does_not_match_wrong_distribution(self, mock_adaptor_cls: MagicMock):
973+
"""When the top-level package has multiple distributions, only match the correct one."""
974+
# GIVEN
975+
mock_adaptor_cls.__module__ = "deadline.max_adaptor.MaxAdaptor.adaptor"
976+
entrypoint = EntryPoint(mock_adaptor_cls)
977+
978+
# deadline-cloud dist does NOT contain the adaptor module file
979+
deadline_cloud_dist = MagicMock()
980+
deadline_cloud_dist.version = "0.57.3"
981+
deadline_cloud_dist.files = [
982+
MagicMock(__str__=lambda self: "deadline/client/__init__.py"),
983+
MagicMock(__str__=lambda self: "deadline/client/cli.py"),
984+
]
985+
986+
# deadline-cloud-for-3ds-max dist DOES contain the adaptor module file
987+
max_adaptor_dist = MagicMock()
988+
max_adaptor_dist.version = "0.3.3"
989+
max_adaptor_dist.files = [
990+
MagicMock(__str__=lambda self: "deadline/max_adaptor/MaxAdaptor/adaptor.py"),
991+
MagicMock(__str__=lambda self: "deadline/max_adaptor/__init__.py"),
992+
]
993+
994+
def fake_distribution(name):
995+
if name == "deadline":
996+
return deadline_cloud_dist
997+
if name == "deadline-cloud-for-3ds-max":
998+
return max_adaptor_dist
999+
raise runtime_entrypoint.importlib.metadata.PackageNotFoundError(name)
1000+
1001+
with (
1002+
patch(
1003+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.packages_distributions",
1004+
return_value={"deadline": ["deadline", "deadline-cloud-for-3ds-max"]},
1005+
),
1006+
patch(
1007+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.distribution",
1008+
side_effect=fake_distribution,
1009+
),
1010+
):
1011+
# WHEN
1012+
result = entrypoint._get_adaptor_package_version()
1013+
1014+
# THEN
1015+
assert result == "0.3.3"
1016+
1017+
def test_falls_back_to_version_module(self, mock_adaptor_cls: MagicMock):
1018+
"""When no distribution file match is found, fall back to _version module in sys.modules."""
1019+
# GIVEN
1020+
mock_adaptor_cls.__module__ = "deadline.max_adaptor.MaxAdaptor.adaptor"
1021+
entrypoint = EntryPoint(mock_adaptor_cls)
1022+
1023+
mock_version_module = MagicMock()
1024+
mock_version_module.version = "3.5.1"
1025+
1026+
with (
1027+
patch(
1028+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.packages_distributions",
1029+
return_value={"deadline": []},
1030+
),
1031+
patch.dict(
1032+
runtime_entrypoint.sys.modules,
1033+
{"deadline.max_adaptor._version": mock_version_module},
1034+
),
1035+
):
1036+
# WHEN
1037+
result = entrypoint._get_adaptor_package_version()
1038+
1039+
# THEN
1040+
assert result == "3.5.1"
1041+
1042+
def test_fallback_walks_up_module_path(self, mock_adaptor_cls: MagicMock):
1043+
"""The _version fallback walks up the module path to find the correct _version module."""
1044+
# GIVEN
1045+
mock_adaptor_cls.__module__ = "deadline.max_adaptor.MaxAdaptor.adaptor"
1046+
entrypoint = EntryPoint(mock_adaptor_cls)
1047+
1048+
mock_version_module = MagicMock()
1049+
mock_version_module.version = "0.3.2"
1050+
1051+
with (
1052+
patch(
1053+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.packages_distributions",
1054+
return_value={"deadline": []},
1055+
),
1056+
patch.dict(
1057+
runtime_entrypoint.sys.modules,
1058+
{"deadline.max_adaptor._version": mock_version_module},
1059+
),
1060+
):
1061+
# WHEN
1062+
result = entrypoint._get_adaptor_package_version()
1063+
1064+
# THEN
1065+
assert result == "0.3.2"
1066+
1067+
def test_returns_unknown_when_version_not_found(self, mock_adaptor_cls: MagicMock):
1068+
"""When no version can be determined, return 'UNKNOWN' and log a warning."""
1069+
# GIVEN
1070+
mock_adaptor_cls.__module__ = "deadline.max_adaptor.MaxAdaptor.adaptor"
1071+
mock_adaptor_cls.__name__ = "MaxAdaptor"
1072+
entrypoint = EntryPoint(mock_adaptor_cls)
1073+
1074+
with (
1075+
patch(
1076+
"openjd.adaptor_runtime._entrypoint.importlib.metadata.packages_distributions",
1077+
return_value={"deadline": []},
1078+
),
1079+
patch.dict(runtime_entrypoint.sys.modules, {}, clear=False),
1080+
):
1081+
runtime_entrypoint.sys.modules.pop("deadline.max_adaptor.MaxAdaptor._version", None)
1082+
runtime_entrypoint.sys.modules.pop("deadline.max_adaptor._version", None)
1083+
runtime_entrypoint.sys.modules.pop("deadline._version", None)
1084+
1085+
# WHEN
1086+
result = entrypoint._get_adaptor_package_version()
1087+
1088+
# THEN
1089+
assert result == "UNKNOWN"

0 commit comments

Comments
 (0)