From 33fab94deb6137afee30f38ea85032bb74c65439 Mon Sep 17 00:00:00 2001 From: Ben Hastings Date: Tue, 12 Dec 2023 11:43:43 +0100 Subject: [PATCH 1/4] tutorials --- learning-projects/yield.ipynb | 163 -------- .../Async-http-benchmarks.ipynb | 0 ...actingMetadataFromFilenamesWithGlob2.ipynb | 388 ++++++++++++++++++ tutorials/Loading Env Files with DotEnv.ipynb | 216 ++++++++++ tutorials/Match-case-statements.ipynb | 163 ++++++++ tutorials/Observable.ipynb | 145 +++++++ tutorials/README.md | 5 + 7 files changed, 917 insertions(+), 163 deletions(-) delete mode 100644 learning-projects/yield.ipynb rename learning-projects/async-http/benchmarks.ipynb => tutorials/Async-http-benchmarks.ipynb (100%) create mode 100644 tutorials/ExtractingMetadataFromFilenamesWithGlob2.ipynb create mode 100644 tutorials/Loading Env Files with DotEnv.ipynb create mode 100644 tutorials/Match-case-statements.ipynb create mode 100644 tutorials/Observable.ipynb create mode 100644 tutorials/README.md diff --git a/learning-projects/yield.ipynb b/learning-projects/yield.ipynb deleted file mode 100644 index b77d5dac..00000000 --- a/learning-projects/yield.ipynb +++ /dev/null @@ -1,163 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "def add_one(num: int):\n", - " x = num + 1\n", - " yield x\n", - " # y = x + 1\n", - " # yield y" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "11" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x =add_one(10)\n", - "next(x)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "ename": "StopIteration", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mStopIteration\u001b[0m: " - ] - } - ], - "source": [ - "next(x)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "def running_total():\n", - " total = 0\n", - " while True:\n", - " num = int(input('Enter a number : '))\n", - " total += num\n", - " print(total)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "def running_total_yield():\n", - " total = 0\n", - " while True:\n", - " num = yield total\n", - " total += num" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "15" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "z = running_total_yield()\n", - "next(z)\n", - "z.send(5)\n", - "z.send(10)" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] - }, - { - "ename": "TypeError", - "evalue": "unsupported operand type(s) for +=: 'int' and 'NoneType'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mi\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m<\u001b[0m\u001b[0;36m100\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mz\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mi\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m\u001b[0m in \u001b[0;36mrunning_total_yield\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mnum\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32myield\u001b[0m \u001b[0mtotal\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mtotal\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mnum\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +=: 'int' and 'NoneType'" - ] - } - ], - "source": [ - "i = 1\n", - "while i<100:\n", - " print(next(z))\n", - " i += 1" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/learning-projects/async-http/benchmarks.ipynb b/tutorials/Async-http-benchmarks.ipynb similarity index 100% rename from learning-projects/async-http/benchmarks.ipynb rename to tutorials/Async-http-benchmarks.ipynb diff --git a/tutorials/ExtractingMetadataFromFilenamesWithGlob2.ipynb b/tutorials/ExtractingMetadataFromFilenamesWithGlob2.ipynb new file mode 100644 index 00000000..7b61e248 --- /dev/null +++ b/tutorials/ExtractingMetadataFromFilenamesWithGlob2.ipynb @@ -0,0 +1,388 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Extracting Metadata from filenames in Python using the `glob2` package" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Installing the Package" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GitHub Page with Docs: https://github.com/miracle2k/python-glob2/" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: glob2 in /opt/conda/lib/python3.10/site-packages (0.7)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install glob2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Our Problem\n", + "\n", + "Below are the filenames that are in our directory. What we want to do are:\n", + "\n", + "1. Get only the CSV files\n", + "2. Get metadata from each filename:\n", + " - The File's ID (the number after the \"t\")\n", + " - The Town from which the file came from (Southampton, Queensland, or Cherbourg)\n", + " - Whether the data comes from survivors or non-survivors (1 and 0, respectively)\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "example_filenames = [\n", + " 't604697_sout_1.csv',\n", + " 't82533_quee_1.csv',\n", + " 't88553_quee_0.csv',\n", + " 't244431_sout_0.csv',\n", + " '.gitpod.yml',\n", + " 'aa.py',\n", + " 't61137_cher_1.csv',\n", + " 't13387_cher_0.csv',\n", + " '.git'\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Setup the Example\n", + "\n", + "In the code below, I'm simply overriding the glob2's corresponding method so that it returns our example files, instead of the actual files on the computer, just to make it easier to try out this example situation.\n", + "\n", + "Note: This wouldn't be done in a real-world situation." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['t604697_sout_1.csv',\n", + " 't82533_quee_1.csv',\n", + " 't88553_quee_0.csv',\n", + " 't244431_sout_0.csv',\n", + " '.gitpod.yml',\n", + " 'aa.py',\n", + " 't61137_cher_1.csv',\n", + " 't13387_cher_0.csv',\n", + " '.git']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from unittest.mock import Mock\n", + "from glob2 import Globber\n", + "\n", + "Globber.listdir = Mock()\n", + "Globber.listdir.return_value = example_filenames\n", + "Globber.listdir()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What Can We Do?\n", + "\n", + "All we need is the `glob()` function from the `glob2` package; the difference from the built-in version is in its extra keywords." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "from glob2 import glob" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Find All Files, just like the built-in glob.glob()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['t604697_sout_1.csv',\n", + " 't82533_quee_1.csv',\n", + " 't88553_quee_0.csv',\n", + " 't244431_sout_0.csv',\n", + " 'aa.py',\n", + " 't61137_cher_1.csv',\n", + " 't13387_cher_0.csv']" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "glob('*')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Get all Files that match a wildcard pattern:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['t604697_sout_1.csv',\n", + " 't82533_quee_1.csv',\n", + " 't88553_quee_0.csv',\n", + " 't244431_sout_0.csv',\n", + " 't61137_cher_1.csv',\n", + " 't13387_cher_0.csv']" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "glob('*.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Retain the data that matched each wildcard! \n", + "\n", + "Notice below, by adding more wildcards around the file seperaters, we could get each filename *and* the pattern it matched." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('t604697_sout_1.csv', ('604697', 'sout', '1')),\n", + " ('t82533_quee_1.csv', ('82533', 'quee', '1')),\n", + " ('t88553_quee_0.csv', ('88553', 'quee', '0')),\n", + " ('t244431_sout_0.csv', ('244431', 'sout', '0')),\n", + " ('t61137_cher_1.csv', ('61137', 'cher', '1')),\n", + " ('t13387_cher_0.csv', ('13387', 'cher', '0'))]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "glob(\"t*_*_*.csv\", with_matches=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Useful Pattern: Organize filenames and metadata into a Pandas DataFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
filenameidcitysurvived
0t604697_sout_1.csv604697sout1
1t82533_quee_1.csv82533quee1
2t88553_quee_0.csv88553quee0
3t244431_sout_0.csv244431sout0
4t61137_cher_1.csv61137cher1
5t13387_cher_0.csv13387cher0
\n", + "
" + ], + "text/plain": [ + " filename id city survived\n", + "0 t604697_sout_1.csv 604697 sout 1\n", + "1 t82533_quee_1.csv 82533 quee 1\n", + "2 t88553_quee_0.csv 88553 quee 0\n", + "3 t244431_sout_0.csv 244431 sout 0\n", + "4 t61137_cher_1.csv 61137 cher 1\n", + "5 t13387_cher_0.csv 13387 cher 0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + " \n", + "records = []\n", + "for fname, (id_number, city, survived) in glob(\"t*_*_*.csv\", with_matches=True):\n", + " record = {'filename': fname, 'id': id_number, 'city': city, 'survived': survived}\n", + " records.append(record)\n", + " \n", + "pd.DataFrame(records)\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's it! Pretty neat, right?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/Loading Env Files with DotEnv.ipynb b/tutorials/Loading Env Files with DotEnv.ipynb new file mode 100644 index 00000000..9d9f9373 --- /dev/null +++ b/tutorials/Loading Env Files with DotEnv.ipynb @@ -0,0 +1,216 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Loading .env Files into Environment Variabiles with Python-DotEnv\n", + "\n", + "Documentation: https://pypi.org/project/python-dotenv/" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting python-dotenv\n", + " Downloading python_dotenv-1.0.0-py3-none-any.whl (19 kB)\n", + "Installing collected packages: python-dotenv\n", + "Successfully installed python-dotenv-1.0.0\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install python-dotenv " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Problem: .env files are great, but IDE Tooling is Fickle\n", + "\n", + "We use .env files to specify which environment variables should be present for a project, but so far actually using them (particularly from the terminal, where we've been copy-pasting `export {KEY}={VALUE}` over and over again) has gotten annoying.\n", + "\n", + "It would be great if the .env files were actually integrated into the code, without sacrificing the security aspects of it and losing out on the IDE features when they are used. Reading a bit, it looks like `python-dotenv` might be the magic there.\n", + "\n", + "\n", + "### Goal: Use Python-DotEnv to load variables in a .env file into the environment that the Python script is using." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Step 1: Create a .env file." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "23" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "env_text = \"\"\"\n", + "VAR1=hello\n", + "VAR2=world\n", + "\"\"\"\n", + "Path('.env').write_text(env_text)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Step 2: Check that the VAR1 and VAR2 environment variables don't exist." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ.get('VAR1')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "os.environ.get('VAR2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Step 3: Load the .env file with `dotenv.load_dotenv()`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dotenv import load_dotenv\n", + "load_dotenv()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Step 3: Check that the VAR1 and VAR2 environment variables now do exist!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'hello'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "os.environ.get('VAR1')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'world'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "os.environ.get('VAR2')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Okay, that was easy. Great tool!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "httpx", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/Match-case-statements.ipynb b/tutorials/Match-case-statements.ipynb new file mode 100644 index 00000000..c9165baf --- /dev/null +++ b/tutorials/Match-case-statements.ipynb @@ -0,0 +1,163 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ced42e3c", + "metadata": {}, + "source": [ + "# Match-case Statements in Python\n", + "\n", + "match-case statements are a more concise alternative to writing large blocks of if statements. \n", + "\n", + "They work like so\n", + "\n", + "```\n", + "match parameter:\n", + " case condition_A: \n", + " do_something()\n", + " case condition_B:\n", + " do_somethingelse()\n", + " case _ :\n", + " nothing()\n", + "```\n", + "\n", + "\n", + "Code indented under each case statement will be executed when that case is matched. \n", + "\n", + "The case ```_``` is run when no other cases are matched.\n", + "\n", + "Multiple paramters may be given, and they can be checked against separate conditions (see example below)\n" + ] + }, + { + "cell_type": "markdown", + "id": "ada4bd4c", + "metadata": {}, + "source": [ + "## Warning\n", + "Match-case statements were introduced in Python 3.10. To check that your Python version is later than 3.10 run" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c0461a6b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python 3.10.12\r\n" + ] + } + ], + "source": [ + "! python --version" + ] + }, + { + "cell_type": "markdown", + "id": "a8d1152f", + "metadata": {}, + "source": [ + "# Example\n", + "\n", + "The family Price has 4 members:\n", + "\n", + "* John, 39 years old\n", + "* Betty, 37 years old\n", + "* Elisa, 8 years old\n", + "* John the dog, 3 years old\n", + "\n", + "The match-case statement below gets the details of a family member, given their name and whether they are human or not.\n", + "\n", + "The values of ```name``` and ```is_human``` are checked whether they are equal to the values given in each case statement" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "5367f9cd", + "metadata": {}, + "outputs": [], + "source": [ + "name = 'John'\n", + "is_human = False\n", + "\n", + "match name, is_human:\n", + " case 'John', True:\n", + " age = 39\n", + " role = 'Father'\n", + " case 'Betty', True:\n", + " age = 37\n", + " role = 'Mother'\n", + " case 'Elisa', True:\n", + " age = 8\n", + " role = 'Daughter'\n", + " case 'John', False:\n", + " age = 3 \n", + " role = 'Dog'" + ] + }, + { + "cell_type": "markdown", + "id": "f0b54589", + "metadata": {}, + "source": [ + "The match-case statement is much more concise and readable that the equivalent block of if statements:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "21dc2038", + "metadata": {}, + "outputs": [], + "source": [ + "if name == 'John':\n", + " if is_human:\n", + " age = 39\n", + " role = 'Father'\n", + " else:\n", + " age = 3 \n", + " role = 'Dog'\n", + "elif name == 'Betty Price':\n", + " age = 37\n", + " role = 'Mother'\n", + "elif name == 'Elisa Price':\n", + " age = 8\n", + " role = 'Daughter'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0eab6d5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/Observable.ipynb b/tutorials/Observable.ipynb new file mode 100644 index 00000000..7882cbc0 --- /dev/null +++ b/tutorials/Observable.ipynb @@ -0,0 +1,145 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Observable \n", + "\n", + "When making a GUI, the system must react only when something is changed (eg. user clicks somewhere). \n", + "\n", + "This class allows a Signal to be sent from an Observable which allows data to be modified" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Callable, Optional, Set, TypeVar, Generic\n", + "\n", + "\n", + "class Signal:\n", + " \"\"\"\n", + " Signals are responsible for connecting obesrvables and observers, passing data from one to the other.\n", + " \n", + " Signal.connect() registers an observer function.\n", + " Signal.send(args, kwargs) calls all registered observer functions with the args and kwargs.\n", + " \"\"\"\n", + " def __init__(self) -> None:\n", + " self._funs: Set[Callable] = set()\n", + "\n", + " def connect(self, fun) -> None:\n", + " self._funs.add(fun)\n", + "\n", + " def send(self, *args, **kwargs) -> None:\n", + " for fun in self._funs:\n", + " fun(*args, **kwargs)\n", + "\n", + "\n", + "T = TypeVar('T')\n", + "\n", + "\n", + "class Observable(Generic[T]):\n", + " \"\"\"\n", + " The Observable sends an \"updated\" Signal whenever its data attribute is updated.\n", + " It's useful for managing state of immutable data.\n", + " \"\"\"\n", + " updated: Signal\n", + "\n", + " def __init__(self, data: T, updated: Optional[Signal] = None):\n", + " self._data = data\n", + " self.updated: Signal = updated if updated else Signal()\n", + "\n", + " def send_all(self) -> None:\n", + " self.updated.send(self._data)\n", + "\n", + " @property\n", + " def data(self) -> T:\n", + " return self._data\n", + " \n", + " @data.setter\n", + " def data(self, value: T) -> None:\n", + " self._data = value\n", + " self.updated.send(self._data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unit tests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from unittest.mock import Mock\n", + "from web.observable import Signal, Observable\n", + "\n", + "\n", + "def test_signal_doesnt_call_functions_on_connection():\n", + " signal = Signal()\n", + " fun1 = Mock()\n", + "\n", + " signal.connect(fun1)\n", + " \n", + " fun1.assert_not_called()\n", + "\n", + "\n", + "def test_signal_pipes_data_to_all_connected_functions():\n", + " signal = Signal()\n", + " fun1 = Mock()\n", + " fun2 = Mock()\n", + "\n", + " signal.connect(fun1)\n", + " \n", + " signal.send(hi='hello')\n", + " fun1.assert_called_once_with(hi='hello')\n", + " fun2.assert_not_called()\n", + "\n", + " signal.connect(fun2)\n", + " signal.send(bye='goodbye')\n", + " fun1.assert_called_with(bye='goodbye')\n", + " fun2.assert_called_once_with(bye='goodbye')\n", + "\n", + " \n", + " \n", + "\n", + "def test_state_sends_update_whenever_a_new_model_is_set():\n", + " update_signal = Mock()\n", + " state = Observable(data=Mock(), updated=update_signal)\n", + " update_signal.send.assert_not_called()\n", + "\n", + " for _ in range(3):\n", + " new_model = Mock()\n", + " state.data = new_model\n", + " update_signal.send.assert_called_with(new_model)\n", + "\n", + "\n", + "def tests_state_sendall_sends_update():\n", + " update_signal = Mock()\n", + " data = Mock()\n", + " state = Observable(data=data, updated=update_signal)\n", + "\n", + " update_signal.send.assert_not_called()\n", + " state.send_all()\n", + " update_signal.send.assert_called_with(data)\n", + "\n", + " " + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 00000000..11cb9bb7 --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,5 @@ +The tutorials directory is a home for reference material to solve a specific problem or highlight a particular feature. + +Each file should be self-contained and focus on one topic. + +Each file must be documented to its purpose self-explanatory. \ No newline at end of file From d0d8e842942f964961308352f0539edd8d2c6a0d Mon Sep 17 00:00:00 2001 From: Ben Hastings Date: Tue, 12 Dec 2023 11:57:02 +0100 Subject: [PATCH 2/4] learning projects --- ...actingMetadataFromFilenamesWithGlob2.ipynb | 388 ------------------ .../Loading Env Files with DotEnv.ipynb | 216 ---------- .../Match-case-statements.ipynb | 163 -------- 3 files changed, 767 deletions(-) delete mode 100644 programming-tricks/ExtractingMetadataFromFilenamesWithGlob2.ipynb delete mode 100644 programming-tricks/Loading Env Files with DotEnv.ipynb delete mode 100644 programming-tricks/Match-case-statements.ipynb diff --git a/programming-tricks/ExtractingMetadataFromFilenamesWithGlob2.ipynb b/programming-tricks/ExtractingMetadataFromFilenamesWithGlob2.ipynb deleted file mode 100644 index 7b61e248..00000000 --- a/programming-tricks/ExtractingMetadataFromFilenamesWithGlob2.ipynb +++ /dev/null @@ -1,388 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Extracting Metadata from filenames in Python using the `glob2` package" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installing the Package" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "GitHub Page with Docs: https://github.com/miracle2k/python-glob2/" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: glob2 in /opt/conda/lib/python3.10/site-packages (0.7)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install glob2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Our Problem\n", - "\n", - "Below are the filenames that are in our directory. What we want to do are:\n", - "\n", - "1. Get only the CSV files\n", - "2. Get metadata from each filename:\n", - " - The File's ID (the number after the \"t\")\n", - " - The Town from which the file came from (Southampton, Queensland, or Cherbourg)\n", - " - Whether the data comes from survivors or non-survivors (1 and 0, respectively)\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "example_filenames = [\n", - " 't604697_sout_1.csv',\n", - " 't82533_quee_1.csv',\n", - " 't88553_quee_0.csv',\n", - " 't244431_sout_0.csv',\n", - " '.gitpod.yml',\n", - " 'aa.py',\n", - " 't61137_cher_1.csv',\n", - " 't13387_cher_0.csv',\n", - " '.git'\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Setup the Example\n", - "\n", - "In the code below, I'm simply overriding the glob2's corresponding method so that it returns our example files, instead of the actual files on the computer, just to make it easier to try out this example situation.\n", - "\n", - "Note: This wouldn't be done in a real-world situation." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['t604697_sout_1.csv',\n", - " 't82533_quee_1.csv',\n", - " 't88553_quee_0.csv',\n", - " 't244431_sout_0.csv',\n", - " '.gitpod.yml',\n", - " 'aa.py',\n", - " 't61137_cher_1.csv',\n", - " 't13387_cher_0.csv',\n", - " '.git']" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from unittest.mock import Mock\n", - "from glob2 import Globber\n", - "\n", - "Globber.listdir = Mock()\n", - "Globber.listdir.return_value = example_filenames\n", - "Globber.listdir()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## What Can We Do?\n", - "\n", - "All we need is the `glob()` function from the `glob2` package; the difference from the built-in version is in its extra keywords." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "from glob2 import glob" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Find All Files, just like the built-in glob.glob()" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['t604697_sout_1.csv',\n", - " 't82533_quee_1.csv',\n", - " 't88553_quee_0.csv',\n", - " 't244431_sout_0.csv',\n", - " 'aa.py',\n", - " 't61137_cher_1.csv',\n", - " 't13387_cher_0.csv']" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "glob('*')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Get all Files that match a wildcard pattern:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['t604697_sout_1.csv',\n", - " 't82533_quee_1.csv',\n", - " 't88553_quee_0.csv',\n", - " 't244431_sout_0.csv',\n", - " 't61137_cher_1.csv',\n", - " 't13387_cher_0.csv']" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "glob('*.csv')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Retain the data that matched each wildcard! \n", - "\n", - "Notice below, by adding more wildcards around the file seperaters, we could get each filename *and* the pattern it matched." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[('t604697_sout_1.csv', ('604697', 'sout', '1')),\n", - " ('t82533_quee_1.csv', ('82533', 'quee', '1')),\n", - " ('t88553_quee_0.csv', ('88553', 'quee', '0')),\n", - " ('t244431_sout_0.csv', ('244431', 'sout', '0')),\n", - " ('t61137_cher_1.csv', ('61137', 'cher', '1')),\n", - " ('t13387_cher_0.csv', ('13387', 'cher', '0'))]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "glob(\"t*_*_*.csv\", with_matches=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Useful Pattern: Organize filenames and metadata into a Pandas DataFrame" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
filenameidcitysurvived
0t604697_sout_1.csv604697sout1
1t82533_quee_1.csv82533quee1
2t88553_quee_0.csv88553quee0
3t244431_sout_0.csv244431sout0
4t61137_cher_1.csv61137cher1
5t13387_cher_0.csv13387cher0
\n", - "
" - ], - "text/plain": [ - " filename id city survived\n", - "0 t604697_sout_1.csv 604697 sout 1\n", - "1 t82533_quee_1.csv 82533 quee 1\n", - "2 t88553_quee_0.csv 88553 quee 0\n", - "3 t244431_sout_0.csv 244431 sout 0\n", - "4 t61137_cher_1.csv 61137 cher 1\n", - "5 t13387_cher_0.csv 13387 cher 0" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import pandas as pd\n", - " \n", - "records = []\n", - "for fname, (id_number, city, survived) in glob(\"t*_*_*.csv\", with_matches=True):\n", - " record = {'filename': fname, 'id': id_number, 'city': city, 'survived': survived}\n", - " records.append(record)\n", - " \n", - "pd.DataFrame(records)\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That's it! Pretty neat, right?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/programming-tricks/Loading Env Files with DotEnv.ipynb b/programming-tricks/Loading Env Files with DotEnv.ipynb deleted file mode 100644 index 9d9f9373..00000000 --- a/programming-tricks/Loading Env Files with DotEnv.ipynb +++ /dev/null @@ -1,216 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Loading .env Files into Environment Variabiles with Python-DotEnv\n", - "\n", - "Documentation: https://pypi.org/project/python-dotenv/" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Collecting python-dotenv\n", - " Downloading python_dotenv-1.0.0-py3-none-any.whl (19 kB)\n", - "Installing collected packages: python-dotenv\n", - "Successfully installed python-dotenv-1.0.0\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install python-dotenv " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### The Problem: .env files are great, but IDE Tooling is Fickle\n", - "\n", - "We use .env files to specify which environment variables should be present for a project, but so far actually using them (particularly from the terminal, where we've been copy-pasting `export {KEY}={VALUE}` over and over again) has gotten annoying.\n", - "\n", - "It would be great if the .env files were actually integrated into the code, without sacrificing the security aspects of it and losing out on the IDE features when they are used. Reading a bit, it looks like `python-dotenv` might be the magic there.\n", - "\n", - "\n", - "### Goal: Use Python-DotEnv to load variables in a .env file into the environment that the Python script is using." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Step 1: Create a .env file." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "23" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from pathlib import Path\n", - "\n", - "env_text = \"\"\"\n", - "VAR1=hello\n", - "VAR2=world\n", - "\"\"\"\n", - "Path('.env').write_text(env_text)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Step 2: Check that the VAR1 and VAR2 environment variables don't exist." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "os.environ.get('VAR1')" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "os.environ.get('VAR2')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Step 3: Load the .env file with `dotenv.load_dotenv()`" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from dotenv import load_dotenv\n", - "load_dotenv()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Step 3: Check that the VAR1 and VAR2 environment variables now do exist!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'hello'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "os.environ.get('VAR1')" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'world'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "os.environ.get('VAR2')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Okay, that was easy. Great tool!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "httpx", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/programming-tricks/Match-case-statements.ipynb b/programming-tricks/Match-case-statements.ipynb deleted file mode 100644 index c9165baf..00000000 --- a/programming-tricks/Match-case-statements.ipynb +++ /dev/null @@ -1,163 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "ced42e3c", - "metadata": {}, - "source": [ - "# Match-case Statements in Python\n", - "\n", - "match-case statements are a more concise alternative to writing large blocks of if statements. \n", - "\n", - "They work like so\n", - "\n", - "```\n", - "match parameter:\n", - " case condition_A: \n", - " do_something()\n", - " case condition_B:\n", - " do_somethingelse()\n", - " case _ :\n", - " nothing()\n", - "```\n", - "\n", - "\n", - "Code indented under each case statement will be executed when that case is matched. \n", - "\n", - "The case ```_``` is run when no other cases are matched.\n", - "\n", - "Multiple paramters may be given, and they can be checked against separate conditions (see example below)\n" - ] - }, - { - "cell_type": "markdown", - "id": "ada4bd4c", - "metadata": {}, - "source": [ - "## Warning\n", - "Match-case statements were introduced in Python 3.10. To check that your Python version is later than 3.10 run" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c0461a6b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Python 3.10.12\r\n" - ] - } - ], - "source": [ - "! python --version" - ] - }, - { - "cell_type": "markdown", - "id": "a8d1152f", - "metadata": {}, - "source": [ - "# Example\n", - "\n", - "The family Price has 4 members:\n", - "\n", - "* John, 39 years old\n", - "* Betty, 37 years old\n", - "* Elisa, 8 years old\n", - "* John the dog, 3 years old\n", - "\n", - "The match-case statement below gets the details of a family member, given their name and whether they are human or not.\n", - "\n", - "The values of ```name``` and ```is_human``` are checked whether they are equal to the values given in each case statement" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "5367f9cd", - "metadata": {}, - "outputs": [], - "source": [ - "name = 'John'\n", - "is_human = False\n", - "\n", - "match name, is_human:\n", - " case 'John', True:\n", - " age = 39\n", - " role = 'Father'\n", - " case 'Betty', True:\n", - " age = 37\n", - " role = 'Mother'\n", - " case 'Elisa', True:\n", - " age = 8\n", - " role = 'Daughter'\n", - " case 'John', False:\n", - " age = 3 \n", - " role = 'Dog'" - ] - }, - { - "cell_type": "markdown", - "id": "f0b54589", - "metadata": {}, - "source": [ - "The match-case statement is much more concise and readable that the equivalent block of if statements:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "21dc2038", - "metadata": {}, - "outputs": [], - "source": [ - "if name == 'John':\n", - " if is_human:\n", - " age = 39\n", - " role = 'Father'\n", - " else:\n", - " age = 3 \n", - " role = 'Dog'\n", - "elif name == 'Betty Price':\n", - " age = 37\n", - " role = 'Mother'\n", - "elif name == 'Elisa Price':\n", - " age = 8\n", - " role = 'Daughter'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f0eab6d5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 7c42654280e7b03cd857eb9889d3a0279a7ac15e Mon Sep 17 00:00:00 2001 From: Ben Hastings Date: Tue, 12 Dec 2023 11:58:40 +0100 Subject: [PATCH 3/4] prog tricks removed --- learning-projects/README.md | 7 +++ programming-tricks/observable/observable.py | 46 ---------------- .../observable/test_observable.py | 53 ------------------- 3 files changed, 7 insertions(+), 99 deletions(-) create mode 100644 learning-projects/README.md delete mode 100644 programming-tricks/observable/observable.py delete mode 100644 programming-tricks/observable/test_observable.py diff --git a/learning-projects/README.md b/learning-projects/README.md new file mode 100644 index 00000000..c1c532ef --- /dev/null +++ b/learning-projects/README.md @@ -0,0 +1,7 @@ +Here is a place for all our learning projects. + +Want to learn how to write machine code? Or run jupyter-notebooks with 32 CPUs? Then here is where you document your learning progress and process. + +Each learning project is housed in a separate folder, which can contain anything you want. + +The **only** requirement is include a README.md file that briefly explains what the learning project is. \ No newline at end of file diff --git a/programming-tricks/observable/observable.py b/programming-tricks/observable/observable.py deleted file mode 100644 index 37ab0a00..00000000 --- a/programming-tricks/observable/observable.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Callable, Optional, Set, TypeVar, Generic - - -class Signal: - """ - Signals are responsible for connecting obesrvables and observers, passing data from one to the other. - - Signal.connect() registers an observer function. - Signal.send(args, kwargs) calls all registered observer functions with the args and kwargs. - """ - def __init__(self) -> None: - self._funs: Set[Callable] = set() - - def connect(self, fun) -> None: - self._funs.add(fun) - - def send(self, *args, **kwargs) -> None: - for fun in self._funs: - fun(*args, **kwargs) - - -T = TypeVar('T') - - -class Observable(Generic[T]): - """ - The Observable sends an "updated" Signal whenever its data attribute is updated. - It's useful for managing state of immutable data. - """ - updated: Signal - - def __init__(self, data: T, updated: Optional[Signal] = None): - self._data = data - self.updated: Signal = updated if updated else Signal() - - def send_all(self) -> None: - self.updated.send(self._data) - - @property - def data(self) -> T: - return self._data - - @data.setter - def data(self, value: T) -> None: - self._data = value - self.updated.send(self._data) \ No newline at end of file diff --git a/programming-tricks/observable/test_observable.py b/programming-tricks/observable/test_observable.py deleted file mode 100644 index df134660..00000000 --- a/programming-tricks/observable/test_observable.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest.mock import Mock -from web.observable import Signal, Observable - - -def test_signal_doesnt_call_functions_on_connection(): - signal = Signal() - fun1 = Mock() - - signal.connect(fun1) - - fun1.assert_not_called() - - -def test_signal_pipes_data_to_all_connected_functions(): - signal = Signal() - fun1 = Mock() - fun2 = Mock() - - signal.connect(fun1) - - signal.send(hi='hello') - fun1.assert_called_once_with(hi='hello') - fun2.assert_not_called() - - signal.connect(fun2) - signal.send(bye='goodbye') - fun1.assert_called_with(bye='goodbye') - fun2.assert_called_once_with(bye='goodbye') - - - - -def test_state_sends_update_whenever_a_new_model_is_set(): - update_signal = Mock() - state = Observable(data=Mock(), updated=update_signal) - update_signal.send.assert_not_called() - - for _ in range(3): - new_model = Mock() - state.data = new_model - update_signal.send.assert_called_with(new_model) - - -def tests_state_sendall_sends_update(): - update_signal = Mock() - data = Mock() - state = Observable(data=data, updated=update_signal) - - update_signal.send.assert_not_called() - state.send_all() - update_signal.send.assert_called_with(data) - - \ No newline at end of file From 3e3a888b0fabd9b67655bce3ca14f57e195d121e Mon Sep 17 00:00:00 2001 From: Ben Hastings Date: Tue, 12 Dec 2023 11:59:50 +0100 Subject: [PATCH 4/4] readme update --- tutorials/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/README.md b/tutorials/README.md index 11cb9bb7..4c72b014 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -2,4 +2,4 @@ The tutorials directory is a home for reference material to solve a specific pro Each file should be self-contained and focus on one topic. -Each file must be documented to its purpose self-explanatory. \ No newline at end of file +Each file must be documented so that its purpose is self-explanatory. \ No newline at end of file