Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Python JSII Support #219

Closed
wants to merge 96 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
4b8af49
Initial Python Runtime Support
dstufft Aug 21, 2018
fa291d7
Bump versions
dstufft Sep 5, 2018
3180d97
Upgrade for latest JSII
dstufft Sep 11, 2018
1a9297c
Handle non-primitive types being returned from JSII runtime
dstufft Sep 11, 2018
c4c652d
Switch to using a single, global Kernel
dstufft Sep 20, 2018
a162358
Rearrange imports to deal with import cycles
dstufft Sep 20, 2018
59c7876
Provide a mechanism to load JSII Assemblies
dstufft Sep 20, 2018
ad57330
Add the WIP Python target for pacmak
dstufft Sep 20, 2018
0e4225f
Prefix all of our interal names with _
dstufft Sep 21, 2018
89e4422
Write out an __all__ that lists our exported names
dstufft Sep 21, 2018
15546d7
Add support for more types
dstufft Sep 21, 2018
6b1cbd6
Fix some MyPy errors
dstufft Sep 21, 2018
882e5c7
Generate actual Python packages
dstufft Sep 21, 2018
0e34a80
Correct the name of the import JSIIAssembly
dstufft Sep 21, 2018
5ea945d
Produce packages for the JSII Python Runtime
dstufft Sep 21, 2018
8275944
Embed the jsii-runtime within the jsii-python-runtime
dstufft Sep 21, 2018
50ad810
Merge branch 'master' into python
dstufft Sep 25, 2018
21f7ecb
Handle collection types in JSII
dstufft Sep 28, 2018
e2708e0
Handle scoped packages
dstufft Sep 28, 2018
340f8e9
Handle the case where a JSII name conflicts with a Python keyword
dstufft Sep 28, 2018
ea14b3c
Work with PEP 420 style namespace pacages
dstufft Sep 28, 2018
2f93077
Deduplicate imports
dstufft Sep 28, 2018
9d6362f
Remove some debug output
dstufft Sep 28, 2018
0cb0ad5
Add support for Union types
dstufft Sep 28, 2018
90b8ebe
Add support for interfaces
dstufft Sep 28, 2018
507f787
Fix setup.py to always include JSII assembly
dstufft Sep 29, 2018
653d774
Handle recurisve generic collection types
dstufft Sep 29, 2018
52f62d8
Handle classes and interfaces without bodies
dstufft Sep 29, 2018
0408cb3
Use a better mechanism of extracting a list of types
dstufft Sep 29, 2018
b8b40c0
Add a bit of extra documentation
dstufft Sep 29, 2018
848ed7a
Bump version
dstufft Sep 29, 2018
855c2a8
Fix formatting to handle nested types
dstufft Sep 29, 2018
d4b1fe8
Refactor various toPythonXXX functions into stand alone functions
dstufft Sep 30, 2018
322f04f
Drastic refactoring to delay emiting lines until the very end
dstufft Sep 30, 2018
6f636dd
Remove code duplication amongst the many Method types
dstufft Sep 30, 2018
78d986d
Remove duplication from property types
dstufft Sep 30, 2018
ea77363
Ensure we generate inherited from classes before we inherit from them
dstufft Sep 30, 2018
4bf7672
Re-enable tslint on python.ts
dstufft Sep 30, 2018
99fa04d
Cleanup interfaces for the nodes
dstufft Sep 30, 2018
3409850
Fix sorting by dependency
dstufft Sep 30, 2018
912a593
Support passing arguments to the class constructor
dstufft Oct 10, 2018
be62f7c
Handle FQN that contain a @ sign
dstufft Oct 10, 2018
41b7025
Merge branch 'master' into python
dstufft Oct 10, 2018
44c43f4
Update the JSII Provider Version Number
dstufft Oct 10, 2018
1807128
Merge branch 'master' into python
dstufft Oct 16, 2018
bac9dc7
Interfaces only need to declare dependencies in the same module
dstufft Oct 17, 2018
379d831
Implement Enums in the Python generator
dstufft Oct 17, 2018
4fdda68
Default to None if there is no result from an Invoke
dstufft Oct 17, 2018
8f93d69
Fix a spelling mistake
dstufft Oct 17, 2018
18ba6e6
Make sure all dependencies have been imported
dstufft Oct 18, 2018
83929ef
Magical Removal
dstufft Oct 20, 2018
86b92fb
Enable Typechecking installed JSII packages
dstufft Oct 20, 2018
98875ba
Implement Python name mangling
dstufft Oct 20, 2018
dd3e31d
Refactor to remove duplication
dstufft Oct 20, 2018
4e9fc08
Always use the global kernel instead of the __jsii_kernel__ indrection
dstufft Oct 21, 2018
f1c6986
Enable subclassing of JSII Classes
dstufft Oct 21, 2018
721f677
Correct the call to the super class
dstufft Oct 21, 2018
9e03851
Use real __init__ methods to enable sublcassing of JSII classes
dstufft Oct 21, 2018
79b4d4d
Make sure that classes inherent from their bases and interfaces
dstufft Oct 21, 2018
67058b9
Correctly limit dependencies to this module, even when faced with sub…
dstufft Oct 21, 2018
f5f0ec5
Implement Dataclasses as TypedDicts
dstufft Oct 21, 2018
a2d6c50
Support typing the JSII package
dstufft Oct 21, 2018
99278c9
Better handle numbers
dstufft Oct 21, 2018
e75585c
Correctly emit class bases
dstufft Oct 21, 2018
530f6c7
Don't inhereit from Interfaces
dstufft Oct 21, 2018
e9b0250
These can be private
dstufft Oct 21, 2018
7edc04e
Handle lifting a data type into keyword arguments
dstufft Oct 21, 2018
771e923
Handle Mandatory/Optional data type members
dstufft Oct 21, 2018
25def60
Use publication to hide implementation details from end users
dstufft Oct 22, 2018
a000fb7
Refactor module dependency to use Assembly metadata
dstufft Oct 19, 2018
a964267
Handle variadic parameters
dstufft Oct 22, 2018
6144260
Merge branch 'master' into python
dstufft Oct 23, 2018
4575e85
Massive refactor to greatly simplify Python generator
dstufft Oct 24, 2018
366c44b
Fix a bug with non-builtin but standard types
dstufft Oct 24, 2018
7505b32
Make sure that __jsii_assembly__ is in __all__
dstufft Oct 24, 2018
70d15d0
Deterministic regex
dstufft Oct 25, 2018
e76d4aa
Fix support for imports from submodules
dstufft Oct 25, 2018
1288b03
Correctly pass in the klass argument to @classproperty properties
dstufft Oct 25, 2018
efcded0
Emit a None default for optional positional params
dstufft Oct 25, 2018
7e7bbc7
Don't add typing.Optional to the auto props expansion
dstufft Oct 25, 2018
6a2ec91
Use "real" forward refs if possible, even with forwardReferences = false
dstufft Oct 25, 2018
22d2a34
Implement Abstract classes
dstufft Oct 25, 2018
2987f36
Don't trust whether the JSII tells us a parameter is optional or not
dstufft Oct 26, 2018
72fb08d
Don't include Optional when emitting Any
dstufft Oct 26, 2018
785ce86
Revert "Use "real" forward refs if possible, even with forwardReferen…
dstufft Oct 26, 2018
413ec40
Ensure all of regexp strings are correctly escaped
dstufft Oct 26, 2018
90618b2
Merge branch 'master' into python
dstufft Oct 29, 2018
db2e266
Move Namespaces as classes instead of modules
dstufft Nov 1, 2018
4754fed
Explicitly use Python3
dstufft Nov 1, 2018
4cdec20
Handle return values that are data types
dstufft Nov 1, 2018
620d850
Generate Proxy class to use when JSII runtime returns an abstract class
dstufft Nov 2, 2018
55f297e
Pass overriden methods/properties into the JSII runtime
dstufft Nov 2, 2018
3c39eb1
Merge branch 'master' into python
dstufft Nov 9, 2018
1702ca8
Merge branch 'master' into python
dstufft Nov 9, 2018
cd16d00
Merge branch 'master' into python
dstufft Nov 15, 2018
b01bf3d
Deal with name collisions
dstufft Nov 15, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[flake8]
max-line-length = 88
exclude = *.egg,*/interfaces.py,node_modules,.state
ignore = W503,E203
select = E,W,F,N
344 changes: 344 additions & 0 deletions packages/jsii-pacmak/lib/targets/python.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
/* tslint:disable */
import path = require('path');
import util = require('util');

import * as spec from 'jsii-spec';
import { Generator, GeneratorOptions } from '../generator';
import { Target, TargetOptions } from '../target';
import { CodeMaker } from 'codemaker';
import { shell } from '../util';

export default class Python extends Target {
protected readonly generator = new PythonGenerator();

constructor(options: TargetOptions) {
super(options);
}

public async build(sourceDir: string, outDir: string): Promise<void> {
// Format our code to make it easier to read, we do this here instead of trying
// to do it in the code generation phase, because attempting to mix style and
// function makes the code generation harder to maintain and read, while doing
// this here is easy.
await shell("black", ["--py36", sourceDir], {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan to keep this for production or is this just for developing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent was to keep it for production, although I don't think it's a requirement if people feel strongly otherwise. One of my goals was that people could easily see the structure of an API by reading the generated code (obviously not the actual implementation) and formatting the code makes it much more pleasant to read, but emiting formatted code from the CodeMaker instance is annoying since you have to split logical lines over multiple lines, depending on the length of the line.


return this.copyFiles(sourceDir, outDir);
}
}

// ##################
// # CODE GENERATOR #
// ##################
const debug = function(o: any) {
console.log(util.inspect(o, false, null, true));
}


class Module {

readonly name: string;
readonly assembly?: spec.Assembly;
readonly assemblyFilename?: string;

private buffer: object[];

constructor(ns: string, assembly?: [spec.Assembly, string]) {
this.name = ns;

if (assembly != undefined) {
this.assembly = assembly[0];
this.assemblyFilename = assembly[1];
}

this.buffer = [];
}

// We're purposely replicating the API of CodeMaker here, because CodeMaker cannot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make sense to push this change upstream into CodeMaker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably would yea, I duplicated it here primarily for expediency, and because I don't feel wholly comfortable submitting code to an OSS JS project since I'm not super familiar with the ecosystem. Although having duplicated it here will make some things easier in general (for instance, when I get support for typing Named types, being able to add imports for the named types, which we won't know what types to emit imports for until we've hit a onMethod or similar that has used them).

// Operate on more than one file at a time, so we have to buffer our calls to
// CodeMaker, otherwise we can end up in inconsistent state when we get things like:
// - onBeginNamespace(foo)
// - onBeginNamespace(foo.bar)
// - OnEndNamespace(foo.bar)
// - Inconsitent State, where we're now back in the scope of foo, but due to
// the fact that if we had opened a file in onBeginNamespace(foo), we would
// have had to close it for onBeginNamespace(foo.bar), and re-opening it
// would overwrite the contents.
// - OnEndNamespace(foo)
// To solve this, we buffer all of the things we *would* have written out via
// CodeMaker via this API, and then we will just iterate over it in the
// onEndNamespace event and write it out then.

public line(...args: any[]) {
this.buffer.push({method: "line", args: args});
}

public indent(...args: any[]) {
this.buffer.push({method: "indent", args: args});
}

public unindent(...args: any[]) {
this.buffer.push({method: "unindent", args: args});
}

public open(...args: any[]) {
this.buffer.push({method: "open", args: args});
}

public close(...args: any[]) {
this.buffer.push({method: "close", args: args});
}

public openBlock(...args: any[]) {
this.buffer.push({method: "openBlock", args: args});
}

public closeBlock(...args: any[]) {
this.buffer.push({method: "closeBlock", args: args});
}

public write(code: CodeMaker) {
// Before we do Anything, we need to write out our module headers, this is where
// we handle stuff like imports, any required initialization, etc.
code.line("from jsii.runtime import JSIIAssembly, JSIIMeta, jsii_method, jsii_property, jsii_classmethod, jsii_classproperty")

// Determine if we need to write out the kernel load line.
if (this.assembly && this.assemblyFilename) {
code.line(`__jsii_assembly__ = JSIIAssembly.load("${this.assembly.name}", "${this.assembly.version}", __name__, "${this.assemblyFilename}")`);
}

// Now that we've gotten all of the module header stuff done, we need to go
// through and actually write out the meat of our module.
for (let buffered of this.buffer) {
let methodName = (buffered as any)["method"] as string;
let args = (buffered as any)["args"] as any[];

(code as any)[methodName](...args);
}
}
}

class PythonGenerator extends Generator {

private moduleStack: Module[];

constructor(options = new GeneratorOptions()) {
super(options);

this.code.openBlockFormatter = s => `${s}:`;
this.code.closeBlockFormatter = _s => "";

this.moduleStack = [];
}

protected getAssemblyOutputDir(mod: spec.Assembly) {
return path.join("src", this.toPythonModuleName(mod.name), "_jsii");
}

protected onBeginAssembly(assm: spec.Assembly, _fingerprint: boolean) {
debug("onBeginAssembly");

// We need to write out an __init__.py for our _jsii package so that
// importlib.resources will be able to load our assembly from it.
const assemblyInitFilename = path.join(this.getAssemblyOutputDir(assm), "__init__.py");

this.code.openFile(assemblyInitFilename);
this.code.closeFile(assemblyInitFilename);
}

protected onBeginNamespace(ns: string) {
debug(`onBeginNamespace: ${ns}`);

const moduleName = this.toPythonModuleName(ns);
const loadAssembly = this.assembly.name == ns ? true : false;

let moduleArgs: any[] = [];

if (loadAssembly) {
moduleArgs.push([this.assembly, this.getAssemblyFileName()]);
}

this.moduleStack.push(new Module(moduleName, ...moduleArgs));
}

protected onEndNamespace(ns: string) {
debug(`onEndNamespace: ${ns}`);

let module = this.moduleStack.pop() as Module;
let moduleFilename = path.join("src", this.toPythonModuleFilename(module.name), "__init__.py");

this.code.openFile(moduleFilename);
module.write(this.code);
this.code.closeFile(moduleFilename);
}

protected onBeginClass(cls: spec.ClassType, abstract: boolean | undefined) {
debug("onBeginClass");

// TODO: Figure out what to do with abstract here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be sufficient to generate an __init__ method that throws?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which immediately makes it useless for anything other than type annotations. Speaking of, are you planning to include those?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left that as a note to myself because I wasn't sure what the semantics of an abstract class was supposed to be. Are people supposed to be able to subclass it? If so an ABC would probably be appropriate.

abstract;

this.currentModule().openBlock(`class ${cls.name}(metaclass=JSIIMeta, jsii_type="${cls.fqn}")`);
}

protected onEndClass(_cls: spec.ClassType) {
debug("onEndClass");

this.currentModule().closeBlock();
}

protected onStaticMethod(_cls: spec.ClassType, method: spec.Method) {
debug("onStaticMethod");

// TODO: Handle the case where the Python name and the JSII name differ.
this.currentModule().line("@jsii_classmethod");
this.emitPythonMethod(method.name, "cls", method.parameters, method.returns);
}

protected onMethod(_cls: spec.ClassType, method: spec.Method) {
debug("onMethod");

this.currentModule().line("@jsii_method");
this.emitPythonMethod(method.name, "self", method.parameters, method.returns);
}

protected onStaticProperty(_cls: spec.ClassType, prop: spec.Property) {
debug("onStaticProperty");

// TODO: Properties have a bunch of states, they can have getters and setters
// we need to better handle all of these cases.
this.currentModule().line("@jsii_classproperty");
this.emitPythonMethod(prop.name, "self", [], prop.type);
}

protected onProperty(_cls: spec.ClassType, prop: spec.Property) {
debug("onProperty");

this.currentModule().line("@jsii_property");
this.emitPythonMethod(prop.name, "self", [], prop.type);
}

private emitPythonMethod(name?: string, implicitParam?: string, params: spec.Parameter[] = [], returns?: spec.TypeReference) {
// TODO: Handle imports (if needed) for type.
const returnType = returns ? this.toPythonType(returns) : "None";

// 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.
// TODO: Handle imports (if needed) for all of these types.

let pythonParams: string[] = implicitParam ? [implicitParam] : [];
for (let param of params) {
pythonParams.push(`${param.name}: ${this.toPythonType(param.type)}`);
}

let module = this.currentModule();

module.openBlock(`def ${name}(${pythonParams.join(", ")}) -> ${returnType}`);
module.line("...");
module.closeBlock();
}

private toPythonType(typeref: spec.TypeReference): string {
if (spec.isPrimitiveTypeReference(typeref)) {
return this.toPythonPrimitive(typeref.primitive);
} else if (spec.isNamedTypeReference(typeref)) {
// TODO: We need to actually handle this, isntead of just returning the FQN
// as a string.
return `"${typeref.fqn}"`;
} else {
throw new Error("Invalid type reference: " + JSON.stringify(typeref));
}

/*
if (spec.isPrimitiveTypeReference(typeref)) {
return [ this.toJavaPrimitive(typeref.primitive) ];
} else if (spec.isCollectionTypeReference(typeref)) {
return [ this.toJavaCollection(typeref, forMarshalling) ];
} else if (spec.isNamedTypeReference(typeref)) {
return [ this.toNativeFqn(typeref.fqn) ];
} else if (typeref.union) {
const types = new Array<string>();
for (const subtype of typeref.union.types) {
for (const t of this.toJavaTypes(subtype, forMarshalling)) {
types.push(t);
}
}
return types;
} else {
throw new Error('Invalid type reference: ' + JSON.stringify(typeref));
}
*/

/*
switch (primitive) {
case spec.PrimitiveType.Boolean: return 'java.lang.Boolean';
case spec.PrimitiveType.Date: return 'java.time.Instant';
case spec.PrimitiveType.Json: return 'com.fasterxml.jackson.databind.node.ObjectNode';
case spec.PrimitiveType.Number: return 'java.lang.Number';
case spec.PrimitiveType.String: return 'java.lang.String';
case spec.PrimitiveType.Any: return 'java.lang.Object';
default:
throw new Error('Unknown primitive type: ' + primitive);
}
*/
}

private toPythonPrimitive(primitive: spec.PrimitiveType): string {
switch (primitive) {
case spec.PrimitiveType.String:
return "str";
default:
throw new Error("Unknown primitive type: " + primitive);
}
}

private currentModule(): Module {
return this.moduleStack.slice(-1)[0];
}

private toPythonModuleName(name: string): string {
return this.code.toSnakeCase(name.replace(/-/g, "_"));
}

private toPythonModuleFilename(name: string): string {
return name.replace(/\./g, "/");
}

// Not Currently Used

protected onEndAssembly(_assm: spec.Assembly, _fingerprint: boolean) {
debug("onEndAssembly");
}

protected onBeginInterface(_ifc: spec.InterfaceType) {
debug("onBeginInterface");
}

protected onEndInterface(_ifc: spec.InterfaceType) {
debug("onEndInterface");
}

protected onInterfaceMethod(_ifc: spec.InterfaceType, _method: spec.Method) {
debug("onInterfaceMethod");
}

protected onInterfaceMethodOverload(_ifc: spec.InterfaceType, _overload: spec.Method, _originalMethod: spec.Method) {
debug("onInterfaceMethodOverload");
}

protected onInterfaceProperty(_ifc: spec.InterfaceType, _prop: spec.Property) {
debug("onInterfaceProperty");
}

protected onUnionProperty(_cls: spec.ClassType, _prop: spec.Property, _union: spec.UnionTypeReference) {
debug("onUnionProperty");
}

protected onMethodOverload(_cls: spec.ClassType, _overload: spec.Method, _originalMethod: spec.Method) {
debug("onMethodOverload");
}

protected onStaticMethodOverload(_cls: spec.ClassType, _overload: spec.Method, _originalMethod: spec.Method) {
debug("onStaticMethodOverload");
}
}
24 changes: 24 additions & 0 deletions packages/jsii-python-runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "jsii-python-runtime",
"version": "0.0.0",
"description": "Python client for jsii runtime",
"main": "index.js",
"scripts": {
"build": "echo ok",
"prepack": "echo ok",
"test": "echo ok"
},
"dependencies": {
"jsii-runtime": "^0.7.1"
},
"repository": {
"type": "git",
"url": "git://github.com/awslabs/jsii"
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com",
"organization": true
},
"license": "Apache-2.0"
}
Empty file.
7 changes: 7 additions & 0 deletions packages/jsii-python-runtime/src/jsii/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
try:
import importlib.resources as importlib_resources
except ImportError:
import importlib_resources


__all__ = ["importlib_resources"]
Loading