From e42e136a6f7569908c52ff951acfa77d1ec9f63e Mon Sep 17 00:00:00 2001 From: James Hutchison <122519877+JamesHutchison@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:33:03 -0800 Subject: [PATCH] Updates for VS Code (#50) * Make daemon auto-start a parameter that defaults to off * Dev container updates * Updates to client and server logic to improve information sent * Add --daemon-start-if-needed argument --- .devcontainer/devcontainer.json | 26 ++-- .devcontainer/postStartBackground.sh | 2 + .github/actions/test/action.yaml | 2 +- README.md | 6 +- poetry.lock | 203 ++++++++------------------- pyproject.toml | 12 +- pytest_hot_reloading/client.py | 38 +++-- pytest_hot_reloading/daemon.py | 23 ++- pytest_hot_reloading/plugin.py | 44 +++--- tests/test_client.py | 15 +- 10 files changed, 171 insertions(+), 200 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3bf4daa..63fc138 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,21 +26,11 @@ "mhutchie.git-graph", "eamodio.gitlens", "charliermarsh.ruff", - "ms-azuretools.vscode-docker" + "ms-azuretools.vscode-docker", + "matangover.mypy" ], "settings": { "python.defaultInterpreterPath": "/workspaces/pytest-hot-reloading/.venv/bin/python", - "python.formatting.provider": "black", - "python.linting.mypyEnabled": true, - "python.linting.mypyPath": "dmypy", - "python.linting.mypyArgs": [ - "run --", - "--ignore-missing-imports", - "--show-column-numbers", - "--no-pretty", - "--python-executable", - "/workspaces/pytest-hot-reloading/.venv/bin/python" - ], "python.testing.pytestArgs": [ "tests" ], @@ -55,7 +45,17 @@ "autoDocstring.customTemplatePath": ".vscode/autodocstring.mustache", "ruff.path": [ "/workspaces/pytest-hot-reloading/.venv/bin/ruff" - ] + ], + // use legacy test adapter + // "python.experiments.optOutFrom": [ + // "pythonTestAdapter" + // ], + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": true + } + } }, "git.branchProtection": [ "main", diff --git a/.devcontainer/postStartBackground.sh b/.devcontainer/postStartBackground.sh index 27415a3..0e73803 100755 --- a/.devcontainer/postStartBackground.sh +++ b/.devcontainer/postStartBackground.sh @@ -1,2 +1,4 @@ #!/usr/bin/env bash +poetry run pytest --daemon & + ptyme-track --standalone diff --git a/.github/actions/test/action.yaml b/.github/actions/test/action.yaml index 4253bfe..37f4302 100644 --- a/.github/actions/test/action.yaml +++ b/.github/actions/test/action.yaml @@ -17,7 +17,7 @@ runs: shell: bash - name: Test with pytest run: | - poetry run pytest + poetry run pytest --daemon-start-if-needed shell: bash - name: Rerun workaround tests to check for incompatibilities run: | diff --git a/README.md b/README.md index f3c76da..b79478b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ The daemon can be stopped with `pytest --stop-daemon`. This can be used if it ge - Default: `4852`. - Command line: `--daemon-port` - `PYTEST_DAEMON_PYTEST_NAME` - - The name of the pytest executable. + - The name of the pytest executable. Used for spawning the daemon. - Default: `pytest`. - Command line: `--pytest-name` - `PYTEST_DAEMON_WATCH_GLOBS` @@ -120,6 +120,10 @@ The daemon can be stopped with `pytest --stop-daemon`. This can be used if it ge - The colon separated globs to ignore. - Default: `./.venv/*`. - Command line: `--daemon-ignore-watch-globs` +- `PYTEST_DAEMON_START_IF_NEEDED` + - Start the pytest daemon if it is not running. + - Default: `False` + - Command line: `--daemon-start-if-needed` ## Workarounds Libraries that use mutated globals may need a workaround to work with this plugin. The preferred diff --git a/poetry.lock b/poetry.lock index b052261..ae22657 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "ansicon" @@ -45,54 +45,6 @@ six = "*" [package.extras] test = ["astroid", "pytest"] -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "blessed" version = "1.20.0" @@ -120,20 +72,6 @@ files = [ {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "codefind" version = "0.1.3" @@ -245,13 +183,13 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jurigged" -version = "0.5.5" +version = "0.5.7" description = "Live update of Python functions" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "jurigged-0.5.5-py3-none-any.whl", hash = "sha256:e850261631483494f6d788924f565d9cddea31f2fbaff4f8b222c49825cbec90"}, - {file = "jurigged-0.5.5.tar.gz", hash = "sha256:e3507407859e5b41d2ad0842c8880bded2261f8f4dc3db49a2ad1d7c5a36f82c"}, + {file = "jurigged-0.5.7-py3-none-any.whl", hash = "sha256:0a13a27bd6bffe71a50aecf75f29577c6de40ed8ef94fb6977f495b8c2071514"}, + {file = "jurigged-0.5.7.tar.gz", hash = "sha256:3afbb05f51f48a83e0f426fb0ea9cb44c3afefd6c49d11a783f6985576dd18db"}, ] [package.dependencies] @@ -265,13 +203,13 @@ develoop = ["giving (>=0.4.1,<0.5.0)", "rich (>=10.13.0)"] [[package]] name = "megamock" -version = "0.1.0b6" +version = "0.1.0b9" description = "Mega mocking capabilities - stop using dot-notated paths!" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "megamock-0.1.0b6-py3-none-any.whl", hash = "sha256:873f2492b3cb87ad009dcc6d5b5119822c032d5798802cf7d05d0767245496c3"}, - {file = "megamock-0.1.0b6.tar.gz", hash = "sha256:07e964c9bc274ab3cc4bd2c3267e6c1ea50412f5e8044904508fffba0f45ad1e"}, + {file = "megamock-0.1.0b9-py3-none-any.whl", hash = "sha256:bc5fc960e0c4635dc0964061f2de26e536333c27b4abf83b2df503ec365346e0"}, + {file = "megamock-0.1.0b9.tar.gz", hash = "sha256:91cbc062cf21dea96f1707159a3fa88c60b78b854f4ccf066cce7a472f3fe513"}, ] [package.dependencies] @@ -280,48 +218,49 @@ varname = {version = ">=0.10.0,<0.11.0", extras = ["asttokens"]} [[package]] name = "mypy" -version = "1.3.0" +version = "1.7.1" description = "Optional static typing for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"}, - {file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"}, - {file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"}, - {file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"}, - {file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"}, - {file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"}, - {file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"}, - {file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"}, - {file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"}, - {file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"}, - {file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"}, - {file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"}, - {file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"}, - {file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"}, - {file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"}, - {file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"}, - {file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"}, - {file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"}, - {file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"}, - {file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -357,32 +296,6 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "platformdirs" -version = "3.5.3" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.5.3-py3-none-any.whl", hash = "sha256:0ade98a4895e87dc51d47151f7d2ec290365a585151d97b4d8d6312ed6132fed"}, - {file = "platformdirs-3.5.3.tar.gz", hash = "sha256:e48fabd87db8f3a7df7150a4a5ea22c546ee8bc39bc2473244730d4b56d2cc4e"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] - [[package]] name = "pluggy" version = "1.0.0" @@ -548,28 +461,28 @@ testing = ["filelock"] [[package]] name = "ruff" -version = "0.0.261" -description = "An extremely fast Python linter, written in Rust." +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"}, - {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"}, - {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"}, - {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"}, - {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"}, - {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"}, - {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"}, - {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, ] [[package]] @@ -713,4 +626,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c57a0b4debe77acb57c1d0df28009212dd49c01309e37639af172b75d56c3138" +content-hash = "9bc19b8e502ae47b7f756158a8edc81ae518c8b5ef7d82131f708b96931d9c13" diff --git a/pyproject.toml b/pyproject.toml index dfb8fdb..949a061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,8 @@ [tool.ruff] -line-length = 120 +line-length = 98 + +[tool.ruff.lint.pycodestyle] +max-line-length = 120 [tool.black] line-length = 98 @@ -17,15 +20,14 @@ python = "^3.10" jurigged = "^0.5.5" cachetools = "^5.3.0" types-cachetools = "^5.3.0.5" +megamock = "^0.1.0b9" [tool.poetry.group.dev.dependencies] mypy = "^1.2.0" -ruff = "^0.0.261" -black = "^23.3.0" +ruff = "^0.1.6" pytest = "^7.2.2" -megamock = "^0.1.0b6" pytest-django = "^4.5.2" -django = "^4.2.2" +django = "4.2.2" psycopg2-binary = "^2.9.6" pytest-env = "^0.8.1" pytest-xdist = "^3.3.1" diff --git a/pytest_hot_reloading/client.py b/pytest_hot_reloading/client.py index e6b406f..05aca08 100644 --- a/pytest_hot_reloading/client.py +++ b/pytest_hot_reloading/client.py @@ -1,7 +1,10 @@ +import json +import os import socket import sys import time import xmlrpc.client +from pathlib import Path from typing import cast @@ -10,14 +13,20 @@ class PytestClient: _daemon_host: str _daemon_port: int _pytest_name: str + _will_start_daemon_if_needed: bool def __init__( - self, daemon_host: str = "localhost", daemon_port: int = 4852, pytest_name: str = "pytest" + self, + daemon_host: str = "localhost", + daemon_port: int = 4852, + pytest_name: str = "pytest", + start_daemon_if_needed: bool = False, ) -> None: self._socket = None self._daemon_host = daemon_host self._daemon_port = daemon_port self._pytest_name = pytest_name + self._will_start_daemon_if_needed = start_daemon_if_needed def _get_server(self) -> xmlrpc.client.ServerProxy: server_url = f"http://{self._daemon_host}:{self._daemon_port}" @@ -25,13 +34,21 @@ def _get_server(self) -> xmlrpc.client.ServerProxy: return server - def run(self, cwd: str, args: list[str]) -> int: - self._start_daemon_if_needed() + def run(self, cwd: Path, args: list[str]) -> int: + if self._will_start_daemon_if_needed: + self._start_daemon_if_needed() + elif not self._daemon_running(): + raise Exception( + "Daemon is not running and must be started, or add --daemon-start-if-needed" + ) server = self._get_server() + env = os.environ.copy() + sys_path = sys.path + start = time.time() - result: dict = cast(dict, server.run_pytest(cwd, args)) + result: dict = cast(dict, server.run_pytest(str(cwd), json.dumps(env), sys_path, args)) print(f"Daemon took {(time.time() - start):.3f} seconds to reply") stdout = result["stdout"].data.decode("utf-8") @@ -60,10 +77,7 @@ def abort(self) -> None: if self._socket: self._socket.close() - def _start_daemon_if_needed(self) -> None: - # check if the daemon is running on the expected host and port - # if not, start the daemon - + def _daemon_running(self) -> bool: # first, try to connect try: self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -71,9 +85,15 @@ def _start_daemon_if_needed(self) -> None: # the daemon is running # close the socket self._socket.close() + return True except ConnectionRefusedError: # the daemon is not running - # start the daemon + return False + + def _start_daemon_if_needed(self) -> None: + # check if the daemon is running on the expected host and port + # if not, start the daemon + if not self._daemon_running(): self._start_daemon() def _start_daemon(self) -> None: diff --git a/pytest_hot_reloading/daemon.py b/pytest_hot_reloading/daemon.py index cba4402..e6c6307 100644 --- a/pytest_hot_reloading/daemon.py +++ b/pytest_hot_reloading/daemon.py @@ -1,4 +1,5 @@ import copy +import json import os import re import socket @@ -73,7 +74,7 @@ def stop(self) -> dict: def wait_to_be_ready(host: str = "localhost", port: int = 4852) -> None: # poll the connection to the daemon using sockets # and return when it is ready - for _ in range(100): + for _ in range(1000): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) @@ -119,7 +120,7 @@ def _kill_existing_daemon(self) -> None: except FileNotFoundError: raise Exception(f"Port {self._daemon_port} is already in use") - def run_pytest(self, cwd: str, args: list[str]) -> dict: + def run_pytest(self, cwd: str, env_json: str, sys_path: list[str], args: list[str]) -> dict: # run pytest using command line args # run the pytest main logic @@ -152,8 +153,20 @@ def run_pytest(self, cwd: str, args: list[str]) -> dict: # store current working directory prev_cwd = os.getcwd() + # switch to client working directory os.chdir(cwd) + # copy the environment + env_old = os.environ.copy() + # switch to client environment + new_env = json.loads(env_json) + os.environ.update(new_env) + + # copy sys.path + sys_path_old = sys.path + # switch to client path + sys.path = sys_path + try: # args must omit the calling program status_code = pytest.main(["--color=yes"] + args) @@ -161,6 +174,12 @@ def run_pytest(self, cwd: str, args: list[str]) -> dict: os.chdir(prev_cwd) self._workaround_library_issues_post(in_progress_workarounds) + # restore sys.path + sys.path = sys_path_old + + # restore environment + os.environ.update(env_old) + # restore originals _pytest.main._main = orig_main diff --git a/pytest_hot_reloading/plugin.py b/pytest_hot_reloading/plugin.py index 42b961c..8aab993 100644 --- a/pytest_hot_reloading/plugin.py +++ b/pytest_hot_reloading/plugin.py @@ -37,7 +37,7 @@ def pytest_addoption(parser) -> None: "--pytest-name", action="store", default=os.getenv("PYTEST_DAEMON_PYTEST_NAME", "pytest"), - help="The name of the pytest executable or module", + help="The name of the pytest executable or module. This is used for starting the daemon.", ) group.addoption( "--daemon-timeout", @@ -63,6 +63,15 @@ def pytest_addoption(parser) -> None: default=False, help="Stop the daemon", ) + group.addoption( + "--daemon-start-if-needed", + action="store_true", + default=os.getenv("PYTEST_DAEMON_START_IF_NEEDED", "False").lower() in ("true", "1"), + help=( + "Start the daemon if it is not running. To use this with VS Code, " + 'you need add "python.experiments.optOutFrom": ["pythonTestAdapter"] to your config.' + ), + ) # list of pytest hooks @@ -78,6 +87,8 @@ def pytest_cmdline_main(config: Config) -> Optional[int]: return None if i_am_server: return None + if config.option.help: + return None status_code = _plugin_logic(config) # dont do any more work. Don't let pytest continue return status_code # status code 0 @@ -181,33 +192,20 @@ def _plugin_logic(config: Config) -> int: sys.exit(0) else: pytest_name = config.option.pytest_name - client = PytestClient(daemon_port=daemon_port, pytest_name=pytest_name) + client = PytestClient( + daemon_port=daemon_port, + pytest_name=pytest_name, + start_daemon_if_needed=config.option.daemon_start_if_needed, + ) if config.option.stop_daemon: client.stop() return 0 - # find the index of the first value that is not None - for idx, val in enumerate( - [ - x.endswith(pytest_name) or x.endswith(f"{pytest_name}/__main__.py") - for x in sys.argv - ] - ): - if val: - pytest_name_index = idx - break - else: - if "pytest_runner" in sys.argv[0]: - pytest_name_index = 0 - else: - print(sys.argv) - raise Exception( - "Could not find pytest name in args. " - "Check the configured name versus the actual name." - ) - cwd = os.getcwd() - status_code = client.run(cwd, sys.argv[pytest_name_index + 1 :]) + cwd = config.invocation_params.dir + args = list(config.invocation_params.args) + + status_code = client.run(cwd, args) return status_code diff --git a/tests/test_client.py b/tests/test_client.py index 447a0f0..532f81f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,6 +2,7 @@ import re import socket import xmlrpc.client +from pathlib import Path import pytest from megamock import Mega, MegaMock, MegaPatch @@ -28,7 +29,7 @@ def test_run(self, capsys: pytest.CaptureFixture) -> None: client = PytestClient() args = ["foo", "bar"] - status_code = client.run(os.getcwd(), args) + status_code = client.run(Path(os.getcwd()), args) out, err = capsys.readouterr() @@ -36,6 +37,18 @@ def test_run(self, capsys: pytest.CaptureFixture) -> None: assert err == "stderr\n" assert status_code == 1 + def test_when_sever_not_avaiable_then_raises_error(self) -> None: + client = PytestClient(start_daemon_if_needed=False) + MegaPatch.it(PytestClient._daemon_running, return_value=False) + + with pytest.raises(Exception) as exc: + client.run(Path(), ["args"]) + + assert ( + str(exc.value) + == "Daemon is not running and must be started, or add --daemon-start-if-needed" + ) + def test_aborting_should_close_the_socket(self) -> None: mock = MegaMock.it(PytestClient) Mega(mock.abort).use_real_logic()