jsii-pacmak
Version:
A code generation framework for jsii backend languages
1,170 lines (1,169 loc) • 95 kB
JavaScript
"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