# Copyright (c) 2013 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import logging
from .import_stack import ImportStack
from ..errors import TankError
from .errors import TankContextChangeNotSupportedError, TankCurrentModuleNotFoundError
from .engine import current_engine, _restart_engine
from ..log import LogManager
def _get_current_bundle():
"""
The current import bundle is set by bundle.import_module() and
and is a way to defuse the chicken/egg situation which happens
when trying to do an import_framework inside a module that is being
loaded by import_module. The crux is that the module._tank_bundle reference
that import_module() sets is constructed at the end of the call,
meaning that the frameworks import cannot find this during the import
this variable is the fallback in this case and it contains a reference
to the current bundle.
:returns: :class:`Application`, :class:`Engine` or :class:`Framework` instance
"""
import sys
current_bundle = ImportStack.get_current_bundle()
if not current_bundle:
# try to figure out the associated bundle using module trickery,
# looking for the module which called this command and looking for
# a ._tank_module property on the module object.
try:
# get the caller's stack frame
caller = sys._getframe(2)
# get the package name from the caller
# for example: 0b3d7089471e42a998027fa668adfbe4.tk_multi_about.environment_browser
calling_name_str = caller.f_globals["__name__"]
# now the module imported by Bundle.import_module is
# 0b3d7089471e42a998027fa668adfbe4.tk_multi_about
# e.g. always the two first items in the name
chunks = calling_name_str.split(".")
calling_package_str = "%s.%s" % (chunks[0], chunks[1])
# get the caller's module from sys.modules
parent_module = sys.modules[calling_package_str]
except Exception:
raise TankCurrentModuleNotFoundError(
"import_framework could not determine the calling module layout! "
"You can only use this method on items imported using the import_module() "
"method!"
)
# ok we got our module
try:
current_bundle = parent_module._tank_bundle
except Exception:
raise TankCurrentModuleNotFoundError(
"import_framework could not access current app/engine on calling module %s. "
"You can only use this method on items imported using the import_module() "
"method!" % parent_module
)
return current_bundle
[docs]def change_context(new_context):
"""
Running change_context will attempt to change the context the engine and
its apps are running in on the fly. The current engine must accept the
context change, otherwise a full restart of the engine will be run instead.
The determination of whether an engine supports context changing comes from
its "context_change_allowed" property. If that property returns True, then
the context change will be allowed to proceed. If it returns False, then
the engine's "change_context" method will raise
:class:`TankContextChangeNotSupportedError`, which will then trigger a restart of
the engine and all of its apps.
In the event that the engine does support context changes, any apps that
support context changing will do so, as well. Any that do not will themselves
be restarted within the new context.
The benefit of supporting context changes in engines and apps is speed. The
end result of this routine should be identical to that of a restart, but
will require less time to complete.
For more information on supporting context changing, see the following:
- :meth:`Engine.context_change_allowed`
- :meth:`Application.context_change_allowed`
- :meth:`change_context`
- :meth:`Application.change_context`
:param new_context: The new Context to change to.
:type new_context: :class:`~sgtk.Context`
"""
engine = current_engine()
if engine is None:
raise TankError("No engine is currently running! Run start_engine instead.")
try:
engine.log_debug("Changing context to %r." % new_context)
engine.change_context(new_context)
engine.log_debug("Context changed successfully.")
except TankContextChangeNotSupportedError:
engine.log_debug("Context change not allowed by engine, restarting instead.")
restart(new_context)
[docs]def restart(new_context=None):
"""
Restarts the currently running Toolkit platform. This includes reloading all
configuration files as well as reloading the code for all apps and engines.
(The Core API, however, is not reloaded). The call does not take any parameters
and does not return any value.
Any open windows will remain open and will use the old code base and settings.
In order to access any changes that have happened as part of a reload, you need
to start up new app windows (typically done via the Shotgun menu) and these will
use the fresh code and configs.
:param new_context: The new Context to start the engine in, if desired. Default behavior
is to restart the engine with its current context.
:type new_context: :class:`~sgtk.Context`
"""
engine = current_engine()
if engine is None:
raise TankError("No engine is currently running! Run start_engine instead.")
try:
# first, reload the template defs
engine.tank.reload_templates()
engine.log_debug("Template definitions were reloaded.")
except TankError as e:
engine.log_error(e)
_restart_engine(new_context or engine.context)
engine.log_info("Toolkit platform was restarted.")
def current_bundle():
"""
Returns the bundle (app, engine or framework) instance for the
app that the calling code is associated with. This is a special method, designed to
be used inside python modules that belong to apps, engines or frameworks.
The calling code needs to have been imported using toolkit's standard import
mechanism, :meth:`Application.import_module()`, otherwise an exception will be raised.
This special helper method can be useful when code deep inside an app needs
to reach out to for example grab a configuration value. Then you can simply do::
app = sgtk.platform.current_bundle()
app.get_setting("frame_range")
:returns: :class:`Application`, :class:`Engine` or :class:`Framework` instance
"""
return _get_current_bundle()
[docs]def get_framework(framework):
"""
Convenience method that returns a framework instance given a framework name.
This is a special method, designed to
be used inside python modules that belong to apps, engines or frameworks.
The calling code needs to have been imported using toolkit's standard import
mechanism, import_module(), otherwise an exception will be raised.
For example, if your app code requires the tk-framework-helpers framework, and you
need to retrieve a configuration setting from this framework, then you can
simply do::
fw = sgtk.platform.get_framework("tk-framework-helpers")
fw.get_setting("frame_range")
:param framework: name of the framework object to access, as defined in the app's
info.yml manifest.
:returns: framework instance
:type: :class:`Framework`
"""
current_bundle = _get_current_bundle()
if framework not in current_bundle.frameworks:
raise Exception(
"import_framework: %s does not have a framework %s associated!"
% (current_bundle, framework)
)
fw = current_bundle.frameworks[framework]
return fw
def import_framework(framework, module):
"""
Convenience method for using frameworks code inside of apps, engines and other frameworks.
This method is intended to replace an import statement.
Instead of typing::
from . import foo_bar
You use the following syntax to load a framework module::
foo_bar = tank.platform.import_framework("tk-framework-mystuff", "foo_bar")
This is a special method, designed to
be used inside python modules that belong to apps, engines or frameworks.
The calling code needs to have been imported using toolkit's standard import
mechanism, :meth:`Bundle.import_module()`, otherwise an exception will be raised.
:param framework: name of the framework object to access, as defined in the app's
info.yml manifest.
:param module: module to load from framework
"""
current_bundle = _get_current_bundle()
if framework not in current_bundle.frameworks:
raise Exception(
"import_framework: %s does not have a framework %s associated!"
% (current_bundle, framework)
)
fw = current_bundle.frameworks[framework]
mod = fw.import_module(module)
return mod
def get_logger(module_name):
"""
Standard sgtk logging access for python code that runs inside apps.
We recommend that you use this method for all logging that takes place
inside of the ``python`` folder inside your app, engine or framework.
We recommend that the following pattern is used - at the top of your
python files, include the following code::
import sgtk
logger = sgtk.platform.get_logger(__name__)
All subsequent code in the file then simply calls this object for logging.
Following this pattern will generate a standard logger that is parented
under the correct bundle.
:param module_name: Pass ``__name__`` to this parameter
:return: Standard python logger
"""
try:
curr_bundle = _get_current_bundle()
full_log_path = "%s.%s" % (curr_bundle.logger.name, module_name)
return logging.getLogger(full_log_path)
except TankCurrentModuleNotFoundError:
return LogManager.get_logger("no_current_bundle.%s" % (module_name,))