Skip to content

Commit 3a8a220

Browse files
Fix: Strings with a single char in Python (#1585)
* Better conversion for string and char type attributes * Err to the side of "string in doubt" Instead of "char in doubt" * Avoid throwing in tryCast
1 parent d5524ec commit 3a8a220

4 files changed

Lines changed: 180 additions & 31 deletions

File tree

include/openPMD/auxiliary/TypeTraits.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@ namespace detail
103103
constexpr static bool value = true;
104104
using type = T;
105105
};
106+
107+
template <typename>
108+
struct IsChar
109+
{
110+
constexpr static bool value = false;
111+
};
112+
template <>
113+
struct IsChar<char>
114+
{
115+
constexpr static bool value = true;
116+
};
117+
template <>
118+
struct IsChar<signed char>
119+
{
120+
constexpr static bool value = true;
121+
};
122+
template <>
123+
struct IsChar<unsigned char>
124+
{
125+
constexpr static bool value = true;
126+
};
106127
} // namespace detail
107128

108129
template <typename T>
@@ -117,6 +138,9 @@ inline constexpr bool IsPointer_v = detail::IsPointer<T>::value;
117138
template <typename T>
118139
using IsPointer_t = typename detail::IsPointer<T>::type;
119140

141+
template <typename C>
142+
inline constexpr bool IsChar_v = detail::IsChar<C>::value;
143+
120144
/** Emulate in the C++ concept ContiguousContainer
121145
*
122146
* Users can implement this trait for a type to signal it can be used as

include/openPMD/backend/Attribute.hpp

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,60 +106,121 @@ class Attribute : public auxiliary::Variant<Datatype, attribute_types>
106106
namespace detail
107107
{
108108
template <typename T, typename U>
109-
auto doConvert(T *pv) -> std::variant<U, std::runtime_error>
109+
auto doConvert(T const *pv) -> std::variant<U, std::runtime_error>
110110
{
111111
(void)pv;
112112
if constexpr (std::is_convertible_v<T, U>)
113113
{
114114
return {static_cast<U>(*pv)};
115115
}
116+
else if constexpr (
117+
std::is_same_v<T, std::string> && auxiliary::IsChar_v<U>)
118+
{
119+
if (pv->size() == 1)
120+
{
121+
return static_cast<U>(pv->at(0));
122+
}
123+
else
124+
{
125+
return {
126+
std::runtime_error("getCast: cast from string to char only "
127+
"possible if string has length 1.")};
128+
}
129+
}
130+
else if constexpr (
131+
auxiliary::IsChar_v<T> && std::is_same_v<U, std::string>)
132+
{
133+
return std::string(1, *pv);
134+
}
116135
else if constexpr (auxiliary::IsVector_v<T> && auxiliary::IsVector_v<U>)
117136
{
137+
U res{};
138+
res.reserve(pv->size());
118139
if constexpr (std::is_convertible_v<
119140
typename T::value_type,
120141
typename U::value_type>)
121142
{
122-
U res{};
123-
res.reserve(pv->size());
124143
std::copy(pv->begin(), pv->end(), std::back_inserter(res));
125144
return {res};
126145
}
127146
else
128147
{
129-
return {
130-
std::runtime_error("getCast: no vector cast possible.")};
148+
// try a dynamic conversion recursively
149+
for (auto const &val : *pv)
150+
{
151+
auto conv = doConvert<
152+
typename T::value_type,
153+
typename U::value_type>(&val);
154+
if (auto conv_val =
155+
std::get_if<typename U::value_type>(&conv);
156+
conv_val)
157+
{
158+
res.push_back(std::move(*conv_val));
159+
}
160+
else
161+
{
162+
auto exception = std::get<std::runtime_error>(conv);
163+
return {std::runtime_error(
164+
std::string(
165+
"getCast: no vector cast possible, recursive "
166+
"error: ") +
167+
exception.what())};
168+
}
169+
}
170+
return {res};
131171
}
132172
}
133173
// conversion cast: array to vector
134174
// if a backend reports a std::array<> for something where
135175
// the frontend expects a vector
136176
else if constexpr (auxiliary::IsArray_v<T> && auxiliary::IsVector_v<U>)
137177
{
178+
U res{};
179+
res.reserve(pv->size());
138180
if constexpr (std::is_convertible_v<
139181
typename T::value_type,
140182
typename U::value_type>)
141183
{
142-
U res{};
143-
res.reserve(pv->size());
144184
std::copy(pv->begin(), pv->end(), std::back_inserter(res));
145185
return {res};
146186
}
147187
else
148188
{
149-
return {std::runtime_error(
150-
"getCast: no array to vector conversion possible.")};
189+
// try a dynamic conversion recursively
190+
for (auto const &val : *pv)
191+
{
192+
auto conv = doConvert<
193+
typename T::value_type,
194+
typename U::value_type>(&val);
195+
if (auto conv_val =
196+
std::get_if<typename U::value_type>(&conv);
197+
conv_val)
198+
{
199+
res.push_back(std::move(*conv_val));
200+
}
201+
else
202+
{
203+
auto exception = std::get<std::runtime_error>(conv);
204+
return {std::runtime_error(
205+
std::string(
206+
"getCast: no array to vector conversion "
207+
"possible, recursive error: ") +
208+
exception.what())};
209+
}
210+
}
211+
return {res};
151212
}
152213
}
153214
// conversion cast: vector to array
154215
// if a backend reports a std::vector<> for something where
155216
// the frontend expects an array
156217
else if constexpr (auxiliary::IsVector_v<T> && auxiliary::IsArray_v<U>)
157218
{
219+
U res{};
158220
if constexpr (std::is_convertible_v<
159221
typename T::value_type,
160222
typename U::value_type>)
161223
{
162-
U res{};
163224
if (res.size() != pv->size())
164225
{
165226
return std::runtime_error(
@@ -175,24 +236,60 @@ namespace detail
175236
}
176237
else
177238
{
178-
return {std::runtime_error(
179-
"getCast: no vector to array conversion possible.")};
239+
// try a dynamic conversion recursively
240+
for (size_t i = 0; i <= res.size(); ++i)
241+
{
242+
auto const &val = (*pv)[i];
243+
auto conv = doConvert<
244+
typename T::value_type,
245+
typename U::value_type>(&val);
246+
if (auto conv_val =
247+
std::get_if<typename U::value_type>(&conv);
248+
conv_val)
249+
{
250+
res[i] = std::move(*conv_val);
251+
}
252+
else
253+
{
254+
auto exception = std::get<std::runtime_error>(conv);
255+
return {std::runtime_error(
256+
std::string(
257+
"getCast: no vector to array conversion "
258+
"possible, recursive error: ") +
259+
exception.what())};
260+
}
261+
}
262+
return {res};
180263
}
181264
}
182265
// conversion cast: turn a single value into a 1-element vector
183266
else if constexpr (auxiliary::IsVector_v<U>)
184267
{
268+
U res{};
269+
res.reserve(1);
185270
if constexpr (std::is_convertible_v<T, typename U::value_type>)
186271
{
187-
U res{};
188-
res.reserve(1);
189272
res.push_back(static_cast<typename U::value_type>(*pv));
190273
return {res};
191274
}
192275
else
193276
{
194-
return {std::runtime_error(
195-
"getCast: no scalar to vector conversion possible.")};
277+
// try a dynamic conversion recursively
278+
auto conv = doConvert<T, typename U::value_type>(pv);
279+
if (auto conv_val = std::get_if<typename U::value_type>(&conv);
280+
conv_val)
281+
{
282+
res.push_back(std::move(*conv_val));
283+
return {res};
284+
}
285+
else
286+
{
287+
auto exception = std::get<std::runtime_error>(conv);
288+
return {std::runtime_error(
289+
std::string("getCast: no scalar to vector conversion "
290+
"possible, recursive error: ") +
291+
exception.what())};
292+
}
196293
}
197294
}
198295
else

src/Mesh.cpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Mesh &Mesh::setDataOrder(Mesh::DataOrder dor)
139139

140140
std::vector<std::string> Mesh::axisLabels() const
141141
{
142-
return getAttribute("axisLabels").get<std::vector<std::string> >();
142+
return getAttribute("axisLabels").get<std::vector<std::string>>();
143143
}
144144

145145
Mesh &Mesh::setAxisLabels(std::vector<std::string> const &als)
@@ -165,7 +165,7 @@ template Mesh &Mesh::setGridSpacing(std::vector<long double> const &gs);
165165

166166
std::vector<double> Mesh::gridGlobalOffset() const
167167
{
168-
return getAttribute("gridGlobalOffset").get<std::vector<double> >();
168+
return getAttribute("gridGlobalOffset").get<std::vector<double>>();
169169
}
170170

171171
Mesh &Mesh::setGridGlobalOffset(std::vector<double> const &ggo)
@@ -331,9 +331,9 @@ void Mesh::read()
331331
aRead.name = "axisLabels";
332332
IOHandler()->enqueue(IOTask(this, aRead));
333333
IOHandler()->flush(internal::defaultFlushParams);
334-
if (*aRead.dtype == DT::VEC_STRING || *aRead.dtype == DT::STRING)
335-
setAxisLabels(
336-
Attribute(*aRead.resource).get<std::vector<std::string> >());
334+
Attribute a = Attribute(*aRead.resource);
335+
if (auto val = a.getOptional<std::vector<std::string>>(); val.has_value())
336+
setAxisLabels(*val);
337337
else
338338
throw error::ReadError(
339339
error::AffectedObject::Attribute,
@@ -346,16 +346,16 @@ void Mesh::read()
346346
aRead.name = "gridSpacing";
347347
IOHandler()->enqueue(IOTask(this, aRead));
348348
IOHandler()->flush(internal::defaultFlushParams);
349-
Attribute a = Attribute(*aRead.resource);
349+
a = Attribute(*aRead.resource);
350350
if (*aRead.dtype == DT::VEC_FLOAT || *aRead.dtype == DT::FLOAT)
351-
setGridSpacing(a.get<std::vector<float> >());
351+
setGridSpacing(a.get<std::vector<float>>());
352352
else if (*aRead.dtype == DT::VEC_DOUBLE || *aRead.dtype == DT::DOUBLE)
353-
setGridSpacing(a.get<std::vector<double> >());
353+
setGridSpacing(a.get<std::vector<double>>());
354354
else if (
355355
*aRead.dtype == DT::VEC_LONG_DOUBLE || *aRead.dtype == DT::LONG_DOUBLE)
356-
setGridSpacing(a.get<std::vector<long double> >());
356+
setGridSpacing(a.get<std::vector<long double>>());
357357
// conversion cast if a backend reports an integer type
358-
else if (auto val = a.getOptional<std::vector<double> >(); val.has_value())
358+
else if (auto val = a.getOptional<std::vector<double>>(); val.has_value())
359359
setGridSpacing(val.value());
360360
else
361361
throw error::ReadError(
@@ -370,7 +370,7 @@ void Mesh::read()
370370
IOHandler()->enqueue(IOTask(this, aRead));
371371
IOHandler()->flush(internal::defaultFlushParams);
372372
if (auto val =
373-
Attribute(*aRead.resource).getOptional<std::vector<double> >();
373+
Attribute(*aRead.resource).getOptional<std::vector<double>>();
374374
val.has_value())
375375
setGridGlobalOffset(val.value());
376376
else

src/binding/python/Attributable.cpp

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include <iterator>
3636
#include <map>
3737
#include <optional>
38+
#include <pybind11/pytypes.h>
3839
#include <string>
3940
#include <type_traits>
4041
#include <vector>
@@ -336,6 +337,16 @@ struct char_to_explicit_char<false>
336337
template <typename TargetType>
337338
std::optional<TargetType> tryCast(py::object const &obj)
338339
{
340+
// Do a check to avoid throwing exceptions
341+
if constexpr (std::is_default_constructible_v<TargetType>)
342+
{
343+
TargetType val{};
344+
auto python_val = py::cast(std::move(val));
345+
if (!py::isinstance(obj, python_val.get_type()))
346+
{
347+
return std::nullopt;
348+
}
349+
}
339350
try
340351
{
341352
return obj.cast<TargetType>();
@@ -358,17 +369,25 @@ bool setAttributeFromObject_char(
358369
std::is_same_v<Char_t, char>,
359370
typename char_to_explicit_char<>::type,
360371
Char_t>;
361-
using ListChar = std::vector<char>;
362372
using ListString = std::vector<std::string>;
363373

374+
/*
375+
* We cannot distinguish strings with length 1 from chars at this place,
376+
* so avoid this cast. If the attribute is actually a char, skipping this
377+
* branch means that it might be upcasted to string.
378+
*/
379+
#if 0
380+
using ListChar = std::vector<char>;
364381
if (auto casted_char = tryCast<char>(obj); casted_char.has_value())
365382
{
366383
return attr.setAttribute<char>(key, *casted_char);
367-
}
384+
} else
385+
#endif
386+
368387
// This must come after tryCast<char>
369388
// because tryCast<string> implicitly covers chars as well.
370-
else if (auto casted_string = tryCast<std::string>(obj);
371-
casted_string.has_value())
389+
if (auto casted_string = tryCast<std::string>(obj);
390+
casted_string.has_value())
372391
{
373392
return attr.setAttribute<std::string>(key, std::move(*casted_string));
374393
}
@@ -386,11 +405,20 @@ bool setAttributeFromObject_char(
386405
// NOW: List casts.
387406
// All list casts must come after all scalar casts,
388407
// because list casts implicitly cover scalars too.
408+
409+
/*
410+
* We cannot distinguish strings with length 1 from chars at this place,
411+
* so avoid this cast. If the attribute is actually a vector of char,
412+
* skipping this branch means that it might be upcasted to a vector of
413+
* string.
414+
*/
415+
#if 0
389416
else if (auto list_of_char = tryCast<ListChar>(obj);
390417
list_of_char.has_value())
391418
{
392419
return attr.setAttribute<ListChar>(key, std::move(*list_of_char));
393420
}
421+
#endif
394422
// this must come after tryCast<vector<char>>,
395423
// because tryCast<vector<string>> implicitly covers chars as well
396424
else if (auto list_of_string = tryCast<ListString>(obj);

0 commit comments

Comments
 (0)