Panda3D
pythonLoaderFileType.cxx
Go to the documentation of this file.
1 /**
2  * PANDA 3D SOFTWARE
3  * Copyright (c) Carnegie Mellon University. All rights reserved.
4  *
5  * All use of this software is subject to the terms of the revised BSD
6  * license. You should have received a copy of this license along
7  * with this source code in a file named "LICENSE."
8  *
9  * @file pythonLoaderFileType.cxx
10  * @author rdb
11  * @date 2019-07-29
12  */
13 
14 #include "pythonLoaderFileType.h"
15 
16 #ifdef HAVE_PYTHON
17 
18 #include "modelRoot.h"
19 #include "pythonThread.h"
20 #include "py_panda.h"
21 #include "virtualFileSystem.h"
22 
23 extern struct Dtool_PyTypedObject Dtool_BamCacheRecord;
24 extern struct Dtool_PyTypedObject Dtool_Filename;
25 extern struct Dtool_PyTypedObject Dtool_LoaderOptions;
26 extern struct Dtool_PyTypedObject Dtool_PandaNode;
27 extern struct Dtool_PyTypedObject Dtool_PythonLoaderFileType;
28 
29 TypeHandle PythonLoaderFileType::_type_handle;
30 
31 /**
32  * This constructor expects init() to be called manually.
33  */
34 PythonLoaderFileType::
35 PythonLoaderFileType() {
36  init_type();
37 }
38 
39 /**
40  * This constructor expects a single pkg_resources.EntryPoint argument for a
41  * deferred loader.
42  */
43 PythonLoaderFileType::
44 PythonLoaderFileType(std::string extension, PyObject *entry_point) :
45  _extension(std::move(extension)),
46  _entry_point(entry_point) {
47 
48  init_type();
49  Py_INCREF(entry_point);
50 }
51 
52 /**
53  * Destructor.
54  */
55 PythonLoaderFileType::
56 ~PythonLoaderFileType() {
57  if (_entry_point != nullptr || _load_func != nullptr || _save_func != nullptr) {
58 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
59  PyGILState_STATE gstate;
60  gstate = PyGILState_Ensure();
61 #endif
62 
63  Py_CLEAR(_entry_point);
64  Py_CLEAR(_load_func);
65  Py_CLEAR(_save_func);
66 
67 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
68  PyGILState_Release(gstate);
69 #endif
70  }
71 }
72 
73 /**
74  * Initializes the fields from the given Python loader object.
75  */
76 bool PythonLoaderFileType::
77 init(PyObject *loader) {
78  nassertr(loader != nullptr, false);
79  nassertr(_load_func == nullptr, false);
80  nassertr(_save_func == nullptr, false);
81 
82  // Check the extensions member. If we already have a registered extension,
83  // it must occur in the list.
84  PyObject *extensions = PyObject_GetAttrString(loader, "extensions");
85  if (extensions != nullptr) {
86  if (PyUnicode_Check(extensions)
87 #if PY_MAJOR_VERSION < 3
88  || PyString_Check(extensions)
89 #endif
90  ) {
91  Dtool_Raise_TypeError("extensions list should be a list or tuple");
92  Py_DECREF(extensions);
93  return false;
94  }
95 
96  PyObject *sequence = PySequence_Fast(extensions, "extensions must be a sequence");
97  PyObject **items = PySequence_Fast_ITEMS(sequence);
98  Py_ssize_t num_items = PySequence_Fast_GET_SIZE(sequence);
99  Py_DECREF(extensions);
100 
101  if (num_items == 0) {
102  PyErr_SetString(PyExc_ValueError, "extensions list may not be empty");
103  Py_DECREF(sequence);
104  return false;
105  }
106 
107  bool found_extension = false;
108 
109  for (Py_ssize_t i = 0; i < num_items; ++i) {
110  PyObject *extension = items[i];
111  const char *extension_str;
112  Py_ssize_t extension_len;
113  #if PY_MAJOR_VERSION >= 3
114  extension_str = PyUnicode_AsUTF8AndSize(extension, &extension_len);
115  #else
116  if (PyString_AsStringAndSize(extension, (char **)&extension_str, &extension_len) == -1) {
117  extension_str = nullptr;
118  }
119  #endif
120 
121  if (extension_str == nullptr) {
122  Py_DECREF(sequence);
123  return false;
124  }
125 
126  if (_extension.empty()) {
127  _extension.assign(extension_str, extension_len);
128  found_extension = true;
129  } else {
130  std::string new_extension(extension_str, extension_len);
131  if (_extension == new_extension) {
132  found_extension = true;
133  } else {
134  if (!_additional_extensions.empty()) {
135  _additional_extensions += ' ';
136  }
137  _additional_extensions += new_extension;
138  }
139  }
140  }
141  Py_DECREF(sequence);
142 
143  if (!found_extension) {
144  PyObject *repr = PyObject_Repr(loader);
145  loader_cat.error()
146  << "Registered extension '" << _extension
147  << "' does not occur in extensions list of "
148 #if PY_MAJOR_VERSION >= 3
149  << PyUnicode_AsUTF8(repr) << "\n";
150 #else
151  << PyString_AsString(repr) << "\n";
152 #endif
153  Py_DECREF(repr);
154  return false;
155  }
156  } else {
157  return false;
158  }
159 
160  PyObject *supports_compressed = PyObject_GetAttrString(loader, "supports_compressed");
161  if (supports_compressed != nullptr) {
162  if (supports_compressed == Py_True) {
163  _supports_compressed = true;
164  }
165  else if (supports_compressed == Py_False) {
166  _supports_compressed = false;
167  }
168  else {
169  Dtool_Raise_TypeError("supports_compressed must be a bool");
170  Py_DECREF(supports_compressed);
171  return false;
172  }
173  Py_DECREF(supports_compressed);
174  }
175 
176  _load_func = PyObject_GetAttrString(loader, "load_file");
177  _save_func = PyObject_GetAttrString(loader, "save_file");
178  PyErr_Clear();
179 
180  if (_load_func == nullptr && _save_func == nullptr) {
181 #if PY_MAJOR_VERSION >= 3
182  PyErr_Format(PyExc_TypeError,
183  "loader plug-in %R does not define load_file or save_file function",
184  loader);
185 #else
186  PyObject *repr = PyObject_Repr(loader);
187  PyErr_Format(PyExc_TypeError,
188  "loader plug-in %s does not define load_file or save_file function",
189  PyString_AsString(repr));
190  Py_DECREF(repr);
191 #endif
192  return false;
193  }
194 
195  // We don't need this any more.
196  Py_CLEAR(_entry_point);
197 
198  return true;
199 }
200 
201 /**
202  * Ensures that the referenced Python module is loaded.
203  */
204 bool PythonLoaderFileType::
205 ensure_loaded() const {
206  if (_load_func != nullptr || _save_func != nullptr) {
207  return true;
208  }
209  nassertr_always(_entry_point != nullptr, false);
210 
211 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
212  PyGILState_STATE gstate;
213  gstate = PyGILState_Ensure();
214 #endif
215 
216  if (loader_cat.is_info()) {
217  PyObject *repr = PyObject_Repr(_entry_point);
218 
219  loader_cat.info()
220  << "loading file type module: "
221 #if PY_MAJOR_VERSION >= 3
222  << PyUnicode_AsUTF8(repr) << "\n";
223 #else
224  << PyString_AsString(repr) << "\n";
225 #endif
226  Py_DECREF(repr);
227  }
228 
229  PyObject *result = PyObject_CallMethod(_entry_point, (char *)"load", nullptr);
230 
231  bool success = false;
232  if (result != nullptr) {
233  success = ((PythonLoaderFileType *)this)->init(result);
234  } else {
235  PyErr_Clear();
236  PyObject *repr = PyObject_Repr(_entry_point);
237 
238  loader_cat.error()
239  << "unable to load "
240 #if PY_MAJOR_VERSION >= 3
241  << PyUnicode_AsUTF8(repr) << "\n";
242 #else
243  << PyString_AsString(repr) << "\n";
244 #endif
245  Py_DECREF(repr);
246  }
247 
248 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
249  PyGILState_Release(gstate);
250 #endif
251 
252  return success;
253 }
254 
255 /**
256  *
257  */
258 std::string PythonLoaderFileType::
259 get_name() const {
260  return "Python loader";
261 }
262 
263 /**
264  *
265  */
266 std::string PythonLoaderFileType::
267 get_extension() const {
268  return _extension;
269 }
270 
271 /**
272  * Returns a space-separated list of extension, in addition to the one
273  * returned by get_extension(), that are recognized by this converter.
274  */
275 std::string PythonLoaderFileType::
276 get_additional_extensions() const {
277  return _additional_extensions;
278 }
279 
280 /**
281  * Returns true if this file type can transparently load compressed files
282  * (with a .pz or .gz extension), false otherwise.
283  */
284 bool PythonLoaderFileType::
285 supports_compressed() const {
286  return ensure_loaded() && _supports_compressed;
287 }
288 
289 /**
290  * Returns true if the file type can be used to load files, and load_file() is
291  * supported. Returns false if load_file() is unimplemented and will always
292  * fail.
293  */
294 bool PythonLoaderFileType::
295 supports_load() const {
296  return ensure_loaded() && _load_func != nullptr;
297 }
298 
299 /**
300  * Returns true if the file type can be used to save files, and save_file() is
301  * supported. Returns false if save_file() is unimplemented and will always
302  * fail.
303  */
304 bool PythonLoaderFileType::
305 supports_save() const {
306  return ensure_loaded() && _save_func != nullptr;
307 }
308 
309 /**
310  *
311  */
312 PT(PandaNode) PythonLoaderFileType::
313 load_file(const Filename &path, const LoaderOptions &options,
314  BamCacheRecord *record) const {
315  // Let's check whether the file even exists before calling Python.
317  PT(VirtualFile) vfile = vfs->get_file(path);
318  if (vfile == nullptr) {
319  return nullptr;
320  }
321 
322  if (!supports_load()) {
323  return nullptr;
324  }
325 
326  if (record != nullptr) {
327  record->add_dependent_file(vfile);
328  }
329 
330 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
331  PyGILState_STATE gstate;
332  gstate = PyGILState_Ensure();
333 #endif
334 
335  // Wrap the arguments.
336  PyObject *args = PyTuple_New(3);
337  PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true));
338  PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true));
339  if (record != nullptr) {
340  record->ref();
341  PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)record, Dtool_BamCacheRecord, true, false, record->get_type_index()));
342  } else {
343  PyTuple_SET_ITEM(args, 2, Py_None);
344  Py_INCREF(Py_None);
345  }
346 
347  PT(PandaNode) node;
348 
349  PyObject *result = PythonThread::call_python_func(_load_func, args);
350  if (result != nullptr) {
351  if (DtoolInstance_Check(result)) {
352  node = (PandaNode *)DtoolInstance_UPCAST(result, Dtool_PandaNode);
353  }
354  Py_DECREF(result);
355  }
356 
357  Py_DECREF(args);
358 
359  if (node == nullptr) {
360  PyObject *exc_type = _PyErr_OCCURRED();
361  if (!exc_type) {
362  loader_cat.error()
363  << "load_file must return valid PandaNode or raise exception\n";
364  } else {
365  loader_cat.error()
366  << "Loading " << path.get_basename()
367  << " failed with " << ((PyTypeObject *)exc_type)->tp_name << " exception.\n";
368  PyErr_Clear();
369  }
370  }
371 
372 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
373  PyGILState_Release(gstate);
374 #endif
375 
376  if (node != nullptr && node->is_of_type(ModelRoot::get_class_type())) {
377  ModelRoot *model_root = DCAST(ModelRoot, node.p());
378  model_root->set_fullpath(path);
379  model_root->set_timestamp(vfile->get_timestamp());
380  }
381 
382  return node;
383 }
384 
385 /**
386  *
387  */
388 bool PythonLoaderFileType::
389 save_file(const Filename &path, const LoaderOptions &options,
390  PandaNode *node) const {
391  if (!supports_save()) {
392  return false;
393  }
394 
395  nassertr(node != nullptr, false);
396  node->ref();
397 
398 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
399  PyGILState_STATE gstate;
400  gstate = PyGILState_Ensure();
401 #endif
402 
403  // Wrap the arguments.
404  PyObject *args = PyTuple_New(3);
405  PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true));
406  PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true));
407  PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)node, Dtool_PandaNode, true, false, node->get_type_index()));
408 
409  PyObject *result = PythonThread::call_python_func(_load_func, args);
410  Py_DECREF(args);
411  if (result != nullptr) {
412  Py_DECREF(result);
413  } else {
414  PyErr_Clear();
415  loader_cat.error()
416  << "save_file failed with an exception.\n";
417  }
418 
419 #if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS)
420  PyGILState_Release(gstate);
421 #endif
422 
423  return (result != nullptr);
424 }
425 
426 #endif // HAVE_PYTHON
An instance of this class is written to the front of a Bam or Txo file to make the file a cached inst...
void add_dependent_file(const Filename &pathname)
Adds the indicated file to the list of files that will be loaded to generate the data in this record.
The name of a file, such as a texture file or an Egg file.
Definition: filename.h:39
std::string get_basename() const
Returns the basename part of the filename.
Definition: filename.I:367
Specifies parameters that may be passed to the loader.
Definition: loaderOptions.h:23
A node of this type is created automatically at the root of each model file that is loaded.
Definition: modelRoot.h:27
set_fullpath
Sets the full pathname of the model represented by this node, as found on disk.
Definition: modelRoot.h:37
set_timestamp
Sets the timestamp of the file on disk that was read for this model.
Definition: modelRoot.h:41
A basic node of the scene graph or data graph.
Definition: pandaNode.h:65
void ref() const
Explicitly increments the reference count.
TypeHandle is the identifier used to differentiate C++ class types.
Definition: typeHandle.h:81
int get_type_index() const
Returns the internal index number associated with this object's TypeHandle, a unique number for each ...
Definition: typedObject.I:20
A hierarchy of directories and files that appears to be one continuous file system,...
PointerTo< VirtualFile > get_file(const Filename &filename, bool status_only=false) const
Looks up the file by the indicated name in the file system.
static VirtualFileSystem * get_global_ptr()
Returns the default global VirtualFileSystem.
The abstract base class for a file or directory within the VirtualFileSystem.
Definition: virtualFile.h:35
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PyObject * DTool_CreatePyInstance(const T *obj, bool memory_rules)
These functions wrap a pointer for a class that defines get_type_handle().
Definition: py_panda.I:124
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.