Skip to content

Commit

Permalink
Fix up generate_from_spec.py and fully generate checkbox from Compone…
Browse files Browse the repository at this point in the history
…ntSpec (and pass tests)
  • Loading branch information
wwwillchen committed Dec 14, 2023
1 parent ffbbec2 commit 69326e9
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 79 deletions.
4 changes: 2 additions & 2 deletions component_specs/BUILD
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//build_defs:defaults.bzl", "py_binary", "py_library")
load("//build_defs:defaults.bzl", "THIRD_PARTY_PY_ABSL_PY", "py_binary", "py_library")

package(
default_visibility = ["//build_defs:mesop_internal"],
Expand All @@ -19,5 +19,5 @@ py_binary(
deps = [
# ":component_specs",
"//mesop/protos:ui_py_pb2",
],
] + THIRD_PARTY_PY_ABSL_PY,
)
81 changes: 71 additions & 10 deletions component_specs/checkbox_spec.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import os.path

from absl import app, flags

import mesop.protos.ui_pb2 as pb
from component_specs.generate_from_spec import (
generate_ng_template,
Expand All @@ -6,35 +10,92 @@
generate_py_component,
)

FLAGS = flags.FLAGS

flags.DEFINE_bool("write", False, "set to true to write files.")


# // <mat-checkbox [checked]="isChecked" (change)="handleCheckboxChange($event)">
# // {{config().getLabel()}}
# // </mat-checkbox>
CHECKBOX_SPEC = pb.ComponentSpec(
type_name="checkbox",
element_name="mat-checkbox",
ng_module=pb.NgModuleSpec(
import_path="@angular/material/checkbox",
module_name="MatCheckboxModule",
other_symbols=["MatCheckboxChange"],
),
props=[
pb.ElementProp(
key="checked",
property_binding=pb.PropertyBinding(name="value", type=pb.JsType.STRING),
property_binding=pb.PropertyBinding(name="value", type=pb.JsType.BOOL),
), # TODO: need a special marker for value (default value)
pb.ElementProp(
key="change",
event_binding=pb.EventBinding(
event_name="MatCheckboxChange",
props=[pb.EventProp(key="changed", type=pb.JsType.BOOL)],
props=[pb.EventProp(key="checked", type=pb.JsType.BOOL)],
),
),
],
content=pb.ContentSpec(name="label", type=pb.JsType.STRING),
)


def main(argv):
if FLAGS.write:
write(
generate_ng_template(CHECKBOX_SPEC),
CHECKBOX_SPEC.type_name,
ext="ng.html",
)
write(
generate_ng_ts(CHECKBOX_SPEC),
CHECKBOX_SPEC.type_name,
ext="ts",
)
write(
generate_proto_schema(CHECKBOX_SPEC),
CHECKBOX_SPEC.type_name,
ext="proto",
)
write(
generate_py_component(CHECKBOX_SPEC),
CHECKBOX_SPEC.type_name,
ext="py",
)

print("Written files")
else:
print(".ng.html:")
print(generate_ng_template(CHECKBOX_SPEC))
print(".ts:")
print(generate_ng_ts(CHECKBOX_SPEC))
print(".proto:")
print(generate_proto_schema(CHECKBOX_SPEC))
print(":.py:")
print(generate_py_component(CHECKBOX_SPEC))


def write(contents: str, name: str, ext: str):
with open(
os.path.join(
get_bazel_workspace_directory(), f"mesop/components/{name}/{name}.{ext}"
),
"w",
) as f:
f.write(contents)


def get_bazel_workspace_directory() -> str:
value = os.environ.get("BUILD_WORKSPACE_DIRECTORY")
assert value
return value


# workspace_dir = get_bazel_workspace_directory()
# print(f"Bazel workspace directory: {workspace_dir}")

if __name__ == "__main__":
print(".ng.html:")
print(generate_ng_template(CHECKBOX_SPEC))
print(".ts:")
print(generate_ng_ts(CHECKBOX_SPEC))
print(".proto:")
print(generate_proto_schema(CHECKBOX_SPEC))
print(":.py:")
print(generate_py_component(CHECKBOX_SPEC))
app.run(main)
7 changes: 4 additions & 3 deletions component_specs/fixtures/component_name.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import {Component, Input} from '@angular/core';
import {
UserEvent,
Key,
Type,
} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
import {ComponentNameType} from 'mesop/mesop/components/component_name/component_name_jspb_proto_pb/mesop/components/component_name/component_name_pb';
import {Channel} from '../../web/src/services/channel';

@Component({
// selector: 'mesop-{component-name}',
templateUrl: 'component_name.ng.html',
standalone: true,
// GENERATE_NG_IMPORTS:
})
export class ComponentNameComponent {
@Input({required: true}) type!: Type;
@Input() key!: Key;
private _config!: ComponentNameType;
value: any;
value!: any;

constructor(private readonly channel: Channel) {}

ngOnChanges() {
this._config = ComponentNameType.deserializeBinary(
this.type.getValue() as unknown as Uint8Array,
);
this.value = this._config.getDefaultValue();
this.value = this._config.getValue();
}

config(): ComponentNameType {
Expand Down
79 changes: 60 additions & 19 deletions component_specs/generate_from_spec.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from typing import Any

import mesop.protos.ui_pb2 as pb

"""
Spec recipe:
1. Generate Angular template (done)
2. Generate Angular TS (done)
3. Generate proto (done)
4. Generate Python method (done)
Generates from component spec the following:
1. Angular template (ng.html)
2. Angular TS
3. Proto schema (.proto)
4. Python component
TODOs:
- seperate out component spec protos into its own file
- make it clear what the spelling (camel vs. snake_case)
MAYBEs:
- doesn't handle event with multiple properties
- have better naming for events (either explicit control, or implicitly strip out Mat)
"""
Expand Down Expand Up @@ -40,44 +46,60 @@ def generate_ng_ts(spec: pb.ComponentSpec) -> str:
default_value_prop = [
prop for prop in spec.props if prop.property_binding.name == "value"
][0]
symbols = ",".join(
[spec.ng_module.module_name] + list(spec.ng_module.other_symbols)
)
symbols = "{" + symbols + "}"

ts_template = (
f"import {symbols} from '{spec.ng_module.import_path}'\n" + ts_template
)
# Do simple string replacements
ts_template = (
ts_template.replace("component_name", spec.type_name)
.replace("ComponentName", upper_camel_case(spec.type_name))
.replace(
"value: any;",
f"value: {format_js_type(default_value_prop.property_binding.type)};",
"value!: any;",
f"value!: {format_js_type(default_value_prop.property_binding.type)};",
)
.replace("// INSERT_EVENT_METHODS:", generate_ts_event_methods(spec))
.replace("// GENERATE_NG_IMPORTS:", generate_ng_imports(spec))
)

# Build event handlers
for prop in spec.props:
if prop.HasField("event_binding"):
ts_template = ts_template.replace(
f"on{prop.event_binding.event_name}",
f"handle{prop.event_binding.event_name}",
f"on{prop.event_binding.event_name}",
)

return ts_template


def generate_ng_imports(spec: pb.ComponentSpec) -> str:
return f"imports: [{spec.ng_module.module_name}]"


def generate_proto_schema(spec: pb.ComponentSpec) -> str:
fields: list[str] = []
index = 0

for prop in spec.props:
index += 1
if prop.HasField("property_binding"):
fields.append(f"{prop.property_binding.name} = {index};")
fields.append(
f"{format_js_type_for_proto(prop.property_binding.type)} {prop.property_binding.name} = {index};"
)
if prop.HasField("event_binding"):
fields.append(
f"on_{snake_case(prop.event_binding.event_name)}_handler_id = {index};"
f"string on_{snake_case(prop.event_binding.event_name)}_handler_id = {index};"
)
if spec.HasField("content"):
index += 1
fields.append(f"{spec.content.name} = {index};")
fields.append(
f"{format_js_type_for_proto(spec.content.type)} {spec.content.name} = {index};"
)

message_contents = (
"{\n" + "\n".join([" " + field for field in fields]) + "\n}"
Expand Down Expand Up @@ -133,26 +155,26 @@ class {event_binding.event_name}Event(MesopEvent):
{event_binding.event_name}Event,
lambda event, key: {event_binding.event_name}Event(
key=key,
{event_binding.props[0].key}=event.{format_js_type_for_python(event_binding.props[0].type)},
{event_binding.props[0].key}=event.{format_js_type_for_proto(event_binding.props[0].type)},
),
)
"""


def generate_py_component_params(spec: pb.ComponentSpec) -> str:
out: list[str] = []
out: list[str] = ["key: str | None = None"]
for prop in spec.props:
if prop.HasField("property_binding"):
out.append(
f"{prop.property_binding.name}: {format_js_type_for_python(prop.property_binding.type)}"
f"{prop.property_binding.name}: {format_js_type_for_python(prop.property_binding.type)}={format_js_type_default_value_for_python(prop.property_binding.type)}"
)
if prop.HasField("event_binding"):
out.append(
f"on_{snake_case(prop.event_binding.event_name)}: Callable[[{prop.event_binding.event_name}Event], Any]"
f"on_{snake_case(prop.event_binding.event_name)}: Callable[[{prop.event_binding.event_name}Event], Any]|None=None"
)
if spec.HasField("content"):
out.append(
f"{spec.content.name}: {format_js_type_for_python(spec.content.type)}"
f"{spec.content.name}: {format_js_type_for_python(spec.content.type)}={format_js_type_default_value_for_python(spec.content.type)}"
)
return ", ".join(out)

Expand All @@ -163,8 +185,9 @@ def generate_py_proto_callsite(spec: pb.ComponentSpec) -> str:
if prop.HasField("property_binding"):
out.append(f"{prop.property_binding.name}={prop.property_binding.name}")
if prop.HasField("event_binding"):
# on_mat_checkbox_change_handler_id=handler_type(on_mat_checkbox_change) if on_mat_checkbox_change else ''
out.append(
f"on_{snake_case(prop.event_binding.event_name)}_handler_id=handler_type(on_{snake_case(prop.event_binding.event_name)})"
f"on_{snake_case(prop.event_binding.event_name)}_handler_id=handler_type(on_{snake_case(prop.event_binding.event_name)}) if on_{snake_case(prop.event_binding.event_name)} else ''"
)
if spec.HasField("content"):
out.append(f"{spec.content.name}={spec.content.name}")
Expand All @@ -181,9 +204,9 @@ def generate_ts_event_methods(spec: pb.ComponentSpec) -> str:

def generate_ts_event_method(event_binding: pb.EventBinding) -> str:
return f"""
handle{event_binding.event_name}(event: {event_binding.event_name}): void {{
on{event_binding.event_name}(event: {event_binding.event_name}): void {{
const userEvent = new UserEvent();
userEvent.set{format_js_type(event_binding.props[0].type).capitalize()}(event.{event_binding.props[0].key})
userEvent.set{format_js_type_for_proto(event_binding.props[0].type).capitalize()}(event.{event_binding.props[0].key})
userEvent.setHandlerId(this.config().getOn{event_binding.event_name}HandlerId())
userEvent.setKey(this.key);
this.channel.dispatch(userEvent);
Expand All @@ -209,6 +232,24 @@ def format_js_type_for_python(type: pb.JsType.ValueType) -> str:
raise Exception("not yet handled", type)


def format_js_type_default_value_for_python(type: pb.JsType.ValueType) -> Any:
if type == pb.JsType.BOOL:
return False
elif type == pb.JsType.STRING:
return "''"
else:
raise Exception("not yet handled", type)


def format_js_type_for_proto(type: pb.JsType.ValueType) -> str:
if type == pb.JsType.BOOL:
return "bool"
elif type == pb.JsType.STRING:
return "string"
else:
raise Exception("not yet handled", type)


def format_element_prop(prop: pb.ElementProp) -> str:
if prop.HasField("event_binding"):
return f"""({prop.key})="on{prop.event_binding.event_name}($event)\""""
Expand Down
3 changes: 0 additions & 3 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
# REF(//scripts/gen_component.py):insert_component_import_export
from mesop.components.box.box import box as box
from mesop.components.button.button import button as button
from mesop.components.checkbox.checkbox import (
CheckboxEvent as CheckboxEvent,
)
from mesop.components.checkbox.checkbox import (
checkbox as checkbox,
)
Expand Down
2 changes: 1 addition & 1 deletion mesop/components/checkbox/checkbox.ng.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<mat-checkbox [checked]="value" (change)="handleCheckboxChange($event)">
<mat-checkbox [checked]="value" (change)="onMatCheckboxChange($event)">
{{config().getLabel()}}
</mat-checkbox>
7 changes: 4 additions & 3 deletions mesop/components/checkbox/checkbox.proto
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

syntax = "proto3";

package mesop.components.checkbox;

message CheckboxType {
string label = 1;
string on_update_handler_id = 2;
bool default_value = 3;
bool value = 1;
string on_mat_checkbox_change_handler_id = 2;
string label = 3;
}
Loading

0 comments on commit 69326e9

Please sign in to comment.