Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions python/src/bind_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,30 @@ void odr_python::bind_functions(py::module_ &m) {
m.def("capabilities_by_file_type", &odr::capabilities_by_file_type,
py::arg("type"), "What this library can do with the file type.");

// Each of these is bound for `File` as well as for a path: a caller holding
// bytes (`File.from_memory`) must be able to reach everything a caller
// holding a path can.
m.def(
"list_file_types",
[](const odr::File &file, const odr::Logger &logger) {
return odr::list_file_types(file, logger);
},
py::arg("file"), py::arg("logger") = odr::Logger::null(),
"Determine the possible file types of a file.");
m.def(
"list_file_types",
[](const std::string &path, const odr::Logger &logger) {
return odr::list_file_types(path, logger);
},
py::arg("path"), py::arg("logger") = odr::Logger::null(),
"Determine the possible file types of a file.");
m.def(
"mimetype",
[](const odr::File &file, const odr::Logger &logger) {
return std::string(odr::mimetype(file, logger));
},
py::arg("file"), py::arg("logger") = odr::Logger::null(),
"Determine the MIME type of a file.");
m.def(
"mimetype",
[](const std::string &path, const odr::Logger &logger) {
Expand All @@ -135,6 +152,31 @@ void odr_python::bind_functions(py::module_ &m) {

// Decoding is long-running, so it must not hold the GIL. Re-entry is safe: a
// Python `ILogger` re-acquires it in the trampoline.
m.def(
"open",
[](const odr::File &file, const odr::Logger &logger) {
return odr::open(file, logger);
},
py::arg("file"), py::arg("logger") = odr::Logger::null(),
py::call_guard<py::gil_scoped_release>(), "Decode a file.");
m.def(
"open",
[](const odr::File &file, const odr::FileType as,
const odr::Logger &logger) { return odr::open(file, as, logger); },
py::arg("file"), py::arg("as_type"),
py::arg("logger") = odr::Logger::null(),
py::call_guard<py::gil_scoped_release>(),
"Decode a file as a specific file type.");
m.def(
"open",
[](const odr::File &file, const odr::DecodePreference &preference,
const odr::Logger &logger) {
return odr::open(file, preference, logger);
},
py::arg("file"), py::arg("preference"),
py::arg("logger") = odr::Logger::null(),
py::call_guard<py::gil_scoped_release>(),
"Decode a file with a decode preference.");
m.def(
"open",
[](const std::string &path, const odr::Logger &logger) {
Expand Down
52 changes: 50 additions & 2 deletions python/src/bind_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ void odr_python::bind_file(py::module_ &m) {
py::class_<odr::File>(m, "File")
.def(py::init<>())
.def(py::init<const std::string &>(), py::arg("path"))
.def_static("from_disk", &odr::File::from_disk, py::arg("path"),
"A file read from `path` on disk.")
.def_static(
"from_memory",
[](const py::bytes &data) {
return odr::File::from_memory(std::string(data));
},
py::arg("data"), "A file held in memory; `data` is its bytes.")
.def("__bool__",
[](const odr::File &file) { return file.impl() != nullptr; })
.def("location", &odr::File::location)
Expand All @@ -169,6 +177,13 @@ void odr_python::bind_file(py::module_ &m) {
py::class_<odr::DecodedFile>(m, "DecodedFile")
.def(py::init<const odr::File &, const odr::Logger &>(), py::arg("file"),
py::arg("logger") = odr::Logger::null())
.def(py::init<const odr::File &, odr::FileType, const odr::Logger &>(),
py::arg("file"), py::arg("as_type"),
py::arg("logger") = odr::Logger::null())
.def(py::init<const odr::File &, const odr::DecodePreference &,
const odr::Logger &>(),
py::arg("file"), py::arg("preference"),
py::arg("logger") = odr::Logger::null())
.def(py::init<const std::string &, const odr::Logger &>(),
py::arg("path"), py::arg("logger") = odr::Logger::null())
.def(py::init<const std::string &, odr::FileType, const odr::Logger &>(),
Expand Down Expand Up @@ -214,9 +229,42 @@ void odr_python::bind_file(py::module_ &m) {
.def("archive", &odr::ArchiveFile::archive);

py::class_<odr::DocumentFile, odr::DecodedFile>(m, "DocumentFile")
.def(py::init<const odr::File &, const odr::Logger &>(), py::arg("file"),
py::arg("logger") = odr::Logger::null())
.def(py::init<const std::string &>(), py::arg("path"))
.def_static("type_by_path", &odr::DocumentFile::type, py::arg("path"))
.def_static("meta_by_path", &odr::DocumentFile::meta, py::arg("path"))
.def_static("from_disk", &odr::DocumentFile::from_disk, py::arg("path"),
py::arg("logger") = odr::Logger::null(),
py::call_guard<py::gil_scoped_release>(),
"Decode the document file at `path` on disk.")
.def_static(
"from_memory",
[](const py::bytes &data, const odr::Logger &logger) {
// the bytes have to be copied out under the GIL; only the decode
// that follows is long-running
std::string bytes(data);
const py::gil_scoped_release release;
return odr::DocumentFile::from_memory(std::move(bytes), logger);
},
py::arg("data"), py::arg("logger") = odr::Logger::null(),
"Decode a document file held in memory; `data` is its bytes.")
// `type`/`meta` are overloaded on `File` and path, so the address of
// either is ambiguous; name the signature.
.def_static(
"type_by_file",
py::overload_cast<const odr::File &>(&odr::DocumentFile::type),
py::arg("file"))
.def_static(
"type_by_path",
py::overload_cast<const std::string &>(&odr::DocumentFile::type),
py::arg("path"))
.def_static(
"meta_by_file",
py::overload_cast<const odr::File &>(&odr::DocumentFile::meta),
py::arg("file"))
.def_static(
"meta_by_path",
py::overload_cast<const std::string &>(&odr::DocumentFile::meta),
py::arg("path"))
.def("document_type", &odr::DocumentFile::document_type)
.def("decrypt", &odr::DocumentFile::decrypt, py::arg("password"),
py::call_guard<py::gil_scoped_release>())
Expand Down
102 changes: 102 additions & 0 deletions python/tests/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ def test_file(txt_path):
assert file.read() == txt_path.read_bytes()


def test_file_from_disk(txt_path):
file = pyodr.File.from_disk(str(txt_path))
assert file.location() == pyodr.FileLocation.disk
assert file.disk_path() == str(txt_path)
assert file.read() == txt_path.read_bytes()


def test_file_from_memory(txt_path):
data = txt_path.read_bytes()
file = pyodr.File.from_memory(data)
assert file.location() == pyodr.FileLocation.memory
assert file.disk_path() is None
assert file.size() == len(data)
assert file.read() == data


def test_file_from_memory_keeps_bytes_verbatim():
# not text, and not valid utf-8 - the bytes must survive the round trip
data = bytes(range(256))
assert pyodr.File.from_memory(data).read() == data


def test_open_missing_file(tmp_path):
with pytest.raises(FileNotFoundError):
pyodr.open(str(tmp_path / "missing.txt"))
Expand Down Expand Up @@ -75,3 +97,83 @@ def test_open_zip_archive(odt_path):

mimetype = filesystem.open("/mimetype").read()
assert mimetype == b"application/vnd.oasis.opendocument.text"


def test_open_from_memory(odt_path):
file = pyodr.File.from_memory(odt_path.read_bytes())

assert pyodr.mimetype(file) == "application/vnd.oasis.opendocument.text"
assert pyodr.FileType.opendocument_text in pyodr.list_file_types(file)

decoded = pyodr.open(file)
assert decoded.file_type() == pyodr.FileType.opendocument_text
assert decoded.is_document_file()
# the bytes are the only copy there is, so decoding has to have kept them
document = decoded.as_document_file().document()
assert document.document_type() == pyodr.DocumentType.text


def test_open_from_memory_as_type(odt_path):
file = pyodr.File.from_memory(odt_path.read_bytes())

assert pyodr.open(file, pyodr.FileType.zip).is_archive_file()

preference = pyodr.DecodePreference()
preference.as_file_type = pyodr.FileType.zip
assert pyodr.open(file, preference).is_archive_file()


def test_decoded_file_from_file(odt_path):
file = pyodr.File.from_memory(odt_path.read_bytes())

assert pyodr.DecodedFile(file).file_type() == pyodr.FileType.opendocument_text
assert pyodr.DecodedFile(file, pyodr.FileType.zip).is_archive_file()

preference = pyodr.DecodePreference()
preference.as_file_type = pyodr.FileType.zip
assert pyodr.DecodedFile(file, preference).is_archive_file()


def test_document_file_from_file(odt_path):
file = pyodr.File.from_memory(odt_path.read_bytes())

assert pyodr.DocumentFile.type_by_file(file) == pyodr.FileType.opendocument_text
assert (
pyodr.DocumentFile.meta_by_file(file).type == pyodr.FileType.opendocument_text
)

document_file = pyodr.DocumentFile(file)
assert document_file.document_type() == pyodr.DocumentType.text


def test_document_file_from_disk_and_from_memory(odt_path):
from_disk = pyodr.DocumentFile.from_disk(str(odt_path))
from_memory = pyodr.DocumentFile.from_memory(odt_path.read_bytes())

assert from_disk.file_type() == pyodr.FileType.opendocument_text
assert from_memory.file_type() == from_disk.file_type()
assert from_memory.document_type() == from_disk.document_type()
assert (
from_memory.document().document_type() == from_disk.document().document_type()
)


def test_document_file_from_memory_rejects_a_non_document():
with pytest.raises(pyodr.Error):
pyodr.DocumentFile.from_memory(b"not a document")


def test_file_and_path_entry_points_agree(odt_path):
path = str(odt_path)
file = pyodr.File.from_disk(path)

assert pyodr.mimetype(file) == pyodr.mimetype(path)
assert pyodr.list_file_types(file) == pyodr.list_file_types(path)
assert pyodr.open(file).file_type() == pyodr.open(path).file_type()
assert pyodr.DocumentFile.type_by_file(file) == pyodr.DocumentFile.type_by_path(
path
)
assert (
pyodr.DocumentFile.meta_by_file(file).type
== pyodr.DocumentFile.meta_by_path(path).type
)
64 changes: 51 additions & 13 deletions src/odr/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ deref(const std::shared_ptr<internal::abstract::File> &impl) {

} // namespace

File File::from_disk(const std::string &path) {
return File(std::make_shared<internal::DiskFile>(path));
}

File File::from_memory(std::string data) {
return File(std::make_shared<internal::MemoryFile>(std::move(data)));
}

File::File() = default;

File::File(std::shared_ptr<internal::abstract::File> impl)
Expand Down Expand Up @@ -74,15 +82,23 @@ void File::copy(const std::string &path) const {

std::shared_ptr<internal::abstract::File> File::impl() const { return m_impl; }

std::vector<FileType> DecodedFile::list_file_types(const File &file,
const Logger &logger) {
return internal::open_strategy::list_file_types(file.impl(), logger);
}

std::vector<FileType> DecodedFile::list_file_types(const std::string &path,
const Logger &logger) {
return internal::open_strategy::list_file_types(
std::make_shared<internal::DiskFile>(path), logger);
return list_file_types(File::from_disk(path), logger);
}

std::string_view DecodedFile::mimetype(const File &file, const Logger &logger) {
return internal::magic::mimetype(file.impl(), logger);
}

std::string_view DecodedFile::mimetype(const std::string &path,
const Logger &logger) {
return internal::magic::mimetype(path, logger);
return mimetype(File::from_disk(path), logger);
}

DecodedFile::DecodedFile(std::shared_ptr<internal::abstract::DecodedFile> impl)
Expand All @@ -100,20 +116,22 @@ DecodedFile::DecodedFile(const File &file, const FileType as,
: DecodedFile(internal::open_strategy::open_file(file.impl(), as, logger)) {
}

DecodedFile::DecodedFile(const File &file, const DecodePreference &preference,
const Logger &logger)
: DecodedFile(internal::open_strategy::open_file(file.impl(), preference,
logger)) {}

DecodedFile::DecodedFile(const std::string &path, const Logger &logger)
: DecodedFile(internal::open_strategy::open_file(
std::make_shared<internal::DiskFile>(path), logger)) {}
: DecodedFile(File::from_disk(path), logger) {}

DecodedFile::DecodedFile(const std::string &path, const FileType as,
const Logger &logger)
: DecodedFile(internal::open_strategy::open_file(
std::make_shared<internal::DiskFile>(path), as, logger)) {}
: DecodedFile(File::from_disk(path), as, logger) {}

DecodedFile::DecodedFile(const std::string &path,
const DecodePreference &preference,
const Logger &logger)
: DecodedFile(internal::open_strategy::open_file(
std::make_shared<internal::DiskFile>(path), preference, logger)) {}
: DecodedFile(File::from_disk(path), preference, logger) {}

File DecodedFile::file() const { return File(m_impl->file()); }

Expand Down Expand Up @@ -271,21 +289,41 @@ ArchiveFile::ArchiveFile(std::shared_ptr<internal::abstract::ArchiveFile> impl)

Archive ArchiveFile::archive() const { return Archive(m_impl->archive()); }

DocumentFile DocumentFile::from_disk(const std::string &path,
const Logger &logger) {
return DocumentFile(File::from_disk(path), logger);
}

DocumentFile DocumentFile::from_memory(std::string data, const Logger &logger) {
return DocumentFile(File::from_memory(std::move(data)), logger);
}

FileType DocumentFile::type(const File &file) {
return DocumentFile(file).file_type();
}

FileType DocumentFile::type(const std::string &path) {
return DocumentFile(path).file_type();
return type(File::from_disk(path));
}

FileMeta DocumentFile::meta(const File &file) {
return DocumentFile(file).file_meta();
}

FileMeta DocumentFile::meta(const std::string &path) {
return DocumentFile(path).file_meta();
return meta(File::from_disk(path));
}

DocumentFile::DocumentFile(
std::shared_ptr<internal::abstract::DocumentFile> impl)
: DecodedFile(impl), m_impl{std::move(impl)} {}

DocumentFile::DocumentFile(const File &file, const Logger &logger)
: DocumentFile(
internal::open_strategy::open_document_file(file.impl(), logger)) {}

DocumentFile::DocumentFile(const std::string &path, const Logger &logger)
: DocumentFile(internal::open_strategy::open_document_file(
std::make_shared<internal::DiskFile>(path), logger)) {}
: DocumentFile(File::from_disk(path), logger) {}

DocumentType DocumentFile::document_type() const {
return m_impl->document_type();
Expand Down
Loading
Loading