-
Notifications
You must be signed in to change notification settings - Fork 245
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
Changes from 8 commits
4b8af49
fa291d7
3180d97
1a9297c
c4c652d
a162358
59c7876
ad57330
0e4225f
89e4422
15546d7
6b1cbd6
882e5c7
0e34a80
5ea945d
8275944
50ad810
21f7ecb
e2708e0
340f8e9
ea14b3c
2f93077
9d6362f
0cb0ad5
90b8ebe
507f787
653d774
52f62d8
0408cb3
b8b40c0
848ed7a
855c2a8
d4b1fe8
322f04f
6f636dd
78d986d
ea77363
4bf7672
99fa04d
3409850
912a593
be62f7c
41b7025
44c43f4
1807128
bac9dc7
379d831
4fdda68
8f93d69
18ba6e6
83929ef
86b92fb
98875ba
dd3e31d
4e9fc08
f1c6986
721f677
9e03851
79b4d4d
67058b9
f5f0ec5
a2d6c50
99278c9
e75585c
530f6c7
e9b0250
7edc04e
771e923
25def60
a000fb7
a964267
6144260
4575e85
366c44b
7505b32
70d15d0
e76d4aa
1288b03
efcded0
7e7bbc7
6a2ec91
22d2a34
2987f36
72fb08d
785ce86
413ec40
90618b2
db2e266
4754fed
4cdec20
620d850
55f297e
3c39eb1
1702ca8
cd16d00
b01bf3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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], {}); | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it not make sense to push this change upstream into CodeMaker? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be sufficient to generate an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
} | ||
} |
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" | ||
} |
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"] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.