From 471550d643c63f31e5c17bdd25dccca5d4488138 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Sat, 2 Mar 2024 12:09:34 +0000 Subject: [PATCH] Minor tidy ups, added comments, renamed 'via_function' to 'with_function' for reading age reasons. --- examples/farmyard/main.py | 34 +++++++---------- src/invent/ui/core.py | 80 ++++++++++++++++++++++++++++----------- src/invent/ui/exporter.py | 18 +++++---- 3 files changed, 81 insertions(+), 51 deletions(-) diff --git a/examples/farmyard/main.py b/examples/farmyard/main.py index 8346a50..1fc73b2 100644 --- a/examples/farmyard/main.py +++ b/examples/farmyard/main.py @@ -36,19 +36,11 @@ def make_oink(message): def make_geese(number_of_honks): - return [ - invent.ui.TextBox(text="🪿") - - for _ in range(number_of_honks) - ] + return [invent.ui.TextBox(text="🪿") for _ in range(number_of_honks)] def make_pigs(number_of_oinks): - return [ - invent.ui.TextBox(text="🐖") - - for _ in range(number_of_oinks) - ] + return [invent.ui.TextBox(text="🐖") for _ in range(number_of_oinks)] # Channels ############################################################################# @@ -92,7 +84,9 @@ def make_pigs(number_of_oinks): ), invent.ui.TextBox( name="number_of_honks", - text=invent.ui.from_datastore("number_of_honks"), + text=invent.ui.from_datastore( + "number_of_honks" + ), position="MIDDLE-CENTER", ), ], @@ -101,8 +95,8 @@ def make_pigs(number_of_oinks): id="geese", position="CENTER", content=invent.ui.from_datastore( - "number_of_honks", via_function=make_geese - ) + "number_of_honks", with_function=make_geese + ), ), invent.ui.Button( name="to_code", @@ -140,7 +134,9 @@ def make_pigs(number_of_oinks): ), invent.ui.TextBox( name="number_of_oinks", - text=invent.ui.from_datastore("number_of_oinks"), + text=invent.ui.from_datastore( + "number_of_oinks" + ), position="MIDDLE-CENTER", ), ], @@ -149,8 +145,8 @@ def make_pigs(number_of_oinks): id="pigs", position="CENTER", content=invent.ui.from_datastore( - "number_of_oinks", via_function=make_pigs - ) + "number_of_oinks", with_function=make_pigs + ), ), invent.ui.Button( name="to_code", @@ -189,10 +185,8 @@ def make_pigs(number_of_oinks): ), ] ), - invent.ui.Code( - code=exporter.as_python_code(app) - ) - ] + invent.ui.Code(code=exporter.as_python_code(app)), + ], ) ) diff --git a/src/invent/ui/core.py b/src/invent/ui/core.py index 7c79022..c2ee4aa 100644 --- a/src/invent/ui/core.py +++ b/src/invent/ui/core.py @@ -112,19 +112,19 @@ class from_datastore: # NOQA orthodox capitalised camel case, for aesthetic reasons. ;-) """ - def __init__(self, key, via_function=None): + def __init__(self, key, with_function=None): """ The key identifies the value in the datastore. """ self.key = key - self.via_function = via_function + self.with_function = with_function def __repr__(self): """Create the expression for a property that gets its value from the datastore.""" expression = f"from_datastore('{self.key}'" - if self.via_function: - expression += f", via_function={self.via_function.__name__}" + if self.with_function: + expression += f", with_function={self.with_function.__name__}" expression += ")" return expression @@ -182,7 +182,8 @@ def __set__(self, obj, value): """ if isinstance(value, from_datastore): - via_function = value.via_function + via_function = value.with_function + def reactor(message): # pragma: no cover """ Set the value in the widget and call the optional @@ -201,9 +202,6 @@ def reactor(message): # pragma: no cover # Attach the "from_datastore" instance to the object. setattr(obj, self.private_name + "_from_datastore", value) - from pyscript import window - window.console.log(f'Setting: {self.private_name + "_from_datastore"}') - # Subscribe to store events for the specified key. invent.subscribe( reactor, to_channel="store-data", when_subject=value.key @@ -301,7 +299,7 @@ def coerce(self, value): result = int(value) return result except ValueError: - pass # Handle below + pass # Handle below raise ValueError(_("Not a valid number: ") + value) def validate(self, value): @@ -402,7 +400,6 @@ def coerce(self, value): """ # Don't coerce None because None may be a valid value. return str(value) if value is not None else None - return str(value) def validate(self, value): """ @@ -465,7 +462,6 @@ def validate(self, value): """ Ensure the property's value is a boolean value (True / False). """ - return super().validate(self.coerce(value)) @@ -508,10 +504,13 @@ class Component: A base class for all user interface components (Widget, Container). Ensures they all have optional names and ids. If they're not given, will - auto-generate them for the user. + auto-generate them for the user. The position of the component determines + how it will be drawn in its parent. """ + # Used for quick component look-up. _components_by_id = {} + # Used for generating unique component names. _component_counter = 0 id = TextProperty("The id of the widget instance in the DOM.") @@ -545,26 +544,50 @@ def update(self, **kwargs): a property on this class, set the property's value to the associated value in the dict. """ + # Set values from the **kwargs for k, v in kwargs.items(): if hasattr(self, k): setattr(self, k, v) - + # Set values from the property's own default_values. for property_name, property_obj in type(self).properties().items(): if property_name not in kwargs: if property_obj.default_value is not None: setattr(self, property_name, property_obj.default_value) def on_id_changed(self): + """ + Automatically called to update the id of the HTML element associated + with the component. + """ self.element.id = self.id def on_name_changed(self): + """ + Automatically called to update the name attribute of the HTML element + associated with the component. + """ if self.name: self.element.setAttribute("name", self.name) else: self.element.removeAttribute("name") + def on_position_changed(self): + """ + Automatically called to update the position information relating to + the HTML element associated with the component. + """ + if self.element.parentElement: + self.set_position(self.element.parentElement) + @classmethod def _generate_name(cls): + """ + Create a human friendly name for the component. + + E.g. + + "Button 1" + """ cls._component_counter += 1 return f"{cls.__name__} {cls._component_counter}" @@ -577,10 +600,18 @@ def get_component_by_id(cls, component_id): return Component._components_by_id.get(component_id) def render(self): + """ + In base classes, return the HTML element used to display the + component. + """ raise NotImplementedError() # pragma: no cover @classmethod def preview(cls): + """ + In base classes, return the outerHTML to display in the menu of + available components. + """ raise NotImplementedError() # pragma: no cover @classmethod @@ -648,7 +679,7 @@ def blueprint(cls): def as_dict(self): """ - Return a dict representation of the state of this component's + Return a dict representation of the state of this instance's properties and message blueprints. """ properties = { @@ -693,7 +724,7 @@ def parse_position(self): if not (horizontal_position or vertical_position): # Bail out if we don't have a valid position state. raise ValueError(f"'{self.position}' is not a valid position.") - return (vertical_position, horizontal_position) + return vertical_position, horizontal_position def set_position(self, container): """ @@ -702,8 +733,10 @@ def set_position(self, container): the element into the expected position in the container. """ - # Reset... def reset(): + """ + Reset the style state for the component and its parent container. + """ self.element.style.removeProperty("width") self.element.style.removeProperty("height") container.style.removeProperty("align-self") @@ -749,26 +782,27 @@ def reset(): class Widget(Component): """ + A widget is a UI component drawn onto the interface in some way. + All widgets have these things: * A unique human friendly name that's meaningful in the context of the application (if none is given, one is automatically generated). * A unique id (if none is given, one is automatically generated). - * An indication of the widget's preferred position (default: top left). + * An indication of the widget's preferred position. * A render function that takes the widget's container and renders itself as an HTML element into the container. - * An optional channel name to which it broadcasts its messages (defaults to - the id). + * An optional indication of the channel[s] to which it broadcasts + messages (defaults to the id). + * A publish method that takes the name of a message blueprint, and + associated kwargs, and publishes it to the channel[s] set for the + widget. """ channel = TextProperty( "A comma separated list of channels to which the widget broadcasts." ) - def on_position_changed(self): - if self.element.parentElement: - self.set_position(self.element.parentElement) - def publish(self, blueprint, **kwargs): """ Given the name of one of the class's MessageBlueprints, publish diff --git a/src/invent/ui/exporter.py b/src/invent/ui/exporter.py index 56941c2..88f713d 100644 --- a/src/invent/ui/exporter.py +++ b/src/invent/ui/exporter.py @@ -4,7 +4,6 @@ """ - from invent.ui import Container @@ -97,13 +96,13 @@ def make_pigs(value_from_datastore): def as_python_code(app): - """ Generate the *textual* Python code for the app.""" + """Generate the *textual* Python code for the app.""" return MAIN_PY_TEMPLATE.format( imports=IMPORTS, datastore=DATASTORE, code=CODE, - app=_pretty_repr_app(app) + app=_pretty_repr_app(app), ) @@ -126,8 +125,7 @@ def _pretty_repr_app(app): """Generate a pretty repr of the App's UI.""" return APP_TEMPLATE.format( - name=app.name, - pages=_pretty_repr_pages(app.content) + name=app.name, pages=_pretty_repr_pages(app.content) ) @@ -136,7 +134,7 @@ def _pretty_repr_pages(pages): lines = [] for page in pages: - _pretty_repr_component(page, lines=lines, indent=" "*8) + _pretty_repr_component(page, lines=lines, indent=" " * 8) return "\n".join(lines) @@ -167,7 +165,9 @@ def _pretty_repr_component(component, lines, indent=""): if is_container and property_name == "content": continue - from_datastore = getattr(component, f"_{property_name}_from_datastore", None) + from_datastore = getattr( + component, f"_{property_name}_from_datastore", None + ) if from_datastore: property_value = from_datastore @@ -186,7 +186,9 @@ def _pretty_repr_component(component, lines, indent=""): else: lines.append(f"{indent}content=[") for child in component.content: - _pretty_repr_component(child, lines=lines, indent=indent + " ") + _pretty_repr_component( + child, lines=lines, indent=indent + " " + ) lines.append(f"{indent}],") # The last line of the component's constructor e.g.")" :) ##########################