Testing!

This commit is contained in:
Joe Kaufeld 2024-08-26 01:56:08 -04:00
parent bd7ace7a66
commit 6b44e34013
16 changed files with 625 additions and 59 deletions

12
conftest.py Normal file
View File

@ -0,0 +1,12 @@
import os
from pytest import fixture
@fixture(autouse=True, scope="session")
def db():
if os.path.exists("spiderweb-tests.db"):
os.remove("spiderweb-tests.db")
yield
if os.path.exists("spiderweb-tests.db"):
os.remove("spiderweb-tests.db")

View File

@ -55,15 +55,15 @@ app = SpiderwebRouter(
], ],
staticfiles_dirs=["static_files"], staticfiles_dirs=["static_files"],
routes=[ routes=[
["/", index], ("/", index),
["/redirect", redirect], ("/redirect", redirect),
["/json", json], ("/json", json),
["/error", error], ("/error", error),
["/middleware", middleware], ("/middleware", middleware),
["/example/<int:id>", example], ("/example/<int:id>", example),
["/form", form, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}], ("/form", form, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}),
], ],
error_routes={"405": http405}, error_routes={405: http405},
) )

148
poetry.lock generated
View File

@ -11,6 +11,25 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
] ]
[[package]]
name = "attrs"
version = "24.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
]
[package.extras]
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]] [[package]]
name = "black" name = "black"
version = "24.8.0" version = "24.8.0"
@ -159,6 +178,90 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "coverage"
version = "7.6.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
{file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
{file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
{file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
{file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
{file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
{file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
{file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
{file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
{file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
{file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
{file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
{file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
{file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
{file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
{file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
{file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
{file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
{file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
{file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
{file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
{file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
{file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
]
[package.extras]
toml = ["tomli"]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "43.0.0" version = "43.0.0"
@ -264,6 +367,38 @@ setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"] tornado = ["tornado (>=0.2)"]
[[package]]
name = "hypothesis"
version = "6.111.2"
description = "A library for property-based testing"
optional = false
python-versions = ">=3.8"
files = [
{file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"},
{file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"},
]
[package.dependencies]
attrs = ">=22.2.0"
sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras]
all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.70)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.13)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"]
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"]
crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["django (>=3.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
ghostwriter = ["black (>=19.10b0)"]
lark = ["lark (>=0.10.1)"]
numpy = ["numpy (>=1.17.3)"]
pandas = ["pandas (>=1.1)"]
pytest = ["pytest (>=4.6)"]
pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"]
zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.7" version = "3.7"
@ -627,6 +762,17 @@ files = [
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
] ]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -641,4 +787,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "84633fc94c48c2a05b5ec77367ad29f327be1dc249a6e4cb76b50ebbe14739b5" content-hash = "17f5dc4b157da57ad75a6f6aa3feb7adfa07500b805d5e79d1f09d640964949f"

View File

@ -41,6 +41,8 @@ ruff = "^0.5.5"
pytest = "^8.3.2" pytest = "^8.3.2"
black = "^24.8.0" black = "^24.8.0"
gunicorn = "^23.0.0" gunicorn = "^23.0.0"
hypothesis = "^6.111.2"
coverage = "^7.6.1"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
@ -53,3 +55,32 @@ Homepage = "https://github.com/itsthejoker/spiderweb"
Documentation = "https://github.com/itsthejoker/spiderweb" Documentation = "https://github.com/itsthejoker/spiderweb"
Repository = "https://git.joekaufeld.com/jkaufeld/spiderweb" Repository = "https://git.joekaufeld.com/jkaufeld/spiderweb"
"Bug Tracker" = "https://github.com/itsthejoker/spiderweb/issues" "Bug Tracker" = "https://github.com/itsthejoker/spiderweb/issues"
[tool.pytest.ini_options]
minversion = "6.0"
addopts = ["--maxfail=2", "-rf"]
[tool.coverage.run]
branch = true
omit = ["conftest.py"]
[tool.coverage.report]
# Regexes for lines to exclude from consideration
exclude_also = [
# Don't complain about missing debug-only code:
"def __repr__",
"if self\\.debug",
# Don't complain if tests don't hit defensive assertion code:
"raise AssertionError",
"raise NotImplementedError",
# Don't complain if non-runnable code isn't run:
"if 0:",
"if __name__ == .__main__.:",
# Don't complain about abstract methods, they aren't run:
"@(abc\\.)?abstractmethod",
]
ignore_errors = true

View File

@ -1,7 +1,7 @@
from peewee import DatabaseProxy from peewee import DatabaseProxy
DEFAULT_ALLOWED_METHODS = ["GET"] DEFAULT_ALLOWED_METHODS = ["GET"]
DEFAULT_ENCODING = "ISO-8859-1" DEFAULT_ENCODING = "UTF-8"
__version__ = "0.10.0" __version__ = "0.10.0"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie

View File

@ -5,9 +5,6 @@ class IntConverter:
def to_python(self, value): def to_python(self, value):
return int(value) return int(value)
def to_url(self, value):
return str(value)
class StrConverter: class StrConverter:
regex = r"[^/]+" regex = r"[^/]+"
@ -16,9 +13,6 @@ class StrConverter:
def to_python(self, value): def to_python(self, value):
return str(value) return str(value)
def to_url(self, value):
return str(value)
class FloatConverter: class FloatConverter:
regex = r"\d+\.\d+" regex = r"\d+\.\d+"
@ -26,6 +20,3 @@ class FloatConverter:
def to_python(self, value): def to_python(self, value):
return float(value) return float(value)
def to_url(self, value):
return str(value)

View File

@ -11,7 +11,7 @@ def http403(request):
def http404(request): def http404(request):
return JsonResponse( return JsonResponse(
data={"error": f"Route {request.url} not found"}, status_code=404 data={"error": f"Route `{request.path}` not found"}, status_code=404
) )

View File

@ -7,7 +7,7 @@ from threading import Thread
from typing import Optional, Callable from typing import Optional, Callable
from wsgiref.simple_server import WSGIServer from wsgiref.simple_server import WSGIServer
from jinja2 import Environment, FileSystemLoader from jinja2 import BaseLoader, Environment, FileSystemLoader
from peewee import Database, SqliteDatabase from peewee import Database, SqliteDatabase
from spiderweb.middleware import MiddlewareMixin from spiderweb.middleware import MiddlewareMixin
@ -46,8 +46,8 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
middleware: list[str] = None, middleware: list[str] = None,
append_slash: bool = False, append_slash: bool = False,
staticfiles_dirs: list[str] = None, staticfiles_dirs: list[str] = None,
routes: list[list[str | Callable | dict]] = None, routes: list[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
error_routes: dict[str, Callable] = None, error_routes: dict[int, Callable] = None,
secret_key: str = None, secret_key: str = None,
session_max_age=60 * 60 * 24 * 14, # 2 weeks session_max_age=60 * 60 * 24 * 14, # 2 weeks
session_cookie_name="swsession", session_cookie_name="swsession",
@ -100,10 +100,14 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
if self.routes: if self.routes:
self.add_routes() self.add_routes()
if self.error_routes:
self.add_error_routes()
if self.templates_dirs: if self.templates_dirs:
self.env = Environment(loader=FileSystemLoader(self.templates_dirs)) self.template_loader = Environment(loader=FileSystemLoader(self.templates_dirs))
else: else:
self.env = None self.template_loader = None
self.string_loader = Environment(loader=BaseLoader())
if self.staticfiles_dirs: if self.staticfiles_dirs:
for static_dir in self.staticfiles_dirs: for static_dir in self.staticfiles_dirs:
@ -131,7 +135,6 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
rendered_output = resp.render() rendered_output = resp.render()
if not isinstance(rendered_output, list): if not isinstance(rendered_output, list):
rendered_output = [rendered_output] rendered_output = [rendered_output]
encoded_resp = [ encoded_resp = [
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
for chunk in rendered_output for chunk in rendered_output
@ -183,12 +186,12 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
def prepare_and_fire_response(self, start_response, request, resp) -> list[bytes]: def prepare_and_fire_response(self, start_response, request, resp) -> list[bytes]:
try: try:
if isinstance(resp, dict): if isinstance(resp, dict):
self.fire_response(start_response, request, JsonResponse(data=resp)) return self.fire_response(start_response, request, JsonResponse(data=resp))
if isinstance(resp, TemplateResponse): if isinstance(resp, TemplateResponse):
resp.set_template_loader(self.env) resp.set_template_loader(self.template_loader)
resp.set_string_loader(self.string_loader)
for middleware in self.middleware: self.process_response_middleware(request, resp)
middleware.process_response(request, resp)
return self.fire_response(start_response, request, resp) return self.fire_response(start_response, request, resp)

View File

@ -21,7 +21,7 @@ class Session(SpiderwebModel):
user_agent = TextField() user_agent = TextField()
class Meta: class Meta:
table_name = 'spiderweb_sessions' table_name = "spiderweb_sessions"
class SessionMiddleware(SpiderwebMiddleware): class SessionMiddleware(SpiderwebMiddleware):
@ -68,7 +68,7 @@ class SessionMiddleware(SpiderwebMiddleware):
} }
# if a new session has been requested, ignore everything else and make that happen # if a new session has been requested, ignore everything else and make that happen
if request._session["new_session"]: if request._session["new_session"] is True:
# we generated a new one earlier, so we can use it now # we generated a new one earlier, so we can use it now
session_key = request._session["id"] session_key = request._session["id"]
response.set_cookie( response.set_cookie(
@ -76,6 +76,8 @@ class SessionMiddleware(SpiderwebMiddleware):
session_key, session_key,
**cookie_settings, **cookie_settings,
) )
if not is_jsonable(request.SESSION):
raise ValueError("Session data is not JSON serializable.")
session = Session( session = Session(
session_key=session_key, session_key=session_key,
session_data=json.dumps(request.SESSION), session_data=json.dumps(request.SESSION),
@ -97,18 +99,6 @@ class SessionMiddleware(SpiderwebMiddleware):
) )
session = request.META["SESSION"] session = request.META["SESSION"]
if not session: session.session_data = json.dumps(request.SESSION)
if not is_jsonable(request.SESSION): session.last_active = datetime.now()
raise ValueError("Session data is not JSON serializable.")
session = Session(
session_key=session_key,
session_data=json.dumps(request.SESSION),
created_at=datetime.now(),
last_active=datetime.now(),
ip_address=request.META.get("client_address"),
user_agent=request.META.get("HTTP_USER_AGENT"),
)
else:
session.session_data = json.dumps(request.SESSION)
session.last_active = datetime.now()
session.save() session.save()

View File

@ -10,7 +10,6 @@ from spiderweb.constants import REGEX_COOKIE_NAME
from spiderweb.exceptions import GeneralException from spiderweb.exceptions import GeneralException
from spiderweb.request import Request from spiderweb.request import Request
mimetypes.init() mimetypes.init()
@ -128,20 +127,39 @@ class RedirectResponse(HttpResponse):
class TemplateResponse(HttpResponse): class TemplateResponse(HttpResponse):
def __init__(self, request: Request, template=None, *args, **kwargs): def __init__(
self,
request: Request,
template_path: str = None,
template_string: str = None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.context["request"] = request self.context["request"] = request
self.template = template self.template_path = template_path
self.loader = None self.template_string = template_string
self.template_loader = None
self.string_loader = None
self._template = None self._template = None
if not template: if not template_path and not template_string:
raise GeneralException("TemplateResponse requires a template.") raise GeneralException("TemplateResponse requires a template.")
def render(self) -> str: def render(self) -> str:
if self.loader is None: if self.template_loader is None:
raise GeneralException("TemplateResponse requires a template loader.") if not self.template_string:
self._template = self.loader.get_template(self.template) raise GeneralException(
"TemplateResponse has no loader. Did you set templates_dirs?"
)
else:
self._template = self.string_loader.from_string(self.template_string)
else:
self._template = self.template_loader.get_template(self.template_path)
return self._template.render(**self.context) return self._template.render(**self.context)
def set_template_loader(self, env): def set_template_loader(self, loader):
self.loader = env self.template_loader = loader
def set_string_loader(self, loader):
self.string_loader = loader

View File

@ -1,5 +1,5 @@
import re import re
from typing import Callable, Any from typing import Callable, Any, Optional
from spiderweb.constants import DEFAULT_ALLOWED_METHODS from spiderweb.constants import DEFAULT_ALLOWED_METHODS
from spiderweb.converters import * # noqa: F403 from spiderweb.converters import * # noqa: F403
@ -30,9 +30,9 @@ class RoutesMixin:
# ones that start with underscores are the compiled versions, non-underscores # ones that start with underscores are the compiled versions, non-underscores
# are the user-supplied versions # are the user-supplied versions
_routes: dict _routes: dict
routes: list[list[str | Callable | dict]] routes: list[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
_error_routes: dict _error_routes: dict
error_routes: dict[str, Callable] error_routes: dict[int, Callable]
append_slash: bool append_slash: bool
def route(self, path, allowed_methods=None) -> Callable: def route(self, path, allowed_methods=None) -> Callable:

View File

@ -0,0 +1 @@
from spiderweb.tests.middleware import ExplodingResponseMiddleware, ExplodingRequestMiddleware

View File

@ -0,0 +1,11 @@
from spiderweb import SpiderwebMiddleware, Request, HttpResponse, UnusedMiddleware
class ExplodingRequestMiddleware(SpiderwebMiddleware):
def process_request(self, request: Request) -> HttpResponse | None:
raise UnusedMiddleware("Boom!")
class ExplodingResponseMiddleware(SpiderwebMiddleware):
def process_response(self, request: Request, response: HttpResponse) -> HttpResponse | None:
raise UnusedMiddleware("Unfinished!")

View File

@ -0,0 +1,88 @@
from datetime import timedelta
from peewee import SqliteDatabase
from spiderweb import SpiderwebRouter, HttpResponse
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.middleware.sessions import Session
from spiderweb.tests.helpers import setup
# app = SpiderwebRouter(
# middleware=[
# "spiderweb.middleware.sessions.SessionMiddleware",
# "spiderweb.middleware.csrf.CSRFMiddleware",
# "example_middleware.TestMiddleware",
# "example_middleware.RedirectMiddleware",
# "spiderweb.middleware.pydantic.PydanticMiddleware",
# "example_middleware.ExplodingMiddleware",
# ],
# )
def index(request):
if "value" in request.SESSION:
request.SESSION['value'] += 1
else:
request.SESSION['value'] = 0
return HttpResponse(body=str(request.SESSION['value']))
def test_session_middleware():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=["spiderweb.middleware.sessions.SessionMiddleware"],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
environ["HTTP_USER_AGENT"] = "hi"
environ["REMOTE_ADDR"] = "1.1.1.1"
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session_key = Session.select().first().session_key
environ["HTTP_COOKIE"] = f"swsession={session_key}"
assert app(environ, start_response) == [bytes(str(1), DEFAULT_ENCODING)]
assert app(environ, start_response) == [bytes(str(2), DEFAULT_ENCODING)]
def test_expired_session():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=["spiderweb.middleware.sessions.SessionMiddleware"],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
environ["HTTP_USER_AGENT"] = "hi"
environ["REMOTE_ADDR"] = "1.1.1.1"
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session = Session.select().first()
session.created_at = session.created_at - timedelta(seconds=app.session_max_age)
session.save()
environ["HTTP_COOKIE"] = f"swsession={session.session_key}"
# it shouldn't increment because we get a new session
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session2 = list(Session.select())[-1]
assert session2.session_key != session.session_key
def test_exploding_middleware():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=[
"spiderweb.tests.middleware.ExplodingRequestMiddleware",
"spiderweb.tests.middleware.ExplodingResponseMiddleware",
],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]

View File

@ -0,0 +1,173 @@
import pytest
from spiderweb import SpiderwebRouter, ConfigError
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.exceptions import NoResponseError, SpiderwebNetworkException
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
from hypothesis import given, strategies as st
from spiderweb.tests.helpers import setup
@given(st.text())
def test_http_response(text):
app, environ, start_response = setup()
@app.route("/")
def index(request):
return HttpResponse(text)
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
def test_json_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return JsonResponse(data={"message": "text"})
assert app(environ, start_response) == [bytes('{"message": "text"}', DEFAULT_ENCODING)]
def test_dict_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return {"message": "Hello, World!"}
assert app(environ, start_response) == [b'{"message": "Hello, World!"}']
@given(st.text())
def test_template_response(text):
app, environ, start_response = setup()
template = "MESSAGE: {{ message }}"
@app.route("/")
def index(request):
return TemplateResponse(
request, template_string=template, context={"message": text}
)
assert app(environ, start_response) == [b"MESSAGE: " + bytes(text, DEFAULT_ENCODING)]
def test_redirect_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return RedirectResponse(location="/redirected")
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/redirected"
def test_add_route_at_server_start():
app, environ, start_response = setup()
def index(request):
return RedirectResponse(location="/redirected")
def view2(request):
return HttpResponse("View 2")
app = SpiderwebRouter(routes=[
("/", index, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}),
("/view2", view2),
])
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/redirected"
def test_redirect_on_append_slash():
_, environ, start_response = setup()
app = SpiderwebRouter(append_slash=True)
@app.route("/hello")
def index(request):
pass
environ["PATH_INFO"] = f"/hello"
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/hello/"
@given(st.text())
def test_template_response_with_template(text):
_, environ, start_response = setup()
app = SpiderwebRouter(templates_dirs=["spiderweb/tests"])
@app.route("/")
def index(request):
return TemplateResponse(
request, "test.html", context={"message": text}
)
assert app(environ, start_response) == [b"TEMPLATE! " + bytes(text, DEFAULT_ENCODING)]
def test_view_returns_none():
app, environ, start_response = setup()
@app.route("/")
def index(request):
pass
with pytest.raises(NoResponseError):
assert app(environ, start_response) == [b'None']
def test_exploding_view():
app, environ, start_response = setup()
@app.route("/")
def index(request):
raise SpiderwebNetworkException("Boom!")
assert app(environ, start_response) == [
b'Something went wrong.\n\nCode: Boom!\n\nMsg: None\n\nDesc: None'
]
def test_missing_view():
app, environ, start_response = setup()
assert app(environ, start_response) == [b'{"error": "Route `/` not found"}']
def test_missing_view_with_custom_404():
app, environ, start_response = setup()
@app.error(404)
def custom_404(request):
return HttpResponse("Custom 404")
assert app(environ, start_response) == [b'Custom 404']
def test_duplicate_error_view():
app, environ, start_response = setup()
@app.error(404)
def custom_404(request):
...
with pytest.raises(ConfigError):
@app.error(404)
def custom_404(request):
...
def test_missing_view_with_custom_404_alt():
_, environ, start_response = setup()
def custom_404(request):
return HttpResponse("Custom 404 2")
app = SpiderwebRouter(error_routes={404: custom_404})
assert app(environ, start_response) == [b'Custom 404 2']

View File

@ -0,0 +1,102 @@
import pytest
from spiderweb import SpiderwebRouter
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.exceptions import ParseError, ConfigError
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
from hypothesis import given, strategies as st, assume
from peewee import SqliteDatabase
from spiderweb.tests.helpers import setup
@given(st.text())
def test_str_converter(text):
assume(len(text) > 0)
assume("/" not in text)
app, environ, start_response = setup()
@app.route("/<str:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{text}"
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
@given(st.text())
def test_default_str_converter(text):
assume(len(text) > 0)
assume("/" not in text)
app, environ, start_response = setup()
@app.route("/<test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{text}"
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
def test_unknown_converter():
app, environ, start_response = setup()
with pytest.raises(ParseError):
@app.route("/<asdf:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
def test_duplicate_route():
app, environ, start_response = setup()
@app.route("/")
def index(request):
...
with pytest.raises(ConfigError):
@app.route("/")
def index(request):
...
def test_url_with_double_underscore():
app, environ, start_response = setup()
with pytest.raises(ConfigError):
@app.route("/<asdf:test__input>")
def index(request, test_input: str):
return HttpResponse(test_input)
@given(st.integers())
def test_int_converter(integer):
assume(integer > 0)
app, environ, start_response = setup()
@app.route("/<int:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{integer}"
assert app(environ, start_response) == [bytes(str(integer), DEFAULT_ENCODING)]
@pytest.mark.parametrize(
"number",
[
1.0000000000000002,
294744.2324,
0000.3,
],
)
def test_float_converter(number):
app, environ, start_response = setup()
@app.route("/<float:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{number}"
assert app(environ, start_response) == [bytes(str(number), DEFAULT_ENCODING)]