Skip to content
Closed
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
Binary file added .DS_Store
Binary file not shown.
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# macOS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes

# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
*.egg

# IDE
.vscode/
.idea/
*.swp
*.swo

# Environment
.env
*.log
Binary file added Python/.DS_Store
Binary file not shown.
Binary file added Python/Startup/.DS_Store
Binary file not shown.
187 changes: 187 additions & 0 deletions Python/Startup/nt_loader/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""
NukeTimelineLoader - nt_loader package init.

Ensures required third-party dependencies (e.g. requests) are available
before the rest of the package is imported. This handles environments like
Hiero/Nuke 17+ where the bundled Python may not include these packages.

Also pre-imports tk-core (tank) using PySide6 natively so its QtImporter
picks the correct code path, and patches Qt resource stubs removed in Qt6.
"""

import sys
import os

# ---------------------------------------------------------------------------
# Qt5 resource function stubs (must run before any tk-core / tank import)
# ---------------------------------------------------------------------------
def _patch_qt_resource_functions():
"""Ensure qRegisterResourceData / qUnregisterResourceData exist.

tk-core's compiled Qt resource files call these functions which existed
in PySide2 (Qt5) but were removed in PySide6 (Qt6). We add no-op stubs
directly on PySide6.QtCore. This is safe to call multiple times.
"""
_stub = lambda *args, **kwargs: True

try:
from PySide6 import QtCore as _qtcore
if not hasattr(_qtcore, "qRegisterResourceData"):
_qtcore.qRegisterResourceData = _stub
if not hasattr(_qtcore, "qUnregisterResourceData"):
_qtcore.qUnregisterResourceData = _stub
except ImportError:
pass

# Also patch any PySide2.QtCore shim already in sys.modules
mod = sys.modules.get("PySide2.QtCore")
if mod is not None:
if not hasattr(mod, "qRegisterResourceData"):
mod.qRegisterResourceData = _stub
if not hasattr(mod, "qUnregisterResourceData"):
mod.qUnregisterResourceData = _stub


_patch_qt_resource_functions()


# ---------------------------------------------------------------------------
# Pre-import tank using PySide6 natively
# ---------------------------------------------------------------------------
def _import_tank_with_pyside6():
"""Pre-import tank while PySide2 shim is hidden from sys.modules.

tk-core's QtImporter tries PySide2 first, then PySide6. The PySide2
shim from menu.py makes the PySide2 path succeed initially, but then
pyside2_patcher fails because the underlying modules are PySide6
(e.g. QTextCodec was removed in Qt6).

By temporarily hiding the PySide2 shim, QtImporter skips the PySide2
path and uses its native PySide6 support (pyside6_patcher) instead.
Once tank is imported, we restore the PySide2 shim for any other code
that may depend on it.
"""
# Save and temporarily remove PySide2/shiboken2 shim entries
saved = {}
for key in list(sys.modules):
if key == "PySide2" or key.startswith("PySide2.") or key == "shiboken2":
saved[key] = sys.modules.pop(key)

try:
import tank # noqa: F401 - triggers QtImporter
print("[NukeTimelineLoader] tank imported via PySide6 path")

# QtImporter may fail silently on BOTH PySide2 and PySide6 paths
# (e.g. missing pyside6_patcher), leaving QtCore = None.
# Fix: inject PySide6 modules directly into qt_abstraction.
try:
import tank.authentication.ui.qt_abstraction as _qa
if _qa.QtCore is None:
from PySide6 import QtCore, QtGui, QtWidgets, QtNetwork
# tk-core expects a PySide1-style API where QtGui contains
# everything from QtWidgets + some classes from QtCore.
# The pyside2/6_patcher normally does this, but since both
# patchers failed, we merge the modules ourselves.
import types
_merged_gui = types.ModuleType("PySide6.QtGui._merged")
# Start with all QtGui attributes
for attr in dir(QtGui):
if not attr.startswith("_"):
setattr(_merged_gui, attr, getattr(QtGui, attr))
# Add all QtWidgets classes (PySide1 had these in QtGui)
for attr in dir(QtWidgets):
if not attr.startswith("_"):
setattr(_merged_gui, attr, getattr(QtWidgets, attr))
# Add QtCore classes that PySide1 exposed via QtGui
for attr in ("QSortFilterProxyModel", "QItemSelectionModel",
"QStringListModel", "QAbstractProxyModel",
"QItemSelection", "QItemSelectionRange"):
if hasattr(QtCore, attr):
setattr(_merged_gui, attr, getattr(QtCore, attr))

_qa.QtCore = QtCore
_qa.QtGui = _merged_gui
_qa.QtNetwork = QtNetwork
print("[NukeTimelineLoader] Patched qt_abstraction with PySide6 modules")
except ImportError:
pass

except Exception as exc:
print(f"[NukeTimelineLoader] WARNING: tank pre-import failed: {exc}")
finally:
# Restore PySide2 shim for other code that depends on it
sys.modules.update(saved)


# Only run on PySide6 environments (Hiero 17+) where the PySide2 shim exists
if "PySide2" in sys.modules:
try:
import PySide6 # noqa: F401 - check if we're in a PySide6 environment
_import_tank_with_pyside6()
except ImportError:
pass

# ---------------------------------------------------------------------------
# Auto-install missing dependencies
# ---------------------------------------------------------------------------
# Format: { "import_name": "pip_name_or_url" }
_REQUIRED_PACKAGES = {
"requests": "requests",
"PIL": "pillow",
"fileseq": "fileseq",
"qtpy": "qtpy",
"cv2": "opencv-python",
"tank_vendor": "git+https://github.com/shotgunsoftware/tk-core.git@v0.21.7",
}


def _ensure_dependencies():
"""Check for required packages and pip-install any that are missing."""
missing = []
for import_name, pip_name in _REQUIRED_PACKAGES.items():
try:
__import__(import_name)
except ImportError:
missing.append(pip_name)

if not missing:
return

# Determine a target directory for installed packages
target = os.environ.get("NTL_SITE_PACKAGES")
if not target:
# Use user site-packages as a safe default
import site
target = site.getusersitepackages()

os.makedirs(target, exist_ok=True)

# Ensure target is on sys.path so subsequent imports find the packages
if target not in sys.path:
sys.path.insert(0, target)

print(f"[NukeTimelineLoader] Installing missing dependencies: {missing}")
try:
# Use pip._internal instead of subprocess because in Nuke/Hiero 17+
# sys.executable points to the Nuke binary (not a Python interpreter),
# which causes SIGSEGV when invoked with "-m pip".
from pip._internal.cli.main import main as pip_main
pip_args = ["install", "--target", target, "--upgrade"] + missing
exit_code = pip_main(pip_args)
if exit_code == 0:
print("[NukeTimelineLoader] Dependencies installed successfully.")
# Refresh sys.path so newly installed packages are importable
import importlib
importlib.invalidate_caches()
else:
raise RuntimeError(f"pip exited with code {exit_code}")
except Exception as exc:
print(
f"[NukeTimelineLoader] WARNING: Could not auto-install dependencies: {exc}\n"
f" Please install manually by running in a terminal:\n"
f" pip3 install --target \"{target}\" {' '.join(missing)}\n"
f" Or use ntl_pip_dependency_installer.py"
)


_ensure_dependencies()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
13 changes: 9 additions & 4 deletions Python/Startup/nt_loader/fn_globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# __INTEGRATE__ Globals for Connection to Shotgrid/Flow
# ---
# setup global for studio SG/Flow site
SHOTGUN_URL = "https://your.studio.shotgrid"
SHOTGUN_URL = "https://blackshipvfx.shotgrid.autodesk.com"
# retrieve the current status icons from shotgrid
STATUS_PNG_URL = f"{SHOTGUN_URL}/images/sg_icon_image_map.png"
# Below can vary depending on studio structure. If the default is blocked follow below instructions
Expand Down Expand Up @@ -121,7 +121,7 @@
# Optional - Add API key string below if using API key connection. Note: enabling this approach will override above
# authentication globals. WARNING you will need to customize this codebase as it is built with the tk-core web login
# with authenticating users permission restrictions in mind.
SHOTGUN_API_KEY = None
SHOTGUN_API_KEY = "Ifzcp3xlaxgnansv!ugjqhqlx"

# ---
# __CUSTOMIZE__ Mixed OS path mapping
Expand All @@ -136,14 +136,19 @@
# "Linux": ["/mnt/media/v", "/mnt/media/z"],
# "Darwin": ["/media/v", "/media/z"] # OSX
# }
SG_MEDIA_PATH_MAP = {}
SG_MEDIA_PATH_MAP = {
"Windows":["v:"],
"Linux": ["/mnt"],
"Darwin": ["/Volumes"] # OSX
}

# ---
# __CUSTOMIZE__ Globals to set correct localization directory
# ---
# To avoid repeated "choose localization directory" dialogs un comment below with desired path
# os.environ["SG_LOCALIZE_DIR"] = ""
# Desired path can be driven by environment variable
if not os.environ.get("SG_LOCALIZE_DIR"):
os.environ["SG_LOCALIZE_DIR"] = os.path.expanduser("~/Documents/NukeTimelineLoader")
DEFAULT_LOCALIZE_DIR = os.environ.get("SG_LOCALIZE_DIR", None)
# Mostly used internally to assess if the project requires Shotgrid/Flow tags to be created
SG_TAGS_CREATED = os.environ.get("SG_TAGS_CREATED", False)
Expand Down
6 changes: 3 additions & 3 deletions Python/Startup/nt_loader/fn_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ def convert_media_path_to_map(path):
converted = pattern.sub(replacement, path)
# Final normalization of the complete path
return os.path.normpath(converted)
raise Exception(
"Error: fn_globals.py:SG_MEDIA_PATH_MAP set to unsupported OS for path substitution"
)
# No mapping match found — return original path unchanged
# This is expected for localized/downloaded files that are already local paths
return path
else:
return path

Expand Down
Loading