-
Notifications
You must be signed in to change notification settings - Fork 197
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
Python SDK: Enum Support #242
Conversation
josephaxisa
commented
Jun 22, 2020
- Made model attributes of type enum string forward references with a registered structure hook
- Added a unit test for a model containing an enum type attribute
- Broke up structure_hook into reserved_kw_structure_hook and forward_ref_structure_hook
Enums are now fully implemented for Typescript, Swift, Kotlin, and C# Python needs a little bit more for full implementation, which will be completed in #242 * Added enum generation for all languages * Enums are not overwritten and are automatically renamed if the values for the enum vary from another enum of the same name * Miscellaneous - replacing `x-looker-values` with `enum` in converted OA spec - putting license statement into generated files using the `LICENSE` file contents in the package folder - made `commentHeader` a little smarter
090b8d4
to
8a2f476
Compare
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.
@josephaxisa and @jkaster
I've finally fixed our [de]serialization issues not only wrt to Enums but also uncovered and fixed a future bug: my commit on this branch is pretty big but what I want to point out is how the generated code will change a bit. I haven't made those changes yet but the first 180 lines of test_serialize.py is an example. It might be easiest to go over this with you guys in a meeting for better understanding.
("/user", "https://self-signed.looker.com:19999/api/3.1/user"), | ||
("user", "https://self-signed.looker.com:19999/api/3.1/user"), | ||
("/user/1", "https://self-signed.looker.com:19999/api/3.1/user/1"), | ||
("user/1", "https://self-signed.looker.com:19999/api/3.1/user/1"), |
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.
now that I'm using cloudtop my base_url is http://joeldodge.c.googlers.com
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.
This looks ok to me but I'm not the person to approve it. I still need to fix my python config then I can at least test it.
3.8.2 | ||
3.7.6 | ||
3.6.10 |
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.
Does this change default python version selection?
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.
sorry, missed this question: yes, the first one in the list is the default version when you cd into the directory. There are more than one version listed so that tox
can find them and use them
That would be fine with me! |
80bc42a
to
5d80f7e
Compare
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.
Minor typos, and a request to remove property sorting calls
packages/sdk-codegen/src/codeGen.ts
Outdated
props.push(this.declareProperty(bump, prop)) | ||
) | ||
Object.values(type.properties) | ||
.sort(this.sortProperties) |
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.
what's this sort call for?
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 should have just closed this PR till it was ready to review. I was going to go through and annotate the PR with why I made certain changes, including this one:
In python I reverted to having all classes define their own __init__
for consistency and readability. That exposed a bug:
when relying on kw_only=True
argument to the attr.s()
class decorator, the order of required vs optional property declarations didn't matter. But now that we're not using that argument, and instead passing init=False
- attr
blows up saying that "optional declarations cannot come before required ones". So, in order for the python sdk to "compile" we need to make sure that all required properties are bubbled up to the top.
packages/sdk-codegen/src/codeGen.ts
Outdated
sortProperties(_a: IProperty, _b: IProperty) { | ||
return 0 | ||
} |
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.
Did you intend to leave this in? I realize it's not sorting anything right now, but I don't think we should be sorting properties for type declarations. This destroys any intentional grouping/order the developer may have created.
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.
yes, that was intended.
for (const prop of Object.values(type.properties).sort( | ||
this.sortProperties | ||
)) { |
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.
We should not be sorting properties of a type.
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.
let's talk about it - I can try to find another solution for python but that might be a bit of work
added ICodeGen.typeProperties to produce the list of properties for declareType. The default is just type.properties but mypy/python requires that all required properties are declared before optional properties on a class so the python generator overrides this to provide the required properties first followed by the optional.
16464d2
to
c952d16
Compare
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.
@josephaxisa @jkaster I think this is ready for you to look over now. I've tried to leave explanatory comments on the PR to guide you. Feel free to push back on anything.
@@ -213,7 +213,7 @@ export const convertSpec = ( | |||
// patch to fix up small errors in source definition (not required, just to ensure smooth process) | |||
// indent no spaces | |||
// output to openApiFilename | |||
run('swagger2openapi', [ | |||
run('yarn swagger2openapi', [ |
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.
part of versionstamp bugfix
it('gets lookerVersion with supplied versions', async () => { | ||
const actual = await fetchLookerVersion(props, {looker_release_version: '7.10.0'}) | ||
expect(actual).toEqual('7.10') | ||
}) | ||
|
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.
part of versionstamp bugfix
return '' | ||
} | ||
} | ||
return lookerVersion | ||
const matches = versions.looker_release_version.match(/^\d+\.\d+/i) | ||
return matches[0] |
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.
part of versionstamp bugfix
@@ -235,7 +239,7 @@ export abstract class CodeGen implements ICodeGen { | |||
) | |||
propertyValues = props.join(this.enumDelimiter) | |||
} else { | |||
Object.values(type.properties).forEach((prop) => | |||
this.typeProperties(type).forEach((prop) => |
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.
this was my solution to allow custom behavior for language generators and keep the previous behavior the default. open to other ways if this doesn't seem right.
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 this looks reasonable
describe('type creation', () => { | ||
it('with arrays and hashes', () => { | ||
const type = apiTestModel.types.Workspace | ||
const actual = gen.declareType(indent, type) | ||
expect(type.properties.id.type.name).toEqual('string') | ||
expect(actual).toEqual(` | ||
@attr.s(auto_attribs=True, kw_only=True) | ||
@attr.s(auto_attribs=True, init=False) |
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.
all models now have these arguments
if (usesReservedPythonKeyword) { | ||
this.hooks.push( | ||
`sr.converter${this.apiRef}.register_structure_hook(\n${bump}${forwardRef}, # type: ignore\n${bump}${this.structureHook} # type:ignore\n)` | ||
`sr.converter${this.apiRef}.register_structure_hook(\n${bump}${type.name}, # type: ignore\n${bump}${this.structureHookTK} # type:ignore\n)` |
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.
looks like we actually were registering the "translate_keys_structure_hook" functionality before, so LookmlModelExploreJoins
probably was working, but now we're testing 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.
tests are good!
@@ -441,11 +487,10 @@ ${this.hooks.join('\n')} | |||
const attrs: string[] = [] | |||
const isEnum = type instanceof EnumType | |||
const baseClass = isEnum ? 'enum.Enum' : 'model.Model' | |||
let attrsArgs = 'auto_attribs=True, kw_only=True' |
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.
now it's always auto_attribs=True, init=False
so no need to have a separate variable
private filterRequiredProps(required: boolean) { | ||
const filteredProps: PropertyList = {} | ||
for (const key in this.properties) { | ||
const prop = this.properties[key] | ||
const condition = required ? prop.required : !prop.required | ||
if (condition) { | ||
filteredProps[key] = prop | ||
} | ||
} | ||
return filteredProps | ||
} | ||
|
||
get requiredProperties() { | ||
return this.filterRequiredProps(true) | ||
} | ||
|
||
get optionalProperties() { | ||
return this.filterRequiredProps(false) | ||
} | ||
|
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.
is this what you had in mind @jkaster ?
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.
yup!
data = _tr_data_keys(data) | ||
actual_type = eval(forward_ref.__forward_arg__, context, locals()) | ||
if issubclass(actual_type, enum.Enum): | ||
instance = converter.structure(data, actual_type) |
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.
handle enum: "Enum"
type declarations in deserialization (well, with the help of the __annotations__
hack converting that to enum: ForwardRef("Enum")
)
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.
Sorry Python doesn't have a better built-in serialization solution!
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, me too. Python was my gold standard scripting language for a while but the Typing implementation and this lack of easy/builtin class-instance/json serialization has disillusioned me. I'd say Typescript is quickly gaining ground for me except the huge barrier between "I wrote this typescript code" and "I want to run this typescript script" (even as a node script) is still daunting to me.
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.
Yeah, the scripting in this repo is a bit awkward now, but if you look at the existing node scripts in the root package.json you can see the pattern that should always work.
@@ -14144,7 +14144,7 @@ typedarray@^0.0.6: | |||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | |||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= | |||
|
|||
[email protected], typescript@^3.8.2, typescript@^3.8.3: | |||
[email protected], typescript@^3.6.0, typescript@^3.8.2, typescript@^3.8.3: |
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 have no idea why I have this change, should I discard it? maybe I have some old version of typescript on my cloudtop?
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.
No, don't discard it. Let yarn.lock
do what yarn.lock
will
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 just realized I responded to all your comments inline rather than doing a review!
Most of this looks good! We could do some very minor cleanup but there's nothing to hold up this PR, and this change and the other ones are clearly showing we need to do some better design for the low-level codegen components. Hopefully we can get to that before we do yet another language!
I cannot start a review since I opened this PR, but this LGTM. Nice work Joel! |
c952d16
to
598a3ad
Compare
refactored serialization structure hooks to handle different types of ForwardRef objects (enum vs model object) Broke up structure_hook into reserved_kw_structure_hook and forward_ref_structure_hook fixed bug where "bare" ForwardRef annotations would bomb out on deserialization - doesn't exist in the wild yet but it probably will at some point - except that for now we always mark all API response model fields as optional because of the "fields" argument... fixed bug where "reserved_keyword_structure_hook" was not being applied to the top level Model type. reversed .python-version list so 3.8.2 is default added tox-pyenv because tox wasn't finding 3.6 on my box
598a3ad
to
a839630
Compare
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.
LGTM. Nothing blocking approval
declareProperty(indent: string, property: IProperty, annotations = false) { | ||
const mappedType = this.typeMapModels(property.type) | ||
let propName = property.name | ||
if (this.pythonKeywords.has(propName)) { | ||
propName = propName + '_' | ||
} | ||
let propType = mappedType.name | ||
if (!property.required) { | ||
propType = `Optional[${mappedType.name}] = ${this.nullStr}` | ||
propType = `Optional[${mappedType.name}]` | ||
} | ||
|
||
let propDef | ||
if (annotations) { | ||
let annotation = propType | ||
if (this.isBareForwardRef(property)) { | ||
annotation = `ForwardRef(${propType})` | ||
} | ||
propDef = `${this.bumper(indent)}"${propName}": ${annotation}` | ||
} else { | ||
if (!property.required) { | ||
propType += ` = ${this.nullStr}` | ||
} | ||
propDef = `${indent}${propName}: ${propType}` |
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.
LOL. Let's worry about it if it breaks :)
// so we need to emit the `__annotations__` property | ||
return ( | ||
prop.required && | ||
(prop.type.customType || prop.type instanceof EnumType) && |
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, EnumType should definitely have a truthy customType
. If it's not, we should get around to fixing that.
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 it doesn't which is why I had to add the EnumType check in here.
/** | ||
* key/value collection of required properties for this type | ||
*/ | ||
requiredProperties: PropertyList |
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 need this as a list, or if it's just for natural order iteration, it could be IProperty[]
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 wanted it to be the same type as properties
- ICodeGen.typeProperties
returns a IProperty[]
because that's all declareType
cares about is natural order iteration