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.
| 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.
probably don't want to leave this in there
|
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.
eladb
left a comment
There was a problem hiding this comment.
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.
Why not just expose spec as public in Jsii-reflect?
There was a problem hiding this comment.
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.
I am assuming this is StructField, no?
| // 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 |
| // 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.
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.
Fair point. It's easy enough to add, might as well do it.
There was a problem hiding this comment.
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.
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.
| 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.
Don't we want this to inherit from MyFirstStruct? Did we test struct inheritence?
There was a problem hiding this comment.
Not even sure what that means.
There was a problem hiding this comment.
Okay it will be used for nominal typing, probably.
| 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) |
|
@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-pacmakdepend onjsii-reflect. This is the proper layering of libraries anyway, sincejsii-reflectadds much-needed--and otherwiseduplicated--interpretation of the spec.
Complete refactoring of
jsii-pacmakto build onjsii-reflectis 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.