Edifice: a declarative GUI library for Python

Overview

Edifice: a declarative GUI library for Python

tests codecov

Installation for version 0.0.5:

    pip install pyedifice

Detailed Documentation: Read the docs

Edifice is a Python library for building reactive UI, inspired by modern Javascript libraries such as React. Edifice makes it simple to build a fully reactive UI without ever leaving Python, getting the best of both worlds:

  • Modern paradigms from web development that simplify UI creation
  • Fast iteration via hot reloading
  • Seamless integration with the Python ecosystem (standard library, numpy, matplotlib, pandas, etc)
  • A native desktop app without the overhead of bundling a browser.

Edifice uses Qt as a backend (although it could be generalized to other backends).

Getting Started

Edifice is inspired by React, so if you have React experience, you'll find Edifice to be very similar. For example, for the React setState function, Edifice has set_state, and for React's this.props, Edifice has self.props. All function names use underscores instead of camel case to conform to Python standards, and "Component" is removed from functions like shouldComponentUpdate (renamed to should_update).

See the tutorial to get started.

Why Edifice?

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

Edifice allows you to describe the GUI as a function mapping state to displayed widgets, leaving the how to the library. User interactions update the state, and state changes update the GUI. Edifice makes it possible to write code like:

View(layout="row")(
    Button("Add 5", on_click=lambda:self.set_state(data=self.data + 5)),
    *[Label(i) for i in self.data]
)

and get the expected result: the values in self.data will be displayed, and clicking the button will add 5 to the array, and this state change will automatically be reflected in the GUI. You only need to specify what is to be displayed given the current state, and Edifice will work to ensure that the displayed widgets always correspond to the internal state.

Edifice is designed to make GUI applications easier for humans to reason about. Thus, the displayed GUI always reflect the internal state, even if an exception occurs part way through rendering --- in that case, the state changes are unwound, the display is unchanged, and the exception is re-raised for the application to handle. You can specify a batch of state changes in a transaction, so that either all changes happen or none of them happens. There is no in-between state for you to worry about.

Declarative UIs are also easier for developer tools to work with. Edifice provides two key features to make development easier:

  • Dynamic reloading of changed source code. This is especially useful for tweaking the looks of your application, allowing you to test if the margin should be 10px or 15px instantly without closing the app, reopening it, and waiting for everything to load.
  • Component inspector. Similar to the Inspect Elements tool of a browser, the component inspector will show you all Components 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.

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

  • Edifice interfaces are created purely in Python, whereas QML is written using a separate language.
  • 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.

An analogy is, 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 encapsulates application state and defines the mapping from the state to UI in the render function. The state of a Component is divided into props and state. Props are state passed to the Component in the constructor, whereas state is the Component's own internal state. Changes to props and state will trigger a rerender 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 diff 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.

Currently, Edifice uses Qt under the hood, though it could be adapated to delegate to other imperative GUI frameworks as well.

Development Tools

Edifices also offers a few tools to aid in development.

set_trace

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

Dynamic reload

One other advantage of declarative code is that it is easier for humans and machines to reason about. Edifice takes advantage of this by offering hot reloading of Components. 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) and reset state. 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.

To run your application with dynamic reload, run:

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

This will run app.py with RootComponent mounted as the root. A separate thread will listen to changes in all Python files in the directory containing app.py (recursing into subdirectories), and will reload and trigger a re-render in the main thread. You can customize which directory to listen to using the --dir flag.

Component Inspector

The Edifice component inspector shows the Component tree of your application along with the props and state of each component.

Other information

Contribution

Contributions are welcome; feel free to send pull requests!

License

Edfice is MIT Licensed.

Edifice uses Qt under the hood, and both PyQt5 and PySide2 (and PySide6) are supported. Note that PyQt5 is distributed with the GPL license while PySide2 and PySide6 are distributed under the more flexible LGPL license.

Comments
  • Difficulties in dynamic reloading a script

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

    opened by AnselmJeong 3
  • Facing Error: qt.qpa.plugin: Could not load the Qt platform plugin

    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

    opened by 24Naman 3
  • dynamical reloading not working

    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.

    opened by mo-han 2
  • Using numpy arrays as images

    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()

    opened by tjtuom 2
  • How to handle any random (custom) hotkey or keyboard shortcut?

    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?

    opened by mo-han 1
  • Form widgets `label_map` doesn't work as expected

    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())
    
    opened by adigitoleo 1
  • use pathlib.Path for main file in runner()

    use pathlib.Path for main file in runner()

    Had an issue when trying to follow the tutorial where running python -m edifice tutorial.py MyApp returned the error

    TypeError:` the 'package' argument is required to perform a relative import for '.tutorial'
    

    Looks like the line parts = list(os.path.split(args.main_file)) returns a two element list, ["", "tutorial.py"] so that stripping the '.py' and executing ".".join(parts) yielded ".tutorial", which importlib misinterprets.

    Replaced that all with using pathlib.Path().stem, which will return the name of the file, minus whatever suffix.

    opened by milescsmith 1
  • Add optional QApplication app name in the App constructor

    Add optional QApplication app name in the App constructor

    Exposes the Qt app name, allowing a unique app name at the window manager level.

    The argument to QApplication is used to set the application name. On Linux (Xorg) this can be queried using the xprop utility. Specifically, this name will be used for the WM_CLASS property [1], which can be used by X-based window managers to manipulate window placement and app-specific behaviour (automatic floating, tiling, positioning, etc). This is also used by Jupyter's QtConsole [2] for example.

    [1] https://tronche.com/gui/x/icccm/sec-4.html#WM_CLASS [2] jupyter/qtconsole#16

    opened by adigitoleo 0
  • Support for Qt Grid Layout

    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.

    enhancement 
    opened by fding 0
  • pyqtgraph integration

    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?

    opened by fakuivan 1
  • Running edifice in a asyncio loop

    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?

    opened by MrEvilOnGitHub 1
  • WebGL/JS/Web support

    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

    opened by fire17 1
  • PySide2 dependency prevents installation on macOS with Apple Silicon

    PySide2 dependency prevents installation on macOS with Apple Silicon

    PySide2 is not officially supported on M1-based machines and no package for it exists for arm64 on macOS. Therefore, Edifice cannot be installed through pip even if one only wants to use the PyQt5 backend.

    opened by MatuaDoc 1
Owner
David Ding
Research Engineer. Harvard CS 2016
David Ding
A library for building modern declarative desktop applications in WX.

re-wx is a library for building modern declarative desktop applications. It's built as a management layer on top of WXPython, which means you get all the goodness of a mature, native, cross-platform UI kit, wrapped up in a modern, React inspired API.

Chris 115 Dec 24, 2022
Declarative User Interfaces for Python

Welcome to Enaml Enaml is a programming language and framework for creating professional-quality user interfaces with minimal effort. What you get A d

null 1.4k Jan 7, 2023
A GUI for designing Python GUI's for PySimpleGUI.

SimpleGUIBuilder A GUI for designing Python GUI's for PySimpleGUI. Installation There is none :) just download the file from a release and run it. Don

Miguel Martins 65 Dec 22, 2022
PyQt5 Sample GUI Program - Python PyQt5 Sample GUI application

Python PyQt5 Sample GUI application Program work like this Designed GUI using De

Dimuth De Zoysa 5 Mar 27, 2022
The GUI application by Python3.8. Using QT Design draw UI and generator UI XML file provides to PySide2 build GUI components

The GUI application by Python3.8. Using QT Design draw UI and generator UI XML file provides to PySide2 build GUI components. Total adopt OOD design class, service, and abstract class. OOP implemented this project.

Jiage 1 Jan 11, 2022
A little Python library for making simple Electron-like HTML/JS GUI apps

Eel Eel is a little Python library for making simple Electron-like offline HTML/JS GUI apps, with full access to Python capabilities and libraries. Ee

Chris Knott 5.4k Jan 7, 2023
Learn to build a Python Desktop GUI app using pywebview, Python, JavaScript, HTML, & CSS.

Python Desktop App Learn how to make a desktop GUI application using Python, JavaScript, HTML, & CSS all thanks to pywebview. pywebview is essentially

Coding For Entrepreneurs 55 Jan 5, 2023
Turn (almost) any Python command line program into a full GUI application with one line

Gooey Turn (almost) any Python 2 or 3 Console Program into a GUI application with one line Support this project Table of Contents Gooey Table of conte

Chris 17k Jan 9, 2023
Build GUI for your Python program with JavaScript, HTML, and CSS

https://pywebview.flowrl.com pywebview is a lightweight cross-platform wrapper around a webview component that allows to display HTML content in its o

Roman 3.3k Jan 1, 2023
A Python native, OS native GUI toolkit.

Toga A Python native, OS native GUI toolkit. Prerequisites Minimum requirements Toga requires Python 3. Python 2 is not supported. If you're on macOS,

BeeWare 3.3k Dec 31, 2022
A Python native, OS native GUI toolkit.

Toga A Python native, OS native GUI toolkit. Prerequisites Minimum requirements Toga requires Python 3. Python 2 is not supported. If you're on macOS,

BeeWare 3.3k Jan 2, 2023
A small pomodoro GUI for Windows/Linux created in Python with PyQt5.

Pomodoro A small pomodoro GUI for Windows/Linux created with PyQt5. Features The "Timer" tab allows you to set your desired work and rest times aswell

Burak Martin 81 Dec 28, 2022
My Git GUI version made in Python and Tkinter.

Description My Git GUI version made in Python and Tkinter. How to use Basically, create a folder in your computer, open the software, select the path

Matheus Golzio 4 Oct 10, 2021
Aplicação GUI feita em Python para estudos de cadastro (forms).

Cadastro de DEVs GUI ?? A ideia original veio do repositório do https://github.com/PedroTomazeti nomeado 'Projetos-Independentes-HTML-CSS' Nele há um

Yago Goltara 3 Aug 15, 2021
Delphi's FireMonkey framework as a Python module for Windows, MacOS, Linux, and Android GUI development.

DelphiFMX4Python Delphi's FireMonkey framework as a Python module for Windows, MacOS, Linux, and Android GUI development. About: The delphifmx library

Embarcadero Technologies 191 Jan 9, 2023
Simple GUI python app to show a stocks graph performance. Made with Matplotlib and Tiingo.

stock-graph-python Simple GUI python app to show a stocks graph performance. Made with Matplotlib and Tiingo. Tiingo API Key You will need to add your

Toby 12 May 14, 2022
A Url Shortener with GUI made in Python.

Url-Shortener-with-GUI-in-python A Url Shortener with GUI made in Python. To Run this download the zip file and run the main file or Clone this repo.

SidTheMiner 1 Nov 12, 2021
Create shortcuts on Windows to your Python, EXE, Batch files or any other file using a GUI made with PySimpleGUI

shor Windows Shortcut Creation Create Windows Shortcuts to your python programs and any other file easily using this application created using PySimpl

PySimpleGUI 7 Nov 16, 2021
Signin/Signup GUI form using tkinter in python

SignIn-SignUpFormRepo Hello there, I am Shahid and this is the Signin/Signup GUI form using tkinter in python if you want to use avatar images then pa

Shahid Akhtar 1 Nov 9, 2021