-
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(jsii-spec): Model parameter optionality #432
Conversation
Method `Parameters` now carry an `optional` flag that indicates whether they are optional or required, and the `TypeReference#optional` field was renamed to `TypeReference#nullable` to better reflect its semantics. This also brings more flexibility in that it is now possible to model a method with a nullable or defaulted argument that is followed by some non-optional argument, and still obtain a reasonable type specification, where previously this was an error. Finally, in order to better reflect the type model of TypeScript and Javascript, all `any` type references are now denoted `nullable`. BREAKING CHANGE: JSII assemblies generated by older versions of the tool will fail loading with this new version, and vice-versa. Re-compile your projects in order to fix this. Fixes #296 Fixes #414
Note that I decided against modeling optional properties, because it is essentially meaningless... A property can be considered optional as long as it's type is |
It slightly concerns me that there are breaking changes in the assembly model. Do we know for a fact users will always use the new kernel to load the new assemblies? Don't we have a |
@rix0rrr new revision should have addressed all of your comments as well, and sets the Schema version of |
@@ -16,7 +16,7 @@ | |||
"type": "git", | |||
"url": "https://github.com/awslabs/jsii.git" | |||
}, | |||
"schema": "jsii/1.0", | |||
"schema": "jsii/0.9.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.
Waitaminit! I don't like how this is version number is going backwards, and we already have a jsiiVersion
field that encodes the JSII version. Why can't we just discretely bump the schema versions 1 -> 2 -> 3 etc? Saves us having to do the whole "what's the next release version" dance, too.
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 don't really care. I kinda felt it was nice to correlate that with the jsii
version so you can, from an error (like "Whoosh the version is jsii/0.9.0
, but I expected to find jsii/0.42.12
), determine what you need to do. I however would only update that version number if/when the spec gets a breaking change (so it acts as a "minimum required version" of sorts.
/** | ||
* A reference to a type (primitive, collection or fqn). | ||
*/ | ||
export type TypeReference = NamedTypeReference | PrimitiveTypeReference | CollectionTypeReference | UnionTypeReference; |
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 support this!
packages/jsii-spec/lib/spec.ts
Outdated
*/ | ||
export type TypeReference = TypeReferenceBase & (NamedTypeReference | PrimitiveTypeReference | CollectionTypeReference | UnionTypeReference); | ||
export interface TypeInstance<T extends TypeReference> { |
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 I'm going to say that this interface ony makes sense for return types, and I think I object to its use in other places.
packages/jsii-spec/lib/spec.ts
Outdated
@@ -390,7 +399,7 @@ export interface CollectionTypeReference extends TypeReferenceBase { | |||
/** | |||
* The type of an element (map keys are always strings). | |||
*/ | |||
elementtype: TypeReference; | |||
elementtype: TypeInstance<TypeReference>; |
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 does not make sense for either array or map elements to be of type Promise<T>
, or optional.
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.
So an array may never contain null
? Or it'll contain "surprise" null
s?
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.
But that is something else though! Feels like now we're expressing nullability, not optionality.
packages/jsii-spec/lib/spec.ts
Outdated
@@ -409,7 +418,7 @@ export interface UnionTypeReference extends TypeReferenceBase { | |||
* All the possible types (including the primary type). | |||
* @minItems 2 | |||
*/ | |||
types: TypeReference[]; | |||
types: Array<TypeInstance<TypeReference>>; |
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 does not make sense for a union type to be something like number | string?
or number | Promise<string>
.
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.
And yet, this is valid TypeScript, and thus could be authored in a TypeScript signature that JSII would totally parse out in that way
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.
Nobody says we can't reject that in jsii. Will the kernel handle it correctly?
packages/jsii-spec/lib/spec.ts
Outdated
*/ | ||
type: TypeReference; | ||
value: TypeInstance<TypeReference>; |
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.
A parameter can never be a Promise<T>
. It can be optional, however.
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.
// In fact, totally possible:
async function impossible(arg: Promise<Foo>): Promise<Bar>;
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.
Sure. What I meant to say is that the kernel does not have adequate provisions to handle this, so we shouldn't allow it either.
packages/jsii-spec/lib/spec.ts
Outdated
@@ -508,7 +521,7 @@ export interface Method extends Documentable, Overridable, SourceLocatable { | |||
/** | |||
* The return type of the method (undefined if void or initializer) | |||
*/ | |||
returns?: TypeReference; | |||
returns?: TypeInstance<TypeReference>; |
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 is the only place where TypeInstance
makes sense to me.
Although I think promise?: boolean
makes more sense as an attribute of Method
to me (async?: boolean
) than as an attribute of the return type.
packages/jsii-spec/lib/spec.ts
Outdated
*/ | ||
type: TypeReference; | ||
value: TypeInstance<TypeReference>; |
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.
Properties cannot be promise
. They can be optional however.
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 can totes declare a public readonly iSwearItWillHaveAValueAtSomePoint: Promise<string>
. If that's an API I like? No. But it can be made (and JSII will happily generate a promise: true
for that type reference.
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.
See my previous comment, I'm not sure the kernel will handle that situation properly.
I think I'd prefer a modeling like this: interface OptionalValue {
optional?: boolean;
type: TypeReference;
}
interface Method {
async?: boolean;
returns?: OptionalValue;
// ...
}
interface Property extends OptionalValue {
// ...
}
interface Parameter extends OptionalValue {
// ...
}
interface Collection {
element: TypeReference;
// ...
} |
"initializer": { | ||
"initializer": true | ||
}, | ||
"initializer": {}, |
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 happened here? We shouldn't use an empty object to represent something...
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.
Empty object here is the default initializer (no parameters, public). I thought of also modeling that as "true" or the literal string "default" instead... but the empty object actually describes what I want, based on its default values... so I figured I wouldn't overthink 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.
I understand where this is coming from, but still don't like this as a general modeling principle. Can we do better?
@@ -469,6 +449,7 @@ | |||
{ | |||
"name": "inp", | |||
"type": { | |||
"optional": 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.
I don't believe this parameter is optional
@@ -204,22 +203,26 @@ export class Assembler implements Emitter { | |||
* | |||
* @returns the de-referenced type, if it was found, otherwise ``undefined``. | |||
*/ | |||
private _dereference(ref: spec.NamedTypeReference, referencingNode: ts.Node | null): spec.Type | undefined { | |||
const [assm, ] = ref.fqn.split('.'); | |||
private _dereference(ref: string | spec.NamedTypeReference, referencingNode: ts.Node | null): spec.Type | undefined { |
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.
For readability, I would create a type alias spec.FullyQualifiedName = string
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 boy. That alias even exists! I can just use it :)
const [assm, ] = ref.fqn.split('.'); | ||
private _dereference(ref: string | spec.NamedTypeReference, referencingNode: ts.Node | null): spec.Type | undefined { | ||
if (typeof ref !== 'string') { | ||
ref = ref.fqn; |
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.
error if ref.fqn
is undefined?
@@ -555,17 +557,17 @@ export class Assembler implements Emitter { | |||
|
|||
// tslint:disable-next-line:no-bitwise | |||
if ((ts.getCombinedModifierFlags(ctorDeclaration) & ts.ModifierFlags.Private) === 0) { | |||
jsiiType.initializer = { initializer: true }; | |||
jsiiType.initializer = {}; |
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.
When I have a method, how do I know if it's an initializer?
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.
You have an initializer, not a method. Methods have a name (guaranteed). Initializers don't (guaranteed).
/** | ||
* Fully Qualified Name | ||
*/ | ||
export type FQN = string; |
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, there we go!
packages/jsii-spec/lib/spec.ts
Outdated
*/ | ||
export interface TypeReferenceBase { | ||
export const CANONICAL_ANY: Readonly<PrimitiveTypeReference> = { optional: true, primitive: PrimitiveType.Any }; |
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 does any
imply optional
? It implies nullable, but that's a different thing.
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 guess I think of optional as allowing "no value" through, and any also allows "no value" though. But I guess I see your point still...
packages/jsii-spec/lib/spec.ts
Outdated
/** | ||
* Qualifiers for a type reference. | ||
*/ | ||
interface QualifiedTypeReference { |
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.
Maybe TypeReferenceQualifiers
, because this is not a type reference.
@@ -58,7 +58,7 @@ def __init__(self) -> None: | |||
jsii.create(AllTypes, self, []) | |||
|
|||
@jsii.member(jsii_name="anyIn") | |||
def any_in(self, inp: typing.Any) -> None: | |||
def any_in(self, inp: typing.Any=None) -> None: |
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.
That's wrong... any does not imply optionality
@RomainMuller what disagreement? |
I want to be able to express:
|
We all sort of agree on optionality, I feel. What we have left to do is decide on whether nullability is a thing or not. In line with your first iteration, my intuition is yes, nullability is a thing. Elad disagreed, and now we're going back and forth. What we couldn't express before is the guaranteed PRESENCE of a value which CAN be null/undefined. Where does this matter? FUNCTION ARGUMENTS For function arguments, it definitely matters. function foo(x: string | undefined);
function bar(x?: string);
bar(); // ok
foo(); // ERROR
foo(undefined); // ok Is the definition of RETURN VALUES Return values are more of a philosophical question. Does a function ever return no value? What if it returns function foo(): string | undefined {
return undefined;
} Does For consistency, I would argue that it always returns a nullable. But on the other hand, what does this return: function foo(): void {
} That should always return nothing. Yet we have it always return a value ( I could be convinced to argue that a function PROPERTIES Do properties have the same behavior as method arguments? In fact, they do. function foo({ arg: string | undefined }) {
}
function bar({ arg?: string }) {
}
bar({}); // ok
foo({}); // ERROR
foo({ arg: undefined }); // ok For structs I would agree with Elad that the definition of For output structs/interfaces, we kind of do an implicit mapping: interface Foo {
attr?: string;
} Gets turned into: interface Foo {
String getAttr();
} So we actually already turn COLLECTIONS This is an interesting one. I will focus on arrays because maps have a trivial reduction into key optionality (just like structs do). Is Is it valid to want to be able to represent the type of this value at all? I would say yes, but we could be free to disallow it because I don't think we use a structure like that anywhere, and its use should be rare. If we wanted to type it, how would we type it? Array<Nullable<string>>
or
Array<Union<string, undefined>> The first requires us to have nullable types. The second leads us to require being able to represent My opinion: I think we should have both optional values (for when we write |
Is it possible to decouple these discussions? We are in alignment on optionals, so let’s wrap up this PR, and follow up with a discussion and a separate PR on nullables. I don’t see a good reason to couple those together, right? |
Additional change discussions will happen in #440 |
I don't think so. We currently have one flag which we interpret as "optional" in a value context (property, argument) and as "nullable" in a type context (return values, I would suggest we either make a decision on what properties we want from our type system and encode those, or we leave the current situation which is occasionally confusing but serviceable enough. There is only one thing we cannot model today, which is a required, nullable parameter. |
packages/jsii-spec/lib/spec.ts
Outdated
*/ | ||
returns?: string; | ||
|
||
/** | ||
* Whether the API item is beta/experimental quality | ||
* | ||
* @default Stability.Experimental |
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.
inherit from parent (and if no parent - stable)
….13.3,<4.5.0 in /packages/@jsii/python-runtime (#4683) Updates the requirements on [typeguard](https://github.com/agronholm/typeguard) to permit the latest version. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/agronholm/typeguard/releases">typeguard's releases</a>.</em></p> <blockquote> <h2>4.4.0</h2> <ul> <li>Added proper checking for method signatures in protocol checks (<a href="https://redirect.github.com/agronholm/typeguard/pull/465">#465</a>)</li> <li>Fixed basic support for intersection protocols (<a href="https://redirect.github.com/agronholm/typeguard/pull/490">#490</a>; PR by <a href="https://github.com/antonagestam"><code>@antonagestam</code></a>)</li> <li>Fixed protocol checks running against the class of an instance and not the instance itself (this produced wrong results for non-method member checks)</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/agronholm/typeguard/blob/master/docs/versionhistory.rst">typeguard's changelog</a>.</em></p> <blockquote> <h1>Version history</h1> <p>This library adheres to <code>Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200></code>_.</p> <p><strong>4.4.0</strong> (2024-10-27)</p> <ul> <li>Added proper checking for method signatures in protocol checks (<code>[#465](agronholm/typeguard#465) <https://github.com/agronholm/typeguard/pull/465></code>_)</li> <li>Fixed basic support for intersection protocols (<code>[#490](agronholm/typeguard#490) <https://github.com/agronholm/typeguard/pull/490></code>_; PR by <a href="https://github.com/antonagestam"><code>@antonagestam</code></a>)</li> <li>Fixed protocol checks running against the class of an instance and not the instance itself (this produced wrong results for non-method member checks)</li> </ul> <p><strong>4.3.0</strong> (2024-05-27)</p> <ul> <li>Added support for checking against static protocols</li> <li>Fixed some compatibility problems when running on Python 3.13 (<code>[#460](agronholm/typeguard#460) <https://github.com/agronholm/typeguard/issues/460></code>_; PR by <a href="https://github.com/JelleZijlstra"><code>@JelleZijlstra</code></a>)</li> <li>Fixed test suite incompatibility with pytest 8.2 (<code>[#461](agronholm/typeguard#461) <https://github.com/agronholm/typeguard/issues/461></code>_)</li> <li>Fixed pytest plugin crashing on pytest version older than v7.0.0 (even if it's just present) (<code>[#343](agronholm/typeguard#343) <https://github.com/agronholm/typeguard/issues/343></code>_)</li> </ul> <p><strong>4.2.1</strong> (2023-03-24)</p> <ul> <li>Fixed missing <code>typing_extensions</code> dependency for Python 3.12 (<code>[#444](agronholm/typeguard#444) <https://github.com/agronholm/typeguard/issues/444></code>_)</li> <li>Fixed deprecation warning in the test suite on Python 3.13 (<code>[#444](agronholm/typeguard#444) <https://github.com/agronholm/typeguard/issues/444></code>_)</li> </ul> <p><strong>4.2.0</strong> (2023-03-23)</p> <ul> <li>Added support for specifying options for the pytest plugin via pytest config files (<code>[#440](agronholm/typeguard#440) <https://github.com/agronholm/typeguard/issues/440></code>_)</li> <li>Avoid creating reference cycles when type checking unions (PR by Shantanu)</li> <li>Fixed <code>Optional[...]</code> being removed from the AST if it was located within a subscript (<code>[#442](agronholm/typeguard#442) <https://github.com/agronholm/typeguard/issues/442></code>_)</li> <li>Fixed <code>TypedDict</code> from <code>typing_extensions</code> not being recognized as one (<code>[#443](agronholm/typeguard#443) <https://github.com/agronholm/typeguard/issues/443></code>_)</li> <li>Fixed <code>typing</code> types (<code>dict[str, int]</code>, <code>List[str]</code>, etc.) not passing checks against <code>type</code> or <code>Type</code> (<code>[#432](agronholm/typeguard#432) <https://github.com/agronholm/typeguard/issues/432></code>_, PR by Yongxin Wang)</li> <li>Fixed detection of optional fields (<code>NotRequired[...]</code>) in <code>TypedDict</code> when using forward references (<code>[#424](agronholm/typeguard#424) <https://github.com/agronholm/typeguard/issues/424></code>_)</li> <li>Fixed mapping checks against Django's <code>MultiValueDict</code> (<code>[#419](agronholm/typeguard#419) <https://github.com/agronholm/typeguard/issues/419></code>_)</li> </ul> <p><strong>4.1.5</strong> (2023-09-11)</p> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/agronholm/typeguard/commit/efa1166c85be9a1280090fea9c287b5e4e9f3830"><code>efa1166</code></a> Added release date</li> <li><a href="https://github.com/agronholm/typeguard/commit/b72794dffe403254881ac0c327155357c43ccebf"><code>b72794d</code></a> Added proper Protocol method signature checking (<a href="https://redirect.github.com/agronholm/typeguard/issues/496">#496</a>)</li> <li><a href="https://github.com/agronholm/typeguard/commit/afad2c7b6be830900776922bb39f9346c2e77f6f"><code>afad2c7</code></a> Sorted the Ruff rules alphabetically</li> <li><a href="https://github.com/agronholm/typeguard/commit/d812f2eba9f5e898544eb4b3e597f8c38b0952e8"><code>d812f2e</code></a> Migrated to native tox TOML configuration</li> <li><a href="https://github.com/agronholm/typeguard/commit/0c50de6144d99eedb402a8e85eb8187098f8c26f"><code>0c50de6</code></a> Declared Python 3.13 support</li> <li><a href="https://github.com/agronholm/typeguard/commit/cf25d56dc0dbf6bb2f51ea29da8436b368ed4857"><code>cf25d56</code></a> Fixed annotation for typeguard_ignore() to match one for typing.no_type_check...</li> <li><a href="https://github.com/agronholm/typeguard/commit/604b08d5ba7c1b6e3d2f2ddd50dcf020f7e2794a"><code>604b08d</code></a> Use get_protocol_members in protocol checking (<a href="https://redirect.github.com/agronholm/typeguard/issues/490">#490</a>)</li> <li><a href="https://github.com/agronholm/typeguard/commit/c72b6752b7069d695898ea29abb2a31983c1bf80"><code>c72b675</code></a> [pre-commit.ci] pre-commit autoupdate (<a href="https://redirect.github.com/agronholm/typeguard/issues/471">#471</a>)</li> <li><a href="https://github.com/agronholm/typeguard/commit/ac7ac342a61db284872430c95d5e6ed7a035b7c0"><code>ac7ac34</code></a> Fixed the documentation build</li> <li><a href="https://github.com/agronholm/typeguard/commit/2c035b306996f742705da66ef64a052e715a94e2"><code>2c035b3</code></a> Assume that typing_extensions is always installed (<a href="https://redirect.github.com/agronholm/typeguard/issues/487">#487</a>)</li> <li>Additional commits viewable in <a href="https://github.com/agronholm/typeguard/compare/2.13.3...4.4.0">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details>
Method
Parameters
,Properties
and method return typesnow carry an
optional
flag that indicates whether they areoptional or required, and the
TypeReference#optional
fieldwas removed.
BREAKING CHANGE: JSII assemblies generated by older versions of the tool
will fail loading with this new version, and vice-versa. Re-compile your
projects in order to fix this.
Fixes #296
Fixes #414
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.