packages/jsii-pacmak/lib/targets/python.ts (2,742 lines of code) (raw):

import * as spec from '@jsii/spec'; import * as assert from 'assert'; import { CodeMaker, toSnakeCase } from 'codemaker'; import * as crypto from 'crypto'; import * as escapeStringRegexp from 'escape-string-regexp'; import * as fs from 'fs-extra'; import * as reflect from 'jsii-reflect'; import { TargetLanguage, RosettaTabletReader, enforcesStrictMode, ApiLocation, } from 'jsii-rosetta'; import * as path from 'path'; import { Generator, GeneratorOptions } from '../generator'; import { warn } from '../logging'; import { md2rst } from '../markdown'; import { Target, TargetOptions } from '../target'; import { shell } from '../util'; import { VERSION } from '../version'; import { renderSummary, PropertyDefinition } from './_utils'; import { NamingContext, toTypeName, PythonImports, mergePythonImports, toPackageName, } from './python/type-name'; import { die, toPythonIdentifier } from './python/util'; import { toPythonVersionRange, toReleaseVersion } from './version-utils'; import { TargetName } from './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}`; export default class Python extends Target { protected readonly generator: PythonGenerator; public constructor(options: TargetOptions) { super(options); this.generator = new PythonGenerator(options.rosetta, options); } public async generateCode(outDir: string, tarball: string): Promise<void> { await super.generateCode(outDir, tarball); } public async build(sourceDir: string, outDir: string): Promise<void> { // 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 shell(process.platform === 'win32' ? 'python' : 'python3', [ '-m', 'venv', '--system-site-packages', // Allow using globally installed packages (saves time & disk space) venv, ]); const env = { ...process.env, PATH: `${venvBin}:${process.env.PATH}`, VIRTUAL_ENV: venv, }; const python = path.join(venvBin, 'python'); // Install the necessary things await 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 shell(python, ['setup.py', 'sdist', '--dist-dir', outDir], { cwd: sourceDir, env, }); await shell( python, ['-m', 'pip', 'wheel', '--no-deps', '--wheel-dir', outDir, sourceDir], { cwd: sourceDir, env, retry: { maxAttempts: 5 }, }, ); await shell(python, ['-m', 'twine', 'check', path.join(outDir, '*')], { cwd: sourceDir, env, }); } } // ################## // # CODE GENERATOR # // ################## interface EmitContext extends NamingContext { /** @deprecated The TypeResolver */ readonly resolver: TypeResolver; /** Whether to emit runtime type checking code */ readonly runtimeTypeChecking: boolean; /** Whether to runtime type check keyword arguments (i.e: struct constructors) */ readonly runtimeTypeCheckKwargs?: boolean; /** The numerical IDs used for type annotation data storing */ readonly typeCheckingHelper: TypeCheckingHelper; } class TypeCheckingHelper { #stubs = new Array<TypeCheckingStub>(); public getTypeHints(fqn: string, args: readonly string[]): string { const stub = new TypeCheckingStub(fqn, args); this.#stubs.push(stub); return `typing.get_type_hints(${stub.name})`; } /** Emits instructions that create the annotations data... */ public flushStubs(code: CodeMaker) { for (const stub of this.#stubs) { stub.emit(code); } // Reset the stubs list this.#stubs = []; } } class TypeCheckingStub { static readonly #PREFIX = '_typecheckingstub__'; readonly #arguments: readonly string[]; readonly #hash: string; public constructor(fqn: string, args: readonly string[]) { // Removing the quoted type names -- this will be emitted at the very end of the module. this.#arguments = args.map((arg) => arg.replace(/"/g, '')); this.#hash = crypto .createHash('sha256') .update(TypeCheckingStub.#PREFIX) .update(fqn) .digest('hex'); } public get name(): string { return `${TypeCheckingStub.#PREFIX}${this.#hash}`; } public emit(code: CodeMaker) { code.line(); openSignature(code, 'def', this.name, this.#arguments, 'None'); code.line(`"""Type checking stubs"""`); code.line('pass'); code.closeBlock(); } } const pythonModuleNameToFilename = (name: string): string => { return path.join(...name.split('.')); }; const toPythonMethodName = (name: string, protectedItem = false): string => { let value = toPythonIdentifier(toSnakeCase(name)); if (protectedItem) { value = `_${value}`; } return value; }; const toPythonPropertyName = ( name: string, constant = false, protectedItem = false, ): string => { let value = toPythonIdentifier(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: string, liftedParamNames = new Set<string>(), ): string { let result = toPythonIdentifier(toSnakeCase(name)); while (liftedParamNames.has(result)) { result += '_'; } return result; } const setDifference = <T>(setA: Set<T>, setB: Set<T>): Set<T> => { const result = new Set<T>(); 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: PythonBase[], resolver: TypeResolver) { // create a map from python name to list of members const map: { [pythonName: string]: PythonBase[] } = {}; 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<PythonBase>(); 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: PythonBase[], resolver: TypeResolver, ): PythonBase[] => { let sortable = new Array<{ member: PythonBase & ISortableType; dependsOn: Set<PythonType>; }>(); const sorted = new Array<PythonBase>(); const seen = new Set<PythonBase>(); // 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; }; interface PythonBase { readonly pythonName: string; readonly docs?: spec.Docs; emit(code: CodeMaker, context: EmitContext, opts?: any): void; requiredImports(context: EmitContext): PythonImports; } interface PythonType extends PythonBase { // The JSII FQN for this item, if this item doesn't exist as a JSII type, then it // doesn't have a FQN and it should be null; readonly fqn?: string; addMember(member: PythonBase): void; } interface ISortableType { dependsOn(resolver: TypeResolver): PythonType[]; } function isSortableType(arg: unknown): arg is ISortableType { return (arg as Partial<ISortableType>).dependsOn !== undefined; } interface PythonTypeOpts { bases?: spec.TypeReference[]; } abstract class BasePythonClassType implements PythonType, ISortableType { protected bases: spec.TypeReference[]; protected members: PythonBase[]; protected readonly separateMembers: boolean = true; public constructor( protected readonly generator: PythonGenerator, public readonly pythonName: string, public readonly spec: spec.Type, public readonly fqn: string | undefined, opts: PythonTypeOpts, public readonly docs: spec.Docs | undefined, ) { const { bases = [] } = opts; this.bases = bases; this.members = []; } public dependsOn(resolver: TypeResolver): PythonType[] { const dependencies = new Array<PythonType>(); 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<string>(); 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; } public requiredImports(context: EmitContext): PythonImports { return mergePythonImports( ...this.bases.map((base) => toTypeName(base).requiredImports(context)), ...this.members.map((mem) => mem.requiredImports(context)), ); } public addMember(member: PythonBase) { this.members.push(member); } public get apiLocation(): ApiLocation { if (!this.fqn) { throw new Error( `Cannot make apiLocation for ${this.pythonName}, does not have FQN`, ); } return { api: 'type', fqn: this.fqn }; } public emit(code: CodeMaker, context: EmitContext) { 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); } } protected boundResolver(resolver: TypeResolver): TypeResolver { if (this.fqn == null) { return resolver; } return resolver.bind(this.fqn); } protected abstract getClassParams(context: EmitContext): string[]; } interface BaseMethodOpts { abstract?: boolean; liftedProp?: spec.InterfaceType; parent: spec.NamedTypeReference; } interface BaseMethodEmitOpts { renderAbstract?: boolean; forceEmitBody?: boolean; } abstract class BaseMethod implements PythonBase { public readonly abstract: boolean; protected abstract readonly implicitParameter: string; protected readonly jsiiMethod!: string; protected readonly decorator?: string; protected readonly classAsFirstParameter: boolean = false; protected readonly returnFromJSIIMethod: boolean = true; protected readonly shouldEmitBody: boolean = true; private readonly liftedProp?: spec.InterfaceType; private readonly parent: spec.NamedTypeReference; public constructor( protected readonly generator: PythonGenerator, public readonly pythonName: string, private readonly jsName: string | undefined, private readonly parameters: spec.Parameter[], private readonly returns: spec.OptionalValue | undefined, public readonly docs: spec.Docs | undefined, public readonly isStatic: boolean, private readonly pythonParent: PythonType, opts: BaseMethodOpts, ) { this.abstract = !!opts.abstract; this.liftedProp = opts.liftedProp; this.parent = opts.parent; } public get apiLocation(): ApiLocation { return { api: 'member', fqn: this.parent.fqn, memberName: this.jsName ?? '', }; } public requiredImports(context: EmitContext): PythonImports { return mergePythonImports( toTypeName(this.returns).requiredImports(context), ...this.parameters.map((param) => toTypeName(param).requiredImports(context), ), ...liftedProperties(this.liftedProp), ); function* liftedProperties( struct: spec.InterfaceType | undefined, ): IterableIterator<PythonImports> { if (struct == null) { return; } for (const prop of struct.properties ?? []) { yield toTypeName(prop.type).requiredImports(context); } for (const base of struct.interfaces ?? []) { const iface = context.resolver.dereference(base) as spec.InterfaceType; for (const imports of liftedProperties(iface)) { yield imports; } } } } public emit( code: CodeMaker, context: EmitContext, opts?: BaseMethodEmitOpts, ) { const { renderAbstract = true, forceEmitBody = false } = opts ?? {}; const returnType: string = 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<string>(); 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: string[] = []; 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: string = toPythonParameterName( param.name, liftedPropNames, ); const paramType = toTypeName(param).pythonType({ ...context, parameterType: true, }); const paramDefault = param.optional ? ' = None' : ''; pythonParams.push(`${paramName}: ${paramType}${paramDefault}`); } const documentableArgs: DocumentableArgument[] = this.parameters .map( (p) => ({ name: p.name, docs: p.docs, definingType: this.parent, }) as DocumentableArgument, ) // 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 = 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, }) as DocumentableArgument, ), ); } 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 = toTypeName(lastParameter.type).pythonType(context); pythonParams.push(`*${paramName}: ${paramType}`); } const decorators = new Array<string>(); 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(); } private emitBody( code: CodeMaker, context: EmitContext, renderAbstract: boolean, forceEmitBody: boolean, liftedPropNames: Set<string>, implicitParameter: string, returnType: string, ) { 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, ); } } private emitAutoProps( code: CodeMaker, context: EmitContext, liftedPropNames: Set<string>, ) { const lastParameter = this.parameters.slice(-1)[0]; const argName = toPythonParameterName(lastParameter.name, liftedPropNames); const typeName = 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 specify 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(); } private emitJsiiMethodCall( code: CodeMaker, context: EmitContext, liftedPropNames: Set<string>, implicitParameter: string, returnType: string, ) { const methodPrefix: string = this.returnFromJSIIMethod ? 'return ' : ''; const jsiiMethodParams: string[] = []; if (this.classAsFirstParameter) { if (this.parent === undefined) { throw new Error('Parent not known.'); } if (this.isStatic) { jsiiMethodParams.push( 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: string[] = []; 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 }`, ); } private getLiftedProperties(resolver: TypeResolver): PropertyDefinition[] { const liftedProperties: PropertyDefinition[] = []; const stack = [this.liftedProp]; const knownIfaces = new Set<string>(); const knownProps = new Set<string>(); 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) as spec.InterfaceType); 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; } } interface BasePropertyOpts { abstract?: boolean; immutable?: boolean; isStatic?: boolean; parent: spec.NamedTypeReference; } interface BasePropertyEmitOpts { renderAbstract?: boolean; forceEmitBody?: boolean; } abstract class BaseProperty implements PythonBase { public readonly abstract: boolean; public readonly isStatic: boolean; protected abstract readonly decorator: string; protected abstract readonly implicitParameter: string; protected readonly jsiiGetMethod!: string; protected readonly jsiiSetMethod!: string; protected readonly shouldEmitBody: boolean = true; private readonly immutable: boolean; private readonly parent: spec.NamedTypeReference; public constructor( private readonly generator: PythonGenerator, public readonly pythonName: string, private readonly jsName: string, private readonly type: spec.OptionalValue, public readonly docs: spec.Docs | undefined, private readonly pythonParent: PythonType, opts: BasePropertyOpts, ) { const { abstract = false, immutable = false, isStatic = false } = opts; this.abstract = abstract; this.immutable = immutable; this.isStatic = isStatic; this.parent = opts.parent; } public get apiLocation(): ApiLocation { return { api: 'member', fqn: this.parent.fqn, memberName: this.jsName }; } public requiredImports(context: EmitContext): PythonImports { return toTypeName(this.type).requiredImports(context); } public emit( code: CodeMaker, context: EmitContext, opts?: BasePropertyEmitOpts, ) { const { renderAbstract = true, forceEmitBody = false } = opts ?? {}; const pythonType = 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,reportRedeclaration]' : 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 }`, ); // In case of a static setter, the 'cls' type is the class type but because we use a custom // decorator to make the setter operate on classes instead of objects, pyright doesn't know about // that and thinks the first argument is an instance instead of a class. Shut it up. code.line( `jsii.${this.jsiiSetMethod}(${this.implicitParameter}, "${this.jsName}", value) # pyright: ignore[reportArgumentType]`, ); } else { code.line('...'); } code.closeBlock(); } } } class Interface extends BasePythonClassType { public emit(code: CodeMaker, context: EmitContext) { 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: string[] = this.bases.map( (b) => // "# type: ignore[misc]" because MyPy cannot check dynamic base classes (naturally) `jsii.proxy_for(${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); } } protected getClassParams(context: EmitContext): string[] { const params: string[] = this.bases.map((b) => toTypeName(b).pythonType({ ...context, typeAnnotation: false }), ); params.push('typing_extensions.Protocol'); return params; } private get proxyClassName(): string { return `_${this.pythonName}Proxy`; } } class InterfaceMethod extends BaseMethod { protected readonly implicitParameter: string = 'self'; protected readonly jsiiMethod: string = 'invoke'; protected readonly shouldEmitBody: boolean = false; } class InterfaceProperty extends BaseProperty { protected readonly decorator: string = 'builtins.property'; protected readonly implicitParameter: string = 'self'; protected readonly jsiiGetMethod: string = 'get'; protected readonly jsiiSetMethod: string = 'set'; protected readonly shouldEmitBody: boolean = false; } class Struct extends BasePythonClassType { protected directMembers = new Array<StructField>(); public addMember(member: PythonBase): void { if (!(member instanceof StructField)) { throw new Error('Must add StructField to Struct'); } this.directMembers.push(member); } public emit(code: CodeMaker, context: EmitContext) { 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); } } public requiredImports(context: EmitContext) { return mergePythonImports( super.requiredImports(context), ...this.allMembers.map((mem) => mem.requiredImports(context)), ); } protected getClassParams(context: EmitContext): string[] { return this.bases.map((b) => toTypeName(b).pythonType({ ...context, typeAnnotation: false }), ); } /** * Find all fields (inherited as well) */ private get allMembers(): StructField[] { return this.thisInterface.allProperties.map( (x) => new StructField(this.generator, x.spec, x.definingType.spec), ); } private get thisInterface() { if (this.fqn == null) { throw new Error('FQN not set'); } return this.generator.reflectAssembly.system.findInterface(this.fqn); } private emitConstructor(code: CodeMaker, context: EmitContext) { 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 = 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(); } private emitConstructorDocstring(code: CodeMaker) { const args: DocumentableArgument[] = 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}`, }); } private emitGetter( member: StructField, code: CodeMaker, context: EmitContext, ) { 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(); } private emitMagicMethods(code: CodeMaker) { 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(); } private propertyMap() { const ret = new Array<string>(); for (const member of this.allMembers) { ret.push( `${JSON.stringify(member.pythonName)}: ${JSON.stringify( member.jsiiName, )}`, ); } return ret; } } class StructField implements PythonBase { public readonly pythonName: string; public readonly jsiiName: string; public readonly docs?: spec.Docs; public readonly type: spec.OptionalValue; public constructor( private readonly generator: PythonGenerator, public readonly prop: spec.Property, private readonly definingType: spec.Type, ) { this.pythonName = toPythonPropertyName(prop.name); this.jsiiName = prop.name; this.type = prop; this.docs = prop.docs; } public get apiLocation(): ApiLocation { return { api: 'member', fqn: this.definingType.fqn, memberName: this.jsiiName, }; } public get optional(): boolean { return !!this.type.optional; } public requiredImports(context: EmitContext): PythonImports { return toTypeName(this.type).requiredImports(context); } public isStruct(generator: PythonGenerator): boolean { return isStruct(generator.reflectAssembly.system, this.type.type); } public constructorDecl(context: EmitContext) { const opt = this.optional ? ' = None' : ''; return `${this.pythonName}: ${this.typeAnnotation({ ...context, parameterType: true, })}${opt}`; } /** * Return the Python type annotation for this type */ public typeAnnotation(context: EmitContext) { return toTypeName(this.type).pythonType(context); } public emitDocString(code: CodeMaker) { this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `prop-${this.pythonName}`, }); } public emit(code: CodeMaker, context: EmitContext) { const resolvedType = this.typeAnnotation(context); code.line(`${this.pythonName}: ${resolvedType}`); this.emitDocString(code); } } interface ClassOpts extends PythonTypeOpts { abstract?: boolean; interfaces?: spec.NamedTypeReference[]; abstractBases?: spec.ClassType[]; } class Class extends BasePythonClassType implements ISortableType { private readonly abstract: boolean; private readonly abstractBases: spec.ClassType[]; private readonly interfaces: spec.NamedTypeReference[]; public constructor( generator: PythonGenerator, name: string, spec: spec.Type, fqn: string, opts: ClassOpts, docs: spec.Docs | undefined, ) { super(generator, name, spec, fqn, opts, docs); const { abstract = false, interfaces = [], abstractBases = [] } = opts; this.abstract = abstract; this.interfaces = interfaces; this.abstractBases = abstractBases; } public dependsOn(resolver: TypeResolver): PythonType[] { const dependencies: PythonType[] = 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<string>(); 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; } public requiredImports(context: EmitContext): PythonImports { return mergePythonImports( super.requiredImports(context), // Takes care of base & members ...this.interfaces.map((base) => toTypeName(base).requiredImports(context), ), ); } public emit(code: CodeMaker, context: EmitContext) { // First we emit our implments decorator if (this.interfaces.length > 0) { const interfaces: string[] = this.interfaces.map((b) => 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(${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}`, ); } } protected getClassParams(context: EmitContext): string[] { const params: string[] = this.bases.map((b) => toTypeName(b).pythonType({ ...context, typeAnnotation: false }), ); const metaclass: string = this.abstract ? 'JSIIAbstractClass' : 'JSIIMeta'; params.push(`metaclass=jsii.${metaclass}`); params.push(`jsii_type="${this.fqn}"`); return params; } private get proxyClassName(): string { return `_${this.pythonName}Proxy`; } } class StaticMethod extends BaseMethod { protected readonly decorator?: string = 'builtins.classmethod'; protected readonly implicitParameter: string = 'cls'; protected readonly jsiiMethod: string = 'sinvoke'; } class Initializer extends BaseMethod { protected readonly implicitParameter: string = 'self'; protected readonly jsiiMethod: string = 'create'; protected readonly classAsFirstParameter: boolean = true; protected readonly returnFromJSIIMethod: boolean = false; } class Method extends BaseMethod { protected readonly implicitParameter: string = 'self'; protected readonly jsiiMethod: string = 'invoke'; } class AsyncMethod extends BaseMethod { protected readonly implicitParameter: string = 'self'; protected readonly jsiiMethod: string = 'ainvoke'; } class StaticProperty extends BaseProperty { protected readonly decorator: string = 'jsii.python.classproperty'; protected readonly implicitParameter: string = 'cls'; protected readonly jsiiGetMethod: string = 'sget'; protected readonly jsiiSetMethod: string = 'sset'; } class Property extends BaseProperty { protected readonly decorator: string = 'builtins.property'; protected readonly implicitParameter: string = 'self'; protected readonly jsiiGetMethod: string = 'get'; protected readonly jsiiSetMethod: string = 'set'; } class Enum extends BasePythonClassType { protected readonly separateMembers = false; public emit(code: CodeMaker, context: EmitContext) { context = nestedContext(context, this.fqn); emitList(code, '@jsii.enum(', [`jsii_type="${this.fqn}"`], ')'); return super.emit(code, context); } protected getClassParams(_context: EmitContext): string[] { return ['enum.Enum']; } public requiredImports(context: EmitContext): PythonImports { return super.requiredImports(context); } } class EnumMember implements PythonBase { public constructor( private readonly generator: PythonGenerator, public readonly pythonName: string, private readonly value: string, public readonly docs: spec.Docs | undefined, private readonly parent: spec.NamedTypeReference, ) { this.pythonName = pythonName; this.value = value; } public get apiLocation(): ApiLocation { return { api: 'member', fqn: this.parent.fqn, memberName: this.value }; } public dependsOnModules() { return new Set<string>(); } public emit(code: CodeMaker, _context: EmitContext) { code.line(`${this.pythonName} = "${this.value}"`); this.generator.emitDocString(code, this.apiLocation, this.docs, { documentableItem: `enum-${this.pythonName}`, }); } public requiredImports(_context: EmitContext): PythonImports { return {}; } } interface ModuleOpts { readonly assembly: spec.Assembly; readonly assemblyFilename: string; readonly loadAssembly?: boolean; readonly package?: Package; /** * The docstring to emit at the top of this module, if any. */ readonly moduleDocumentation?: string; } /** * Python module * * Will be called for jsii submodules and namespaces. */ class PythonModule implements PythonType { /** * Converted to put on the module * * The format is in markdown, with code samples converted from TS to Python. */ public readonly moduleDocumentation?: string; private readonly assembly: spec.Assembly; private readonly assemblyFilename: string; private readonly loadAssembly: boolean; private readonly members = new Array<PythonBase>(); private readonly modules = new Array<PythonModule>(); public constructor( public readonly pythonName: string, public readonly fqn: string | undefined, opts: ModuleOpts, ) { this.assembly = opts.assembly; this.assemblyFilename = opts.assemblyFilename; this.loadAssembly = !!opts.loadAssembly; this.moduleDocumentation = opts.moduleDocumentation; } public addMember(member: PythonBase) { this.members.push(member); } public addPythonModule(pyMod: PythonModule) { 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); } } public requiredImports(context: EmitContext): PythonImports { return mergePythonImports( ...this.members.map((mem) => mem.requiredImports(context)), ); } public emit(code: CodeMaker, context: EmitContext) { 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 // depends on how they are laid out on disk if deep imports of multiple packages // will succeed. `pip` merges all packages into the same directory, and deep // imports work automatically. `bazel` puts packages into different directories, // and `import aws_cdk.subpackage` will fail if `aws_cdk/__init__.py` and // `aws_cdk/subpackage/__init__.py` are not in the same directory. // // We can get around this by using `pkgutil` to extend the search path for the // current module (`__path__`) with all packages found on `sys.path`. code.line('from pkgutil import extend_path'); code.line('__path__ = extend_path(__path__, __name__)'); code.line(); code.line('import abc'); code.line('import builtins'); code.line('import datetime'); code.line('import enum'); code.line('import typing'); code.line(); code.line('import jsii'); code.line('import publication'); code.line('import typing_extensions'); code.line(); code.line('import typeguard'); code.line( 'from importlib.metadata import version as _metadata_package_version', ); code.line( "TYPEGUARD_MAJOR_VERSION = int(_metadata_package_version('typeguard').split('.')[0])", ); code.line(); code.openBlock( 'def check_type(argname: str, value: object, expected_type: typing.Any) -> typing.Any', ); code.openBlock('if TYPEGUARD_MAJOR_VERSION <= 2'); code.line( 'return typeguard.check_type(argname=argname, value=value, expected_type=expected_type) # type:ignore', ); code.closeBlock(); code.openBlock('else'); code.line( 'if isinstance(value, jsii._reference_map.InterfaceDynamicProxy): # pyright: ignore [reportAttributeAccessIssue]', ); code.line(' pass'); code.openBlock('else'); code.openBlock('if TYPEGUARD_MAJOR_VERSION == 3'); code.line( 'typeguard.config.collection_check_strategy = typeguard.CollectionCheckStrategy.ALL_ITEMS # type:ignore', ); code.line( 'typeguard.check_type(value=value, expected_type=expected_type) # type:ignore', ); code.closeBlock(); code.openBlock('else'); code.line( 'typeguard.check_type(value=value, expected_type=expected_type, collection_check_strategy=typeguard.CollectionCheckStrategy.ALL_ITEMS) # type:ignore', ); code.closeBlock(); code.closeBlock(); code.closeBlock(); code.closeBlock(); // Determine if we need to write out the kernel load line. if (this.loadAssembly) { this.emitDependencyImports(code); code.line(); emitList( code, '__jsii_assembly__ = jsii.JSIIAssembly.load(', [ JSON.stringify(this.assembly.name), JSON.stringify(this.assembly.version), '__name__[0:-6]', `${JSON.stringify(this.assemblyFilename)}`, ], ')', ); } else { // Then we must import the ._jsii subpackage. code.line(); let distanceFromRoot = 0; for ( let curr = this.fqn!; curr !== this.assembly.name; curr = curr.substring(0, curr.lastIndexOf('.')) ) { distanceFromRoot++; } code.line(`from ${'.'.repeat(distanceFromRoot + 1)}_jsii import *`); this.emitRequiredImports(code, context); } // Emit all of our members. for (const member of prepareMembers(this.members, resolver)) { code.line(); code.line(); member.emit(code, context); } // Whatever names we've exported, we'll write out our __all__ that lists them. // // __all__ is normally used for when users write `from library import *`, but we also // use it with the `publication` module to hide everything that's NOT in the list. // // Normally adding submodules to `__all__` has the (negative?) side-effect // that all submodules get loaded when the user does `import *`, but we // already load submodules anyway so it doesn't make a difference, and in combination // with the `publication` module NOT having them in this list hides any submodules // we import as part of typechecking. const exportedMembers = [ ...this.members.map((m) => `"${m.pythonName}"`), ...this.modules .filter((m) => this.isDirectChild(m)) .map((m) => `"${lastComponent(m.pythonName)}"`), ]; if (this.loadAssembly) { exportedMembers.push('"__jsii_assembly__"'); } // Declare the list of "public" members this module exports if (this.members.length > 0) { code.line(); } code.line(); if (exportedMembers.length > 0) { code.indent('__all__ = ['); for (const member of exportedMembers.sort()) { // Writing one by line might be _a lot_ of lines, but it'll make reviewing changes to the list easier. Trust me. code.line(`${member},`); } code.unindent(']'); } else { code.line('__all__: typing.List[typing.Any] = []'); } // Next up, we'll use publication to ensure that all of the non-public names // get hidden from dir(), tab-complete, etc. code.line(); code.line('publication.publish()'); // Finally, we'll load all registered python modules if (this.modules.length > 0) { code.line(); code.line( '# Loading modules to ensure their types are registered with the jsii runtime library', ); for (const module of this.modules.sort((l, r) => l.pythonName.localeCompare(r.pythonName), )) { // Rather than generating an absolute import like // "import jsii_calc.submodule" this builds a relative import like // "from . import submodule". This enables distributing python packages // and using the generated modules in the same codebase. const submodule = module.pythonName.substring( this.pythonName.length + 1, ); code.line(`from . import ${submodule}`); } } } /** * Emit the bin scripts if bin section defined. */ public emitBinScripts(code: CodeMaker): string[] { const scripts = new Array<string>(); if (this.loadAssembly) { if (this.assembly.bin != null) { for (const name of Object.keys(this.assembly.bin)) { const script_file = path.join( 'src', pythonModuleNameToFilename(this.pythonName), 'bin', name, ); code.openFile(script_file); code.line('#!/usr/bin/env python'); code.line(); code.line('import jsii'); code.line('import sys'); code.line('import os'); code.line(); code.openBlock('if "JSII_RUNTIME_PACKAGE_CACHE" not in os.environ'); code.line('os.environ["JSII_RUNTIME_PACKAGE_CACHE"] = "disabled"'); code.closeBlock(); code.line(); emitList( code, '__jsii_assembly__ = jsii.JSIIAssembly.load(', [ JSON.stringify(this.assembly.name), JSON.stringify(this.assembly.version), JSON.stringify(this.pythonName.replace('._jsii', '')), `${JSON.stringify(this.assemblyFilename)}`, ], ')', ); code.line(); emitList( code, 'exit_code = __jsii_assembly__.invokeBinScript(', [ JSON.stringify(this.assembly.name), JSON.stringify(name), 'sys.argv[1:]', ], ')', ); code.line('exit(exit_code)'); code.closeFile(script_file); scripts.push(script_file.replace(/\\/g, '/')); } } } return scripts; } private isDirectChild(pyMod: PythonModule) { if ( this.pythonName === pyMod.pythonName || !pyMod.pythonName.startsWith(`${this.pythonName}.`) ) { return false; } // Must include only one more component return !pyMod.pythonName .substring(this.pythonName.length + 1) .includes('.'); } /** * Emit the README as module docstring if this is the entry point module (it loads the assembly) */ private emitModuleDocumentation(code: CodeMaker) { if (this.moduleDocumentation) { code.line(RAW_DOCSTRING_QUOTES); // raw string so that python does not attempt to interpret invalid escapes that are valid in markdown code.line(this.moduleDocumentation); code.line(DOCSTRING_QUOTES); } } private emitDependencyImports(code: CodeMaker) { // Collect all the (direct) dependencies' ._jsii packages. const deps = Object.keys(this.assembly.dependencies ?? {}) .map( (dep) => this.assembly.dependencyClosure?.[dep]?.targets?.python?.module ?? die(`No Python target was configrued for the dependency "${dep}".`), ) .map((mod) => `${mod}._jsii`) .sort(); // Now actually write the import statements... if (deps.length > 0) { code.line(); for (const moduleName of deps) { code.line(`import ${moduleName}`); } } } private emitRequiredImports(code: CodeMaker, context: EmitContext) { const requiredImports = this.requiredImports(context); const statements = Object.entries(requiredImports) .map(([sourcePackage, items]) => toImportStatements(sourcePackage, items)) .reduce( (acc, elt) => [...acc, ...elt], new Array<{ emit: () => void; comparisonBase: string }>(), ) .sort(importComparator); if (statements.length > 0) { code.line(); } for (const statement of statements) { statement.emit(code); } function toImportStatements( sourcePkg: string, items: ReadonlySet<string>, ): Array<{ emit: (code: CodeMaker) => void; comparisonBase: string }> { const result = new Array<{ emit: (code: CodeMaker) => void; comparisonBase: string; }>(); if (items.has('')) { result.push({ comparisonBase: `import ${sourcePkg}`, emit(code) { code.line(this.comparisonBase); }, }); } const pieceMeal = Array.from(items) .filter((i) => i !== '') .sort(); if (pieceMeal.length > 0) { result.push({ comparisonBase: `from ${sourcePkg} import`, emit: (code) => emitList(code, `from ${sourcePkg} import `, pieceMeal, '', { ifMulti: ['(', ')'], }), }); } return result; } function importComparator( left: { comparisonBase: string }, right: { comparisonBase: string }, ) { if ( left.comparisonBase.startsWith('import') === right.comparisonBase.startsWith('import') ) { return left.comparisonBase.localeCompare(right.comparisonBase); } // We want "from .foo import (...)" to be *after* "import bar" return right.comparisonBase.localeCompare(left.comparisonBase); } } } interface PackageData { filename: string; data: string | undefined; } class Package { /** * The PythonModule that represents the root module of the package */ public rootModule?: PythonModule; public readonly name: string; public readonly version: string; public readonly metadata: spec.Assembly; private readonly modules = new Map<string, PythonModule>(); private readonly data = new Map<string, PackageData[]>(); public constructor(name: string, version: string, metadata: spec.Assembly) { this.name = name; this.version = version; this.metadata = metadata; } public addModule(module: PythonModule) { this.modules.set(module.pythonName, module); // This is the module that represents the assembly if (module.fqn === this.metadata.name) { this.rootModule = module; } } public addData( module: PythonModule, filename: string, data: string | undefined, ) { if (!this.data.has(module.pythonName)) { this.data.set(module.pythonName, []); } this.data.get(module.pythonName)!.push({ filename, data }); } public write(code: CodeMaker, context: EmitContext) { const modules = [...this.modules.values()].sort((a, b) => a.pythonName.localeCompare(b.pythonName), ); const scripts = new Array<string>(); // Iterate over all of our modules, and write them out to disk. for (const mod of modules) { const filename = path.join( 'src', pythonModuleNameToFilename(mod.pythonName), '__init__.py', ); code.openFile(filename); mod.emit(code, context); context.typeCheckingHelper.flushStubs(code); code.closeFile(filename); scripts.push(...mod.emitBinScripts(code)); } // Handle our package data. const packageData: { [key: string]: string[] } = {}; for (const [mod, pdata] of this.data) { for (const data of pdata) { if (data.data != null) { const filepath = path.join( 'src', pythonModuleNameToFilename(mod), data.filename, ); code.openFile(filepath); code.line(data.data); code.closeFile(filepath); } } packageData[mod] = pdata.map((pd) => pd.filename); } // Compute our list of dependencies const dependencies: string[] = []; for (const [depName, version] of Object.entries( this.metadata.dependencies ?? {}, )) { const depInfo = this.metadata.dependencyClosure![depName]; dependencies.push( `${depInfo.targets!.python!.distName}${toPythonVersionRange(version)}`, ); } // Need to always write this file as the build process depends on it. // Make up some contents if we don't have anything useful to say. code.openFile('README.md'); code.line( this.rootModule?.moduleDocumentation ?? `${this.name}\n${'='.repeat(this.name.length)}`, ); code.closeFile('README.md'); const setupKwargs = { name: this.name, version: this.version, description: this.metadata.description, license: this.metadata.license, url: this.metadata.homepage, long_description_content_type: 'text/markdown', author: this.metadata.author.name + (this.metadata.author.email !== undefined ? `<${this.metadata.author.email}>` : ''), bdist_wheel: { universal: true, }, project_urls: { Source: this.metadata.repository.url, }, package_dir: { '': 'src' }, packages: modules.map((m) => m.pythonName), package_data: packageData, python_requires: '~=3.9', install_requires: [ `jsii${toPythonVersionRange(`^${VERSION}`)}`, 'publication>=0.0.3', // 4.3.0 is incompatible with generated bindings, see https://github.com/aws/jsii/issues/4658 'typeguard>=2.13.3,<4.3.0', ] .concat(dependencies) .sort(), classifiers: [ 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: JavaScript', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Typing :: Typed', ], scripts, }; // Packages w/ a deprecated message may have a non-deprecated stability (e.g: when EoL happens // for a stable package). We pretend it's deprecated for the purpose of trove classifiers when // this happens. switch ( this.metadata.docs?.deprecated ? spec.Stability.Deprecated : this.metadata.docs?.stability ) { case spec.Stability.Experimental: setupKwargs.classifiers.push('Development Status :: 4 - Beta'); break; case spec.Stability.Stable: setupKwargs.classifiers.push( 'Development Status :: 5 - Production/Stable', ); break; case spec.Stability.Deprecated: setupKwargs.classifiers.push('Development Status :: 7 - Inactive'); break; case spec.Stability.External: case undefined: default: // No 'Development Status' trove classifier for you! } if (spdxLicenseList[this.metadata.license]?.osiApproved) { setupKwargs.classifiers.push('License :: OSI Approved'); } const additionalClassifiers = this.metadata.targets?.python?.classifiers; if (additionalClassifiers != null) { if (!Array.isArray(additionalClassifiers)) { throw new Error( `The "jsii.targets.python.classifiers" value must be an array of strings if provided, but found ${JSON.stringify( additionalClassifiers, null, 2, )}`, ); } // We discourage using those since we automatically set a value for them for (let classifier of additionalClassifiers.sort()) { if (typeof classifier !== 'string') { throw new Error( `The "jsii.targets.python.classifiers" value can only contain strings, but found ${JSON.stringify( classifier, null, 2, )}`, ); } // We'll split on `::` and re-join later so classifiers are "normalized" to a standard spacing const parts = classifier.split('::').map((part) => part.trim()); const reservedClassifiers = [ 'Development Status', 'License', 'Operating System', 'Typing', ]; if (reservedClassifiers.includes(parts[0])) { warn( `Classifiers starting with ${reservedClassifiers .map((x) => `"${x} ::"`) .join( ', ', )} are automatically set and should not be manually configured`, ); } classifier = parts.join(' :: '); if (setupKwargs.classifiers.includes(classifier)) { continue; } setupKwargs.classifiers.push(classifier); } } // We Need a setup.py to make this Package, actually a Package. code.openFile('setup.py'); code.line('import json'); code.line('import setuptools'); code.line(); code.line('kwargs = json.loads('); code.line(' """'); code.line(JSON.stringify(setupKwargs, null, 4)); code.line('"""'); code.line(')'); code.line(); code.openBlock('with open("README.md", encoding="utf8") as fp'); code.line('kwargs["long_description"] = fp.read()'); code.closeBlock(); code.line(); code.line(); code.line('setuptools.setup(**kwargs)'); code.closeFile('setup.py'); // Because we're good citizens, we're going to go ahead and support pyproject.toml // as well. // TODO: Might be easier to just use a TOML library to write this out. code.openFile('pyproject.toml'); code.line('[build-system]'); const buildTools = fs .readFileSync(requirementsFile, { encoding: 'utf-8' }) .split('\n') .map((line) => /^\s*(.+)\s*#\s*build-system\s*$/.exec(line)?.[1]?.trim()) .reduce( (buildTools, entry) => (entry ? [...buildTools, entry] : buildTools), new Array<string>(), ); code.line(`requires = [${buildTools.map((x) => `"${x}"`).join(', ')}]`); code.line('build-backend = "setuptools.build_meta"'); code.line(); code.line('[tool.pyright]'); code.line('defineConstant = { DEBUG = true }'); code.line('pythonVersion = "3.9"'); code.line('pythonPlatform = "All"'); code.line('reportSelfClsParameterName = false'); code.closeFile('pyproject.toml'); // We also need to write out a MANIFEST.in to ensure that all of our required // files are included. code.openFile('MANIFEST.in'); code.line('include pyproject.toml'); code.closeFile('MANIFEST.in'); } } type FindModuleCallback = (fqn: string) => spec.AssemblyConfiguration; type FindTypeCallback = (fqn: string) => spec.Type; class TypeResolver { private readonly types: Map<string, PythonType>; private readonly boundTo?: string; private readonly boundRe!: RegExp; private readonly moduleName?: string; private readonly moduleRe!: RegExp; private readonly findModule: FindModuleCallback; private readonly findType: FindTypeCallback; public constructor( types: Map<string, PythonType>, findModule: FindModuleCallback, findType: FindTypeCallback, boundTo?: string, moduleName?: string, ) { this.types = types; this.findModule = findModule; this.findType = findType; this.moduleName = moduleName; this.boundTo = boundTo !== undefined ? this.toPythonFQN(boundTo) : boundTo; if (this.moduleName !== undefined) { this.moduleRe = new RegExp( `^(${escapeStringRegexp(this.moduleName)})\\.(.+)$`, ); } if (this.boundTo !== undefined) { this.boundRe = new RegExp( `^(${escapeStringRegexp(this.boundTo)})\\.(.+)$`, ); } } public bind(fqn: string, moduleName?: string): TypeResolver { return new TypeResolver( this.types, this.findModule, this.findType, fqn, moduleName !== undefined ? moduleName.startsWith('.') ? `${this.moduleName}${moduleName}` : moduleName : this.moduleName, ); } public isInModule(typeRef: spec.NamedTypeReference | string): boolean { const pythonType = typeof typeRef !== 'string' ? this.toPythonFQN(typeRef.fqn) : typeRef; return this.moduleRe.test(pythonType); } public isInNamespace(typeRef: spec.NamedTypeReference | string): boolean { const pythonType = typeof typeRef !== 'string' ? this.toPythonFQN(typeRef.fqn) : typeRef; return this.boundRe.test(pythonType); } public getParent(typeRef: spec.NamedTypeReference | string): PythonType { const fqn = typeof typeRef !== 'string' ? typeRef.fqn : typeRef; const matches = /^(.+)\.[^.]+$/.exec(fqn); if (matches == null || !Array.isArray(matches)) { throw new Error(`Invalid FQN: ${fqn}`); } const [, parentFQN] = matches; const parent = this.types.get(parentFQN); if (parent === undefined) { throw new Error(`Could not find parent: ${parentFQN}`); } return parent; } public getDefiningPythonModule( typeRef: spec.NamedTypeReference | string, ): string { const fqn = typeof typeRef !== 'string' ? typeRef.fqn : typeRef; const parent = this.types.get(fqn); if (parent) { let mod = parent; while (!(mod instanceof PythonModule)) { mod = this.getParent(mod.fqn!); } return mod.pythonName; } const matches = /^([^.]+)\./.exec(fqn); if (matches == null || !Array.isArray(matches)) { throw new Error(`Invalid FQN: ${fqn}`); } const [, assm] = matches; return this.findModule(assm).targets!.python!.module; } public getType(typeRef: spec.NamedTypeReference): PythonType { const type = this.types.get(typeRef.fqn); if (type === undefined) { throw new Error(`Could not locate type: "${typeRef.fqn}"`); } return type; } public dereference(typeRef: string | spec.NamedTypeReference): spec.Type { if (typeof typeRef !== 'string') { typeRef = typeRef.fqn; } return this.findType(typeRef); } private toPythonFQN(fqn: string): string { const [assemblyName, ...qualifiedIdentifiers] = fqn.split('.'); const fqnParts: string[] = [ this.findModule(assemblyName).targets!.python!.module, ]; for (const part of qualifiedIdentifiers) { fqnParts.push(toPythonIdentifier(part)); } return fqnParts.join('.'); } } class PythonGenerator extends Generator { private package!: Package; private rootModule?: PythonModule; private readonly types: Map<string, PythonType>; public constructor( private readonly rosetta: RosettaTabletReader, options: GeneratorOptions, ) { super(options); this.code.openBlockFormatter = (s) => `${s}:`; this.code.closeBlockFormatter = (_s) => false; this.types = new Map(); } // eslint-disable-next-line complexity public emitDocString( code: CodeMaker, apiLocation: ApiLocation, docs: spec.Docs | undefined, options: { arguments?: DocumentableArgument[]; documentableItem?: string; trailingNewLine?: boolean; } = {}, ) { if ((!docs || Object.keys(docs).length === 0) && !options.arguments) { return; } docs ??= {}; const lines = new Array<string>(); if (docs.summary) { lines.push(md2rst(renderSummary(docs))); brk(); } else { lines.push(''); } function brk() { if (lines.length > 0 && lines[lines.length - 1].trim() !== '') { lines.push(''); } } function block(heading: string, content: string, doBrk = true) { if (doBrk) { brk(); } const contentLines = md2rst(content).split('\n'); if (contentLines.length <= 1) { lines.push(`:${heading}: ${contentLines.join('')}`.trim()); } else { lines.push(`:${heading}:`); brk(); for (const line of contentLines) { lines.push(line.trim()); } } if (doBrk) { brk(); } } if (docs.remarks) { brk(); lines.push( ...md2rst(this.convertMarkdown(docs.remarks ?? '', apiLocation)).split( '\n', ), ); brk(); } if (options.arguments?.length ?? 0 > 0) { brk(); for (const param of options.arguments!) { // Add a line for every argument. Even if there is no description, we need // the docstring so that the Sphinx extension can add the type annotations. lines.push( `:param ${toPythonParameterName(param.name)}: ${onelineDescription( param.docs, )}`, ); } brk(); } if (docs.default) { block('default', docs.default); } if (docs.returns) { block('return', docs.returns); } if (docs.deprecated) { block('deprecated', docs.deprecated); } if (docs.see) { block('see', docs.see, false); } if (docs.stability && shouldMentionStability(docs.stability)) { block('stability', docs.stability, false); } if (docs.subclassable) { block('subclassable', 'Yes'); } for (const [k, v] of Object.entries(docs.custom ?? {})) { block(k, v, false); } if (docs.example) { brk(); lines.push('Example::'); lines.push(''); const exampleText = this.convertExample(docs.example, apiLocation); for (const line of exampleText.split('\n')) { lines.push(` ${line}`); } brk(); } while (lines.length > 0 && lines[lines.length - 1] === '') { lines.pop(); } if (lines.length === 0) { return; } if (lines.length === 1) { code.line(`${DOCSTRING_QUOTES}${lines[0]}${DOCSTRING_QUOTES}`); } else { code.line(`${DOCSTRING_QUOTES}${lines[0]}`); lines.splice(0, 1); for (const line of lines) { code.line(line); } code.line(DOCSTRING_QUOTES); } if (options.trailingNewLine) { code.line(); } } public convertExample(example: string, apiLoc: ApiLocation): string { const translated = this.rosetta.translateExample( apiLoc, example, TargetLanguage.PYTHON, enforcesStrictMode(this.assembly), ); return translated.source; } public convertMarkdown(markdown: string, apiLoc: ApiLocation): string { return this.rosetta.translateSnippetsInMarkdown( apiLoc, markdown, TargetLanguage.PYTHON, enforcesStrictMode(this.assembly), ); } public getPythonType(fqn: string): PythonType { const type = this.types.get(fqn); if (type === undefined) { throw new Error(`Could not locate type: "${fqn}"`); } return type; } protected getAssemblyOutputDir(assm: spec.Assembly) { return path.join( 'src', pythonModuleNameToFilename(this.getAssemblyModuleName(assm)), ); } protected onBeginAssembly(assm: spec.Assembly, _fingerprint: boolean) { this.package = new Package( assm.targets!.python!.distName, toReleaseVersion(assm.version, TargetName.PYTHON), assm, ); // This is the '<packagename>._jsii' module for this assembly const assemblyModule = new PythonModule( this.getAssemblyModuleName(assm), undefined, { assembly: assm, assemblyFilename: this.getAssemblyFileName(), loadAssembly: true, package: this.package, }, ); this.package.addModule(assemblyModule); this.package.addData(assemblyModule, this.getAssemblyFileName(), undefined); } protected onEndAssembly(assm: spec.Assembly, _fingerprint: boolean) { const resolver = new TypeResolver( this.types, (fqn: string) => this.findModule(fqn), (fqn: string) => this.findType(fqn), ); this.package.write(this.code, { assembly: assm, emittedTypes: new Set(), resolver, runtimeTypeChecking: this.runtimeTypeChecking, submodule: assm.name, typeCheckingHelper: new TypeCheckingHelper(), typeResolver: (fqn) => resolver.dereference(fqn), }); } /** * Will be called for assembly root, namespaces and submodules (anything that contains other types, based on its FQN) */ protected onBeginNamespace(ns: string) { // 'ns' contains something like '@scope/jsii-calc-base-of-base' const submoduleLike = ns === this.assembly.name ? this.assembly : this.assembly.submodules?.[ns]; const readmeLocation: ApiLocation = { api: 'moduleReadme', moduleFqn: ns }; const module = new PythonModule(toPackageName(ns, this.assembly), ns, { assembly: this.assembly, assemblyFilename: this.getAssemblyFileName(), package: this.package, moduleDocumentation: submoduleLike?.readme ? this.convertMarkdown( submoduleLike.readme?.markdown, readmeLocation, ).trim() : undefined, }); this.package.addModule(module); this.types.set(ns, module); if (ns === this.assembly.name) { // This applies recursively to submodules, so no need to duplicate! this.package.addData(module, 'py.typed', ''); } if (ns === this.assembly.name) { this.rootModule = module; } else { this.rootModule!.addPythonModule(module); } } protected onEndNamespace(ns: string) { if (ns === this.assembly.name) { delete this.rootModule; } } protected onBeginClass(cls: spec.ClassType, abstract: boolean | undefined) { const klass = new Class( this, toPythonIdentifier(cls.name), cls, cls.fqn, { abstract, bases: cls.base ? [this.findType(cls.base)] : undefined, interfaces: cls.interfaces?.map((base) => this.findType(base)), abstractBases: abstract ? this.getAbstractBases(cls) : [], }, cls.docs, ); if (cls.initializer !== undefined) { const { parameters = [] } = cls.initializer; klass.addMember( new Initializer( this, '__init__', undefined, parameters, undefined, cls.initializer.docs, false, // Never static klass, { liftedProp: this.getliftedProp(cls.initializer), parent: cls }, ), ); } this.addPythonType(klass); } protected onStaticMethod(cls: spec.ClassType, method: spec.Method) { const { parameters = [] } = method; const klass = this.getPythonType(cls.fqn); klass.addMember( new StaticMethod( this, toPythonMethodName(method.name), method.name, parameters, method.returns, method.docs, true, // Always static klass, { abstract: method.abstract, liftedProp: this.getliftedProp(method), parent: cls, }, ), ); } protected onStaticProperty(cls: spec.ClassType, prop: spec.Property) { const klass = this.getPythonType(cls.fqn); klass.addMember( new StaticProperty( this, toPythonPropertyName(prop.name, prop.const), prop.name, prop, prop.docs, klass, { abstract: prop.abstract, immutable: prop.immutable, isStatic: prop.static, parent: cls, }, ), ); } protected onMethod(cls: spec.ClassType, method: spec.Method) { const { parameters = [] } = method; const klass = this.getPythonType(cls.fqn); if (method.async) { klass.addMember( new AsyncMethod( this, toPythonMethodName(method.name, method.protected), method.name, parameters, method.returns, method.docs, !!method.static, klass, { abstract: method.abstract, liftedProp: this.getliftedProp(method), parent: cls, }, ), ); } else { klass.addMember( new Method( this, toPythonMethodName(method.name, method.protected), method.name, parameters, method.returns, method.docs, !!method.static, klass, { abstract: method.abstract, liftedProp: this.getliftedProp(method), parent: cls, }, ), ); } } protected onProperty(cls: spec.ClassType, prop: spec.Property) { const klass = this.getPythonType(cls.fqn); klass.addMember( new Property( this, toPythonPropertyName(prop.name, prop.const, prop.protected), prop.name, prop, prop.docs, klass, { abstract: prop.abstract, immutable: prop.immutable, isStatic: prop.static, parent: cls, }, ), ); } protected onUnionProperty( cls: spec.ClassType, prop: spec.Property, _union: spec.UnionTypeReference, ) { this.onProperty(cls, prop); } protected onBeginInterface(ifc: spec.InterfaceType) { let iface: Interface | Struct; if (ifc.datatype) { iface = new Struct( this, toPythonIdentifier(ifc.name), ifc, ifc.fqn, { bases: ifc.interfaces?.map((base) => this.findType(base)) }, ifc.docs, ); } else { iface = new Interface( this, toPythonIdentifier(ifc.name), ifc, ifc.fqn, { bases: ifc.interfaces?.map((base) => this.findType(base)) }, ifc.docs, ); } this.addPythonType(iface); } protected onEndInterface(_ifc: spec.InterfaceType) { return; } protected onInterfaceMethod(ifc: spec.InterfaceType, method: spec.Method) { const { parameters = [] } = method; const klass = this.getPythonType(ifc.fqn); klass.addMember( new InterfaceMethod( this, toPythonMethodName(method.name, method.protected), method.name, parameters, method.returns, method.docs, !!method.static, klass, { liftedProp: this.getliftedProp(method), parent: ifc }, ), ); } protected onInterfaceProperty(ifc: spec.InterfaceType, prop: spec.Property) { let ifaceProperty: InterfaceProperty | StructField; const klass = this.getPythonType(ifc.fqn); if (ifc.datatype) { ifaceProperty = new StructField(this, prop, ifc); } else { ifaceProperty = new InterfaceProperty( this, toPythonPropertyName(prop.name, prop.const, prop.protected), prop.name, prop, prop.docs, klass, { immutable: prop.immutable, isStatic: prop.static, parent: ifc }, ); } klass.addMember(ifaceProperty); } protected onBeginEnum(enm: spec.EnumType) { this.addPythonType( new Enum(this, toPythonIdentifier(enm.name), enm, enm.fqn, {}, enm.docs), ); } protected onEnumMember(enm: spec.EnumType, member: spec.EnumMember) { this.getPythonType(enm.fqn).addMember( new EnumMember( this, toPythonIdentifier(member.name), member.name, member.docs, enm, ), ); } protected onInterfaceMethodOverload( _ifc: spec.InterfaceType, _overload: spec.Method, _originalMethod: spec.Method, ) { throw new Error('Unhandled Type: InterfaceMethodOverload'); } protected onMethodOverload( _cls: spec.ClassType, _overload: spec.Method, _originalMethod: spec.Method, ) { throw new Error('Unhandled Type: MethodOverload'); } protected onStaticMethodOverload( _cls: spec.ClassType, _overload: spec.Method, _originalMethod: spec.Method, ) { throw new Error('Unhandled Type: StaticMethodOverload'); } private getAssemblyModuleName(assm: spec.Assembly): string { return `${assm.targets!.python!.module}._jsii`; } private getParentFQN(fqn: string): string { const m = /^(.+)\.[^.]+$/.exec(fqn); if (m == null) { throw new Error(`Could not determine parent FQN of: ${fqn}`); } return m[1]; } private getParent(fqn: string): PythonType { return this.getPythonType(this.getParentFQN(fqn)); } private addPythonType(type: PythonType) { if (type.fqn == null) { throw new Error('Cannot add a Python type without a FQN.'); } this.getParent(type.fqn).addMember(type); this.types.set(type.fqn, type); } private getliftedProp( method: spec.Method | spec.Initializer, ): spec.InterfaceType | undefined { // If there are parameters to this method, and if the last parameter's type is // a datatype interface, then we want to lift the members of that last paramter // as keyword arguments to this function. if (method.parameters?.length ?? 0 >= 1) { const lastParameter = method.parameters!.slice(-1)[0]; if ( !lastParameter.variadic && spec.isNamedTypeReference(lastParameter.type) ) { const lastParameterType = this.findType(lastParameter.type.fqn); if ( spec.isInterfaceType(lastParameterType) && lastParameterType.datatype ) { return lastParameterType; } } } return undefined; } private getAbstractBases(cls: spec.ClassType): spec.ClassType[] { const abstractBases: spec.ClassType[] = []; if (cls.base !== undefined) { const base = this.findType(cls.base); if (!spec.isClassType(base)) { throw new Error("Class inheritance that isn't a class?"); } if (base.abstract) { abstractBases.push(base); } } return abstractBases; } } /** * Positional argument or keyword parameter */ interface DocumentableArgument { name: string; definingType: spec.Type; docs?: spec.Docs; } /** * Render a one-line description of the given docs, used for method arguments and inlined properties */ function onelineDescription(docs: spec.Docs | undefined) { // Only consider a subset of fields here, we don't have a lot of formatting space if (!docs || Object.keys(docs).length === 0) { return '-'; } const parts = []; if (docs.summary) { parts.push(md2rst(renderSummary(docs))); } if (docs.remarks) { parts.push(md2rst(docs.remarks)); } if (docs.default) { parts.push(`Default: ${md2rst(docs.default)}`); } return parts.join(' ').replace(/\s+/g, ' '); } function shouldMentionStability(s: spec.Stability) { // Don't render "stable" or "external", those are both stable by implication. return s === spec.Stability.Deprecated || s === spec.Stability.Experimental; } function isStruct( typeSystem: reflect.TypeSystem, ref: spec.TypeReference, ): boolean { if (!spec.isNamedTypeReference(ref)) { return false; } const type = typeSystem.tryFindFqn(ref.fqn); return !!(type?.isInterfaceType() && type?.isDataType()); } /** * Appends `_` at the end of `name` until it no longer conflicts with any of the * entries in `inUse`. * * @param name the name to be slugified. * @param inUse the names that are already being used. * * @returns the slugified name. */ function slugifyAsNeeded(name: string, inUse: readonly string[]): string { const inUseSet = new Set(inUse); while (inUseSet.has(name)) { name = `${name}_`; } return name; } //////////////////////////////////////////////////////////////////////////////// // BEHOLD: Helpers to output code that looks like what Black would format into... // // @see https://black.readthedocs.io/en/stable/the_black_code_style.html const TARGET_LINE_LENGTH = 88; function openSignature( code: CodeMaker, keyword: 'class', name: string, params: readonly string[], ): void; function openSignature( code: CodeMaker, keyword: 'def', name: string, params: readonly string[], returnType: string, comment?: string, ): void; function openSignature( code: CodeMaker, keyword: 'class' | 'def', name: string, params: readonly string[], returnType?: string, lineComment?: string, ) { const prefix = `${keyword} ${name}`; const suffix = returnType ? ` -> ${returnType}` : ''; if (params.length === 0) { code.openBlock(`${prefix}${returnType ? '()' : ''}${suffix}`); return; } const join = ', '; const { elementsSize, joinSize } = totalSizeOf(params, join); const hasComments = params.some((param) => /#\s*.+$/.exec(param) != null); if ( !hasComments && TARGET_LINE_LENGTH > code.currentIndentLength + prefix.length + elementsSize + joinSize + suffix.length + 2 ) { code.indent( `${prefix}(${params.join(join)})${suffix}:${ lineComment ? ` # ${lineComment}` : '' }`, ); return; } code.indent(`${prefix}(`); for (const param of params) { code.line(param.replace(/(\s*# .+)?$/, ',$1')); } code.unindent(false); code.indent(`)${suffix}:${lineComment ? ` # ${lineComment}` : ''}`); } /** * Emits runtime type checking code for parameters. * * @param code the CodeMaker to use for emitting code. * @param context the emit context used when emitting this code. * @param params the parameter signatures to be type-checked. * @params pythonName the name of the Python function being checked (qualified). */ function emitParameterTypeChecks( code: CodeMaker, context: EmitContext, params: readonly string[], fqn: string, ): boolean { if (!context.runtimeTypeChecking) { return false; } const paramInfo = params.map((param) => { const [name] = param.split(/\s*[:=#]\s*/, 1); if (name === '*') { return { kwargsMark: true }; } else if (name.startsWith('*')) { return { name: name.slice(1), is_rest: true }; } return { name }; }); const paramNames = paramInfo .filter((param) => param.name != null) .map((param) => param.name!.split(/\s*:\s*/)[0]); const typesVar = slugifyAsNeeded('type_hints', paramNames); let openedBlock = false; for (const { is_rest, kwargsMark, name } of paramInfo) { if (kwargsMark) { if (!context.runtimeTypeCheckKwargs) { // This is the keyword-args separator, we won't check keyword arguments here because the kwargs will be rolled // up into a struct instance, and that struct's constructor will be checking again... break; } // Skip this (there is nothing to be checked as this is just a marker...) continue; } if (!openedBlock) { code.openBlock('if __debug__'); code.line( `${typesVar} = ${context.typeCheckingHelper.getTypeHints(fqn, params)}`, ); openedBlock = true; } let expectedType = `${typesVar}[${JSON.stringify(name)}]`; let comment = ''; if (is_rest) { // This is a vararg, so the value will appear as a tuple. expectedType = `typing.Tuple[${expectedType}, ...]`; // Need to ignore reportGeneralTypeIssues because pyright incorrectly parses that as a type annotation 😒 comment = ' # pyright: ignore [reportGeneralTypeIssues]'; } code.line( `check_type(argname=${JSON.stringify( `argument ${name}`, )}, value=${name}, expected_type=${expectedType})${comment}`, ); } if (openedBlock) { code.closeBlock(); return true; } // We did not reference type annotations data if we never opened a type-checking block. return false; } function assignCallResult( code: CodeMaker, variable: string, funct: string, params: readonly string[], ) { const prefix = `${variable} = ${funct}(`; const suffix = ')'; if (params.length === 0) { code.line(`${prefix}${suffix}`); return; } const join = ', '; const { elementsSize, joinSize } = totalSizeOf(params, join); if ( TARGET_LINE_LENGTH > code.currentIndentLength + prefix.length + elementsSize + joinSize + suffix.length ) { code.line(`${prefix}${params.join(join)}${suffix}`); return; } code.indent(prefix); if (TARGET_LINE_LENGTH > code.currentIndentLength + elementsSize + joinSize) { code.line(params.join(join)); } else { for (const param of params) { code.line(`${param},`); } } code.unindent(suffix); } function assignDictionary( code: CodeMaker, variable: string, elements: readonly string[], trailing?: string, compact = false, ): void { const space = compact ? '' : ' '; const prefix = `${variable}${space}=${space}{`; const suffix = `}${trailing ?? ''}`; if (elements.length === 0) { code.line(`${prefix}${suffix}`); return; } if (compact) { const join = ', '; const { elementsSize, joinSize } = totalSizeOf(elements, join); if ( TARGET_LINE_LENGTH > prefix.length + code.currentIndentLength + elementsSize + joinSize + suffix.length ) { code.line(`${prefix}${elements.join(join)}${suffix}`); return; } } code.indent(prefix); for (const elt of elements) { code.line(`${elt},`); } code.unindent(suffix); } function emitList( code: CodeMaker, prefix: string, elements: readonly string[], suffix: string, opts?: { ifMulti: [string, string] }, ) { if (elements.length === 0) { code.line(`${prefix}${suffix}`); return; } const join = ', '; const { elementsSize, joinSize } = totalSizeOf(elements, join); if ( TARGET_LINE_LENGTH > code.currentIndentLength + prefix.length + elementsSize + joinSize + suffix.length ) { code.line(`${prefix}${elements.join(join)}${suffix}`); return; } const [before, after] = opts?.ifMulti ?? ['', '']; code.indent(`${prefix}${before}`); if (elements.length === 1) { code.line(elements[0]); } else { if ( TARGET_LINE_LENGTH > code.currentIndentLength + elementsSize + joinSize ) { code.line(elements.join(join)); } else { for (const elt of elements) { code.line(`${elt},`); } } } code.unindent(`${after}${suffix}`); } function totalSizeOf(strings: readonly string[], join: string) { return { elementsSize: strings .map((str) => str.length) .reduce((acc, elt) => acc + elt, 0), joinSize: strings.length > 1 ? join.length * (strings.length - 1) : 0, }; } function nestedContext( context: EmitContext, fqn: string | undefined, ): EmitContext { return { ...context, surroundingTypeFqns: fqn != null ? [...(context.surroundingTypeFqns ?? []), fqn] : context.surroundingTypeFqns, }; } const isDeprecated = (x: PythonBase) => x.docs?.deprecated !== undefined; /** * Last component of a .-separated name */ function lastComponent(n: string) { const parts = n.split('.'); return parts[parts.length - 1]; }