From 22e35f73415bfc2d0a97d99bb90c787b486423c1 Mon Sep 17 00:00:00 2001 From: Siddhant Wahal Date: Tue, 12 Jul 2022 12:50:04 -0500 Subject: [PATCH] Add jupyter notebooks for slides --- slides/01_intro.ipynb | 289 +++++++ slides/01_intro.md | 1 + slides/02_traits.ipynb | 1075 +++++++++++++++++++++++++ slides/02_traits.md | 3 +- slides/03_traitsui.ipynb | 893 ++++++++++++++++++++ slides/03_traitsui.md | 1 + slides/07_background_processing.ipynb | 78 ++ slides/07_background_processing.md | 5 +- slides/template.md | 5 +- 9 files changed, 2345 insertions(+), 5 deletions(-) create mode 100644 slides/01_intro.ipynb create mode 100644 slides/02_traits.ipynb create mode 100644 slides/03_traitsui.ipynb create mode 100644 slides/07_background_processing.ipynb diff --git a/slides/01_intro.ipynb b/slides/01_intro.ipynb new file mode 100644 index 0000000..2432de9 --- /dev/null +++ b/slides/01_intro.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bd484b42", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sharing scientific tools: script to desktop application\n", + "\n", + "**Jonathan Rocher, Siddhant Wahal, Jason Chambless, Corran Webster, Prabhu Ramachandran**\n", + "\n", + "**SciPy 2022**\n" + ] + }, + { + "cell_type": "markdown", + "id": "d143aab8", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Preliminaries\n", + "\n", + "If you haven't already:\n", + "- Clone the repository: https://github.com/jonathanrocher/ets_tutorial\n", + "- Install packages:\n", + " - Using Enthought Deployment Manager (recommended)\n", + " (https://www.enthought.com/edm):\n", + "\n", + " ```bash\n", + " edm envs create bootstrap\n", + " edm install --environment bootstrap click\n", + " edm run -e bootstrap -- python ci build --environment ets_tutorial\n", + " ```\n", + " - `ets_tutorial` will be our working Python environment. To activate:\n", + " ```bash\n", + " edm shell -e ets_tutorial\n", + " ```\n", + " - Follow instructions in README for conda and pip" + ] + }, + { + "cell_type": "markdown", + "id": "b3d7f1d2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Motivation\n", + "\n", + "- Some tasks are easier with a GUI\n", + "- Seeing a lot of information in one shot\n", + "- Easier for non-programmers\n", + "- Easier to share\n" + ] + }, + { + "cell_type": "markdown", + "id": "282af803", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Why ETS? When ETS?\n", + "\n", + "- Open-source\n", + "- Mature\n", + "- Easy to start, easy to grow\n", + "- Tools promotes reusable code and good design patterns\n", + "- Largely declarative UI\n", + "- Backend-agnostic: avoid having to update when PyQt, wxPython, ... update!\n", + "- Limitless around data tools, in particular plotting!\n", + "- Reduced development costs (single programming language)\n", + "- No architecture mind shift necessary (client-server) and no exposure to \n", + " server hacking.\n", + "\n", + "Limitations:\n", + "- Limited by back-end when it comes to widgets compared to Javascript\n", + "- Desktop application isn't a solution for all needs\n" + ] + }, + { + "cell_type": "markdown", + "id": "03d0901c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## What is ETS?\n", + "\n", + "- Enthought Tool Suite: https://docs.enthought.com/ets\n", + "- Open Source\n", + "- Packages\n", + " - Traits: Python object attributes on steroids\n", + " - TraitsUI: Easy GUI-building\n", + " - PyFace: Low-level GUI components\n", + " - Envisage: plug-in application framework\n", + " - Chaco: interactive plotting library\n", + " - Mayavi: 3D plotting\n", + " - traits_futures: running tasks in parallel/background\n", + " - And many others...\n" + ] + }, + { + "cell_type": "markdown", + "id": "b04fb05b", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Layered package design\n", + "\n", + "
\n", + "\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "516819ae", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sample screenshots\n", + "\n", + "- Can make quite sophisticated UIs\n", + "- Much less code\n", + "- Easy to write\n" + ] + }, + { + "cell_type": "markdown", + "id": "699c3043", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## A Mayavi-based dialog\n", + "\n", + "
\n", + "\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "98615314", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## A customized viewer\n", + "\n", + "
\n", + "\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "949e9a9c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Goals\n", + "\n", + "- Start with simple Python script\n", + " - Detect faces\n", + " - Extract Image metadata\n", + "\n", + "- Build a full-fledged desktop application\n", + " - Easy to use UI\n", + " - Learn a little MVC\n", + " - Design application to scale\n", + "\n", + "- Share the application with others\n" + ] + }, + { + "cell_type": "markdown", + "id": "37ef3b1a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Result\n", + "\n", + "Final application we will be building in this tutorial:\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2f69a9db", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Schedule\n", + "\n", + "- Step 1: Python script\n", + "- Step 2: Using Traits.\n", + "- Step 3: Basic GUI using TraitsUI\n", + "- Step 4: PyFace application: tree navigator\n", + "- Step 5: More features\n", + "- Step 6: Menus and branding\n", + "- Step 7: [OPTIONAL] Advanced features\n", + "- Step 8: [OPTIONAL] Packaging and sharing\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "dceead88", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Basic Python script\n", + "\n", + "- Uses: `PIL`, `skimage`, and `matplotlib`\n", + "- Detects faces in a given image\n", + "- Look inside ...\n" + ] + }, + { + "cell_type": "markdown", + "id": "923e8267", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Next steps\n", + "\n", + "- Learn more about traits\n", + "- Build a clean model for our task with traits\n", + "- Learn why models are useful\n" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/slides/01_intro.md b/slides/01_intro.md index 93a2f1a..8620ae0 100644 --- a/slides/01_intro.md +++ b/slides/01_intro.md @@ -1,6 +1,7 @@ --- jupyter: jupytext: + formats: ipynb,md text_representation: extension: .md format_name: markdown diff --git a/slides/02_traits.ipynb b/slides/02_traits.ipynb new file mode 100644 index 0000000..971bd31 --- /dev/null +++ b/slides/02_traits.ipynb @@ -0,0 +1,1075 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "80c778c8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Step1: Using Traits\n", + "\n", + "**Jonathan Rocher, Siddhant Wahal, Jason Chambless, Prabhu Ramachandran**\n", + "\n", + "**SciPy 2022**\n" + ] + }, + { + "cell_type": "markdown", + "id": "3592ddc1", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Introduction to Traits\n", + "\n", + "- **trait**: Python object attribute with additional characteristics\n", + " - Typed attributes\n", + " - Reactive\n", + " - Observable\n", + " - Cleaner code\n", + " - Easy UI\n", + "\n", + "
\n", + "\n", + "- https://docs.enthought.com/traits/\n", + "- https://github.com/enthought/traits/\n" + ] + }, + { + "cell_type": "markdown", + "id": "20d20314", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Why all this?\n", + "\n", + "- No pain, no gain! (Only a little pain, we promise!)\n", + "\n", + "- Small change to thinking yields big benefits\n", + "\n", + "- Do not mix GUI code with core model\n", + "\n", + "- Build a clean model first\n", + "\n", + " - Easier to understand/maintain\n", + " - Separation of concerns\n", + " - Easier to test\n", + " - Generally better reuse\n" + ] + }, + { + "cell_type": "markdown", + "id": "c838455c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Trait features\n", + "\n", + "- Initialization: default value\n", + "- Validation: strongly typed\n", + "- Deferral/Delegation: value delegation\n", + "- Notification: events\n", + "- Visualization: MVC, automatic GUI!\n" + ] + }, + { + "cell_type": "markdown", + "id": "0eb9feed", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## An example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c547dbe", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import Delegate, HasStrictTraits, Instance, Int, Str, observe\n", + "\n", + "class Parent(HasStrictTraits):\n", + " # INITIALIZATION: 'last_name' initialized to ''\n", + " last_name = Str('')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38748687", + "metadata": {}, + "outputs": [], + "source": [ + "class Child(HasStrictTraits):\n", + " age = Int\n", + " # VALIDATION: 'father' must be Parent instance\n", + " father = Instance(Parent)\n", + " # DELEGATION: 'last_name' delegated to father's\n", + " last_name = Delegate('father')\n", + " # NOTIFICATION: Method called when 'age' changes\n", + " def _age_changed(self, old, new):\n", + " print('Age changed from %s to %s ' % (old, new))\n" + ] + }, + { + "cell_type": "markdown", + "id": "7202bbdb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Using this\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a4c3825", + "metadata": {}, + "outputs": [], + "source": [ + "joe = Parent()\n", + "joe.last_name = 'Johnson'\n", + "moe = Child()\n", + "moe.father = joe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb469352", + "metadata": {}, + "outputs": [], + "source": [ + "# Delegation\n", + "moe.last_name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34ebc9f7", + "metadata": {}, + "outputs": [], + "source": [ + "# Notification\n", + "moe.age = 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8acf77fd", + "metadata": {}, + "outputs": [], + "source": [ + "# Validation\n", + "moe.age = '1'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "160c5cff", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualization\n", + "moe.configure_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "66c13032", + "metadata": {}, + "source": [ + "- Live editing!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f80c023b", + "metadata": {}, + "outputs": [], + "source": [ + "%gui qt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21b7db21", + "metadata": {}, + "outputs": [], + "source": [ + "moe.edit_traits()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c83bc5a8", + "metadata": {}, + "outputs": [], + "source": [ + "moe.age = 21" + ] + }, + { + "cell_type": "markdown", + "id": "fd22ce8e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## What if you want to override `__init__`?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b519879", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "class Child(HasStrictTraits):\n", + " age = Int\n", + " father = Instance(Parent)\n", + " last_name = Delegate('father')\n", + "\n", + " def __init__(self, **traits):\n", + " super(HasStrictTraits, self).__init__(**traits)\n", + "\n", + " def _age_changed(self, old, new):\n", + " print('Age changed from %s to %s ' % (old, new))" + ] + }, + { + "cell_type": "markdown", + "id": "1a0144fa", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Predefined trait types\n", + "\n", + "- Standard: `Bool, Complex, Int, Float, Str, Tuple, List, Dict`\n", + "- Constrained: `Range, Regex, Expression, ReadOnly`\n", + "- Special: `Date, Either/Union, Enum, Array, File, Color, Font, Button`\n", + "- Generic: `Instance, Any, Callable`\n", + "- Custom traits: 2D/3D plots etc.\n" + ] + }, + { + "cell_type": "markdown", + "id": "bb904538", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## `HasStrictTraits` vs. `HasStrictTraits`\n", + "\n", + "- Better to use `HasStrictTraits`\n", + "- Will catch errors when you mistype or misspell an attribute\n", + "- Will not allow setting any attribute not already declared\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0eae271c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Notification example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f7fb681", + "metadata": {}, + "outputs": [], + "source": [ + "class Parent(HasStrictTraits):\n", + " last_name = Str('')\n", + "\n", + "\n", + "class Child(HasStrictTraits):\n", + " age = Int\n", + " father = Instance(Parent)\n", + "\n", + " def _age_changed(self, old, new):\n", + " print('Age changed from %s to %s ' % (old, new))\n", + "\n", + " @observe('father.last_name')\n", + " def _dad_name_updated(self, event):\n", + " print('Father name changed to', self.father.last_name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f248a955", + "metadata": {}, + "outputs": [], + "source": [ + "dad = Parent(last_name='Zubizaretta')\n", + "c = Child(father=dad)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7df6db6d", + "metadata": {}, + "outputs": [], + "source": [ + "dad.last_name = 'Valderrama'" + ] + }, + { + "cell_type": "markdown", + "id": "bf19f52c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Trait change notification\n", + "\n", + "- Static: `def __changed()`\n", + "- Decorator: `@observe('extended.trait.name')`\n", + "\n", + "- See documentation: https://docs.enthought.com/traits/traits_user_manual/notification.html\n" + ] + }, + { + "cell_type": "markdown", + "id": "6bd4fbdd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise\n", + "\n", + "- Modify the first example to produce the above example\n", + "- Add a `first_name` trait\n", + "- Add a `Bool` trait to specify if person is alive\n", + "- Add an `Enum` for the gender of the child\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "543377f8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Solution\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e6fa054", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import Bool, Enum\n", + "\n", + "class Parent(HasStrictTraits):\n", + " last_name = Str('')\n", + "\n", + "\n", + "class Child(HasStrictTraits):\n", + " age = Int\n", + " father = Instance(Parent)\n", + " first_name = Str('')\n", + " likes_queso = Bool(True)\n", + " handedness = Enum('right', 'left')\n", + "\n", + " def _age_changed(self, old, new):\n", + " print('Age changed from %s to %s ' % (old, new))\n", + "\n", + " @observe('father.last_name')\n", + " def _dad_name_updated(self, event):\n", + " print('Dad name', self.father.last_name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9de87c98", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "p = Parent(last_name='Ray')\n", + "c = Child(age=21, father=p, first_name='Romano', handedness='right')" + ] + }, + { + "cell_type": "markdown", + "id": "f4efe08a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Trait change event\n", + "\n", + "- Recall this" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea2fd23c", + "metadata": {}, + "outputs": [], + "source": [ + " @observe('father.last_name')\n", + " def _dad_name_updated(self, event):\n", + " print('Dad name', self.father.last_name)" + ] + }, + { + "cell_type": "markdown", + "id": "08be8895", + "metadata": {}, + "source": [ + "- `event` is a `TraitChangeEvent` instance\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f44dd68d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Child(HasStrictTraits):\n", + " age = Int\n", + " father = Instance(Parent)\n", + " first_name = Str('')\n", + " likes_queso = Bool(True)\n", + " handedness = Enum('right', 'left')\n", + "\n", + " def _age_changed(self, old, new):\n", + " print('Age changed from %s to %s ' % (old, new))\n", + "\n", + " @observe('father.last_name')\n", + " def _dad_name_updated(self, event):\n", + " print(event.object, event.name, event.old, event.new)\n", + " print('Dad name', self.father.last_name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3622a877", + "metadata": {}, + "outputs": [], + "source": [ + "p = Parent(last_name='Ray')\n", + "c = Child(age=21, father=p, first_name='Romano', handedness='right')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "156f7733", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "p.last_name = 'Ahmed'" + ] + }, + { + "cell_type": "markdown", + "id": "d2e6388f", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8bb91b22", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Container traits\n", + "\n", + "- `List`, `Dict` and `Set`\n" + ] + }, + { + "cell_type": "markdown", + "id": "77540ac6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Trait Lists\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dfa270a", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import List\n", + "\n", + "class Bowl(HasStrictTraits):\n", + " fruits = List(Str)\n", + "\n", + " @observe(\"fruits\")\n", + " def _fruits_list_updated(self, event):\n", + " print(\"fruits list updated\", type(event))\n", + " print(event.old, event.new)\n", + "\n", + " @observe(\"fruits.items\")\n", + " def _fruits_updated(self, event):\n", + " print(\"Fruits items changed\", type(event))\n", + " print(event.added, event.index, event.removed)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffbdbe48", + "metadata": {}, + "outputs": [], + "source": [ + "b = Bowl()\n", + "b.fruits = ['apple']\n", + "b.fruits.append('mango')" + ] + }, + { + "cell_type": "markdown", + "id": "6d88bf36", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Other Trait Events\n", + "\n", + "- `List` trait changes: `ListChangeEvent`\n", + "- `Dict` trait changes: `DictChangeEvent`\n", + "- `Set` trait changes: `SetChangeEvent`\n" + ] + }, + { + "cell_type": "markdown", + "id": "debbaf5b", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Some more useful traits\n", + "\n", + "- `File`, `Directory` and `Dict`\n", + "- Useful for our application\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dfd1f59", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import Dict, Directory, File\n", + "\n", + "class Folder(HasStrictTraits):\n", + " root = Directory\n", + " files = List(File)\n", + " sizes = Dict" + ] + }, + { + "cell_type": "markdown", + "id": "b796369f", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Walk through\n", + "\n", + "- Here the dictionary can have any keys or values\n", + "- Can use the `key_trait` and `value_trait` to specify them\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1c904ab", + "metadata": {}, + "outputs": [], + "source": [ + "class Folder(HasStrictTraits):\n", + " root = Directory\n", + " files = List(File)\n", + " sizes = Dict(key_trait=Str, value_trait=Dict(Str, Int))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e694c56", + "metadata": {}, + "outputs": [], + "source": [ + "f = Folder(root='/tmp')" + ] + }, + { + "cell_type": "markdown", + "id": "0347ea6f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise\n", + "\n", + "Modify the above example so when you set `root`, it finds all the files in\n", + "that directory and the file sizes and sets the appropriate traits.\n", + "\n", + "Hint: Use `os.listdir` and `os.path.getsize`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc6fbe0f", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "# Solution" + ] + }, + { + "cell_type": "markdown", + "id": "eb75758a", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "54fd569e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Property traits\n", + "\n", + "- What if you have a quantity that is computed?\n", + "- Use `Property` traits here\n", + "- Use the `observe=` kwarg\n", + "- Use `@cached_property` to cache output\n", + "- Use the `_get_propname` and `_set_propname` (optional)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d82cc76b", + "metadata": {}, + "outputs": [], + "source": [ + "from math import pi\n", + "from traits.api import Range, Float, Property, cached_property\n", + "\n", + "class Circle(HasStrictTraits):\n", + " radius = Range(0.0, 1000.0)\n", + " area = Property(Float, observe='radius')\n", + "\n", + " @cached_property\n", + " def _get_area(self):\n", + " print(\"computing area\")\n", + " return pi*self.radius**2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f2c2c2c", + "metadata": {}, + "outputs": [], + "source": [ + "c = Circle(radius=2)\n", + "c.area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11d4f829", + "metadata": {}, + "outputs": [], + "source": [ + "c.area" + ] + }, + { + "cell_type": "markdown", + "id": "03a2a4d0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## `Array` traits\n", + "\n", + "- Can handle numpy arrays of arbitrary shape\n", + "- Can specify dtype, shape, and casting options using kwargs\n", + "- Warning: cannot \"listen\" to changes inside the array\n", + "- Simple example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a67f149", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from traits.api import Array, Range, observe\n", + "\n", + "class Beats(HasStrictTraits):\n", + " f1 = Range(1.0, 200.0, value=100)\n", + " f2 = Range(low=1.0, high=200.0, value=104)\n", + " signal = Array(dtype=float, shape=(None,))\n", + "\n", + " @observe('f1, f2')\n", + " def update(self, event=None):\n", + " t = np.linspace(0, 1, 500)\n", + " pi = np.pi\n", + " self.signal = np.sin(2*pi*self.f1*t) + np.sin(2*pi*self.f2*t)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a38948e", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "b = Beats()\n", + "b.f2 = 103" + ] + }, + { + "cell_type": "markdown", + "id": "adf52c07", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Setting default values\n", + "\n", + "- For simple cases, use the default of the trait\n", + "- For more complex cases use a special method\n", + "- A simple example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28970321", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "from traits.api import HasStrictTraits, Date, Range\n", + "\n", + "class Thing(HasStrictTraits):\n", + " date = Date()\n", + " age = Int(12)\n", + "\n", + " def _date_default(self):\n", + " print('default')\n", + " return datetime.datetime.today()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cffea04e", + "metadata": {}, + "outputs": [], + "source": [ + "t = Thing()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bda85e1", + "metadata": {}, + "outputs": [], + "source": [ + "type(t.age)" + ] + }, + { + "cell_type": "markdown", + "id": "0996d132", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## `Event` traits\n", + "\n", + "- Holds no value but can be set and be listened to\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1347051", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import Event, File, HasStrictTraits, Instance, observe, Str\n", + "\n", + "class DataFile(HasStrictTraits):\n", + " file = File\n", + " data_changed = Event\n", + "\n", + "\n", + "class DataReader(HasStrictTraits):\n", + " file = Instance(DataFile)\n", + " content = Str\n", + "\n", + " @observe(\"file.data_changed\")\n", + " def file_data_changed(self, event):\n", + " print(\"File data changed\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65feabb3", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "f = DataFile(file='/tmp/junk.dat')\n", + "r = DataReader(file=f)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de8a8ad7", + "metadata": {}, + "outputs": [], + "source": [ + "f.data_changed = True" + ] + }, + { + "cell_type": "markdown", + "id": "b61ac993", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise time!\n", + "\n", + "- From the starting script (`stage1_starting_script/face_detect.py`), extract\n", + " an object that represents an image file.\n", + "- The class for the object should:\n", + " - Be a traits model, i.e., inherit from `HasStrictTraits`, and, expose\n", + " - Attributes:\n", + " - `filepath`: the absolute path to the image file\n", + " - `metadata`: a dictionary storing EXIF data\n", + " - `data` a numpy array containing the RGB data\n", + " - `faces`: a list containing detected faces\n", + " - Methods:\n", + " `detect_faces`: returns the list of detected faces\n", + " - Be reactive:\n", + " - Ensure `metadata` and `data` are updated with `filepath` is modified\n", + "- Copy `stage1_starting_script/face_detect.py` to `stage2.1_traited_script` and\n", + " work there\n", + "- *Do not do any plotting in the model!*\n", + "\n", + "- Hint for computing RGB data:\n", + "\n", + "```python\n", + "import numpy as np\n", + "import PIL.Image\n", + "\n", + "with PIL.Image.open(filepath) as img:\n", + " data = np.asarray(img)\n", + "```\n", + "- Example images available at `ets_tutorial/sample_images` for testing\n" + ] + }, + { + "cell_type": "markdown", + "id": "378f662f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Solution\n", + "`stage2.1_traited_script/traited_face_detect.py`" + ] + }, + { + "cell_type": "markdown", + "id": "dd6fc870", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise time!\n", + "\n", + "- Develop another traits model, one that represents a folder containing several\n", + " image files\n", + "- The class for the object should expose:\n", + " - Attributes:\n", + " - `directory`: the absolute path to the folder\n", + " - `images`: a list of `ImageFile` instances from the previous exercise\n", + " - `data`: a pandas `DataFrame` to store metadata for each file in the folder\n", + " - Be reactive:\n", + " - Ensure `images` and `data` are updated when `directory` is modified\n", + " - Override `__init__` to ensure directory exists at object initialization\n", + " - Save work in `stage2.1_traited_script/image_folder.py`\n", + "\n", + "- Hints:\n", + " - Create a `DataFrame` from `List(Dict)`:\n", + " ```python\n", + " import pandas as pd\n", + " >>> records = [\n", + " {'A': 5, 'B': 0, 'C': 3, 'D': 3},\n", + " {'A': 7, 'B': 9, 'C': 3, 'D': 5},\n", + " {'A': 2, 'B': 4, 'C': 7, 'D': 6}\n", + " ]\n", + " >>> df = pd.DataFrame(records)\n", + " A B C D\n", + " 0 5 0 3 3\n", + " 1 7 9 3 5\n", + " 2 2 4 7 6\n", + " ```\n", + " - `os.path.isdir(directory)` to determine if `directory` is valid\n" + ] + }, + { + "cell_type": "markdown", + "id": "0a8c77cd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Solution\n", + "`stage2.1_traited_script/image_folder.py`" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/slides/02_traits.md b/slides/02_traits.md index 0df62d5..58dc174 100644 --- a/slides/02_traits.md +++ b/slides/02_traits.md @@ -1,13 +1,14 @@ --- jupyter: jupytext: + formats: ipynb,md text_representation: extension: .md format_name: markdown format_version: '1.3' jupytext_version: 1.13.7 kernelspec: - display_name: Python 3 (ipykernel) + display_name: Python 3 language: python name: python3 --- diff --git a/slides/03_traitsui.ipynb b/slides/03_traitsui.ipynb new file mode 100644 index 0000000..ca18943 --- /dev/null +++ b/slides/03_traitsui.ipynb @@ -0,0 +1,893 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2a6239cd", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sharing scientific tools: script to desktop application\n", + "\n", + "### TraitsUI\n", + "\n", + "**Jonathan Rocher, Siddhant Wahal, Jason Chambless, Corran Webster, Prabhu Ramachandran**\n", + "\n", + "**SciPy 2022**\n" + ] + }, + { + "cell_type": "markdown", + "id": "adea5ec8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## TraitsUI: Easy GUI building\n", + "\n", + "- Meant for traits\n", + "- Declarative UI\n", + "- Interoperates with Qt and wxPython\n", + "- Docs: https://docs.enthought.com/traitsui\n", + "- GH: https://github.com/enthought/traitsui\n" + ] + }, + { + "cell_type": "markdown", + "id": "3dc33099", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Approach\n", + "\n", + "- Just declare what needs to be done\n", + "- Do not need to write a lot of code\n", + "- Embed 2D plots with `matplotlib` or `chaco`\n", + "- Embed 3D plots with `mayavi`\n", + "- Build rich scientific dialogs\n" + ] + }, + { + "cell_type": "markdown", + "id": "547fc5eb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Model-View-Controller (MVC) design pattern\n", + "\n", + "- Model: manages data, state, and internal logic\n", + "- View: presents the model in a graphically interactive way\n", + "- Controller: manages information between view and model\n", + "\n", + "
\n", + "\n", + "- For simple cases, View and Controller may be the same\n" + ] + }, + { + "cell_type": "markdown", + "id": "1598aebc", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## MVC with traitsui\n", + "\n", + "- Model: `HasStrictTraits` object\n", + "- View: `traitsui`, `View` class\n", + "- Controller: `traitsui` `Handler` class\n" + ] + }, + { + "cell_type": "markdown", + "id": "903c056f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Views\n", + "\n", + "- A declarative specification for a GUI\n", + "- Made up of `Item` and `Group` objects\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b8970e5", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Simple example\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2132490", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import HasStrictTraits, Int, Str, Enum, Bool\n", + "\n", + "class Person(HasStrictTraits):\n", + " name = Str\n", + " age = Int\n", + " handedness = Enum('left', 'right')\n", + " drinks = Bool(False)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1551d751", + "metadata": {}, + "outputs": [], + "source": [ + "%gui qt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93acd631", + "metadata": {}, + "outputs": [], + "source": [ + "p = Person(name='Worf')\n", + "p.edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "5d150cf5", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Specifying a View\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2de723d", + "metadata": {}, + "outputs": [], + "source": [ + "from traitsui.api import Item, View\n", + "view1 = View(\n", + " Item(name='name', style='readonly'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " Item(name='drinks', visible_when='age >= 18'),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51d3c33a", + "metadata": {}, + "outputs": [], + "source": [ + "p.edit_traits(view=view1)" + ] + }, + { + "cell_type": "markdown", + "id": "d66c18f0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Common attributes of `Item`\n", + "\n", + "- `label`: UI label instead of the name\n", + "- `show_label`: Bool\n", + "- `tooltip`/`help`: Str\n", + "- `editor`: `ItemEditor` to use\n", + "- `style`: `{'simple', custom', 'text', 'readonly'}`\n", + "- `enabled_when`, `visible_when`, `defined_when`: Python expression\n", + "- `resizable`: bool\n" + ] + }, + { + "cell_type": "markdown", + "id": "07c17f27", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Groups\n", + "\n", + "- Handy for complex UIs\n", + "- Common attributes:\n", + " - `columns`\n", + " - `label`\n", + " - `layout`: `{'normal', 'flow', 'split', 'tabbed'}`\n", + " - `orientation`: ` {'vertical', 'horizontal'}`\n", + " - `show_border`: bool\n", + " - `enabled_when`, `visible_when`, `defined_when`: Python expression\n", + "- `HGroup`, `VGroup`, `HSplit`, `VSplit`, `Tabbed`: shortcuts\n" + ] + }, + { + "cell_type": "markdown", + "id": "e5467eeb", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## A simpler way\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2be6ebbf", + "metadata": {}, + "outputs": [], + "source": [ + "from traitsui.api import Group\n", + "\n", + "class Person(HasStrictTraits):\n", + " name = Str\n", + " age = Int\n", + " handedness = Enum('left', 'right')\n", + "\n", + " traits_view = View(\n", + " Group(\n", + " Item(name='name'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " label='Person profile',\n", + " show_border=True\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cfb0aa25", + "metadata": {}, + "outputs": [], + "source": [ + "p = Person(name='Worf', age=20)\n", + "p.edit_traits()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89045589", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "78af2189", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## View attributes\n", + "\n", + "- `dock`: `{'fixed', 'horizontal', 'vertical', 'tabbed'}`\n", + "- `height`/`width`: int\n", + "- `icon`/`image`\n", + "- `resizable`: bool\n", + "- `scrollable`: bool\n", + "- `title`: name of the window\n", + "- `buttons`\n", + "- `key_bindings`\n", + "- See docs for more: https://docs.enthought.com/traitsui/traitsui_user_manual/\n" + ] + }, + { + "cell_type": "markdown", + "id": "c9554af8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Simple example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "257c8652", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from traitsui.api import CancelButton, OKButton\n", + "\n", + "class Person(HasStrictTraits):\n", + " name = Str\n", + " age = Int\n", + " likes_queso = Bool\n", + " handedness = Enum('left', 'right')\n", + "\n", + " traits_view = View(\n", + " Group(\n", + " Item(name='name'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " label='Person profile',\n", + " show_border=True,\n", + " ),\n", + " buttons=[OKButton, CancelButton]\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "139492bc", + "metadata": {}, + "outputs": [], + "source": [ + "p = Person(name='Worf', age=20)\n", + "p.edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "6b673e70", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Specifying an editor\n", + "\n", + "- Editors: encapsulate display instructions for a trait type\n", + " - Hide GUI-toolkit code behind an abstraction layer\n", + " - All standard traits has a predefined editor that is automatically\n", + " displayed when the trait is displayed, unless overridden" + ] + }, + { + "cell_type": "markdown", + "id": "dff99b18", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Examples\n", + "\n", + "This code automatically uses `StrEditor`, the default for `Str` traits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbfeeb5c", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "class Stringy(HasStrictTraits):\n", + " characters = Str()\n", + "\n", + "s = Stringy(characters='Stringy characters')\n", + "s.edit_traits(\n", + " view=View(\n", + " Item(\"characters\")\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "bf881140", + "metadata": { + "lines_to_next_cell": 0, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "This code uses an HTMLEditor:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a564b42", + "metadata": {}, + "outputs": [], + "source": [ + "from traitsui.api import HTMLEditor\n", + "s.edit_traits(\n", + " view=View(\n", + " Item(\"characters\", editor=HTMLEditor())\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ea5d9fb6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "## A few useful editors\n", + "- We illustrate the powerful `InstanceEditor` here\n", + "- Consider the following\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af9ff270", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import Instance\n", + "\n", + "class Person(HasStrictTraits):\n", + " name = Str\n", + " age = Int\n", + " handedness = Enum('left', 'right')\n", + " bff = Instance('Person') # Notice the quotes.\n", + "\n", + " traits_view = View(\n", + " Group(\n", + " Item(name='name'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " Item(name='bff', style='custom'),\n", + " label='Person profile',\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd2e6938", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "frodo = Person(name='Frodo', age=30)\n", + "sam = Person(name='Sam', age=29, bff=frodo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "190ec815", + "metadata": {}, + "outputs": [], + "source": [ + "sam.edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "e6a69344", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Discussion\n", + "\n", + "- Note the embedding\n", + "- Implicitly uses an InstanceEditor\n", + "- Can configure the view it uses if needed\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11c09ed3", + "metadata": {}, + "outputs": [], + "source": [ + "from traitsui.api import InstanceEditor\n", + "\n", + "bff_view = View(Group(\n", + " Item(name='name'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " label='BFF',\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7548d32b", + "metadata": { + "lines_to_next_cell": 2, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Person(HasStrictTraits):\n", + " name = Str\n", + " age = Int\n", + " handedness = Enum('left', 'right')\n", + " bff = Instance('Person')\n", + "\n", + " traits_view = View(\n", + " Group(\n", + " Item(name='name'),\n", + " Item(name='age'),\n", + " Item(name='handedness'),\n", + " Item(name='bff', style='custom', show_label=False,\n", + " editor=InstanceEditor(view=bff_view)),\n", + " label='Person profile',\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e52a3b5", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "frodo = Person(name='Frodo', age=30)\n", + "sam = Person(name='Sam', age=29, bff=frodo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10a32f0d", + "metadata": {}, + "outputs": [], + "source": [ + "sam.edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "d424c3c5", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "- Another useful editor allows us to interface with `DataFrame`s" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "881af927", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from traits.api import Event, Instance, Int \n", + "from traitsui.api import ModelView\n", + "from traitsui.ui_editors.data_frame_editor import DataFrameEditor\n", + "\n", + "class FramedData(HasStrictTraits):\n", + " data = Instance(pd.DataFrame)\n", + "\n", + " def _data_default(self):\n", + " return pd.DataFrame([\n", + " {'A': 5, 'B': 0, 'C': 3, 'D': 3},\n", + " {'A': 7, 'B': 9, 'C': 3, 'D': 5},\n", + " {'A': 2, 'B': 4, 'C': 7, 'D': 6}\n", + " ])\n", + "\n", + "class FramedDataView(ModelView):\n", + " model = Instance(FramedData)\n", + "\n", + " view = View(\n", + " Item(\"model.data\", editor=DataFrameEditor(editable=True))\n", + " )\n", + "\n", + "FramedDataView(model=FramedData()).edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "710488f9", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Enter plotting\n", + "- Another useful editor is the `MplFigureEditor`\n", + "- Allows interacting with `matplotlib.figure.Figure` instances\n", + "- Included in the `ets_tutorial` package bundled in this repository" + ] + }, + { + "cell_type": "markdown", + "id": "53e45dde", + "metadata": { + "lines_to_next_cell": 0, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d1eb3f5", + "metadata": { + "lines_to_next_cell": 0 + }, + "outputs": [], + "source": [ + "from matplotlib.figure import Figure\n", + "import numpy as np\n", + "from skimage.data import chelsea\n", + "from traits.api import Array, HasStrictTraits, Instance\n", + "from traitsui.api import View, Item\n", + "\n", + "from ets_tutorial.util.mpl_figure_editor import MplFigureEditor\n", + "\n", + "class ImageViewer(HasStrictTraits):\n", + " data = Array()\n", + "\n", + " figure = Instance(Figure)\n", + "\n", + " traits_view = View(\n", + " Item(\"figure\", editor=MplFigureEditor(), show_label=False)\n", + " )\n", + "\n", + " def _data_default(self):\n", + " return chelsea()\n", + "\n", + " def _figure_default(self):\n", + " figure = Figure()\n", + " axes = figure.add_subplot(111)\n", + " axes.imshow(chelsea())\n", + " return figure\n", + "\n", + "ImageViewer().edit_traits()" + ] + }, + { + "cell_type": "markdown", + "id": "389f0064", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The ModelView object:\n", + "- We want our science model to be free of UI code\n", + "- But it's still useful for models and views to respond to changes to one\n", + " another -- `ModelView`s \n", + "- `ModelView`s also monitor UI toolkit events like window creation,\n", + " closing, user clicking OK or Cancel buttons\n", + "- Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbaa7698", + "metadata": {}, + "outputs": [], + "source": [ + "from traits.api import observe\n", + "class Image(HasStrictTraits):\n", + " data = Array()\n", + "\n", + " def _data_default(self):\n", + " return chelsea()\n", + "\n", + "class ImageView(ModelView):\n", + " model = Instance(Image)\n", + "\n", + " figure = Instance(Figure)\n", + "\n", + " view = View(\n", + " Item(\"figure\", editor=MplFigureEditor(), show_label=False)\n", + " )\n", + "\n", + " @observe(\"model.data\")\n", + " def build_mpl_figure(self, event):\n", + " figure = Figure()\n", + " axes = figure.add_subplot(111)\n", + " axes.imshow(self.model.data)\n", + " self.figure = figure\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25cf1c10", + "metadata": {}, + "outputs": [], + "source": [ + "image = Image()\n", + "image_view = ImageView(model=image)\n", + "image_view.edit_traits()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9307e16c", + "metadata": {}, + "outputs": [], + "source": [ + "from skimage.data import astronaut\n", + "image.data = astronaut()" + ] + }, + { + "cell_type": "markdown", + "id": "07c09986", + "metadata": { + "lines_to_next_cell": 0, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise time!\n", + "- Starting from where we left off in Stage 2.1:\n", + " - Create a `ModelView` for the `ImageFile` object that displays its filepath\n", + " (readonly), and the image array in a matplotlib figure\n", + " - Ensure figure is updated if the `filepath` attribute of `ImageFile` is\n", + " modified\n", + " - Create a `ModelView` for the `ImageFolder` object that displays the directory\n", + " (readonly) and the `DataFrame`\n", + " - Bonus points:\n", + " - What mechanism would we use to hide the `DataFrame` if the directory doesn't have any images\n", + " and instead show a helpful message? \n", + " - Hint: keyword arguments for `Item`" + ] + }, + { + "cell_type": "markdown", + "id": "ff18f817", + "metadata": { + "lines_to_next_cell": 0, + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Solution\n" + ] + }, + { + "cell_type": "markdown", + "id": "fef295a6", + "metadata": {}, + "source": [ + "## Toolkit selection\n", + "\n", + "- TraitsUI supports: Qt or wxPython\n", + "- Can set the toolkit in a program\n", + " - 'qt' or 'qt4'\n", + " - 'wx'\n", + " - 'null'\n", + "- Or with the `ETS_TOOLKIT` environment variable\n", + "\n", + "```\n", + "export ETS_TOOLKIT=qt\n", + "```\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be3ed234", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "from traits.etsconfig.api import ETSConfig\n", + "ETSConfig.toolkit = 'qt'" + ] + }, + { + "cell_type": "markdown", + "id": "c807d778", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Other documentation\n", + "\n", + "- Interesting tutorial: https://docs.enthought.com/traitsui/tutorials\n" + ] + }, + { + "cell_type": "markdown", + "id": "68520cb7", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Exercise:\n", + "\n", + "\n" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/slides/03_traitsui.md b/slides/03_traitsui.md index af95413..4a02839 100644 --- a/slides/03_traitsui.md +++ b/slides/03_traitsui.md @@ -1,6 +1,7 @@ --- jupyter: jupytext: + formats: ipynb,md text_representation: extension: .md format_name: markdown diff --git a/slides/07_background_processing.ipynb b/slides/07_background_processing.ipynb new file mode 100644 index 0000000..be5cd7b --- /dev/null +++ b/slides/07_background_processing.ipynb @@ -0,0 +1,78 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c33a15af", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sharing scientific tools: script to desktop application\n", + "\n", + "### Background processing with Traits Futures\n", + "\n", + "**Jonathan Rocher, Siddhant Wahal, Jason Chambless, Corran Webster, Prabhu Ramachandran**\n", + "\n", + "**SciPy 2022**\n" + ] + }, + { + "cell_type": "markdown", + "id": "99747c70", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Traits Futures:\n", + "\n", + "- Common problems with GUI frameworks:\n", + " - GUIs are unresponsive during heavy computation\n", + " - GUI toolkits generally require that widgets are only updated from the\n", + " thread from which they were created\n", + "- Traits Futures solves both these problems:\n", + " - keeping the UI responsive\n", + " - safely update the UI in response to calculation results\n", + "- Dispatching a background task returns a `future` object which provides:\n", + " - information about job status (e.g., job partially finished, completed,\n", + " failed)\n", + " - access to the job result\n", + "- Incoming results arrive as trait changes on the main thread, ensuring thread\n", + " safety\n", + "- Supports simple callbacks, iterations, and progress-reporting functions\n", + "- Supports thread pools (default) and process pools\n" + ] + }, + { + "cell_type": "markdown", + "id": "6c6c4653", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Traits Futures\n", + "- Let's dive in with an example\n", + "- Installation:\n", + " - `edm install -e ets_tutorial traits_futures`\n", + " - Conda/pip: Activate virtual environment and `pip install traits_futures`\n" + ] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,md" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/slides/07_background_processing.md b/slides/07_background_processing.md index 5503a01..496e916 100644 --- a/slides/07_background_processing.md +++ b/slides/07_background_processing.md @@ -1,11 +1,12 @@ --- jupyter: jupytext: + formats: ipynb,md text_representation: extension: .md format_name: markdown format_version: '1.3' - jupytext_version: 1.14.0 + jupytext_version: 1.13.7 kernelspec: display_name: Python 3 (ipykernel) language: python @@ -51,4 +52,4 @@ jupyter: - `edm install -e ets_tutorial traits_futures` - Conda/pip: Activate virtual environment and `pip install traits_futures` - \ No newline at end of file + diff --git a/slides/template.md b/slides/template.md index 3d60407..44e3998 100644 --- a/slides/template.md +++ b/slides/template.md @@ -1,11 +1,12 @@ --- jupyter: jupytext: + formats: ipynb,md text_representation: extension: .md format_name: markdown - format_version: '1.2' - jupytext_version: 1.3.2 + format_version: '1.3' + jupytext_version: 1.13.7 kernelspec: display_name: Python 3 language: python