UNPKG

jsii-pacmak

Version:

A code generation framework for jsii backend languages

1,170 lines (1,169 loc) 95 kB
"use strict"; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _TypeCheckingHelper_stubs, _a, _TypeCheckingStub_PREFIX, _TypeCheckingStub_arguments, _TypeCheckingStub_hash; Object.defineProperty(exports, "__esModule", { value: true }); const spec = require("@jsii/spec"); const assert = require("assert"); const codemaker_1 = require("codemaker"); const crypto = require("crypto"); const escapeStringRegexp = require("escape-string-regexp"); const fs = require("fs-extra"); const jsii_rosetta_1 = require("jsii-rosetta"); const path = require("path"); const generator_1 = require("../generator"); const logging_1 = require("../logging"); const markdown_1 = require("../markdown"); const target_1 = require("../target"); const util_1 = require("../util"); const version_1 = require("../version"); const _utils_1 = require("./_utils"); const type_name_1 = require("./python/type-name"); const util_2 = require("./python/util"); const version_utils_1 = require("./version-utils"); const index_1 = require("./index"); // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports const spdxLicenseList = require('spdx-license-list'); const requirementsFile = path.resolve(__dirname, 'python', 'requirements-dev.txt'); // we use single-quotes for multi-line strings to allow examples within the // docstrings themselves to include double-quotes (see https://github.com/aws/jsii/issues/2569) const DOCSTRING_QUOTES = "'''"; const RAW_DOCSTRING_QUOTES = `r${DOCSTRING_QUOTES}`; class Python extends target_1.Target { constructor(options) { super(options); this.generator = new PythonGenerator(options.rosetta, options); } async generateCode(outDir, tarball) { await super.generateCode(outDir, tarball); } async build(sourceDir, outDir) { // Create a fresh virtual env const venv = await fs.mkdtemp(path.join(sourceDir, '.env-')); const venvBin = path.join(venv, process.platform === 'win32' ? 'Scripts' : 'bin'); // On Windows, there is usually no python3.exe (the GitHub action workers will have a python3 // shim, but using this actually results in a WinError with Python 3.7 and 3.8 where venv will // fail to copy the python binary if it's not invoked as python.exe). More on this particular // issue can be read here: https://bugs.python.org/issue43749 await (0, util_1.shell)(process.platform === 'win32' ? 'python' : 'python3', [ '-m', 'venv', '--system-site-packages', venv, ]); const env = { ...process.env, PATH: `${venvBin}:${process.env.PATH}`, VIRTUAL_ENV: venv, }; const python = path.join(venvBin, 'python'); // Install the necessary things await (0, util_1.shell)(python, ['-m', 'pip', 'install', '--no-input', '-r', requirementsFile], { cwd: sourceDir, env, retry: { maxAttempts: 5 }, }); // Actually package up our code, both as a sdist and a wheel for publishing. await (0, util_1.shell)(python, ['setup.py', 'sdist', '--dist-dir', outDir], { cwd: sourceDir, env, }); await (0, util_1.shell)(python, ['-m', 'pip', 'wheel', '--no-deps', '--wheel-dir', outDir, sourceDir], { cwd: sourceDir, env, retry: { maxAttempts: 5 }, }); await (0, util_1.shell)(python, ['-m', 'twine', 'check', path.join(outDir, '*')], { cwd: sourceDir, env, }); } } exports.default = Python; class TypeCheckingHelper { constructor() { _TypeCheckingHelper_stubs.set(this, new Array()); } getTypeHints(fqn, args) { const stub = new TypeCheckingStub(fqn, args); __classPrivateFieldGet(this, _TypeCheckingHelper_stubs, "f").push(stub); return `typing.get_type_hints(${stub.name})`; } /** Emits instructions that create the annotations data... */ flushStubs(code) { for (const stub of __classPrivateFieldGet(this, _TypeCheckingHelper_stubs, "f")) { stub.emit(code); } // Reset the stubs list __classPrivateFieldSet(this, _TypeCheckingHelper_stubs, [], "f"); } } _TypeCheckingHelper_stubs = new WeakMap(); class TypeCheckingStub { constructor(fqn, args) { _TypeCheckingStub_arguments.set(this, void 0); _TypeCheckingStub_hash.set(this, void 0); // Removing the quoted type names -- this will be emitted at the very end of the module. __classPrivateFieldSet(this, _TypeCheckingStub_arguments, args.map((arg) => arg.replace(/"/g, '')), "f"); __classPrivateFieldSet(this, _TypeCheckingStub_hash, crypto .createHash('sha256') .update(__classPrivateFieldGet(TypeCheckingStub, _a, "f", _TypeCheckingStub_PREFIX)) .update(fqn) .digest('hex'), "f"); } get name() { return `${__classPrivateFieldGet(TypeCheckingStub, _a, "f", _TypeCheckingStub_PREFIX)}${__classPrivateFieldGet(this, _TypeCheckingStub_hash, "f")}`; } emit(code) { code.line(); openSignature(code, 'def', this.name, __classPrivateFieldGet(this, _TypeCheckingStub_arguments, "f"), 'None'); code.line(`"""Type checking stubs"""`); code.line('pass'); code.closeBlock(); } } _a = TypeCheckingStub, _TypeCheckingStub_arguments = new WeakMap(), _TypeCheckingStub_hash = new WeakMap(); _TypeCheckingStub_PREFIX = { value: '_typecheckingstub__' }; const pythonModuleNameToFilename = (name) => { return path.join(...name.split('.')); }; const toPythonMethodName = (name, protectedItem = false) => { let value = (0, util_2.toPythonIdentifier)((0, codemaker_1.toSnakeCase)(name)); if (protectedItem) { value = `_${value}`; } return value; }; const toPythonPropertyName = (name, constant = false, protectedItem = false) => { let value = (0, util_2.toPythonIdentifier)((0, codemaker_1.toSnakeCase)(name)); if (constant) { value = value.toUpperCase(); } if (protectedItem) { value = `_${value}`; } return value; }; /** * Converts a given signature's parameter name to what should be emitted in Python. It slugifies the * positional parameter names that collide with a lifted prop by appending trailing `_`. There is no * risk of conflicting with an other positional parameter that ends with a `_` character because * this is prohibited by the `jsii` compiler (parameter names MUST be camelCase, and only a single * `_` is permitted when it is on **leading** position) * * @param name the name of the parameter that needs conversion. * @param liftedParamNames the list of "lifted" keyword parameters in this signature. This must be * omitted when generating a name for a parameter that **is** lifted. */ function toPythonParameterName(name, liftedParamNames = new Set()) { let result = (0, util_2.toPythonIdentifier)((0, codemaker_1.toSnakeCase)(name)); while (liftedParamNames.has(result)) { result += '_'; } return result; } const setDifference = (setA, setB) => { const result = new Set(); for (const item of setA) { if (!setB.has(item)) { result.add(item); } } return result; }; /** * Prepare python members for emission. * * If there are multiple members of the same name, they will all map to the same python * name, so we will filter all deprecated members and expect that there will be only one * left. * * Returns the members in a sorted list. */ function prepareMembers(members, resolver) { // create a map from python name to list of members const map = {}; for (const m of members) { let list = map[m.pythonName]; if (!list) { list = map[m.pythonName] = []; } list.push(m); } // now return all the members const ret = new Array(); for (const [name, list] of Object.entries(map)) { let member; if (list.length === 1) { // if we have a single member for this normalized name, then use it member = list[0]; } else { // we found more than one member with the same python name, filter all // deprecated versions and check that we are left with exactly one. // otherwise, they will overwrite each other // see https://github.com/aws/jsii/issues/2508 const nonDeprecated = list.filter((x) => !isDeprecated(x)); if (nonDeprecated.length > 1) { throw new Error(`Multiple non-deprecated members which map to the Python name "${name}"`); } if (nonDeprecated.length === 0) { throw new Error(`Multiple members which map to the Python name "${name}", but all of them are deprecated`); } member = nonDeprecated[0]; } ret.push(member); } return sortMembers(ret, resolver); } const sortMembers = (members, resolver) => { let sortable = new Array(); const sorted = new Array(); const seen = new Set(); // The first thing we want to do, is push any item which is not sortable to the very // front of the list. This will be things like methods, properties, etc. for (const member of members) { if (!isSortableType(member)) { sorted.push(member); seen.add(member); } else { sortable.push({ member, dependsOn: new Set(member.dependsOn(resolver)) }); } } // Now that we've pulled out everything that couldn't possibly have dependencies, // we will go through the remaining items, and pull off any items which have no // dependencies that we haven't already sorted. while (sortable.length > 0) { for (const { member, dependsOn } of sortable) { const diff = setDifference(dependsOn, seen); if ([...diff].find((dep) => !(dep instanceof PythonModule)) == null) { sorted.push(member); seen.add(member); } } const leftover = sortable.filter(({ member }) => !seen.has(member)); if (leftover.length === sortable.length) { throw new Error(`Could not sort members (circular dependency?). Leftover: ${leftover .map((lo) => lo.member.pythonName) .join(', ')}`); } else { sortable = leftover; } } return sorted; }; function isSortableType(arg) { return arg.dependsOn !== undefined; } class BasePythonClassType { constructor(generator, pythonName, spec, fqn, opts, docs) { this.generator = generator; this.pythonName = pythonName; this.spec = spec; this.fqn = fqn; this.docs = docs; this.separateMembers = true; const { bases = [] } = opts; this.bases = bases; this.members = []; } dependsOn(resolver) { const dependencies = new Array(); const parent = resolver.getParent(this.fqn); // We need to return any bases that are in the same module at the same level of // nesting. const seen = new Set(); for (const base of this.bases) { if (spec.isNamedTypeReference(base)) { if (resolver.isInModule(base)) { // Given a base, we need to locate the base's parent that is the same as // our parent, because we only care about dependencies that are at the // same level of our own. // TODO: We might need to recurse into our members to also find their // dependencies. let baseItem = resolver.getType(base); let baseParent = resolver.getParent(base); while (baseParent !== parent) { baseItem = baseParent; baseParent = resolver.getParent(baseItem.fqn); } if (!seen.has(baseItem.fqn)) { dependencies.push(baseItem); seen.add(baseItem.fqn); } } } } return dependencies; } requiredImports(context) { return (0, type_name_1.mergePythonImports)(...this.bases.map((base) => (0, type_name_1.toTypeName)(base).requiredImports(context)), ...this.members.map((mem) => mem.requiredImports(context))); } addMember(member) { this.members.push(member); } get apiLocation() { if (!this.fqn) { throw new Error(`Cannot make apiLocation for ${this.pythonName}, does not have FQN`); } return { api: 'type', fqn: this.fqn }; } emit(code, context) { context = nestedContext(context, this.fqn); const classParams = this.getClassParams(context); openSignature(code, 'class', this.pythonName, classParams); this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `class-${this.pythonName}`, trailingNewLine: true, }); if (this.members.length > 0) { const resolver = this.boundResolver(context.resolver); let shouldSeparate = false; for (const member of prepareMembers(this.members, resolver)) { if (shouldSeparate) { code.line(); } shouldSeparate = this.separateMembers; member.emit(code, { ...context, resolver }); } } else { code.line('pass'); } code.closeBlock(); if (this.fqn != null) { context.emittedTypes.add(this.fqn); } } boundResolver(resolver) { if (this.fqn == null) { return resolver; } return resolver.bind(this.fqn); } } class BaseMethod { constructor(generator, pythonName, jsName, parameters, returns, docs, isStatic, pythonParent, opts) { this.generator = generator; this.pythonName = pythonName; this.jsName = jsName; this.parameters = parameters; this.returns = returns; this.docs = docs; this.isStatic = isStatic; this.pythonParent = pythonParent; this.classAsFirstParameter = false; this.returnFromJSIIMethod = true; this.shouldEmitBody = true; this.abstract = !!opts.abstract; this.liftedProp = opts.liftedProp; this.parent = opts.parent; } get apiLocation() { return { api: 'member', fqn: this.parent.fqn, memberName: this.jsName ?? '', }; } requiredImports(context) { return (0, type_name_1.mergePythonImports)((0, type_name_1.toTypeName)(this.returns).requiredImports(context), ...this.parameters.map((param) => (0, type_name_1.toTypeName)(param).requiredImports(context)), ...liftedProperties(this.liftedProp)); function* liftedProperties(struct) { if (struct == null) { return; } for (const prop of struct.properties ?? []) { yield (0, type_name_1.toTypeName)(prop.type).requiredImports(context); } for (const base of struct.interfaces ?? []) { const iface = context.resolver.dereference(base); for (const imports of liftedProperties(iface)) { yield imports; } } } } emit(code, context, opts) { const { renderAbstract = true, forceEmitBody = false } = opts ?? {}; const returnType = (0, type_name_1.toTypeName)(this.returns).pythonType(context); // We cannot (currently?) blindly use the names given to us by the JSII for // initializers, because our keyword lifting will allow two names to clash. // This can hopefully be removed once we get https://github.com/aws/jsii/issues/288 // resolved, so build up a list of all of the prop names so we can check against // them later. const liftedPropNames = new Set(); if (this.liftedProp?.properties != null) { for (const prop of this.liftedProp.properties) { liftedPropNames.add(toPythonParameterName(prop.name)); } } // We need to turn a list of JSII parameters, into Python style arguments with // gradual typing, so we'll have to iterate over the list of parameters, and // build the list, converting as we go. const pythonParams = []; for (const param of this.parameters) { // We cannot (currently?) blindly use the names given to us by the JSII for // initializers, because our keyword lifting will allow two names to clash. // This can hopefully be removed once we get https://github.com/aws/jsii/issues/288 // resolved. const paramName = toPythonParameterName(param.name, liftedPropNames); const paramType = (0, type_name_1.toTypeName)(param).pythonType({ ...context, parameterType: true, }); const paramDefault = param.optional ? ' = None' : ''; pythonParams.push(`${paramName}: ${paramType}${paramDefault}`); } const documentableArgs = this.parameters .map((p) => ({ name: p.name, docs: p.docs, definingType: this.parent, })) // If there's liftedProps, the last argument is the struct and it won't be _actually_ emitted. .filter((_, index) => this.liftedProp != null ? index < this.parameters.length - 1 : true) .map((param) => ({ ...param, name: toPythonParameterName(param.name, liftedPropNames), })); // If we have a lifted parameter, then we'll drop the last argument to our params // and then we'll lift all of the params of the lifted type as keyword arguments // to the function. if (this.liftedProp !== undefined) { // Remove our last item. pythonParams.pop(); const liftedProperties = this.getLiftedProperties(context.resolver); if (liftedProperties.length >= 1) { // All of these parameters are keyword only arguments, so we'll mark them // as such. pythonParams.push('*'); // Iterate over all of our props, and reflect them into our params. for (const prop of liftedProperties) { const paramName = toPythonParameterName(prop.prop.name); const paramType = (0, type_name_1.toTypeName)(prop.prop).pythonType({ ...context, parameterType: true, typeAnnotation: true, }); const paramDefault = prop.prop.optional ? ' = None' : ''; pythonParams.push(`${paramName}: ${paramType}${paramDefault}`); } } // Document them as keyword arguments documentableArgs.push(...liftedProperties.map((p) => ({ name: p.prop.name, docs: p.prop.docs, definingType: p.definingType, }))); } else if (this.parameters.length >= 1 && this.parameters[this.parameters.length - 1].variadic) { // Another situation we could be in, is that instead of having a plain parameter // we have a variadic parameter where we need to expand the last parameter as a // *args. pythonParams.pop(); const lastParameter = this.parameters.slice(-1)[0]; const paramName = toPythonParameterName(lastParameter.name); const paramType = (0, type_name_1.toTypeName)(lastParameter.type).pythonType(context); pythonParams.push(`*${paramName}: ${paramType}`); } const decorators = new Array(); if (this.jsName !== undefined) { decorators.push(`@jsii.member(jsii_name="${this.jsName}")`); } if (this.decorator !== undefined) { decorators.push(`@${this.decorator}`); } if (renderAbstract && this.abstract) { decorators.push('@abc.abstractmethod'); } if (decorators.length > 0) { for (const decorator of decorators) { code.line(decorator); } } pythonParams.unshift(slugifyAsNeeded(this.implicitParameter, pythonParams.map((param) => param.split(':')[0].trim()))); openSignature(code, 'def', this.pythonName, pythonParams, returnType); this.generator.emitDocString(code, this.apiLocation, this.docs, { arguments: documentableArgs, documentableItem: `method-${this.pythonName}`, }); if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) { emitParameterTypeChecks(code, context, pythonParams.slice(1), `${this.pythonParent.fqn ?? this.pythonParent.pythonName}#${this.pythonName}`); } this.emitBody(code, context, renderAbstract, forceEmitBody, liftedPropNames, pythonParams[0], returnType); code.closeBlock(); } emitBody(code, context, renderAbstract, forceEmitBody, liftedPropNames, implicitParameter, returnType) { if ((!this.shouldEmitBody && !forceEmitBody) || (renderAbstract && this.abstract)) { code.line('...'); } else { if (this.liftedProp !== undefined) { this.emitAutoProps(code, context, liftedPropNames); } this.emitJsiiMethodCall(code, context, liftedPropNames, implicitParameter, returnType); } } emitAutoProps(code, context, liftedPropNames) { const lastParameter = this.parameters.slice(-1)[0]; const argName = toPythonParameterName(lastParameter.name, liftedPropNames); const typeName = (0, type_name_1.toTypeName)(lastParameter.type).pythonType({ ...context, typeAnnotation: false, }); // We need to build up a list of properties, which are mandatory, these are the // ones we will specifiy to start with in our dictionary literal. const liftedProps = this.getLiftedProperties(context.resolver).map((p) => new StructField(this.generator, p.prop, p.definingType)); const assignments = liftedProps .map((p) => p.pythonName) .map((v) => `${v}=${v}`); assignCallResult(code, argName, typeName, assignments); code.line(); } emitJsiiMethodCall(code, context, liftedPropNames, implicitParameter, returnType) { const methodPrefix = this.returnFromJSIIMethod ? 'return ' : ''; const jsiiMethodParams = []; if (this.classAsFirstParameter) { if (this.parent === undefined) { throw new Error('Parent not known.'); } if (this.isStatic) { jsiiMethodParams.push((0, type_name_1.toTypeName)(this.parent).pythonType({ ...context, typeAnnotation: false, })); } else { // Using the dynamic class of `self`. jsiiMethodParams.push(`${implicitParameter}.__class__`); } } jsiiMethodParams.push(implicitParameter); if (this.jsName !== undefined) { jsiiMethodParams.push(`"${this.jsName}"`); } // If the last arg is variadic, expand the tuple const params = []; for (const param of this.parameters) { let expr = toPythonParameterName(param.name, liftedPropNames); if (param.variadic) { expr = `*${expr}`; } params.push(expr); } const value = `jsii.${this.jsiiMethod}(${jsiiMethodParams.join(', ')}, [${params.join(', ')}])`; code.line(`${methodPrefix}${this.returnFromJSIIMethod && returnType ? `typing.cast(${returnType}, ${value})` : value}`); } getLiftedProperties(resolver) { const liftedProperties = []; const stack = [this.liftedProp]; const knownIfaces = new Set(); const knownProps = new Set(); for (let current = stack.shift(); current != null; current = stack.shift()) { knownIfaces.add(current.fqn); // Add any interfaces that this interface depends on, to the list. if (current.interfaces !== undefined) { for (const iface of current.interfaces) { if (knownIfaces.has(iface)) { continue; } stack.push(resolver.dereference(iface)); knownIfaces.add(iface); } } // Add all of the properties of this interface to our list of properties. if (current.properties !== undefined) { for (const prop of current.properties) { if (knownProps.has(prop.name)) { continue; } liftedProperties.push({ prop, definingType: current }); knownProps.add(prop.name); } } } return liftedProperties; } } class BaseProperty { constructor(generator, pythonName, jsName, type, docs, pythonParent, opts) { this.generator = generator; this.pythonName = pythonName; this.jsName = jsName; this.type = type; this.docs = docs; this.pythonParent = pythonParent; this.shouldEmitBody = true; const { abstract = false, immutable = false, isStatic = false } = opts; this.abstract = abstract; this.immutable = immutable; this.isStatic = isStatic; this.parent = opts.parent; } get apiLocation() { return { api: 'member', fqn: this.parent.fqn, memberName: this.jsName }; } requiredImports(context) { return (0, type_name_1.toTypeName)(this.type).requiredImports(context); } emit(code, context, opts) { const { renderAbstract = true, forceEmitBody = false } = opts ?? {}; const pythonType = (0, type_name_1.toTypeName)(this.type).pythonType(context); code.line(`@${this.decorator}`); code.line(`@jsii.member(jsii_name="${this.jsName}")`); if (renderAbstract && this.abstract) { code.line('@abc.abstractmethod'); } openSignature(code, 'def', this.pythonName, [this.implicitParameter], pythonType, // PyRight and MyPY both special-case @property, but not custom implementations such as our @classproperty... // MyPY reports on the re-declaration, but PyRight reports on the initial declaration (duh!) this.isStatic && !this.immutable ? 'pyright: ignore [reportGeneralTypeIssues]' : undefined); this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `prop-${this.pythonName}`, }); // NOTE: No parameters to validate here, this is a getter... if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) { code.line(`return typing.cast(${pythonType}, jsii.${this.jsiiGetMethod}(${this.implicitParameter}, "${this.jsName}"))`); } else { code.line('...'); } code.closeBlock(); if (!this.immutable) { code.line(); // PyRight and MyPY both special-case @property, but not custom implementations such as our @classproperty... // MyPY reports on the re-declaration, but PyRight reports on the initial declaration (duh!) code.line(`@${this.pythonName}.setter${this.isStatic ? ' # type: ignore[no-redef]' : ''}`); if (renderAbstract && this.abstract) { code.line('@abc.abstractmethod'); } openSignature(code, 'def', this.pythonName, [this.implicitParameter, `value: ${pythonType}`], 'None'); if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) { emitParameterTypeChecks(code, context, [`value: ${pythonType}`], `${this.pythonParent.fqn ?? this.pythonParent.pythonName}#${this.pythonName}`); code.line(`jsii.${this.jsiiSetMethod}(${this.implicitParameter}, "${this.jsName}", value)`); } else { code.line('...'); } code.closeBlock(); } } } class Interface extends BasePythonClassType { emit(code, context) { context = nestedContext(context, this.fqn); emitList(code, '@jsii.interface(', [`jsii_type="${this.fqn}"`], ')'); // First we do our normal class logic for emitting our members. super.emit(code, context); code.line(); code.line(); // Then, we have to emit a Proxy class which implements our proxy interface. const proxyBases = this.bases.map((b) => // "# type: ignore[misc]" because MyPy cannot check dynamic base classes (naturally) `jsii.proxy_for(${(0, type_name_1.toTypeName)(b).pythonType({ ...context, typeAnnotation: false, })}) # type: ignore[misc]`); openSignature(code, 'class', this.proxyClassName, proxyBases); this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `class-${this.pythonName}`, trailingNewLine: true, }); code.line(`__jsii_type__: typing.ClassVar[str] = "${this.fqn}"`); if (this.members.length > 0) { for (const member of this.members) { if (this.separateMembers) { code.line(); } member.emit(code, context, { forceEmitBody: true }); } } else { code.line('pass'); } code.closeBlock(); code.line(); code.line('# Adding a "__jsii_proxy_class__(): typing.Type" function to the interface'); code.line(`typing.cast(typing.Any, ${this.pythonName}).__jsii_proxy_class__ = lambda : ${this.proxyClassName}`); if (this.fqn != null) { context.emittedTypes.add(this.fqn); } } getClassParams(context) { const params = this.bases.map((b) => (0, type_name_1.toTypeName)(b).pythonType({ ...context, typeAnnotation: false })); params.push('typing_extensions.Protocol'); return params; } get proxyClassName() { return `_${this.pythonName}Proxy`; } } class InterfaceMethod extends BaseMethod { constructor() { super(...arguments); this.implicitParameter = 'self'; this.jsiiMethod = 'invoke'; this.shouldEmitBody = false; } } class InterfaceProperty extends BaseProperty { constructor() { super(...arguments); this.decorator = 'builtins.property'; this.implicitParameter = 'self'; this.jsiiGetMethod = 'get'; this.jsiiSetMethod = 'set'; this.shouldEmitBody = false; } } class Struct extends BasePythonClassType { constructor() { super(...arguments); this.directMembers = new Array(); } addMember(member) { if (!(member instanceof StructField)) { throw new Error('Must add StructField to Struct'); } this.directMembers.push(member); } emit(code, context) { context = nestedContext(context, this.fqn); const baseInterfaces = this.getClassParams(context); code.indent('@jsii.data_type('); code.line(`jsii_type=${JSON.stringify(this.fqn)},`); emitList(code, 'jsii_struct_bases=[', baseInterfaces, '],'); assignDictionary(code, 'name_mapping', this.propertyMap(), ',', true); code.unindent(')'); openSignature(code, 'class', this.pythonName, baseInterfaces); this.emitConstructor(code, context); for (const member of this.allMembers) { code.line(); this.emitGetter(member, code, context); } this.emitMagicMethods(code); code.closeBlock(); if (this.fqn != null) { context.emittedTypes.add(this.fqn); } } requiredImports(context) { return (0, type_name_1.mergePythonImports)(super.requiredImports(context), ...this.allMembers.map((mem) => mem.requiredImports(context))); } getClassParams(context) { return this.bases.map((b) => (0, type_name_1.toTypeName)(b).pythonType({ ...context, typeAnnotation: false })); } /** * Find all fields (inherited as well) */ get allMembers() { return this.thisInterface.allProperties.map((x) => new StructField(this.generator, x.spec, x.definingType.spec)); } get thisInterface() { if (this.fqn == null) { throw new Error('FQN not set'); } return this.generator.reflectAssembly.system.findInterface(this.fqn); } emitConstructor(code, context) { const members = this.allMembers; const kwargs = members.map((m) => m.constructorDecl(context)); const implicitParameter = slugifyAsNeeded('self', members.map((m) => m.pythonName)); const constructorArguments = kwargs.length > 0 ? [implicitParameter, '*', ...kwargs] : [implicitParameter]; openSignature(code, 'def', '__init__', constructorArguments, 'None'); this.emitConstructorDocstring(code); // Re-type struct arguments that were passed as "dict". Do this before validating argument types... for (const member of members.filter((m) => m.isStruct(this.generator))) { // Note that "None" is NOT an instance of dict (that's convenient!) const typeName = (0, type_name_1.toTypeName)(member.type.type).pythonType({ ...context, typeAnnotation: false, }); code.openBlock(`if isinstance(${member.pythonName}, dict)`); code.line(`${member.pythonName} = ${typeName}(**${member.pythonName})`); code.closeBlock(); } if (kwargs.length > 0) { emitParameterTypeChecks(code, // Runtime type check keyword args as this is a struct __init__ function. { ...context, runtimeTypeCheckKwargs: true }, ['*', ...kwargs], `${this.fqn ?? this.pythonName}#__init__`); } // Required properties, those will always be put into the dict assignDictionary(code, `${implicitParameter}._values: typing.Dict[builtins.str, typing.Any]`, members .filter((m) => !m.optional) .map((member) => `${JSON.stringify(member.pythonName)}: ${member.pythonName}`)); // Optional properties, will only be put into the dict if they're not None for (const member of members.filter((m) => m.optional)) { code.openBlock(`if ${member.pythonName} is not None`); code.line(`${implicitParameter}._values["${member.pythonName}"] = ${member.pythonName}`); code.closeBlock(); } code.closeBlock(); } emitConstructorDocstring(code) { const args = this.allMembers.map((m) => ({ name: m.pythonName, docs: m.docs, definingType: this.spec, })); this.generator.emitDocString(code, this.apiLocation, this.docs, { arguments: args, documentableItem: `class-${this.pythonName}`, }); } emitGetter(member, code, context) { const pythonType = member.typeAnnotation(context); code.line('@builtins.property'); openSignature(code, 'def', member.pythonName, ['self'], pythonType); member.emitDocString(code); // NOTE: No parameter to validate here, this is a getter. code.line(`result = self._values.get(${JSON.stringify(member.pythonName)})`); if (!member.optional) { // Add an assertion to maye MyPY happy! code.line(`assert result is not None, "Required property '${member.pythonName}' is missing"`); } code.line(`return typing.cast(${pythonType}, result)`); code.closeBlock(); } emitMagicMethods(code) { code.line(); code.openBlock('def __eq__(self, rhs: typing.Any) -> builtins.bool'); code.line('return isinstance(rhs, self.__class__) and rhs._values == self._values'); code.closeBlock(); code.line(); code.openBlock('def __ne__(self, rhs: typing.Any) -> builtins.bool'); code.line('return not (rhs == self)'); code.closeBlock(); code.line(); code.openBlock('def __repr__(self) -> str'); code.indent(`return "${this.pythonName}(%s)" % ", ".join(`); code.line('k + "=" + repr(v) for k, v in self._values.items()'); code.unindent(')'); code.closeBlock(); } propertyMap() { const ret = new Array(); for (const member of this.allMembers) { ret.push(`${JSON.stringify(member.pythonName)}: ${JSON.stringify(member.jsiiName)}`); } return ret; } } class StructField { constructor(generator, prop, definingType) { this.generator = generator; this.prop = prop; this.definingType = definingType; this.pythonName = toPythonPropertyName(prop.name); this.jsiiName = prop.name; this.type = prop; this.docs = prop.docs; } get apiLocation() { return { api: 'member', fqn: this.definingType.fqn, memberName: this.jsiiName, }; } get optional() { return !!this.type.optional; } requiredImports(context) { return (0, type_name_1.toTypeName)(this.type).requiredImports(context); } isStruct(generator) { return isStruct(generator.reflectAssembly.system, this.type.type); } constructorDecl(context) { const opt = this.optional ? ' = None' : ''; return `${this.pythonName}: ${this.typeAnnotation({ ...context, parameterType: true, })}${opt}`; } /** * Return the Python type annotation for this type */ typeAnnotation(context) { return (0, type_name_1.toTypeName)(this.type).pythonType(context); } emitDocString(code) { this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `prop-${this.pythonName}`, }); } emit(code, context) { const resolvedType = this.typeAnnotation(context); code.line(`${this.pythonName}: ${resolvedType}`); this.emitDocString(code); } } class Class extends BasePythonClassType { constructor(generator, name, spec, fqn, opts, docs) { super(generator, name, spec, fqn, opts, docs); const { abstract = false, interfaces = [], abstractBases = [] } = opts; this.abstract = abstract; this.interfaces = interfaces; this.abstractBases = abstractBases; } dependsOn(resolver) { const dependencies = super.dependsOn(resolver); const parent = resolver.getParent(this.fqn); // We need to return any ifaces that are in the same module at the same level of // nesting. const seen = new Set(); for (const iface of this.interfaces) { if (resolver.isInModule(iface)) { // Given a iface, we need to locate the ifaces's parent that is the same // as our parent, because we only care about dependencies that are at the // same level of our own. // TODO: We might need to recurse into our members to also find their // dependencies. let ifaceItem = resolver.getType(iface); let ifaceParent = resolver.getParent(iface); while (ifaceParent !== parent) { ifaceItem = ifaceParent; ifaceParent = resolver.getParent(ifaceItem.fqn); } if (!seen.has(ifaceItem.fqn)) { dependencies.push(ifaceItem); seen.add(ifaceItem.fqn); } } } return dependencies; } requiredImports(context) { return (0, type_name_1.mergePythonImports)(super.requiredImports(context), // Takes care of base & members ...this.interfaces.map((base) => (0, type_name_1.toTypeName)(base).requiredImports(context))); } emit(code, context) { // First we emit our implments decorator if (this.interfaces.length > 0) { const interfaces = this.interfaces.map((b) => (0, type_name_1.toTypeName)(b).pythonType({ ...context, typeAnnotation: false })); code.line(`@jsii.implements(${interfaces.join(', ')})`); } // Then we do our normal class logic for emitting our members. super.emit(code, context); // Then, if our class is Abstract, we have to go through and redo all of // this logic, except only emiting abstract methods and properties as non // abstract, and subclassing our initial class. if (this.abstract) { context = nestedContext(context, this.fqn); const proxyBases = [this.pythonName]; for (const base of this.abstractBases) { // "# type: ignore[misc]" because MyPy cannot check dynamic base classes (naturally) proxyBases.push(`jsii.proxy_for(${(0, type_name_1.toTypeName)(base).pythonType({ ...context, typeAnnotation: false, })}) # type: ignore[misc]`); } code.line(); code.line(); openSignature(code, 'class', this.proxyClassName, proxyBases); // Filter our list of members to *only* be abstract members, and not any // other types. const abstractMembers = this.members.filter((m) => (m instanceof BaseMethod || m instanceof BaseProperty) && m.abstract); if (abstractMembers.length > 0) { let first = true; for (const member of abstractMembers) { if (this.separateMembers) { if (first) { first = false; } else { code.line(); } } member.emit(code, context, { renderAbstract: false }); } } else { code.line('pass'); } code.closeBlock(); code.line(); code.line('# Adding a "__jsii_proxy_class__(): typing.Type" function to the abstract class'); code.line(`typing.cast(typing.Any, ${this.pythonName}).__jsii_proxy_class__ = lambda : ${this.proxyClassName}`); } } getClassParams(context) { const params = this.bases.map((b) => (0, type_name_1.toTypeName)(b).pythonType({ ...context, typeAnnotation: false })); const metaclass = this.abstract ? 'JSIIAbstractClass' : 'JSIIMeta'; params.push(`metaclass=jsii.${metaclass}`); params.push(`jsii_type="${this.fqn}"`); return params; } get proxyClassName() { return `_${this.pythonName}Proxy`; } } class StaticMethod extends BaseMethod { constructor() { super(...arguments); this.decorator = 'builtins.classmethod'; this.implicitParameter = 'cls'; this.jsiiMethod = 'sinvoke'; } } class Initializer extends BaseMethod { constructor() { super(...arguments); this.implicitParameter = 'self'; this.jsiiMethod = 'create'; this.classAsFirstParameter = true; this.returnFromJSIIMethod = false; } } class Method extends BaseMethod { constructor() { super(...arguments); this.implicitParameter = 'self'; this.jsiiMethod = 'invoke'; } } class AsyncMethod extends BaseMethod { constructor() { super(...arguments); this.implicitParameter = 'self'; this.jsiiMethod = 'ainvoke'; } } class StaticProperty extends BaseProperty { constructor() { super(...arguments); this.decorator = 'jsii.python.classproperty'; this.implicitParameter = 'cls'; this.jsiiGetMethod = 'sget'; this.jsiiSetMethod = 'sset'; } } class Property extends BaseProperty { constructor() { super(...arguments); this.decorator = 'builtins.property'; this.implicitParameter = 'self'; this.jsiiGetMethod = 'get'; this.jsiiSetMethod = 'set'; } } class Enum extends BasePythonClassType { constructor() { super(...arguments); this.separateMembers = false; } emit(code, context) { context = nestedContext(context, this.fqn); emitList(code, '@jsii.enum(', [`jsii_type="${this.fqn}"`], ')'); return super.emit(code, context); } getClassParams(_context) { return ['enum.Enum']; } requiredImports(context) { return super.requiredImports(context); } } class EnumMember { constructor(generator, pythonName, value, docs, parent) { this.generator = generator; this.pythonName = pythonName; this.value = value; this.docs = docs; this.parent = parent; this.pythonName = pythonName; this.value = value; } get apiLocation() { return { api: 'member', fqn: this.parent.fqn, memberName: this.value }; } dependsOnModules() { return new Set(); } emit(code, _context) { code.line(`${this.pythonName} = "${this.value}"`); this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `enum-${this.pythonName}`, }); } requiredImports(_context) { return {}; } } /** * Python module * * Will be called for jsii submodules and namespaces. */ class PythonModule { constructor(pythonName, fqn, opts) { this.pythonName = pythonName; this.fqn = fqn; this.members = new Array(); this.modules = new Array(); this.assembly = opts.assembly; this.assemblyFilename = opts.assemblyFilename; this.loadAssembly = !!opts.loadAssembly; this.moduleDocumentation = opts.moduleDocumentation; } addMember(member) { this.members.push(member); } addPythonModule(pyMod) { assert(!this.loadAssembly, 'PythonModule.addPythonModule CANNOT be called on assembly-loading modules (it would cause a load cycle)!'); assert(pyMod.pythonName.startsWith(`${this.pythonName}.`), `Attempted to register ${pyMod.pythonName} as a child module of ${this.pythonName}, but the names don't match!`); const [firstLevel, ...rest] = pyMod.pythonName .substring(this.pythonName.length + 1) .split('.'); if (rest.length === 0) { // This is a direct child module... this.modules.push(pyMod); } else { // This is a nested child module, so we delegate to the directly nested module... const parent = this.modules.find((m) => m.pythonName === `${this.pythonName}.${firstLevel}`); if (!parent) { throw new Error(`Attempted to register ${pyMod.pythonName} within ${this.pythonName}, but ${this.pythonName}.${firstLevel} wasn't registered yet!`); } parent.addPythonModule(pyMod); } } requiredImports(context) { return (0, type_name_1.mergePythonImports)(...this.members.map((mem) => mem.requiredImports(context))); } emit(code, context) { this.emitModuleDocumentation(code); const resolver = this.fqn ? context.resolver.bind(this.fqn, this.pythonName) : context.resolver; context = { ...context, submodule: this.fqn ?? context.submodule, resolver, }; // Before we write anything else, we need to write out our module headers, this // is where we handle stuff like imports, any required initialization, etc. // If multiple packages use the same namespace (in Python, a directory) it