From 2176e1513b6e81fc7643984128fa97b8fa4ab37d Mon Sep 17 00:00:00 2001
From: Thorsten Hater <24411438+thorstenhater@users.noreply.github.com>
Date: Thu, 10 Feb 2022 16:06:30 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=90=8D=20Be=20more=20lenient=20when=20acc?=
 =?UTF-8?q?epting=20args=20to=20file=20I/O=20(#1819)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PyArbor I/O routines now try to cast `filename` arguments to a string.
This allows passing `pathlib.Path` objects.

Example
```py
here = Path(__file__).parent
cat = A.load_catalogue(here / 'local-catalogue.so')
```
---
 python/cable_cell_io.cpp | 38 +++++++++++++++++++++-----------------
 python/mechanism.cpp     |  3 ++-
 python/morphology.cpp    | 16 ++++++++++------
 python/util.hpp          | 27 +++++++++++++++++++++++++++
 4 files changed, 60 insertions(+), 24 deletions(-)
 create mode 100644 python/util.hpp

diff --git a/python/cable_cell_io.cpp b/python/cable_cell_io.cpp
index b5c1272c..2ff3ac8b 100644
--- a/python/cable_cell_io.cpp
+++ b/python/cable_cell_io.cpp
@@ -9,14 +9,18 @@
 #include <arborio/cableio.hpp>
 
 #include "error.hpp"
+#include "util.hpp"
 #include "strprintf.hpp"
 
 namespace pyarb {
 
-arborio::cable_cell_component load_component(const std::string& fname) {
+namespace py = pybind11;
+
+arborio::cable_cell_component load_component(py::object fn) {
+    const auto fname = util::to_path(fn);
     std::ifstream fid{fname};
     if (!fid.good()) {
-        throw pyarb_error("Can't open file '{}'" + fname);
+        throw arb::file_not_found_error(fname);
     }
     auto component = arborio::parse_component(fid);
     if (!component) {
@@ -26,13 +30,13 @@ arborio::cable_cell_component load_component(const std::string& fname) {
 };
 
 template<typename T>
-void write_component(const T& component, const std::string& fname) {
-    std::ofstream fid(fname);
+void write_component(const T& component, py::object fn) {
+    std::ofstream fid(util::to_path(fn));
     arborio::write_component(fid, component, arborio::meta_data{});
 }
 
-void write_component(const arborio::cable_cell_component& component, const std::string& fname) {
-    std::ofstream fid(fname);
+void write_component(const arborio::cable_cell_component& component, py::object fn) {
+    std::ofstream fid(util::to_path(fn));
     arborio::write_component(fid, component);
 }
 
@@ -43,40 +47,40 @@ void register_cable_loader(pybind11::module& m) {
           "Load arbor-component (decor, morphology, label_dict, cable_cell) from file.");
 
     m.def("write_component",
-          [](const arborio::cable_cell_component& d, const std::string& fname) {
-            return write_component(d, fname);
+          [](const arborio::cable_cell_component& d, py::object fn) {
+            return write_component(d, fn);
           },
           pybind11::arg_v("object", "the cable_component object."),
-          pybind11::arg_v("filename", "the name of the file."),
+          pybind11::arg_v("filename", "the path of the file."),
           "Write cable_component to file.");
 
     m.def("write_component",
-          [](const arb::decor& d, const std::string& fname) {
-            return write_component<arb::decor>(d, fname);
+          [](const arb::decor& d, py::object fn) {
+            return write_component<arb::decor>(d, fn);
           },
           pybind11::arg_v("object", "the decor object."),
           pybind11::arg_v("filename", "the name of the file."),
           "Write decor to file.");
 
     m.def("write_component",
-          [](const arb::label_dict& d, const std::string& fname) {
-            return write_component<arb::label_dict>(d, fname);
+          [](const arb::label_dict& d, py::object fn) {
+            return write_component<arb::label_dict>(d, fn);
           },
           pybind11::arg_v("object", "the label_dict object."),
           pybind11::arg_v("filename", "the name of the file."),
           "Write label_dict to file.");
 
     m.def("write_component",
-          [](const arb::morphology& d, const std::string& fname) {
-            return write_component<arb::morphology>(d, fname);
+          [](const arb::morphology& d, py::object fn) {
+            return write_component<arb::morphology>(d, fn);
           },
           pybind11::arg_v("object", "the morphology object."),
           pybind11::arg_v("filename", "the name of the file."),
           "Write morphology to file.");
 
     m.def("write_component",
-          [](const arb::cable_cell& d, const std::string& fname) {
-            return write_component<arb::cable_cell>(d, fname);
+          [](const arb::cable_cell& d, py::object fn) {
+            return write_component<arb::cable_cell>(d, fn);
           },
           pybind11::arg_v("object", "the cable_cell object."),
           pybind11::arg_v("filename", "the name of the file."),
diff --git a/python/mechanism.cpp b/python/mechanism.cpp
index a774cad4..80db1030 100644
--- a/python/mechanism.cpp
+++ b/python/mechanism.cpp
@@ -11,6 +11,7 @@
 
 #include "arbor/mechinfo.hpp"
 
+#include "util.hpp"
 #include "conversion.hpp"
 #include "strprintf.hpp"
 
@@ -197,7 +198,7 @@ void register_mechanisms(pybind11::module& m) {
     m.def("default_catalogue", [](){return arb::global_default_catalogue();});
     m.def("allen_catalogue", [](){return arb::global_allen_catalogue();});
     m.def("bbp_catalogue", [](){return arb::global_bbp_catalogue();});
-    m.def("load_catalogue", [](const std::string& fn){return arb::load_catalogue(fn);});
+    m.def("load_catalogue", [](pybind11::object fn) { return arb::load_catalogue(util::to_string(fn)); });
 
     // arb::mechanism_desc
     // For specifying a mechanism in the cable_cell interface.
diff --git a/python/morphology.cpp b/python/morphology.cpp
index a5d3fb0d..8f4293ca 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -20,6 +20,7 @@
 #include <arborio/neuroml.hpp>
 #endif
 
+#include "util.hpp"
 #include "error.hpp"
 #include "proxy.hpp"
 #include "strprintf.hpp"
@@ -250,10 +251,11 @@ void register_morphology(py::module& m) {
     // Function that creates a morphology from an swc file.
     // Wraps calls to C++ functions arborio::parse_swc() and arborio::load_swc_arbor().
     m.def("load_swc_arbor",
-        [](std::string fname) {
+        [](py::object fn) {
+            const auto fname = util::to_path(fn);
             std::ifstream fid{fname};
             if (!fid.good()) {
-                throw pyarb_error(util::pprintf("can't open file '{}'", fname));
+                throw arb::file_not_found_error(fname);
             }
             try {
                 auto data = arborio::parse_swc(fid);
@@ -275,10 +277,11 @@ void register_morphology(py::module& m) {
         "  are no gaps in the resulting morphology.");
 
     m.def("load_swc_neuron",
-        [](std::string fname) {
+        [](py::object fn) {
+            const auto fname = util::to_path(fn);
             std::ifstream fid{fname};
             if (!fid.good()) {
-                throw pyarb_error(util::pprintf("can't open file '{}'", fname));
+                throw arb::file_not_found_error(fname);
             }
             try {
                 auto data = arborio::parse_swc(fid);
@@ -383,10 +386,11 @@ void register_morphology(py::module& m) {
     neuroml
         // constructors
         .def(py::init(
-            [](std::string fname) {
+            [](py::object fn) {
+                const auto fname = util::to_path(fn);
                 std::ifstream fid{fname};
                 if (!fid.good()) {
-                    throw pyarb_error(util::pprintf("can't open file '{}'", fname));
+                    throw arb::file_not_found_error(fname);
                 }
                 try {
                     std::string string_data((std::istreambuf_iterator<char>(fid)),
diff --git a/python/util.hpp b/python/util.hpp
new file mode 100644
index 00000000..fe1b6b07
--- /dev/null
+++ b/python/util.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <pybind11/pybind11.h>
+
+#include "strprintf.hpp"
+
+namespace pyarb {
+namespace util {
+
+namespace py = pybind11;
+
+inline
+std::string to_path(py::object fn) {
+    if (py::isinstance<py::str>(fn)) {
+        return std::string{py::str(fn)};
+    }
+    else if (py::isinstance(fn,
+                            py::module_::import("pathlib").attr("Path"))) {
+        return std::string{py::str(fn)};
+    }
+    throw std::runtime_error(
+        util::strprintf("Cannot convert objects of type '{}' to a path-like.",
+                        std::string{py::str(fn.get_type())}));
+}
+
+}
+}
-- 
GitLab