Skip to content

Commit bf38e01

Browse files
committed
feat(python): formally allow dicts to be passed in lieu of structs
Formalize the contract that it is allowed to pass a dict in places where a struct instance is expected (this provides less type checking guarantees, and the developer is responsible for passing the right keys in). This should address a false-positive issue with the runtime type-checking introduced in 1.63.0 (#3660).
1 parent 7c24e36 commit bf38e01

File tree

4 files changed

+97
-27
lines changed

4 files changed

+97
-27
lines changed

packages/jsii-pacmak/lib/targets/python.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2530,6 +2530,7 @@ class PythonGenerator extends Generator {
25302530
emittedTypes: new Set(),
25312531
resolver,
25322532
submodule: assm.name,
2533+
typeResolver: (fqn) => resolver.dereference(fqn),
25332534
});
25342535
}
25352536

packages/jsii-pacmak/lib/targets/python/type-name.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
PrimitiveTypeReference,
1111
isUnionTypeReference,
1212
Type,
13+
isInterfaceType,
1314
} from '@jsii/spec';
1415
import { toSnakeCase } from 'codemaker';
1516
import { createHash } from 'crypto';
@@ -37,6 +38,9 @@ export interface NamingContext {
3738
/** The assembly in which the PythonType is expressed. */
3839
readonly assembly: Assembly;
3940

41+
/** A resolver to obtain complete information about a type. */
42+
readonly typeResolver: (fqn: string) => Type;
43+
4044
/** The submodule of the assembly in which the PythonType is expressed (could be the module root) */
4145
readonly submodule: string;
4246

@@ -293,14 +297,27 @@ class UserType implements TypeName {
293297
submodule,
294298
surroundingTypeFqns,
295299
typeAnnotation = true,
300+
parameterType,
301+
typeResolver,
296302
}: NamingContext) {
297303
const { assemblyName, packageName, pythonFqn } = toPythonFqn(
298304
this.#fqn,
299305
assembly,
300306
);
307+
308+
// If this is a type annotation for a parameter, allow dicts to be passed where structs are expected.
309+
const type = typeResolver(this.#fqn);
310+
const isStruct = isInterfaceType(type) && !!type.datatype;
311+
const wrapType =
312+
typeAnnotation && parameterType && isStruct
313+
? (pyType: string) =>
314+
`typing.Union[${pyType}, typing.Dict[str, typing.Any]]`
315+
: (pyType: string) => pyType;
316+
301317
if (assemblyName !== assembly.name) {
302318
return {
303-
pythonType: pythonFqn,
319+
// If it's a struct, then we allow passing as a dict, too...
320+
pythonType: wrapType(pythonFqn),
304321
requiredImport: {
305322
sourcePackage: packageName,
306323
item: '',
@@ -330,8 +347,8 @@ class UserType implements TypeName {
330347
) {
331348
// Possibly a forward reference, outputting the stringifierd python FQN
332349
return {
333-
pythonType: JSON.stringify(
334-
pythonFqn.substring(submodulePythonName.length + 1),
350+
pythonType: wrapType(
351+
JSON.stringify(pythonFqn.substring(submodulePythonName.length + 1)),
335352
),
336353
};
337354
}
@@ -345,7 +362,9 @@ class UserType implements TypeName {
345362

346363
// We'll just make a module-qualified reference at this point.
347364
return {
348-
pythonType: pythonFqn.substring(submodulePythonName.length + 1),
365+
pythonType: wrapType(
366+
pythonFqn.substring(submodulePythonName.length + 1),
367+
),
349368
};
350369
}
351370

packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.js.snap

+41-23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/jsii-pacmak/test/targets/python/type-name.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
NamedTypeReference,
55
PrimitiveType,
66
SchemaVersion,
7+
Type,
8+
TypeKind,
79
TypeReference,
810
} from '@jsii/spec';
911

@@ -57,6 +59,11 @@ const assembly: Assembly = {
5759
fqn: `@foo/bar.other.${OTHER_SUBMODULE_TYPE}`,
5860
namespace: 'other',
5961
},
62+
[`@foo/bar.Struct`]: {
63+
datatype: true,
64+
fqn: `@foo/bar.Struct`,
65+
kind: TypeKind.Interface,
66+
},
6067
},
6168
} as any;
6269

@@ -78,6 +85,15 @@ describe(toTypeName, () => {
7885
readonly inSubmodule?: string;
7986
/** The nesting context in which to generate names (if not provided, none) */
8087
readonly inNestingContext?: readonly string[];
88+
/** Additional context keys to register */
89+
readonly context?: Omit<
90+
NamingContext,
91+
| 'assembly'
92+
| 'emittedTypes'
93+
| 'surroundingTypeFqns'
94+
| 'submodule'
95+
| 'typeResolver'
96+
>;
8197
};
8298

8399
const examples: readonly Example[] = [
@@ -221,14 +237,30 @@ describe(toTypeName, () => {
221237
},
222238
inSubmodule: `${assembly.name}.submodule`,
223239
},
240+
// ############################# SPECIAL CASES##############################
241+
{
242+
name: 'Struct parameter type annotation',
243+
input: { fqn: `${assembly.name}.Struct` },
244+
forwardPythonType: `typing.Union["Struct", typing.Dict[str, typing.Any]]`,
245+
pythonType: `typing.Union[Struct, typing.Dict[str, typing.Any]]`,
246+
context: {
247+
typeAnnotation: true,
248+
parameterType: true,
249+
},
250+
},
224251
];
225252

226253
for (const example of examples) {
227254
const context: NamingContext = {
255+
...example.context,
228256
assembly,
229257
emittedTypes: new Set(),
230258
surroundingTypeFqns: example.inNestingContext,
231259
submodule: example.inSubmodule ?? assembly.name,
260+
typeResolver: (fqn) => {
261+
const type = assembly.types?.[fqn];
262+
return type ?? ({ fqn } as any as Type);
263+
},
232264
};
233265
const contextWithEmittedType: NamingContext = {
234266
...context,

0 commit comments

Comments
 (0)