1#
2# Copyright 2016 Pixar
3#
4# Licensed under the Apache License, Version 2.0 (the "Apache License")
5# with the following modification; you may not use this file except in
6# compliance with the Apache License and the following modification to it:
7# Section 6. Trademarks. is deleted and replaced with:
8#
9# 6. Trademarks. This License does not grant permission to use the trade
10#    names, trademarks, service marks, or product names of the Licensor
11#    and its affiliates, except as required to comply with Section 4(c) of
12#    the License and to reproduce the content of the NOTICE file.
13#
14# You may obtain a copy of the Apache License at
15#
16#     http://www.apache.org/licenses/LICENSE-2.0
17#
18# Unless required by applicable law or agreed to in writing, software
19# distributed under the Apache License with the above modification is
20# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21# KIND, either express or implied. See the Apache License for the specific
22# language governing permissions and limitations under the Apache License.
23#
24"""
25Tf -- Tools Foundation
26"""
27
28# Type to help handle DLL import paths on Windows with python interpreters v3.8
29# and newer. These interpreters don't search for DLLs in the path anymore, you
30# have to provide a path explicitly. This re-enables path searching for USD
31# dependency libraries
32import platform, sys
33if sys.version_info >= (3, 8) and platform.system() == "Windows":
34    import contextlib
35
36    @contextlib.contextmanager
37    def WindowsImportWrapper():
38        import os
39        dirs = []
40        import_paths = os.getenv('PXR_USD_WINDOWS_DLL_PATH')
41        if import_paths is None:
42            import_paths = os.getenv('PATH', '')
43        for path in import_paths.split(os.pathsep):
44            # Calling add_dll_directory raises an exception if paths don't
45            # exist, or if you pass in dot
46            if os.path.exists(path) and path != '.':
47                dirs.append(os.add_dll_directory(path))
48        # This block guarantees we clear the dll directories if an exception
49        # is raised in the with block.
50        try:
51            yield
52        finally:
53            for dll_dir in dirs:
54                dll_dir.close()
55        del os
56    del contextlib
57else:
58    class WindowsImportWrapper(object):
59        def __enter__(self):
60            pass
61        def __exit__(self, exc_type, ex_val, exc_tb):
62            pass
63del platform, sys
64
65
66def PreparePythonModule(moduleName=None):
67    """Prepare an extension module at import time.  This will import the
68    Python module associated with the caller's module (e.g. '_tf' for 'pxr.Tf')
69    or the module with the specified moduleName and copy its contents into
70    the caller's local namespace.
71
72    Generally, this should only be called by the __init__.py script for a module
73    upon loading a boost python module (generally '_libName.so')."""
74    import importlib
75    import inspect
76    frame = inspect.currentframe().f_back
77    try:
78        f_locals = frame.f_locals
79
80        # If an explicit moduleName is not supplied, construct it from the
81        # caller's module name, like "pxr.Tf", and our naming conventions,
82        # which results in "_tf".
83        if moduleName is None:
84            moduleName = f_locals["__name__"].split(".")[-1]
85            moduleName = "_" + moduleName[0].lower() + moduleName[1:]
86
87        with WindowsImportWrapper():
88            module = importlib.import_module(
89                    "." + moduleName, f_locals["__name__"])
90
91        PrepareModule(module, f_locals)
92        try:
93            del f_locals[moduleName]
94        except KeyError:
95            pass
96
97        try:
98            module = importlib.import_module(".__DOC", f_locals["__name__"])
99            module.Execute(f_locals)
100            try:
101                del f_locals["__DOC"]
102            except KeyError:
103                pass
104        except Exception:
105            pass
106
107    finally:
108        del frame
109
110def PrepareModule(module, result):
111    """PrepareModule(module, result) -- Prepare an extension module at import
112    time.  Generally, this should only be called by the __init__.py script for a
113    module upon loading a boost python module (generally '_libName.so')."""
114    # inject into result.
115    ignore = frozenset(['__name__', '__package__', '__builtins__',
116                        '__doc__', '__file__', '__path__'])
117    newModuleName = result.get('__name__')
118
119    for key, value in module.__dict__.items():
120        if not key in ignore:
121            result[key] = value
122
123            # Lie about the module from which value came.
124            if newModuleName and hasattr(value, '__module__'):
125                try:
126                    setattr(value, '__module__', newModuleName)
127                except AttributeError as e:
128                    # The __module__ attribute of Boost.Python.function
129                    # objects is not writable, so we get this exception
130                    # a lot.  Just ignore it.  We're really only concerned
131                    # about the data objects like enum values and such.
132                    #
133                    pass
134
135def GetCodeLocation(framesUp):
136    """Returns a tuple (moduleName, functionName, fileName, lineNo).
137
138    To trace the current location of python execution, use GetCodeLocation().
139    By default, the information is returned at the current stack-frame; thus
140
141        info = GetCodeLocation()
142
143    will return information about the line that GetCodeLocation() was called
144    from. One can write:
145
146        def genericDebugFacility():
147            info = GetCodeLocation(1)
148            # print out data
149
150
151        def someCode():
152            ...
153            if bad:
154                genericDebugFacility()
155
156    and genericDebugFacility() will get information associated with its caller,
157    i.e. the function someCode()."""
158    import sys
159    f_back = sys._getframe(framesUp).f_back
160    return (f_back.f_globals['__name__'], f_back.f_code.co_name,
161            f_back.f_code.co_filename, f_back.f_lineno)
162
163PreparePythonModule()
164
165# Need to provide an exception type that tf errors will show up as.
166class ErrorException(RuntimeError):
167    def __init__(self, *args):
168        RuntimeError.__init__(self, *args)
169        self.__TfException = True
170
171    def __str__(self):
172        return '\n\t' + '\n\t'.join([str(e) for e in self.args])
173__SetErrorExceptionClass(ErrorException)
174
175def Warn(msg, template=""):
176    """Issue a warning via the TfDiagnostic system.
177
178    At this time, template is ignored.
179    """
180    codeInfo = GetCodeLocation(framesUp=1)
181    _Warn(msg, codeInfo[0], codeInfo[1], codeInfo[2], codeInfo[3])
182
183def Status(msg, verbose=True):
184    """Issues a status update to the Tf diagnostic system.
185
186    If verbose is True (the default) then information about where in the code
187    the status update was issued from is included.
188    """
189    if verbose:
190        codeInfo = GetCodeLocation(framesUp=1)
191        _Status(msg, codeInfo[0], codeInfo[1], codeInfo[2], codeInfo[3])
192    else:
193        _Status(msg, "", "", "", 0)
194
195def RaiseCodingError(msg):
196    """Raise a coding error to the Tf Diagnostic system."""
197    codeInfo = GetCodeLocation(framesUp=1)
198    _RaiseCodingError(msg, codeInfo[0], codeInfo[1], codeInfo[2], codeInfo[3])
199
200def RaiseRuntimeError(msg):
201    """Raise a runtime error to the Tf Diagnostic system."""
202    codeInfo = GetCodeLocation(framesUp=1)
203    _RaiseRuntimeError(msg, codeInfo[0], codeInfo[1], codeInfo[2], codeInfo[3])
204
205def Fatal(msg):
206    """Raise a fatal error to the Tf Diagnostic system."""
207    codeInfo = GetCodeLocation(framesUp=1)
208    _Fatal(msg, codeInfo[0], codeInfo[1], codeInfo[2], codeInfo[3])
209
210
211class NamedTemporaryFile(object):
212    """A named temporary file which keeps the internal file handle closed.
213       A class which constructs a temporary file(that isn't open) on __enter__,
214       provides its name as an attribute, and deletes it on __exit__.
215
216       Note: The constructor args for this object match those of
217       python's tempfile.mkstemp() function, and will have the same effect on
218       the underlying file created."""
219
220    def __init__(self, suffix='', prefix='', dir=None, text=False):
221        # Note that we defer creation until the enter block to
222        # prevent users from unintentionally creating a bunch of
223        # temp files that don't get cleaned up.
224        self._args = (suffix,  prefix, dir, text)
225
226    def __enter__(self):
227        from tempfile import mkstemp
228        from os import close
229
230        fd, path = mkstemp(*self._args)
231        close(fd)
232
233        # XXX: We currently only expose the name attribute
234        # more can be added based on client needs in the future.
235        self._name = path
236
237        return self
238
239    def __exit__(self, *args):
240        import os
241        os.remove(self.name)
242
243    @property
244    def name(self):
245        """The path for the temporary file created."""
246        return self._name
247