From 9a9f76eec5c2f2d9683ae91e3fa62d8c49439cdb Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Wed, 13 May 2026 15:49:35 +0100 Subject: [PATCH 01/11] SG-43217 Integrate Flow Data SDK as a vendored beta library Adds requirements/any/ for Python-version-independent vendor zips and teaches python/tank_vendor/__init__.py to auto-discover and load them alongside the existing pkgs.zip. Drops in flow_data_sdk-beta.zip as the first such vendor. The loader refactor extracts the existing pkgs.zip init into a reusable _load_packages_from_zip helper. Shared zips load after pkgs.zip so per-version pins win on name collision; collisions warn and skip rather than overwrite. Per-package import failures continue to warn-and-continue (the SDK uses 3.10+ syntax, so it'll simply be absent on 3.7/3.9 instead of breaking import tank_vendor). Includes a small _patch_flow_data_sdk_version workaround for an upstream bug: the SDK's _version.py queries importlib.metadata.version( "adsk-flow-data") but the published wheel's distribution name is "flow-data-sdk", so SDK_VERSION otherwise falls back to "local_dev" even with .dist-info present. The patch is a self-disabling no-op once upstream is fixed. Tests cover the new package via PACKAGES_TO_TEST (3.10+ gated) plus a TestFlowDataSDK class with a dist-info canary that catches future regressions in the zip's metadata packaging. Co-Authored-By: Claude Opus 4.7 (1M context) --- developer/README.md | 4 + python/tank_vendor/__init__.py | 303 +++++++++++++++--------- requirements/any/flow_data_sdk-beta.zip | Bin 0 -> 68904 bytes tests/core_tests/test_tank_vendor.py | 79 +++++- 4 files changed, 277 insertions(+), 109 deletions(-) create mode 100644 requirements/any/flow_data_sdk-beta.zip diff --git a/developer/README.md b/developer/README.md index 27bd1f162..8dc9c22df 100644 --- a/developer/README.md +++ b/developer/README.md @@ -27,6 +27,10 @@ The `requirements/update_python_packages.py` script automates the creation and m - Generate the `frozen_requirements.txt` file for consistency. 3. Validate that the `pkgs.zip` file contains all necessary packages and matches the updated requirements. +### Shared (Python-version-independent) vendor zips + +In addition to the per-version `pkgs.zip`, `requirements/any/` holds pure-Python packages that are safe to load across every supported Python version (e.g. the Autodesk Flow Data Beta SDK). Each zip is auto-discovered by `tank_vendor/__init__.py` and contains the importable package plus its `.dist-info/` directory at the zip's root. + ## How to upgrade ruamel.yaml Until version `0.10.10`, the contents of the library was located at `tank_vendor/ruamel_yaml`. diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 6fe433ca2..2d6b5b2dd 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -12,9 +12,11 @@ tank_vendor module - Third-party dependency management for Shotgun Toolkit. This module handles loading and importing third-party Python packages from -version-specific ZIP archives (pkgs.zip). It provides: +ZIP archives. It provides: -1. Auto-discovery of packages in pkgs.zip +1. Auto-discovery of packages in two locations: + - requirements/./pkgs.zip (per-Python-version, mandatory) + - requirements/any/*.zip (Python-version-independent, optional) 2. Lazy import hook for transparent tank_vendor.* namespace aliasing 3. Package-specific patches (e.g., SSL certificate handling for shotgun_api3) @@ -22,13 +24,20 @@ # Direct imports work automatically: from tank_vendor import yaml from tank_vendor.shotgun_api3 import Shotgun + from tank_vendor import flow_data_sdk # Submodule imports work via lazy loading: from tank_vendor.shotgun_api3.lib import httplib2 + from tank_vendor.flow_data_sdk.base import client # Mock.patch works seamlessly: mock.patch("tank_vendor.shotgun_api3.Shotgun.find") +Shared zips in requirements/any/ are loaded after pkgs.zip, so per-version +pinned packages take precedence over anything in the shared directory. +Packages whose top-level name is already registered are skipped with a +warning. + Supported Python versions: 3.7+ """ @@ -194,12 +203,46 @@ def _patched_get_certs_file(ca_certs=None): shotgun_api3.Shotgun._get_certs_file = staticmethod(_patched_get_certs_file) +def _patch_flow_data_sdk_version(): + """ + Work around an upstream bug in the Flow Data SDK. + + flow_data_sdk/base/_version.py hardcodes the wheel distribution name as + "adsk-flow-data" and queries importlib.metadata.version() with it. The + published wheel is actually named "flow-data-sdk", so the lookup raises + PackageNotFoundError and SDK_VERSION falls back to the literal string + "local_dev" even when the .dist-info is present in our shared zip. + + Until the SDK ships a fix (either rename the wheel to "adsk-flow-data" or + change _version.py to query "flow-data-sdk"), this patch overrides + SDK_VERSION with the value pip recorded in the wheel's METADATA. The + patch is a no-op once SDK_VERSION is no longer "local_dev", so it + quietly disappears the moment upstream is fixed. + """ + if "flow_data_sdk" not in sys.modules: + return + + flow_data_sdk = sys.modules["flow_data_sdk"] + if getattr(flow_data_sdk, "SDK_VERSION", None) != "local_dev": + return + + from importlib.metadata import PackageNotFoundError, version + + # The wheel's current published distribution name. If upstream eventually + # renames to adsk-flow-data, _version.py will resolve correctly and this + # patch returns early above; this lookup is the bridge for today's wheel. + try: + flow_data_sdk.SDK_VERSION = version("flow-data-sdk") + except PackageNotFoundError: + pass + + def _install_import_hook(): """ Install a lazy import hook that redirects tank_vendor.* imports to real packages. This enables transparent namespace aliasing, allowing code to use tank_vendor.package - while the actual package is loaded from pkgs.zip without the tank_vendor prefix. + while the actual package is loaded from a ZIP without the tank_vendor prefix. Examples: from tank_vendor.shotgun_api3.lib import httplib2 @@ -208,7 +251,7 @@ def _install_import_hook(): How it works: 1. Intercepts imports starting with "tank_vendor." 2. Strips the "tank_vendor." prefix to get the real module name - 3. Imports the real module (e.g., "shotgun_api3" → tank_vendor.shotgun_api3) + 3. Imports the real module (e.g., "shotgun_api3" -> tank_vendor.shotgun_api3) 4. Creates an alias in sys.modules so both names refer to the same module Why lazy loading: @@ -227,121 +270,165 @@ def _install_import_hook(): sys.meta_path.insert(0, sys._tank_vendor_meta_finder) -# ============================================================================ -# MAIN INITIALIZATION: Load third-party packages from pkgs.zip -# ============================================================================ +def _discover_top_level_packages(zip_path): + """ + Return the set of top-level importable package names inside a zip. + + Filters out: + - .dist-info: Package metadata directories (still in zip for importlib.metadata, + but not importable as packages) + - __pycache__: Python bytecode cache + - .pyd/.so/.dylib: Platform-specific binary extensions + - _*: Private/internal modules (e.g., _ruamel_yaml.cp311-win_amd64.pyd) + """ + with zipfile.ZipFile(zip_path, "r") as zf: + top_level = set() + for name in zf.namelist(): + parts = name.split("/") + if parts[0] and not parts[0].endswith(".py"): + top_level.add(parts[0]) + elif parts[0].endswith(".py") and parts[0] != "__pycache__": + top_level.add(parts[0][:-3]) + + return { + pkg + for pkg in top_level + if not pkg.endswith(".dist-info") + and pkg != "__pycache__" + and not pkg.endswith(".py") + and not pkg.endswith(".pyd") + and not pkg.endswith(".so") + and not pkg.endswith(".dylib") + and not pkg.startswith("_") + } + + +def _load_packages_from_zip(zip_path, *, required, path_position): + """ + Validate a vendor zip, insert it on sys.path, and register its top-level + packages under the tank_vendor namespace. -# Construct path to Python version-specific pkgs.zip containing third-party dependencies. -# Path structure: /requirements/./pkgs.zip -# Example: requirements/3.11/pkgs.zip for Python 3.11 -pkgs_zip_path = ( - pathlib.Path(__file__).resolve().parent.parent.parent - / "requirements" - / f"{sys.version_info.major}.{sys.version_info.minor}" - / "pkgs.zip" -) + Args: + zip_path: pathlib.Path to the zip file. + required: If True, failure to validate or import raises RuntimeError. + If False, failures emit warnings and the loader continues. + path_position: Index passed to sys.path.insert. Use 0 for the primary + zip (pkgs.zip) so it wins over system installs; higher indices for + additional zips so the primary still takes precedence. + + Returns: + True if the zip was successfully loaded, False otherwise. + """ + # Validate zip before attempting to load from it. + if not zip_path.exists() or not zip_path.is_file(): + if required: + return False + # Optional zip simply absent: nothing to do, no warning. + return False + + try: + with zipfile.ZipFile(zip_path, "r") as zf: + zf.namelist() + except (zipfile.BadZipFile, OSError, IOError) as e: + msg = ( + f"Failed to load packages from {zip_path}: {e}. " + "Third-party dependencies from this zip will not be available." + ) + if required: + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return False + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return False + + # Insertion ordering is load-bearing: importlib.metadata.version() resolves + # dist-info inside a zip only after the zip is on sys.path. + sys.path.insert(path_position, str(zip_path)) -# Validate pkgs.zip before attempting to load from it. -# This provides backward compatibility for: -# - Installations using old vendored copies -# - Temporary locations without the requirements directory -# - CI/CD environments where pkgs.zip might be extracted to a directory -_pkgs_zip_valid = False -if pkgs_zip_path.exists(): - # Check if it's a file (not a directory) - in some CI environments, - # pkgs.zip might be extracted to a directory instead of kept as a ZIP. - if pkgs_zip_path.is_file(): - # Validate that it's actually a valid ZIP file before adding to sys.path - try: - with zipfile.ZipFile(pkgs_zip_path, "r") as zf: - # Quick validation - just check that we can read the ZIP directory - zf.namelist() - _pkgs_zip_valid = True - except (zipfile.BadZipFile, OSError, IOError) as e: - # Not a valid ZIP file or can't be read - skip loading from pkgs.zip - warnings.warn( - f"Failed to load packages from {pkgs_zip_path}: {e}. " - "Third-party dependencies will be loaded from the Python environment instead.", - RuntimeWarning, - stacklevel=2, - ) - -# If pkgs.zip is not found, assume pip-style installation where dependencies -# are installed directly in the Python environment. In this case, we still -# install the import hook to enable tank_vendor.* aliasing for compatibility. -if not _pkgs_zip_valid: - # Install import hook even without pkgs.zip for pip installations - _install_import_hook() -else: - # Add pkgs.zip to sys.path so Python can import packages directly from the ZIP. - # Insert at position 0 to prioritize over other installed packages. - sys.path.insert(0, str(pkgs_zip_path)) try: - # Step 1: Auto-discover all top-level packages in pkgs.zip import importlib - with zipfile.ZipFile(pkgs_zip_path, "r") as zf: - # Get all top-level package names from the ZIP - top_level_packages = set() - for name in zf.namelist(): - # Extract first component of path (top-level package/module) - parts = name.split("/") - if parts[0] and not parts[0].endswith(".py"): - # It's a package directory - top_level_packages.add(parts[0]) - elif parts[0].endswith(".py") and parts[0] != "__pycache__": - # It's a top-level module file - top_level_packages.add(parts[0][:-3]) # Remove .py - - # Filter out non-importable items: - # - .dist-info: Package metadata directories - # - __pycache__: Python bytecode cache - # - .py: Single file modules (already captured as packages) - # - .pyd/.so/.dylib: Platform-specific binary extensions - # - _*: Private/internal modules (e.g., _ruamel_yaml.cp311-win_amd64.pyd) - top_level_packages = { - pkg - for pkg in top_level_packages - if not pkg.endswith(".dist-info") - and pkg != "__pycache__" - and not pkg.endswith(".py") - and not pkg.endswith(".pyd") # Windows binary modules - and not pkg.endswith(".so") # Unix/Linux binary modules - and not pkg.endswith(".dylib") # macOS binary modules - and not pkg.startswith("_") # Private/internal modules - } - - # Step 2: Import and register each top-level package under tank_vendor namespace + top_level_packages = _discover_top_level_packages(zip_path) + for package_name in sorted(top_level_packages): + # Collision check: an earlier zip already claimed this name. + # Earlier zips win (pkgs.zip is loaded before requirements/any/). + if f"tank_vendor.{package_name}" in sys.modules: + warnings.warn( + f"Skipping {package_name} from {zip_path}: " + f"already registered under tank_vendor.{package_name} " + f"from an earlier zip.", + RuntimeWarning, + ) + continue + try: - # Import the package mod = importlib.import_module(package_name) - - # Register in sys.modules under tank_vendor namespace sys.modules[f"tank_vendor.{package_name}"] = mod - - # Also set as attribute on tank_vendor module for direct access globals()[package_name] = mod - except ImportError as e: - # Some packages might not import cleanly on all platforms - # Log but don't fail - they might not be needed - warnings.warn(f"Could not import {package_name} from pkgs.zip: {e}") + # Per-package import failures are tolerated. The most common + # cause is a package using syntax newer than the current Python + # (e.g. flow_data_sdk uses types.UnionType which is 3.10+). + warnings.warn( + f"Could not import {package_name} from {zip_path}: {e}" + ) - # Step 3: Install import hook for lazy submodule loading - # This enables imports like: from tank_vendor.shotgun_api3.lib import httplib2 - # without pre-importing all submodules (which can fail on version incompatibilities) - _install_import_hook() + except Exception as e: + # Clean up sys.path on a wholesale failure so we don't leave a + # non-functional zip on the path interfering with other imports. + try: + sys.path.remove(str(zip_path)) + except ValueError: + pass + if required: + raise RuntimeError( + f"Failed to import required modules from {zip_path}: {e}" + ) from e + warnings.warn( + f"Failed to import modules from {zip_path}: {e}", + RuntimeWarning, + ) + return False + + return True - # Step 4: Apply package-specific patches - # These patches work around limitations or fix issues with specific packages - if "shotgun_api3" in sys.modules: - _patch_shotgun_api3_certs(pkgs_zip_path) - except Exception as e: - # Clean up sys.path on failure to avoid leaving it in an inconsistent state - # with a non-functional ZIP path that could interfere with subsequent imports - sys.path.remove(str(pkgs_zip_path)) - raise RuntimeError( - f"Failed to import required modules from {pkgs_zip_path}: {e}" - ) from e +# ============================================================================ +# MAIN INITIALIZATION +# ============================================================================ + +_requirements_dir = pathlib.Path(__file__).resolve().parent.parent.parent / "requirements" + +# 1. Per-Python-version zip (mandatory). Contains pinned dependencies with +# binary extensions; load order keeps it ahead of any shared zips so its +# versions take precedence on name collision. +_pkgs_zip_path = ( + _requirements_dir + / f"{sys.version_info.major}.{sys.version_info.minor}" + / "pkgs.zip" +) +_pkgs_loaded = _load_packages_from_zip( + _pkgs_zip_path, required=True, path_position=0 +) +if _pkgs_loaded and "shotgun_api3" in sys.modules: + _patch_shotgun_api3_certs(_pkgs_zip_path) + +# 2. Shared zips (optional, Python-version-independent). Drop a *.zip into +# requirements/any/ and it will be loaded automatically. Shared vendors are +# expected to use the system trust store and not ship data files that would +# need extraction from inside the zip. +_shared_dir = _requirements_dir / "any" +if _shared_dir.is_dir(): + for _i, _shared_zip in enumerate(sorted(_shared_dir.glob("*.zip")), start=1): + _load_packages_from_zip( + _shared_zip, required=False, path_position=_i + ) + +# Bridge an upstream bug in the Flow Data SDK's _version.py — see the +# docstring on _patch_flow_data_sdk_version. No-op once upstream is fixed. +_patch_flow_data_sdk_version() + +# 3. Install the lazy import hook for nested submodule access. +# Idempotent via the _tank_vendor_meta_finder guard, so calling it once +# after both load steps is safe and sufficient. +_install_import_hook() diff --git a/requirements/any/flow_data_sdk-beta.zip b/requirements/any/flow_data_sdk-beta.zip new file mode 100644 index 0000000000000000000000000000000000000000..7a68a900784f535efe4cf3ee772f252580aebb91 GIT binary patch literal 68904 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∋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 literal 0 HcmV?d00001 diff --git a/tests/core_tests/test_tank_vendor.py b/tests/core_tests/test_tank_vendor.py index 2853154ec..d58c4eb4d 100644 --- a/tests/core_tests/test_tank_vendor.py +++ b/tests/core_tests/test_tank_vendor.py @@ -9,6 +9,7 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import importlib +import re import sys import unittest from unittest import mock @@ -17,7 +18,8 @@ from tank_test.tank_test_base import ShotgunTestBase # Configuration: Add or remove packages here to test different third-party libraries -# Only include packages that are directly bundled in requirements//pkgs.zip +# Packages from pkgs.zip are always tested. Packages from requirements/any/ +# are version-gated below. PACKAGES_TO_TEST = [ { "name": "yaml", @@ -41,6 +43,24 @@ }, ] +# Flow Data SDK uses types.UnionType and typing.TypeAlias, both 3.10+ only. +# On 3.7/3.9 the shared loader will warn-and-continue; do not assert it here. +if sys.version_info >= (3, 10): + PACKAGES_TO_TEST.append( + { + "name": "flow_data_sdk", + "attributes": [ + "GQLClient", + "WorkflowContext", + "SDK_VERSION", + "DEFAULT_ENDPOINT", + "DEFAULT_AUTH_BASE_URL", + "GQLAPIError", + ], + "description": "Autodesk Flow Data SDK (beta)", + } + ) + class TestTankVendorImports(ShotgunTestBase): """Test importing third-party packages via tank_vendor namespace.""" @@ -246,5 +266,62 @@ def test_cert_file_returns_path(self): self.assertTrue(len(cert_path) > 0) +@unittest.skipIf( + sys.version_info < (3, 10), + "Flow Data SDK requires Python 3.10+ (uses types.UnionType / typing.TypeAlias)", +) +class TestFlowDataSDK(ShotgunTestBase): + """Test the Flow Data SDK loaded from requirements/any/.""" + + def test_submodule_import(self): + """Lazy meta-finder resolves nested imports inside the shared zip.""" + from tank_vendor.flow_data_sdk.base import client + from tank_vendor.flow_data_sdk.base.exceptions import GQLAPIError + + self.assertTrue(hasattr(client, "BaseGQLClient")) + self.assertIsNotNone(GQLAPIError) + + def test_sdk_version_resolved_from_dist_info(self): + """ + Canary: SDK_VERSION must NOT fall back to 'local_dev'. + + flow_data_sdk/base/_version.py resolves SDK_VERSION via + importlib.metadata, which only succeeds when the SDK's .dist-info + directory was preserved in the shared zip. If this fails, the shared + zip in requirements/any/ is missing its .dist-info. + """ + from tank_vendor import flow_data_sdk + + self.assertNotEqual( + flow_data_sdk.SDK_VERSION, + "local_dev", + "SDK_VERSION fell back to 'local_dev' — the shared zip is " + "missing .dist-info.", + ) + self.assertRegex( + flow_data_sdk.SDK_VERSION, + r"^\d+\.\d+", + "SDK_VERSION is not a PEP 440 version", + ) + + def test_dist_info_via_importlib_metadata(self): + """ + importlib.metadata can read the version from the dist-info inside + the shared zip. + + NOTE: the wheel's distribution name is "flow-data-sdk" but the SDK's + own _version.py queries the wrong name ("adsk-flow-data"). Until + upstream fixes that, tank_vendor patches SDK_VERSION at load time + (see _patch_flow_data_sdk_version in tank_vendor/__init__.py). This + test asserts the wheel's actual name resolves, which is what proves + the dist-info is being discovered from inside the zip. + """ + from importlib.metadata import version + + from tank_vendor import flow_data_sdk + + self.assertEqual(version("flow-data-sdk"), flow_data_sdk.SDK_VERSION) + + if __name__ == "__main__": unittest.main() From 13366d6f5a9cabf08251a08a78891fa86d523dde Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Thu, 14 May 2026 13:04:11 +0100 Subject: [PATCH 02/11] SG-43217 Drop _patch_flow_data_sdk_version now that upstream is fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Flow Data SDK previously hardcoded the wrong distribution name in flow_data_sdk/base/_version.py ("adsk-flow-data" instead of the actual published name "flow-data-sdk"), so importlib.metadata.version() always raised PackageNotFoundError and SDK_VERSION fell back to "local_dev" even with .dist-info present in our shared zip. The new SDK zip ships the upstream one-line fix — _version.py now queries the correct name and SDK_VERSION resolves to the real version on its own. The local workaround patch can go. Removes: - _patch_flow_data_sdk_version function and its call site in tank_vendor/__init__.py (~37 lines) - The upstream-bug commentary in the test_dist_info_via_importlib_metadata docstring (the assertion itself is unchanged and still passes) Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 38 ------------------------ requirements/any/flow_data_sdk-beta.zip | Bin 68904 -> 68899 bytes tests/core_tests/test_tank_vendor.py | 12 +------- 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 2d6b5b2dd..5a15f8fe0 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -203,40 +203,6 @@ def _patched_get_certs_file(ca_certs=None): shotgun_api3.Shotgun._get_certs_file = staticmethod(_patched_get_certs_file) -def _patch_flow_data_sdk_version(): - """ - Work around an upstream bug in the Flow Data SDK. - - flow_data_sdk/base/_version.py hardcodes the wheel distribution name as - "adsk-flow-data" and queries importlib.metadata.version() with it. The - published wheel is actually named "flow-data-sdk", so the lookup raises - PackageNotFoundError and SDK_VERSION falls back to the literal string - "local_dev" even when the .dist-info is present in our shared zip. - - Until the SDK ships a fix (either rename the wheel to "adsk-flow-data" or - change _version.py to query "flow-data-sdk"), this patch overrides - SDK_VERSION with the value pip recorded in the wheel's METADATA. The - patch is a no-op once SDK_VERSION is no longer "local_dev", so it - quietly disappears the moment upstream is fixed. - """ - if "flow_data_sdk" not in sys.modules: - return - - flow_data_sdk = sys.modules["flow_data_sdk"] - if getattr(flow_data_sdk, "SDK_VERSION", None) != "local_dev": - return - - from importlib.metadata import PackageNotFoundError, version - - # The wheel's current published distribution name. If upstream eventually - # renames to adsk-flow-data, _version.py will resolve correctly and this - # patch returns early above; this lookup is the bridge for today's wheel. - try: - flow_data_sdk.SDK_VERSION = version("flow-data-sdk") - except PackageNotFoundError: - pass - - def _install_import_hook(): """ Install a lazy import hook that redirects tank_vendor.* imports to real packages. @@ -424,10 +390,6 @@ def _load_packages_from_zip(zip_path, *, required, path_position): _shared_zip, required=False, path_position=_i ) -# Bridge an upstream bug in the Flow Data SDK's _version.py — see the -# docstring on _patch_flow_data_sdk_version. No-op once upstream is fixed. -_patch_flow_data_sdk_version() - # 3. Install the lazy import hook for nested submodule access. # Idempotent via the _tank_vendor_meta_finder guard, so calling it once # after both load steps is safe and sufficient. diff --git a/requirements/any/flow_data_sdk-beta.zip b/requirements/any/flow_data_sdk-beta.zip index 7a68a900784f535efe4cf3ee772f252580aebb91..a931e87c0e3805765d2e581a9d8a25ddf36c6b3b 100644 GIT binary patch delta 3206 zcmZ8j2{=^y8$M^u41*cmzcluJ=i**M_AJ>VBCa)(sJL=TrHrmfSq29qYYdS!*E-1! zSyI^}+l?&IGL{xu!ku%b+w=IJdFFiI?|r}hoaZ@TNhX?_iN+tbVnOo)@U`h%nT(fb zfra)#L|ph87S+XM7FgD3qxnHtfgCaF2oR6)M1Yzl>@+)q4Dis{5G6sN5|1eQa$Cm{ zMVHjXL^k6!KmsF-+w614s$xcBeh?BT3qVeZqWEXsDld;%C|SHvw&^|&T~8DTEup)K z$~>|$G%O~3a|?85g@)P8{-8l~i7Iedh>uL;frB9}e7`9}$)l9)9Anizo+sE6S z+_bE}M{4r+)!d(>o9=M6tUUce6Uu`Y)bx10OS6)WNl{-+2lCZ7UDh%QCkN`5eI|PS zR=2?-DLZ25MU?RM2!!8F}5Xk`r7#6~A+PF)Tnk@|5<4s3J36 z{l~HAU7aTsg;Qj>+O&uQ*S+PoZ-?yIIB6=-)>7w`SVP$lNk+d7?2*z-)bQz@WS5mu ztR=0}-tA|T=I6bdGh1nUW9YM+j(e6|+?o3>Nm;ArfmSo^>SDH~nddfi3vX!f$Tokj zcOc|z{n>mxcj@Y;en(Hhqo#4Mvc7azY5IvFYimr0{!rGwO1r|OGZ{WZ73%HWMneW? zcZ&IQ?N1!>%BEg6RKQ=>_sdig!7l$LU?Rdw>XoCV#!(k1&B~_@iCBGSiKX%Kiz?>4 zQnzDA9UJlQCAP#>Oy|$LMw{~Wj+vTPi9C{Q9#kI|YFp(xEHw1mrTs1OO;!BJz93is z)QZAbBku0P#yKNQjGO2wap&y9KC3$Sp?<1!EUI00#v(K4&1LqkTASJ5>%#F4N-hHJ zq;2ztPeE^-Q{qf$tJDv^8>UV<^6sVY9lI~tn2M#Ye>hMTN$My*Z=J_cQ>AXIE!Sx2 z6x^Uji@GV!^3W>oAKrK2rKv8RIwI=PrH-t2-uK8pPr%Hyt z+^Xcb?)q{D^j>L17fNbqmfe%Q5PC~o2qcNdbWr-Z_tE_u4XTAtJ*$7Au%)x$-6^X$ zaP)5Q&n4xox_4ztY_Tpi(apy|G1P8?fZa;{EJpth58 zJGFD2E>FI!DA7B!yXB($I9gg}p}_Esltku{sbnSVSA4yh%Iih`)KR%H*ai}O!D{o=CK7{5q=0F`884fm4hygw}^lUrn zhJsSFF#B& zER-tu$AK5Gbl%=PzJ;qSp$3@~^p&F}PsyFeC)RjG`OO@Ib~Uf4SrT8+srl;0UQV%! z(H=kDmHP+9ComJ(_m#kJ`*mbWcJ3Roo7kdX_WsxT`F`iMa|%*CQ>q7#wP;p&kfK#H zuf|AO9-jO;yK;K}=+FRAAz~np-9XVhF2qSIKPdX^9Cy>^B>9_ z9r1h~-Tqr_r+xrsIdxYJC$m{HxRxA#KgB=iRzTJ3hXRC>8+}p(6*+eGhAF=G5#?U( zUGuZjZ=)7+GL}pZG$;GH@<*43){`^@*o8BCL|DeeZVeu{$QTvmLl<>z!KN6Y6<)#zytlxm%Mw{iB@BJPg60`xC$1xP)7N zQ(yWq#mC*{<=iJd(*lvgKB`)C4qrT#;+;KR(DT4xuK83=sY1pSk5KL}R71AE{q1k3vfOSCZ12?I zNal>qe5On0FLuq;?bQE#_R^92zman5#zeQ`?x|_orW_7hyx`F?AXrcx{-CrflBF~u z-32mZKY2z51pjP}z^P&$^C-DalRkv2s}Qf^3KK&AG9em6_V+oi%Nc}TbuNBc8<7<_6owwVRHs>$Z-1KlclG;wH;Rq<#j#H}gR|*oVGpv4 z4)j#t`s6%B>JE}NI!f|ePRMqv3Ve05&KNVZG3YaJCA6(3!eOL^t1(;|`2E%`jJL>E zPu_p)2OGS8KyKTn$d|5g+ZmC&+xdv3?r0-&Y6tlOLQ1>($T?uw1d+A7hY-2JJf|6g z2%dHWJPhM$1E3WwDWc$aB>)7?5u_0Wk&qM&3N*vRAJrdQ3B?%&8X!iQC{Pf^Muv|G~ zhl%rdz7tr=Gz-X&5Z_=zvj`?Ax@d4el2s@pzHbW!0QYbJp!y>#0MuXWETasdL1{_` z8sry+3oLyyWI2Ea04HGpP}*b4Md?R_I!F=%hLJ>}7D-}*0eKP68kvVj+F1d>RsjH% z_jqbhu3lc5zV3*}q$VS#)zi zg8)#@Xo#_0{kZC{jr1}`Cjw5O&|Nr4N#1dH?u>f$A007c^EM3+ROLk81CnSd`CnJX?C7BbHpty5_ z`;bSWJi79UhvCM>`0stQQeaEwo7D$<(h*NcWe3A|)C$AM$k?G7zX(QN@(z6Z7pyLA A3;+NC delta 3272 zcmZWr2{=@H8$V|lOJnRJ#E6o0E|0Y&H%s;p5Is0mkRo61LN9Bf;y&$$O=JJ3n277C%d4!jd3FNRx!4G2kxf97T-ZTB&=`rlXxW zWMiRf;RGd647|n- z;-G=yO;m4aBzG%2YLvL;l#HF-Y7RG{wB_hB!?!{e)88=cEU0v*47F!imZ(ZGs{zl; zZvS9`Yj`CT{6&Kdw`b&=3twOG*13oo2znlRhr>&VU{vZw^Yjus>?I^@H~#o;@ocB1 zO?Y@uS8B@!ao-r3-kuek+P54{d04Zdmi+2Tc0Ox#G45f<9**f}*5?H-pSnK5$&kI= z9Oe^I`zQ$ByG9a|)s9_0LiGb<^5rsoHom2;-SdjhsdA|z2lvCch6c$Ar}VC-y2DG8 zypeOy--68Cg-D95WuN~r=GqBX;E0=!t?zhK`d*3R$kN2h$B7)uEA$$>;}#~85JS;XYl_&U)GD`FK3E-CP*&8e0D1#t4}b6kD7qb_`5-Q0_P+FcO(F zKGV(c>pS39Dl{E_2&~-KZEYnU@HUP@+;?HDZ#~GV*?hNPvixx1yKfpk7t4QddY_mj z686H26!s>S{|^P9Q?s<*UqTZ06~1nX>oN(jCR}|GJFsS_(b#|?Q8F;v$MR}e?}`GJ zHuP49x)hSh3Re`{<&Q|4!s>ol@z$SK6ZU5`cXT|xYQk$&PyeMgX3SYXDN^Z)t=C>^ zRL!)DSkt#P@GI4~7<-K=Tc_vCEVHgsQg+`{Mx1;yXnwq15F+%ser$0lw4v#@AN#%j zZ+oW;Pkqzvsst@Wa+4mp#=2+fXI34sYMzJ5r7ywgcqZrFP} zCPofl9H3J;p)VG1KULk|h}jUzeWXOnm1r6ieOEN0e6nM`dy2CAddi}lioj(bme2mM z6!M`ZFZiyYt+&EDxr+Y5*>=q%;D9gojR}8alrD4_dZwIGZ;o%sSEJgB-BX(9*L9YvR-@23j+E|cnnq@|e z-TC~;K5ghUQ%B&pJ6CTS--VJQD={ur8m_8u)jU4l$d|i*zh#_Dz}w?UTf`zuv#9QZ zh)vpjNT-Mu)nGE(fw59cv>ugntF63P*PtkpWVTU7+f1T2xBn?iA`JERi zdJpMw84qq~QEzf7e#`q=^kZ@Rrc|^4e!f}!$5XE9AqQ8aZn!%>ENv9teR^tAHBIJj zcbYoGvBJP@>GG|ws^tqx#H<&OtMA@=5#1qS%{xx6^%Hpz$tz*xH1B>!-}rfX#l<|i z(?^e<;9J7Jx%ovfG3kT>tgSe0_pli+U(}kX;fPHVektU6&a2|IN{ee!tDzb!dwa8& z*KfAqME4C+RbNz=)g?xVd#c%My13CNw~Nku)huwgY6gXc?*)1rV6-fv7Ni0$|BYmf->H*n?Nu8Hm^Aqo_QCox z(_!^hg2{}pE|jI`)s#!6l_r^iyZTKs3ZrTdlaHTm{MAlKB4RD22G1>TqDmlewKbpc zA$}NfZS+pm%lFX@w6n%p(zzLJ=ScXF^VJS_Yj7?xML(T47Im=xOKQVNkX*bxIBm1RTDclZM z14?23NQ2UciLe?6$cKUk90(|Q3v!^zLlt>k#PjMQQTA3KMigLgVdy)bIs3C_$`wuk zs1pVNtsl~et&0zZD>*=80=tD}we|0&TmW!I8UR#xkn{@K>aZ>+D2|qN;6zH&OVE2s9%9Se$~ zZBc48I)jlN%E9!XZ4u~fCge`X!R}n!62V-E#85X9kb>|%IJn|R;2Arxmx0VqAIcfz zf~B}YbfJUQxsib7;C4U+W+8(`3-8#scFZGu7-5^J{&ULwFKe-e$Z}?h1pu=h2^}M- zyzHC*nN>0mQa5;%9e^izwkNyF26M+y(4H3*MH`v1nDFSp0Kjco05IO6yE%?Bn|MJ9 zl$r4=-d_soKnr0T?O>`f(GJK=-v4XcOycRiY$QQ-FS1weNGg~{<+FEz;;6iHzzTR8 zVL-^Ow}Y8CgD{EE>Sd&Q0{ZS#5XpcvQf4R(e903r*0p#T5? diff --git a/tests/core_tests/test_tank_vendor.py b/tests/core_tests/test_tank_vendor.py index d58c4eb4d..f9432a402 100644 --- a/tests/core_tests/test_tank_vendor.py +++ b/tests/core_tests/test_tank_vendor.py @@ -305,17 +305,7 @@ def test_sdk_version_resolved_from_dist_info(self): ) def test_dist_info_via_importlib_metadata(self): - """ - importlib.metadata can read the version from the dist-info inside - the shared zip. - - NOTE: the wheel's distribution name is "flow-data-sdk" but the SDK's - own _version.py queries the wrong name ("adsk-flow-data"). Until - upstream fixes that, tank_vendor patches SDK_VERSION at load time - (see _patch_flow_data_sdk_version in tank_vendor/__init__.py). This - test asserts the wheel's actual name resolves, which is what proves - the dist-info is being discovered from inside the zip. - """ + """importlib.metadata sees the same version as the SDK reports.""" from importlib.metadata import version from tank_vendor import flow_data_sdk From 176b338ca6dea0cb627f54bdc81f5adaeb212a30 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Thu, 14 May 2026 13:37:13 +0100 Subject: [PATCH 03/11] SG-43217 Drop _load_packages_from_zip required flag The required parameter was only meaningful in the late-stage-exception branch (raise vs warn). The other two failure paths returned False identically in both modes, so the parameter was mostly dead weight. Collapse to always-raise for any zip's wholesale load failure, matching the original pkgs.zip posture. Shared zips in requirements/any/ are now held to the same standard: a corrupt or import-broken shared zip will fail import tank_vendor with a clean RuntimeError instead of silently degrading. Per-package ImportError inside the loop still warns and skips, so flow_data_sdk being absent on Python 3.7/3.9 (3.10+ syntax) remains non-fatal. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 45 +++++++++++++--------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 5a15f8fe0..c5f4ef45e 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -269,41 +269,40 @@ def _discover_top_level_packages(zip_path): } -def _load_packages_from_zip(zip_path, *, required, path_position): +def _load_packages_from_zip(zip_path, *, path_position): """ Validate a vendor zip, insert it on sys.path, and register its top-level packages under the tank_vendor namespace. + Missing or unreadable zips are tolerated (return False, with a warning + for unreadable). A wholesale failure during package discovery/import + raises RuntimeError after cleaning the zip path off sys.path. Individual + package import failures inside the zip warn and are skipped. + Args: zip_path: pathlib.Path to the zip file. - required: If True, failure to validate or import raises RuntimeError. - If False, failures emit warnings and the loader continues. path_position: Index passed to sys.path.insert. Use 0 for the primary zip (pkgs.zip) so it wins over system installs; higher indices for additional zips so the primary still takes precedence. Returns: - True if the zip was successfully loaded, False otherwise. + True if the zip was successfully loaded, False if it was missing + or unreadable. """ # Validate zip before attempting to load from it. if not zip_path.exists() or not zip_path.is_file(): - if required: - return False - # Optional zip simply absent: nothing to do, no warning. return False try: with zipfile.ZipFile(zip_path, "r") as zf: zf.namelist() except (zipfile.BadZipFile, OSError, IOError) as e: - msg = ( + warnings.warn( f"Failed to load packages from {zip_path}: {e}. " - "Third-party dependencies from this zip will not be available." + "Third-party dependencies from this zip will not be available.", + RuntimeWarning, + stacklevel=2, ) - if required: - warnings.warn(msg, RuntimeWarning, stacklevel=2) - return False - warnings.warn(msg, RuntimeWarning, stacklevel=2) return False # Insertion ordering is load-bearing: importlib.metadata.version() resolves @@ -346,15 +345,9 @@ def _load_packages_from_zip(zip_path, *, required, path_position): sys.path.remove(str(zip_path)) except ValueError: pass - if required: - raise RuntimeError( - f"Failed to import required modules from {zip_path}: {e}" - ) from e - warnings.warn( - f"Failed to import modules from {zip_path}: {e}", - RuntimeWarning, - ) - return False + raise RuntimeError( + f"Failed to import required modules from {zip_path}: {e}" + ) from e return True @@ -373,9 +366,7 @@ def _load_packages_from_zip(zip_path, *, required, path_position): / f"{sys.version_info.major}.{sys.version_info.minor}" / "pkgs.zip" ) -_pkgs_loaded = _load_packages_from_zip( - _pkgs_zip_path, required=True, path_position=0 -) +_pkgs_loaded = _load_packages_from_zip(_pkgs_zip_path, path_position=0) if _pkgs_loaded and "shotgun_api3" in sys.modules: _patch_shotgun_api3_certs(_pkgs_zip_path) @@ -386,9 +377,7 @@ def _load_packages_from_zip(zip_path, *, required, path_position): _shared_dir = _requirements_dir / "any" if _shared_dir.is_dir(): for _i, _shared_zip in enumerate(sorted(_shared_dir.glob("*.zip")), start=1): - _load_packages_from_zip( - _shared_zip, required=False, path_position=_i - ) + _load_packages_from_zip(_shared_zip, path_position=_i) # 3. Install the lazy import hook for nested submodule access. # Idempotent via the _tank_vendor_meta_finder guard, so calling it once From b907871a12e4dc2ec0c11fe72054436162b43e35 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Thu, 14 May 2026 13:42:02 +0100 Subject: [PATCH 04/11] Removed unused import --- tests/core_tests/test_tank_vendor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core_tests/test_tank_vendor.py b/tests/core_tests/test_tank_vendor.py index f9432a402..477066ea9 100644 --- a/tests/core_tests/test_tank_vendor.py +++ b/tests/core_tests/test_tank_vendor.py @@ -9,7 +9,6 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import importlib -import re import sys import unittest from unittest import mock From f96c7d685bbae7b240089718853fb13ec6c1eb8a Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 15 May 2026 12:07:51 +0100 Subject: [PATCH 05/11] SG-43217 Put shared zips ahead of pkgs.zip on sys.path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a regression in the share_core integration test on Windows / Python 3.13. flow_data_sdk's _version.py calls importlib.metadata.version( "flow-data-sdk") at import time. importlib.metadata iterates sys.path in order, and FastPath.zip_children() is @lru_cache'd — the cached FastPath holds an open zipfile.ZipFile to whichever zip it probed. Previously pkgs.zip was at sys.path[0] and flow_data_sdk-beta.zip at sys.path[1], so the scan probed pkgs.zip first (no match → cached open handle), then matched in flow_data_sdk-beta.zip. The lingering handle on pkgs.zip caused share_core's shutil.move to fail with WinError 32 when relocating install/core on Windows. Reorder so shared zips end up at sys.path[0], pkgs.zip at sys.path[1]. importlib.metadata then short-circuits on the first probe and never opens pkgs.zip. pkgs.zip is still loaded into sys.modules first, so collision precedence is unchanged. Drop the path_position parameter from _load_packages_from_zip — every zip is now always inserted at sys.path[0], and the call order in tank_vendor/__init__.py determines the final sys.path ordering. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 48 ++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index c5f4ef45e..09337be23 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -269,21 +269,23 @@ def _discover_top_level_packages(zip_path): } -def _load_packages_from_zip(zip_path, *, path_position): +def _load_packages_from_zip(zip_path): """ - Validate a vendor zip, insert it on sys.path, and register its top-level - packages under the tank_vendor namespace. + Validate a vendor zip, insert it at the front of sys.path, and register + its top-level packages under the tank_vendor namespace. Missing or unreadable zips are tolerated (return False, with a warning for unreadable). A wholesale failure during package discovery/import raises RuntimeError after cleaning the zip path off sys.path. Individual package import failures inside the zip warn and are skipped. + Each zip is always inserted at sys.path[0], so the LAST zip loaded ends + up at the front of sys.path. Collisions are resolved by sys.modules + (first-registered wins), independent of sys.path order — see callers + for the intentional load order. + Args: zip_path: pathlib.Path to the zip file. - path_position: Index passed to sys.path.insert. Use 0 for the primary - zip (pkgs.zip) so it wins over system installs; higher indices for - additional zips so the primary still takes precedence. Returns: True if the zip was successfully loaded, False if it was missing @@ -307,7 +309,7 @@ def _load_packages_from_zip(zip_path, *, path_position): # Insertion ordering is load-bearing: importlib.metadata.version() resolves # dist-info inside a zip only after the zip is on sys.path. - sys.path.insert(path_position, str(zip_path)) + sys.path.insert(0, str(zip_path)) try: import importlib @@ -358,26 +360,38 @@ def _load_packages_from_zip(zip_path, *, path_position): _requirements_dir = pathlib.Path(__file__).resolve().parent.parent.parent / "requirements" -# 1. Per-Python-version zip (mandatory). Contains pinned dependencies with -# binary extensions; load order keeps it ahead of any shared zips so its -# versions take precedence on name collision. +# Load order matters for two distinct reasons: +# +# 1. sys.modules registration: the FIRST zip to register a top-level package +# wins (later zips' duplicates are skipped). So pkgs.zip is loaded first +# to keep its version-pinned dependencies authoritative. +# +# 2. sys.path order: we insert each zip at sys.path[0], so the LAST zip +# loaded ends up at the front. We want shared zips ahead of pkgs.zip on +# sys.path so that importlib.metadata.version() lookups (e.g. flow_data_sdk's +# _version.py querying its own dist-info) short-circuit on the shared zip +# and never scan pkgs.zip. Scanning pkgs.zip via importlib.metadata caches +# a FastPath instance that holds an open zipfile, which on Windows +# prevents the tank share_core command from moving install/core. +# +# So: load pkgs.zip first (sys.modules), then shared zips (sys.path front). _pkgs_zip_path = ( _requirements_dir / f"{sys.version_info.major}.{sys.version_info.minor}" / "pkgs.zip" ) -_pkgs_loaded = _load_packages_from_zip(_pkgs_zip_path, path_position=0) +_pkgs_loaded = _load_packages_from_zip(_pkgs_zip_path) if _pkgs_loaded and "shotgun_api3" in sys.modules: _patch_shotgun_api3_certs(_pkgs_zip_path) -# 2. Shared zips (optional, Python-version-independent). Drop a *.zip into -# requirements/any/ and it will be loaded automatically. Shared vendors are -# expected to use the system trust store and not ship data files that would -# need extraction from inside the zip. +# Shared zips (optional, Python-version-independent). Drop a *.zip into +# requirements/any/ and it will be loaded automatically. Shared vendors are +# expected to use the system trust store and not ship data files that would +# need extraction from inside the zip. _shared_dir = _requirements_dir / "any" if _shared_dir.is_dir(): - for _i, _shared_zip in enumerate(sorted(_shared_dir.glob("*.zip")), start=1): - _load_packages_from_zip(_shared_zip, path_position=_i) + for _shared_zip in sorted(_shared_dir.glob("*.zip")): + _load_packages_from_zip(_shared_zip) # 3. Install the lazy import hook for nested submodule access. # Idempotent via the _tank_vendor_meta_finder guard, so calling it once From 8c006ca1823e8fa92ca892b6e803c83b999a86e1 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 15 May 2026 13:28:08 +0100 Subject: [PATCH 06/11] SG-43217 Release importlib.metadata file handles after loading vendor zips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous reorder commit (f96c7d68) moved the share_core WinError 32 from pkgs.zip to flow_data_sdk-beta.zip — same root cause, different file. This is the actual fix. importlib.metadata.FastPath.__new__ is @lru_cache'd. The FastPath instance for whichever zip importlib.metadata probes is kept alive forever, and inside FastPath.zip_children() the line `self.joinpath = zip_path.joinpath` binds a zipfile.Path (with its underlying open ZipFile) as an instance attribute. The result: the cache permanently pins an open file handle on every zip that ever yielded a metadata match. flow_data_sdk's _version.py triggers this by calling importlib.metadata.version("flow-data-sdk") at module import time. The cached FastPath then keeps flow_data_sdk-beta.zip open, which on Windows blocks share_core's shutil.move(install/core, ...). Fix: after all zips are loaded, call MetadataPathFinder().invalidate_caches() to drop FastPath references, then gc.collect() so the underlying ZipFile.__del__ fires immediately and releases the OS handle. invalidate_caches is called on an instance, not the class, because it isn't decorated as @classmethod in older Python versions but takes `cls` by convention. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 09337be23..fc2249c2e 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -397,3 +397,42 @@ def _load_packages_from_zip(zip_path): # Idempotent via the _tank_vendor_meta_finder guard, so calling it once # after both load steps is safe and sufficient. _install_import_hook() + + +def _release_importlib_metadata_handles(): + """ + Release file handles that importlib.metadata holds on vendor zips. + + importlib.metadata.FastPath.__new__ is @lru_cache'd, so the FastPath + instance for any zip it probes is kept alive forever. Inside + FastPath.zip_children(), the line `self.joinpath = zip_path.joinpath` + binds the zipfile.Path (and its underlying open ZipFile) as an instance + attribute on the cached FastPath — so the file handle stays open for + the lifetime of the cache. + + This bites us on Windows / Python 3.13 when flow_data_sdk's _version.py + runs importlib.metadata.version("flow-data-sdk") during import. The + cached FastPath keeps our shared zip open, which then prevents the + tank share_core command from moving install/core (WinError 32 sharing + violation). + + invalidate_caches() calls FastPath.__new__.cache_clear() which drops + the FastPath references. gc.collect() forces __del__ on the underlying + ZipFile objects so the handles close immediately rather than at the + next garbage collection cycle. + """ + try: + from importlib.metadata import MetadataPathFinder + except ImportError: + # Python < 3.8 has no stdlib importlib.metadata; nothing to clear. + return + # invalidate_caches() is declared as `def invalidate_caches(cls)` without + # @classmethod in some Python versions, so call it on an instance for + # cross-version compatibility. + MetadataPathFinder().invalidate_caches() + import gc + + gc.collect() + + +_release_importlib_metadata_handles() From b6388cface2624300c80791cb0d0f379488897ad Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 15 May 2026 14:40:26 +0100 Subject: [PATCH 07/11] SG-43217 Gate importlib.metadata handle release to Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleanup added in 8c006ca1 (clearing FastPath cache + gc.collect to release zipfile handles) is a workaround for Windows' sharing-violation file-move semantics. Linux and macOS allow moving files with open handles, so the cleanup is unnecessary there — and it was observed to break a Linux / Python 3.13 integration test in CI. Guard with sys.platform == "win32" so non-Windows platforms get the previous behaviour unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index fc2249c2e..2bd132f1e 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -403,6 +403,8 @@ def _release_importlib_metadata_handles(): """ Release file handles that importlib.metadata holds on vendor zips. + Windows-only workaround. + importlib.metadata.FastPath.__new__ is @lru_cache'd, so the FastPath instance for any zip it probes is kept alive forever. Inside FastPath.zip_children(), the line `self.joinpath = zip_path.joinpath` @@ -416,11 +418,18 @@ def _release_importlib_metadata_handles(): tank share_core command from moving install/core (WinError 32 sharing violation). + Linux and macOS don't have Windows' sharing-violation semantics — moving + or deleting files with open handles is allowed — so this cleanup is a + no-op on those platforms (and was observed to break a Linux/3.13 + integration test, so we gate strictly on win32). + invalidate_caches() calls FastPath.__new__.cache_clear() which drops the FastPath references. gc.collect() forces __del__ on the underlying ZipFile objects so the handles close immediately rather than at the next garbage collection cycle. """ + if sys.platform != "win32": + return try: from importlib.metadata import MetadataPathFinder except ImportError: From 5aca09aa5f6b5cb8d491b4dd47c22e0be224dfef Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 15 May 2026 16:37:32 +0100 Subject: [PATCH 08/11] SG-43217 Address Copilot review feedback Three changes prompted by PR review on #1098: 1. Broaden per-package import catch from ImportError to Exception. The inner try/except is best-effort by design (the outer wholesale-failure handler still raises). A future shared vendor using PEP 604 unions or other syntax-level newness would currently break import tank_vendor on older Pythons with a SyntaxError escaping the inner catch; widening the except matches the documented intent. 2. Add TestFlowDataSDKAbsentOnOldPython, gated to Python < 3.10. Pins the contract that the loader warns and continues when a shared vendor fails to import, so the PR's behavioural claim ("on 3.7/3.9 the SDK is simply absent") is actually exercised in CI rather than just asserted in the description. 3. Soften the misleading "mandatory" label on pkgs.zip in the module docstring. Missing pkgs.zip is tolerated to support pip-installed tk-core where dependencies come from the environment; the docstring for _load_packages_from_zip already says so, but the top-of-file summary still claimed otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 18 ++++++++++++----- tests/core_tests/test_tank_vendor.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 2bd132f1e..848949ce2 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -15,7 +15,10 @@ ZIP archives. It provides: 1. Auto-discovery of packages in two locations: - - requirements/./pkgs.zip (per-Python-version, mandatory) + - requirements/./pkgs.zip (per-Python-version; present in + source checkouts, absent when + tk-core is pip-installed and + dependencies come from the env) - requirements/any/*.zip (Python-version-independent, optional) 2. Lazy import hook for transparent tank_vendor.* namespace aliasing 3. Package-specific patches (e.g., SSL certificate handling for shotgun_api3) @@ -332,10 +335,15 @@ def _load_packages_from_zip(zip_path): mod = importlib.import_module(package_name) sys.modules[f"tank_vendor.{package_name}"] = mod globals()[package_name] = mod - except ImportError as e: - # Per-package import failures are tolerated. The most common - # cause is a package using syntax newer than the current Python - # (e.g. flow_data_sdk uses types.UnionType which is 3.10+). + except Exception as e: + # Per-package import failures are tolerated. The catch is + # intentionally broad: a future shared vendor using syntax + # newer than the current Python (e.g. PEP 604 union syntax + # `int | None`) would raise SyntaxError at parse time, not + # ImportError. flow_data_sdk on Python 3.9 raises ImportError + # for its references to types.UnionType / typing.TypeAlias, + # which this catch also handles. Wholesale loader failures + # are still handled by the outer try/except. warnings.warn( f"Could not import {package_name} from {zip_path}: {e}" ) diff --git a/tests/core_tests/test_tank_vendor.py b/tests/core_tests/test_tank_vendor.py index 477066ea9..f34969b71 100644 --- a/tests/core_tests/test_tank_vendor.py +++ b/tests/core_tests/test_tank_vendor.py @@ -312,5 +312,35 @@ def test_dist_info_via_importlib_metadata(self): self.assertEqual(version("flow-data-sdk"), flow_data_sdk.SDK_VERSION) +@unittest.skipIf( + sys.version_info >= (3, 10), + "Test verifies behaviour when the SDK is unimportable due to <3.10 syntax/types", +) +class TestFlowDataSDKAbsentOnOldPython(ShotgunTestBase): + """ + On Python 3.7 and 3.9, flow_data_sdk fails to import because its source + references types.UnionType and typing.TypeAlias (both 3.10+). The shared + loader is supposed to warn and continue, leaving tank_vendor itself + fully usable. These tests pin that contract. + """ + + def test_tank_vendor_imports_cleanly(self): + """`import tank_vendor` must succeed even when shared vendors fail to load.""" + import importlib + + import tank_vendor + + # Re-importing is a no-op when the module is already cached, but the + # call would raise if the loader had been left in an inconsistent + # state by a per-package failure. + importlib.import_module("tank_vendor") + self.assertIsNotNone(tank_vendor) + + def test_flow_data_sdk_unavailable(self): + """The SDK is not registered under the tank_vendor namespace.""" + with self.assertRaises(ImportError): + from tank_vendor import flow_data_sdk # noqa: F401 + + if __name__ == "__main__": unittest.main() From f41a952310672c068bbe7d40d8ff7250c04759a3 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 19 May 2026 12:52:03 +0100 Subject: [PATCH 09/11] SG-43217 Address review feedback - Filter top-level .dll files in _discover_top_level_packages so a stray Windows DLL at the root of a vendor zip is not treated as an importable package (Carlos). - Reword the unreadable-zip warning to acknowledge that affected dependencies may still resolve from the Python environment instead of implying a guaranteed failure (Copilot). - Pass RuntimeWarning + stacklevel=2 on per-package import failures so they match the other warnings in this module and point at the caller (Copilot). Co-Authored-By: Claude Opus 4.7 (1M context) --- python/tank_vendor/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index 848949ce2..cae507f02 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -247,7 +247,7 @@ def _discover_top_level_packages(zip_path): - .dist-info: Package metadata directories (still in zip for importlib.metadata, but not importable as packages) - __pycache__: Python bytecode cache - - .pyd/.so/.dylib: Platform-specific binary extensions + - .pyd/.so/.dylib/.dll: Platform-specific binary extensions - _*: Private/internal modules (e.g., _ruamel_yaml.cp311-win_amd64.pyd) """ with zipfile.ZipFile(zip_path, "r") as zf: @@ -268,6 +268,7 @@ def _discover_top_level_packages(zip_path): and not pkg.endswith(".pyd") and not pkg.endswith(".so") and not pkg.endswith(".dylib") + and not pkg.endswith(".dll") and not pkg.startswith("_") } @@ -304,7 +305,8 @@ def _load_packages_from_zip(zip_path): except (zipfile.BadZipFile, OSError, IOError) as e: warnings.warn( f"Failed to load packages from {zip_path}: {e}. " - "Third-party dependencies from this zip will not be available.", + "Any dependencies it would have provided will need to be resolved " + "from the Python environment, or will fail at import time.", RuntimeWarning, stacklevel=2, ) @@ -345,7 +347,9 @@ def _load_packages_from_zip(zip_path): # which this catch also handles. Wholesale loader failures # are still handled by the outer try/except. warnings.warn( - f"Could not import {package_name} from {zip_path}: {e}" + f"Could not import {package_name} from {zip_path}: {e}", + RuntimeWarning, + stacklevel=2, ) except Exception as e: From 27d1e01b9803652f9640671a08b7e83c638cab2b Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Wed, 20 May 2026 16:38:29 +0100 Subject: [PATCH 10/11] SG-43217 Address Julien's review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore the unicode → arrow in the _install_import_hook docstring. - Wrap the new requirements/any/ paragraph in developer/README.md. - Restore the Step 1/2/3/4 navigational comments inside _load_packages_from_zip that the refactor had stripped. - Drop the Python<3.8 fallback in _release_importlib_metadata_handles; Python 3.7/3.8 compatibility was discontinued after March 2026. - Reorganise __init__.py so all helper defs (including _release_importlib_metadata_handles) come before the MAIN INITIALIZATION block, and call the release helper from the main block. Co-Authored-By: Claude Opus 4.7 (1M context) --- developer/README.md | 6 +- python/tank_vendor/__init__.py | 113 ++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/developer/README.md b/developer/README.md index 8dc9c22df..d1992e524 100644 --- a/developer/README.md +++ b/developer/README.md @@ -29,7 +29,11 @@ The `requirements/update_python_packages.py` script automates the creation and m ### Shared (Python-version-independent) vendor zips -In addition to the per-version `pkgs.zip`, `requirements/any/` holds pure-Python packages that are safe to load across every supported Python version (e.g. the Autodesk Flow Data Beta SDK). Each zip is auto-discovered by `tank_vendor/__init__.py` and contains the importable package plus its `.dist-info/` directory at the zip's root. +In addition to the per-version `pkgs.zip`, `requirements/any/` holds pure-Python +packages that are safe to load across every supported Python version (e.g. the +Autodesk Flow Data Beta SDK). Each zip is auto-discovered by +`tank_vendor/__init__.py` and contains the importable package plus its +`.dist-info/` directory at the zip's root. ## How to upgrade ruamel.yaml diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index cae507f02..f5bd16ef0 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -41,7 +41,7 @@ Packages whose top-level name is already registered are skipped with a warning. -Supported Python versions: 3.7+ +Supported Python versions: 3.9+ """ import pathlib @@ -220,7 +220,7 @@ def _install_import_hook(): How it works: 1. Intercepts imports starting with "tank_vendor." 2. Strips the "tank_vendor." prefix to get the real module name - 3. Imports the real module (e.g., "shotgun_api3" -> tank_vendor.shotgun_api3) + 3. Imports the real module (e.g., "shotgun_api3" → tank_vendor.shotgun_api3) 4. Creates an alias in sys.modules so both names refer to the same module Why lazy loading: @@ -295,7 +295,10 @@ def _load_packages_from_zip(zip_path): True if the zip was successfully loaded, False if it was missing or unreadable. """ - # Validate zip before attempting to load from it. + # Step 1: Validate the zip exists, is a file (not a directory of extracted + # contents, as some CI environments produce), and can be opened as a ZIP. + # Missing zips are silent (pip-installed setups have no pkgs.zip). Unreadable + # zips warn so the failure mode is visible, but don't fail the import. if not zip_path.exists() or not zip_path.is_file(): return False @@ -312,6 +315,7 @@ def _load_packages_from_zip(zip_path): ) return False + # Step 2: Put the zip on sys.path so Python can import directly from it. # Insertion ordering is load-bearing: importlib.metadata.version() resolves # dist-info inside a zip only after the zip is on sys.path. sys.path.insert(0, str(zip_path)) @@ -319,8 +323,11 @@ def _load_packages_from_zip(zip_path): try: import importlib + # Step 3: Auto-discover all top-level packages in the zip. top_level_packages = _discover_top_level_packages(zip_path) + # Step 4: Import and register each top-level package under the + # tank_vendor namespace. for package_name in sorted(top_level_packages): # Collision check: an earlier zip already claimed this name. # Earlier zips win (pkgs.zip is loaded before requirements/any/). @@ -334,6 +341,10 @@ def _load_packages_from_zip(zip_path): continue try: + # Import the real module and alias it under tank_vendor.* in + # sys.modules; also expose it as an attribute on this package + # so `from tank_vendor import ` works without going + # through the meta path finder. mod = importlib.import_module(package_name) sys.modules[f"tank_vendor.{package_name}"] = mod globals()[package_name] = mod @@ -366,6 +377,47 @@ def _load_packages_from_zip(zip_path): return True +def _release_importlib_metadata_handles(): + """ + Release file handles that importlib.metadata holds on vendor zips. + + Windows-only workaround. + + importlib.metadata.FastPath.__new__ is @lru_cache'd, so the FastPath + instance for any zip it probes is kept alive forever. Inside + FastPath.zip_children(), the line `self.joinpath = zip_path.joinpath` + binds the zipfile.Path (and its underlying open ZipFile) as an instance + attribute on the cached FastPath — so the file handle stays open for + the lifetime of the cache. + + This bites us on Windows / Python 3.13 when flow_data_sdk's _version.py + runs importlib.metadata.version("flow-data-sdk") during import. The + cached FastPath keeps our shared zip open, which then prevents the + tank share_core command from moving install/core (WinError 32 sharing + violation). + + Linux and macOS don't have Windows' sharing-violation semantics — moving + or deleting files with open handles is allowed — so this cleanup is a + no-op on those platforms (and was observed to break a Linux/3.13 + integration test, so we gate strictly on win32). + + invalidate_caches() calls FastPath.__new__.cache_clear() which drops + the FastPath references. gc.collect() forces __del__ on the underlying + ZipFile objects so the handles close immediately rather than at the + next garbage collection cycle. + """ + if sys.platform != "win32": + return + from importlib.metadata import MetadataPathFinder + # invalidate_caches() is declared as `def invalidate_caches(cls)` without + # @classmethod in some Python versions, so call it on an instance for + # cross-version compatibility. + MetadataPathFinder().invalidate_caches() + import gc + + gc.collect() + + # ============================================================================ # MAIN INITIALIZATION # ============================================================================ @@ -405,55 +457,12 @@ def _load_packages_from_zip(zip_path): for _shared_zip in sorted(_shared_dir.glob("*.zip")): _load_packages_from_zip(_shared_zip) -# 3. Install the lazy import hook for nested submodule access. -# Idempotent via the _tank_vendor_meta_finder guard, so calling it once -# after both load steps is safe and sufficient. +# Install the lazy import hook for nested submodule access. +# Idempotent via the _tank_vendor_meta_finder guard, so calling it once +# after both load steps is safe and sufficient. _install_import_hook() - -def _release_importlib_metadata_handles(): - """ - Release file handles that importlib.metadata holds on vendor zips. - - Windows-only workaround. - - importlib.metadata.FastPath.__new__ is @lru_cache'd, so the FastPath - instance for any zip it probes is kept alive forever. Inside - FastPath.zip_children(), the line `self.joinpath = zip_path.joinpath` - binds the zipfile.Path (and its underlying open ZipFile) as an instance - attribute on the cached FastPath — so the file handle stays open for - the lifetime of the cache. - - This bites us on Windows / Python 3.13 when flow_data_sdk's _version.py - runs importlib.metadata.version("flow-data-sdk") during import. The - cached FastPath keeps our shared zip open, which then prevents the - tank share_core command from moving install/core (WinError 32 sharing - violation). - - Linux and macOS don't have Windows' sharing-violation semantics — moving - or deleting files with open handles is allowed — so this cleanup is a - no-op on those platforms (and was observed to break a Linux/3.13 - integration test, so we gate strictly on win32). - - invalidate_caches() calls FastPath.__new__.cache_clear() which drops - the FastPath references. gc.collect() forces __del__ on the underlying - ZipFile objects so the handles close immediately rather than at the - next garbage collection cycle. - """ - if sys.platform != "win32": - return - try: - from importlib.metadata import MetadataPathFinder - except ImportError: - # Python < 3.8 has no stdlib importlib.metadata; nothing to clear. - return - # invalidate_caches() is declared as `def invalidate_caches(cls)` without - # @classmethod in some Python versions, so call it on an instance for - # cross-version compatibility. - MetadataPathFinder().invalidate_caches() - import gc - - gc.collect() - - +# Windows-only cleanup: drop importlib.metadata's cached file handles on our +# vendor zips so the tank share_core command can move install/core without +# hitting WinError 32 sharing violations. No-op on Linux/macOS. _release_importlib_metadata_handles() From 838120394d279a6bb6e45cca9206e5472ecdeb4e Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Wed, 20 May 2026 16:46:50 +0100 Subject: [PATCH 11/11] Put the comments back that Claude decided were not worthy of existing --- python/tank_vendor/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/tank_vendor/__init__.py b/python/tank_vendor/__init__.py index f5bd16ef0..00ddf5859 100644 --- a/python/tank_vendor/__init__.py +++ b/python/tank_vendor/__init__.py @@ -345,7 +345,11 @@ def _load_packages_from_zip(zip_path): # sys.modules; also expose it as an attribute on this package # so `from tank_vendor import ` works without going # through the meta path finder. + + # Import the package mod = importlib.import_module(package_name) + + # Register in sys.modules under tank_vendor namespace sys.modules[f"tank_vendor.{package_name}"] = mod globals()[package_name] = mod except Exception as e: