-
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
feat(python): idiomatic capitalization for structs #586
Conversation
This change addresses 4 issues: - Structs now use idiomatic (snake_case) capitalization of fields (instead of JSII-inherited camelCase). - IDE support -- replace TypedDict usage with regular classes. This makes it so that we can't use dicts anymore, but mypy support in IDEs wasn't great and by using POPOs (Plain Old Python Objects) IDEs get their support back. - Structs in a variadic argument use to be incorrectly lifted to keyword arguments, this no longer happens. - Stop emitting "Stable" stabilities in docstrings, "Stable" is implied. In order to make this change, I've had to make `jsii-pacmak` depend on `jsii-reflect`. This is the proper layering of libraries anyway, since `jsii-reflect` adds much-needed--and otherwise duplicated--interpretation of the spec. Complete refactoring of `jsii-pacmak` to build on `jsii-reflect` is too much work right now, however, so I've added "escape hatching" where generators can take advantage of the power of jsii-reflect if they want to, but most of the code still works on the raw spec level. Added a refactoring where we load the assembly once and reuse the same instance for all generators, instead of loading the assembly for every generator. Assembly-loading, especially with a lot of dependencies, takes a non-negligible amount of time, so this has the side effect of making the packaging step faster (shaves off 100 packages * 3 targets * a couple of seconds).
For reference, structs now look like this: @jsii.data_type(jsii_type="jsii-calc.TopLevelStruct", jsii_struct_bases=[], name_mapping={'required': 'required', 'second_level': 'secondLevel', 'optional': 'optional'})
class TopLevelStruct:
def __init__(self, *, required: str, second_level: typing.Union[jsii.Number, "SecondLevelStruct"], optional: typing.Optional[str]=None):
"""
Arguments:
required: This is a required field.
second_level: A union to really stress test our serialization.
optional: You don't have to pass this.
Stability:
experimental
"""
self._values = {
'required': required,
'second_level': second_level,
}
if optional is not None: self._values["optional"] = optional
@property
def required(self) -> str:
"""This is a required field.
Stability:
experimental
"""
return self._values.get('required')
@property
def second_level(self) -> typing.Union[jsii.Number, "SecondLevelStruct"]:
"""A union to really stress test our serialization.
Stability:
experimental
"""
return self._values.get('second_level')
@property
def optional(self) -> typing.Optional[str]:
"""You don't have to pass this.
Stability:
experimental
"""
return self._values.get('optional')
def _to_jsii(self) -> dict:
return {
'required': self.required,
'secondLevel': self.second_level,
'optional': self.optional,
}
def __eq__(self, rhs) -> bool:
return isinstance(rhs, self.__class__) and rhs._values == self._values
def __ne__(self, rhs) -> bool:
return not (rhs == self)
def __repr__(self) -> str:
return 'TopLevelStruct(%s)' % ', '.join(k + '=' + repr(v) for k, v in self._values.items()) |
And a jsii function now looks like this: @jsii.member(jsii_name="roundTrip")
@classmethod
def round_trip(cls, _positional: jsii.Number, *, required: str, second_level: typing.Union[jsii.Number, "SecondLevelStruct"], optional: typing.Optional[str]=None) -> "TopLevelStruct":
"""
Arguments:
_positional: -
input: -
required: This is a required field.
second_level: A union to really stress test our serialization.
optional: You don't have to pass this.
Stability:
experimental
"""
input = TopLevelStruct(required=required, second_level=second_level, optional=optional)
return jsii.sinvoke(cls, "roundTrip", jsii.structs_to_jsii([_positional, input]))
|
… with unmapping Remove now-unused dependency on mypy_extensions.
@@ -101,18 +101,29 @@ def _recursize_dereference(kernel, d): | |||
def _dereferenced(fn): | |||
@functools.wraps(fn) | |||
def wrapped(kernel, *args, **kwargs): | |||
return _recursize_dereference(kernel, fn(kernel, *args, **kwargs)) | |||
output = fn(kernel, *args, **kwargs) | |||
print(output) |
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.
probably don't want to leave this in there
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.
Tisk :)
The last commit, switching to ReST-style docstrings (in order to help IDEs out, poor things!) is speculative. It might be better with, or without. Any reviewers/mergers/editors of this PR should feel fully free to drop and/or amend it :). |
There was a problem with ctors that take no parameters. It was getting generated as: class foo():
def __init__(self, *): but you can't have a bare asterisk that isn't followed by any params. I made this change:
and tested it and it worked. There may be a better way to do it in TypeScript. I don't think I can push the change here myself. |
Common mixed-case abbrevations will be snake-cased as a while, instead of being broken up into separate word parts.
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.
I think we need support for struct inheritance...
// | ||
// These functions exist to break the isolation barrier until we have time | ||
// to rewrite the code to properly use jsii-reflect. | ||
|
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.
Why not just expose spec as public in Jsii-reflect?
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.
I went down that path first, but I think it's the wrong way round. Don't feel that spec should be exposed.
|
||
public addMember(member: PythonBase): void { | ||
if (!(member instanceof StructField)) { | ||
throw new Error('Must add TypedDictProperties to ValueType'); |
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.
I am assuming this is StructField
, no?
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.
Oh yes
// and one with all optional keys in order to emulate this. So we'll go ahead | ||
// and implement this "split" class logic. | ||
// Not actually rendering these as class params, | ||
// we splat the entire |
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.
"the entire..." what?
// Required properties, those will always be put into the dict | ||
code.line('self._values = {'); | ||
for (const member of members.filter(m => !m.optional)) { | ||
code.line(` '${member.pythonName}': ${member.pythonName},`); |
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 we need to validate that we actually got values in all required fields or are we leaving that to jsii?
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.
Fair point. It's easy enough to add, might as well do it.
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.
Ah no, we don't have to, Python will complain. The constructor looks like this:
def __init__(self, *, required, optional=None):
...
So you MUST pass a value for required.
bar: str | ||
@jsii.data_type(jsii_type="@scope/jsii-calc-base.BaseProps", jsii_struct_bases=[scope.jsii_calc_base_of_base.VeryBaseProps], name_mapping={'foo': 'foo', 'bar': 'bar'}) | ||
class BaseProps: | ||
def __init__(self, *, foo: scope.jsii_calc_base_of_base.Very, bar: str): |
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.
Why do we need the "*"?
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.
It makes it so you can't pass any of the following values without using the keyword. Which is required, otherwise adding optional parameters later on might change the order which is breaking.
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.
Oh wow. nice!
anotherOptional: typing.Mapping[str,scope.jsii_calc_lib.Value] | ||
"""This is optional. | ||
@jsii.data_type(jsii_type="jsii-calc.DerivedStruct", jsii_struct_bases=[scope.jsii_calc_lib.MyFirstStruct], name_mapping={'anumber': 'anumber', 'astring': 'astring', 'first_optional': 'firstOptional', 'another_required': 'anotherRequired', 'bool': 'bool', 'non_primitive': 'nonPrimitive', 'another_optional': 'anotherOptional', 'optional_any': 'optionalAny', 'optional_array': 'optionalArray'}) | ||
class DerivedStruct: |
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.
Don't we want this to inherit from MyFirstStruct
? Did we test struct inheritence?
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.
Not even sure what that means.
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.
Okay it will be used for nominal typing, probably.
@@ -141,7 +141,7 @@ def jdefault(obj): | |||
return {"$jsii.date": obj.isoformat()} | |||
elif isinstance(obj, datetime.datetime): | |||
raise TypeError("Naive datetimes are not supported, please add a timzone.") | |||
raise TypeError | |||
raise TypeError("Don't know how to convert object to JSON: %r" % obj) |
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.
YAS!
@garnaat please review |
LGTM |
This change addresses 4 issues:
(instead of JSII-inherited camelCase).
makes it so that we can't use dicts anymore, but mypy support in
IDEs wasn't great and by using POPOs (Plain Old Python Objects)
IDEs get their support back.
keyword arguments, this no longer happens.
In order to make this change, I've had to make
jsii-pacmak
depend onjsii-reflect
. This is the proper layering of libraries anyway, sincejsii-reflect
adds much-needed--and otherwiseduplicated--interpretation of the spec.
Complete refactoring of
jsii-pacmak
to build onjsii-reflect
is toomuch work right now, however, so I've added "escape hatching" where
generators can take advantage of the power of jsii-reflect if they want
to, but most of the code still works on the raw spec level.
Added a refactoring where we load the assembly once and reuse the same
instance for all generators, instead of loading the assembly for every
generator. Assembly-loading, especially with a lot of dependencies, takes
a non-negligible amount of time, so this has the side effect of making
the packaging step faster (shaves off 100 packages * 3 targets * a
couple of seconds).
Fixes #537
Fixes #577
Fixes #578
Fixes #588
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.