-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix typing component generation & explicitize_args #3287
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
Changes from all commits
3349174
a13849b
d1e614f
a7569df
1ae86a0
8a57d3a
207aef9
6e08afc
49631fa
8b3a96a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,13 +22,21 @@ | |
import_string = """# AUTO GENERATED FILE - DO NOT EDIT | ||
|
||
import typing # noqa: F401 | ||
import numbers # noqa: F401 | ||
from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401 | ||
from dash.development.base_component import Component | ||
try: | ||
from dash.development.base_component import ComponentType # noqa: F401 | ||
except ImportError: | ||
ComponentType = typing.TypeVar("ComponentType", bound=Component) | ||
from dash.development.base_component import Component, _explicitize_args | ||
{custom_imports} | ||
ComponentType = typing.Union[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect this will eventually grow to |
||
str, | ||
int, | ||
float, | ||
Component, | ||
None, | ||
typing.Sequence[typing.Union[str, int, float, Component, None]], | ||
] | ||
|
||
NumberType = typing.Union[ | ||
typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex | ||
] | ||
|
||
|
||
""" | ||
|
@@ -80,7 +88,6 @@ def generate_class_string( | |
_namespace = '{namespace}' | ||
_type = '{typename}' | ||
{shapes} | ||
_explicitize_dash_init = True | ||
|
||
def __init__( | ||
self, | ||
|
@@ -98,6 +105,8 @@ def __init__( | |
args = {args} | ||
{required_validation} | ||
super({typename}, self).__init__({argtext}) | ||
|
||
setattr({typename}, "__init__", _explicitize_args({typename}.__init__)) | ||
''' | ||
|
||
filtered_props = ( | ||
|
@@ -239,7 +248,6 @@ def generate_class_file( | |
Returns | ||
------- | ||
""" | ||
imports = import_string | ||
|
||
class_string = generate_class_string( | ||
typename, | ||
|
@@ -255,8 +263,11 @@ def generate_class_file( | |
custom_imp = custom_imp.get(typename) or custom_imp.get("*") | ||
|
||
if custom_imp: | ||
imports += "\n".join(custom_imp) | ||
imports += "\n\n" | ||
imports = import_string.format( | ||
custom_imports="\n" + "\n".join(custom_imp) + "\n\n" | ||
) | ||
else: | ||
imports = import_string.format(custom_imports="") | ||
|
||
file_name = f"{typename:s}.py" | ||
|
||
|
@@ -321,6 +332,9 @@ def generate_class( | |
"TypedDict": TypedDict, | ||
"NotRequired": NotRequired, | ||
"Literal": Literal, | ||
"NumberType": typing.Union[ | ||
typing.SupportsFloat, typing.SupportsComplex, typing.SupportsInt | ||
], | ||
} | ||
# pylint: disable=exec-used | ||
exec(string, scope) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -183,16 +183,10 @@ def get_prop_typing( | |
"exact": generate_shape, | ||
"string": generate_type("str"), | ||
"bool": generate_type("bool"), | ||
"number": generate_type( | ||
"typing.Union[typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex]" | ||
), | ||
"node": generate_type( | ||
"typing.Union[str, int, float, ComponentType," | ||
" typing.Sequence[typing.Union" | ||
"[str, int, float, ComponentType]]]" | ||
), | ||
"number": generate_type("NumberType"), | ||
"node": generate_type("ComponentType"), | ||
"func": generate_any, | ||
"element": generate_type("ComponentType"), | ||
"element": generate_type("Component"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
"union": generate_union, | ||
"any": generate_any, | ||
"custom": generate_any, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,11 +11,55 @@ | |
t = TypeScriptComponent({0}) | ||
""" | ||
|
||
basic_app_template = """ | ||
from dash import Dash, html, dcc, callback, Input, Output | ||
|
||
def run_pyright(codefile: str): | ||
app = Dash() | ||
|
||
{0} | ||
app.layout = {1} | ||
|
||
@callback(Output("out", "children"), Input("btn", "n_clicks")) | ||
def on_click() -> html.Div: | ||
return {2} | ||
""" | ||
|
||
valid_layout = """html.Div([ | ||
html.H2('Valid'), | ||
'String in middle', | ||
123, | ||
404.4, | ||
dcc.Input(value='', id='in') | ||
]) | ||
""" | ||
valid_layout_list = """[ | ||
html.H2('Valid'), | ||
'String in middle', | ||
123, | ||
404.4, | ||
dcc.Input(value='', id='in') | ||
] | ||
""" | ||
valid_layout_function = """ | ||
def layout() -> html.Div: | ||
return html.Div(["hello layout"]) | ||
|
||
""" | ||
|
||
invalid_layout = """html.Div([ | ||
{"invalid": "dictionary in children"} | ||
]) | ||
""" | ||
# There is not invalid layout for function & list as explicitly typed as Any to avoid special cases. | ||
|
||
valid_callback = "html.Div('Valid')" | ||
invalid_callback = "[]" | ||
|
||
|
||
def run_module(codefile: str, module: str, extra: str = ""): | ||
|
||
cmd = shlex.split( | ||
f"pyright {codefile}", | ||
f"{sys.executable} -m {module} {codefile}{extra}", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
posix=sys.platform != "win32", | ||
comments=True, | ||
) | ||
|
@@ -32,17 +76,51 @@ def run_pyright(codefile: str): | |
return out.decode(), err.decode(), proc.poll() | ||
|
||
|
||
def assert_pyright_output( | ||
codefile: str, expected_outputs=tuple(), expected_errors=tuple(), expected_status=0 | ||
def assert_output( | ||
codefile: str, | ||
code: str, | ||
expected_outputs=tuple(), | ||
expected_errors=tuple(), | ||
expected_status=0, | ||
module="pyright", | ||
): | ||
output, error, status = run_pyright(codefile) | ||
output, error, status = run_module(codefile, module) | ||
assert ( | ||
status == expected_status | ||
), f"Status: {status}\nOutput: {output}\nError: {error}" | ||
), f"Status: {status}\nOutput: {output}\nError: {error}\nCode: {code}" | ||
for ex_out in expected_outputs: | ||
assert ex_out in output, f"Invalid output:\n {output}" | ||
for ex_err in expected_errors: | ||
assert ex_err in error | ||
assert ex_out in output, f"Invalid output:\n {output}\n\nCode: {code}" | ||
|
||
|
||
def format_template_and_save(template, filename, *args): | ||
formatted = template.format(*args) | ||
with open(filename, "w") as f: | ||
f.write(formatted) | ||
return formatted | ||
|
||
|
||
def expect(status=None, outputs=None, modular=False): | ||
data = {} | ||
if status is not None: | ||
data["expected_status"] = status | ||
if outputs is not None: | ||
data["expected_outputs"] = outputs | ||
if modular: | ||
# The expectations are per module. | ||
data["modular"] = modular | ||
return data | ||
|
||
|
||
@pytest.fixture() | ||
def change_dir(): | ||
original_dir = os.getcwd() | ||
|
||
def change(dirname): | ||
os.chdir(dirname) | ||
|
||
yield change | ||
|
||
os.chdir(original_dir) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
|
@@ -205,7 +283,7 @@ def assert_pyright_output( | |
"expected_status": 1, | ||
"expected_outputs": [ | ||
'Argument of type "tuple[Literal[1], Literal[2]]" cannot be assigned ' | ||
'to parameter "a_tuple" of type "Tuple[SupportsFloat | SupportsInt | SupportsComplex, str] | None' | ||
'to parameter "a_tuple" of type "Tuple[NumberType, str] | None' | ||
], | ||
}, | ||
), | ||
|
@@ -247,9 +325,35 @@ def assert_pyright_output( | |
), | ||
], | ||
) | ||
def test_component_typing(arguments, assertions, tmp_path): | ||
def test_typi001_component_typing(arguments, assertions, tmp_path): | ||
codefile = os.path.join(tmp_path, "code.py") | ||
with open(codefile, "w") as f: | ||
f.write(component_template.format(arguments)) | ||
code = format_template_and_save(component_template, codefile, arguments) | ||
assert_output(codefile, code, module="pyright", **assertions) | ||
|
||
|
||
typing_modules = ["pyright"] | ||
|
||
if sys.version_info.minor >= 10: | ||
typing_modules.append("mypy") | ||
|
||
assert_pyright_output(codefile, **assertions) | ||
|
||
@pytest.mark.parametrize("typing_module", typing_modules) | ||
@pytest.mark.parametrize( | ||
"prelayout, layout, callback_return, assertions", | ||
[ | ||
("", valid_layout, valid_callback, expect(status=0)), | ||
("", valid_layout_list, valid_callback, expect(status=0)), | ||
(valid_layout_function, "layout", valid_callback, expect(status=0)), | ||
("", valid_layout, invalid_callback, expect(status=1)), | ||
("", invalid_layout, valid_callback, expect(status=1)), | ||
], | ||
) | ||
def test_typi002_typing_compliance( | ||
typing_module, prelayout, layout, callback_return, assertions, tmp_path, change_dir | ||
): | ||
codefile = os.path.join(tmp_path, "code.py") | ||
os.chdir(tmp_path) | ||
code = format_template_and_save( | ||
basic_app_template, codefile, prelayout, layout, callback_return | ||
) | ||
assert_output(codefile, code, module=typing_module, **assertions) |
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.
Note: types defined in try except don't always resolve.
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 makes sense now that you've said it but damn - I'm liking Python's type system less and less as time goes by…