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

Python SDK: Enum Support #242

Merged
merged 3 commits into from
Jul 2, 2020
Merged

Python SDK: Enum Support #242

merged 3 commits into from
Jul 2, 2020

Conversation

josephaxisa
Copy link
Contributor

  • 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

jkaster added a commit that referenced this pull request Jun 23, 2020
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
Base automatically changed from jk/enums to master June 23, 2020 19:28
@joeldodge79 joeldodge79 force-pushed the jax/python-enums branch 6 times, most recently from 090b8d4 to 8a2f476 Compare June 25, 2020 23:12
Copy link
Contributor

@joeldodge79 joeldodge79 left a 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.

Comment on lines -204 to -207
("/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"),
Copy link
Contributor

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

Copy link
Contributor

@jkaster jkaster left a 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.

Comment on lines 1 to +3
3.8.2
3.7.6
3.6.10
Copy link
Contributor

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?

Copy link
Contributor

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

@jkaster
Copy link
Contributor

jkaster commented Jun 26, 2020

It might be easiest to go over this with you guys in a meeting for better understanding.

That would be fine with me!

@joeldodge79 joeldodge79 force-pushed the jax/python-enums branch 2 times, most recently from 80bc42a to 5d80f7e Compare June 30, 2020 00:01
Copy link
Contributor

@jkaster jkaster left a 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

props.push(this.declareProperty(bump, prop))
)
Object.values(type.properties)
.sort(this.sortProperties)
Copy link
Contributor

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?

Copy link
Contributor

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.

Comment on lines 226 to 228
sortProperties(_a: IProperty, _b: IProperty) {
return 0
}
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that was intended.

Comment on lines 487 to 489
for (const prop of Object.values(type.properties).sort(
this.sortProperties
)) {
Copy link
Contributor

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.

Copy link
Contributor

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

python/tests/rtl/test_serialize.py Outdated Show resolved Hide resolved
python/tests/rtl/test_serialize.py Outdated Show resolved Hide resolved
@josephaxisa josephaxisa marked this pull request as ready for review June 30, 2020 09:20
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.
@joeldodge79 joeldodge79 force-pushed the jax/python-enums branch 2 times, most recently from 16464d2 to c952d16 Compare June 30, 2020 23:53
Copy link
Contributor

@joeldodge79 joeldodge79 left a 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', [
Copy link
Contributor

Choose a reason for hiding this comment

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

part of versionstamp bugfix

Comment on lines +63 to +67
it('gets lookerVersion with supplied versions', async () => {
const actual = await fetchLookerVersion(props, {looker_release_version: '7.10.0'})
expect(actual).toEqual('7.10')
})

Copy link
Contributor

Choose a reason for hiding this comment

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

part of versionstamp bugfix

Comment on lines +297 to +301
return ''
}
}
return lookerVersion
const matches = versions.looker_release_version.match(/^\d+\.\d+/i)
return matches[0]
Copy link
Contributor

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) =>
Copy link
Contributor

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.

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 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)
Copy link
Contributor

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

Comment on lines +511 to +512
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)`
Copy link
Contributor

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 :-)

Copy link
Contributor

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'
Copy link
Contributor

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

Comment on lines +1405 to +1424
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)
}

Copy link
Contributor

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 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

yup!

Comment on lines +120 to +123
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)
Copy link
Contributor

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"))

Copy link
Contributor

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!

Copy link
Contributor

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.

Copy link
Contributor

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=

typescript@3.8.2, typescript@^3.8.2, typescript@^3.8.3:
typescript@3.8.2, typescript@^3.6.0, typescript@^3.8.2, typescript@^3.8.3:
Copy link
Contributor

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?

Copy link
Contributor

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

@joeldodge79 joeldodge79 requested a review from jkaster June 30, 2020 23:55
@jkaster jkaster dismissed their stale review July 1, 2020 01:05

Resolved change request

jkaster
jkaster previously approved these changes Jul 1, 2020
Copy link
Contributor

@jkaster jkaster left a 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!

@josephaxisa
Copy link
Contributor Author

I cannot start a review since I opened this PR, but this LGTM. Nice work Joel!

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
Copy link
Contributor

@jkaster jkaster left a 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

Comment on lines +198 to +216
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}`
Copy link
Contributor

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) &&
Copy link
Contributor

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.

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 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
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 need this as a list, or if it's just for natural order iteration, it could be IProperty[]

Copy link
Contributor

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

@joeldodge79 joeldodge79 merged commit 1b64e1b into master Jul 2, 2020
@joeldodge79 joeldodge79 deleted the jax/python-enums branch July 2, 2020 00:08
This was referenced Mar 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants