Browse Source

Implement Python 3.6 fspath protocol; allow passing a pathlib.Path wherever Filename is expected
The Python 3.6 fspath protocol allows passing Filename objects into any Python standard library calls that take a path.

rdb 9 years ago
parent
commit
e778c529b2

+ 15 - 10
dtool/src/dtoolutil/filename.I

@@ -38,7 +38,6 @@ Filename(const char *filename) {
   (*this) = filename;
 }
 
-
 /**
  *
  */
@@ -84,6 +83,20 @@ Filename(Filename &&from) NOEXCEPT :
 }
 #endif  // USE_MOVE_SEMANTICS
 
+/**
+ * Creates an empty Filename.
+ */
+INLINE Filename::
+Filename() :
+  _dirname_end(0),
+  _basename_start(0),
+  _basename_end(string::npos),
+  _extension_start(string::npos),
+  _hash_start(string::npos),
+  _hash_end(string::npos),
+  _flags(0) {
+}
+
 /**
  *
  */
@@ -155,14 +168,6 @@ pattern_filename(const string &filename) {
   return result;
 }
 
-/**
- *
- */
-INLINE Filename::
-~Filename() {
-}
-
-
 /**
  *
  */
@@ -233,7 +238,7 @@ operator = (string &&filename) NOEXCEPT {
  */
 INLINE Filename &Filename::
 operator = (Filename &&from) NOEXCEPT {
-  _filename = MOVE(from._filename);
+  _filename = move(from._filename);
   _dirname_end = from._dirname_end;
   _basename_start = from._basename_start;
   _basename_end = from._basename_end;

+ 8 - 5
dtool/src/dtoolutil/filename.h

@@ -55,20 +55,22 @@ public:
   };
 
   INLINE Filename(const char *filename);
-
-PUBLISHED:
-  INLINE Filename(const string &filename = "");
+  INLINE Filename(const string &filename);
   INLINE Filename(const wstring &filename);
   INLINE Filename(const Filename &copy);
-  Filename(const Filename &dirname, const Filename &basename);
-  INLINE ~Filename();
 
 #ifdef USE_MOVE_SEMANTICS
   INLINE Filename(string &&filename) NOEXCEPT;
   INLINE Filename(Filename &&from) NOEXCEPT;
 #endif
 
+PUBLISHED:
+  INLINE Filename();
+  Filename(const Filename &dirname, const Filename &basename);
+
 #ifdef HAVE_PYTHON
+  EXTENSION(Filename(PyObject *path));
+
   EXTENSION(PyObject *__reduce__(PyObject *self) const);
 #endif
 
@@ -118,6 +120,7 @@ PUBLISHED:
   INLINE char operator [] (size_t n) const;
 
   EXTENSION(PyObject *__repr__() const);
+  EXTENSION(PyObject *__fspath__() const);
 
   INLINE string substr(size_t begin) const;
   INLINE string substr(size_t begin, size_t end) const;

+ 119 - 0
panda/src/express/filename_ext.cxx

@@ -14,6 +14,115 @@
 #include "filename_ext.h"
 
 #ifdef HAVE_PYTHON
+
+#ifndef CPPPARSER
+extern Dtool_PyTypedObject Dtool_Filename;
+#endif  // CPPPARSER
+
+/**
+ * Constructs a Filename object from a str, bytes object, or os.PathLike.
+ */
+void Extension<Filename>::
+__init__(PyObject *path) {
+  nassertv(path != NULL);
+  nassertv(_this != NULL);
+
+  Py_ssize_t length;
+
+  if (PyUnicode_CheckExact(path)) {
+    wchar_t *data;
+#if PY_VERSION_HEX >= 0x03020000
+    data = PyUnicode_AsWideCharString(path, &length);
+#else
+    length = PyUnicode_GET_SIZE(path);
+    data = (wchar_t *)alloca(sizeof(wchar_t) * (length + 1));
+    PyUnicode_AsWideChar((PyUnicodeObject *)path, data, length);
+#endif
+    (*_this) = wstring(data, length);
+
+#if PY_VERSION_HEX >= 0x03020000
+    PyMem_Free(data);
+#endif
+    return;
+  }
+
+  if (PyBytes_CheckExact(path)) {
+    char *data;
+    PyBytes_AsStringAndSize(path, &data, &length);
+    (*_this) = string(data, length);
+    return;
+  }
+
+  if (Py_TYPE(path) == &Dtool_Filename._PyType) {
+    // Copy constructor.
+    (*_this) = *((Filename *)((Dtool_PyInstDef *)path)->_ptr_to_object);
+    return;
+  }
+
+  PyObject *path_str;
+
+#if PY_VERSION_HEX >= 0x03060000
+  // It must be an os.PathLike object.  Check for an __fspath__ method.
+  PyObject *fspath = PyObject_GetAttrString((PyObject *)Py_TYPE(path), "__fspath__");
+  if (fspath == NULL) {
+    PyErr_Format(PyExc_TypeError, "expected str, bytes or os.PathLike object, not %s", Py_TYPE(path)->tp_name);
+    return;
+  }
+
+  path_str = PyObject_CallFunctionObjArgs(fspath, path, NULL);
+  Py_DECREF(fspath);
+#else
+  // There is no standard path protocol before Python 3.6, but let's try and
+  // support taking pathlib paths anyway.  We don't version check this to
+  // allow people to use backports of the pathlib module.
+  if (PyObject_HasAttrString(path, "_format_parsed_parts")) {
+    path_str = PyObject_Str(path);
+  } else {
+#if PY_VERSION_HEX >= 0x03040000
+    PyErr_Format(PyExc_TypeError, "expected str, bytes, Path or Filename object, not %s", Py_TYPE(path)->tp_name);
+#elif PY_MAJOR_VERSION >= 3
+    PyErr_Format(PyExc_TypeError, "expected str, bytes or Filename object, not %s", Py_TYPE(path)->tp_name);
+#else
+    PyErr_Format(PyExc_TypeError, "expected str or unicode object, not %s", Py_TYPE(path)->tp_name);
+#endif
+    return;
+  }
+#endif
+
+  if (path_str == NULL) {
+    return;
+  }
+
+  if (PyUnicode_CheckExact(path_str)) {
+    wchar_t *data;
+#if PY_VERSION_HEX >= 0x03020000
+    data = PyUnicode_AsWideCharString(path_str, &length);
+#else
+    length = PyUnicode_GET_SIZE(path_str);
+    data = (wchar_t *)alloca(sizeof(wchar_t) * (length + 1));
+    PyUnicode_AsWideChar((PyUnicodeObject *)path_str, data, length);
+#endif
+    (*_this) = Filename::from_os_specific_w(wstring(data, length));
+
+#if PY_VERSION_HEX >= 0x03020000
+    PyMem_Free(data);
+#endif
+
+  } else if (PyBytes_CheckExact(path_str)) {
+    char *data;
+    PyBytes_AsStringAndSize(path_str, &data, &length);
+    (*_this) = Filename::from_os_specific(string(data, length));
+
+  } else {
+#if PY_MAJOR_VERSION >= 3
+    PyErr_Format(PyExc_TypeError, "expected str or bytes object, not %s", Py_TYPE(path_str)->tp_name);
+#else
+    PyErr_Format(PyExc_TypeError, "expected str or unicode object, not %s", Py_TYPE(path_str)->tp_name);
+#endif
+  }
+  Py_DECREF(path_str);
+}
+
 /**
  * This special Python method is implement to provide support for the pickle
  * module.
@@ -62,6 +171,16 @@ __repr__() const {
   return result;
 }
 
+/**
+ * Allows a Filename object to be passed to any Python function that accepts
+ * an os.PathLike object.
+ */
+PyObject *Extension<Filename>::
+__fspath__() const {
+  wstring filename = _this->to_os_specific_w();
+  return PyUnicode_FromWideChar(filename.data(), (Py_ssize_t)filename.size());
+}
+
 /**
  * This variant on scan_directory returns a Python list of strings on success,
  * or None on failure.

+ 3 - 0
panda/src/express/filename_ext.h

@@ -29,8 +29,11 @@
 template<>
 class Extension<Filename> : public ExtensionBase<Filename> {
 public:
+  void __init__(PyObject *path);
+
   PyObject *__reduce__(PyObject *self) const;
   PyObject *__repr__() const;
+  PyObject *__fspath__() const;
   PyObject *scan_directory() const;
 };