1 /**
2    Functions to wrap entities in D modules for Python consumption.
3 
4    These functions are usually not called directly, but from the mixin generated by
5    autowrap.python.boilerplate.pydBoilerplate.
6  */
7 module autowrap.python.wrap;
8 
9 import autowrap.reflection: isUserAggregate, isModule;
10 import std.meta: allSatisfy;
11 import std.traits: isArray;
12 
13 
14 private alias I(alias T) = T;
15 private enum isString(alias T) = is(typeof(T) == string);
16 
17 ///  Wrap global functions from multiple modules
18 void wrapAllFunctions(Modules...)() if(allSatisfy!(isModule, Modules)) {
19     import autowrap.reflection: AllFunctions;
20     import pyd.pyd: def, PyName;
21 
22     static foreach(function_; AllFunctions!Modules)
23         def!(function_.symbol, PyName!(toSnakeCase(function_.name)))();
24 }
25 
26 
27 /// Converts an identifier from camelCase or PascalCase to snake_case.
28 string toSnakeCase(in string str) @safe pure {
29 
30     import std.algorithm: all, map;
31     import std.ascii: isUpper;
32 
33     if(str.all!isUpper) return str;
34 
35     string ret;
36 
37     string convert(in size_t index, in char c) {
38         import std.ascii: isLower, toLower;
39 
40         const prefix = index == 0 ? "" : "_";
41         const isHump =
42             (index == 0 && c.isUpper) ||
43             (index > 0 && c.isUpper && str[index - 1].isLower);
44 
45         return isHump ? prefix ~ c.toLower : "" ~ c;
46     }
47 
48     foreach(i, c; str) {
49         ret ~= convert(i, c);
50     }
51 
52     return ret;
53 }
54 
55 
56 @("toSnakeCase empty")
57 @safe pure unittest {
58     static assert("".toSnakeCase == "");
59 }
60 
61 @("toSnakeCase no caps")
62 @safe pure unittest {
63     static assert("foo".toSnakeCase == "foo");
64 }
65 
66 @("toSnakeCase camelCase")
67 @safe pure unittest {
68     static assert("toSnakeCase".toSnakeCase == "to_snake_case");
69 }
70 
71 @("toSnakeCase PascalCase")
72 @safe pure unittest {
73     static assert("PascalCase".toSnakeCase == "pascal_case");
74 }
75 
76 @("toSnakeCase ALLCAPS")
77 @safe pure unittest {
78     static assert("ALLCAPS".toSnakeCase == "ALLCAPS");
79 }
80 
81 
82 /**
83    wrap all aggregates found in the given modules, specified by their name
84    (to avoid importing all of them first).
85 
86    This function wraps all struct and class definitions, and also all struct and class
87    types that are parameters or return types of any functions found.
88  */
89 void wrapAllAggregates(Modules...)() if(allSatisfy!(isModule, Modules)) {
90 
91     import autowrap.reflection: AllAggregates, Module;
92     import std.meta: staticMap;
93     import std.traits: fullyQualifiedName;
94 
95     static foreach(aggregate; AllAggregates!Modules) {
96         static if(__traits(compiles, wrapAggregate!aggregate))
97             wrapAggregate!aggregate;
98         else {
99             pragma(msg, "\nERROR! Autowrap could not wrap aggregate `", fullyQualifiedName!aggregate, "` for Python\n");
100             //wrapAggregate!aggregate; // uncomment to see the error messages from the compiler
101         }
102     }
103 }
104 
105 /**
106    Wrap aggregate of type T.
107  */
108 auto wrapAggregate(T)() if(isUserAggregate!T) {
109 
110     import autowrap.reflection: Symbol;
111     import autowrap.python.pyd.class_wrap: MemberFunction;
112     import pyd.pyd: wrap_class, Member, Init;
113     import std.meta: staticMap, Filter, AliasSeq;
114     import std.traits: Parameters, FieldNameTuple, hasMember;
115     import std.typecons: Tuple;
116 
117     alias AggMember(string memberName) = Symbol!(T, memberName);
118     alias members = staticMap!(AggMember, __traits(allMembers, T));
119 
120     alias memberFunctions = Filter!(isMemberFunction, members);
121 
122     static if(hasMember!(T, "__ctor"))
123         alias constructors = AliasSeq!(__traits(getOverloads, T, "__ctor"));
124     else
125         alias constructors = AliasSeq!();
126 
127     // If we staticMap with std.traits.Parameters, we end up with a collapsed tuple
128     // i.e. with one constructor that takes int and another that takes int, string,
129     // we'd end up with 3 elements (int, int, string) instead of 2 ((int), (int, string))
130     // so we package them up in a std.typecons.Tuple to avoid flattening
131     // each being an AliasSeq of types for the constructor
132     alias ParametersTuple(alias F) = Tuple!(Parameters!F);
133 
134     // A tuple, with as many elements as constructors. Each element is a
135     // std.typecons.Tuple of the constructor parameter types.
136     alias constructorParamTuples = staticMap!(ParametersTuple, constructors);
137 
138     // Apply pyd's Init to the unpacked types of the parameter Tuple.
139     alias InitTuple(alias Tuple) = Init!(Tuple.Types);
140 
141     enum isPublic(string fieldName) = __traits(getProtection, __traits(getMember, T, fieldName)) == "public";
142     alias publicFields = Filter!(isPublic, FieldNameTuple!T);
143 
144     wrap_class!(
145         T,
146         staticMap!(Member, publicFields),
147         staticMap!(MemberFunction, memberFunctions),
148         staticMap!(InitTuple, constructorParamTuples),
149    );
150 }
151 
152 
153 // must be a global template
154 private template isMemberFunction(A...) if(A.length == 1) {
155     alias T = A[0];
156     static if(__traits(compiles, __traits(identifier, T)))
157         enum isMemberFunction = isPublicFunction!T && __traits(identifier, T) != "__ctor";
158     else
159         enum isMemberFunction = false;
160 }
161 
162 
163 private template isPublicFunction(alias F) {
164     import std.traits: isFunction;
165     enum prot = __traits(getProtection, F);
166     enum isPublicFunction = isFunction!F && (prot == "export" || prot == "public");
167 }