GithubHelp home page GithubHelp logo

pyedifice / pyedifice Goto Github PK

View Code? Open in Web Editor NEW
262.0 262.0 11.0 12.58 MB

Declarative GUI framework for Python and Qt

Home Page: https://pyedifice.github.io

License: MIT License

Python 96.55% Shell 0.05% Nix 3.40%
declarative-ui gui model-view-update python qt virtualdom

pyedifice's Introduction

Edifice
Declarative GUI framework for Python and Qt

Edifice is a Python library for building declarative application user interfaces.

  • Modern declarative UI paradigm from web development.
  • 100% Python application development, no language inter-op.
  • A native desktop app instead of a bundled web browser.
  • Fast iteration via hot-reloading.

This modern declarative UI paradigm is also known as “The Elm Architecture,” or “Model-View-Update.”

Edifice uses PySide6 or PyQt6 as a backend. Edifice is like React, but with Python instead of JavaScript, and Qt Widgets instead of the HTML DOM.

If you have React experience, you'll find Edifice to be very easy to pick up. Edifice has function props and Hooks just like React.

Getting Started

Why Edifice?

Declarative

The premise of Edifice is that GUI designers should only need to worry about what is rendered on the screen, not how the content is rendered.

Most existing GUI libraries in Python, such as Tkinter and Qt, operate imperatively. To create a dynamic application using these libraries, you must not only think about what to display to the user given state changes, but also how to issue the commands to achieve the desired display.

Edifice allows you to declare what should be rendered given the current state, leaving the how to the library.

User interactions update the state, and state changes update the GUI. You only need to specify what is to be displayed given the current state and how user interactions modify this state.

With Edifice you write code like:

number, set_number = use_state(0)

with View():
    Button("Add 5", on_click=lambda event: set_number(number+5))
    Label(str(number))

and get the expected result: the GUI always displays a button and a label displaying the current value of number. Clicking the button adds 5 to the number, and Edifice will handle updating the GUI.

Edifice vs. QML

QML is Qt’s declarative GUI framework for Qt. Edifice differs from QML in these aspects:

  • Edifice programs are written purely in Python, whereas QML programs are written in Python + a special QML language + JavaScript.
  • Because Edifice interfaces are built in Python code, binding the code to the declared UI is much more straightforward.
  • Edifice makes it easy to create dynamic applications. It's easy to create, shuffle, and destroy widgets because the interface is written in Python code. QML assumes a much more static interface.

By analogy, QML is like HTML + JavaScript, whereas Edifice is like React.js. While QML and HTML are both declarative UI frameworks, they require imperative logic to add dynamism. Edifice and React allow fully dynamic applications to be specified declaratively.

How it works

An Edifice component declares the mapping from the state to UI. The state of a component is divided into props and state. props are passed to the component in the constructor, whereas state is the component’s own internal state.

Changes to props or state will trigger a re-render of the component and all its children. The old and new component trees will be compared to one another, and a diffing algorithm will determine which components previously existed and which ones are new (the algorithm behaves similarly to the React diffing algorithm). Components that previously existed will maintain their state, whereas their props will be updated. Finally, Edifice will try to ensure that the minimal update commands are issued to the UI. All this logic is handled by the library, and the components need not care about it.

Development Tools

Dynamic hot-reload

Dyanamic hot-reload is very useful for fine-tuning the presentation styles of Elements deep within your application. You can test if the margin should be 10px or 15px instantly without closing the app, reopening it, and waiting for everything to load.

To run your application with dynamic hot-reload, run:

python -m edifice path/to/app.py MyRootElement

This will run app.py with MyRootElement mounted as the root. A separate thread will listen to changes in all Python files in the directory containing app.py (recursing into subdirectories). You can customize which directory to listen to using the --dir flag.

When a file in your application is changed, the loader will reload all components in that file with preserved props (since that state comes from the caller), reset state, and trigger a re-render in the main thread.

Because rendering is abstracted away, it is simple to diff the UI trees and have the Edifice renderer figure out what to do using its normal logic.

Element Inspector

Similar to the Inspect Elements tool of a browser, the Element inspector will show you all Elements in your application along with the props and state, allowing you to examine the internal state of your complex component without writing a million print statements. Since the UI is specified as a (pure) function of state, the state you see completely describes your application, and you can even do things like rewinding to a previous state.

set_trace()

PDB does not work well with PyQt applications. edifice.set_trace() is equivalent to pdb.set_trace(), but it can properly pause the PyQt event loop to enable use of the debugger (users of PySide need not worry about this).

License

Edifice is MIT Licensed.

Edifice uses Qt under the hood, and both PyQt6 and PySide6 are supported. Note that PyQt6 is distributed with the GPL license while PySide6 is distributed under the more flexible LGPL license. See PyQt vs PySide Licensing.

Version History / Change Log / Release Notes

See Release Notes (source: versions.rst)

Contribution

Contributions are welcome; please send Pull Requests! See DEVELOPMENT.md for development notes.

When submitting a Pull Request, think about adding tests to tests and adding a line to the Unreleased section of the change log versions.rst.

Poetry Build System

The Poetry pyproject.toml specifies the package dependecies.

Because Edifice supports PySide6 and PyQt6 at the same time, neither are required by [tool.poetry.dependencies]. Instead they are both optional [tool.poetry.group.dev.dependencies]. A project which depends on Edifice should also depend on either PySide6 or PyQt6.

The requirements.txt is generated by

poetry export -f requirements.txt --output requirements.txt

To use the latest Edifice with a Poetry pyproject.toml from Github instead of PyPI, see Poetry git dependencies, for example:

[tool.poetry.dependencies]
python = ">=3.10,<3.11"
pyedifice = {git = "https://github.com/pyedifice/pyedifice.git"}
pyside6 = "6.5.1.1"

pyedifice's People

Contributors

adigitoleo avatar considerate avatar fding avatar jamesdbrock avatar joancoco avatar milescsmith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pyedifice's Issues

TextInput active prop

How to inactivate a TextInput?
TextInput('---', active=False)
Tried this way but active is not an allowed argument.

should_update() bug

Encountered error while rendering. Unwinding changes.
Exception in callback App._defer_rerender.<locals>.rerender_callback() at /nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py:199
handle: <Handle App._defer_rerender.<locals>.rerender_callback() at /nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py:199>
Traceback (most recent call last):
  File "/nix/store/s6fgyqbk8vn1014daznm5kqx90xdn86x-python3-3.10.13/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py", line 202, in rerender_callback
    self._request_rerender(list(els), {})
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py", line 232, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 700, in _request_rerender
    widget_trees = self._gen_widget_trees(components, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 691, in _gen_widget_trees
    widget_trees.append(self._render(component, render_context))
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 665, in _render
    render_context.widget_tree[component] = self._update_old_component(
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 544, in _recycle_children
    self._update_old_component(old_children[0], component.children[0], render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 665, in _render
    render_context.widget_tree[component] = self._update_old_component(
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 544, in _recycle_children
    self._update_old_component(old_children[0], component.children[0], render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 665, in _render
    render_context.widget_tree[component] = self._update_old_component(
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 544, in _recycle_children
    self._update_old_component(old_children[0], component.children[0], render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in _recycle_children
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 557, in <listcomp>
    children = [self._get_child_using_key(key_to_old_child, new_child._key, new_child, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 524, in _get_child_using_key
    self._update_old_component(d[key], newchild, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 513, in _update_old_component
    rerendered_obj = self._render(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 625, in _render
    ret = self._render_base_component(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 580, in _render_base_component
    children = self._recycle_children(component, render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 544, in _recycle_children
    self._update_old_component(old_children[0], component.children[0], render_context)
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 511, in _update_old_component
    if component._should_update(newprops, {}):
  File "/nix/store/jimxmklvfixbav2cbc55risln5fy7f35-python3-3.10.13-env/lib/python3.10/site-packages/edifice/_component.py", line 489, in _should_update
    if v2 is None or v2 != v:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

Facing Error: qt.qpa.plugin: Could not load the Qt platform plugin "windows" in "" even though it was found.

Hi,
I'm trying to use pyedifice but on running the code, faced with error

qt.qpa.plugin: Could not load the Qt platform plugin "windows" in "" even though it was found.
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.

Available platform plugins are: direct2d, minimal, offscreen, windows.

The program used is calculator.py which is provided in the examples directory.

Solutions Tried:

1. Installed pyqt5 (pip install pyqt5)
2. Installed PyQt5-tools (pip install PyQt5-tools)
3. Reinstalled pyedifice

Systems detials:

  • OS: Windows 10 (Build: 19042.746)
  • Python Version: 3.8.5

Regards

Form widgets `label_map` doesn't work as expected

The label_map argument for Form and FormDialog doesn't work: You are trying to subscribe to the label instead of the actual key.

import pathlib as p

import edifice as ed
from edifice.components.forms import Form

form_data = ed.StateManager(
    {
        "Value 1": p.Path(""),
        "Value 2": "Some text",
        "Value 3": 4.2,
    }
)

labels = {
    # Uncommenting the second or third label causes a KeyError.
    "Value 1": "First value",
    # "Value 2": "Second value",
    # "Value 3": "Third value",
}

ed.App(ed.Window()(Form(form_data, label_map=labels))).start()
print(form_data.as_dict())

Support for Qt Grid Layout

Grid layouts are a powerful tool for laying out elements exactly how you want. The available space is divided into a large grid (e.g. 12 by 12), and each child specifies how much space it needs. The layout algorithm should put each child in the top-left of the available space.

Running edifice in a asyncio loop

Hello,

I'd like to know if there is a method for me to run an Edifice application in an event loop from AsyncIO? I'd need this to avoid workarounds for a project I am working on in which I would ideally like to utilize TwitchIO for a chat bot and Edifice as a front end to control said bot.

Judging from the code of App.start() I would assume that that should be possible, but I didn't find a good method to do so. Any insights?

Nothing is displayed

Hi,

When I run any of the examples in the docs I never get anything displayed on the screen. I don't get any errors logged and I see the python app spawning but macos says it's not responding. Top shows it using 100% of CPU and I need to kill it to get it to stop. I have tried it with both PySide2 and PyQt5 with the same results. The plain Qt example you have works fine.

Any ideas?

Dynamic loading maximum recursion error

import edifice as ed
from edifice import Label, TextInput, View, Button

METER_TO_FEET = 3.28084

def str_to_float(s:str):
    try:
        return float(s)
    except ValueError:
        return 0.0

class ConversionWidget(ed.Component):
    @ed.register_props
    def __init__(self, from_unit, to_unit, factor):
        super().__init__()
        self.current_text = "0.0"

    def render(self):
        from_text = self.current_text
        to_text = "%.3f" % (str_to_float(from_text) * self.props.factor)

        from_label_style = {"width": 170}
        to_label_style = {"margin-left": 20, "width": 200}
        input_style = {"padding": 2, "width": 120}
        return View(layout="row", style={"margin": 10, "width": 560})(
            Label(f"Measurement in {self.props.from_unit}:", style=from_label_style),
            TextInput(self.current_text, style=input_style,
                      on_change=lambda text: self.set_state(current_text=text)),
            Label(f"Measurement in {self.props.to_unit}: {to_text}", style=to_label_style),
        )

class MyApp(ed.Component):
    def __init__(self):
        super().__init__()
        self.meters:str = "0.0"
        self.conversions: list[ConversionWidget] = [
            ConversionWidget("meters", "feet", METER_TO_FEET),
            ConversionWidget("feet", "meters", 1/METER_TO_FEET),
        ]
        self.new_from:str = ""
        self.new_to:str = ""
        self.new_factor:str = ""

    def render(self):
        conversion = self.conversions
        return View(layout="column")(
            *conversion,
            View(layout="row")(
                Label("From"),
                TextInput(self.new_from, on_change=lambda text:self.set_state(new_from=text)),
                Label("To"),
                TextInput(self.new_to, on_change=lambda text:self.set_state(new_to=text)),
                Label("Factor"),
                TextInput(self.new_factor, on_change=lambda text:self.set_state(new_factor=text)),
                Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
            )
        )

if __name__ == "__main__":
    ed.App(MyApp()).start()

So was trying to add dynamic button in tutorial code.
This ran into problem (I didn't make on_click at the button to callable, adding lambda x: before self.set_state solved the problem)
And this is what I get when I run the code

Traceback (most recent call last):
  File "/workspaces/test-edifice/test.py", line 60, in <module>
    ed.App(MyApp()).start()
    ^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 121, in __init__
    rendered_component = component.render()
                         ^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute '_request_rerender'

However, if I try to dynamic load the code using python -m pyedifice ./test.py MyApp from the code which can be run properly, something like this is occured

(...)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 193, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 425, in _request_rerender
    widget_trees = self._gen_widget_trees(components, render_context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 418, in _gen_widget_trees
    widget_trees.append(self._render(component, render_context))
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 387, in _render
    sub_component = component.render()
                    ^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 193, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 425, in _request_rerender
    widget_trees = self._gen_widget_trees(components, render_context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 418, in _gen_widget_trees
    widget_trees.append(self._render(component, render_context))
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 387, in _render
    sub_component = component.render()
                    ^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 193, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 425, in _request_rerender
    widget_trees = self._gen_widget_trees(components, render_context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 418, in _gen_widget_trees
    widget_trees.append(self._render(component, render_context))
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 387, in _render
    sub_component = component.render()
                    ^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 193, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 425, in _request_rerender
    widget_trees = self._gen_widget_trees(components, render_context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 418, in _gen_widget_trees
    widget_trees.append(self._render(component, render_context))
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 387, in _render
    sub_component = component.render()
                    ^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 408, in set_state
    raise e
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/_component.py", line 404, in set_state
    self._controller._request_rerender([self], kwargs)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/app.py", line 193, in _request_rerender
    render_result = self._render_engine._request_rerender(components)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 423, in _request_rerender
    with _storage_manager() as storage_manager:
  File "/usr/local/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 47, in _storage_manager
    changes.unwind()
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/engine.py", line 27, in unwind
    logger.warning("Encountered error while rendering. Unwinding changes.")
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1501, in warning
    self._log(WARNING, msg, args, **kwargs)
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1632, in _log
    record = self.makeRecord(self.name, level, fn, lno, msg, args,
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1601, in makeRecord
    rv = _logRecordFactory(name, level, fn, lno, msg, args, exc_info, func,
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/logging/__init__.py", line 326, in __init__
    self.filename = os.path.basename(pathname)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen posixpath>", line 143, in basename
Stemming from these renders:
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/test-edifice/test.py", line 55, in render
    Button("Press Me", on_click=self.set_state(conversions=conversion + [ConversionWidget(self.new_from, self.new_to, str_to_float(self.new_factor))])),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 160 more times]
  File "/usr/local/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1501, in warning
    self._log(WARNING, msg, args, **kwargs)
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1632, in _log
    record = self.makeRecord(self.name, level, fn, lno, msg, args,
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/logging/__init__.py", line 1601, in makeRecord
    rv = _logRecordFactory(name, level, fn, lno, msg, args, exc_info, func,
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/logging/__init__.py", line 326, in __init__
    self.filename = os.path.basename(pathname)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen posixpath>", line 143, in basename
RecursionError: maximum recursion depth exceeded

new component element and hot-reloading

The new @component elements don't work with hot reloading.

Encountered exception while reloading: Error while reloading <MyApp id=0x7fffdbac48e0  />: New class expects prop (args) not present in old class
Traceback (most recent call last):
  File "/home/jbrock/work/oth/pyedifice/pyedifice/edifice/app.py", line 157, in event
    render_result = self._render_engine._refresh_by_class(classes)
  File "/home/jbrock/work/oth/pyedifice/pyedifice/edifice/engine.py", line 451, in _refresh_by_class
    raise ValueError(
Stemming from these renders:
ValueError: Error while reloading <MyApp id=0x7fffdbac48e0  />: New class expects prop (args) not present in old class

I think it has something to do with this.

def __init__(self, *args: P.args, **kwargs: P.kwargs):
name_to_val = defaults.copy()
name_to_val.update(filter(not_ignored, zip(varnames, args, strict=False)))
name_to_val.update(((k, v) for (k, v) in kwargs.items() if k[0] != "_"))
name_to_val["children"] = name_to_val.get("children") or []
self._register_props(name_to_val)

keyboard, `space bar` or `enter` cannot trigger a button clicking

pressing space bar when focusing on a button will trigger an animation of button click, but the button's on_click is not called, have to use mouse to do a "real click"

while pressing enter cannot even generate a button clicked animation

p.s. this project has very promising future, but current version still very incomplete, i would hope to be more skillful to contribute to this project but right now i'm a newbie especially in Qt

Error on using dynamic loading

Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/home/vscode/.local/lib/python3.11/site-packages/watchdog/observers/api.py", line 204, in run
    self.dispatch_events(self.event_queue)
  File "/home/vscode/.local/lib/python3.11/site-packages/watchdog/observers/api.py", line 380, in dispatch_events
    handler.dispatch(event)
  File "/home/vscode/.local/lib/python3.11/site-packages/watchdog/events.py", line 276, in dispatch
    {
  File "/home/vscode/.local/lib/python3.11/site-packages/edifice/runner.py", line 119, in on_modified
    if isinstance(observer, FSEventsObserver) and event.is_directory:
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

Looks like importing watchdog.observers.fsevents.FSEventsObserver caused ModuleNotFoundError somehow (I don't know why so I added it to issue of watchdog: gorakhargosh/watchdog#1008) and messed up

try:
    from watchdog.observers.fsevents import FSEventsObserver
except ImportError:
    FSEventsObserver = None

in runner.py.

I monkey-patched the code like this -

        def on_modified(self, event):
            if FSEventsObserver is not None and isinstance(observer, FSEventsObserver) and event.is_directory:

(line 117-118) and it worked properly

Maybe something related to my installation (I install this on codespace with devcontainer and quite a lot of apt install mess) or installation of watchdog went something wrong.

(Oh and plus - this project doesn't requires watchdog in requirements.txt which made me install watchdog manually, would be nice if it is installed along with edifice when I just pip install pyedifice.)

Crash on hot reload with dropdown

Hot reloading crashes for me when using a dropdown. I noticed this while working on something of mine, but the financial example experiences the same crash.

I'm using Python 3.11.7 with PySide6 running on macOS 14.1.1

Here's the error message I get:

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndexedSubscript:]: index 0 beyond bounds for empty array'
*** First throw call stack:
(
        0   CoreFoundation                      0x000000018a526800 __exceptionPreprocess + 176
        1   libobjc.A.dylib                     0x000000018a01deb4 objc_exception_throw + 60
        2   CoreFoundation                      0x000000018a49cfc0 -[__NSCFString hasSuffix:] + 0
        3   libqcocoa.dylib                     0x000000029442a044 _ZN20QCocoaSystemTrayIcon13emitActivatedEv + 274796
        4   libqcocoa.dylib                     0x000000029442a0c8 _ZN20QCocoaSystemTrayIcon13emitActivatedEv + 274928
        5   libqcocoa.dylib                     0x0000000294428688 _ZN20QCocoaSystemTrayIcon13emitActivatedEv + 268208
        6   QtWidgets                           0x0000000128bc11a8 _ZN9QListView16selectionChangedERK14QItemSelectionS2_ + 204
        7   QtCore                              0x000000011b61f48c _ZN11QMetaObject8activateEP7QObjectPKS_iPPv + 3808
        8   QtCore                              0x000000011b7e5e44 _ZN19QItemSelectionModel20emitSelectionChangedERK14QItemSelectionS2_ + 216
        9   QtCore                              0x000000011b7e5a2c _ZN19QItemSelectionModel6selectERK14QItemSelection6QFlagsINS_13SelectionFlagEE + 636
        10  QtCore                              0x000000011b7e570c _ZN19QItemSelectionModel6selectERK11QModelIndex6QFlagsINS_13SelectionFlagEE + 68
        11  QtCore                              0x000000011b7e68cc _ZN19QItemSelectionModel15setCurrentIndexERK11QModelIndex6QFlagsINS_13SelectionFlagEE + 260
        12  QtWidgets                           0x0000000128a1f338 _ZNK9QComboBox5countEv + 828
        13  QtWidgets                           0x0000000128a228b4 _ZN9QComboBoxC1ER16QComboBoxPrivateP7QWidget + 2412
        14  QtCore                              0x000000011b61f224 _ZN11QMetaObject8activateEP7QObjectPKS_iPPv + 3192
        15  QtCore                              0x000000011b7ce944 _ZN18QAbstractItemModel13endInsertRowsEv + 200
        16  QtGui                               0x000000016bc4f1a8 _ZN18QStandardItemModel11itemChangedEP13QStandardItem + 844
        17  QtWidgets                           0x0000000128a25e38 _ZN9QComboBox11insertItemsEiRK5QListI7QStringE + 340
        18  QtWidgets.abi3.so                   0x00000001699c990c _ZL26Sbk_QComboBoxFunc_addItemsP7_objectS0_ + 156
        19  libpython3.11.dylib                 0x00000001056478a0 cfunction_vectorcall_O + 332
        20  libpython3.11.dylib                 0x00000001056de8e0 _PyEval_EvalFrameDefault + 40744
        21  libpython3.11.dylib                 0x00000001056d4918 _PyEval_Vector + 200
        22  libpython3.11.dylib                 0x00000001055ff678 method_vectorcall + 448
        23  QtWidgets.abi3.so                   0x000000016952e814 _ZN14QWidgetWrapper5eventEP6QEvent + 220
        24  QtWidgets                           0x000000012890c594 _ZN19QApplicationPrivate13notify_helperEP7QObjectP6QEvent + 272
        25  QtWidgets                           0x000000012890df18 _ZN12QApplication6notifyEP7QObjectP6QEvent + 3368
        26  QtCore                              0x000000011b5d5474 _ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent + 292
        27  QtCore                              0x000000011b5d66fc _ZN23QCoreApplicationPrivate16sendPostedEventsEP7QObjectiP11QThreadData + 1448
        28  libqcocoa.dylib                     0x00000002943cacb0 qt_plugin_instance + 54476
        29  libqcocoa.dylib                     0x00000002943cbd74 qt_plugin_instance + 58768
        30  CoreFoundation                      0x000000018a4b1cfc __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28
        31  CoreFoundation                      0x000000018a4b1c90 __CFRunLoopDoSource0 + 176
        32  CoreFoundation                      0x000000018a4b1a00 __CFRunLoopDoSources0 + 244
        33  CoreFoundation                      0x000000018a4b05f0 __CFRunLoopRun + 828
        34  CoreFoundation                      0x000000018a4afc5c CFRunLoopRunSpecific + 608
        35  HIToolbox                           0x0000000194a2c448 RunCurrentEventLoopInMode + 292
        36  HIToolbox                           0x0000000194a2c284 ReceiveNextEventCommon + 648
        37  HIToolbox                           0x0000000194a2bfdc _BlockUntilNextEventMatchingListInModeWithFilter + 76
        38  AppKit                              0x000000018dc8ac54 _DPSNextEvent + 660
        39  AppKit                              0x000000018e460ebc -[NSApplication(NSEventRouting) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 716
        40  AppKit                              0x000000018dc7e100 -[NSApplication run] + 476
        41  libqcocoa.dylib                     0x00000002943c98fc qt_plugin_instance + 49432
        42  QtCore                              0x000000011b5df160 _ZN10QEventLoop4execE6QFlagsINS_17ProcessEventsFlagEE + 540
        43  QtCore                              0x000000011b5d5b00 _ZN16QCoreApplication4execEv + 112
        44  QtWidgets.abi3.so                   0x0000000169a8b2d8 _ZL25Sbk_QApplicationFunc_execP7_object + 60
        45  libpython3.11.dylib                 0x0000000105647734 cfunction_vectorcall_NOARGS + 324
        46  libpython3.11.dylib                 0x00000001055fcf88 PyObject_Vectorcall + 80
        47  libpython3.11.dylib                 0x00000001056dcdcc _PyEval_EvalFrameDefault + 33812
        48  libpython3.11.dylib                 0x00000001056d47e8 PyEval_EvalCode + 272
        49  libpython3.11.dylib                 0x00000001056d1320 builtin_exec + 916
        50  libpython3.11.dylib                 0x00000001056475d0 cfunction_vectorcall_FASTCALL_KEYWORDS + 160
        51  libpython3.11.dylib                 0x00000001055fcf88 PyObject_Vectorcall + 80
        52  libpython3.11.dylib                 0x00000001056dcdcc _PyEval_EvalFrameDefault + 33812
        53  libpython3.11.dylib                 0x00000001056d4918 _PyEval_Vector + 200
        54  libpython3.11.dylib                 0x0000000105745270 pymain_run_module + 200
        55  libpython3.11.dylib                 0x0000000105744b18 Py_RunMain + 1284
        56  libpython3.11.dylib                 0x00000001057450fc pymain_main + 324
        57  libpython3.11.dylib                 0x000000010574519c Py_BytesMain + 40
        58  dyld                                0x000000018a0590e0 start + 2360
)
libc++abi: terminating due to uncaught exception of type NSException

Let me know if I can provide any further details, thanks!

dynamical reloading not working

  File "C:\Users\mo-han\AppData\Local\Programs\Python\Python36\lib\site-packages\edifice\runner.py", line 118, in on_modified
    if isinstance(observer, FSEventsObserver) and event.is_directory:
TypeError: isinstance() arg 2 must be a type or tuple of types

because

try:
    from watchdog.observers.fsevents import FSEventsObserver
except ImportError:
    FSEventsObserver = None

and

In [1]: from watchdog.observers.fsevents import FSEventsObserver
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-1-16acfe703209> in <module>
----> 1 from watchdog.observers.fsevents import FSEventsObserver

c:\users\mo-han\appdata\local\programs\python\python36\lib\site-packages\watchdog\observers\fsevents.py in <module>
     29 import threading
     30 import unicodedata
---> 31 import _watchdog_fsevents as _fsevents
     32
     33 from watchdog.events import (

ModuleNotFoundError: No module named '_watchdog_fsevents'

in /watchdog/observers/__init__.py there are conditional importing:

...
elif platform.is_darwin():
    try:
        from .fsevents import FSEventsObserver as Observer
...
elif platform.is_windows():
    # TODO: find a reliable way of checking Windows version and import
    # polling explicitly for Windows XP
    try:
        from .read_directory_changes import WindowsApiObserver as Observer
    except Exception:
        from .polling import PollingObserver as Observer
        warnings.warn("Failed to import read_directory_changes. Fall back to polling.")

i've no experience for watchdog but maybe it's wrong to import that FSEventsObserver which is designed for Darwin.

Add message box widgets

For example, enaml has 'about', 'critical', 'information', 'question', and 'warning' functions that display an appropriate message box popup. Possibly relies on resolving #11, as they need to be in a separate window. In my opinion these are as important as the grid layout. It seems logical to use the Qt Dialog class for this:

https://doc.qt.io/qt-5/qdialog.html

Hooks

We want Hooks.

The pyedifice API is based on the old legacy React Components. I can see why React Hooks was adopted — the legacy Components API is really bad.

We especially want an async useEffect.

The async useEffect Hook would have four important features which are bothersome to implement manually over and over.

  1. Runs effect only when props change.
  2. Cancels the effect if the Component is unmounted before the effect runs.
  3. Cancels the effect if another effect starts before this one finishes.
  4. Runs an additional cleanup effect.

Idea: implement Hooks as a with statement context manager

See also PureScript useAff

Using numpy arrays as images

I get an exception when using numpy arrays as images with the Image component. I get my image from opencv imread. I guess the problem is that it is trying to compare the arrays when deciding whether to render or not.

Traceback (most recent call last): File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/base_components.py", line 255, in _mouse_release self._mouse_clicked(ev) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/base_components.py", line 260, in _mouse_clicked self._on_click(ev) File "/Users/tjtuom/Code/takomo/annotate/annote_edifice.py", line 56, in <lambda> *[Label(text=f, on_click=lambda ev, f=f: self.on_click(f)) for f in self.props.files] File "/Users/tjtuom/Code/takomo/annotate/annote_edifice.py", line 52, in on_click self.props.on_image_selected(f) File "/Users/tjtuom/Code/takomo/annotate/annote_edifice.py", line 117, in on_image_selected self.current_image = path.join(IMAGE_DIR, f) File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/contextlib.py", line 124, in __exit__ next(self.gen) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/_component.py", line 373, in render_changes self.set_state(**changes_context) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/_component.py", line 408, in set_state raise e File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/_component.py", line 404, in set_state self._controller._request_rerender([self], kwargs) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/app.py", line 178, in _request_rerender render_result = self._render_engine._request_rerender(components) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 431, in _request_rerender commands = self._gen_commands(widget_trees, render_context) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 413, in _gen_commands commands.extend(widget_tree.gen_qt_commands(render_context)) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 75, in gen_qt_commands rendered = child.gen_qt_commands(render_context) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 75, in gen_qt_commands rendered = child.gen_qt_commands(render_context) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 75, in gen_qt_commands rendered = child.gen_qt_commands(render_context) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 82, in gen_qt_commands new_props = PropsDict({k: v for k, v in self.component.props._items if k not in old_props or _try_neq(old_props[k], v)}) File "/Users/tjtuom/Code/takomo/experiments/venv/lib/python3.9/site-packages/edifice/engine.py", line 82, in <dictcomp> new_props = PropsDict({k: v for k, v in self.component.props._items if k not in old_props or _try_neq(old_props[k], v)}) ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

`KeyError: None` with dynamic hot-reload

Trivial changes in the tutorial code causes a KeyError when using dynamic hot-reload. The application does not reload.

Traceback (most recent call last):
  File "/nix/store/yifccjf1gck1xz8cjhiihydy7z24wsjp-python3-3.11.6-env/lib/python3.11/site-packages/edifice/app.py", line 152, in event
    render_result = self._render_engine._refresh_by_class(classes)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/yifccjf1gck1xz8cjhiihydy7z24wsjp-python3-3.11.6-env/lib/python3.11/site-packages/edifice/engine.py", line 497, in _refresh_by_class
    if isinstance(self._component_tree[parent_comp], list):
                  ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
Stemming from these renders:
KeyError: None

I am using python 3.11.6, edifice 0.3.0, pyside6 6.6.0, qasync 0.27.0, and watchdog 3.0.0 on wayland (sway).

Window example throws TypeError

The example usage of a Window component throws a TypeError:

TypeError: __init__() got multiple values for argument 'title'

I was going to submit a fix, but my proposed change still doesn't shut down the app properly (terminal is blocked after closing):

class MyApp(Component):

    def render(self):
        return View()(
            Window(title="Hello")(Label("Hello")),
        )

if __name__ == "__main__":
    App(MyApp()).start()

EDIT: General discussion moved to #11

pyedifice error (failed assertion)

This gets written to the console and the program hangs:

Exception in callback App._defer_rerender.<locals>.rerender_callback() at /nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py:199
handle: <Handle App._defer_rerender.<locals>.rerender_callback() at /nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py:199>
Traceback (most recent call last):
  File "/nix/store/s6fgyqbk8vn1014daznm5kqx90xdn86x-python3-3.10.13/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py", line 202, in rerender_callback
    self._request_rerender(list(els), {})
  File "/nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/app.py", line 233, in _request_rerender
    render_result.run()
  File "/nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/engine.py", line 301, in run
    command.fn(*command.args, **command.kwargs)
  File "/nix/store/5p5w586v2gr5zmf0g94iwrka60mbwn4g-python3-3.10.13-env/lib/python3.10/site-packages/edifice/base_components/base_components.py", line 1682, in _set_on_change
    assert self.underlying is not None
AssertionError

This gets written to the label (truncated):

/home/...
[Errno 2] No such file or directory: '/home/...'

I think we should try to ensure that edifice crashes in a more graceful way.

hot-reload examples

Because we no longer have implicit Window components the examples no longer work with hot-reloading.

Because hot-reload cannot reload with the root component. And the root component is declared in the one-file examples.

Drag and Drop

https://doc.qt.io/qtforpython-6/overviews/dnd.html#drag-and-drop

https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QWidget.acceptDrops

I think the best way to support this would be to add an on_drop prop to the View.

class View(_LinearView):

 def __init__(self, layout: tp.Text = "column", on_drop: tp.Optional[tp.Callable[[QDropEvent], tp.Any]], **kwargs): 

Then we can put a View around any region which we want to be droppable.

Maybe we also need the other events, QDragEnterEvent, QDragMoveEvent, QDragLeaveEvent.

Hot-reloading Hook state

We lose all of the Hook state when we hot-reload. We should keep the Hook state when we hot-reload.

Difficulties in dynamic reloading a script

I've been trying to replicate the tutorial example on my computer.
Let's say my script is named temp.py

This python script went well with the command python temp.py.

However, when I issued the command python -m edifice temp.py MyApp, it crashed with the error message
TypeError: the 'package' argument is required to perform a relative import for '.temp'

If I changed the filename with the absolute path like
python -m edifice E:/temp.py MyApp, this time the error message was
ModuleNotFoundError: No module named '/E:'.

I'm in windows machine, therefore it might be the problem with absolute path expression. Is there any walkaround?

Thanks in advance.....

should_update default implementation check props and state?

Instead of this default implementation,

def should_update(self, newprops: PropsDict, newstate: tp.Mapping[tp.Text, tp.Any]) -> bool:
"""Determines if the component should rerender upon receiving new props and state.
The arguments, newprops and newstate, reflect the props and state that change: they
may be a subset of the props and the state. When this function is called,
all props and state of this Component are the old values, so you can compare
`component.props` and `newprops` to determine changes.
By default, this function returns true, even if props and state are unchanged.

why doesn’t the default implementation of should_update check for changes in props and state, maybe like this?

    changed = False

    # Check for changes in state
    for k,v in newstate.items():
        v2 = getattr(stateobject, k, None)
        if v2 is None or v2 != v:
            changed = True

    # Check for changes in props
    for k,v in newprops._items:
        v2 = oldprops._get(k, None)
        if v2 is None or v2 != v:
            changed = True

    return changed

Dynamic View and multiple Windows per App

Related to #10 but hopefully this issue can serve as a more general discussion about handling Apps with multiple Windows.

The first issue I have encountered is that a View cannot contain exclusively Window components. See the snippet in #10 for which the App is not terminated. I think it's because the App is creating a window for its children, however when these are Windows themselves then the root window is "invisible" and can't be closed. Adding any other component to the View resolves this:

import edifice as ed


class MyApp(ed.Component):
    def render(self):
        return ed.View()(
            ed.Label("Hello"),
            ed.Window()(ed.Label("Hello")),
        )


if __name__ == "__main__":
    ed.App(MyApp()).start()

The second thing is that I am not quite sure how to go about dynamically managing windows. An example of spawning a window from a button click would be interesting (if it is possible). Then there is also the possibility of dynamic Views where some components could be invisible depending on a conditional...

Lastly, the snippet above creates two windows (one for MyApp, implicitly, and one for the second Label). They can be closed independently. In many cases, it would be desirable to assign one window as the root or parent window. When closed, it should close all of it's children and terminate the App.

As time permits, I'll dig into thee implementations some more and hopefully I can help in some way (even if it's just testing or documentation).

WebGL/JS/Web support

Hi guys! Just came across this project and I really love the concept
Do you think that you could also render to some web framework ?
Then I can design my app once, and either run it locally or host it as a site with the same visual core code

Could be super useful
Love how you have dynamic bindings
Would love to have these features on the browser

Question about style { "margin": }

It seems to me that the “margin” in the style props

https://github.com/fding/pyedifice/blob/53007e112912d870c9202e14c9d8a0884d83bf24/edifice/base_components.py#L438

calls setContentsMargins, which “Sets the margins around the contents of the widget”, which is equivalent to setting the padding in the style sheets Box Model. So there is no way to sets the style sheet Box Model margin of a QWidget?

Is this deliberate? Is @fding trying to avoid or discourage using the Qt style sheets feature? Maybe because “Qt style sheets are currently not supported for custom QStyle subclasses”, or because of some other bad interaction between Qt style sheets and QStyle?

Module typing_extensions not found

Hello. I wanted to test pyedifice so I installed doing pip install pyedifice. Then I copied and tried to run the attached example found on the web, but got an error ModuleNotFoundError: No module named 'typing_extensions'. Is it possible to add to PYPI all module dependencies so that PIP would automatically install them?

I am using:
Windows 11
python 3.12
virtual environment created using venv in standard library

Modules installed:
pyedifice 0.3.2
PySide6 6.6.1

imagen

How to handle any random (custom) hotkey or keyboard shortcut?

Say, what if I'd like the app to quit when Esc is tapped? Or when a specific text input area is focused, pressing Enter has the same effect as clicking a specific button? I believe this is doable via Qt, thus doable via pyedifice as well. But I'm not skillful enough to implement this, any suggestions?

pyqtgraph integration

pyqtgraph is much faster than matplotlib for live graphs. I need this for oscilloscope like functionality I want to put into an app, the overhead caused by matplotlib is quite prohibitive, and the extra functionality is not needed in this case. How would a rough implementation using CustomWidget look like?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.