Skip to content

Commit

Permalink
Release v1.2.0
Browse files Browse the repository at this point in the history
* Merge pull request #36 from shotgunsoftware/develop
  • Loading branch information
staceyoue authored Feb 1, 2024
2 parents dc8fcd7 + aaf3dcb commit 2af94aa
Show file tree
Hide file tree
Showing 21 changed files with 2,351 additions and 77 deletions.
2 changes: 1 addition & 1 deletion dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ make clean

## <a name="embed_py_package"></a>Embeddable Python Package

The `get-pip.py` script is included for convenience to set up the embeddable Python packages in `dist` (see [pip installation](https://pip.pypa.io/en/stable/installation/)) The base embeddable Python packages do not come with pip, so this script can be used to make pip available to the embeddable Python, which can then be used to install any additional required Python packages. See [dist](https://github.com/shotgunsoftware/tk-framework-alias/blob/develop/dist/README.md) for more details on how to set up the embeddable Python package.
The `get-pip.py` script is not included to set up the embeddable Python packages in `dist` (see [pip installation](https://pip.pypa.io/en/stable/installation/)). Download from [here](https://bootstrap.pypa.io/get-pip.py). The base embeddable Python packages do not come with pip, so this script can be used to make pip available to the embeddable Python, which can then be used to install any additional required Python packages. See [dist](https://github.com/shotgunsoftware/tk-framework-alias/blob/develop/dist/README.md) for more details on how to set up the embeddable Python package.

## Required Python Packages

Expand Down
60 changes: 39 additions & 21 deletions dev/update_python_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@ def zip_recursively(zip_file, root_dir, folder_name):
zip_file.write(full_file_path, full_file_path.relative_to(root_dir))


def modify_pyside2(pyside2_path):
"""Modify the PySide2 package such that it only includes the necessary files."""
def modify_pyside(pyside_path):
"""
Modify the PySide package such that it only includes the necessary files.
# Required files by name. These are absoultely required. No override.
This function supports modifying PySide versions:
- PySide2
- PySide6
"""

# Required files by name.
required_files = [
"QtCore.pyd",
"QtCore.pyi",
"Qt5Core.dll",
"Qt5Core.dll", # PySide2
"Qt6Core.dll", # PySide6
]
# Required files by file type extension. Files may be ignored if they fall into one of the
# specified ignore patterns, even if they are one of these required file types
Expand All @@ -46,17 +53,21 @@ def modify_pyside2(pyside2_path):
# Ignore files that start with these prefixes, unless they are in the 'required_files'
ignore_files_startswith = ["Qt"]

# First create a temp directory to create the stripped down PySide2 package
with TemporaryDirectory() as pyside2_temp_dir:
# First create a temp directory to create the stripped down PySide package
with TemporaryDirectory() as pyside_temp_dir:
# Add all required files
for required_file in required_files:
src_path = os.path.join(pyside2_path, required_file)
dst_path = os.path.join(pyside2_temp_dir, required_file)
src_path = os.path.join(pyside_path, required_file)
if not os.path.exists(src_path):
# Some required files may be skipped due to version differences
print(f"\tSkipping {required_file}")
continue
dst_path = os.path.join(pyside_temp_dir, required_file)
shutil.copyfile(src_path, dst_path)

# Go through the PySide2 top-level directory and add all files with the required file
# Go through the PySide top-level directory and add all files with the required file
# type, unless it falls into one of the ignore patterns, or is already copied over.
for file_name in os.listdir(pyside2_path):
for file_name in os.listdir(pyside_path):
# Check ignore patterns
ignore = False
for startswith_pattern in ignore_files_startswith:
Expand All @@ -72,18 +83,18 @@ def modify_pyside2(pyside2_path):
continue

# Check if it already exists
dst_path = os.path.join(pyside2_temp_dir, file_name)
dst_path = os.path.join(pyside_temp_dir, file_name)
if os.path.exists(dst_path):
continue

# Copy the file to the new PySide2 package
src_path = os.path.join(pyside2_path, file_name)
# Copy the file to the new PySide package
src_path = os.path.join(pyside_path, file_name)
shutil.copyfile(src_path, dst_path)

# Remove the original PySide2 package
shutil.rmtree(pyside2_path)
# Remove the original PySide package
shutil.rmtree(pyside_path)
# Copy the temp package to the original path
shutil.copytree(pyside2_temp_dir, pyside2_path)
shutil.copytree(pyside_temp_dir, pyside_path)


#
Expand Down Expand Up @@ -153,7 +164,13 @@ def modify_pyside2(pyside2_path):
assert len(package_names) >= nb_dependencies

# TODO auto-detect C extension modules (and other dynamic modules)
c_extension_modules = ["greenlet", "PySide2", "shiboken2"]
c_extension_modules = [
"greenlet",
"PySide2",
"shiboken2",
"PySide6",
"shiboken6",
]

# Write out the zip file for python packages. Compress the zip file with ZIP_DEFLATED. Note
# that this requires zlib to decompress when importing. Compression also causes import to
Expand All @@ -171,10 +188,11 @@ def modify_pyside2(pyside2_path):

full_package_path = temp_dir_path / package_name

if package_name == "PySide2":
# Special handling for PySide2 to limit the package size. Only the QtCore module is
# needed, so this package will be stripped down to only include the necessary files
modify_pyside2(full_package_path)
if package_name.startswith("PySide"):
# Special handling for PySide packages to limit the package size. Only the QtCore
# module is needed, so this package will be stripped down to only include the
# necessary files
modify_pyside(full_package_path)

if package_name in c_extension_modules:
# Cannot include C extension modules in zip. These module types cannot be imported
Expand Down
Binary file modified dist/Alias/python3.7/2024.0/alias_api.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.7/2024.0/alias_api_om.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.7/2024.1/alias_api.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.7/2024.1/alias_api_om.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.9/2024.0/alias_api.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.9/2024.0/alias_api_om.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.9/2024.1/alias_api.pyd
Binary file not shown.
Binary file modified dist/Alias/python3.9/2024.1/alias_api_om.pyd
Binary file not shown.
Binary file modified dist/Python/Python37/packages/c_extensions.zip
Binary file not shown.
23 changes: 12 additions & 11 deletions dist/Python/Python37/packages/frozen_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
bidict==0.22.1
certifi==2023.7.22
certifi==2023.11.17
cffi==1.15.1
charset-normalizer==3.2.0
cryptography==41.0.4
charset-normalizer==3.3.2
cryptography==41.0.7
dnspython==2.3.0
eventlet==0.33.3
greenlet==2.0.2
eventlet==0.34.3
greenlet==3.0.3
h11==0.14.0
idna==3.4
idna==3.6
pycparser==2.21
PySide2==5.15.0
python-engineio==4.7.1
python-socketio==5.9.0
PySide6==6.2.1
python-engineio==4.8.2
python-socketio==5.11.0
requests==2.31.0
shiboken2==5.15.0
simple-websocket==0.10.1
six==1.16.0
shiboken6==6.2.1
simple-websocket==1.0.0
typing-extensions==4.7.1
urllib3==2.0.5
urllib3==2.0.7
websocket-client==1.6.1
wsproto==1.2.0
Binary file modified dist/Python/Python37/packages/pkgs.zip
Binary file not shown.
1 change: 1 addition & 0 deletions dist/Python/Python37/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
cryptography
eventlet
PySide2==5.15.0
PySide6==6.2.1
python-socketio
requests
websocket-client
22 changes: 14 additions & 8 deletions python/tk_framework_alias/client/socketio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

import filecmp
import json
import os
import shutil
import socketio
import threading


from .client_json import AliasClientJSON
from ..utils.decorators import check_server_result
from tk_framework_alias_utils import utils as framework_utils
Expand Down Expand Up @@ -312,18 +313,21 @@ def get_alias_api(self):
# Get information about the api module
api_info = self.call_threadsafe("get_alias_api_info")

# Get the cache file path for the api module
# Get the cached files for the api module
filename = os.path.basename(api_info["file_path"]).split(".")[0]
cache_filepath = framework_env_utils.get_alias_api_cache_file_path(
filename, api_info["alias_version"], api_info["python_version"]
)
api_ext = os.path.splitext(api_info["file_path"])[1]
cache_api_filepath = os.path.join(
os.path.dirname(cache_filepath),
f"{os.path.splitext(cache_filepath)[0]}{api_ext}",
)

cache_loaded = False
if os.path.exists(cache_filepath):
# The cache exists. Check the cache and api file modified dates to see if the
# cache is stale or not.
cache_last_modified = os.stat(cache_filepath).st_mtime
if api_info["last_modified"] < cache_last_modified:
if os.path.exists(cache_filepath) and os.path.exists(cache_api_filepath):
# The cache exists, check if it requires updating before using it.
if filecmp.cmp(api_info["file_path"], cache_api_filepath):
# The cache is still up to date, load it in.
with open(cache_filepath, "r") as fp:
module_proxy = json.load(fp, cls=self.get_json_decoder())
Expand All @@ -333,12 +337,14 @@ def get_alias_api(self):
cache_folder = os.path.dirname(cache_filepath)
if not os.path.exists(cache_folder):
os.mkdir(cache_folder)

# The api was not loaded from cache, make a server request to get the api module,
# and cache it
module_proxy = self.call_threadsafe("get_alias_api")
with open(cache_filepath, "w") as fp:
json.dump(module_proxy, fp=fp, cls=self.get_json_encoder())
# Copy the api module to the cache folder in order to determine next time if the
# cache requies an update
shutil.copyfile(api_info["file_path"], cache_api_filepath)

return module_proxy.get_or_create_module(self)

Expand Down
24 changes: 24 additions & 0 deletions python/tk_framework_alias/server/alias_bridge.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 logging
import os
import subprocess
import sys
Expand Down Expand Up @@ -135,6 +136,8 @@ def start_server(self, host=None, port=None, max_retries=None):
:rtype: bool
"""

self.__log("Starting server...")

if self.__server_socket:
# Already started
host_in_use, port_in_use = self.__server_socket.getsockname()
Expand Down Expand Up @@ -182,6 +185,8 @@ def stop_server(self):
will receive the shutdown event in the correct thread.
"""

self.__log("Stopping server...")

# Destroy the server scope, this will remove any event handlers registered. Do this
# before shutting down clients so that their shutdown does not trigger any events.
self.alias_data_model.destroy()
Expand Down Expand Up @@ -245,6 +250,8 @@ def register_client_namespace(self, client_name, client_info):
:rtype: dict
"""

self.__log(f"Registering client namespace: {client_name}\n{client_info}")

if self.__clients.get(client_name):
raise ClientAlreadyRegistered("Client already registered")

Expand Down Expand Up @@ -287,6 +294,8 @@ def bootstrap_client(self, client, client_info=None):
:rtype: bool
"""

self.__log(f"Bootstrapping client: {client}")

# Check if the server is ready to have a client connect to it.
if not self.__server_socket:
return False
Expand Down Expand Up @@ -408,6 +417,7 @@ def bootstrap_client(self, client, client_info=None):
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW

# Start the client in a new a process, don't wait for it to finish.
self.__log(f"Executing subprocess: {args}")
subprocess.Popen(args, env=startup_env, startupinfo=si)

return True
Expand All @@ -420,6 +430,8 @@ def restart_client(self, client_namespace):
:type client_namespace: str
"""

self.__log(f"Restarting client {client_namespace}...")

client = self.get_client_by_namespace(client_namespace)
if not client:
return
Expand Down Expand Up @@ -489,3 +501,15 @@ def __serve_app(self):
# created in).
wsgi_logger = framework_utils.get_logger(self.__class__.__name__, "wsgi")
eventlet.wsgi.server(self.__server_socket, self.__app, log=wsgi_logger)

def __log(self, msg, level=logging.INFO):
"""
Log a message to the logger.
:param msg: The message to log.
:type msg: str
:param level: The log level to log the message at.
:type level: int
"""

self.__server_sio.logger.log(level, msg)
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ def on_restart(self, sid):
# Emit event back to the client to shutdown (though do not wait for it, else this will
# hang the server), then re-bootstrap the client.
self.emit("shutdown", namespace=self.namespace)

self._log_message(sid, f"Restarting client: {self.namespace}", logging.INFO)
alias_bridge.AliasBridge().restart_client(self.namespace)

def on_get_alias_api(self, sid):
Expand Down Expand Up @@ -289,7 +291,7 @@ def _handle_api_event(self, event, sid, *args):

# Make the Alias API call
request = args[0] if args else None
self._log_message(None, f"Excuting Alias API request: {request}", logging.DEBUG)
self._log_message(None, f"Excuting Alias API request: {request}", logging.INFO)
result = self._execute_request(event, request)

try:
Expand Down
36 changes: 32 additions & 4 deletions python/tk_framework_alias/server/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ class ServerAlreadyRunning(AliasBridgeException):
"""Exception for AliasBridge when attempting to connect to server when it is already running."""


class QtImportError(AliasBridgeException):
"""Exception for AliasBridge when attempting to import Qt."""


# AliasServerNamespace exceptions
# ----------------------------------------------------------------------------------------

Expand Down Expand Up @@ -79,3 +75,35 @@ class AliasApiRequestNotSupported(AliasApiRequestException):

class AliasApiPostProcessRequestError(AliasApiRequestException):
"""Exception for Alias API request post process error."""


# Qt exceptions
# ----------------------------------------------------------------------------------------


class QtImportError(Exception):
"""Base Exception for errors related to importing the Qt framework module."""


class QtModuleNotFound(QtImportError):
"""
Exception thrown when Qt was imported without error but a specific Qt module was not found.
If Qt was imported without error but a specific Qt module was not found, then this
indicates that the error is due to the Qt version used by Alias and the PySide version used
by the framework are not compatibile. To avoid this Qt version mismatch error, the PySide
version should match the version that Alias is running with.
"""


class QtAppInstanceNotFound(QtImportError):
"""
Exception thrown when Qt was imported and modules found wihtout error but the Qt app
instance was not found.
Alias creates the Qt app instance that this framework will interact with. The Qt app is
shared between C++ and Python.
This error may occur for developers when running in Debug mode with Alias, and there are
no debug symbols available for PySide.
"""
Loading

0 comments on commit 2af94aa

Please sign in to comment.