/
bundle.py
1287 lines (1048 loc) · 52.3 KB
/
bundle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 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.
"""
Base class for Abstract classes for Engines, Apps and Frameworks
"""
import os
import sys
import imp
import uuid
from .. import hook
from ..util import sgre as re
from ..util.metrics import EventMetric
from ..log import LogManager
from ..errors import TankError, TankNoDefaultValueError
from .errors import TankContextChangeNotSupportedError
from . import constants
from .import_stack import ImportStack
from tank_vendor import six
core_logger = LogManager.get_logger(__name__)
class TankBundle(object):
"""
Abstract Base class for any engine, framework app etc in tank
"""
def __init__(self, tk, context, settings, descriptor, env, log):
"""
Constructor.
:param tk: :class:`~sgtk.Sgtk` instance
:param context: A context object to define the context on disk where the engine is operating
:type context: :class:`~sgtk.Context`
:param settings: dictionary of settings to associate with this object
:param descriptor: Descriptor pointing at associated code.
:param env: An Environment object to associate with this bundle.
:param log: A python logger to associate with this bundle
"""
self.__tk = tk
self.__context = context
self.__settings = settings
self.__sg = None
self.__cache_location = {}
self.__module_uid = None
self.__descriptor = descriptor
self.__frameworks = {}
self.__environment = env
self.__log = log
# emit an engine started event
tk.execute_core_hook(constants.TANK_BUNDLE_INIT_HOOK_NAME, bundle=self)
##########################################################################################
# internal API
def log_metric(self, action, log_version=False, log_once=False, command_name=None):
"""
Log metrics for this bundle and the given action.
:param str action: Action string to log, e.g. 'Opened Workfile'.
:param log_version: Deprecated and ignored, but kept for backward compatibility.
:param bool log_once: ``True`` if this metric should be ignored if it
has already been logged. Defaults to ``False``.
:param str command_name: A Toolkit command name to add to the metric properties.
Internal Use Only - We provide no guarantees that this method
will be backwards compatible.
"""
properties = {}
if command_name:
properties[EventMetric.KEY_COMMAND] = command_name
EventMetric.log(
EventMetric.GROUP_TOOLKIT,
action,
properties=properties,
log_once=log_once,
bundle=self,
)
##########################################################################################
# properties used by internal classes, not part of the public interface
@property
def descriptor(self):
"""
Internal method - not part of Tank's public interface.
This method may be changed or even removed at some point in the future.
We leave no guarantees that it will remain unchanged over time, so
do not use in any app code.
"""
return self.__descriptor
@property
def settings(self):
"""
Internal method - not part of Tank's public interface.
This method may be changed or even removed at some point in the future.
We leave no guarantees that it will remain unchanged over time, so
do not use in any app code.
"""
return self.__settings
##########################################################################################
# methods used by internal classes, not part of the public interface
def get_setting_from(self, other_settings, key, default=None):
"""
Internal method - not part of Tank's public interface.
Get a value from the settings dictionary passed in
using the logic from this application
:param other_settings: dictionary to use to find setting
:param key: setting name
:param default: default value to return
"""
return self.__resolve_setting_value(other_settings, key, default)
def get_template_from(self, other_settings, key):
"""
Internal method - not part of Tank's public interface.
A shortcut for looking up which template is referenced in the given setting from
the settings dictionary passed in. It then calls get_template_by_name() on it.
:param other_settings: dictionary to use to find setting
:param key: setting name
"""
template_name = self.get_setting_from(other_settings, key)
return self.get_template_by_name(template_name)
##########################################################################################
# properties
@property
def name(self):
"""
The short name for the item (e.g. tk-maya)
:returns: name as string
"""
return self.__descriptor.system_name
@property
def display_name(self):
"""
The display name for the item (e.g. Maya Engine)
:returns: display name as string
"""
return self.__descriptor.display_name
@property
def description(self):
"""
A short description of the item
:returns: string
"""
return self.__descriptor.description
@property
def version(self):
"""
The version of the item (e.g. 'v0.2.3')
:returns: string representing the version
"""
return self.__descriptor.version
@property
def icon_256(self):
"""
Path to a 256x256 pixel png file which describes the item
"""
return self.__descriptor.icon_256
@property
def style_constants(self):
"""
Returns a dictionary of style constants. These can be used to build
UIs using standard colors and other style components. All keys returned
in this dictionary can also be used inside a style.qss that lives
at the root level of the app, engine or framework. Use a
``{{DOUBLE_BACKET}}`` syntax in the stylesheet file, for example::
QWidget
{
color: {{SG_FOREGROUND_COLOR}};
}
This property returns the values for all constants, for example::
{
"SG_HIGHLIGHT_COLOR": "#18A7E3",
"SG_ALERT_COLOR": "#FC6246",
"SG_FOREGROUND_COLOR": "#C8C8C8"
}
:returns: Dictionary. See above for example
"""
return constants.SG_STYLESHEET_CONSTANTS
@property
def documentation_url(self):
"""
Return the relevant documentation url for this item.
:returns: url string, None if no documentation was found
"""
return self.__descriptor.documentation_url
@property
def support_url(self):
"""
Return the relevant support url for this item.
:returns: url string, None if no documentation was found
"""
return self.__descriptor.support_url
@property
def disk_location(self):
"""
The folder on disk where this item is located.
This can be useful if you want to write app code
to retrieve a local resource::
app_font = os.path.join(self.disk_location, "resources", "font.fnt")
"""
# note: the reason we don't call __file__ directly is because
# we don't want to return the location of the 'bundle.py'
# base class but rather the location of object that
# has been derived from this class.
path_to_this_file = os.path.abspath(sys.modules[self.__module__].__file__)
return os.path.dirname(path_to_this_file)
@property
def cache_location(self):
"""
An item-specific location on disk where the app or engine can store
random cache data. This location is guaranteed to exist on disk.
This location is configurable via the ``cache_location`` hook.
It typically points at a path in the local filesystem, e.g on a mac::
~/Library/Caches/Shotgun/SITENAME/PROJECT_ID/BUNDLE_NAME
This can be used to store cache data that the app wants to reuse across
sessions::
stored_query_data_path = os.path.join(self.cache_location, "query.dat")
"""
project_id = self.__tk.pipeline_configuration.get_project_id()
return self.get_project_cache_location(project_id)
@property
def site_cache_location(self):
"""
A site location on disk where the app or engine can store
random cache data. This location is guaranteed to exist on disk.
This location is configurable via the ``cache_location`` hook.
It typically points at a path in the local filesystem, e.g on a mac::
~/Library/Caches/Shotgun/SITENAME/BUNDLE_NAME
This can be used to store cache data that the app wants to reuse across
sessions and can be shared across a site::
stored_query_data_path = os.path.join(self.site_cache_location, "query.dat")
"""
# this method is memoized for performance since it is being called a lot!
if self.__cache_location.get("site") is None:
self.__cache_location["site"] = self.__tk.execute_core_hook_method(
constants.CACHE_LOCATION_HOOK_NAME,
"get_bundle_data_cache_path",
project_id=None,
plugin_id=None,
pipeline_configuration_id=None,
bundle=self,
)
return self.__cache_location["site"]
@property
def context(self):
"""
The context associated with this item.
:returns: :class:`~sgtk.Context`
"""
return self.__context
@property
def context_change_allowed(self):
"""
Whether a context change is allowed without the need for a restart.
If a bundle supports on-the-fly context changing, this property should
be overridden in the deriving class and forced to return True.
:returns: bool
"""
return False
@property
def tank(self):
"""
Returns the Toolkit API instance associated with this item
:returns: :class:`~sgtk.Tank`
"""
return self.__tk
# new name compatibility
sgtk = tank
@property
def frameworks(self):
"""
List of all frameworks associated with this item
:returns: List of framework objects
"""
return self.__frameworks
@property
def logger(self):
"""
Standard python :class:`~logging.Logger` for this engine, app or framework.
Use this whenever you want to emit or process
log messages. If you are developing an app,
engine or framework, call this method for generic logging.
.. note:: Inside the ``python`` area of your app, engine or framework,
we recommend that you use :meth:`sgtk.platform.get_logger`
for your logging.
Logging will be dispatched to a logger parented under the
main toolkit logging namespace::
# pattern
sgtk.env.environment_name.engine_instance_name
# for example
sgtk.env.asset.tk-maya
.. note:: If you want all log messages that you are emitting in your
app, engine or framework to be written to a log file or
to a logging console, you can attach a std log handler here.
"""
return self.__log
##########################################################################################
# public methods
def change_context(self, new_context):
"""
Abstract method for context changing.
Implemented by deriving classes that wish to support context changes and
require specific logic to do so safely.
:param new_context: The context being changed to.
:type context: :class:`~sgtk.Context`
"""
if not self.context_change_allowed:
self.log_debug("Bundle %r does not allow context changes." % self)
raise TankContextChangeNotSupportedError()
def pre_context_change(self, old_context, new_context):
"""
Called before a context change.
Implemented by deriving classes.
:param old_context: The context being changed away from.
:type old_context: :class:`~sgtk.Context`
:param new_context: The context being changed to.
:type new_context: :class:`~sgtk.Context`
"""
pass
def post_context_change(self, old_context, new_context):
"""
Called after a context change.
Implemented by deriving classes.
:param old_context: The context being changed away from.
:type old_context: :class:`~sgtk.Context`
:param new_context: The context being changed to.
:type new_context: :class:`~sgtk.Context`
"""
pass
def import_module(self, module_name):
r"""
Special import command for Toolkit bundles. Imports the python folder inside
an app and returns the specified module name that exists inside the python folder.
Each Toolkit App or Engine can have a python folder which contains additional
code. In order to ensure that Toolkit can run multiple versions of the same app,
as well as being able to reload app code if it changes, it is recommended that
this method is used whenever you want to access code in the python location.
For example, imagine you had the following structure::
tk-multi-mybundle
|- app.py or engine.py or framework.py
|- info.yml
\- python
|- __init__.py <--- Needs to contain 'from . import tk_multi_mybundle'
\- tk_multi_mybundle
The above structure is a standard Toolkit app outline. ``app.py`` is a
light weight wrapper and the python module
``tk_multi_myapp`` module contains the actual code payload.
In order to import this in a Toolkit friendly way, you need to run
the following when you want to load the payload module inside of app.py::
module_obj = self.import_module("tk_multi_myapp")
"""
# local import to avoid cycles
# first, set the module we are currently processing
ImportStack.push_current_bundle(self)
try:
# get the python folder
python_folder = os.path.join(
self.disk_location, constants.BUNDLE_PYTHON_FOLDER
)
if not os.path.exists(python_folder):
raise TankError(
"Cannot import - folder %s does not exist!" % python_folder
)
# and import
if self.__module_uid is None:
self.log_debug("Importing python modules in %s..." % python_folder)
# alias the python folder with a UID to ensure it is unique every time it is imported
self.__module_uid = "tkimp%s" % uuid.uuid4().hex
imp.load_module(
self.__module_uid, None, python_folder, ("", "", imp.PKG_DIRECTORY)
)
# we can now find our actual module in sys.modules as GUID.module_name
mod_name = "%s.%s" % (self.__module_uid, module_name)
if mod_name not in sys.modules:
raise TankError(
"Cannot find module %s as part of %s!"
% (module_name, python_folder)
)
# lastly, append our own object to the added module. This is to make it easier to
# do elegant imports in the class scope via the tank.platform.import_framework method
sys.modules[mod_name]._tank_bundle = self
finally:
# no longer processing this one
ImportStack.pop_current_bundle()
return sys.modules[mod_name]
def get_project_cache_location(self, project_id):
"""
Gets the bundle's cache-location path for the given project id.
:param project_id: The project Entity id number.
:type project_id: int
:returns: Cache location directory path.
:rtype str:
"""
# this method is memoized for performance since it is being called a lot!
if self.__cache_location.get(project_id) is None:
self.__cache_location[project_id] = self.__tk.execute_core_hook_method(
constants.CACHE_LOCATION_HOOK_NAME,
"get_bundle_data_cache_path",
project_id=project_id,
plugin_id=self.__tk.pipeline_configuration.get_plugin_id(),
pipeline_configuration_id=self.__tk.pipeline_configuration.get_shotgun_id(),
bundle=self,
)
return self.__cache_location[project_id]
def get_setting(self, key, default=None):
"""
Get a value from the item's settings::
>>> app.get_setting('entity_types')
['Sequence', 'Shot', 'Asset', 'Task']
:param key: config name
:param default: default value to return
:returns: Value from the environment configuration
"""
return self.__resolve_setting_value(self.__settings, key, default)
def get_template(self, key):
"""
Returns a template object for a particular template setting in the Framework configuration.
This method will look at the app configuration, determine which template is being referred to
in the setting, go into the main platform Template API and fetch that particular template object.
This is a convenience method. Shorthand for ``self.sgtk.templates[ self.get_setting(key) ]``.
:param key: Setting to retrieve template for
:returns: :class:`~Template` object
"""
template_name = self.get_setting(key)
return self.get_template_by_name(template_name)
def get_template_by_name(self, template_name):
"""
Note: This is for advanced use cases - Most of the time you should probably use
:meth:`get_template()`. Find a particular template, the way it is named in the master
config file ``templates.yml``. This method will access the master templates file
directly and pull out a specifically named template without using the app config.
Note that using this method may result in code which is less portable across
studios, since it makes assumptions about how templates are named and defined in
the master config. Generally speaking, it is often better to access templates using
the app configuration and the get_template() method.
This is a convenience method. Shorthand for ``self.sgtk.templates[template_name]``.
:param template_name
:returns: :class:`~Template` object
"""
return self.tank.templates.get(template_name)
def execute_hook(self, key, base_class=None, **kwargs):
"""
Execute a hook that is part of the environment configuration for the current bundle.
Convenience method that calls :meth:`execute_hook_method()` with the
method_name parameter set to "execute".
.. warning:: This method is present for backwards compatibility. For
all new hooks, we recommend using :meth:`execute_hook_method`
instead.
You simply pass the name of the hook setting that you want to execute and
the accompanying arguments, and toolkit will find the correct hook file based
on the currently configured setting and then execute the execute() method for
that hook.
An optional ``base_class`` can be provided to override the default :class:`~sgtk.Hook`
base class. This is useful for bundles that want to define and document a strict
interface for hooks. The classes defined in the hook have to derived from this classes.
.. note:: For more information about hooks, see :class:`~sgtk.Hook`
:param key: The name of the hook setting you want to execute.
:param base_class: A python class to use as the base class for the created
hook. This will override the default hook base class, ``Hook``.
:returns: The return value from the hook
"""
hook_name = self.get_setting(key)
resolved_hook_paths = self.__resolve_hook_expression(key, hook_name)
return hook.execute_hook_method(
resolved_hook_paths, self, None, base_class=base_class, **kwargs
)
def execute_hook_method(self, key, method_name, base_class=None, **kwargs):
"""
Execute a specific method in a hook that is part of the
environment configuration for the current bundle.
You simply pass the name of the hook setting that you want to execute, the
name of the method you want to execute and the accompanying arguments.
Toolkit will find the correct hook file based on the currently configured
setting and then execute the specified method.
Hooks form a flexible way to extend and make toolkit apps or engines configurable.
A hook acts like a setting in that it needs to be configured as part of the app's
configuration, but instead of being a simple value, it is a code snippet contained
inside a class.
Apps typically provide default hooks to make installation and overriding easy.
Each hook is represented by a setting, similar to the ones you access via
the :meth:`get_setting()` method, however instead of retrieving a fixed value,
you execute code which generates a value.
This method will execute a specific method for a given hook setting. Toolkit will
find the actual python hook file and handle initialization and execution for you,
by looking at the configuration settings and resolve a path based on this.
Arguments should always be passed in by name. This is to make it easy to add new
parameters without breaking backwards compatibility, for example
``execute_hook_method("validator", "pre_check", name=curr_scene, version=curr_ver).``
An optional ``base_class`` can be provided to override the default :class:`~sgtk.Hook`
base class. This is useful for bundles that want to define and document a strict
interface for hooks. The classes defined in the hook have to derived from this classes.
.. note:: For more information about hooks, see :class:`~sgtk.Hook`
:param key: The name of the hook setting you want to execute.
:param method_name: Name of the method to execute
:param base_class: A python class to use as the base class for the created
hook. This will override the default hook base class, ``Hook``.
:returns: The return value from the hook
"""
hook_name = self.get_setting(key)
resolved_hook_paths = self.__resolve_hook_expression(key, hook_name)
return hook.execute_hook_method(
resolved_hook_paths, self, method_name, base_class=base_class, **kwargs
)
def execute_hook_expression(
self, hook_expression, method_name, base_class=None, **kwargs
):
"""
Execute an arbitrary hook via an expression. While the methods execute_hook
and execute_hook_method allows you to execute a particular hook setting as
specified in the app configuration manifest, this methods allows you to
execute a hook directly by passing a hook expression, for example
``{config}/path/to/my_hook.py``
This is useful if you are doing rapid app development and don't necessarily
want to expose a hook as a configuration setting just yet. It is also useful
if you have app settings that are nested deep inside of lists or dictionaries.
In that case, you cannot use execute_hook, but instead will have to retrieve
the value specifically and then run it.
An optional ``base_class`` can be provided to override the default :class:`~sgtk.Hook`
base class. This is useful for bundles that want to define and document a strict
interface for hooks. The classes defined in the hook have to derived from this classes.
.. note:: For more information about hooks, see :class:`~sgtk.Hook`
:param hook_expression: Path to hook to execute. See above for syntax details.
:param method_name: Method inside the hook to execute.
:param base_class: A python class to use as the base class for the created
hook. This will override the default hook base class, ``Hook``.
:returns: The return value from the hook
"""
resolved_hook_paths = self.__resolve_hook_expression(None, hook_expression)
return hook.execute_hook_method(
resolved_hook_paths, self, method_name, base_class=base_class, **kwargs
)
def execute_hook_by_name(self, hook_name, **kwargs):
"""
Execute an arbitrary hook located in the hooks folder for this project.
The hook_name is the name of the python file in which the hook resides,
without the file extension.
In most use cases, the execute_hook method is the preferred way to
access a hook from an app.
.. warning:: Now deprecated - Please use :meth:`execute_hook_expression`
instead.
This method is typically only used when you want to execute an arbitrary
list of hooks, for example if you want to run a series of arbitrary
user defined pre-publish validation hooks.
.. note:: For more information about hooks, see :class:`~sgtk.Hook`
:param hook_name: name of the legacy hook file to execute.
"""
hook_folder = self.tank.pipeline_configuration.get_hooks_location()
hook_path = os.path.join(hook_folder, "%s.py" % hook_name)
return hook.execute_hook(hook_path, self, **kwargs)
def create_hook_instance(self, hook_expression, base_class=None):
"""
Returns the instance of a hook object given an expression.
This is useful for complex workflows where it is beneficial to
maintain a handle to a hook instance. Normally, hooks are stateless
and every time a hook is called, a new instance is returned. This method
provides a standardized way to retrieve an instance of a hook::
self._plugin = app_object.create_hook_instance("{config}/path/to/my_hook.py")
self._plugin.execute_method_x()
self._plugin.execute_method_y()
self._plugin.execute_method_z()
The hook expression is the raw value that is specified in the configuration file.
If you want to access a configuration setting instead (like how for example
:meth:`execute_hook_method` works), simply call :meth:`get_setting()` to retrieve
the value and then pass the settings value to this method.
.. note:: For more information about hook syntax, see :class:`~sgtk.Hook`
An optional ``base_class`` can be provided to override the default :class:`~sgtk.Hook`
base class. This is useful for bundles that create hook instances at
execution time and wish to provide default implementation without the need
to configure the base hook. The supplied class must inherit from Hook.
:param hook_expression: Path to hook to execute. See above for syntax details.
:param base_class: A python class to use as the base class for the created
hook. This will override the default hook base class, ``Hook``.
:returns: :class:`Hook` instance.
"""
resolved_hook_paths = self.__resolve_hook_expression(None, hook_expression)
return hook.create_hook_instance(
resolved_hook_paths, self, base_class=base_class
)
def ensure_folder_exists(self, path):
"""
Make sure that the given folder exists on disk.
Convenience method to make it easy for apps and engines to create folders in a
standardized fashion. While the creation of high level folder structure such as
Shot and Asset folders is typically handled by the folder creation system in
Toolkit, Apps tend to need to create leaf-level folders such as publish folders
and work areas. These are often created just in time of the operation.
.. note:: This method calls out to the ``ensure_folder_exists`` core hook, making
the I/O operation user configurable. We recommend using this method
over the methods provided in ``sgtk.util.filesystem``.
:param path: path to create
"""
try:
self.__tk.execute_core_hook(
"ensure_folder_exists", path=path, bundle_obj=self
)
except Exception as e:
raise TankError("Error creating folder %s: %s" % (path, e))
def get_metrics_properties(self):
"""
Should be re-implemented in deriving classes and return a dictionary with
the properties needed to log a metric event for this bundle.
:raises: NotImplementedError
"""
raise NotImplementedError
##########################################################################################
# internal helpers
def _set_context(self, new_context):
"""
Sets the current context associated with this item.
:param new_context: The new context to associate with the bundle.
"""
self.__context = new_context
def _set_settings(self, settings):
"""
Sets the bundle's internal settings dictionary.
:param settings: The new settings dict to store.
"""
self.__settings = settings
def __resolve_hook_path(self, settings_name, hook_expression):
"""
Resolves a hook settings path into an absolute path.
:param settings_name: The name of the hook setting in the configuration. If the
hook expression passed in to this method is not directly
associated with a configuration setting, for example if it
comes from a nested settings structure and is resolved via
execute_hook_by_name, this parameter will be None.
:param hook_expression: The hook expression value that should be resolved.
:returns: A full path to a hook file.
"""
if hook_expression is None:
raise TankError(
"%s config setting %s: Configuration value cannot be None!"
% (self, settings_name)
)
path = None
# make sure to replace the `{engine_name}` token if it exists.
if constants.TANK_HOOK_ENGINE_REFERENCE_TOKEN in hook_expression:
engine_name = self._get_engine_name()
if not engine_name:
raise TankError(
"No engine could be determined for hook expression '%s'. "
"The hook could not be resolved." % (hook_expression,)
)
else:
hook_expression = hook_expression.replace(
constants.TANK_HOOK_ENGINE_REFERENCE_TOKEN, engine_name
)
# first the legacy, old-style hooks case
if hook_expression == constants.TANK_BUNDLE_DEFAULT_HOOK_SETTING:
# hook settings points to the default one.
# find the name of the hook from the manifest
manifest = self.__descriptor.configuration_schema
engine_name = self._get_engine_name()
# Entries are on the following form
#
# hook_publish_file:
# type: hook
# description: Called when a file is published, e.g. copied from a work area to a publish area.
# default_value: maya_publish_file
#
resolved_hook_name = resolve_default_value(
manifest.get(settings_name), engine_name=engine_name
)
# get the full path for the resolved hook name:
if resolved_hook_name.startswith("{self}"):
# new format hook:
# default_value: '{self}/my_hook.py'
hooks_folder = os.path.join(self.disk_location, "hooks")
path = resolved_hook_name.replace("{self}", hooks_folder)
path = path.replace("/", os.path.sep)
else:
# old style hook:
# default_value: 'my_hook'
path = os.path.join(
self.disk_location, "hooks", "%s.py" % resolved_hook_name
)
# if the hook uses the engine name then output a more useful error message if a hook for
# the engine can't be found.
if engine_name and not os.path.exists(path):
# produce user friendly error message
raise TankError(
"%s config setting %s: This hook is using an engine specific "
"hook setup (e.g '%s') but no hook '%s' has been provided with the app. "
"In order for this app to work with engine %s, you need to provide a "
"custom hook implementation. Please contact support for more "
"information"
% (self, settings_name, resolved_hook_name, path, engine_name)
)
elif hook_expression.startswith("{self}"):
# bundle local reference
hooks_folder = os.path.join(self.disk_location, "hooks")
path = hook_expression.replace("{self}", hooks_folder)
path = path.replace("/", os.path.sep)
elif hook_expression.startswith("{config}"):
# config hook
hooks_folder = self.tank.pipeline_configuration.get_hooks_location()
path = hook_expression.replace("{config}", hooks_folder)
path = path.replace("/", os.path.sep)
elif hook_expression.startswith("{engine}"):
# look for the hook in the currently running engine
try:
engine = self.engine
except AttributeError:
raise TankError(
"%s config setting %s: Could not determine the current "
"engine. Unable to resolve hook path for: '%s'"
% (self, settings_name, hook_expression)
)
hooks_folder = os.path.join(engine.disk_location, "hooks")
path = hook_expression.replace("{engine}", hooks_folder)
path = path.replace("/", os.path.sep)
elif hook_expression.startswith("{$") and "}" in hook_expression:
# environment variable: {$HOOK_PATH}/path/to/foo.py
env_var = re.match(r"^\{\$([^\}]+)\}", hook_expression).group(1)
if env_var not in os.environ:
raise TankError(
"%s config setting %s: This hook is referring to the configuration value '%s', "
"but no environment variable named '%s' can be "
"found!" % (self, settings_name, hook_expression, env_var)
)
env_var_value = os.environ[env_var]
path = hook_expression.replace("{$%s}" % env_var, env_var_value)
path = path.replace("/", os.path.sep)
elif hook_expression.startswith("{") and "}" in hook_expression:
# bundle instance (e.g. '{tk-framework-perforce_v1.x.x}/foo/bar.py' )
# first find the bundle instance
instance = re.match(r"^\{([^\}]+)\}", hook_expression).group(1)
# for now, only look at framework instance names. Later on,
# if the request ever comes up, we could consider extending
# to supporting app instances etc. However we would need to
# have some implicit rules for handling ambiguity since
# there can be multiple items (engines, apps etc) potentially
# having the same instance name.
fw_instances = self.__environment.get_frameworks()
if instance not in fw_instances:
raise TankError(
"%s config setting %s: This hook is referring to the configuration value '%s', "
"but no framework with instance name '%s' can be found in the currently "
"running environment. The currently loaded frameworks "
"are %s."
% (
self,
settings_name,
hook_expression,
instance,
", ".join(fw_instances),
)
)
fw_desc = self.__environment.get_framework_descriptor(instance)
if not (fw_desc.exists_local()):
raise TankError(
"%s config setting %s: This hook is referring to the configuration value '%s', "
"but the framework with instance name '%s' does not exist on disk. Please run "
"the tank cache_apps command."
% (self, settings_name, hook_expression, instance)
)
# get path to framework on disk
hooks_folder = os.path.join(fw_desc.get_path(), "hooks")
# create the path to the file
path = hook_expression.replace("{%s}" % instance, hooks_folder)
path = path.replace("/", os.path.sep)
else:
# old school config hook name, e.g. just 'foo'
hook_folder = self.tank.pipeline_configuration.get_hooks_location()
path = os.path.join(hook_folder, "%s.py" % hook_expression)
return path
def __resolve_hook_expression(self, settings_name, hook_expression):
"""
Internal method for resolving hook expressions. This method handles
resolving an environment configuration value into a path on disk.
There are two generations of hook formats - old-style and new-style.
Old style formats:
- hook_setting: foo -- Resolves 'foo' to CURRENT_PC/hooks/foo.py
- hook_setting: default -- Resolves the value from the info.yml manifest and uses
the default hook code supplied by the bundle.
New style formats:
- hook_setting: {$HOOK_PATH}/path/to/foo.py -- environment variable.
- hook_setting: {self}/path/to/foo.py -- looks in the hooks folder in the local bundle
- hook_setting: {config}/path/to/foo.py -- looks in the hooks folder in the config
- hook_setting: {engine}/path/to/foo.py -- looks in the hooks folder of the current engine.
- hook_setting: {tk-framework-perforce_v1.x.x}/path/to/foo.py -- looks in the hooks folder of a
framework instance that exists in the current environment. Basically, each entry inside the
frameworks section in the current environment can be specified here - all these entries are
on the form frameworkname_versionpattern, for example tk-framework-widget_v0.1.2 or
tk-framework-shotgunutils_v1.3.x.
:param settings_name: If this hook is associated with a setting in the bundle, this is the
name of that setting. This is used to identify the inheritance relationships
between the hook expression that is evaluated and if this hook derives from
a hook inside an app.
:param hook_expression: The path expression to a hook.
:returns: List of paths to hooks files.
"""
# split up the config value into distinct items
unresolved_hook_paths = hook_expression.split(":")
# first of all, see if we should add a base class hook to derive from:
#
# Basically, any overridden hook implicitly derives from the default hook.
# specified in the manifest.
# if the settings value is not {self} add this to the inheritance chain.
# Examples:
#
# Manifest: {self}/foo_{engine_name}.py
# In config: {config}/my_custom_hook.py
# The my_custom_hook.py implicitly derives from the python class defined
# in the manifest, so prepend it:
# hook_paths: ["{self}/foo_tk-maya.py", "{config}/my_custom_hook.py" ]
#
# Check only new-style hooks. All new style hooks start with a {
if unresolved_hook_paths[0].startswith("{") and not unresolved_hook_paths[
0
].startswith("{self}"):
# this is a new style hook that is not the default hook value.
# now prepend the default hook first in the list
manifest = self.__descriptor.configuration_schema
default_value = None