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.
This commit is contained in:
rdb 2016-12-07 00:42:44 +01:00
parent 3fa5b6b4ee
commit e778c529b2
4 changed files with 145 additions and 15 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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;
};