Skip to content

Commit 3d8d09e

Browse files
committed
Add fast C++ path for csp.Struct.from_dict and take into account the cases which may not be supported
Signed-off-by: Aadya Chinubhai <aadyachinubhai@gmail.com>
1 parent 42cc26e commit 3d8d09e

5 files changed

Lines changed: 159 additions & 2 deletions

File tree

cpp/csp/python/CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ add_library(csptypesimpl
1717
PyCspType.cpp
1818
PyStruct.cpp
1919
PyStructToJson.cpp
20-
PyStructToDict.cpp)
20+
PyStructToDict.cpp
21+
PyStructFromDict.cpp)
2122

2223
set_target_properties(csptypesimpl PROPERTIES PUBLIC_HEADER "${CSPTYPESIMPL_PUBLIC_HEADERS}")
2324
target_compile_definitions(csptypesimpl PUBLIC RAPIDJSON_HAS_STDSTRING=1)
@@ -48,7 +49,8 @@ set(CSPIMPL_PUBLIC_HEADERS
4849
PyOutputProxy.h
4950
PyConstants.h
5051
PyStructToJson.h
51-
PyStructToDict.h)
52+
PyStructToDict.h
53+
PyStructFromDict.h)
5254

5355
add_library(cspimpl SHARED
5456
cspimpl.cpp

cpp/csp/python/PyStruct.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <csp/python/PyStructList_impl.h>
88
#include <csp/python/PyStructToJson.h>
99
#include <csp/python/PyStructToDict.h>
10+
#include <csp/python/PyStructFromDict.h>
1011
#include <unordered_set>
1112
#include <type_traits>
1213

@@ -1052,6 +1053,20 @@ PyObject * PyStruct_to_dict( PyStruct * self, PyObject * args, PyObject * kwargs
10521053
CSP_RETURN_NULL;
10531054
}
10541055

1056+
PyObject * PyStruct_from_dict( PyStructMeta * cls, PyObject * args, PyObject * kwargs) {
1057+
CSP_BEGIN_METHOD;
1058+
1059+
PyObject* dict = NULL;
1060+
1061+
if (!PyArg_ParseTuple(args, "O:from_dict", &dict)) {
1062+
return NULL;
1063+
}
1064+
auto& struct_meta = cls->structMeta;
1065+
return toPython(structFromDict(struct_meta, dict));
1066+
1067+
CSP_RETURN_NULL;
1068+
}
1069+
10551070
PyObject * PyStruct_to_json( PyStruct * self, PyObject * args, PyObject * kwargs )
10561071
{
10571072
CSP_BEGIN_METHOD;
@@ -1087,6 +1102,7 @@ static PyMethodDef PyStruct_methods[] = {
10871102
{ "all_fields_set", (PyCFunction) PyStruct_all_fields_set, METH_NOARGS, "return true if all fields on the struct are set" },
10881103
{ "to_dict", (PyCFunction) PyStruct_to_dict, METH_VARARGS | METH_KEYWORDS, "return a python dict of the struct by recursively converting struct members into python dicts" },
10891104
{ "to_json", (PyCFunction) PyStruct_to_json, METH_VARARGS | METH_KEYWORDS, "return a json string of the struct by recursively converting struct members into json format" },
1105+
{ "from_dict", (PyCFunction) PyStruct_from_dict, METH_VARARGS | METH_KEYWORDS | METH_CLASS, "return a struct by recursively reading the values from a python dictionary"},
10901106
{ NULL}
10911107
};
10921108

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#include <csp/python/PyStructFromDict.h>
2+
3+
namespace csp::python{
4+
5+
StructPtr structFromDict (const StructMetaPtr& struct_meta, PyObject* dict) {
6+
PyObject* py_key;
7+
PyObject* py_value;
8+
Py_ssize_t ppos = 0;
9+
10+
if (!PyDict_Check(dict)) {
11+
CSP_THROW(TypeError, "Wrong type used in `from_dict`, expected a dict.");
12+
}
13+
14+
StructPtr s = struct_meta->create();
15+
while( PyDict_Next( dict, &ppos, &py_key, &py_value ) )
16+
{
17+
if( !PyUnicode_Check( py_key ) )
18+
CSP_THROW( KeyError, "Unexpected key " << PyObjectPtr::incref( py_key )
19+
<< " for type " << struct_meta -> name() );
20+
21+
auto & field = struct_meta -> field( PyUnicode_AsUTF8( py_key ) );
22+
if( !field )
23+
CSP_THROW( KeyError, "Unexpected key " << PyObjectPtr::incref( py_key )
24+
<< " for type " << struct_meta -> name() );
25+
26+
switchCspType(field->type(), [&] (auto tag)
27+
{
28+
using CType = typename decltype( tag )::type;
29+
auto * typedField = static_cast<const typename StructField::upcast<CType>::type *>( field.get() );
30+
31+
//optional fields accept None, same as PyStruct_setattrs
32+
if( typedField -> isOptional() && py_value == Py_None )
33+
{
34+
typedField -> setNone( s.get() );
35+
typedField -> clearValue( s.get() );
36+
return;
37+
}
38+
39+
if constexpr (std::is_same_v<CType, StructPtr>)
40+
{
41+
auto& nestedMeta = static_cast<const CspStructType &>(* field->type()).meta();
42+
typedField -> setValue(s.get(), structFromDict(nestedMeta, py_value));
43+
}
44+
else if constexpr( std::is_same_v<CType, CspEnum> )
45+
{
46+
//enums arrive as name strings unless to_dict was called with preserve_enums
47+
auto & enumMeta = static_cast<const CspEnumType &>( *field -> type() ).meta();
48+
if( PyUnicode_Check( py_value ) )
49+
typedField -> setValue( s.get(), enumMeta -> fromString( PyUnicode_AsUTF8( py_value ) ) );
50+
else
51+
typedField -> setValue( s.get(), fromPython<CType>( py_value, *field -> type() ) );
52+
}
53+
else if constexpr( std::is_same_v<CType, std::vector<CspEnum>> )
54+
{
55+
//convert element-wise, entries may be names or enum objects
56+
auto & elemType = static_cast<const CspArrayType &>( *field -> type() ).elemType();
57+
auto & enumMeta = static_cast<const CspEnumType &>( *elemType ).meta();
58+
59+
PyObjectPtr seq = PyObjectPtr::own( PySequence_Fast( py_value, "expected a sequence of enum values" ) );
60+
if( !seq.ptr() )
61+
CSP_THROW( PythonPassthrough, "" );
62+
63+
std::vector<CspEnum> out;
64+
Py_ssize_t n = PySequence_Fast_GET_SIZE( seq.ptr() );
65+
out.reserve( n );
66+
for( Py_ssize_t i = 0; i < n; ++i )
67+
{
68+
PyObject * elem = PySequence_Fast_GET_ITEM( seq.ptr(), i );
69+
out.push_back( PyUnicode_Check( elem ) ? enumMeta -> fromString( PyUnicode_AsUTF8( elem ) )
70+
: fromPython<CspEnum>( elem, *elemType ) );
71+
}
72+
typedField -> setValue( s.get(), out );
73+
}
74+
else
75+
{
76+
typedField -> setValue(s.get(), fromPython<CType>(py_value, *field->type()));
77+
}
78+
});
79+
80+
}
81+
82+
//we bypass __init__ so run the strict-struct check ourselves
83+
if( !s -> validate() ) [[unlikely]]
84+
CSP_THROW( ValueError, "Struct " << struct_meta -> name() << " is not valid; required fields "
85+
<< s -> formatAllUnsetStrictFields() << " were not set on init" );
86+
87+
return s;
88+
}
89+
90+
}

cpp/csp/python/PyStructFromDict.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pragma once
2+
3+
#include <csp/python/Conversions.h>
4+
5+
namespace csp::python
6+
{
7+
8+
// Build a csp struct from a python dictionary.
9+
StructPtr structFromDict( const StructMetaPtr& struct_meta, PyObject* dict);
10+
11+
}

csp/impl/struct.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
import typing
33
from copy import deepcopy
4+
from datetime import date, datetime, time, timedelta
45

56
import ruamel.yaml
67
from deprecated import deprecated
@@ -13,6 +14,27 @@
1314
# Avoid recreating this object every call its expensive!
1415
g_YAML = ruamel.yaml.YAML()
1516

17+
# field annotations the C++ from_dict converts natively
18+
_CPP_FROMDICT_SCALARS = (int, float, bool, str, bytes, datetime, date, time, timedelta, object)
19+
20+
21+
def _cpp_fromdict_type_ok(typ, seen):
22+
if CspTypingUtils.is_union_type(typ):
23+
return True
24+
if CspTypingUtils.is_generic_container(typ):
25+
origin = CspTypingUtils.get_origin(typ)
26+
if origin in (csp.typing.NumpyNDArray, csp.typing.Numpy1DArray):
27+
return True
28+
if origin in (typing.List, typing.Set, typing.Tuple, FastList):
29+
elem = typ.__args__[0]
30+
return isinstance(elem, type) and not issubclass(elem, Struct) and _cpp_fromdict_type_ok(elem, seen)
31+
return False # Dict[K,V] etc: type parameters are erased before C++
32+
if isinstance(typ, type):
33+
if issubclass(typ, Struct):
34+
return typ._cpp_from_dict_ok(seen)
35+
return issubclass(typ, csp.Enum) or typ in _CPP_FROMDICT_SCALARS
36+
return False # Literal, ForwardRef, etc
37+
1638

1739
class StructMeta(_csptypesimpl.PyStructMeta):
1840
def __new__(cls, name, bases, dct, strict=False):
@@ -275,10 +297,26 @@ def _obj_from_python(cls, json, obj_type):
275297
else:
276298
return obj_type(json)
277299

300+
@classmethod
301+
def _cpp_from_dict_ok(cls, _seen=None):
302+
"""True if every field (recursively) is convertible by the C++ from_dict, cached per class"""
303+
ok = cls.__dict__.get("__cpp_fromdict_ok__")
304+
if ok is None:
305+
_seen = _seen if _seen is not None else set()
306+
if cls in _seen:
307+
return True
308+
_seen.add(cls)
309+
cls.__cpp_fromdict_ok__ = ok = all(
310+
_cpp_fromdict_type_ok(t, _seen) for t in cls.__full_metadata_typed__.values()
311+
)
312+
return ok
313+
278314
@classmethod
279315
def from_dict(cls, json: dict, use_pydantic: bool = False):
280316
if use_pydantic:
281317
return cls.type_adapter().validate_python(json)
318+
if cls._cpp_from_dict_ok():
319+
return super().from_dict(json)
282320
return cls._obj_from_python(json, cls)
283321

284322
def to_dict_depr(self):

0 commit comments

Comments
 (0)