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

Merged
merged 61 commits into from
Nov 8, 2024
Merged

Add typing to component init #2276

merged 61 commits into from
Nov 8, 2024

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

@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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

hmph ok, numbers.Real doesn't catch Decimal for some reason, but numbers.Number does. So OK, we can leave it as you have it (int, float, numbers.Number) - better to allow some invalid items through than to prevent some valid items.

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.

@gvwilson
Copy link
Contributor

@alexcjohnson can you please review this for us? thank you - @gvwilson

typed = get_prop_typing(
type_info["value"]["name"], component_name, prop_name, type_info["value"]
)
return f"typing.Union[typing.Sequence[{typed}], typing.Tuple]"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need the Union here? Sequence includes tuples, I believe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I changed from List but didn't remove the Tuple.



PROP_TYPING = {
"array": generate_type("typing.Union[typing.Sequence, typing.Tuple]"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, doesn't Sequence include Tuple already?

Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

Just a couple of minor comments, this looks great! 💃

I'd still like us to put a deprecation notice on runtime-generated components, no-one should be using those anymore, right? Doesn't need to be in this PR though.

Copy link
Contributor

@gvwilson gvwilson left a comment

Choose a reason for hiding this comment

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

a couple of questions for my own learning, but 👍 from me. (caution: I'm not a very informed reviewer)

dash/__init__.py Show resolved Hide resolved
interval: int = 1000,
progress: Optional[Output] = None,
progress_default: Any = None,
running: Optional[List[Tuple[Output, Any, Any]]] = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

took me a moment to parse this :-)

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 given as [(Output('id', 'prop), "on", "off")]

Copy link
Contributor

@eff-kay eff-kay Aug 26, 2024

Choose a reason for hiding this comment

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

I think you can just stop at a higher level Optional[List], and be les concrete, leave room for future modifications.



# pylint: disable=unused-argument,too-many-locals
# pylint: disable=unused-argument,too-many-locals,too-many-branches
Copy link
Contributor

Choose a reason for hiding this comment

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

:-)

dash/development/_py_components_generation.py Show resolved Hide resolved
shape_template.format(
name=name, values=" {\n" + ",\n".join(props) + "\n }"
),
" ",
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need to indent an empty string at this point?

# This wrapper adds an argument given to generated Component.__init__
# with the actual given parameters by the user as a list of string.
# This is then checked in the generated init to check if required
# props were provided.
Copy link
Contributor

Choose a reason for hiding this comment

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

thank you for the explanation

warnings.warn(
DeprecationWarning(
"Dynamic components loading has been deprecated and will be removed"
" in dash 3.0.\n"
Copy link
Contributor

Choose a reason for hiding this comment

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

is there an issue filed to removed dynamic component loading so we don't forget?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: #2982

assert ex_err in error


@pytest.mark.parametrize(
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithExactAndNestedDescription=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs):
def __init__(
self,
children: typing.Optional[typing.Union[str, int, float, ComponentType, typing.Sequence[typing.Union[str, int, float, ComponentType]]]] = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

whoa... :-)

Copy link
Contributor

Choose a reason for hiding this comment

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

We can use type-aliases to make our lives easier here.

BasicType: TypeAlias = Union[str, int, float]
ComponentOrBasic: TypeAlias = Union[BasicType, ComponentType]
ChildrenType: TypeAlias = Optional[Union[ComponentOrBasic, Sequence[ComponentOrBasic]]]



children: ChildrenType = None,

@T4rk1n T4rk1n added the dash-3.0 Going in dash-3.0 release. label Sep 4, 2024
@T4rk1n T4rk1n changed the base branch from dev to dash-3.0 September 9, 2024 20:01
@T4rk1n T4rk1n merged commit 9705ae5 into dash-3.0 Nov 8, 2024
3 checks passed
@T4rk1n T4rk1n deleted the prop-typing branch November 8, 2024 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dash-3.0 Going in dash-3.0 release. feature something new P2 considered for next cycle
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants