diff --git a/docs/requirements.txt b/docs/requirements.txt index 37f8f4fb3..f4c5ff1aa 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,8 @@ alabaster==0.7.16 ; python_version >= "3.10" and python_version < "3.11" asciitree==0.3.3 ; python_version >= "3.10" and python_version < "3.11" -astmonkey==0.3.6 ; python_version >= "3.10" and python_version < "3.11" astroid==3.1.0 ; python_version >= "3.10" and python_version < "3.11" babel==2.14.0 ; python_version >= "3.10" and python_version < "3.11" -black==24.3.0 ; python_version >= "3.10" and python_version < "3.11" +black==24.4.1 ; python_version >= "3.10" and python_version < "3.11" bytecode==0.15.1 ; python_version >= "3.10" and python_version < "3.11" certifi==2024.2.2 ; python_version >= "3.10" and python_version < "3.11" charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "3.11" @@ -11,37 +10,34 @@ click==8.1.7 ; python_version >= "3.10" and python_version < "3.11" colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.11" and (sys_platform == "win32" or platform_system == "Windows") docstring-parser==0.16 ; python_version >= "3.10" and python_version < "3.11" docutils==0.20.1 ; python_version >= "3.10" and python_version < "3.11" -exceptiongroup==1.2.0 ; python_version >= "3.10" and python_version < "3.11" -idna==3.6 ; python_version >= "3.10" and python_version < "3.11" +exceptiongroup==1.2.1 ; python_version >= "3.10" and python_version < "3.11" +idna==3.7 ; python_version >= "3.10" and python_version < "3.11" imagesize==1.4.1 ; python_version >= "3.10" and python_version < "3.11" iniconfig==2.0.0 ; python_version >= "3.10" and python_version < "3.11" jellyfish==1.0.3 ; python_version >= "3.10" and python_version < "3.11" jinja2==3.1.3 ; python_version >= "3.10" and python_version < "3.11" -libcst==1.2.0 ; python_version >= "3.10" and python_version < "3.11" +libcst==1.3.1 ; python_version >= "3.10" and python_version < "3.11" markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "3.11" markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "3.11" mdurl==0.1.2 ; python_version >= "3.10" and python_version < "3.11" -mutpy-pynguin==0.7.1 ; python_version >= "3.10" and python_version < "3.11" mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "3.11" -networkx==3.2.1 ; python_version >= "3.10" and python_version < "3.11" +networkx==3.3 ; python_version >= "3.10" and python_version < "3.11" packaging==24.0 ; python_version >= "3.10" and python_version < "3.11" pathspec==0.12.1 ; python_version >= "3.10" and python_version < "3.11" -platformdirs==4.2.0 ; python_version >= "3.10" and python_version < "3.11" -pluggy==1.4.0 ; python_version >= "3.10" and python_version < "3.11" -pydot==2.0.0 ; python_version >= "3.10" and python_version < "3.11" +platformdirs==4.2.1 ; python_version >= "3.10" and python_version < "3.11" +pluggy==1.5.0 ; python_version >= "3.10" and python_version < "3.11" pygments==2.17.2 ; python_version >= "3.10" and python_version < "3.11" -pyparsing==3.1.2 ; python_version >= "3.10" and python_version < "3.11" pytest==8.1.1 ; python_version >= "3.10" and python_version < "3.11" pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "3.11" requests==2.31.0 ; python_version >= "3.10" and python_version < "3.11" rich==13.7.1 ; python_version >= "3.10" and python_version < "3.11" simple-parsing==0.1.5 ; python_version >= "3.10" and python_version < "3.11" snowballstemmer==2.2.0 ; python_version >= "3.10" and python_version < "3.11" -sphinx-autodoc-typehints==2.0.0 ; python_version >= "3.10" and python_version < "3.11" +sphinx-autodoc-typehints==2.1.0 ; python_version >= "3.10" and python_version < "3.11" sphinx-hoverxref==1.3.0 ; python_version >= "3.10" and python_version < "3.11" sphinx-rtd-theme==2.0.0 ; python_version >= "3.10" and python_version < "3.11" sphinx-selective-exclude==1.0.3 ; python_version >= "3.10" and python_version < "3.11" -sphinx==7.2.6 ; python_version >= "3.10" and python_version < "3.11" +sphinx==7.3.7 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-applehelp==1.0.8 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-devhelp==1.0.6 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-htmlhelp==2.0.5 ; python_version >= "3.10" and python_version < "3.11" @@ -49,8 +45,7 @@ sphinxcontrib-jquery==4.1 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-qthelp==1.0.7 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-serializinghtml==1.1.10 ; python_version >= "3.10" and python_version < "3.11" -termcolor==2.4.0 ; python_version >= "3.10" and python_version < "3.11" tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" -typing-extensions==4.10.0 ; python_version >= "3.10" and python_version < "3.11" +typing-extensions==4.11.0 ; python_version >= "3.10" and python_version < "3.11" typing-inspect==0.9.0 ; python_version >= "3.10" and python_version < "3.11" urllib3==2.2.1 ; python_version >= "3.10" and python_version < "3.11" diff --git a/poetry.lock b/poetry.lock index 8d1253454..2ef1b2769 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,19 +21,6 @@ files = [ {file = "asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e"}, ] -[[package]] -name = "astmonkey" -version = "0.3.6" -description = "astmonkey is a set of tools to play with Python AST." -optional = false -python-versions = "*" -files = [ - {file = "astmonkey-0.3.6.tar.gz", hash = "sha256:f82dbdd18a2d1810ef43782d3a29743bacd2b09422b8193a72a572b118c8cfb5"}, -] - -[package.dependencies] -pydot = "*" - [[package]] name = "astroid" version = "3.1.0" @@ -83,33 +70,33 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "black" -version = "24.3.0" +version = "24.4.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f7749fd0d97ff9415975a1432fac7df89bf13c3833cea079e55fa004d5f28c0"}, + {file = "black-24.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859f3cc5d2051adadf8fd504a01e02b0fd866d7549fff54bc9202d524d2e8bd7"}, + {file = "black-24.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59271c9c29dfa97f7fda51f56c7809b3f78e72fd8d2205189bbd23022a0618b6"}, + {file = "black-24.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:5ed9c34cba223149b5a0144951a0f33d65507cf82c5449cb3c35fe4b515fea9a"}, + {file = "black-24.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dae3ae59d6f2dc93700fd5034a3115434686e66fd6e63d4dcaa48d19880f2b0"}, + {file = "black-24.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f8698974a81af83283eb47644f2711b5261138d6d9180c863fce673cbe04b13"}, + {file = "black-24.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f404b6e77043b23d0321fb7772522b876b6de737ad3cb97d6b156638d68ce81"}, + {file = "black-24.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:c94e52b766477bdcd010b872ba0714d5458536dc9d0734eff6583ba7266ffd89"}, + {file = "black-24.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:962d9e953872cdb83b97bb737ad47244ce2938054dc946685a4cad98520dab38"}, + {file = "black-24.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d8e3b2486b7dd522b1ab2ba1ec4907f0aa8f5e10a33c4271fb331d1d10b70c"}, + {file = "black-24.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed77e214b785148f57e43ca425b6e0850165144aa727d66ac604e56a70bb7825"}, + {file = "black-24.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4ef4764437d7eba8386689cd06e1fb5341ee0ae2e9e22582b21178782de7ed94"}, + {file = "black-24.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:92b183f8eef5baf7b20a513abcf982ad616f544f593f6688bb2850d2982911f1"}, + {file = "black-24.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:945abd7b3572add997757c94295bb3e73c6ffaf3366b1f26cb2356a4bffd1dc3"}, + {file = "black-24.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db5154b9e5b478031371d8bc41ff37b33855fa223a6cfba456c9b73fb96f77d4"}, + {file = "black-24.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:afc84c33c1a9aaf3d73140cee776b4ddf73ff429ffe6b7c56dc1c9c10725856d"}, + {file = "black-24.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0889f4eb8b3bdf8b189e41a71cf0dbb8141a98346cd1a2695dea5995d416e940"}, + {file = "black-24.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5bb0143f175db45a55227eefd63e90849d96c266330ba31719e9667d0d5ec3b9"}, + {file = "black-24.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713a04a78e78f28ef7e8df7a16fe075670ea164860fcef3885e4f3dffc0184b3"}, + {file = "black-24.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:171959bc879637a8cdbc53dc3fddae2a83e151937a28cf605fd175ce61e0e94a"}, + {file = "black-24.4.1-py3-none-any.whl", hash = "sha256:ecbab810604fe02c70b3a08afd39beb599f7cc9afd13e81f5336014133b4fe35"}, + {file = "black-24.4.1.tar.gz", hash = "sha256:5241612dc8cad5b6fd47432b8bd04db80e07cfbc53bb69e9ae18985063bcb8dd"}, ] [package.dependencies] @@ -286,63 +273,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.dependencies] @@ -386,13 +373,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -400,29 +387,29 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.13.1" +version = "3.13.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"}, + {file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "hypothesis" -version = "6.99.11" +version = "6.100.1" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.99.11-py3-none-any.whl", hash = "sha256:9c010b219ec611c995bac7e11a3edc96cea3c2c0b04a9d08ec586a7981715353"}, - {file = "hypothesis-6.99.11.tar.gz", hash = "sha256:3df99ecf501e056c7e2c6ac7a9e3726f4fe8d2139dcce068aceffdab58265f1e"}, + {file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"}, + {file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"}, ] [package.dependencies] @@ -431,10 +418,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 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.51)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.2)", "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)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.54)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.2)", "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.51)", "hypothesis-crosshair (>=0.0.2)"] +crosshair = ["crosshair-tool (>=0.0.54)", "hypothesis-crosshair (>=0.0.2)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=3.2)"] dpcontracts = ["dpcontracts (>=0.4)"] @@ -449,13 +436,13 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] [[package]] name = "identify" -version = "2.5.35" +version = "2.5.36" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] [package.extras] @@ -463,13 +450,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -601,45 +588,43 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "libcst" -version = "1.2.0" +version = "1.3.1" description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.12 programs." optional = false python-versions = ">=3.9" files = [ - {file = "libcst-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0149d24a455536ff2e41b3a48b16d3ebb245e28035013c91bd868def16592a0"}, - {file = "libcst-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba24b8cf789db6b87c6e23a6c6365f5f73cb7306d929397581d5680149e9990c"}, - {file = "libcst-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbb4e442224da46b59a248d7d632ed335eae023a921dea1f5c72d2a059f6be9"}, - {file = "libcst-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4919978c2b395079b64d8a654357854767adbabab13998b39c1f0bc67da8a7"}, - {file = "libcst-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e54389abdea995b39ee96ad736ed1b0b8402ed30a7956b7a279c10baf0c0294"}, - {file = "libcst-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d45718f7e7a1405a16fd8e7fc75c365120001b6928bfa3c4112f7e533990b9a"}, - {file = "libcst-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f080e9af843ff609f8f35fc7275c8bf08b02c31115e7cd5b77ca3b6a56c75096"}, - {file = "libcst-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3c7c0edfe3b878d64877671261c7b3ffe9d23181774bfad5d8fcbdbbbde9f064"}, - {file = "libcst-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b5fecb2b26fa3c1efe6e05ef1420522bd31bb4dae239e4c41fdf3ddbd853aeb"}, - {file = "libcst-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:968b93400e66e6711a29793291365e312d206dbafd3fc80219cfa717f0f01ad5"}, - {file = "libcst-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e01879aa8cd478bb8b1e4285cfd0607e64047116f7ab52bc2a787cde584cd686"}, - {file = "libcst-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:b4066dcadf92b183706f81ae0b4342e7624fc1d9c5ca2bf2b44066cb74bf863f"}, - {file = "libcst-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5c0d548d92c6704bb07ce35d78c0e054cdff365def0645c1b57c856c8e112bb4"}, - {file = "libcst-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82373a35711a8bb2a664dba2b7aeb20bbcce92a4db40af964e9cb2b976f989e7"}, - {file = "libcst-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb92398236566f0b73a0c73f8a41a9c4906c793e8f7c2745f30e3fb141a34b5"}, - {file = "libcst-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ca9fe82326d82feb2c7b0f5a320ce7ed0d707c32919dd36e1f40792459bf6f"}, - {file = "libcst-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dded0e4f2e18150c4b07fedd7ef84a9abc7f9bd2d47cc1c485248ee1ec58e5cc"}, - {file = "libcst-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:dece0362540abfc39cd2cf5c98cde238b35fd74a1b0167e2563e4b8bb5f47489"}, - {file = "libcst-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c80f36f4a02d530e28eac7073aabdea7c6795fc820773a02224021d79d164e8b"}, - {file = "libcst-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b56130f18aca9a98b3bcaf5962b2b26c2dcdd6d5132decf3f0b0b635f4403ba"}, - {file = "libcst-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4973a9d509cf1a59e07fac55a98f70bc4fd35e09781dffb3ec93ee32fc0de7af"}, - {file = "libcst-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd388c74c04434b41e3b25fc4a0fafa3e6abf91f97181df55e8f8327fd903cc"}, - {file = "libcst-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38fbd56f885e1f77383a6d1d798a917ffbc6d28dc6b1271eddbf8511c194213e"}, - {file = "libcst-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:f2342634f6c61fc9076dc0baf21e9cf5ef0195a06e1e95c0c9dc583ba3a30d00"}, - {file = "libcst-1.2.0.tar.gz", hash = "sha256:71dd69fff76e7edaf8fae0f63ffcdbf5016e8cd83165b1d0688d6856aa48186a"}, + {file = "libcst-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:de93193cba6d54f2a4419e94ba2de642b111f52e4fa01bb6e2c655914585f65b"}, + {file = "libcst-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2d64d86dcd6c80a5dac2e243c5ed7a7a193242209ac33bad4b0639b24f6d131"}, + {file = "libcst-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db084f7bbf825c7bd5ed256290066d0336df6a7dc3a029c9870a64cd2298b87f"}, + {file = "libcst-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16880711be03a1f5da7028fe791ba5b482a50d830225a70272dc332dfd927652"}, + {file = "libcst-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:189bb28c19c5dd3c64583f969b72f7732dbdb1dee9eca3acc85099e4cef9148b"}, + {file = "libcst-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:181372386c986e3de07d7a93f269214cd825adc714f1f9da8252b44f05e181c4"}, + {file = "libcst-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c2020f7449270e3ff0bdc481ae244d812f2d9a8b7dbff0ea66b830f4b350f54"}, + {file = "libcst-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:be3bf9aaafebda6a21e241e819f0ab67e186e898c3562704e41241cf8738353a"}, + {file = "libcst-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a0d250fb6a2c1d158f30d25ba5e33e3ed3672d2700d480dd47beffd1431a008"}, + {file = "libcst-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad5741b251d901f3da1819ac539192230cc6f8f81aaf04eb4ec0009c1c97285"}, + {file = "libcst-1.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b740dc0c3d1adbd91442fb878343d5a11e23a0e3ac5484be301fd8d148bcb085"}, + {file = "libcst-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9e6bc95fa7dde79cde63a34a0412cd4a3d9fcc27be781a590f8c45c840c83658"}, + {file = "libcst-1.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4186076ce12141609ce950d61867b2a73ea199a7a9870dbafa76ad600e075b3c"}, + {file = "libcst-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ed52a1a2fe4d8603de51649db5e438317b8116ebb9fc09ec68703535fe6c1c8"}, + {file = "libcst-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0886a9963597367b227345f19b24931b3ed6a4703fff237760745f90f0e6a20"}, + {file = "libcst-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:904c4cc5c801a5747e64b43e0accc87c67a4c804842d977ee215872c4cf8cf88"}, + {file = "libcst-1.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cdb7e8a118b60e064a02f6cbfa4d328212a3a115d125244495190f405709d5f"}, + {file = "libcst-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:431badf0e544b79c0ac9682dbd291ff63ddbc3c3aca0d13d3cc7a10c3a9db8a2"}, + {file = "libcst-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:701f5335e4fd566871497b9af1e871c98e1ef10c30b3b244f39343d709213401"}, + {file = "libcst-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c6e709623b68ca9148e8ecbdc145f7b83befb26032e4bf6a8122500ba558b17"}, + {file = "libcst-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ede0f026a82b03b33a559ec566860085ece2e76d8f9bc21cb053eedf9cde8c79"}, + {file = "libcst-1.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c12b7b01d8745f82dd86a82acd2a9f8e8e7d6c94ddcfda996896e83d1a8d5c42"}, + {file = "libcst-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2995ca687118a9d3d41876f7270bc29305a2d402f4b8c81a3cff0aeee6d4c81"}, + {file = "libcst-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:2dbac1ac0a9d59ea7bbc3f87cdcca5bfe98835e31c668e95cb6f3d907ffc53fc"}, + {file = "libcst-1.3.1.tar.gz", hash = "sha256:03b1df1ae02456f1d465fcd5ead1d0d454bb483caefd8c8e6bde515ffdb53d1b"}, ] [package.dependencies] pyyaml = ">=5.2" -typing-extensions = ">=3.7.4.2" -typing-inspect = ">=0.4.0" [package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==23.12.1)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.0.0)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.3)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<1.5)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.3.0)", "usort (==1.0.7)"] +dev = ["Sphinx (>=5.1.1)", "black (==23.12.1)", "build (>=0.10.0)", "coverage (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.0.0)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.3)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<1.5)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"] [[package]] name = "markdown-it-py" @@ -745,60 +730,40 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "mutpy-pynguin" -version = "0.7.1" -description = "Mutation testing tool for Python 3.x source code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "MutPy-Pynguin-0.7.1.tar.gz", hash = "sha256:31fe168eff221ece0129768b68375ca0d03c6514567caf19878ae7a618b9ab89"}, - {file = "MutPy_Pynguin-0.7.1-py3-none-any.whl", hash = "sha256:9c6ce1afe1f818e97a16f498d0f3ecf65127d93ac74f70eea5ee7b0833122a4e"}, -] - -[package.dependencies] -astmonkey = ">=0.3.6" -Jinja2 = ">=2.7.1" -PyYAML = ">=5.3.1" -termcolor = ">=1.0.0" - -[package.extras] -pytest = ["pytest (>=3.0)"] - [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] @@ -825,20 +790,20 @@ files = [ [[package]] name = "networkx" -version = "3.2.1" +version = "3.3" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, + {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, + {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, ] [package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] @@ -879,28 +844,29 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -909,13 +875,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.2" +version = "3.7.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, - {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, ] [package.dependencies] @@ -925,25 +891,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pydot" -version = "2.0.0" -description = "Python interface to Graphviz's Dot" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydot-2.0.0-py3-none-any.whl", hash = "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28"}, - {file = "pydot-2.0.0.tar.gz", hash = "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235"}, -] - -[package.dependencies] -pyparsing = ">=3" - -[package.extras] -dev = ["black", "chardet"] -release = ["zest.releaser[recommended]"] -tests = ["black", "chardet", "tox"] - [[package]] name = "pygments" version = "2.17.2" @@ -959,20 +906,6 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pyparsing" -version = "3.1.2" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" version = "8.1.1" @@ -1065,13 +998,13 @@ dev = ["black", "flake8", "pre-commit"] [[package]] name = "pyupgrade" -version = "3.15.1" +version = "3.15.2" description = "A tool to automatically upgrade syntax for newer versions." optional = false python-versions = ">=3.8.1" files = [ - {file = "pyupgrade-3.15.1-py2.py3-none-any.whl", hash = "sha256:c5e005de2805edcd333d1deb04553200ec69da85e4bc9db37b16345ed9e27ed9"}, - {file = "pyupgrade-3.15.1.tar.gz", hash = "sha256:7690857cae0f6253f39241dcd2e57118c333c438b78609fc3c17a5aa61227b7d"}, + {file = "pyupgrade-3.15.2-py2.py3-none-any.whl", hash = "sha256:ce309e0ff8ecb73f56a45f12570be84bbbde9540d13697cacb261a7f595fb1f5"}, + {file = "pyupgrade-3.15.2.tar.gz", hash = "sha256:c488d6896c546d25845712ef6402657123008d56c1063174e27aabe15bd6b4e5"}, ] [package.dependencies] @@ -1177,44 +1110,44 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.3.4" +version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, - {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, - {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, - {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, - {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, - {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, - {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, - {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1262,20 +1195,20 @@ files = [ [[package]] name = "sphinx" -version = "7.2.6" +version = "7.3.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ - {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, - {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" Jinja2 = ">=3.0" packaging = ">=21.0" @@ -1288,30 +1221,31 @@ sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autodoc-typehints" -version = "2.0.0" +version = "2.1.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinx_autodoc_typehints-2.0.0-py3-none-any.whl", hash = "sha256:12c0e161f6fe191c2cdfd8fa3caea271f5387d9fbc67ebcd6f4f1f24ce880993"}, - {file = "sphinx_autodoc_typehints-2.0.0.tar.gz", hash = "sha256:7f2cdac2e70fd9787926b6e9e541cd4ded1e838d2b46fda2a1bb0a75ec5b7f3a"}, + {file = "sphinx_autodoc_typehints-2.1.0-py3-none-any.whl", hash = "sha256:46f1a710b3ed35904f63a77c5e68334c5ee1c2e22828b75fdcd147f1c52c199b"}, + {file = "sphinx_autodoc_typehints-2.1.0.tar.gz", hash = "sha256:51bf8dc77c4fba747e32f0735002a91500747d0553cae616863848e8f5e49fe8"}, ] [package.dependencies] -sphinx = ">=7.1.2" +sphinx = ">=7.3.5" [package.extras] -docs = ["furo (>=2023.9.10)"] +docs = ["furo (>=2024.1.29)"] numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.8)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] [[package]] name = "sphinx-hoverxref" @@ -1507,13 +1441,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] @@ -1550,13 +1484,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.25.1" +version = "20.26.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, + {file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"}, + {file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"}, ] [package.dependencies] @@ -1565,10 +1499,10 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.10, <3.11" -content-hash = "015408ad8cf08a40699062294cec09a855c6dbc78b0edb6d2506e80856afd592" +content-hash = "912106a01b6eed88004475883a9796122343a8c7e9874fd5f484e49f5165b7ee" diff --git a/pyproject.toml b/pyproject.toml index 62f28f11f..05514d513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ black = "^24.3.0" bytecode = "^0.15.1" jellyfish = "^1.0.3" Jinja2 = "^3.1.3" -MutPy-Pynguin = "^0.7.1" networkx = "^3.2" rich = "^13.7.1" Pygments = "^2.17.2" diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index d1ea606e0..241b73629 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -15,12 +15,11 @@ from typing import TYPE_CHECKING -import mutpy - import pynguin.assertion.assertion as ass import pynguin.assertion.assertion_trace as at import pynguin.assertion.assertiontraceobserver as ato -import pynguin.assertion.mutation_analysis.mutationadapter as ma +import pynguin.assertion.mutation_analysis.controller as ct +import pynguin.assertion.mutation_analysis.mutators as mu import pynguin.configuration as config import pynguin.ga.chromosomevisitor as cv import pynguin.testcase.execution as ex @@ -232,70 +231,122 @@ def get_score(self) -> float: return self.num_killed_mutants / divisor -class MutationAnalysisAssertionGenerator(AssertionGenerator): - """Uses mutation analysis to filter out less relevant assertions.""" +class InstrumentedMutationController(ct.MutationController): + """A controller that creates instrumented mutants.""" - def _create_module_with_instrumentation( - self, ast_node, module_name="mutant", module_dict=None - ): - # Mimics mutpy.utils.create_module but adds instrumentation to the resulting - # module + def __init__( + self, + mutant_generator: mu.Mutator, + module_ast: ast.Module, + module: types.ModuleType, + tracer: ex.ExecutionTracer, + *, + testing: bool = False, + ) -> None: + """Create new controller. + + Args: + mutant_generator: The mutant generator. + module_ast: The module AST. + module: The module. + tracer: The execution tracer. + testing: Enable test mode, currently required for integration testing. + """ + super().__init__(mutant_generator, module_ast, module) + + self._tracer = tracer + + self._transformer = build_transformer( + tracer, + {config.CoverageMetric.BRANCH}, + DynamicConstantProvider(ConstantPool(), EmptyConstantProvider(), 0, 1), + ) + + # Some debug information + self._testing = testing + self._testing_created_mutants: list[str] = [] + + @property + def tracer(self) -> ex.ExecutionTracer: + """Provides the execution tracer. + + Returns: + The execution tracer. + """ + return self._tracer + + def create_mutant(self, ast_node: ast.Module) -> types.ModuleType: # noqa: D102 + self._tracer.current_thread_identifier = threading.current_thread().ident + self._tracer.reset() + module_name = self._module.__name__ code = compile(ast_node, module_name, "exec") if self._testing: self._testing_created_mutants.append(ast.unparse(ast_node)) code = self._transformer.instrument_module(code) module = types.ModuleType(module_name) - module.__dict__.update(module_dict or {}) - exec(code, module.__dict__) # noqa: S102 + self._tracer.store_import_trace() return module - def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False): + +class MutationAnalysisAssertionGenerator(AssertionGenerator): + """Uses mutation analysis to filter out less relevant assertions.""" + + def __init__( + self, + plain_executor: ex.TestCaseExecutor, + mutation_controller: InstrumentedMutationController, + *, + testing: bool = False, + ): """Initializes the generator. Args: plain_executor: Executor used for plain execution + mutation_controller: Controller for mutation analysis testing: Enable test mode, currently required for integration testing. """ super().__init__(plain_executor) # We use a separate tracer and executor to execute tests on the mutants. - self._mutation_tracer = ex.ExecutionTracer() - self._mutation_tracer.current_thread_identifier = ( - threading.current_thread().ident - ) - self._mutation_executor = ex.TestCaseExecutor(self._mutation_tracer) + self._mutation_executor = ex.TestCaseExecutor(mutation_controller.tracer) self._mutation_executor.add_observer(ato.AssertionVerificationObserver()) - self._transformer = build_transformer( - self._mutation_tracer, - {config.CoverageMetric.BRANCH}, - DynamicConstantProvider(ConstantPool(), EmptyConstantProvider(), 0, 1), - ) + self._mutation_controller = mutation_controller + # Some debug information self._testing = testing - self._testing_created_mutants: list[str] = [] self._testing_mutation_summary: _MutationSummary = _MutationSummary() - adapter = ma.MutationAdapter() - - # Evil hack to change the way mutpy creates mutated modules. - mutpy.utils.create_module = self._create_module_with_instrumentation - self._mutated_modules = [x for x, _ in adapter.mutate_module()] def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]] = [ + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]] = [ (test, []) for test in test_cases ] + mutant_count = self._mutation_controller.mutant_count() + with self._mutation_executor.temporarily_add_observer( ato.AssertionVerificationObserver() ): - for idx, mutated_module in enumerate(self._mutated_modules): + for idx, (mutated_module, _) in enumerate( + self._mutation_controller.create_mutants(), start=1 + ): + if mutated_module is None: + self._logger.info( + "Skipping mutant %3i/%i because " + "it created an invalid module", + idx, + mutant_count, + ) + for _, results in tests_and_results: + results.append(None) + continue + self._logger.info( "Running tests on mutant %3i/%i", - idx + 1, - len(self._mutated_modules), + idx, + mutant_count, ) self._mutation_executor.module_provider.add_mutated_version( module_name=config.configuration.module_name, @@ -304,15 +355,13 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): for test, results in tests_and_results: results.append(self._mutation_executor.execute(test)) - summary = self.__compute_mutation_summary( - len(self._mutated_modules), tests_and_results - ) + summary = self.__compute_mutation_summary(mutant_count, tests_and_results) self.__report_mutation_summary(summary) self.__remove_non_relevant_assertions(tests_and_results, summary) @staticmethod def __remove_non_relevant_assertions( - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]], + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]], mutation_summary: _MutationSummary, ) -> None: for test, results in tests_and_results: @@ -321,7 +370,7 @@ def __remove_non_relevant_assertions( results, mutation_summary.mutant_information, strict=True ): # Ignore timed out executions - if len(mut.timed_out_by) == 0: + if result is not None and len(mut.timed_out_by) == 0: merged.merge(result.assertion_verification_trace) for stmt_idx, statement in enumerate(test.statements): for assertion_idx, assertion in reversed( @@ -333,13 +382,13 @@ def __remove_non_relevant_assertions( @staticmethod def __compute_mutation_summary( number_of_mutants: int, - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]], + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]], ) -> _MutationSummary: mutation_info = [_MutantInfo(i) for i in range(number_of_mutants)] for test_num, (_, results) in enumerate(tests_and_results): # For each mutation, check if we had a violated assertion for info, result in zip(mutation_info, results, strict=True): - if info.timed_out_by: + if result is None or info.timed_out_by: continue if result.timeout: # Mutant caused timeout diff --git a/src/pynguin/assertion/mutation_analysis/__init__.py b/src/pynguin/assertion/mutation_analysis/__init__.py index 7ee809ddb..b447269e7 100644 --- a/src/pynguin/assertion/mutation_analysis/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/__init__.py @@ -4,4 +4,4 @@ # # SPDX-License-Identifier: MIT # -"""Provides an adapter for MutPy's mutation analysis.""" +"""Provides the classes required for mutation analysis.""" diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py new file mode 100644 index 000000000..a25e87020 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -0,0 +1,94 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides a controller for generating mutants.""" +from __future__ import annotations + +import ast +import logging + +from typing import TYPE_CHECKING +from typing import Generator + +from pynguin.assertion.mutation_analysis.transformer import create_module + + +if TYPE_CHECKING: + import types + + from types import ModuleType + + import pynguin.assertion.mutation_analysis.mutators as mu + + from pynguin.assertion.mutation_analysis.operators.base import Mutation + + +_LOGGER = logging.getLogger(__name__) + + +class MutationController: + """A controller that creates mutants.""" + + def __init__( + self, + mutant_generator: mu.Mutator, + module_ast: ast.Module, + module: types.ModuleType, + ) -> None: + """Initialize the controller. + + Args: + mutant_generator: The mutant generator to use. + module_ast: The AST of the module to mutate. + module: The module to mutate. + """ + self._mutant_generator = mutant_generator + self._module_ast = module_ast + self._module = module + + def create_mutant(self, mutant_ast: ast.Module) -> ModuleType: + """Creates a mutant of the module. + + Args: + mutant_ast: The mutant AST. + + Returns: + The created mutant module. + """ + return create_module(mutant_ast, self._module.__name__) + + def create_mutants( + self, + ) -> Generator[tuple[ModuleType | None, list[Mutation]], None, None]: + """Creates mutants for the module. + + Returns: + A generator of tuples where the first entry is the mutated module or None + if the mutated module cannot be created and the second part is a list of + all the mutations operators applied. + """ + for mutations, mutant_ast in self._mutant_generator.mutate( + self._module_ast, self._module + ): + assert isinstance(mutant_ast, ast.Module) + + try: + mutant_module = self.create_mutant(mutant_ast) + except Exception as exception: # noqa: BLE001 + _LOGGER.debug("Error creating mutant: %s", exception) + mutant_module = None + + yield mutant_module, mutations + + def mutant_count(self) -> int: + """Calculates the number of mutants that can be created. + + Returns: + The number of mutants that can be created. + """ + return sum( + 1 for _ in self._mutant_generator.mutate(self._module_ast, self._module) + ) diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py deleted file mode 100644 index fec3c520d..000000000 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ /dev/null @@ -1,125 +0,0 @@ -# This file is part of Pynguin. -# -# SPDX-FileCopyrightText: 2019–2024 Pynguin Contributors -# -# SPDX-License-Identifier: MIT -# -"""Provides an adapter for the MutPy mutation testing framework.""" -from __future__ import annotations - -import logging - -from typing import TYPE_CHECKING - -import mutpy.controller as mc -import mutpy.operators as mo -import mutpy.operators.loop as mol -import mutpy.utils as mu -import mutpy.views as mv - -import pynguin.configuration as config - -from pynguin.utils.exceptions import ConfigurationException - - -if TYPE_CHECKING: - from collections.abc import Callable - from types import ModuleType - from typing import ClassVar - - -_LOGGER = logging.getLogger(__name__) - - -class MutationAdapter: - """Adapter class for interactions with the MutPy mutation testing framework.""" - - _strategies: ClassVar[ - dict[config.MutationStrategy, Callable[[int], mc.HOMStrategy]] - ] = { - config.MutationStrategy.FIRST_TO_LAST: mc.FirstToLastHOMStrategy, - config.MutationStrategy.BETWEEN_OPERATORS: mc.BetweenOperatorsHOMStrategy, - config.MutationStrategy.RANDOM: mc.RandomHOMStrategy, - config.MutationStrategy.EACH_CHOICE: mc.EachChoiceHOMStrategy, - } - - def __init__(self): # noqa: D107 - self.target_loader: mu.ModulesLoader | None = None - - def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: - """Mutates the modules specified in the configuration. - - Uses MutPy's mutation procedure. - - Returns: - A list of tuples where the first entry is the mutated module and the second - part is a list of all the mutations operators applied. - """ - controller = self._build_mutation_controller() - controller.score = mc.MutationScore() - - mutants = [] - - if self.target_loader is not None: - for target_module, to_mutate in self.target_loader.load(): - _LOGGER.info("Build AST for %s", target_module.__name__) - target_ast = controller.create_target_ast(target_module) - _LOGGER.info("Mutate module %s", target_module.__name__) - mutant_modules = controller.mutate_module( - target_module=target_module, - to_mutate=to_mutate, - target_ast=target_ast, - ) - for mutant_module, mutations in mutant_modules: - mutants.append((mutant_module, mutations)) - _LOGGER.info("Generated %d mutants", len(mutants)) - return mutants - - def _build_mutation_controller(self) -> mc.MutationController: - _LOGGER.info("Setup mutation controller") - built_views = self._get_views() - mutant_generator = self._get_mutant_generator() - self.target_loader = mu.ModulesLoader( - [config.configuration.module_name], config.configuration.project_path - ) - - return mc.MutationController( - runner_cls=None, - target_loader=self.target_loader, - test_loader=None, - views=built_views, - mutant_generator=mutant_generator, - ) - - def _get_mutant_generator(self) -> mc.FirstOrderMutator: - operators_set = set() - operators_set |= mo.standard_operators - - # Only use a selected set of the experimental operators. - operators_set |= { - mol.OneIterationLoop, - mol.ReverseIterationLoop, - mol.ZeroIterationLoop, - } - - # percentage of the generated mutants (mutation sampling) - percentage = 100 - - mutation_strategy = config.configuration.test_case_output.mutation_strategy - - if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: - return mc.FirstOrderMutator(operators_set, percentage) - - order = config.configuration.test_case_output.mutation_order - if order <= 0: - raise ConfigurationException("Mutation order should be > 0.") - - if mutation_strategy in self._strategies: - hom_strategy = self._strategies[mutation_strategy](order) - return mc.HighOrderMutator(operators_set, percentage, hom_strategy) - raise ConfigurationException("No suitable mutation strategy found.") - - @staticmethod - def _get_views() -> list[mv.QuietTextView]: - # We do not want any output from MutPy here - return [mv.QuietTextView()] diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py new file mode 100644 index 000000000..6b4ad2fa5 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -0,0 +1,125 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutating ASTs. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py +and integrated in Pynguin. +""" +from __future__ import annotations + +import abc + +from typing import TYPE_CHECKING + +from pynguin.assertion.mutation_analysis.strategies import FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import HOMStrategy + + +if TYPE_CHECKING: + import ast + import types + + from collections.abc import Generator + + from pynguin.assertion.mutation_analysis.operators.base import Mutation + from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + + +class Mutator(abc.ABC): + """A mutator is responsible for mutating an AST.""" + + @abc.abstractmethod + def mutate( + self, + target_ast: ast.AST, + module: types.ModuleType, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + """Mutate the given AST. + + Args: + target_ast: The AST to mutate. + module: The module to mutate. + + Yields: + A generator of mutations and the mutated AST. + """ + + +class FirstOrderMutator(Mutator): + """A mutator that applies first order mutations.""" + + def __init__(self, operators: list[type[MutationOperator]]) -> None: + """Initialize the mutator. + + Args: + operators: The operators to use for mutation. + """ + self.operators = operators + + def mutate( # noqa: D102 + self, + target_ast: ast.AST, + module: types.ModuleType, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + for op in self.operators: + for mutation, mutant in op.mutate(target_ast, module): + yield [mutation], mutant + + +class HighOrderMutator(FirstOrderMutator): + """A mutator that applies high order mutations.""" + + def __init__( + self, + operators: list[type[MutationOperator]], + hom_strategy: HOMStrategy | None = None, + ) -> None: + """Initialize the mutator. + + Args: + operators: The operators to use for mutation. + hom_strategy: The strategy to use for higher order mutations. + """ + super().__init__(operators) + self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() + + def mutate( # noqa: D102 + self, + target_ast: ast.AST, + module: types.ModuleType, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + mutations = self._generate_all_mutations(module, target_ast) + for mutations_to_apply in self.hom_strategy.generate(mutations): + generators = [] + applied_mutations = [] + mutant = target_ast + for mutation in mutations_to_apply: + generator = mutation.operator.mutate(mutant, module, mutation) + next_value = next(generator, None) + assert next_value is not None + new_mutation, mutant = next_value + applied_mutations.append(new_mutation) + generators.append(generator) + yield applied_mutations, mutant + self._finish_generators(generators) + + def _generate_all_mutations( + self, + module: types.ModuleType, + target_ast: ast.AST, + ) -> list[Mutation]: + mutations: list[Mutation] = [] + for op in self.operators: + for mutation, _ in op.mutate(target_ast, module): + mutations.append(mutation) + return mutations + + @staticmethod + def _finish_generators(generators: list[Generator]) -> None: + for generator in reversed(generators): + value = next(generator, None) + assert value is None, "too many mutations!" diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py new file mode 100644 index 000000000..0e2523e67 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -0,0 +1,94 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides mutation operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py +and integrated in Pynguin. +""" + +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.decorator import DecoratorDeletion +from pynguin.assertion.mutation_analysis.operators.exception import ( + ExceptionHandlerDeletion, +) +from pynguin.assertion.mutation_analysis.operators.exception import ExceptionSwallowing +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + HidingVariableDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverriddenMethodCallingPositionChange, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverridingMethodDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + SuperCallingDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import SuperCallingInsert +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorInsertion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalConnectorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + RelationalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.loop import OneIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ReverseIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ZeroIterationLoop +from pynguin.assertion.mutation_analysis.operators.misc import ( + AssignmentOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.misc import BreakContinueReplacement +from pynguin.assertion.mutation_analysis.operators.misc import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.misc import SliceIndexRemove + + +standard_operators: list[type[MutationOperator]] = [ + ArithmeticOperatorDeletion, + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + BreakContinueReplacement, + ConditionalOperatorDeletion, + ConditionalOperatorInsertion, + ConstantReplacement, + DecoratorDeletion, + ExceptionHandlerDeletion, + ExceptionSwallowing, + HidingVariableDeletion, + LogicalConnectorReplacement, + LogicalOperatorDeletion, + LogicalOperatorReplacement, + OverriddenMethodCallingPositionChange, + OverridingMethodDeletion, + RelationalOperatorReplacement, + SliceIndexRemove, + SuperCallingDeletion, + SuperCallingInsert, +] + +experimental_operators: list[type[MutationOperator]] = [ + OneIterationLoop, + ReverseIterationLoop, + ZeroIterationLoop, +] diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py new file mode 100644 index 000000000..a94949089 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -0,0 +1,233 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides arithmetic operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/arithmetic.py +and integrated in Pynguin. +""" + +import abc +import ast + +from pynguin.assertion.mutation_analysis.operators.base import ( + AbstractUnaryOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + + +class ArithmeticOperatorDeletion(AbstractUnaryOperatorDeletion): + """A class that mutate arithmetic operators by deleting them.""" + + def get_operator_type(self) -> type: # noqa: D102 + return ast.UAdd | ast.USub # type: ignore[return-value] + + +class AbstractArithmeticOperatorReplacement(abc.ABC, MutationOperator): + """An abstract class that mutates arithmetic operators by replacing them.""" + + @abc.abstractmethod + def should_mutate(self, node: ast.AST) -> bool: + """Check if the operator should be mutated. + + Args: + node: The node to check. + + Returns: + True if the operator should be mutated, False otherwise. + """ + + def mutate_Add(self, node: ast.Add) -> ast.Sub | None: # noqa: N802 + """Mutate an Add operator to a Sub operator. + + Args: + node: The Add operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Sub() + + def mutate_Sub(self, node: ast.Sub) -> ast.Add | None: # noqa: N802 + """Mutate a Sub operator to an Add operator. + + Args: + node: The Sub operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Add() + + def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div | None: # noqa: N802 + """Mutate a Mult operator to a Div operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Div() + + def mutate_Mult_to_FloorDiv( # noqa: N802 + self, node: ast.Mult + ) -> ast.FloorDiv | None: + """Mutate a Mult operator to a FloorDiv operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.FloorDiv() + + def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow | None: # noqa: N802 + """Mutate a Mult operator to a Pow operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Pow() + + def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult | None: # noqa: N802 + """Mutate a Div operator to a Mult operator. + + Args: + node: The Div operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Div_to_FloorDiv( # noqa: N802 + self, node: ast.Div + ) -> ast.FloorDiv | None: + """Mutate a Div operator to a FloorDiv operator. + + Args: + node: The Div operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.FloorDiv() + + def mutate_FloorDiv_to_Div( # noqa: N802 + self, node: ast.FloorDiv + ) -> ast.Div | None: + """Mutate a FloorDiv operator to a Div operator. + + Args: + node: The FloorDiv operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Div() + + def mutate_FloorDiv_to_Mult( # noqa: N802 + self, node: ast.FloorDiv + ) -> ast.Mult | None: + """Mutate a FloorDiv operator to a Mult operator. + + Args: + node: The FloorDiv operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Mod(self, node: ast.Mod) -> ast.Mult | None: # noqa: N802 + """Mutate a Mod operator to a Mult operator. + + Args: + node: The Mod operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Pow(self, node: ast.Pow) -> ast.Mult | None: # noqa: N802 + """Mutate a Pow operator to a Mult operator. + + Args: + node: The Pow operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ + if not self.should_mutate(node): + return None + + return ast.Mult() + + +class ArithmeticOperatorReplacement(AbstractArithmeticOperatorReplacement): + """A class that mutates arithmetic operators by replacing them.""" + + def should_mutate(self, node: ast.AST) -> bool: # noqa: D102 + parent = node.parent # type: ignore[attr-defined] + return not isinstance(parent, ast.AugAssign) + + def mutate_USub(self, node: ast.USub) -> ast.UAdd: # noqa: N802 + """Mutate a USub operator to a UAdd operator. + + Args: + node: The USub operator to mutate. + + Returns: + The mutated operator. + """ + return ast.UAdd() + + def mutate_UAdd(self, node: ast.UAdd) -> ast.USub: # noqa: N802 + """Mutate a UAdd operator to a USub operator. + + Args: + node: The UAdd operator to mutate. + + Returns: + The mutated operator. + """ + return ast.USub() diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py new file mode 100644 index 000000000..53609eec9 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -0,0 +1,298 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides base classes for mutation operators. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/base.py +and integrated in Pynguin. +""" +from __future__ import annotations + +import abc +import ast +import copy +import re + +from dataclasses import dataclass +from typing import TYPE_CHECKING +from typing import TypeVar + + +if TYPE_CHECKING: + import types + + from collections.abc import Callable + from collections.abc import Generator + from collections.abc import Iterable + + +def fix_lineno(node: ast.AST) -> None: + """Fix the line number of a node if it is not set. + + Args: + node: The node to fix. + """ + parent = node.parent # type: ignore[attr-defined] + if not hasattr(node, "lineno") and parent is not None and hasattr(parent, "lineno"): + parent_lineno = parent.lineno + node.lineno = parent_lineno + + +def fix_node_internals(old_node: ast.AST, new_node: ast.AST) -> None: + """Fix the internals of a node. + + Args: + old_node: The old node. + new_node: The new node. + """ + if not hasattr(new_node, "parent"): + old_node_children = old_node.children # type: ignore[attr-defined] + old_node_parent = old_node.parent # type: ignore[attr-defined] + new_node.children = old_node_children # type: ignore[attr-defined] + new_node.parent = old_node_parent # type: ignore[attr-defined] + + if not hasattr(new_node, "lineno") and hasattr(old_node, "lineno"): + old_node_lineno = old_node.lineno + new_node.lineno = old_node_lineno + + if hasattr(old_node, "marker"): + old_node_marker = old_node.marker + new_node.marker = old_node_marker # type: ignore[attr-defined] + + +def set_lineno(node: ast.AST, lineno: int) -> None: + """Set the line number of a node. + + Args: + node: The node to set the line number for. + lineno: The line number to set. + """ + for child_node in ast.walk(node): + if hasattr(child_node, "lineno"): + child_node.lineno = lineno + + +T = TypeVar("T", bound=ast.AST) + + +def shift_lines(nodes: list[T], shift_by: int = 1) -> None: + """Shift the line numbers of a list of nodes. + + Args: + nodes: The nodes to shift. + shift_by: The amount to shift by. + """ + for node in nodes: + ast.increment_lineno(node, shift_by) + + +@dataclass(frozen=True) +class Mutation: + """Represents a mutation.""" + + node: ast.AST + replacement_node: ast.AST + operator: type[MutationOperator] + visitor_name: str + + def __post_init__(self): + """Initialize the mutation. + + Raises: + ValueError: If the visitor is not found in the operator. + """ + if self.visitor_name not in dir(self.operator): + raise ValueError( + f"Visitor {self.visitor_name} not found in operator {self.operator}" + ) + + +def copy_node(node: T) -> T: + """Copy a node. + + Args: + node: The node to copy. + + Returns: + The copied node. + """ + parent = node.parent # type: ignore[attr-defined] + return copy.deepcopy( + node, + memo={ + id(parent): parent, + }, + ) + + +class MutationOperator: + """A class that represents a mutation operator.""" + + @classmethod + def mutate( + cls, + node: T, + module: types.ModuleType, + only_mutation: Mutation | None = None, + ) -> Generator[tuple[Mutation, ast.AST], None, None]: + """Mutate a node. + + This method will temporarily modify the node provided and yield itself modified + with the mutations. If you want to keep the original node while using the + generator, you should copy it before passing it to this method. + + Args: + node: The node to mutate. + module: The module to use. + only_mutation: The mutation to apply. + + Yields: + A tuple containing the mutation and the mutated node. + """ + operator = cls(module, only_mutation) + + for ( + current_node, + replacement_node, + mutated_node, + visitor_name, + ) in operator.visit(node): + yield Mutation( + current_node, replacement_node, cls, visitor_name + ), mutated_node + + def __init__( + self, + module: types.ModuleType, + only_mutation: Mutation | None, + ) -> None: + """Initializes the operator. + + Args: + module: The module to use. + only_mutation: The mutation to apply. + """ + self.module = module + self.only_mutation = only_mutation + + def visit( + self, node: T + ) -> Generator[tuple[ast.AST, ast.AST, ast.AST, str], None, None]: + """Visit a node. + + This method will temporarily modify the node provided and yield itself modified + with other information. If you want to keep the original node while using the + generator, you should copy it before passing it to this method. + + Args: + node: The node to visit. + + Yields: + A tuple (current node, replacement node, mutated node, visitor name). + """ + node_children = node.children # type: ignore[attr-defined] + + if ( + self.only_mutation + and self.only_mutation.node != node + and self.only_mutation.node not in node_children + ): + return + + fix_lineno(node) + + for visitor in self._find_visitors(node): + if ( + self.only_mutation is None + or ( + self.only_mutation.node == node + and self.only_mutation.visitor_name == visitor.__name__ + ) + ) and (mutated_node := visitor(node)) is not None: + fix_node_internals(node, mutated_node) + ast.fix_missing_locations(mutated_node) + + yield node, mutated_node, mutated_node, visitor.__name__ + + yield from self._generic_visit(node) + + def _generic_visit( + self, node: ast.AST + ) -> Generator[tuple[ast.AST, ast.AST, ast.AST, str], None, None]: + for field, old_value in ast.iter_fields(node): + generator: Iterable[tuple[ast.AST, ast.AST, str]] + if isinstance(old_value, list): + generator = self._generic_visit_list(old_value) + elif isinstance(old_value, ast.AST): + generator = self._generic_visit_real_node(node, field, old_value) + else: + generator = () + + for current_node, replacement_node, visitor_name in generator: + yield current_node, replacement_node, node, visitor_name + + def _generic_visit_list( + self, old_value: list + ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + for position, value in enumerate(old_value.copy()): + if isinstance(value, ast.AST): + for ( + current_node, + replacement_node, + mutated_node, + visitor_name, + ) in self.visit(value): + old_value[position] = mutated_node + yield current_node, replacement_node, visitor_name + + old_value[position] = value + + def _generic_visit_real_node( + self, node: ast.AST, field: str, old_value: ast.AST + ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + for current_node, replacement_node, mutated_node, visitor_name in self.visit( + old_value + ): + setattr(node, field, mutated_node) + yield current_node, replacement_node, visitor_name + + setattr(node, field, old_value) + + def _find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: + node_name = node.__class__.__name__ + method_prefix_pattern = re.compile(f"^mutate_{node_name}(_\\w+)?$") + return [ + visitor + for attr in dir(self) + if method_prefix_pattern.match(attr) is not None + and callable(visitor := getattr(self, attr)) + ] + + +class AbstractUnaryOperatorDeletion(abc.ABC, MutationOperator): + """An abstract class that mutates unary operators by deleting them.""" + + @abc.abstractmethod + def get_operator_type(self) -> type: + """Get the operator type. + + Returns: + The operator type. + """ + + def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr | None: # noqa: N802 + """Mutate a unary operator. + + Args: + node: The node to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + if not isinstance(node.op, self.get_operator_type()): + return None + + return node.operand diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py new file mode 100644 index 000000000..62785940e --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -0,0 +1,36 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides decorators operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/decorator.py +and integrated in Pynguin. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node + + +class DecoratorDeletion(MutationOperator): + """A class that mutates decorators by deleting them.""" + + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: # noqa: N802 + """Mutate a function definition by deleting its decorators. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the decorators should not be mutated. + """ + if not node.decorator_list: + return None + + mutated_node = copy_node(node) + mutated_node.decorator_list = [] + return mutated_node diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py new file mode 100644 index 000000000..14da8a2a4 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -0,0 +1,90 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides exception operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/exception.py +and integrated in Pynguin. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + + +def replace_exception_handler( + exception_handler: ast.ExceptHandler, + body: list[ast.stmt], +) -> ast.ExceptHandler: + """Replace an exception handler with a new body. + + Args: + exception_handler: The exception handler to replace. + body: The new body. + + Returns: + The new exception handler. + """ + return ast.ExceptHandler( + type=exception_handler.type, + name=exception_handler.name, + lineno=exception_handler.lineno, + body=body, + ) + + +class ExceptionHandlerDeletion(MutationOperator): + """A class that mutates exception handlers by deleting them.""" + + def mutate_ExceptHandler( # noqa: N802 + self, node: ast.ExceptHandler + ) -> ast.ExceptHandler | None: + """Mutate an exception handler by deleting it. + + Args: + node: The exception handler to mutate. + + Returns: + The mutated node, or None if the exception handler should not be mutated. + """ + if not node.body: + return None + + first_statement = node.body[0] + + if isinstance(first_statement, ast.Raise): + return None + + return replace_exception_handler( + node, [ast.Raise(lineno=first_statement.lineno)] + ) + + +class ExceptionSwallowing(MutationOperator): + """A class that mutates exception handlers by ignoring the caught exception.""" + + def mutate_ExceptHandler( # noqa: N802 + self, node: ast.ExceptHandler + ) -> ast.ExceptHandler | None: + """Mutate an exception handler by ignoring the caught exception. + + Args: + node: The exception handler to mutate. + + Returns: + The mutated node, or None if the exception handler should not be mutated. + """ + if not node.body: + return None + + first_statement = node.body[0] + + if len(node.body) == 1 and isinstance(first_statement, ast.Pass): + return None + + return replace_exception_handler( + node, [ast.Pass(lineno=first_statement.lineno)] + ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py new file mode 100644 index 000000000..3795a01a8 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -0,0 +1,391 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides inheritance operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py +and integrated in Pynguin. +""" + +import ast +import functools + +from collections.abc import Iterable +from typing import Any +from typing import cast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node +from pynguin.assertion.mutation_analysis.operators.base import set_lineno +from pynguin.assertion.mutation_analysis.operators.base import shift_lines +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer + + +def getattr_rec(obj: object, attr: Iterable[str]) -> Any: + """Get an attribute recursively. + + Args: + obj: The object to get the attribute from. + attr: The attribute to get. + + Returns: + The attribute. + """ + return functools.reduce(getattr, attr, obj) + + +class AbstractOverriddenElementModification(MutationOperator): + """An abstract class that provides a method to check if an element is overridden.""" + + def is_overridden(self, node: ast.AST, name: str) -> bool | None: + """Check if a method is overridden. + + Args: + node: The node to check. + name: The name of the method to check. + + Returns: + True if the method is overridden, False if it is not, None on error. + """ + parent: ast.AST = node.parent # type: ignore[attr-defined] + + if not isinstance(parent, ast.ClassDef) or not isinstance( + node, ast.FunctionDef | ast.AsyncFunctionDef | ast.Assign + ): + return None + + parent_names: list[str] = [] + + while parent is not None: + if not isinstance(parent, ast.Module): + parent_names.append(parent.name) # type: ignore[attr-defined] + if not isinstance(parent, ast.ClassDef) and not isinstance( + parent, ast.Module + ): + return None + parent = parent.parent # type: ignore[attr-defined,union-attr] + + try: + klass = getattr_rec(self.module, reversed(parent_names)) + except AttributeError: + return None + + return any(hasattr(base_klass, name) for base_klass in type.mro(klass)[1:-1]) + + +class HidingVariableDeletion(AbstractOverriddenElementModification): + """A class that mutates hiding variables by deleting them.""" + + def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: # noqa: N802 + """Mutate an assignment by deleting a hiding variable. + + Args: + node: The assignment to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + if len(node.targets) != 1: + return None + + first_expression = node.targets[0] + + if isinstance(first_expression, ast.Name): + overridden = self.is_overridden(node, first_expression.id) + + if overridden is None or not overridden: + return None + + return ast.Pass() + + if isinstance(first_expression, ast.Tuple) and isinstance( + node.value, ast.Tuple + ): + return self.mutate_unpack(node) + + return None + + def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: + """Mutate an assignment by deleting a hiding variable in an unpacking. + + Args: + node: The assignment to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + if not node.targets: + return None + + mutated_node = copy_node(node) + + target = cast(ast.List | ast.Tuple | ast.Set, mutated_node.targets[0]) + value = cast(ast.List | ast.Tuple | ast.Set, mutated_node.value) + + new_targets: list[ast.expr] = [] + new_values: list[ast.expr] = [] + for target_element, value_element in zip(target.elts, value.elts, strict=False): + if not isinstance(target_element, ast.Name) or not isinstance( + value_element, ast.expr + ): + continue + + overridden = self.is_overridden(mutated_node, target_element.id) + + if overridden is None: + return None + + if not overridden: + new_targets.append(target_element) + new_values.append(value_element) + + if len(new_targets) == len(target.elts): + return None + + if not new_targets: + return ast.Pass() + if len(new_targets) == 1 and len(new_values) == 1: + mutated_node.targets = new_targets + mutated_node.value = new_values[0] + return mutated_node + target.elts = new_targets + value.elts = new_values + return mutated_node + + +def is_super_call(node: ast.FunctionDef, stmt: ast.stmt) -> bool: + """Check if a statement is a super call. + + Args: + node: The function definition to check. + stmt: The statement to check. + + Returns: + True if the statement is a super call, False otherwise. + """ + return ( + isinstance(stmt, ast.Expr) + and isinstance(stmt.value, ast.Call) + and isinstance(stmt.value.func, ast.Attribute) + and isinstance(stmt.value.func.value, ast.Call) + and isinstance(stmt.value.func.value.func, ast.Name) + and stmt.value.func.value.func.id == "super" + and stmt.value.func.attr == node.name + ) + + +def get_super_call(node: ast.FunctionDef) -> tuple[int, ast.stmt] | None: + """Get the super call from a function definition. + + Args: + node: The function definition to get the super call from. + + Returns: + The index and the statement of the super call, or None if it does not exist. + """ + for index, stmt in enumerate(node.body): + if is_super_call(node, stmt): + return index, stmt + return None + + +class AbstractSuperCallingModification(MutationOperator): + """A class that provides methods to mutate super calls.""" + + def should_mutate(self, node: ast.FunctionDef) -> bool: + """Check if the node should be mutated. + + Args: + node: The node to check. + + Returns: + True if the node should be mutated, False otherwise. + """ + parent = node.parent # type: ignore[attr-defined] + return isinstance(parent, ast.ClassDef) + + +class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): + """A class that mutates the position of the super call in an overridden method.""" + + def should_mutate(self, node: ast.FunctionDef) -> bool: # noqa: D102 + return super().should_mutate(node) and len(node.body) > 1 + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate the position of the super call in an overridden method. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + if not self.should_mutate(node) or not node.body: + return None + + mutated_node = copy_node(node) + + super_call = get_super_call(mutated_node) + + if super_call is None: + return None + + index, statement = super_call + + del mutated_node.body[index] + + if index == 0: + set_lineno(statement, mutated_node.body[-1].lineno) + shift_lines(mutated_node.body, -1) + mutated_node.body.append(statement) + else: + set_lineno(statement, mutated_node.body[0].lineno) + shift_lines(mutated_node.body, 1) + mutated_node.body.insert(0, statement) + + return mutated_node + + +class OverridingMethodDeletion(AbstractOverriddenElementModification): + """A class that mutates overriding methods by deleting them.""" + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.Pass | None: + """Mutate a function definition by deleting it. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + overridden = self.is_overridden(node, node.name) + + if overridden is None or not overridden: + return None + + return ast.Pass() + + +class SuperCallingDeletion(AbstractSuperCallingModification): + """A class that mutates super calls by deleting them.""" + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate a function definition by deleting the super call. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + if not self.should_mutate(node) or not node.body: + return None + + mutated_node = copy_node(node) + + super_call = get_super_call(mutated_node) + + if super_call is None: + return None + + index, _ = super_call + + mutated_node.body[index] = ast.Pass(lineno=mutated_node.body[index].lineno) + + return mutated_node + + +class SuperCallingInsert( + AbstractSuperCallingModification, AbstractOverriddenElementModification +): + """A class that mutates super calls by inserting them.""" + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate a function definition by inserting the super call. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + overridden = self.is_overridden(node, node.name) + + if ( + not self.should_mutate(node) + or not node.body + or overridden is None + or not overridden + ): + return None + + mutated_node = copy_node(node) + + super_call = get_super_call(mutated_node) + + if super_call is not None: + return None + + mutated_node.body.insert(0, self._create_super_call(mutated_node)) + shift_lines(mutated_node.body[1:], 1) + + return mutated_node + + def _create_super_call(self, node: ast.FunctionDef) -> ast.Expr: + module = ParentNodeTransformer.create_ast(f"super().{node.name}()") + + assert module.body + + super_call = module.body[0] + + assert isinstance(super_call, ast.Expr) + + super_call_value = super_call.value + + assert isinstance(super_call_value, ast.Call) + + for arg in node.args.args[1 : -len(node.args.defaults) or None]: + super_call_value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) + + for arg, default in zip( + node.args.args[-len(node.args.defaults) :], node.args.defaults, strict=False + ): + super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + + for arg, default in zip( # type: ignore[assignment] + node.args.kwonlyargs, node.args.kw_defaults, strict=False + ): + super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + + if node.args.vararg is not None: + self._add_vararg_to_super_call(super_call_value, node.args.vararg) + + if node.args.kwarg is not None: + self._add_kwarg_to_super_call(super_call_value, node.args.kwarg) + + set_lineno(super_call, node.body[0].lineno) + + return super_call + + @staticmethod + def _add_kwarg_to_super_call(super_call_value: ast.Call, kwarg: ast.arg) -> None: + super_call_value.keywords.append( + ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load())) + ) + + @staticmethod + def _add_vararg_to_super_call(super_call_value: ast.Call, vararg: ast.arg) -> None: + super_call_value.args.append( + ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load())) + ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py new file mode 100644 index 000000000..877cea091 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -0,0 +1,301 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides logical operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/logical.py +and integrated in Pynguin. +""" + +import ast + +from typing import TypeVar + +from pynguin.assertion.mutation_analysis.operators.base import ( + AbstractUnaryOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node + + +class ConditionalOperatorDeletion(AbstractUnaryOperatorDeletion): + """A class that mutates conditional operators by deleting them.""" + + def get_operator_type(self) -> type: + """Get the operator type.""" + return ast.Not + + def mutate_NotIn(self, node: ast.NotIn) -> ast.In: # noqa: N802 + """Mutate a NotIn operator to an In operator. + + Args: + node: The NotIn operator to mutate. + + Returns: + The mutated operator. + """ + return ast.In() + + +T = TypeVar("T", ast.If, ast.While) + + +def negate_test(node: T) -> T: + """Negate the test of a node. + + Args: + node: The node to negate. + + Returns: + The mutated node. + """ + mutated_node = copy_node(node) + not_node = ast.UnaryOp(op=ast.Not(), operand=mutated_node.test) + mutated_node.test = not_node + return mutated_node + + +class ConditionalOperatorInsertion(MutationOperator): + """A class that mutates conditional operators by inserting them.""" + + def mutate_While(self, node: ast.While) -> ast.While: # noqa: N802 + """Mutate a While node by negating its test. + + Args: + node: The While node to mutate. + + Returns: + The mutated node. + """ + return negate_test(node) + + def mutate_If(self, node: ast.If) -> ast.If: # noqa: N802 + """Mutate an If node by negating its test. + + Args: + node: The If node to mutate. + + Returns: + The mutated node. + """ + return negate_test(node) + + def mutate_In(self, node: ast.In) -> ast.NotIn: # noqa: N802 + """Mutate an In operator to a NotIn operator. + + Args: + node: The In operator to mutate. + + Returns: + The mutated operator. + """ + return ast.NotIn() + + +class LogicalConnectorReplacement(MutationOperator): + """A class that mutates logical connectors by replacing them.""" + + def mutate_And(self, node: ast.And) -> ast.Or: # noqa: N802 + """Mutate an And operator to an Or operator. + + Args: + node: The And operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Or() + + def mutate_Or(self, node: ast.Or) -> ast.And: # noqa: N802 + """Mutate an Or operator to an And operator. + + Args: + node: The Or operator to mutate. + + Returns: + The mutated operator. + """ + return ast.And() + + +class LogicalOperatorDeletion(AbstractUnaryOperatorDeletion): + """A class that mutates logical operators by deleting them.""" + + def get_operator_type(self) -> type: # noqa: D102 + return ast.Invert + + +class LogicalOperatorReplacement(MutationOperator): + """A class that mutates logical operators by replacing them.""" + + def mutate_BitAnd(self, node: ast.BitAnd) -> ast.BitOr: # noqa: N802 + """Mutate a BitAnd operator to a BitOr operator. + + Args: + node: The BitAnd operator to mutate. + + Returns: + The mutated operator. + """ + return ast.BitOr() + + def mutate_BitOr(self, node: ast.BitOr) -> ast.BitAnd: # noqa: N802 + """Mutate a BitOr operator to a BitAnd operator. + + Args: + node: The BitOr operator to mutate. + + Returns: + The mutated operator. + """ + return ast.BitAnd() + + def mutate_BitXor(self, node: ast.BitXor) -> ast.BitAnd: # noqa: N802 + """Mutate a BitXor operator to a BitAnd operator. + + Args: + node: The BitXor operator to mutate. + + Returns: + The mutated operator. + """ + return ast.BitAnd() + + def mutate_LShift(self, node: ast.LShift) -> ast.RShift: # noqa: N802 + """Mutate a LShift operator to a RShift operator. + + Args: + node: The LShift operator to mutate. + + Returns: + The mutated operator. + """ + return ast.RShift() + + def mutate_RShift(self, node: ast.RShift) -> ast.LShift: # noqa: N802 + """Mutate a RShift operator to a LShift operator. + + Args: + node: The RShift operator to mutate. + + Returns: + The mutated operator. + """ + return ast.LShift() + + +class RelationalOperatorReplacement(MutationOperator): + """A class that mutates relational operators by replacing them.""" + + def mutate_Lt(self, node: ast.Lt) -> ast.Gt: # noqa: N802 + """Mutate a Lt operator to a Gt operator. + + Args: + node: The Lt operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Gt() + + def mutate_Lt_to_LtE(self, node: ast.Lt) -> ast.LtE: # noqa: N802 + """Mutate a Lt operator to a LtE operator. + + Args: + node: The Lt operator to mutate. + + Returns: + The mutated operator. + """ + return ast.LtE() + + def mutate_Gt(self, node: ast.Gt) -> ast.Lt: # noqa: N802 + """Mutate a Gt operator to a Lt operator. + + Args: + node: The Gt operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Lt() + + def mutate_Gt_to_GtE(self, node: ast.Gt) -> ast.GtE: # noqa: N802 + """Mutate a Gt operator to a GtE operator. + + Args: + node: The Gt operator to mutate. + + Returns: + The mutated operator. + """ + return ast.GtE() + + def mutate_LtE(self, node: ast.LtE) -> ast.GtE: # noqa: N802 + """Mutate a LtE operator to a GtE operator. + + Args: + node: The LtE operator to mutate. + + Returns: + The mutated operator. + """ + return ast.GtE() + + def mutate_LtE_to_Lt(self, node: ast.LtE) -> ast.Lt: # noqa: N802 + """Mutate a LtE operator to a Lt operator. + + Args: + node: The LtE operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Lt() + + def mutate_GtE(self, node: ast.GtE) -> ast.LtE: # noqa: N802 + """Mutate a GtE operator to a LtE operator. + + Args: + node: The GtE operator to mutate. + + Returns: + The mutated operator. + """ + return ast.LtE() + + def mutate_GtE_to_Gt(self, node: ast.GtE) -> ast.Gt: # noqa: N802 + """Mutate a GtE operator to a Gt operator. + + Args: + node: The GtE operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Gt() + + def mutate_Eq(self, node: ast.Eq) -> ast.NotEq: # noqa: N802 + """Mutate an Eq operator to a NotEq operator. + + Args: + node: The Eq operator to mutate. + + Returns: + The mutated operator. + """ + return ast.NotEq() + + def mutate_NotEq(self, node: ast.NotEq) -> ast.Eq: # noqa: N802 + """Mutate a NotEq operator to an Eq operator. + + Args: + node: The NotEq operator to mutate. + + Returns: + The mutated operator. + """ + return ast.Eq() diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py new file mode 100644 index 000000000..55a3ad197 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -0,0 +1,130 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides loop operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/loop.py +and integrated in Pynguin. +""" + +import ast +import typing + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node + + +T = typing.TypeVar("T", ast.For, ast.While) + + +def one_iteration(node: T) -> T | None: + """Mutate a loop to have only one iteration. + + Args: + node: The loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + if not node.body: + return None + + mutated_node = copy_node(node) + mutated_node.body.append(ast.Break(lineno=mutated_node.body[-1].lineno + 1)) + return mutated_node + + +def zero_iteration(node: T) -> T | None: + """Mutate a loop to have zero iterations. + + Args: + node: The loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + if not node.body: + return None + + mutated_node = copy_node(node) + mutated_node.body = [ast.Break(lineno=mutated_node.body[0].lineno)] + return mutated_node + + +class OneIterationLoop(MutationOperator): + """A class that mutates loops to have only one iteration.""" + + def mutate_For(self, node: ast.For) -> ast.For | None: # noqa: N802 + """Mutate a For loop to have only one iteration. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + return one_iteration(node) + + def mutate_While(self, node: ast.While) -> ast.While | None: # noqa: N802 + """Mutate a While loop to have only one iteration. + + Args: + node: The While loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + return one_iteration(node) + + +class ReverseIterationLoop(MutationOperator): + """A class that mutates loops by reversing their iteration.""" + + def mutate_For(self, node: ast.For) -> ast.For: # noqa: N802 + """Mutate a For loop by reversing its iteration. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop. + """ + mutated_node = copy_node(node) + old_iter = mutated_node.iter + mutated_node.iter = ast.Call( + func=ast.Name(id=reversed.__name__, ctx=ast.Load()), + args=[old_iter], + keywords=[], + starargs=None, + kwargs=None, + ) + return mutated_node + + +class ZeroIterationLoop(MutationOperator): + """A class that mutates loops to have zero iterations.""" + + def mutate_For(self, node: ast.For) -> ast.For | None: # noqa: N802 + """Mutate a For loop to have zero iterations. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + return zero_iteration(node) + + def mutate_While(self, node: ast.While) -> ast.While | None: # noqa: N802 + """Mutate a While loop to have zero iterations. + + Args: + node: The While loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ + return zero_iteration(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py new file mode 100644 index 000000000..b4f28e768 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -0,0 +1,230 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides miscellaneous operators for mutation analysis. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/misc.py +and https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py +and integrated in Pynguin. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + AbstractArithmeticOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + + +def is_docstring(node: ast.AST) -> bool: + """Check if the given node is a docstring. + + Args: + node: The node to check. + + Returns: + True if the node is a docstring, False otherwise. + """ + if not isinstance(node, ast.Str): + return False + + expression_node: ast.AST = node.parent # type: ignore[attr-defined] + + if not isinstance(expression_node, ast.Expr): + return False + + def_node: ast.AST = expression_node.parent # type: ignore[attr-defined] + + return ( + isinstance(def_node, ast.FunctionDef | ast.ClassDef | ast.Module) + and def_node.body # type: ignore[return-value] + and def_node.body[0] == expression_node + ) + + +class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): + """A class that mutates assignment operators by replacing them.""" + + def should_mutate(self, node: ast.AST) -> bool: # noqa: D102 + parent = node.parent # type: ignore[attr-defined] + return isinstance(parent, ast.AugAssign) + + +class BreakContinueReplacement(MutationOperator): + """A class that mutates break and continue statements by replacing them.""" + + def mutate_Break(self, node: ast.Break) -> ast.Continue: # noqa: N802 + """Mutate a Break statement to a Continue statement. + + Args: + node: The Break statement to mutate. + + Returns: + The mutated statement. + """ + return ast.Continue() + + def mutate_Continue(self, node: ast.Continue) -> ast.Break: # noqa: N802 + """Mutate a Continue statement to a Break statement. + + Args: + node: The Continue statement to mutate. + + Returns: + The mutated statement. + """ + return ast.Break() + + +class ConstantReplacement(MutationOperator): + """A class that mutates constants by replacing them.""" + + FIRST_CONST_STRING = "mutpy" + SECOND_CONST_STRING = "python" + + def help_str(self, node: ast.Constant) -> str | None: + """Help function for mutating strings. + + Args: + node: The string to mutate. + + Returns: + The mutated string, or None if the string should not be mutated. + """ + if is_docstring(node): + return None + + if node.value == self.FIRST_CONST_STRING: + return self.SECOND_CONST_STRING + + return self.FIRST_CONST_STRING + + @staticmethod + def help_str_empty(node: ast.Constant) -> str | None: + """Help function for mutating empty strings. + + Args: + node: The string to mutate. + + Returns: + The mutated string, or None if the string should not be mutated. + """ + if not node.value or is_docstring(node): + return None + + return "" + + def mutate_Constant_num( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate a numeric constant by adding 1. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ + value = node.value + + if not isinstance(value, int | float) or isinstance(value, bool): + return None + + return ast.Constant(value + 1) + + def mutate_Constant_str( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate a string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ + if not isinstance(node.value, str): + return None + + new_value = self.help_str(node) + + if new_value is None: + return None + + return ast.Constant(new_value) + + def mutate_Constant_str_empty( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate an empty string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ + if not isinstance(node.value, str): + return None + + new_value = self.help_str_empty(node) + + if new_value is None: + return None + + return ast.Constant(new_value) + + +class SliceIndexRemove(MutationOperator): + """A class that mutates slice indices by removing them.""" + + def mutate_Slice_remove_lower( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the lower bound. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ + if node.lower is None: + return None + + return ast.Slice(lower=None, upper=node.upper, step=node.step) + + def mutate_Slice_remove_upper( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the upper bound. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ + if node.upper is None: + return None + + return ast.Slice(lower=node.lower, upper=None, step=node.step) + + def mutate_Slice_remove_step( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the step. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ + if node.step is None: + return None + + return ast.Slice(lower=node.lower, upper=node.upper, step=None) diff --git a/src/pynguin/assertion/mutation_analysis/strategies.py b/src/pynguin/assertion/mutation_analysis/strategies.py new file mode 100644 index 000000000..4c2d4aa7b --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/strategies.py @@ -0,0 +1,167 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides strategies for higher order mutations. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py +and integrated in Pynguin. +""" +from __future__ import annotations + +import abc + +from typing import TYPE_CHECKING + +from pynguin.utils import randomness + + +if TYPE_CHECKING: + from collections.abc import Generator + + from pynguin.assertion.mutation_analysis.operators.base import Mutation + + +def remove_bad_mutations( + mutations_to_apply: list[Mutation], + available_mutations: list[Mutation], + allow_same_operators: bool = True, # noqa: FBT001, FBT002 +) -> None: + """Remove bad mutations from the available mutations. + + Args: + mutations_to_apply: The mutations that are already selected. + available_mutations: The mutations that are available. + allow_same_operators: Whether the same operator should be allowed. + + Returns: + The list of available mutations without the bad mutations. + """ + for mutation_to_apply in mutations_to_apply: + for available_mutation in available_mutations.copy(): + if ( + mutation_to_apply.node == available_mutation.node + or mutation_to_apply.node in available_mutation.node.children # type: ignore[attr-defined] + or available_mutation.node in mutation_to_apply.node.children # type: ignore[attr-defined] + or ( + not allow_same_operators + and mutation_to_apply.operator == available_mutation.operator + ) + ): + available_mutations.remove(available_mutation) + + +class HOMStrategy(abc.ABC): + """A strategy for higher order mutations.""" + + def __init__(self, order: int = 2) -> None: + """Initialize the strategy. + + Args: + order: The order of the mutations. + """ + self.order = order + + @abc.abstractmethod + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + """Generate the mutations. + + Args: + mutations: The mutations to generate from. + + Returns: + A generator for the mutations. + """ + + +class FirstToLastHOMStrategy(HOMStrategy): + """A strategy that selects the first mutation and then the last one.""" + + def generate( # noqa: D102 + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + while mutations: + mutations_to_apply: list[Mutation] = [] + index = 0 + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(index) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + index = 0 if index == -1 else -1 + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class EachChoiceHOMStrategy(HOMStrategy): + """A strategy that selects the mutations in order.""" + + def generate( # noqa: D102 + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + while mutations: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class BetweenOperatorsHOMStrategy(HOMStrategy): + """A strategy that selects mutations between different operators.""" + + def generate( # noqa: D102 + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + usage = dict.fromkeys(mutations, 0) + not_used = mutations.copy() + while not_used: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + available_mutations.sort(key=lambda x: usage[x]) + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + if usage[mutation] == 0: + not_used.remove(mutation) + usage[mutation] += 1 + remove_bad_mutations( + mutations_to_apply, available_mutations, allow_same_operators=False + ) + yield mutations_to_apply + + +class RandomHOMStrategy(HOMStrategy): + """A strategy that selects mutations randomly.""" + + def __init__(self, order: int = 2) -> None: + """Initialize the strategy. + + Args: + order: The order of the mutations. + """ + super().__init__(order) + + def generate( # noqa: D102 + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + randomness.RNG.shuffle(mutations) + while mutations: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply diff --git a/src/pynguin/assertion/mutation_analysis/transformer.py b/src/pynguin/assertion/mutation_analysis/transformer.py new file mode 100644 index 000000000..97d4f7dd5 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/transformer.py @@ -0,0 +1,100 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides a transformer for modules ASTs. + +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py +and integrated in Pynguin. +""" + +import ast +import copy +import types + +from typing import TypeVar + + +def create_module(ast_node: ast.Module, module_name: str) -> types.ModuleType: + """Creates a module from an AST node. + + Args: + ast_node: The AST node. + module_name: The name of the module. + + Returns: + The created module. + """ + code = compile(ast_node, module_name, "exec") + module = types.ModuleType(module_name) + exec(code, module.__dict__) # noqa: S102 + return module + + +T = TypeVar("T", bound=ast.AST) + + +class ParentNodeTransformer(ast.NodeTransformer): + """A transformer that adds a parent attribute to each node of the AST.""" + + @classmethod + def create_ast(cls, code: str) -> ast.Module: + """Create an AST from a string. + + Args: + code: The code to parse. + + Returns: + The module node of the AST with the parent and children attributes set. + """ + return cls().visit(ast.parse(code)) + + def __init__(self) -> None: + """Initialize the transformer.""" + super().__init__() + self.parent: ast.AST | None = None + + def visit(self, node: T) -> T: + """Transform a node of the AST. + + Args: + node: The node to transform. + + Returns: + The transformed node. + """ + # Copy the node because an optimisation of the AST makes it + # reuse the same node at multiple places in the tree to + # improve memory usage. It would break our goal to create a + # tree with a single parent for each node if we don't copy. + if hasattr(node, "parent"): + node = copy.copy(node) + if hasattr(node, "lineno"): + delattr(node, "lineno") + + node.parent = self.parent # type: ignore[attr-defined] + node.children = set() # type: ignore[attr-defined] + + parent_save = self.parent + self.parent = node + + # Visit the children of the node and discard the result + # as it returns the same node with the children modified. + super().visit(node) + + self.parent = parent_save + + # Add all the ancestors of the node to the children list + # of the parent if it exists. This is done here so that + # the tree has been fully traversed before adding the children. + if self.parent is not None: + parent_children: set[ast.AST] = self.parent.children # type: ignore[attr-defined] + + parent_children.add(node) + + node_children: set[ast.AST] = node.children # type: ignore[attr-defined] + parent_children.update(node_children) + + return node diff --git a/src/pynguin/generator.py b/src/pynguin/generator.py index 24f647159..32f44f36b 100644 --- a/src/pynguin/generator.py +++ b/src/pynguin/generator.py @@ -21,6 +21,7 @@ import datetime import enum import importlib +import inspect import json import logging import sys @@ -31,6 +32,9 @@ from typing import cast import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo +import pynguin.assertion.mutation_analysis.strategies as ms import pynguin.configuration as config import pynguin.ga.chromosome as chrom import pynguin.ga.chromosomevisitor as cv @@ -47,6 +51,7 @@ from pynguin.analyses.constants import RestrictedConstantPool from pynguin.analyses.constants import collect_static_constants from pynguin.analyses.module import generate_test_cluster +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.instrumentation.machinery import InstrumentationFinder from pynguin.instrumentation.machinery import install_import_hook from pynguin.slicer.statementslicingobserver import StatementSlicingObserver @@ -55,6 +60,7 @@ from pynguin.testcase.execution import ExecutionTracer from pynguin.testcase.execution import TestCaseExecutor from pynguin.utils import randomness +from pynguin.utils.exceptions import ConfigurationException from pynguin.utils.report import get_coverage_report from pynguin.utils.report import render_coverage_report from pynguin.utils.report import render_xml_coverage_report @@ -62,7 +68,10 @@ if TYPE_CHECKING: + from collections.abc import Callable + from pynguin.analyses.module import ModuleTestCluster + from pynguin.assertion.mutation_analysis.operators.base import MutationOperator from pynguin.ga.algorithms.generationalgorithm import GenerationAlgorithm @@ -591,14 +600,71 @@ def _minimize_assertions(generation_result: tsc.TestSuiteChromosome): ) +_strategies: dict[config.MutationStrategy, Callable[[int], ms.HOMStrategy]] = { + config.MutationStrategy.FIRST_TO_LAST: ms.FirstToLastHOMStrategy, + config.MutationStrategy.BETWEEN_OPERATORS: ms.BetweenOperatorsHOMStrategy, + config.MutationStrategy.RANDOM: ms.RandomHOMStrategy, + config.MutationStrategy.EACH_CHOICE: ms.EachChoiceHOMStrategy, +} + + +def _setup_mutant_generator() -> mu.Mutator: + operators: list[type[MutationOperator]] = [ + *mo.standard_operators, + *mo.experimental_operators, + ] + + mutation_strategy = config.configuration.test_case_output.mutation_strategy + + if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: + return mu.FirstOrderMutator(operators) + + order = config.configuration.test_case_output.mutation_order + + if order <= 0: + raise ConfigurationException("Mutation order should be > 0.") + + if mutation_strategy in _strategies: + hom_strategy = _strategies[mutation_strategy](order) + return mu.HighOrderMutator(operators, hom_strategy=hom_strategy) + + raise ConfigurationException("No suitable mutation strategy found.") + + +def _setup_mutation_analysis_assertion_generator( + executor: TestCaseExecutor, +) -> ag.MutationAnalysisAssertionGenerator: + _LOGGER.info("Setup mutation generator") + mutant_generator = _setup_mutant_generator() + + _LOGGER.info("Import module %s", config.configuration.module_name) + module = importlib.import_module(config.configuration.module_name) + + _LOGGER.info("Build AST for %s", module.__name__) + executor.tracer.current_thread_identifier = threading.current_thread().ident + module_source_code = inspect.getsource(module) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + + _LOGGER.info("Mutate module %s", module.__name__) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) + assertion_generator = ag.MutationAnalysisAssertionGenerator( + executor, mutation_controller + ) + + _LOGGER.info("Generated %d mutants", mutation_controller.mutant_count()) + return assertion_generator + + def _generate_assertions(executor, generation_result): ass_gen = config.configuration.test_case_output.assertion_generation if ass_gen != config.AssertionGenerator.NONE: _LOGGER.info("Start generating assertions") + generator: cv.ChromosomeVisitor if ass_gen == config.AssertionGenerator.MUTATION_ANALYSIS: - generator: cv.ChromosomeVisitor = ag.MutationAnalysisAssertionGenerator( - executor - ) + generator = _setup_mutation_analysis_assertion_generator(executor) else: generator = ag.AssertionGenerator(executor) generation_result.accept(generator) diff --git a/tests/assertion/mutation_analysis/operators/__init__.py b/tests/assertion/mutation_analysis/operators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/assertion/mutation_analysis/operators/test_arithmetic.py b/tests/assertion/mutation_analysis/operators/test_arithmetic.py new file mode 100644 index 000000000..c98a8426b --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_arithmetic.py @@ -0,0 +1,291 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from tests.testutils import assert_mutation + + +def test_usub_deletion(): + assert_mutation( + ArithmeticOperatorDeletion, + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_uadd_deletion(): + assert_mutation( + ArithmeticOperatorDeletion, + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_add_to_sub_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x + 2 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x + 2 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x - 2 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + }, + ) + + +def test_sub_to_add_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x - 2 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x - 2 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x + 2 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + }, + ) + + +def test_mult_to_div_and_pow_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ): ("mutate_Mult_to_Div", ast.Mult, ast.Div), + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ): ("mutate_Mult_to_FloorDiv", ast.Mult, ast.FloorDiv), + inspect.cleandoc( + """ + x = 0 + y = x ** 1 + """ + ): ("mutate_Mult_to_Pow", ast.Mult, ast.Pow), + }, + ) + + +def test_div_to_mult_and_floordiv_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Div_to_Mult", ast.Div, ast.Mult), + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ): ("mutate_Div_to_FloorDiv", ast.Div, ast.FloorDiv), + }, + ) + + +def test_floor_div_to_mult_and_div_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_FloorDiv_to_Mult", ast.FloorDiv, ast.Mult), + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ): ("mutate_FloorDiv_to_Div", ast.FloorDiv, ast.Div), + }, + ) + + +def test_mod_to_mult_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x % 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Mod", ast.Mod, ast.Mult), + }, + ) + + +def test_pow_to_mult_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x ** 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Pow", ast.Pow, ast.Mult), + }, + ) + + +def test_augmented_assign_ignore(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ), + {}, + ) + + +def test_usub_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ): ("mutate_USub", ast.USub, ast.UAdd), + }, + ) + + +def test_uadd_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ): ("mutate_UAdd", ast.UAdd, ast.USub), + }, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_decorator.py b/tests/assertion/mutation_analysis/operators/test_decorator.py new file mode 100644 index 000000000..9c8cac7fc --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_decorator.py @@ -0,0 +1,89 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.decorator import DecoratorDeletion +from tests.testutils import assert_mutation + + +def test_single_decorator_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + import atexit + + @atexit.register + def foo(): + pass + """ + ), + { + inspect.cleandoc( + """ + import atexit + + def foo(): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_multiple_decorators_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + from abc import ABC, abstractmethod + + class Foo(ABC): + @classmethod + @abstractmethod + def bar(): + pass + """ + ), + { + inspect.cleandoc( + """ + from abc import ABC, abstractmethod + + class Foo(ABC): + def bar(): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_decorator_with_arguments_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + from functools import lru_cache + + @lru_cache(maxsize=128) + def foo(x: int) -> int: + return x + """ + ), + { + inspect.cleandoc( + """ + from functools import lru_cache + + def foo(x: int) -> int: + return x + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_exception.py b/tests/assertion/mutation_analysis/operators/test_exception.py new file mode 100644 index 000000000..a8a0c2a3b --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_exception.py @@ -0,0 +1,130 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.exception import ( + ExceptionHandlerDeletion, +) +from pynguin.assertion.mutation_analysis.operators.exception import ExceptionSwallowing +from tests.testutils import assert_mutation + + +def test_exception_handler_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_two_exception_handler_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + except ZeroDivisionError: + pass + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + except ZeroDivisionError: + pass + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + except ZeroDivisionError: + raise + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_raise_no_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ), + {}, + ) + + +def test_exception_swallowing(): + assert_mutation( + ExceptionSwallowing, + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_exception_no_swallowing_when_pass(): + assert_mutation( + ExceptionSwallowing, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ), + {}, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_inheritance.py b/tests/assertion/mutation_analysis/operators/test_inheritance.py new file mode 100644 index 000000000..ee99d3e60 --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_inheritance.py @@ -0,0 +1,334 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + HidingVariableDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverriddenMethodCallingPositionChange, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverridingMethodDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + SuperCallingDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import SuperCallingInsert +from tests.testutils import assert_mutation + + +def test_hiding_variable_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Bar(Foo): + x = 2 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Bar(Foo): + pass + """ + ): ("mutate_Assign", ast.Assign, ast.Pass), + }, + ) + + +def test_hiding_variable_in_2_elements_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + x, y = 2, 3 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + y = 3 + """ + ): ("mutate_Assign", ast.Assign, ast.Assign), + }, + ) + + +def test_hiding_variable_in_3_elements_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + x, y, z = 2, 3, 4 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + y, z = 3, 4 + """ + ): ("mutate_Assign", ast.Assign, ast.Assign), + }, + ) + + +def test_hiding_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x, y = 1, 2 + class Baz(Foo): + x, y = 3, 4 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x, y = 1, 2 + class Baz(Foo): + pass + """ + ): ("mutate_Assign", ast.Assign, ast.Pass), + }, + ) + + +def test_super_call_position_change_from_first_to_last(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + super().baz(x) + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_position_change_from_last_to_first(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + super().baz(x) + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_position_ignore_when_only_one_statement(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + """ + ), + {}, + ) + + +def test_overriding_method_deletion(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_overriding_method_deletion_in_inner_class(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + class Outer: + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Outer: + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_overriding_method_deletion_when_base_class_in_another_module(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + from ast import NodeTransformer + + class Foo(NodeTransformer): + def visit(self, node): + pass + """ + ), + { + inspect.cleandoc( + """ + from ast import NodeTransformer + + class Foo(NodeTransformer): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_super_call_deletion(): + assert_mutation( + SuperCallingDeletion, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_insertion(): + assert_mutation( + SuperCallingInsert, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_logical.py b/tests/assertion/mutation_analysis/operators/test_logical.py new file mode 100644 index 000000000..5eab3deae --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_logical.py @@ -0,0 +1,497 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorInsertion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalConnectorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + RelationalOperatorReplacement, +) +from tests.testutils import assert_mutation + + +def test_not_operator_negation(): + assert_mutation( + ConditionalOperatorDeletion, + inspect.cleandoc( + """ + x = True + y = not x + """ + ), + { + inspect.cleandoc( + """ + x = True + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_not_in_operator_negation(): + assert_mutation( + ConditionalOperatorDeletion, + inspect.cleandoc( + """ + x = 1 not in [1, 2, 3] + """ + ), + { + inspect.cleandoc( + """ + x = 1 in [1, 2, 3] + """ + ): ("mutate_NotIn", ast.NotIn, ast.In), + }, + ) + + +def test_while_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + while x < 10: + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + while not (x < 10): + x += 1 + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_if_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + if not (x < 10): + x += 1 + """ + ): ("mutate_If", ast.If, ast.If), + }, + ) + + +def test_if_elif_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + elif x < 20: + x += 2 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + if not (x < 10): + x += 1 + elif x < 20: + x += 2 + """ + ): ("mutate_If", ast.If, ast.If), + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + elif not (x < 20): + x += 2 + """ + ): ("mutate_If", ast.If, ast.If), + }, + ) + + +def test_in_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + y = 1 in [1, 2, 3] + """ + ), + { + inspect.cleandoc( + """ + y = 1 not in [1, 2, 3] + """ + ): ("mutate_In", ast.In, ast.NotIn), + }, + ) + + +def test_and_to_or_replacement(): + assert_mutation( + LogicalConnectorReplacement, + inspect.cleandoc( + """ + x = True + y = False + z = x and y + """ + ), + { + inspect.cleandoc( + """ + x = True + y = False + z = x or y + """ + ): ("mutate_And", ast.And, ast.Or), + }, + ) + + +def test_or_to_and_replacement(): + assert_mutation( + LogicalConnectorReplacement, + inspect.cleandoc( + """ + x = True + y = False + z = x or y + """ + ), + { + inspect.cleandoc( + """ + x = True + y = False + z = x and y + """ + ): ("mutate_Or", ast.Or, ast.And), + }, + ) + + +def test_logical_operator_deletion(): + assert_mutation( + LogicalOperatorDeletion, + inspect.cleandoc( + """ + x = True + y = ~x + """ + ), + { + inspect.cleandoc( + """ + x = True + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_bin_and_to_bin_or_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x | y + """ + ): ("mutate_BitAnd", ast.BitAnd, ast.BitOr), + }, + ) + + +def test_bin_or_to_bin_and_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x | y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ): ("mutate_BitOr", ast.BitOr, ast.BitAnd), + }, + ) + + +def test_bin_xor_to_bin_and_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x ^ y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ): ("mutate_BitXor", ast.BitXor, ast.BitAnd), + }, + ) + + +def test_bin_lshift_to_bin_rshift_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x << y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >> y + """ + ): ("mutate_LShift", ast.LShift, ast.RShift), + }, + ) + + +def test_bin_rshift_to_bin_lshift_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >> y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x << y + """ + ): ("mutate_RShift", ast.RShift, ast.LShift), + }, + ) + + +def test_lt_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ): ("mutate_Lt", ast.Lt, ast.Gt), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ): ("mutate_Lt_to_LtE", ast.Lt, ast.LtE), + }, + ) + + +def test_gt_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ): ("mutate_Gt", ast.Gt, ast.Lt), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ): ("mutate_Gt_to_GtE", ast.Gt, ast.GtE), + }, + ) + + +def test_lte_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ): ("mutate_LtE", ast.LtE, ast.GtE), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ): ("mutate_LtE_to_Lt", ast.LtE, ast.Lt), + }, + ) + + +def test_gte_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ): ("mutate_GtE", ast.GtE, ast.LtE), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ): ("mutate_GtE_to_Gt", ast.GtE, ast.Gt), + }, + ) + + +def test_eq_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x == y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x != y + """ + ): ("mutate_Eq", ast.Eq, ast.NotEq), + }, + ) + + +def test_not_eq_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x != y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x == y + """ + ): ("mutate_NotEq", ast.NotEq, ast.Eq), + }, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_loop.py b/tests/assertion/mutation_analysis/operators/test_loop.py new file mode 100644 index 000000000..2195c5aad --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_loop.py @@ -0,0 +1,177 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.loop import OneIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ReverseIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ZeroIterationLoop +from tests.testutils import assert_mutation + + +def test_one_iteration_for_loop_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + pass + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_one_iteration_while_loop_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_one_iteration_multiple_loops_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + pass + break + i = 0 + while i < 10: + i += 1 + """ + ): ("mutate_For", ast.For, ast.For), + inspect.cleandoc( + """ + for x in range(10): + pass + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_zero_iteration_for_loop_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_zero_iteration_while_loop_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_zero_iteration_for_loop_multiple_statements_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_reverse_iteration_for_loop(): + assert_mutation( + ReverseIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in reversed(range(10)): + pass + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) diff --git a/tests/assertion/mutation_analysis/operators/test_misc.py b/tests/assertion/mutation_analysis/operators/test_misc.py new file mode 100644 index 000000000..ce8822833 --- /dev/null +++ b/tests/assertion/mutation_analysis/operators/test_misc.py @@ -0,0 +1,258 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.misc import ( + AssignmentOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.misc import BreakContinueReplacement +from pynguin.assertion.mutation_analysis.operators.misc import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.misc import SliceIndexRemove +from tests.testutils import assert_mutation + + +def test_add_to_sub_replacement(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + x -= 1 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + }, + ) + + +def test_sub_to_add_replacement(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x -= 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + }, + ) + + +def test_normal_use_ignore(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x = x + 1 + """ + ), + {}, + ) + + +def test_break_to_continue_replacement(): + assert_mutation( + BreakContinueReplacement, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + continue + """ + ): ("mutate_Break", ast.Break, ast.Continue), + }, + ) + + +def test_continue_to_break_replacement(): + assert_mutation( + BreakContinueReplacement, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + continue + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_Continue", ast.Continue, ast.Break), + }, + ) + + +def test_numbers_increase(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + x = 1 - 2 + 99 + """ + ), + { + inspect.cleandoc( + """ + x = 2 - 2 + 99 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 1 - 3 + 99 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 1 - 2 + 100 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + }, + ) + + +def test_string_replacement(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + x = 'x' + y = '' + 'y' + """ + ), + { + inspect.cleandoc( + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' + y = '' + 'y' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = 'x' + y = '{ConstantReplacement.FIRST_CONST_STRING}' + 'y' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = 'x' + y = '' + '{ConstantReplacement.FIRST_CONST_STRING}' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = '' + y = '' + 'y' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 'x' + y = '' + '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 'x' + y = '' + '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + }, + ) + + +def test_first_constant_string_replacement(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' + """ + ), + { + inspect.cleandoc( + f""" + x = '{ConstantReplacement.SECOND_CONST_STRING}' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + }, + ) + + +def test_docstring_ignore(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + def foo(): + '''Docstring''' + pass + """ + ), + {}, + ) + + +def test_slice_index_replacement(): + assert_mutation( + SliceIndexRemove, + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[1:2] + """ + ), + { + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[1:] + """ + ): ("mutate_Slice_remove_upper", ast.Slice, ast.Slice), + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[:2] + """ + ): ("mutate_Slice_remove_lower", ast.Slice, ast.Slice), + }, + ) diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py new file mode 100644 index 000000000..eb838d968 --- /dev/null +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -0,0 +1,41 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import importlib +import inspect +import threading + +import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo +import pynguin.configuration as config + +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +from pynguin.testcase.execution import ExecutionTracer + + +def test_create_mutants(): + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) + + module = importlib.import_module("tests.fixtures.examples.triangle") + module_source_code = inspect.getsource(module) + + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) + config.configuration.seeding.seed = 42 + + mutation_controller.tracer.current_thread_identifier = ( + threading.current_thread().ident + ) + mutations = tuple(mutation_controller.create_mutants()) + mutant_count = mutation_controller.mutant_count() + assert len(mutations) == 14 + assert mutant_count == 14 diff --git a/tests/assertion/mutation_analysis/test_mutationadapter.py b/tests/assertion/mutation_analysis/test_mutationadapter.py deleted file mode 100644 index 8287de3d9..000000000 --- a/tests/assertion/mutation_analysis/test_mutationadapter.py +++ /dev/null @@ -1,37 +0,0 @@ -# This file is part of Pynguin. -# -# SPDX-FileCopyrightText: 2019–2024 Pynguin Contributors -# -# SPDX-License-Identifier: MIT -# -from unittest import mock -from unittest.mock import MagicMock - -import mutpy.controller - -import pynguin.assertion.mutation_analysis.mutationadapter as ma - - -class FooAdapter(ma.MutationAdapter): - pass - - -class FooMutController(mutpy.controller.MutationController): - pass - - -def test_mutate_module(): - adapter = FooAdapter() - controller = FooMutController( - MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() - ) - with mock.patch.object( # noqa: SIM117 - controller, "mutate_module", MagicMock() - ) as mutated: - with mock.patch.object( - adapter, "_build_mutation_controller", mutated - ) as mock_obj: - adapter.target_loader = MagicMock() - adapter.mutate_module() - mock_obj.assert_called_once() - mutated.assert_called_once() diff --git a/tests/assertion/mutation_analysis/test_mutators.py b/tests/assertion/mutation_analysis/test_mutators.py new file mode 100644 index 000000000..c10ce6dcc --- /dev/null +++ b/tests/assertion/mutation_analysis/test_mutators.py @@ -0,0 +1,157 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.mutators import FirstOrderMutator +from pynguin.assertion.mutation_analysis.mutators import HighOrderMutator +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorDeletion +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import AssignmentOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import ConstantReplacement +from tests.testutils import assert_mutator_mutation + + +def test_first_order_mutator_generation(): + assert_mutator_mutation( + FirstOrderMutator( + [ + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x + y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z -= x + y + """ + ): {(AssignmentOperatorReplacement, "mutate_Add", ast.Add, ast.Sub)}, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x - y + """ + ): {(ArithmeticOperatorReplacement, "mutate_Add", ast.Add, ast.Sub)}, + }, + ) + + +def test_high_order_mutator_generation(): + assert_mutator_mutation( + HighOrderMutator( + [ + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x + y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z -= x - y + """ + ): { + (AssignmentOperatorReplacement, "mutate_Add", ast.Add, ast.Sub), + (ArithmeticOperatorReplacement, "mutate_Add", ast.Add, ast.Sub), + }, + }, + ) + + +def test_high_order_mutator_generation_with_same_node(): + assert_mutator_mutation( + HighOrderMutator( + [ + ArithmeticOperatorDeletion, + ArithmeticOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = x + """ + ): { + (ArithmeticOperatorDeletion, "mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + inspect.cleandoc( + """ + x = 1 + y = +x + """ + ): { + (ArithmeticOperatorReplacement, "mutate_USub", ast.USub, ast.UAdd), + }, + }, + ) + + +def test_high_order_mutator_generation_with_multiple_visitors(): + assert_mutator_mutation( + HighOrderMutator([ConstantReplacement]), + inspect.cleandoc( + """ + x = 'test' + """ + ), + { + inspect.cleandoc( + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' + """ + ): { + ( + ConstantReplacement, + "mutate_Constant_str", + ast.Constant, + ast.Constant, + ), + }, + inspect.cleandoc( + """ + x = '' + """ + ): { + ( + ConstantReplacement, + "mutate_Constant_str_empty", + ast.Constant, + ast.Constant, + ), + }, + }, + ) diff --git a/tests/assertion/mutation_analysis/test_strategies.py b/tests/assertion/mutation_analysis/test_strategies.py new file mode 100644 index 000000000..f7ca2b310 --- /dev/null +++ b/tests/assertion/mutation_analysis/test_strategies.py @@ -0,0 +1,195 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast + +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorDeletion +from pynguin.assertion.mutation_analysis.operators import AssignmentOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.strategies import BetweenOperatorsHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import EachChoiceHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import RandomHOMStrategy +from pynguin.utils.randomness import RNG +from tests.testutils import create_aor_mutation_on_substraction + + +def test_first_to_last_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1]], + ] + + +def test_first_to_last_hom_strategy_same_node(): + node = ast.Sub(children=[]) + + mutations = [ + create_aor_mutation_on_substraction(node), + create_aor_mutation_on_substraction(node), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_first_to_last_hom_strategy_child_node_generation(): + child_node = ast.Sub(children=[]) + node = ast.UnaryOp(children=[child_node]) + + mutations = [ + create_aor_mutation_on_substraction(child_node), + Mutation( + node=node, + replacement_node=ast.Name(id="foo", children=[]), + operator=ArithmeticOperatorDeletion, + visitor_name="mutate_UnaryOp", + ), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_each_choice_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = EachChoiceHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[1]], + [mutations[2]], + ] + + +def test_between_operators_hom_strategy_generation_if_one_operator(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_between_operators_hom_strategy_generation_if_two_operators(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + Mutation( + node=ast.Sub(children=[]), + replacement_node=ast.Add(children=[]), + operator=AssignmentOperatorReplacement, + visitor_name="mutate_Sub", + ), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1], mutations[2]], + ] + + +def test_between_operators_hom_strategy_generation_if_three_operators(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + Mutation( + node=ast.Sub(children=[]), + replacement_node=ast.Add(children=[]), + operator=AssignmentOperatorReplacement, + visitor_name="mutate_Sub", + ), + Mutation( + node=ast.Constant(value=1, children=[]), + replacement_node=ast.Constant(value=2, children=[]), + operator=ConstantReplacement, + visitor_name="mutate_Constant_num", + ), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1], mutations[3]], + ] + + +def test_random_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = RandomHOMStrategy(order) + + RNG.seed(42) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[1], mutations[0]], + [mutations[2]], + ] diff --git a/tests/assertion/test_assertion_generation_integration.py b/tests/assertion/test_assertion_generation_integration.py index acfb20407..2290c745c 100644 --- a/tests/assertion/test_assertion_generation_integration.py +++ b/tests/assertion/test_assertion_generation_integration.py @@ -7,11 +7,14 @@ # ruff: noqa: E501 import ast import importlib +import inspect import threading import pytest import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo import pynguin.configuration as config import pynguin.ga.testcasechromosome as tcc import pynguin.ga.testsuitechromosome as tsc @@ -21,6 +24,7 @@ from pynguin.analyses.constants import EmptyConstantProvider from pynguin.analyses.module import generate_test_cluster from pynguin.analyses.seeding import AstToTestCaseTransformer +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.instrumentation.machinery import install_import_hook from pynguin.testcase.execution import ExecutionTracer from pynguin.testcase.execution import TestCaseExecutor @@ -55,7 +59,8 @@ def test_generate_mutation_assertions(generator, expected_result): tracer = ExecutionTracer() tracer.current_thread_identifier = threading.current_thread().ident with install_import_hook(module_name, tracer): - importlib.reload(importlib.import_module(module_name)) + module = importlib.import_module(module_name) + importlib.reload(module) cluster = generate_test_cluster(module_name) transformer = AstToTestCaseTransformer( cluster, False, EmptyConstantProvider() # noqa: FBT003 @@ -76,7 +81,19 @@ def test_generate_mutation_assertions(generator, expected_result): suite = tsc.TestSuiteChromosome() suite.add_test_case_chromosome(chromosome) - gen = generator(TestCaseExecutor(tracer)) + if generator is ag.MutationAnalysisAssertionGenerator: + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) + module_source_code = inspect.getsource(module) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) + gen = generator(TestCaseExecutor(tracer), mutation_controller) + else: + gen = generator(TestCaseExecutor(tracer)) suite.accept(gen) visitor = tc_to_ast.TestCaseToAstVisitor(ns.NamingScope(prefix="module"), set()) @@ -270,7 +287,8 @@ def test_mutation_analysis_integration_full( # noqa: PLR0917 tracer = ExecutionTracer() tracer.current_thread_identifier = threading.current_thread().ident with install_import_hook(module_name, tracer): - importlib.reload(importlib.import_module(module_name)) + module_type = importlib.import_module(module_name) + importlib.reload(module_type) cluster = generate_test_cluster(module_name) transformer = AstToTestCaseTransformer( cluster, False, EmptyConstantProvider() # noqa: FBT003 @@ -282,8 +300,17 @@ def test_mutation_analysis_integration_full( # noqa: PLR0917 suite = tsc.TestSuiteChromosome() suite.add_test_case_chromosome(chromosome) + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) + module_source_code = inspect.getsource(module_type) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module_type, mutation_tracer, testing=True + ) gen = ag.MutationAnalysisAssertionGenerator( - TestCaseExecutor(tracer), testing=True + TestCaseExecutor(tracer), mutation_controller, testing=True ) suite.accept(gen) @@ -299,7 +326,7 @@ def test_mutation_analysis_integration_full( # noqa: PLR0917 ) assert summary.get_metrics() == metrics - assert gen._testing_created_mutants == mutants + assert mutation_controller._testing_created_mutants == mutants visitor = tc_to_ast.TestCaseToAstVisitor(ns.NamingScope(prefix="module"), set()) test_case.accept(visitor) source = ast.unparse( diff --git a/tests/testutils.py b/tests/testutils.py index 864fca6e9..2847a74a4 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -5,11 +5,21 @@ # SPDX-License-Identifier: MIT # """Some utilites to make testing easier.""" +import ast + import pynguin.utils.generic.genericaccessibleobject as gao from pynguin.analyses.typesystem import Instance from pynguin.analyses.typesystem import ProperType from pynguin.analyses.typesystem import TypeSystem +from pynguin.assertion.mutation_analysis.mutators import FirstOrderMutator +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +from pynguin.assertion.mutation_analysis.transformer import create_module def feed_typesystem(system: TypeSystem, generic: gao.GenericAccessibleObject): @@ -39,3 +49,122 @@ def feed(typ: ProperType): if isinstance(generic, gao.GenericField): system.to_type_info(generic.owner.raw_type) + + +def assert_mutation( + operator: type[MutationOperator], + source_code: str, + expected_mutants_source_code: dict[str, tuple[str, type[ast.AST], type[ast.AST]]], +) -> None: + module_ast = ParentNodeTransformer.create_ast(source_code) + module = create_module(module_ast, "mutant") + + expected_mutants_processed_source_code = { + ast.unparse( + ParentNodeTransformer.create_ast(expected_mutant_source_code) + ): expected_mutant_info + for expected_mutant_source_code, expected_mutant_info in expected_mutants_source_code.items() + } + + for mutation, mutant_ast in operator.mutate(module_ast, module): + assert mutation.operator is operator, f"{mutation.operator} is not {operator}" + assert mutation.visitor_name in dir( + operator + ), f"{mutation.visitor_name} not in {dir(operator)}" + + mutant_source_code = ast.unparse(mutant_ast) + + assert ( + mutant_source_code in expected_mutants_processed_source_code + ), f"{repr(mutant_source_code)} not in {expected_mutants_processed_source_code}" + + expected_mutant_info = expected_mutants_processed_source_code.pop( + mutant_source_code + ) + + mutant_info = ( + mutation.visitor_name, + type(mutation.node), + type(mutation.replacement_node), + ) + + assert ( + expected_mutant_info == mutant_info + ), f"{expected_mutant_info} != {mutant_info}" + + assert ( + not expected_mutants_processed_source_code + ), f"Remaining mutants: {expected_mutants_processed_source_code}" + + processed_source_code = ast.unparse(module_ast) + expected_source_code = ast.unparse(ast.parse(source_code)) + + assert ( + expected_source_code == processed_source_code + ), f"Source code changed: {processed_source_code} != {expected_source_code}" + + +def assert_mutator_mutation( + mutator: FirstOrderMutator, + source_code: str, + expected_mutants_source_code: dict[ + str, set[tuple[type[MutationOperator], str, type[ast.AST], type[ast.AST]]] + ], +) -> None: + module_ast = ParentNodeTransformer.create_ast(source_code) + module = create_module(module_ast, "mutant") + + expected_mutants_processed_source_code = { + ast.unparse( + ParentNodeTransformer.create_ast(expected_mutant_source_code) + ): expected_mutant_info + for expected_mutant_source_code, expected_mutant_info in expected_mutants_source_code.items() + } + + for mutations, mutant_ast in mutator.mutate(module_ast, module): + mutant_source_code = ast.unparse(mutant_ast) + + assert ( + mutant_source_code in expected_mutants_processed_source_code + ), f"{repr(mutant_source_code)} not in {expected_mutants_processed_source_code}" + + expected_mutant_info = expected_mutants_processed_source_code.pop( + mutant_source_code + ) + + mutant_info = { + ( + mutation.operator, + mutation.visitor_name, + type(mutation.node), + type(mutation.replacement_node), + ) + for mutation in mutations + } + + assert ( + expected_mutant_info == mutant_info + ), f"{expected_mutant_info} != {mutant_info}" + + assert ( + not expected_mutants_processed_source_code + ), f"Remaining mutants: {expected_mutants_processed_source_code}" + + processed_source_code = ast.unparse(module_ast) + expected_source_code = ast.unparse(ast.parse(source_code)) + + assert ( + expected_source_code == processed_source_code + ), f"Source code changed: {processed_source_code} != {expected_source_code}" + + +def create_aor_mutation_on_substraction(node: ast.Sub | None = None) -> Mutation: + if node is None: + node = ast.Sub(children=[]) + + return Mutation( + node=node, + replacement_node=ast.Add(children=[]), + operator=ArithmeticOperatorReplacement, + visitor_name="mutate_Sub", + )