From eea657c78ecc01f08eec238055ab40598701ab37 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 01:39:14 -0700 Subject: [PATCH 01/32] Add a little flask application to demonstrate facets with python. --- python/example-pytest-selfie/README.md | 7 + python/example-pytest-selfie/app.py | 197 ++++++++++++++++ python/example-pytest-selfie/poetry.lock | 246 ++++++++++++++++++-- python/example-pytest-selfie/pyproject.toml | 1 + 4 files changed, 426 insertions(+), 25 deletions(-) create mode 100644 python/example-pytest-selfie/README.md create mode 100644 python/example-pytest-selfie/app.py diff --git a/python/example-pytest-selfie/README.md b/python/example-pytest-selfie/README.md new file mode 100644 index 00000000..a7d5d227 --- /dev/null +++ b/python/example-pytest-selfie/README.md @@ -0,0 +1,7 @@ +The purpose of this project is to demonstrate selfie for the manual. + +Go to https://selfie.dev/py/facets for the tutorial. + +- First run `poetry install` +- You can run the app locally with `poetry run python app.py` +- You can run the tests with `poetry run pytest` diff --git a/python/example-pytest-selfie/app.py b/python/example-pytest-selfie/app.py new file mode 100644 index 00000000..95fdf598 --- /dev/null +++ b/python/example-pytest-selfie/app.py @@ -0,0 +1,197 @@ +# app.py +import base64 +import hashlib +import secrets +import threading +import time +from datetime import datetime, timedelta +from functools import wraps + +from flask import ( + Flask, + jsonify, + make_response, + redirect, + render_template_string, + request, +) + +app = Flask(__name__) + +# In-memory database (replace with a real database in production) +database = {} + +# Email storage for development +email_storage = [] +email_lock = threading.Lock() +email_condition = threading.Condition(email_lock) + + +class DevTime: + def __init__(self): + self.now = datetime(2000, 1, 1) + + def set_year(self, year): + self.now = datetime(year, 1, 1) + + def advance_24hrs(self): + self.now += timedelta(days=1) + + def now(self): + return self.now + + +dev_time = DevTime() + + +def repeatable_random(length): + # This is a simplified version, not as secure as Java's SecureRandom + return "".join( + secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + for _ in range(length) + ) + + +def send_email(to_email, subject, html_content): + email = {"to": to_email, "subject": subject, "html_content": html_content} + with email_lock: + email_storage.append(email) + email_condition.notify_all() + + +def sign_email(email): + terrible_security = "password" + return base64.urlsafe_b64encode( + hashlib.sha256(f"{email}{terrible_security}".encode()).digest() + ).decode() + + +def auth_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + user = auth_user() + if user is None: + return redirect("/") + return f(user, *args, **kwargs) + + return decorated_function + + +def auth_user(): + login_cookie = request.cookies.get("login") + if not login_cookie: + return None + email, signature = login_cookie.split("|") + if signature != sign_email(email): + return None + return {"email": email} + + +@app.route("/") +def index(): + user = auth_user() + if user: + return render_template_string( + """ + +

Welcome back {{ username }}

+ + """, + username=user["email"], + ) + else: + return render_template_string(""" + +

Please login

+
+ + +
+ + """) + + +@app.route("/login", methods=["POST"]) +def login(): + email = request.form["email"] + random_code = repeatable_random(7) + database[random_code] = email + + login_link = f"http://{request.host}/login-confirm/{random_code}" + send_email( + email, + "Login to example.com", + f'Click here to login.', + ) + + return render_template_string(""" + +

Email sent!

+

Check your email for your login link.

+ + """) + + +@app.route("/login-confirm/") +def login_confirm(code): + email = database.pop(code, None) + if email is None: + return render_template_string(""" + +

Login link expired.

+

Sorry, try again.

+ + """) + + response = make_response(redirect("/")) + response.set_cookie("login", f"{email}|{sign_email(email)}") + return response + + +@app.route("/email") +def email_list(): + messages = email_storage + html = "

Messages

" + return html + + +@app.route("/email/message/") +def email_message(idx): + idx -= 1 + if 0 <= idx < len(email_storage): + return email_storage[idx]["html_content"] + else: + return "No such message" + + +def wait_for_incoming_email(timeout=1): + start_time = time.time() + with email_lock: + while len(email_storage) == 0: + remaining_time = timeout - (time.time() - start_time) + if remaining_time <= 0: + raise TimeoutError("Email wasn't sent within the specified timeout") + email_condition.wait(timeout=remaining_time) + return email_storage[-1] + + +@app.route("/dev/time", methods=["POST"]) +def set_dev_time(): + action = request.json.get("action") + if action == "set_year": + year = request.json.get("year") + dev_time.set_year(year) + elif action == "advance_24hrs": + dev_time.advance_24hrs() + return jsonify({"current_time": dev_time.now().isoformat()}) + + +if __name__ == "__main__": + print("Opening selfie demo app at http://localhost:5000") + app.run(debug=True) diff --git a/python/example-pytest-selfie/poetry.lock b/python/example-pytest-selfie/poetry.lock index 642e49bb..e1bc5c89 100644 --- a/python/example-pytest-selfie/poetry.lock +++ b/python/example-pytest-selfie/poetry.lock @@ -33,6 +33,17 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "blinker" +version = "1.8.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, +] + [[package]] name = "certifi" version = "2024.6.2" @@ -44,6 +55,20 @@ files = [ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -80,6 +105,29 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + [[package]] name = "h11" version = "0.14.0" @@ -147,6 +195,25 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "importlib-metadata" +version = "7.2.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, + {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -158,6 +225,103 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -171,13 +335,13 @@ files = [ [[package]] name = "openai" -version = "1.34.0" +version = "1.35.3" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.34.0-py3-none-any.whl", hash = "sha256:018623c2f795424044675c6230fa3bfbf98d9e0aab45d8fd116f2efb2cfb6b7e"}, - {file = "openai-1.34.0.tar.gz", hash = "sha256:95c8e2da4acd6958e626186957d656597613587195abd0fb2527566a93e76770"}, + {file = "openai-1.35.3-py3-none-any.whl", hash = "sha256:7b26544cef80f125431c073ffab3811d2421fbb9e30d3bd5c2436aba00b042d5"}, + {file = "openai-1.35.3.tar.gz", hash = "sha256:d6177087f150b381d49499be782d764213fdf638d391b29ca692b84dd675a389"}, ] [package.dependencies] @@ -330,13 +494,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyright" -version = "1.1.367" +version = "1.1.368" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.367-py3-none-any.whl", hash = "sha256:89de6502ae02f1552d0c4df4b46867887a419849f379db617695ef9308cf01eb"}, - {file = "pyright-1.1.367.tar.gz", hash = "sha256:b1e5522ceb246ee6bc293a43d6d0162719d6467c1f1e9b81cee741aa11cdacbd"}, + {file = "pyright-1.1.368-py3-none-any.whl", hash = "sha256:4a86e34b61c755b43b367af7fbf927fc6466fff6b81a9dcea07d42416c640af3"}, + {file = "pyright-1.1.368.tar.gz", hash = "sha256:9b2aa48142d9d9fc9a6aedff743c76873cc4e615f3297cdbf893d5793f75b306"}, ] [package.dependencies] @@ -387,28 +551,28 @@ url = "../pytest-selfie" [[package]] name = "ruff" -version = "0.4.9" +version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b262ed08d036ebe162123170b35703aaf9daffecb698cd367a8d585157732991"}, - {file = "ruff-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98ec2775fd2d856dc405635e5ee4ff177920f2141b8e2d9eb5bd6efd50e80317"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4555056049d46d8a381f746680db1c46e67ac3b00d714606304077682832998e"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e91175fbe48f8a2174c9aad70438fe9cb0a5732c4159b2a10a3565fea2d94cde"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8e7b95673f22e0efd3571fb5b0cf71a5eaaa3cc8a776584f3b2cc878e46bff"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2d45ddc6d82e1190ea737341326ecbc9a61447ba331b0a8962869fcada758505"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78de3fdb95c4af084087628132336772b1c5044f6e710739d440fc0bccf4d321"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06b60f91bfa5514bb689b500a25ba48e897d18fea14dce14b48a0c40d1635893"}, - {file = "ruff-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88bffe9c6a454bf8529f9ab9091c99490578a593cc9f9822b7fc065ee0712a06"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:673bddb893f21ab47a8334c8e0ea7fd6598ecc8e698da75bcd12a7b9d0a3206e"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c1aff58c31948cc66d0b22951aa19edb5af0a3af40c936340cd32a8b1ab7438"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:784d3ec9bd6493c3b720a0b76f741e6c2d7d44f6b2be87f5eef1ae8cc1d54c84"}, - {file = "ruff-0.4.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:732dd550bfa5d85af8c3c6cbc47ba5b67c6aed8a89e2f011b908fc88f87649db"}, - {file = "ruff-0.4.9-py3-none-win32.whl", hash = "sha256:8064590fd1a50dcf4909c268b0e7c2498253273309ad3d97e4a752bb9df4f521"}, - {file = "ruff-0.4.9-py3-none-win_amd64.whl", hash = "sha256:e0a22c4157e53d006530c902107c7f550b9233e9706313ab57b892d7197d8e52"}, - {file = "ruff-0.4.9-py3-none-win_arm64.whl", hash = "sha256:5d5460f789ccf4efd43f265a58538a2c24dbce15dbf560676e430375f20a8198"}, - {file = "ruff-0.4.9.tar.gz", hash = "sha256:f1cb0828ac9533ba0135d148d214e284711ede33640465e706772645483427e3"}, + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] [[package]] @@ -477,7 +641,39 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "werkzeug" +version = "3.0.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "7140f7d8043cc3ad3175d38c361c7958659861f69a85c46d25db61243ff659c4" +content-hash = "b9053aea209511b1d8564ac0d29c13f5b813d54607806c10bb1d273d8bfd8465" diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index fd65b04d..e8ed1aab 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" [tool.poetry.dependencies] python = "^3.9" openai = "^1.0.0" +flask = "^3.0.3" [tool.poetry.group.dev.dependencies] ruff = "^0.4.0" From a0ebd28cb9bef9ef3515ee6ee82648f58f8d10e1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 01:46:47 -0700 Subject: [PATCH 02/32] Add requests for testing and sort the toml. --- python/example-pytest-selfie/poetry.lock | 139 +++++++++++++++++++- python/example-pytest-selfie/pyproject.toml | 7 +- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/python/example-pytest-selfie/poetry.lock b/python/example-pytest-selfie/poetry.lock index e1bc5c89..85b79910 100644 --- a/python/example-pytest-selfie/poetry.lock +++ b/python/example-pytest-selfie/poetry.lock @@ -55,6 +55,105 @@ files = [ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -549,6 +648,27 @@ selfie-lib = {path = "../selfie-lib", develop = true} type = "directory" url = "../pytest-selfie" +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.4.10" @@ -641,6 +761,23 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "werkzeug" version = "3.0.3" @@ -676,4 +813,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "b9053aea209511b1d8564ac0d29c13f5b813d54607806c10bb1d273d8bfd8465" +content-hash = "ecdecb9840a60081626874befa13f0b2e828530300d42d19d87ce2263008ca3c" diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index e8ed1aab..3608e0a7 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -6,14 +6,15 @@ authors = ["Selina Delgado ","Harvir Sahota Date: Fri, 21 Jun 2024 01:47:33 -0700 Subject: [PATCH 03/32] We aren't shipping the example package, no need for `__init__` and all that. --- python/example-pytest-selfie/example_pytest_selfie/__init__.py | 0 python/example-pytest-selfie/pyproject.toml | 1 + 2 files changed, 1 insertion(+) delete mode 100644 python/example-pytest-selfie/example_pytest_selfie/__init__.py diff --git a/python/example-pytest-selfie/example_pytest_selfie/__init__.py b/python/example-pytest-selfie/example_pytest_selfie/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index 3608e0a7..92f41c2c 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "An example project for using the pytest plugin for selfie snapshot testing." authors = ["Selina Delgado ","Harvir Sahota ","Ned Twigg ","Edwin Ye "] license = "Apache-2.0" +package-mode = false [tool.poetry.dependencies] flask = "^3.0.3" From 985f330c662a9a07d8856601506634089f1d606f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 01:57:44 -0700 Subject: [PATCH 04/32] Add markdownify. --- python/example-pytest-selfie/poetry.lock | 60 ++++++++++++++++++++- python/example-pytest-selfie/pyproject.toml | 1 + 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/python/example-pytest-selfie/poetry.lock b/python/example-pytest-selfie/poetry.lock index 85b79910..d7de12a8 100644 --- a/python/example-pytest-selfie/poetry.lock +++ b/python/example-pytest-selfie/poetry.lock @@ -33,6 +33,27 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "blinker" version = "1.8.2" @@ -352,6 +373,21 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdownify" +version = "0.12.1" +description = "Convert HTML to markdown." +optional = false +python-versions = "*" +files = [ + {file = "markdownify-0.12.1-py3-none-any.whl", hash = "sha256:a3805abd8166dbb7b27783c5599d91f54f10d79894b2621404d85b333c7ce561"}, + {file = "markdownify-0.12.1.tar.gz", hash = "sha256:1fb08c618b30e0ee7a31a39b998f44a18fb28ab254f55f4af06b6d35a2179e27"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.9,<5" +six = ">=1.15,<2" + [[package]] name = "markupsafe" version = "2.1.5" @@ -708,6 +744,17 @@ develop = true type = "directory" url = "../selfie-lib" +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -719,6 +766,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -813,4 +871,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "ecdecb9840a60081626874befa13f0b2e828530300d42d19d87ce2263008ca3c" +content-hash = "1543179a3f5e724b773b5ca97b1a3cf7d153dbb16052705033f565c83cbe659c" diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index 92f41c2c..75af55ef 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -18,6 +18,7 @@ requests = "^2.32.3" ruff = "^0.4.0" selfie-lib = { path = "../selfie-lib", develop = true } pytest-selfie = { path = "../pytest-selfie", develop = true } +markdownify = "^0.12.1" [build-system] requires = ["poetry-core"] From 0e5666fb7537a58d8236c312ea3a5d1853afa3df Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 11:08:13 -0700 Subject: [PATCH 05/32] Make the random numbers in our test app deterministic. --- python/example-pytest-selfie/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/example-pytest-selfie/app.py b/python/example-pytest-selfie/app.py index 95fdf598..f9a78dfb 100644 --- a/python/example-pytest-selfie/app.py +++ b/python/example-pytest-selfie/app.py @@ -1,11 +1,11 @@ # app.py import base64 import hashlib -import secrets import threading import time from datetime import datetime, timedelta from functools import wraps +from random import Random from flask import ( Flask, @@ -16,6 +16,7 @@ request, ) +random_0 = Random(0) app = Flask(__name__) # In-memory database (replace with a real database in production) @@ -47,7 +48,9 @@ def now(self): def repeatable_random(length): # This is a simplified version, not as secure as Java's SecureRandom return "".join( - secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + random_0.choice( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + ) for _ in range(length) ) From 8ad58afa4e513e809475fc0dd2c9b4c7fb59e190 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 11:15:46 -0700 Subject: [PATCH 06/32] Remove unnecessary whitespace, and add a test for basic camera functionality. --- python/example-pytest-selfie/app.py | 53 +++++++------- .../tests/app_account_test.py | 71 +++++++++++++++++++ 2 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 python/example-pytest-selfie/tests/app_account_test.py diff --git a/python/example-pytest-selfie/app.py b/python/example-pytest-selfie/app.py index f9a78dfb..2e67a97e 100644 --- a/python/example-pytest-selfie/app.py +++ b/python/example-pytest-selfie/app.py @@ -96,22 +96,22 @@ def index(): if user: return render_template_string( """ - -

Welcome back {{ username }}

- - """, + +

Welcome back {{ username }}

+""", username=user["email"], ) else: - return render_template_string(""" - -

Please login

-
- - -
- - """) + return render_template_string( + """ + +

Please login

+
+ + +
+""" + ) @app.route("/login", methods=["POST"]) @@ -127,25 +127,26 @@ def login(): f'Click here to login.', ) - return render_template_string(""" - -

Email sent!

-

Check your email for your login link.

- - """) + return render_template_string( + """ + +

Email sent!

+

Check your email for your login link.

+""" + ) @app.route("/login-confirm/") def login_confirm(code): email = database.pop(code, None) if email is None: - return render_template_string(""" - -

Login link expired.

-

Sorry, try again.

- - """) - + return render_template_string( + """ + +

Login link expired.

+

Sorry, try again.

+""" + ) response = make_response(redirect("/")) response.set_cookie("login", f"{email}|{sign_email(email)}") return response diff --git a/python/example-pytest-selfie/tests/app_account_test.py b/python/example-pytest-selfie/tests/app_account_test.py new file mode 100644 index 00000000..dd41a73f --- /dev/null +++ b/python/example-pytest-selfie/tests/app_account_test.py @@ -0,0 +1,71 @@ +import pytest +from selfie_lib import expect_selfie + +from app import app, wait_for_incoming_email + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +def test_homepage(client): + response = client.get("/") + expect_selfie(response.data.decode()).to_be(""" + +

Please login

+
+ + +
+""") + + +def test_T01_not_logged_in(client): + response = client.get("/") + expect_selfie(response.data.decode()).to_be(""" + +

Please login

+
+ + +
+""") + + +def test_T02_login(client): + response = client.post("/login", data={"email": "user@domain.com"}) + expect_selfie(response.data.decode()).to_be(""" + +

Email sent!

+

Check your email for your login link.

+""") + + email = wait_for_incoming_email() + expect_selfie(email).to_be( + { + "to": "user@domain.com", + "subject": "Login to example.com", + "html_content": 'Click here to login.', + } + ) + + +def test_T03_login_confirm(client): + response = client.get("/login-confirm/erjchFY=", follow_redirects=False) + expect_selfie(headers_to_string(response)).to_be("""200 OK +Content-Type=text/html; charset=utf-8""") + + +def headers_to_string(response): + headers = [f"{response.status}"] + for name, value in response.headers.items(): + if name.lower() not in ["server", "date", "content-length"]: + headers.append(f"{name}={value}") + return "\n".join(headers) + + +if __name__ == "__main__": + pytest.main() From 8253dd0a80a91fa71031c5535e8b6e967bd2f95f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 12:30:22 -0700 Subject: [PATCH 07/32] Add a method to conveniently create a camera with a lambda. --- python/selfie-lib/selfie_lib/Lens.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Lens.py b/python/selfie-lib/selfie_lib/Lens.py index 47cc9b7c..b3317a20 100644 --- a/python/selfie-lib/selfie_lib/Lens.py +++ b/python/selfie-lib/selfie_lib/Lens.py @@ -82,12 +82,18 @@ def snapshot(self, subject: T) -> Snapshot: pass def with_lens(self, lens: Lens) -> "Camera[T]": + parent = self + class WithLensCamera(Camera): - def __init__(self, camera: Camera[T], lens: Callable[[Snapshot], Snapshot]): - self.__camera = camera - self.__lens = lens + def snapshot(self, subject: T) -> Snapshot: + return lens(parent.snapshot(subject)) + + return WithLensCamera() + @staticmethod + def of(lambda_func: Callable[[T], Snapshot]) -> "Camera[T]": + class LambdaCamera(Camera): def snapshot(self, subject: T) -> Snapshot: - return self.__lens(self.__camera.snapshot(subject)) + return lambda_func(subject) - return WithLensCamera(self, lens) + return LambdaCamera() From 3648c4b046d3c4dde9dad6bfcded7f2e0efc583c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 13:01:31 -0700 Subject: [PATCH 08/32] Minor bit of wiring up facets. --- python/selfie-lib/selfie_lib/SelfieImplementations.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SelfieImplementations.py b/python/selfie-lib/selfie_lib/SelfieImplementations.py index c879a887..86202699 100644 --- a/python/selfie-lib/selfie_lib/SelfieImplementations.py +++ b/python/selfie-lib/selfie_lib/SelfieImplementations.py @@ -60,8 +60,9 @@ class BinaryFacet(FluentFacet, ABC): def to_be_base64(self, expected: str) -> bytes: pass + @abstractmethod def to_be_base64_TODO(self, _: Any = None) -> bytes: - return self.to_be_base64_TODO() + pass @abstractmethod def to_be_file(self, subpath: str) -> bytes: @@ -97,10 +98,10 @@ def to_match_disk_TODO(self, sub: str = "") -> "DiskSelfie": ) def facet(self, facet: str) -> "StringFacet": - raise NotImplementedError + return StringSelfie(self.actual, self.disk, [facet]) def facets(self, *facets: str) -> "StringFacet": - raise NotImplementedError + return StringSelfie(self.actual, self.disk, list(facets)) def facet_binary(self, facet: str) -> "BinaryFacet": raise NotImplementedError From 5d17726e7d30d5700734aa25fa1d94ea25e615b0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 13:34:14 -0700 Subject: [PATCH 09/32] Add the facet and selfie classes to the module exports. --- python/selfie-lib/selfie_lib/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index fb9c01eb..90768a11 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -14,6 +14,12 @@ from .PerCharacterEscaper import PerCharacterEscaper as PerCharacterEscaper from .Roundtrip import Roundtrip as Roundtrip from .Selfie import expect_selfie as expect_selfie +from .SelfieImplementations import BinaryFacet as BinaryFacet +from .SelfieImplementations import BinarySelfie as BinarySelfie +from .SelfieImplementations import FluentFacet as FluentFacet +from .SelfieImplementations import ReprSelfie as ReprSelfie +from .SelfieImplementations import StringFacet as StringFacet +from .SelfieImplementations import StringSelfie as StringSelfie from .Slice import Slice as Slice from .Snapshot import Snapshot as Snapshot from .SnapshotFile import SnapshotFile as SnapshotFile From e91464b631896588c2ab5000d8f081473166de2f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 13:57:36 -0700 Subject: [PATCH 10/32] Add a `BinarySelfie` and wire up its constructor. --- .../selfie_lib/SelfieImplementations.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SelfieImplementations.py b/python/selfie-lib/selfie_lib/SelfieImplementations.py index 86202699..59a60427 100644 --- a/python/selfie-lib/selfie_lib/SelfieImplementations.py +++ b/python/selfie-lib/selfie_lib/SelfieImplementations.py @@ -51,8 +51,9 @@ class StringFacet(FluentFacet, ABC): def to_be(self, expected: str) -> str: pass + @abstractmethod def to_be_TODO(self, _: Any = None) -> str: - return self.to_be_TODO() + pass class BinaryFacet(FluentFacet, ABC): @@ -104,7 +105,7 @@ def facets(self, *facets: str) -> "StringFacet": return StringSelfie(self.actual, self.disk, list(facets)) def facet_binary(self, facet: str) -> "BinaryFacet": - raise NotImplementedError + return BinarySelfie(self.actual, self.disk, facet) class StringSelfie(DiskSelfie, StringFacet, ReprSelfie[str]): @@ -171,6 +172,32 @@ def to_be(self, expected: str) -> str: ) +class BinarySelfie(DiskSelfie, BinaryFacet): + def __init__(self, actual: Snapshot, disk: DiskStorage, only_facet: str): + super().__init__(actual, disk) + self.only_facet = only_facet + + facet_value = actual.subject_or_facet_maybe(only_facet) + if facet_value is None: + raise ValueError(f"The facet {only_facet} was not found in the snapshot") + elif not facet_value.is_binary: + raise ValueError( + f"The facet {only_facet} is a string, not a binary snapshot" + ) + + def to_be_base64(self, expected: str) -> bytes: + raise NotImplementedError + + def to_be_base64_TODO(self, _: Any = None) -> bytes: + raise NotImplementedError + + def to_be_file(self, subpath: str) -> bytes: + raise NotImplementedError + + def to_be_file_TODO(self, subpath: str) -> bytes: + raise NotImplementedError + + def _checkSrc(value: T) -> T: _selfieSystem().mode.can_write(False, recordCall(True), _selfieSystem()) return value From bd9877b1801f84a0a1248a1ea4e040463cdd0bfb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 14:22:42 -0700 Subject: [PATCH 11/32] Simplify the selfie routing. --- python/selfie-lib/selfie_lib/Selfie.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Selfie.py b/python/selfie-lib/selfie_lib/Selfie.py index 0aadaca4..eb0bc6f6 100644 --- a/python/selfie-lib/selfie_lib/Selfie.py +++ b/python/selfie-lib/selfie_lib/Selfie.py @@ -4,16 +4,15 @@ from .Snapshot import Snapshot from .SnapshotSystem import _selfieSystem -# Declare T as covariant -T = TypeVar("T", covariant=True) +T = TypeVar("T") @overload def expect_selfie(actual: str) -> StringSelfie: ... -# @overload -# def expect_selfie(actual: bytes) -> BinarySelfie: ... # noqa: ERA001 +@overload +def expect_selfie(actual: Snapshot) -> StringSelfie: ... @overload @@ -21,11 +20,11 @@ def expect_selfie(actual: T) -> ReprSelfie[T]: ... def expect_selfie( - actual: Union[str, Any], + actual: Any, ) -> Union[StringSelfie, ReprSelfie]: if isinstance(actual, str): - snapshot = Snapshot.of(actual) - diskStorage = _selfieSystem().disk_thread_local() - return StringSelfie(snapshot, diskStorage) + return StringSelfie(Snapshot.of(actual), _selfieSystem().disk_thread_local()) + elif isinstance(actual, Snapshot): + return StringSelfie(actual, _selfieSystem().disk_thread_local()) else: return ReprSelfie(actual) From 8444a70e07cea4e4d5488bdd6671451d001c36ae Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 19:03:01 -0700 Subject: [PATCH 12/32] Finally solve the binary / string / repr typing issue, by making `BinarySelfie` into a `ReprSelfie[bytes]`. This takes great advantage of Python's byte literals. --- python/selfie-lib/selfie_lib/Selfie.py | 19 +++++++--- .../selfie_lib/SelfieImplementations.py | 37 ++++++++++--------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Selfie.py b/python/selfie-lib/selfie_lib/Selfie.py index eb0bc6f6..525b4847 100644 --- a/python/selfie-lib/selfie_lib/Selfie.py +++ b/python/selfie-lib/selfie_lib/Selfie.py @@ -1,6 +1,6 @@ -from typing import Any, TypeVar, Union, overload +from typing import Any, TypeVar, overload -from .SelfieImplementations import ReprSelfie, StringSelfie +from .SelfieImplementations import BinarySelfie, DiskSelfie, ReprSelfie, StringSelfie from .Snapshot import Snapshot from .SnapshotSystem import _selfieSystem @@ -15,16 +15,23 @@ def expect_selfie(actual: str) -> StringSelfie: ... def expect_selfie(actual: Snapshot) -> StringSelfie: ... +@overload +def expect_selfie(actual: bytes) -> BinarySelfie: ... + + @overload def expect_selfie(actual: T) -> ReprSelfie[T]: ... def expect_selfie( actual: Any, -) -> Union[StringSelfie, ReprSelfie]: +) -> DiskSelfie: + disk_storage = _selfieSystem().disk_thread_local() if isinstance(actual, str): - return StringSelfie(Snapshot.of(actual), _selfieSystem().disk_thread_local()) + return StringSelfie(Snapshot.of(actual), disk_storage) elif isinstance(actual, Snapshot): - return StringSelfie(actual, _selfieSystem().disk_thread_local()) + return StringSelfie(actual, disk_storage) + elif isinstance(actual, bytes): + return BinarySelfie(Snapshot.of(actual), disk_storage, "") else: - return ReprSelfie(actual) + return ReprSelfie(actual, Snapshot.of(repr(actual)), disk_storage) diff --git a/python/selfie-lib/selfie_lib/SelfieImplementations.py b/python/selfie-lib/selfie_lib/SelfieImplementations.py index 59a60427..de4f95aa 100644 --- a/python/selfie-lib/selfie_lib/SelfieImplementations.py +++ b/python/selfie-lib/selfie_lib/SelfieImplementations.py @@ -18,20 +18,6 @@ T = TypeVar("T") -class ReprSelfie(Generic[T]): - def __init__(self, actual: T): - self.actual = actual - - def to_be_TODO(self, _: Optional[T] = None) -> T: - return _toBeDidntMatch(None, self.actual, LiteralRepr()) - - def to_be(self, expected: T) -> T: - if self.actual == expected: - return _checkSrc(self.actual) - else: - return _toBeDidntMatch(expected, self.actual, LiteralRepr()) - - class FluentFacet(ABC): @abstractmethod def facet(self, facet: str) -> "StringFacet": @@ -108,14 +94,29 @@ def facet_binary(self, facet: str) -> "BinaryFacet": return BinarySelfie(self.actual, self.disk, facet) -class StringSelfie(DiskSelfie, StringFacet, ReprSelfie[str]): +class ReprSelfie(DiskSelfie, Generic[T]): + def __init__(self, actual_before_repr: T, actual: Snapshot, disk: DiskStorage): + super().__init__(actual, disk) + self.actual_before_repr = actual_before_repr + + def to_be_TODO(self, _: Optional[T] = None) -> T: + return _toBeDidntMatch(None, self.actual_before_repr, LiteralRepr()) + + def to_be(self, expected: T) -> T: + if self.actual_before_repr == expected: + return _checkSrc(self.actual_before_repr) + else: + return _toBeDidntMatch(expected, self.actual_before_repr, LiteralRepr()) + + +class StringSelfie(ReprSelfie[str], StringFacet): def __init__( self, actual: Snapshot, disk: DiskStorage, only_facets: Optional[List[str]] = None, ): - super().__init__(actual, disk) + super().__init__("", actual, disk) self.only_facets = only_facets if self.only_facets is not None: @@ -172,9 +173,9 @@ def to_be(self, expected: str) -> str: ) -class BinarySelfie(DiskSelfie, BinaryFacet): +class BinarySelfie(ReprSelfie[bytes], BinaryFacet): def __init__(self, actual: Snapshot, disk: DiskStorage, only_facet: str): - super().__init__(actual, disk) + super().__init__(actual.subject.value_binary(), actual, disk) self.only_facet = only_facet facet_value = actual.subject_or_facet_maybe(only_facet) From 9c0b4c69cdcf40f8f529c76be8aa43a0139b28bf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 19:27:58 -0700 Subject: [PATCH 13/32] Fix some typing issues in `app.py`. --- python/example-pytest-selfie/app.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python/example-pytest-selfie/app.py b/python/example-pytest-selfie/app.py index 2e67a97e..07f86e20 100644 --- a/python/example-pytest-selfie/app.py +++ b/python/example-pytest-selfie/app.py @@ -30,16 +30,16 @@ class DevTime: def __init__(self): - self.now = datetime(2000, 1, 1) + self.current_time = datetime(2000, 1, 1) def set_year(self, year): - self.now = datetime(year, 1, 1) + self.current_time = datetime(year, 1, 1) def advance_24hrs(self): - self.now += timedelta(days=1) + self.current_time += timedelta(days=1) def now(self): - return self.now + return self.current_time dev_time = DevTime() @@ -187,9 +187,11 @@ def wait_for_incoming_email(timeout=1): @app.route("/dev/time", methods=["POST"]) def set_dev_time(): - action = request.json.get("action") + json = request.json + assert json is not None + action = json.get("action") if action == "set_year": - year = request.json.get("year") + year = json.get("year") dev_time.set_year(year) elif action == "advance_24hrs": dev_time.advance_24hrs() From a43437be777a1b4a73194e9798bca0121c9f866d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 19:28:56 -0700 Subject: [PATCH 14/32] Run `example-pytest-selfie` in CI. --- .github/workflows/python-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 1b0fa46e..6a8f1f72 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -48,11 +48,11 @@ jobs: - name: example-pytest-selfie - poetry install run: poetry install working-directory: python/example-pytest-selfie - # - run: poetry run pytest -vv - # working-directory: python/example-pytest-selfie - # - name: example-pytest-selfie - pyright - # run: poetry run pyright - # working-directory: python/example-pytest-selfie + - run: poetry run pytest -vv + working-directory: python/example-pytest-selfie + - name: example-pytest-selfie - pyright + run: poetry run pyright + working-directory: python/example-pytest-selfie - name: example-pytest-selfie - ruff run: poetry run ruff format --check && poetry run ruff check working-directory: python/example-pytest-selfie From deb06de993cfcd0ed5007228738150ff46951d55 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 19:44:00 -0700 Subject: [PATCH 15/32] Set the first section of the selfie.dev stuff. --- .../tests/homepage_test.py | 47 +++++++++++++++++++ selfie.dev/src/pages/py/index.mdx | 4 +- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 python/example-pytest-selfie/tests/homepage_test.py diff --git a/python/example-pytest-selfie/tests/homepage_test.py b/python/example-pytest-selfie/tests/homepage_test.py new file mode 100644 index 00000000..0d41108a --- /dev/null +++ b/python/example-pytest-selfie/tests/homepage_test.py @@ -0,0 +1,47 @@ +from selfie_lib import expect_selfie + + +def primes_below(n): + if n <= 2: + return [] + sieve = [True] * n + sieve[0] = sieve[1] = False + + for i in range(2, int(n**0.5) + 1): + if sieve[i]: + for j in range(i * i, n, i): + sieve[j] = False + + return [i for i in range(n) if sieve[i]] + + +def test_primes_below_100(): + expect_selfie(primes_below(100)).to_be( + [ + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19, + 23, + 29, + 31, + 37, + 41, + 43, + 47, + 53, + 59, + 61, + 67, + 71, + 73, + 79, + 83, + 89, + 97, + ] + ) diff --git a/selfie.dev/src/pages/py/index.mdx b/selfie.dev/src/pages/py/index.mdx index 15db4c95..ff8bec48 100644 --- a/selfie.dev/src/pages/py/index.mdx +++ b/selfie.dev/src/pages/py/index.mdx @@ -8,8 +8,6 @@ export const description = -## NOT READY YET - WIP - This is a reasonable way to test. ```python @@ -38,7 +36,7 @@ When you run the test, selfie will automatically rewrite `_TODO()` into whatever ```python def test_primes_below100(): expect_selfie(primes_below(100)) - .to_be("[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]") + .to_be([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]) ``` And from now on it's a proper assertion, but you didn't have to spend any time writing it. It's not only less work, but also more complete than the usual `.startsWith().endsWith()` rigamarole. From 16e51e56b5ac0ebd989fdf583c5f9a38cd649208 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 21 Jun 2024 19:45:04 -0700 Subject: [PATCH 16/32] Fix npm deps. --- selfie.dev/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/selfie.dev/package-lock.json b/selfie.dev/package-lock.json index a56713b6..60b11340 100644 --- a/selfie.dev/package-lock.json +++ b/selfie.dev/package-lock.json @@ -1055,11 +1055,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1546,9 +1546,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, From 1133911af192cbfbacfb2a5bfe85f699271742ba Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Jun 2024 17:27:07 -0700 Subject: [PATCH 17/32] Add beautifulsoup so we can prettify. --- python/example-pytest-selfie/poetry.lock | 2 +- python/example-pytest-selfie/pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/python/example-pytest-selfie/poetry.lock b/python/example-pytest-selfie/poetry.lock index d7de12a8..75d6e851 100644 --- a/python/example-pytest-selfie/poetry.lock +++ b/python/example-pytest-selfie/poetry.lock @@ -871,4 +871,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "1543179a3f5e724b773b5ca97b1a3cf7d153dbb16052705033f565c83cbe659c" +content-hash = "ccfd70e070cd0fd1cdeb9a56541b2f1e7dcaa17010ec5d120225e5d25caf2545" diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index 75af55ef..a5d1eac1 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -19,6 +19,7 @@ ruff = "^0.4.0" selfie-lib = { path = "../selfie-lib", develop = true } pytest-selfie = { path = "../pytest-selfie", develop = true } markdownify = "^0.12.1" +beautifulsoup4 = "^4.12.3" [build-system] requires = ["poetry-core"] From 97d35d8be6e461f2c30c69234d8005f20f854f7d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 12:54:36 -0700 Subject: [PATCH 18/32] Add `werkzeug` dependency for its types. --- python/example-pytest-selfie/poetry.lock | 2 +- python/example-pytest-selfie/pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/python/example-pytest-selfie/poetry.lock b/python/example-pytest-selfie/poetry.lock index 75d6e851..7959912d 100644 --- a/python/example-pytest-selfie/poetry.lock +++ b/python/example-pytest-selfie/poetry.lock @@ -871,4 +871,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "ccfd70e070cd0fd1cdeb9a56541b2f1e7dcaa17010ec5d120225e5d25caf2545" +content-hash = "957599782b5e3aa8ce4fbbb34705034f6881f786d7a08e5b9b6197937c011e67" diff --git a/python/example-pytest-selfie/pyproject.toml b/python/example-pytest-selfie/pyproject.toml index a5d1eac1..798e4117 100644 --- a/python/example-pytest-selfie/pyproject.toml +++ b/python/example-pytest-selfie/pyproject.toml @@ -20,6 +20,7 @@ selfie-lib = { path = "../selfie-lib", develop = true } pytest-selfie = { path = "../pytest-selfie", develop = true } markdownify = "^0.12.1" beautifulsoup4 = "^4.12.3" +werkzeug = "^3.0.3" [build-system] requires = ["poetry-core"] From 2538ee0bb883ddaf24e91dc24674b7d75b1f7299 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 12:56:58 -0700 Subject: [PATCH 19/32] Update `get-started`. --- selfie.dev/src/pages/py/get-started.mdx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/selfie.dev/src/pages/py/get-started.mdx b/selfie.dev/src/pages/py/get-started.mdx index 9b6173f4..a4f06e30 100644 --- a/selfie.dev/src/pages/py/get-started.mdx +++ b/selfie.dev/src/pages/py/get-started.mdx @@ -7,28 +7,17 @@ export const imageUrl = "https://selfie.dev/get-started.webp"; -TODO: adapt this to Python - -To start snapshot testing in Python, all you need is to add a [single dependency](https://pypi.org/project/pytest-selfie/). +To start snapshot testing in Python, all you need is to add a single dependency. ## Installation ### Requirements -Selfie snapshot testing works with the following Python test runners: +Selfie requires Python 3.9 or newer. It has plugins for the following Python test runners: -- Pytest +- Pytest via [pytest-selfie](https://pypi.org/project/pytest-selfie/). - PRs welcome for other test runners (see [here](https://github.com/diffplug/selfie/issues/350) for a guide) -Both disk and inline snapshots can be used with Python test code. - -### Poetry - -Dependencies are managed using [poetry](https://python-poetry.org/docs/#installing-with-the-official-installer), -then just cd into `selfie-lib` and `run poetry install`. - -Replace `ver_SELFIE` with the [latest available version of selfie](https://github.com/diffplug/selfie/blob/main/python/CHANGELOG.md). - ## Quickstart _If you haven't seen the [GIF on our GitHub](https://github.com/diffplug/selfie), you might want to watch that first (give us a ⭐ while you're at it 😉)._ From 68f6d746c0596901e60a6b46d8ad63bf793a4240 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 12:57:18 -0700 Subject: [PATCH 20/32] Progress on facets. --- .../tests/app_account_test.py | 10 +- selfie.dev/src/pages/py/facets.mdx | 96 +++++++------------ 2 files changed, 41 insertions(+), 65 deletions(-) diff --git a/python/example-pytest-selfie/tests/app_account_test.py b/python/example-pytest-selfie/tests/app_account_test.py index dd41a73f..e96af9ae 100644 --- a/python/example-pytest-selfie/tests/app_account_test.py +++ b/python/example-pytest-selfie/tests/app_account_test.py @@ -1,5 +1,6 @@ import pytest -from selfie_lib import expect_selfie +from selfie_lib import StringSelfie, expect_selfie +from werkzeug.test import TestResponse from app import app, wait_for_incoming_email @@ -11,9 +12,12 @@ def client(): yield client +def web_selfie(response: TestResponse) -> StringSelfie: + return expect_selfie(response.data.decode()) + + def test_homepage(client): - response = client.get("/") - expect_selfie(response.data.decode()).to_be(""" + web_selfie(client.get("/")).to_be("""

Please login

diff --git a/selfie.dev/src/pages/py/facets.mdx b/selfie.dev/src/pages/py/facets.mdx index 0710cb1a..a082cbc1 100644 --- a/selfie.dev/src/pages/py/facets.mdx +++ b/selfie.dev/src/pages/py/facets.mdx @@ -7,19 +7,11 @@ export const imageUrl = "https://selfie.dev/advanced.webp"; -**_THIS IS BROKEN. [WE ARE WORKING ON THIS](https://github.com/diffplug/selfie/issues/303)._** - -**_THIS IS BROKEN. [WE ARE WORKING ON THIS](https://github.com/diffplug/selfie/issues/303)._** - -**_THIS IS BROKEN. [WE ARE WORKING ON THIS](https://github.com/diffplug/selfie/issues/303)._** - -**TODO: Facets is currently not implemented [yet](https://github.com/diffplug/selfie/issues/303)** - Assuming you have [installed selfie](/py/get-started#installation) and glanced through the [quickstart](/py/get-started#quickstart), then you're ready to start taking multifaceted snapshots of arbitrary typed data. ## Our toy project -We'll be using the [`example-pytest-selfie`](https://github.com/diffplug/selfie/tree/main/python/example-pytest-selfie) project from the selfie GitHub repo. You can clone the code and follow along, but there's no need to. If you did clone the project, you could run **Not Implemented** and you'd have a webapp running at `localhost:8080`. +We'll be using the [`example-pytest-selfie`](https://github.com/diffplug/selfie/tree/main/python/example-pytest-selfie) project from the selfie GitHub repo. You can clone the code and follow along, but there's no need to. If you did clone the project, you could run `poetry run python app.py` and you'd have a little flask webapp running at `127.0.0.1:5000`. It has a homepage where we can login. We can go to `/email` to see the emails the server has sent and click our login link, and boom we've got some auth cookies. @@ -27,75 +19,55 @@ There's nothing web-specific about selfie, it's just a familiar example. ## Typed snapshots -**TODO: Use request instead?, This implementation of toy project not added [yet](https://github.com/diffplug/selfie/tree/main/python/example-pytest-selfie).** - -Let's use [requests](https://pypi.org/project/requests/) to do gets and posts. So if we want to assert that the homepage is working, we can do this: +Since it's a flask app, we can use its built-in test client. So if we want to assert that the homepage is working, we can do this: ```python -@Test -public void homepage() { - expect_selfie(requests.get("/").text).to_be(""" +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + +def test_homepage(client): + expect_selfie(client.get("/").data.decode()).to_be(""" -\u0020

Please login

-\u0020 -\u0020 -\u0020 -\u0020 +

Please login

+
+ + +
""") -} ``` -Since you [saw the quickstart](/py/get-started#quickstart), you know that selfie wrote that big bad string literal for us. The `\u0020` is just escaped whitespace, to protect it from getting mangled by terrible autoformatters like [spotless](https://github.com/diffplug/spotless). - -**TODO: `Camera` and `expect_selfie(T, Camera<T>)` not yet implemented, [PRs welcomed](https://github.com/diffplug/selfie/issues/302)** - -The first thing to notice is that we'll be doing a lot of `requests.get().body().text`. It would be nice if we could just do `expect_selfie(get("/"))`, but we'll have to write our own `expect_selfie(requests.models.Response)` method. Selfie gives us [`expect_selfie(T, Camera<T>)`](https://github.com/diffplug/selfie/issues/302) and [`Camera`](https://github.com/diffplug/selfie/issues/302) to do exactly that. +Since you [saw the quickstart](/py/get-started#quickstart), you know that selfie wrote that big bad string literal for us. -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/302)** +The first thing to notice is that we'll be doing a lot of `.data.decode()`. It would be nice if we could just do `expect_selfie(get("/"))`, so let's add a `web_selfie` method to handle that. I'm going to use static types, but you can ignore those if you want. ```python -class Selfie : - @staticmethod - def expect_selfie(actual, camera): - return Selfie.DiskSelfie(actual, camera) - - class DiskSelfie: - def __init__(self, actual, camera): - self.actual = actual - self.camera = camera - - def __enter__(self): - self.snapshot = self.camera.snapshot(self.actual) - return self -``` - -We can write our `expect_selfie(Response)` anywhere, but we recommend putting it into a class named `SelfieSettings` in the package `selfie`, but you can use any name and put these methods anywhere. We recommend `expect_selfie` because it's a good hint that the string constants are self-updating. - -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/302)** - -```python -import requests -from selfie import Camera, Selfie, Snapshot - -def request_camera(request): - return Snapshot(request.text) +from selfie_lib import expect_selfie, StringSelfie +from werkzeug.test import TestResponse # this is what `app.test_client().get` returns -class SelfieSettings: - REQUEST_CAMERA = Camera(request_camera) +... - @staticmethod - def expect_selfie(request): - return Selfie.expect_selfie(request, SelfieSettings.REQUEST_CAMERA) +def web_selfie(response: TestResponse) -> StringSelfie: + return expect_selfie(response.data.decode()) +def test_homepage(client): + web_selfie(client.get("/")).to_be(""" + +

Please login

+
+ + +
+""") ``` -## Facets - -**TODO: Facets is currently not implemented [yet](https://github.com/diffplug/selfie/issues/303)** +You can write `web_selfie` anywhere, but we recommend putting it into `selfie_settings.py`. We allso recommend keeping the `xxx_selfie` pattern because it's a good hint that the string constants are self-updating. -Every snapshot has a "subject": `Snapshot.of(String subject)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status line. +## Facets -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/303)** +Every snapshot has a "subject": `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status line. ```python def response_camera(response): From 734c9d112f40b84128c5d80d034ad10d55637992 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 15:04:39 -0700 Subject: [PATCH 21/32] Move `web_selfie` into `selfie_settings.py`. --- .../tests/app_account_test.py | 12 ++-- .../tests/selfie_settings.py | 22 ++++++ selfie.dev/src/pages/py/facets.mdx | 71 ++++++++----------- 3 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 python/example-pytest-selfie/tests/selfie_settings.py diff --git a/python/example-pytest-selfie/tests/app_account_test.py b/python/example-pytest-selfie/tests/app_account_test.py index e96af9ae..587a8893 100644 --- a/python/example-pytest-selfie/tests/app_account_test.py +++ b/python/example-pytest-selfie/tests/app_account_test.py @@ -1,8 +1,8 @@ import pytest -from selfie_lib import StringSelfie, expect_selfie -from werkzeug.test import TestResponse +from selfie_lib import expect_selfie from app import app, wait_for_incoming_email +from tests.selfie_settings import web_selfie @pytest.fixture @@ -12,10 +12,6 @@ def client(): yield client -def web_selfie(response: TestResponse) -> StringSelfie: - return expect_selfie(response.data.decode()) - - def test_homepage(client): web_selfie(client.get("/")).to_be(""" @@ -24,7 +20,9 @@ def test_homepage(client): -""") + +╔═ [status] ═╗ +200 OK""") def test_T01_not_logged_in(client): diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py new file mode 100644 index 00000000..3313ea3c --- /dev/null +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -0,0 +1,22 @@ +from selfie_lib import Snapshot, StringSelfie, expect_selfie +from werkzeug.test import TestResponse + +REDIRECTS = { + 303: "See Other", + 302: "Found", + 307: "Temporary Redirect", + 301: "Moved Permanently", +} + + +def web_selfie(response: TestResponse) -> StringSelfie: + redirect_reason = REDIRECTS.get(response.status_code) + if redirect_reason is not None: + actual = Snapshot.of( + f"REDIRECT {response.status_code} {redirect_reason} to {response.headers.get("Location")}" + ) + else: + actual = Snapshot.of(response.data.decode()).plus_facet( + "status", response.status + ) + return expect_selfie(actual) diff --git a/selfie.dev/src/pages/py/facets.mdx b/selfie.dev/src/pages/py/facets.mdx index a082cbc1..c583ab49 100644 --- a/selfie.dev/src/pages/py/facets.mdx +++ b/selfie.dev/src/pages/py/facets.mdx @@ -50,10 +50,10 @@ from werkzeug.test import TestResponse # this is what `app.test_client().get` re ... def web_selfie(response: TestResponse) -> StringSelfie: - return expect_selfie(response.data.decode()) + return expect_selfie(response.data.decode()) def test_homepage(client): - web_selfie(client.get("/")).to_be(""" + web_selfie(client.get("/")).to_be("""

Please login

@@ -67,67 +67,56 @@ You can write `web_selfie` anywhere, but we recommend putting it into `selfie_se ## Facets -Every snapshot has a "subject": `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status line. +Every snapshot has a "subject": `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status code. ```python -def response_camera(response): - return ( - Snapshot(response.text) - .plus_facet("statusLine", response.status_line) - ) +def web_selfie(response: TestResponse) -> StringSelfie: + actual = Snapshot.of(response.data.decode()) \ + .plus_facet("status", response.status) + return expect_selfie(actual) ``` -And now our snapshot has `statusLine` at the bottom, which we can use in both literal and disk snapshots. - -**[TODO: Not implemented](https://github.com/diffplug/selfie/issues/303)** +And now our snapshot has `status` at the bottom, which we can use in both literal and disk snapshots. ```python def test_homepage(): expect_selfie(get("/")).toBe(""" -\u0020

Please login

-\u0020 -\u0020 -\u0020 -\u0020
+

Please login

+
+ + +
-╔═ [statusLine] ═╗ -HTTP/1.1 200 OK"""); +╔═ [status] ═╗ +200 OK""") ``` Now that we have the status code, it begs the question: what should the subject be for a 301 redirect? Surely the redirected URL, not just an empty string? -**[TODO: Not implemented](https://github.com/diffplug/selfie/issues/303)** - ```python -def request_camera(request): - redirect_reason = REDIRECTS.get(request.status_code) - if redirect_reason is not None: - return Snapshot.of("REDIRECT " + str(request.status_code) + " " + redirect_reason + " to " + request.headers.get("Location")) - else: - return Snapshot.of(request.body).plus_facet("statusLine", request.status_line) - REDIRECTS = { - status.value: status.name - for status in StatusCode - if status in { - StatusCode.SEE_OTHER, - StatusCode.FOUND, - StatusCode.TEMPORARY_REDIRECT, - StatusCode.MOVED_PERMANENTLY - } + 303: "See Other", + 302: "Found", + 307: "Temporary Redirect", + 301: "Moved Permanently", } + +def web_selfie(response: TestResponse) -> StringSelfie: + redirect_reason = REDIRECTS.get(response.status_code) + if redirect_reason is not None: + actual = Snapshot.of(f"REDIRECT {response.status_code} {redirect_reason} to {response.headers.get("Location")}") + else: + actual = Snapshot.of(response.data.decode()) \ + .plus_facet("status", response.status) + return expect_selfie(actual) ``` So a snapshot doesn't have to be only one value, and it's fine if the schema changes depending on the content of the value being snapshotted. The snapshots are for you to read (and look at diffs of), so record whatever is meaningful to you. ## Lenses -**TODO: Lenses is not [yet](https://github.com/diffplug/selfie/issues/323) implemented, PRs welcomed!** - -A [Lens](https://github.com/diffplug/selfie/issues/323) is a function that transforms one `Snapshot` into another `Snapshot`, transforming / creating / removing values along the way. For example, we might want to pretty-print the HTML in our snapshots. - -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/323)** +A `Lens` is a function that transforms one `Snapshot` into another `Snapshot`, transforming / creating / removing values along the way. For example, we might want to pretty-print the HTML in our snapshots. ```python from bs4 import BeautifulSoup @@ -162,8 +151,6 @@ def expect_selfie(data: Union[Response, Email], camera): ## Compound lens -**TODO: Compound lens not implemented[yet](https://github.com/diffplug/selfie/issues/324), PRs welcomed!** - Selfie has a useful class called [`CompoundLens`](https://github.com/diffplug/selfie/issues/324). It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets. We can easily mutate a specific facet, such as to pretty-print HTML in the subject... From d470a3a9c1a5f421f13adaa9a0fa3073024dde99 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 15:41:58 -0700 Subject: [PATCH 22/32] Workaround f-string issues in old versions of Python. --- python/example-pytest-selfie/tests/selfie_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index 3313ea3c..cdb59242 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -13,7 +13,8 @@ def web_selfie(response: TestResponse) -> StringSelfie: redirect_reason = REDIRECTS.get(response.status_code) if redirect_reason is not None: actual = Snapshot.of( - f"REDIRECT {response.status_code} {redirect_reason} to {response.headers.get("Location")}" + f"REDIRECT {response.status_code} {redirect_reason} to " + + response.headers.get("Location") ) else: actual = Snapshot.of(response.data.decode()).plus_facet( From 918ac615adb792d9e45c15f3943f4f32d3d5120f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 16:12:58 -0700 Subject: [PATCH 23/32] Another shot at f-string fixes. --- python/example-pytest-selfie/tests/selfie_settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index cdb59242..88d7da31 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -13,8 +13,7 @@ def web_selfie(response: TestResponse) -> StringSelfie: redirect_reason = REDIRECTS.get(response.status_code) if redirect_reason is not None: actual = Snapshot.of( - f"REDIRECT {response.status_code} {redirect_reason} to " - + response.headers.get("Location") + f"REDIRECT {response.status_code} {redirect_reason} to {response.headers.get("Location", "")}" ) else: actual = Snapshot.of(response.data.decode()).plus_facet( From 5910ea6a5125e4540b76dfb02396e7d73480c722 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 16:40:20 -0700 Subject: [PATCH 24/32] Try again with an actual `Camera` to see if that helps. --- .../tests/selfie_settings.py | 21 ++++++++++++------- python/selfie-lib/selfie_lib/Selfie.py | 13 ++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index 88d7da31..edea5bca 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -1,4 +1,4 @@ -from selfie_lib import Snapshot, StringSelfie, expect_selfie +from selfie_lib import Camera, Snapshot, StringSelfie, expect_selfie from werkzeug.test import TestResponse REDIRECTS = { @@ -9,14 +9,19 @@ } -def web_selfie(response: TestResponse) -> StringSelfie: +def web_camera(response: TestResponse) -> Snapshot: redirect_reason = REDIRECTS.get(response.status_code) if redirect_reason is not None: - actual = Snapshot.of( - f"REDIRECT {response.status_code} {redirect_reason} to {response.headers.get("Location", "")}" + return Snapshot.of( + f"REDIRECT {response.status_code} {redirect_reason} to " + + response.headers.get("Location", "") ) else: - actual = Snapshot.of(response.data.decode()).plus_facet( - "status", response.status - ) - return expect_selfie(actual) + return Snapshot.of(response.data.decode()).plus_facet("status", response.status) + + +WEB_CAMERA = Camera.of(web_camera) + + +def web_selfie(response: TestResponse) -> StringSelfie: + return expect_selfie(response, WEB_CAMERA) diff --git a/python/selfie-lib/selfie_lib/Selfie.py b/python/selfie-lib/selfie_lib/Selfie.py index 525b4847..eaa427f2 100644 --- a/python/selfie-lib/selfie_lib/Selfie.py +++ b/python/selfie-lib/selfie_lib/Selfie.py @@ -1,5 +1,6 @@ from typing import Any, TypeVar, overload +from .Lens import Camera from .SelfieImplementations import BinarySelfie, DiskSelfie, ReprSelfie, StringSelfie from .Snapshot import Snapshot from .SnapshotSystem import _selfieSystem @@ -23,11 +24,15 @@ def expect_selfie(actual: bytes) -> BinarySelfie: ... def expect_selfie(actual: T) -> ReprSelfie[T]: ... -def expect_selfie( - actual: Any, -) -> DiskSelfie: +@overload +def expect_selfie(actual: T, camera: Camera[T]) -> StringSelfie: ... + + +def expect_selfie(actual: Any, camera: Any = None) -> DiskSelfie: disk_storage = _selfieSystem().disk_thread_local() - if isinstance(actual, str): + if camera is not None: + return StringSelfie(camera.snapshot(actual), disk_storage) + elif isinstance(actual, str): return StringSelfie(Snapshot.of(actual), disk_storage) elif isinstance(actual, Snapshot): return StringSelfie(actual, disk_storage) From 881004322d471c4df3964ca2f7f5a2a7568bafeb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 18:16:22 -0700 Subject: [PATCH 25/32] Fix UTF-8 issues on windows. --- python/selfie-lib/selfie_lib/CommentTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/selfie-lib/selfie_lib/CommentTracker.py b/python/selfie-lib/selfie_lib/CommentTracker.py index 5119b5df..df3219f0 100644 --- a/python/selfie-lib/selfie_lib/CommentTracker.py +++ b/python/selfie-lib/selfie_lib/CommentTracker.py @@ -54,7 +54,7 @@ def commentString(typedPath: TypedPath) -> Tuple[str, int]: @staticmethod def __commentAndLine(typedPath: TypedPath) -> Tuple[WritableComment, int]: - with open(typedPath.absolute_path) as file: + with open(typedPath.absolute_path, encoding="utf-8") as file: content = Slice(file.read()) for comment_str in [ "# selfieonce", From cf5c255ac6c25470424a654497e9504b8b88b0a9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 18:30:09 -0700 Subject: [PATCH 26/32] Selfie should accept any function as a camera. --- python/selfie-lib/selfie_lib/Selfie.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Selfie.py b/python/selfie-lib/selfie_lib/Selfie.py index eaa427f2..25c4d8b8 100644 --- a/python/selfie-lib/selfie_lib/Selfie.py +++ b/python/selfie-lib/selfie_lib/Selfie.py @@ -1,4 +1,4 @@ -from typing import Any, TypeVar, overload +from typing import Any, Callable, TypeVar, overload from .Lens import Camera from .SelfieImplementations import BinarySelfie, DiskSelfie, ReprSelfie, StringSelfie @@ -28,10 +28,18 @@ def expect_selfie(actual: T) -> ReprSelfie[T]: ... def expect_selfie(actual: T, camera: Camera[T]) -> StringSelfie: ... +@overload +def expect_selfie(actual: T, camera: Callable[[T], Snapshot]) -> StringSelfie: ... + + def expect_selfie(actual: Any, camera: Any = None) -> DiskSelfie: disk_storage = _selfieSystem().disk_thread_local() if camera is not None: - return StringSelfie(camera.snapshot(actual), disk_storage) + if isinstance(camera, Camera): + actual_snapshot = camera.snapshot(actual) + else: + actual_snapshot = camera(actual) + return StringSelfie(actual_snapshot, disk_storage) elif isinstance(actual, str): return StringSelfie(Snapshot.of(actual), disk_storage) elif isinstance(actual, Snapshot): From 26d99d4ac98a769ea16901830954464337571ecf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 18:30:39 -0700 Subject: [PATCH 27/32] Example using a function rather than a `Camera`. Progress on `facets.mdx`. --- .../tests/selfie_settings.py | 6 +- selfie.dev/src/pages/py/facets.mdx | 57 ++++++++++++------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index edea5bca..fa721aab 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -9,7 +9,7 @@ } -def web_camera(response: TestResponse) -> Snapshot: +def _web_camera(response: TestResponse) -> Snapshot: redirect_reason = REDIRECTS.get(response.status_code) if redirect_reason is not None: return Snapshot.of( @@ -20,8 +20,8 @@ def web_camera(response: TestResponse) -> Snapshot: return Snapshot.of(response.data.decode()).plus_facet("status", response.status) -WEB_CAMERA = Camera.of(web_camera) +WEB_CAMERA = Camera.of(_web_camera) def web_selfie(response: TestResponse) -> StringSelfie: - return expect_selfie(response, WEB_CAMERA) + return expect_selfie(response, _web_camera) diff --git a/selfie.dev/src/pages/py/facets.mdx b/selfie.dev/src/pages/py/facets.mdx index c583ab49..c256f6a3 100644 --- a/selfie.dev/src/pages/py/facets.mdx +++ b/selfie.dev/src/pages/py/facets.mdx @@ -114,6 +114,25 @@ def web_selfie(response: TestResponse) -> StringSelfie: So a snapshot doesn't have to be only one value, and it's fine if the schema changes depending on the content of the value being snapshotted. The snapshots are for you to read (and look at diffs of), so record whatever is meaningful to you. +## Cameras + +So if you want to capture multiple facets of something, you need a function which turns that something into a `Snapshot`. Selfie calls this idea a `Camera`. You can pass a `Camera` as the second argument to `expect_selfie`, which would look like so: + +```python +def _web_camera(response: TestResponse) -> Snapshot: + redirect_reason = REDIRECTS.get(response.status_code) + if redirect_reason is not None: + return Snapshot.of( + f"REDIRECT {response.status_code} {redirect_reason} to " + + response.headers.get("Location", "") + ) + else: + return Snapshot.of(response.data.decode()).plus_facet("status", response.status) + +def web_selfie(response: TestResponse) -> StringSelfie: + return expect_selfie(response, _web_camera) +``` + ## Lenses A `Lens` is a function that transforms one `Snapshot` into another `Snapshot`, transforming / creating / removing values along the way. For example, we might want to pretty-print the HTML in our snapshots. @@ -121,34 +140,30 @@ A `Lens` is a function that transforms one `Snapshot` into another `Snapshot`, t ```python from bs4 import BeautifulSoup -def pretty_print_html(html): - soup = BeautifulSoup(html, 'html.parser') - return soup.prettify() - -def response_camera(response): - (...) - // call prettyPrint when we take the snapshot - return Snapshot.of(pretty_print_html(response.body)) - .plus_facet("statusLine", response.status_line) - +def _pretty_print_html(html : str) -> str: + return BeautifulSoup(html, 'html.parser').prettify() ``` -Calling transformation functions inside the `Camera` is fine, but another option is to create a `Lens` and then use `Camera.withLens`. This approach is especially helpful if there are multiple `Camera`s which need the same transformation. - -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/323)** +One option is to call this function inside the `Camera`. But this mixes concerns - its better to have one function that grabs all the data (the `Camera`), and other functions that clean it up (the `Lens`es). Selfie makes it easy to combine these like so: ```python -def pretty_print(snapshot): - subject = snapshot.subject.value_string() - if "" in subject: - return snapshot.plus_or_replace("", pretty_print_html(subject)) - else: - return snapshot +def _web_camera(response: TestResponse) -> Snapshot: ... +def _pretty_print_html(html : str) -> str: ... +def _pretty_print_lens(snapshot : Snapshot) -> Snapshot: + if (snapshot.subject.contains(" StringSelfie: + return expect_selfie(response, _WEB_CAMERA) ``` +Calling transformation functions inside the `Camera` is fine, but another option is to create a `Lens` and then use `Camera.withLens`. This approach is especially helpful if there are multiple `Camera`s which need the same transformation. + + ## Compound lens Selfie has a useful class called [`CompoundLens`](https://github.com/diffplug/selfie/issues/324). It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets. From 0383de217526f0d5959cae04b196033ea2fdf5b5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 18:53:51 -0700 Subject: [PATCH 28/32] Pretty-print the example. --- .../tests/app_account_test.py | 19 ++++++++++++------- .../tests/selfie_settings.py | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/python/example-pytest-selfie/tests/app_account_test.py b/python/example-pytest-selfie/tests/app_account_test.py index 587a8893..6dc2231a 100644 --- a/python/example-pytest-selfie/tests/app_account_test.py +++ b/python/example-pytest-selfie/tests/app_account_test.py @@ -2,7 +2,8 @@ from selfie_lib import expect_selfie from app import app, wait_for_incoming_email -from tests.selfie_settings import web_selfie + +from .selfie_settings import web_selfie @pytest.fixture @@ -13,14 +14,18 @@ def client(): def test_homepage(client): - web_selfie(client.get("/")).to_be(""" - -

Please login

+ web_selfie(client.get("/")).to_be(""" + +

+ Please login +

- - + +
- + + + ╔═ [status] ═╗ 200 OK""") diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index fa721aab..26d01eea 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -1,3 +1,4 @@ +from bs4 import BeautifulSoup from selfie_lib import Camera, Snapshot, StringSelfie, expect_selfie from werkzeug.test import TestResponse @@ -20,8 +21,21 @@ def _web_camera(response: TestResponse) -> Snapshot: return Snapshot.of(response.data.decode()).plus_facet("status", response.status) -WEB_CAMERA = Camera.of(_web_camera) +def _pretty_print_html(html: str) -> str: + return BeautifulSoup(html, "html.parser").prettify() + + +def _pretty_print_lens(snapshot: Snapshot) -> Snapshot: + if " StringSelfie: - return expect_selfie(response, _web_camera) + return expect_selfie(response, WEB_CAMERA) From a09e90b20cfa14261d3ec709a93181f65ec1700a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 21:13:44 -0700 Subject: [PATCH 29/32] Use `CompoundLens` to get html to md working. --- .../tests/app_account_test.py | 2 + .../tests/selfie_settings.py | 45 ++++++++++++++----- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/python/example-pytest-selfie/tests/app_account_test.py b/python/example-pytest-selfie/tests/app_account_test.py index 6dc2231a..bf74a4d1 100644 --- a/python/example-pytest-selfie/tests/app_account_test.py +++ b/python/example-pytest-selfie/tests/app_account_test.py @@ -26,6 +26,8 @@ def test_homepage(client): +╔═ [md] ═╗ +Please login ╔═ [status] ═╗ 200 OK""") diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index 26d01eea..b8e0968d 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -1,5 +1,8 @@ +import re + from bs4 import BeautifulSoup -from selfie_lib import Camera, Snapshot, StringSelfie, expect_selfie +from markdownify import markdownify as md +from selfie_lib import Camera, CompoundLens, Snapshot, StringSelfie, expect_selfie from werkzeug.test import TestResponse REDIRECTS = { @@ -21,20 +24,42 @@ def _web_camera(response: TestResponse) -> Snapshot: return Snapshot.of(response.data.decode()).plus_facet("status", response.status) -def _pretty_print_html(html: str) -> str: - return BeautifulSoup(html, "html.parser").prettify() +def _pretty_print_html(html: str): + return BeautifulSoup(html, "html.parser").prettify() if " Snapshot: - if " tags + clean_html = re.sub(r"", "", html) + + # Convert HTML to Markdown + md_text = md(clean_html) + + # Remove specific patterns from lines + md_text = re.sub(r"(?m)^====+", "", md_text) + md_text = re.sub(r"(?m)^---+", "", md_text) + md_text = re.sub(r"(?m)^\*\*\*[^\* ]+", "", md_text) + + # Replace multiple newlines with double newlines + md_text = re.sub(r"\n\n+", "\n\n", md_text) + + # Trim each line + trim_lines = "\n".join(line.strip() for line in md_text.split("\n")) + + return trim_lines.strip() + +HTML_LENS = ( + CompoundLens() + .mutate_facet("", _pretty_print_html) + .replace_all_regex("http://localhost:\\d+/", "https://demo.selfie.dev/") + .set_facet_from("md", "", _html_to_md) +) -WEB_CAMERA = Camera.of(_web_camera).with_lens(_pretty_print_lens) +WEB_CAMERA = Camera.of(_web_camera).with_lens(HTML_LENS) def web_selfie(response: TestResponse) -> StringSelfie: From a29633f7edd363f768197753c88826933275c228 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 27 Jun 2024 15:08:51 -0700 Subject: [PATCH 30/32] Remove the `.python-version` file (our base is 3.9, not 3.8, and I'm not confident this file was helping us anyway). --- python/.python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 python/.python-version diff --git a/python/.python-version b/python/.python-version deleted file mode 100644 index cc1923a4..00000000 --- a/python/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.8 From 41ed4e0ef552aa4dd87be5927dd4b0340dc4bd6c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 28 Jun 2024 15:40:41 -0700 Subject: [PATCH 31/32] Turn some unnecessary `println` to `print` --- selfie.dev/src/pages/py/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfie.dev/src/pages/py/index.mdx b/selfie.dev/src/pages/py/index.mdx index ff8bec48..2519a222 100644 --- a/selfie.dev/src/pages/py/index.mdx +++ b/selfie.dev/src/pages/py/index.mdx @@ -24,7 +24,7 @@ def test_mc_test_face(): print(primes_below(100)) ``` -With literal snapshots, you can `println` directly into your testcode, combining the speed and freedom of `println` with the repeatability and collaborative spirit of conventional assertions. +With literal snapshots, you can `print` directly into your testcode, combining the speed and freedom of `print` with the repeatability and collaborative spirit of conventional assertions. ```python def test_primes_below_100(): From 99c683123e52428ae3cc4245bbbc47e8089e2f78 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 28 Jun 2024 15:41:25 -0700 Subject: [PATCH 32/32] The example and docs for `/py/facets` are almost done. --- .../tests/selfie_settings.py | 6 +- selfie.dev/src/pages/py/facets.mdx | 62 +++++++------------ 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/python/example-pytest-selfie/tests/selfie_settings.py b/python/example-pytest-selfie/tests/selfie_settings.py index b8e0968d..d6f7d02c 100644 --- a/python/example-pytest-selfie/tests/selfie_settings.py +++ b/python/example-pytest-selfie/tests/selfie_settings.py @@ -52,15 +52,15 @@ def _html_to_md(html: str): return trim_lines.strip() -HTML_LENS = ( +_HTML_LENS = ( CompoundLens() .mutate_facet("", _pretty_print_html) .replace_all_regex("http://localhost:\\d+/", "https://demo.selfie.dev/") .set_facet_from("md", "", _html_to_md) ) -WEB_CAMERA = Camera.of(_web_camera).with_lens(HTML_LENS) +_WEB_CAMERA = Camera.of(_web_camera).with_lens(_HTML_LENS) def web_selfie(response: TestResponse) -> StringSelfie: - return expect_selfie(response, WEB_CAMERA) + return expect_selfie(response, _WEB_CAMERA) diff --git a/selfie.dev/src/pages/py/facets.mdx b/selfie.dev/src/pages/py/facets.mdx index c256f6a3..78f99739 100644 --- a/selfie.dev/src/pages/py/facets.mdx +++ b/selfie.dev/src/pages/py/facets.mdx @@ -67,7 +67,7 @@ You can write `web_selfie` anywhere, but we recommend putting it into `selfie_se ## Facets -Every snapshot has a "subject": `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of "facets", which are other named values. For example, maybe we want to add the response's status code. +Every snapshot has a **subject**: `Snapshot.of(subject: str)`. But each snapshot can also have an unlimited number of **facets**, which are other named values. For example, maybe we want to add the response's status code. ```python def web_selfie(response: TestResponse) -> StringSelfie: @@ -116,7 +116,7 @@ So a snapshot doesn't have to be only one value, and it's fine if the schema cha ## Cameras -So if you want to capture multiple facets of something, you need a function which turns that something into a `Snapshot`. Selfie calls this idea a `Camera`. You can pass a `Camera` as the second argument to `expect_selfie`, which would look like so: +If you want to capture multiple facets of something, you need a function which turns that something into a `Snapshot`. Selfie calls this a `Camera`. You can pass a `Camera` as the second argument to `expect_selfie`, which would look like so: ```python def _web_camera(response: TestResponse) -> Snapshot: @@ -149,10 +149,13 @@ One option is to call this function inside the `Camera`. But this mixes concerns ```python def _web_camera(response: TestResponse) -> Snapshot: ... def _pretty_print_html(html : str) -> str: ... -def _pretty_print_lens(snapshot : Snapshot) -> Snapshot: - if (snapshot.subject.contains(" Snapshot: + if " StringSelfie: return expect_selfie(response, _WEB_CAMERA) ``` -Calling transformation functions inside the `Camera` is fine, but another option is to create a `Lens` and then use `Camera.withLens`. This approach is especially helpful if there are multiple `Camera`s which need the same transformation. - +By keeping the lens separate from the camera, you can also reuse the lens in other cameras. For example, you might want to pretty-print the HTML in an email. ## Compound lens -Selfie has a useful class called [`CompoundLens`](https://github.com/diffplug/selfie/issues/324). It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets. +The example above has some nasty plumbing for dealing with the `Snapshot` API. To make this easier, you can use `CompoundLens`. It is a fluent API for mutating facets and piping data through functions from one facet into another. An important gotcha here is that the **subject** can be treated as a facet named `""` (empty string). `CompoundLens` uses this hack to simplify a snapshot into only a map of facets, instead of a subject plus a map of facets. We can easily mutate a specific facet, such as to pretty-print HTML in the subject... -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324)** - ```python -HTML = CompoundLens().mutate_facet("", lambda maybe_html: pretty_print_html(maybe_html) if "" in maybe_html else None) +_HTML_LENS = CompoundLens().mutate_facet("", _pretty_print_html) ``` -Or we can mutate all facets, such as to remove a random local port number... - -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324)** +Or we can mutate every facet, such as to remove a random local port number... ```python -HTML = ( - CompoundLens() - .mutate_facet("", lambda maybe_html: pretty_print_html(maybe_html) if "" in maybe_html else None) +_HTML_LENS = CompoundLens() \ + .mutate_facet("", _pretty_print_html) \ .replace_all_regex("http://localhost:\\d+/", "https://www.example.com/") -) ``` Or we can render HTML into markdown, and store the easy-to-read markdown in its own facet... -**TODO: [Not implemented](https://github.com/diffplug/selfie/issues/324) yet, use markdown2?** - ```python -import markdown2 +from markdownify import markdownify as md -def html_to_md(html): - return markdown2.markdown(html) +def _html_to_md(html: str) -> str: + return md(html) if "