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

Add typing to component init. #2276

Open
wants to merge 36 commits into
base: dev
Choose a base branch
from
Open

Add typing to component init. #2276

wants to merge 36 commits into from

Conversation

T4rk1n
Copy link
Contributor

@T4rk1n T4rk1n commented Oct 20, 2022

Add python typing to generated components __init__.

Contributor Checklist

  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR
  • I have added entry in the CHANGELOG.md

def __init__(
self,
{default_argtext}
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder, now that we're Py3-only, maybe we should call black on the generated files as part of the generation process? We already do that in the core component packages as a separate step.

@tuchandra
Copy link

Hi - this is really, really cool. I would love to see this landed into Dash, and if I can help with reviewing this PR, debugging any tests, etc., please let me know. I was working on a separate effort to generate stub (.pyi) files, so I have tinkered around with this part of the codebase a little bit. I'm excited to see where this goes!

optionalArray: typing.List = Component.UNDEFINED,
optionalBool: bool = Component.UNDEFINED,
optionalFunc: typing.Any = Component.UNDEFINED,
optionalNumber: typing.Union[float, int] = Component.UNDEFINED,
Copy link
Contributor

Choose a reason for hiding this comment

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

@alexcjohnson @T4rk1n just wondering if people can pass in Decimals in here as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If to_json_plotly can serialize it then yes. It's pretty flexible, so probably Decimal is fine, but I haven't tested it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Decimal can serialize, I think we can put numbers.Number to get all the number types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

numbers.Number is too much, it includes complex - how about numbers.Real?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://docs.python.org/3/library/numbers.html#the-numeric-tower

Number is the base class, then they extend the last one, to get the Integral you need all the other 3 implemented. Tested with numpy and Number is what get the most support and our to_json supports them, I dont think custom implementation of those works.

def __init__(
self,
children: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = None,
optionalArray: typing.List = Component.UNDEFINED,
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be a List[Any]. mypy will throw an error on this.

also, wondering if we should use the parent type Iterable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

typing.List is same as typing.List[typing.Any], iterable might not json compatible, I think the List is the most safe for this typing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There are certainly a number of other iterables people frequently use though - tuple, range, dict_keys, dict_values, etc etc... I wouldn't want to push people into casting everything to a list, that would be unnecessary overhead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

range, dict_keys, dict_values,

Those already needs to be cast to list for json.

I guess we could do a union of Tuple and List.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still open 😎

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can add Tuple as union for untyped arrays, but arrayOf is really a different typing on both side.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for updating array. Not sure what you mean by "arrayOf is really a different typing on both side", but I think it's fairly important to accept tuples in arrayOf. I recall for example various database drivers returning query results as either nested tuples or tuples of dicts.

I wonder if it would be worth defining up front a couple of extra types we can reuse later, like:

JsonNumber = typing.Union[int, float, numbers.Real]  # or whatever we end up with for numbers
JsonArray = typing.Union[typing.List, typing.Tuple]
ReactNode = typing.Union[
    typing.Union[str, JsonNumber, Component],  # note more numbers here!
    JsonArray[typing.Union[str, JsonNumber, Component]]
]

If we did something like that, would we be able to change:

optionalArrayOf: typing.List[typing.Union[int, float, numbers.Number]] = Component.UNDEFINED

into this?

optionalArrayOf: JsonArray[JsonNumber] = Component.UNDEFINED

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's better to go without defining type aliases that would require to be imported and would break backward compatibility for component libraries that compiled with the new typing but the user has dash locked to a previous version.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Oct 26, 2022

I went with pyright instead of mypy for the static checker because it has better package/module resolution and default argument gets automatically added as an union type.

The checker and my IDE don't recognize the type of Component and as such put it as Any which breaks the checking for those types.

@T4rk1n T4rk1n changed the title [WIP] Add typing to component init. Add typing to component init. Nov 30, 2022
optionalString: str = Component.UNDEFINED,
optionalSymbol: typing.Any = Component.UNDEFINED,
optionalNode: typing.Union[str, int, float, Component, typing.List[typing.Union[str, int, float, Component]]] = Component.UNDEFINED,
optionalElement: Component = Component.UNDEFINED,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Huh, never noticed this before... do we actually support Element as distinct from Node?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it ends up rendering the same as node but for typing it allows only a single component to be put and not a list of string/number/components.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there any examples of this in existing components? Does it work, either as children or with components-as-props?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before component as props, it was kind of invalid prop as only the children would be hydrated and children would be treated as a node, component as props also doesn't differ those Element from node.

The collect_nodes make both node and element into component:

def is_node(value):
return value in ("node", "element")

I think the only other place we have Element defined is in the typescript props:


But the prop itself was not tested as it was before component as props was supported.

@@ -5,3 +5,5 @@ dash_core_components==2.0.0
dash_table==5.0.0
importlib-metadata==4.8.3;python_version<"3.7"
contextvars==2.4;python_version<"3.7"
typing_extensions==4.1.1;python_version<"3.7"
typing_extensions>=4.4.0;python_version>"3.6"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
typing_extensions>=4.4.0;python_version>"3.6"
typing_extensions>=4.4.0;python_version>="3.7"

Just to be more obviously symmetric with the <"3.7"

I wonder though, if 4.1.1 is sufficient would we be better off just collapsing these into one line? If someone on 3.7+ has a version >=4.1.1,<4.4.0 pinned for some reason is that a problem?

typing_extensions>=4.1.1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

4.1.1 has fix for imports on 3.7, it's the last version that works with 3.6. I don't see anything we actually uses in the later version so we can try a single version >=4.1.1

"import typing # noqa: F401\n"
"import numbers # noqa: F401\n"
"import enum # noqa: F401\n"
"from typing_extensions import TypedDict, NotRequired # noqa: F401\n"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a big deal, but in principle we can avoid noqa: F401 if we look at class_string and drop any imports we don't need.

@T4rk1n

This comment was marked as outdated.

@T4rk1n

This comment was marked as outdated.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented May 2, 2023

The docstring on the components are not in the right position for IDE support, the docstring for the props should be under the __init__ for them to appear on the tooltip when hovering/typing a component prop. The type in the docstring can now be removed as it is duplicate.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented May 11, 2023

There is a typing error with plotly Figure objects on dcc.Graph.figure prop expecting a dict, need to find a way to add it to possible types for that prop.

@@ -5,3 +5,5 @@ dash_core_components==2.0.0
dash_table==5.0.0
importlib-metadata==4.8.3;python_version<"3.7"
contextvars==2.4;python_version<"3.7"
typing_extensions>=4.1.1
stringcase>=1.2.0
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need stringcase in requires-install? Couldn't it go in requires-dev?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's needed for the old runtime component loader 😞

Copy link
Collaborator

Choose a reason for hiding this comment

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

haha ok - we should put a deprecation warning on runtime component generation, and remove it whenever we decide to do a Dash 3.0

@gvwilson gvwilson self-assigned this May 23, 2024
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.

None yet

5 participants