diff --git a/poetry.lock b/poetry.lock index 7383d82e..2e94863f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,39 @@ # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "astroid" +version = "3.3.9" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248"}, + {file = "astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550"}, +] + [[package]] name = "babel" -version = "2.13.0" +version = "2.16.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, - {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.extras] @@ -34,116 +58,192 @@ files = [ [package.extras] extras = ["regex"] +[[package]] +name = "blis" +version = "1.0.2" +description = "The Blis BLAS-like linear algebra library, as a self-contained C-extension." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "blis-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4424d5156f231c2b3553b32128a13b469d07dd276500a8b6637d2cb8bf4462c"}, + {file = "blis-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f22845f9150daf8497f457fd41d0d0181ced730f7241fda9e196f6f6513224c8"}, + {file = "blis-1.0.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24d86fc5d5590b7df3df257a876f4499a9941848aa25505452f9f17653fdd7d"}, + {file = "blis-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e75fc58cae6d0bda3e3fad7d55345112eafb0270f5a5d9254ef99059285632"}, + {file = "blis-1.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa733177e52136d25917235ae0ba05467703b3837ef541b42b24ee8004e6833f"}, + {file = "blis-1.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a99c543cc2f6c9bdf406accf24e219bda41249550bd35ad675c6fef5925b174"}, + {file = "blis-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:eb9a69853e6e1c64bc811c69f5b86ee88517cec4fc4310f78bcaca6c2f8488c7"}, + {file = "blis-1.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1639fbbc531c9666b4f5a396fb546789cb1887d68d07c67b31a28ff5bbabf61d"}, + {file = "blis-1.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91a69c723b855354733c59c1269188e49df30675d43262f4ec5cb569f83baec9"}, + {file = "blis-1.0.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66bf7b8bbef048253ba22bacb43b29ad0d4ce2782022234a5b07ce8c8583161"}, + {file = "blis-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5aef5aa1eb56b9ea36f7b4c8714b87c039da423db2437336949b0b6d17faf97"}, + {file = "blis-1.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a019baf7b22bda35ba9e6e75a138e0ee5f4a8e7bc35bf2e606b2032833874d63"}, + {file = "blis-1.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b957fc00303cfb5bafb3e8bd5dceea4cdd0d78f594d92619a84c54a7d9855132"}, + {file = "blis-1.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e372b1143230b20d199d0b70274f6efa20fef3d9f9ebeafb34291bb8c00244c3"}, + {file = "blis-1.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df0c2ba44d0520bae68a3f7978048f961d230dbf93e13b9475455611e7f9eb94"}, + {file = "blis-1.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da4ac030c843c6020d1ce9bc7eb918640c2714263f7c6b4ca1db8eafb4a23041"}, + {file = "blis-1.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:411105aef67e0ca9a8254487e7ee0fc9a8acce38e9faeb7bfcf44168604efc16"}, + {file = "blis-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bd7b825e7d53449e8ff5077f04737d3d7cb565e3e54892516a81e8bf0ea650b"}, + {file = "blis-1.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0497e37b3e1d2afdb1a87b45889ea751e2dbf61a6d8fa7541de2a55e1230dd5d"}, + {file = "blis-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d38ab8b8bd3beb824a069e952d76bced762f89a646bf8d0835b48bbaf960e061"}, + {file = "blis-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:3208346a4376453af03b118e4fa8cd9dc021c165a5227f362663ea36837f0952"}, + {file = "blis-1.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec90d0e9cfddfaecf4a118504f052b3330442a1cfe67a8959f805d000944e1e7"}, + {file = "blis-1.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f43dba62dd2873f133e6642caf75d6f21d7fd87724a25a79156b19a4b6dc573"}, + {file = "blis-1.0.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e571457aab754587067a5298cbcdeeaf7054501fb59222e22f04fc17e40c143"}, + {file = "blis-1.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eec0e57473ed62deb886bf1f48a61aba21e1f3388384fe3341837789913aebc8"}, + {file = "blis-1.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db737d3308ad887e2b812a7e466e2a2c4028fee8bba226aaa7d5c731f8bcc56c"}, + {file = "blis-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ac705ba0069c3ec3ef494136cef10ba20b8ad444b25476dba031103c6d48ec65"}, + {file = "blis-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b3979875de89c1f9c1393e39a034fe2e58536599ab199e7665ab268987a1fc8"}, + {file = "blis-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:986488f85c526f91af0f300725b81dd20c99864323ce9663db81227f1e971357"}, + {file = "blis-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:afe81958a14952331c1c33044e7fe10ca44a465bb26b8d3b88203525cedd11cf"}, + {file = "blis-1.0.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5938d1b1385456a4ed87a22de36da56791fe6bf42de53e97fac8f4b69ee933c"}, + {file = "blis-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde12fb18a5beffd2fbb73e912dbf646494593da98ecbfc8328675ca946b9750"}, + {file = "blis-1.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a5b21c06fae00b239e919ad96f5c21b23389af25434fab3b50888f1ecd46b0a"}, + {file = "blis-1.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:05ac1bbe39d8342e9710315da5ef91cb8fed8c24813370e65205f9636cacaaac"}, + {file = "blis-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e5ce133c743391c573ab7908f245733542667976f1158a53c50be8d0761f5ab"}, + {file = "blis-1.0.2.tar.gz", hash = "sha256:68df878871a9db34efd58f648e12e06806e381991bd9e70df198c33d7b259383"}, +] + +[package.dependencies] +numpy = ">=2.0.0,<3.0.0" + +[[package]] +name = "catalogue" +version = "2.0.10" +description = "Super lightweight function registries for your library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f"}, + {file = "catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15"}, +] + [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["docs"] +groups = ["main", "docs"] files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] name = "charset-normalizer" -version = "3.3.0" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" -groups = ["docs"] +groups = ["main", "docs"] files = [ - {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, - {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -152,7 +252,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["docs"] +groups = ["main", "docs"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -161,6 +261,24 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "cloudpathlib" +version = "0.20.0" +description = "pathlib-style classes for cloud storage services." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cloudpathlib-0.20.0-py3-none-any.whl", hash = "sha256:7af3bcefbf73392ae7f31c08b3660ec31607f8c01b7f6262d4d73469a845f641"}, + {file = "cloudpathlib-0.20.0.tar.gz", hash = "sha256:f6ef7ca409a510f7ba4639ba50ab3fc5b6dee82d6dff0d7f5715fd0c9ab35891"}, +] + +[package.extras] +all = ["cloudpathlib[azure]", "cloudpathlib[gs]", "cloudpathlib[s3]"] +azure = ["azure-storage-blob (>=12)", "azure-storage-file-datalake (>=12)"] +gs = ["google-cloud-storage"] +s3 = ["boto3 (>=1.34.0)"] + [[package]] name = "colorama" version = "0.4.6" @@ -174,81 +292,133 @@ files = [ ] markers = {dev = "sys_platform == \"win32\""} +[[package]] +name = "confection" +version = "0.1.5" +description = "The sweetest config system for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14"}, + {file = "confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +srsly = ">=2.4.0,<3.0.0" + [[package]] name = "coverage" -version = "7.6.4" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, - {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, - {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, - {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, - {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, - {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, - {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, - {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, - {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, - {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, - {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, - {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, - {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, - {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, - {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, - {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "cymem" +version = "2.0.10" +description = "Manage calls to calloc/free through Cython" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "cymem-2.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:010f78804cf5e2fbd08abad210d2b78a828bea1a9f978737e28e1614f5a258b4"}, + {file = "cymem-2.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9688f691518859e76c24c37686314dc5163f2fae1b9df264714220fc087b09a5"}, + {file = "cymem-2.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61ce538c594f348b90037b03910da31ce7aacca090ea64063593688c55f6adad"}, + {file = "cymem-2.0.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d45b99c727dfc303db3bb9f136b86731a4d231fbf9c27ce5745ea4a527da0b5"}, + {file = "cymem-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:a03abe0e2f8925707c3dee88060bea1a94b9a24afc7d07ee17f319022126bcb4"}, + {file = "cymem-2.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18dc5a7b6a325d5fc0b2b40beb02673f36f64655ee086649c91e44ce092c7b36"}, + {file = "cymem-2.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d30ce83ff9009e5c5c8186845d9d583f867dace88113089bfc0ee1c348e45d5a"}, + {file = "cymem-2.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6cb07416c82633503974f331abde9e1514c90aae8b3240884e749c2a60adbc"}, + {file = "cymem-2.0.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34406e2bff8707719f3f4b262e50b04876369233d5277a7c2d0c2e73a8579b46"}, + {file = "cymem-2.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:51218af9645541005a1313d6640bf6e86e7fb4b38a87268a5ea428d50ac3cec2"}, + {file = "cymem-2.0.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6ed8b1ed448cd65e12405a02aa71b22a4094d8a623205625057c4c73ba4b133"}, + {file = "cymem-2.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e57928d9e93c61265281ea01a1d24499d397625b2766a0c5735b99bceb3ba75"}, + {file = "cymem-2.0.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4932060a5d55648fa4a3960f1cad9905572ed5c6f02af42f849e869d2803d4"}, + {file = "cymem-2.0.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4bc6c823b400d32cddcfeefb3f352d52a0cc911cb0b5c1ef64e3f9741fd56b9"}, + {file = "cymem-2.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:6ae7f22af4bc4311f06c925df61c62219c11939dffc9c91d67caf89a7e1557a5"}, + {file = "cymem-2.0.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5698a515900dc697874444fa05d8d852bbad43543de2e7834ec3895156cc2aad"}, + {file = "cymem-2.0.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6580d657d0f208d675d62cc052fb908529d52d24282342e24a9843de85352b88"}, + {file = "cymem-2.0.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea72cf0e369f3cf1f10038d572143d88ce7c959222cf7d742acbeb45e00ac5c0"}, + {file = "cymem-2.0.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33d7f5014ad36af22995847fccd82ca0bd4b0394fb1d9dd9fef1e8cefdab2444"}, + {file = "cymem-2.0.10-cp313-cp313-win_amd64.whl", hash = "sha256:82f19a39052747309ced6b948b34aff62aa00c795c9d9d3d31a071e8c791efee"}, + {file = "cymem-2.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e644c3c48663d2c0580292e1d636e7eb8885bfe9df75f929d8ad0403621b75fe"}, + {file = "cymem-2.0.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f2bc8c69a23e3243e3a0c0feca08c9d4454d3cb7934bb11f5e1b3333151d69d"}, + {file = "cymem-2.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5369f1974854102ee1751577f13acbbb6a13ba73f9fbb44580f8f3275dae0205"}, + {file = "cymem-2.0.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ffb6181d589e65c46c2d515d8326746a2e0bda31b67c8b1edfbf0663249f84fb"}, + {file = "cymem-2.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:9805f7dbf078a0e2eb417b7e1166cedc590887b55e38a3f3ba5349649c93e6be"}, + {file = "cymem-2.0.10.tar.gz", hash = "sha256:f51700acfa1209b4a221dc892cca8030f4bc10d4c153dec098042f484c7f07a4"}, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -284,16 +454,19 @@ colorama = ">=0.4" [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" -groups = ["docs"] +python-versions = ">=3.6" +groups = ["main", "docs"] files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -312,7 +485,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["docs"] +groups = ["main", "docs"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -324,90 +497,258 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "langcodes" +version = "3.5.0" +description = "Tools for labeling human languages with IETF language tags" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "langcodes-3.5.0-py3-none-any.whl", hash = "sha256:853c69d1a35e0e13da2f427bb68fb2fa4a8f4fb899e0c62ad8df8d073dcfed33"}, + {file = "langcodes-3.5.0.tar.gz", hash = "sha256:1eef8168d07e51e131a2497ffecad4b663f6208e7c3ae3b8dc15c51734a6f801"}, +] + +[package.dependencies] +language-data = ">=1.2" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "language-data" +version = "1.3.0" +description = "Supplementary data about languages used by the langcodes module" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "language_data-1.3.0-py3-none-any.whl", hash = "sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf"}, + {file = "language_data-1.3.0.tar.gz", hash = "sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec"}, +] + +[package.dependencies] +marisa-trie = ">=1.1.0" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "marisa-trie" +version = "1.2.1" +description = "Static memory-efficient and fast Trie-like structures for Python." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8"}, + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6"}, + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6"}, + {file = "marisa_trie-1.2.1-cp310-cp310-win32.whl", hash = "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5"}, + {file = "marisa_trie-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a"}, + {file = "marisa_trie-1.2.1-cp311-cp311-win32.whl", hash = "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571"}, + {file = "marisa_trie-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:638506eacf20ca503fff72221a7e66a6eadbf28d6a4a6f949fcf5b1701bb05ec"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de1665eaafefa48a308e4753786519888021740501a15461c77bdfd57638e6b4"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f713af9b8aa66a34cd3a78c7d150a560a75734713abe818a69021fd269e927fa"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2a7d00f53f4945320b551bccb826b3fb26948bde1a10d50bb9802fabb611b10"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98042040d1d6085792e8d0f74004fc0f5f9ca6091c298f593dd81a22a4643854"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6532615111eec2c79e711965ece0bc95adac1ff547a7fff5ffca525463116deb"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20948e40ab2038e62b7000ca6b4a913bc16c91a2c2e6da501bd1f917eeb28d51"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66b23e5b35dd547f85bf98db7c749bc0ffc57916ade2534a6bbc32db9a4abc44"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6704adf0247d2dda42e876b793be40775dff46624309ad99bc7537098bee106d"}, + {file = "marisa_trie-1.2.1-cp312-cp312-win32.whl", hash = "sha256:3ad356442c2fea4c2a6f514738ddf213d23930f942299a2b2c05df464a00848a"}, + {file = "marisa_trie-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2806f75817392cedcacb24ac5d80b0350dde8d3861d67d045c1d9b109764114"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b5ea16e69bfda0ac028c921b58de1a4aaf83d43934892977368579cd3c0a2554"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f627f4e41be710b6cb6ed54b0128b229ac9d50e2054d9cde3af0fef277c23cf"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e649f3dc8ab5476732094f2828cc90cac3be7c79bc0c8318b6fda0c1d248db4"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46e528ee71808c961baf8c3ce1c46a8337ec7a96cc55389d11baafe5b632f8e9"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aa4401a1180615f74d575571a6550081d84fc6461e9aefc0bb7b2427af098e"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce59bcd2cda9bb52b0e90cc7f36413cd86c3d0ce7224143447424aafb9f4aa48"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4cd800704a5fc57e53c39c3a6b0c9b1519ebdbcb644ede3ee67a06eb542697d"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2428b495003c189695fb91ceeb499f9fcced3a2dce853e17fa475519433c67ff"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:735c363d9aaac82eaf516a28f7c6b95084c2e176d8231c87328dc80e112a9afa"}, + {file = "marisa_trie-1.2.1-cp313-cp313-win32.whl", hash = "sha256:eba6ca45500ca1a042466a0684aacc9838e7f20fe2605521ee19f2853062798f"}, + {file = "marisa_trie-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:aa7cd17e1c690ce96c538b2f4aae003d9a498e65067dd433c52dd069009951d4"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5e43891a37b0d7f618819fea14bd951289a0a8e3dd0da50c596139ca83ebb9b1"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6946100a43f933fad6bc458c502a59926d80b321d5ac1ed2ff9c56605360496f"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4177dc0bd1374e82be9b2ba4d0c2733b0a85b9d154ceeea83a5bee8c1e62fbf"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f35c2603a6be168088ed1db6ad1704b078aa8f39974c60888fbbced95dcadad4"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d659fda873d8dcb2c14c2c331de1dee21f5a902d7f2de7978b62c6431a8850ef"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b0ef26733d3c836be79e812071e1a431ce1f807955a27a981ebb7993d95f842b"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:536ea19ce6a2ce61c57fed4123ecd10d18d77a0db45cd2741afff2b8b68f15b3"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-win32.whl", hash = "sha256:0ee6cf6a16d9c3d1c94e21c8e63c93d8b34bede170ca4e937e16e1c0700d399f"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7e7b1786e852e014d03e5f32dbd991f9a9eb223dd3fa9a2564108b807e4b7e1c"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:952af3a5859c3b20b15a00748c36e9eb8316eb2c70bd353ae1646da216322908"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24a81aa7566e4ec96fc4d934581fe26d62eac47fc02b35fa443a0bb718b471e8"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9c9b32b14651a6dcf9e8857d2df5d29d322a1ea8c0be5c8ffb88f9841c4ec62b"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ac170d20b97beb75059ba65d1ccad6b434d777c8992ab41ffabdade3b06dd74"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da4e4facb79614cc4653cfd859f398e4db4ca9ab26270ff12610e50ed7f1f6c6"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25688f34cac3bec01b4f655ffdd6c599a01f0bd596b4a79cf56c6f01a7df3560"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1db3213b451bf058d558f6e619bceff09d1d130214448a207c55e1526e2773a1"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d5648c6dcc5dc9200297fb779b1663b8a4467bda034a3c69bd9c32d8afb33b1d"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5bd39a4e1cc839a88acca2889d17ebc3f202a5039cd6059a13148ce75c8a6244"}, + {file = "marisa_trie-1.2.1-cp38-cp38-win32.whl", hash = "sha256:594f98491a96c7f1ffe13ce292cef1b4e63c028f0707effdea0f113364c1ae6c"}, + {file = "marisa_trie-1.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:5fe5a286f997848a410eebe1c28657506adaeb405220ee1e16cfcfd10deb37f2"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0fe2ace0cb1806badbd1c551a8ec2f8d4cf97bf044313c082ef1acfe631ddca"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67f0c2ec82c20a02c16fc9ba81dee2586ef20270127c470cb1054767aa8ba310"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3c98613180cf1730e221933ff74b454008161b1a82597e41054127719964188"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:429858a0452a7bedcf67bc7bb34383d00f666c980cb75a31bcd31285fbdd4403"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2eacb84446543082ec50f2fb563f1a94c96804d4057b7da8ed815958d0cdfbe"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:852d7bcf14b0c63404de26e7c4c8d5d65ecaeca935e93794331bc4e2f213660b"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e58788004adda24c401d1751331618ed20c507ffc23bfd28d7c0661a1cf0ad16"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aefe0973cc4698e0907289dc0517ab0c7cdb13d588201932ff567d08a50b0e2e"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c50c861faad0a5c091bd763e0729f958c316e678dfa065d3984fbb9e4eacbcd"}, + {file = "marisa_trie-1.2.1-cp39-cp39-win32.whl", hash = "sha256:b1ce340da608530500ab4f963f12d6bfc8d8680900919a60dbdc9b78c02060a4"}, + {file = "marisa_trie-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:ce37d8ca462bb64cc13f529b9ed92f7b21fe8d1f1679b62e29f9cb7d0e888b49"}, + {file = "marisa_trie-1.2.1.tar.gz", hash = "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +test = ["hypothesis", "pytest", "readme-renderer"] + [[package]] name = "markdown" -version = "3.4.4" +version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" -version = "2.1.3" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false python-versions = ">=3.7" -groups = ["docs"] +groups = ["main"] files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] @@ -514,14 +855,50 @@ recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2. [[package]] name = "mkdocs-material-extensions" -version = "1.3" +version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "mkdocs_material_extensions-1.3-py3-none-any.whl", hash = "sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc"}, - {file = "mkdocs_material_extensions-1.3.tar.gz", hash = "sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"}, + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "murmurhash" +version = "1.0.11" +description = "Cython bindings for MurmurHash" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "murmurhash-1.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a73cf9f55c8218d5aa47b3b6dac28fa2e1730bbca0874e7eabe5e1a6024780c5"}, + {file = "murmurhash-1.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48716859a12596024d9adecf399e356c3c5c38ba2eb0d8270bd6655c05a0af28"}, + {file = "murmurhash-1.0.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1967ccc893c80798a420c5c3829ea9755d0b4a4972b0bf6e5c34d1117f5d0222"}, + {file = "murmurhash-1.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:904c4d6550c640e0f640b6357ecaa13406e6d925e55fbb4ac9e1f27ff25bee3c"}, + {file = "murmurhash-1.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:4c24f1c96e8ce720ac85058c37e6e775be6017f0966abff2863733d91368e03e"}, + {file = "murmurhash-1.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53ed86ce0bef2475af9314f732ca66456e7b00abb1d1a6c29c432e5f0f49bad5"}, + {file = "murmurhash-1.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51e7c61f59e0ee1c465c841f530ef6373a98dc028059048fc0c857dfd5d57b1c"}, + {file = "murmurhash-1.0.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b9a5109e29d43c79bfdca8dbad9bee7190846a88ec6d4135754727fb49a64e5"}, + {file = "murmurhash-1.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12845ad43a2e54734b52f58e8d228eacd03803d368b689b3868a0bdec4c10da1"}, + {file = "murmurhash-1.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:e3d0bdbffd82924725cd6549b03ee11997a2c58253f0fdda571a5fedacc894a1"}, + {file = "murmurhash-1.0.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:185b2cd20b81fa876eaa2249faafd0b7b3d0c54ef04714e38135d9f482cf6ce9"}, + {file = "murmurhash-1.0.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd3083c6d977c2bc1e2f35ff999c39de43de09fd588f780243ec78debb316406"}, + {file = "murmurhash-1.0.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49a3cf4d26f7213d0f4a6c2c49496cbe9f78b30d56b1c3b17fbc74676372ea3f"}, + {file = "murmurhash-1.0.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1bdb3c3fe32d93f7c461f11e6b2f7bbe64b3d70f56e48052490435853ed5c91"}, + {file = "murmurhash-1.0.11-cp312-cp312-win_amd64.whl", hash = "sha256:0b507dd8ea10f3e5204b397ea9917a3a5f11756859d91406a8f485f18a411bdf"}, + {file = "murmurhash-1.0.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:036aea55d160d65698888a903fd2a19c4258be711f7bf2ab1b6cebdf41e09e09"}, + {file = "murmurhash-1.0.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f4b991b5bd88f5d57550a6328f8adb2f16656781e9eade9c16e55b41f6fab7"}, + {file = "murmurhash-1.0.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5527ec305236a2ef404a38e0e57b1dc886a431e2032acf4c7ce3b17382c49ef"}, + {file = "murmurhash-1.0.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b26cf1be87c13fb242b9c252f11a25da71056c8fb5f22623e455129cce99592a"}, + {file = "murmurhash-1.0.11-cp313-cp313-win_amd64.whl", hash = "sha256:24aba80a793bf371de70fffffc1f16c06810e4d8b90125b5bb762aabda3174d1"}, + {file = "murmurhash-1.0.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:234cc9719a5df1bffe174664b84b8381f66016a1f094d43db3fb8ffca1d72207"}, + {file = "murmurhash-1.0.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faf1db780cfca0a021ce32542ac750d24b9b3e81e2a4a6fcb78efcc8ec611813"}, + {file = "murmurhash-1.0.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f7f7c8bce5fa1c50c6214421af27eb0bbb07cc55c4a35efa5735ceaf1a6a1c"}, + {file = "murmurhash-1.0.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b8d8fad28cf7d9661486f8e3d48e4215db69f5f9b091e78edcccf2c46459846a"}, + {file = "murmurhash-1.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:6ae5fc4f59be8eebcb8d24ffee49f32ee4eccdc004060848834eb2540ee3a056"}, + {file = "murmurhash-1.0.11.tar.gz", hash = "sha256:87ff68a255e54e7648d0729ff4130f43f7f38f03288a376e567934e16db93767"}, ] [[package]] @@ -589,56 +966,117 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "numpy" +version = "2.0.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, +] + [[package]] name = "packaging" -version = "23.2" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" -groups = ["dev", "docs"] +python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "paginate" -version = "0.5.6" +version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" groups = ["docs"] files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, ] +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" -version = "3.11.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -656,35 +1094,216 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "preshed" +version = "3.0.9" +description = "Cython hash table that trusts the keys are pre-hashed" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "preshed-3.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f96ef4caf9847b2bb9868574dcbe2496f974e41c2b83d6621c24fb4c3fc57e3"}, + {file = "preshed-3.0.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a61302cf8bd30568631adcdaf9e6b21d40491bd89ba8ebf67324f98b6c2a2c05"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99499e8a58f58949d3f591295a97bca4e197066049c96f5d34944dd21a497193"}, + {file = "preshed-3.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea6b6566997dc3acd8c6ee11a89539ac85c77275b4dcefb2dc746d11053a5af8"}, + {file = "preshed-3.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:bfd523085a84b1338ff18f61538e1cfcdedc4b9e76002589a301c364d19a2e36"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7c2364da27f2875524ce1ca754dc071515a9ad26eb5def4c7e69129a13c9a59"}, + {file = "preshed-3.0.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182138033c0730c683a6d97e567ceb8a3e83f3bff5704f300d582238dbd384b3"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:345a10be3b86bcc6c0591d343a6dc2bfd86aa6838c30ced4256dfcfa836c3a64"}, + {file = "preshed-3.0.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51d0192274aa061699b284f9fd08416065348edbafd64840c3889617ee1609de"}, + {file = "preshed-3.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:96b857d7a62cbccc3845ac8c41fd23addf052821be4eb987f2eb0da3d8745aa1"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4fe6720012c62e6d550d6a5c1c7ad88cacef8388d186dad4bafea4140d9d198"}, + {file = "preshed-3.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e04f05758875be9751e483bd3c519c22b00d3b07f5a64441ec328bb9e3c03700"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a55091d0e395f1fdb62ab43401bb9f8b46c7d7794d5b071813c29dc1ab22fd0"}, + {file = "preshed-3.0.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7de8f5138bcac7870424e09684dc3dd33c8e30e81b269f6c9ede3d8c7bb8e257"}, + {file = "preshed-3.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:24229c77364628743bc29c5620c5d6607ed104f0e02ae31f8a030f99a78a5ceb"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73b0f7ecc58095ebbc6ca26ec806008ef780190fe685ce471b550e7eef58dc2"}, + {file = "preshed-3.0.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb90ecd5bec71c21d95962db1a7922364d6db2abe284a8c4b196df8bbcc871e"}, + {file = "preshed-3.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:e304a0a8c9d625b70ba850c59d4e67082a6be9c16c4517b97850a17a282ebee6"}, + {file = "preshed-3.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1fa6d3d5529b08296ff9b7b4da1485c080311fd8744bbf3a86019ff88007b382"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e5173809d85edd420fc79563b286b88b4049746b797845ba672cf9435c0e7"}, + {file = "preshed-3.0.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fe81eb21c7d99e8b9a802cc313b998c5f791bda592903c732b607f78a6b7dc4"}, + {file = "preshed-3.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:78590a4a952747c3766e605ce8b747741005bdb1a5aa691a18aae67b09ece0e6"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3452b64d97ce630e200c415073040aa494ceec6b7038f7a2a3400cbd7858e952"}, + {file = "preshed-3.0.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ac970d97b905e9e817ec13d31befd5b07c9cfec046de73b551d11a6375834b79"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eebaa96ece6641cd981491cba995b68c249e0b6877c84af74971eacf8990aa19"}, + {file = "preshed-3.0.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d473c5f6856e07a88d41fe00bb6c206ecf7b34c381d30de0b818ba2ebaf9406"}, + {file = "preshed-3.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:0de63a560f10107a3f0a9e252cc3183b8fdedcb5f81a86938fd9f1dcf8a64adf"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3a9ad9f738084e048a7c94c90f40f727217387115b2c9a95c77f0ce943879fcd"}, + {file = "preshed-3.0.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a671dfa30b67baa09391faf90408b69c8a9a7f81cb9d83d16c39a182355fbfce"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23906d114fc97c17c5f8433342495d7562e96ecfd871289c2bb2ed9a9df57c3f"}, + {file = "preshed-3.0.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778cf71f82cedd2719b256f3980d556d6fb56ec552334ba79b49d16e26e854a0"}, + {file = "preshed-3.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:a6e579439b329eb93f32219ff27cb358b55fbb52a4862c31a915a098c8a22ac2"}, + {file = "preshed-3.0.9.tar.gz", hash = "sha256:721863c5244ffcd2651ad0928951a2c7c77b102f4e11a251ad85d37ee7621660"}, +] + +[package.dependencies] +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=0.28.0,<1.1.0" + +[[package]] +name = "pydantic" +version = "2.10.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" -version = "2.16.1" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" -groups = ["docs"] +python-versions = ">=3.8" +groups = ["main", "docs"] files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata ; python_version < \"3.8\""] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.3" +version = "10.12" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, - {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.6" pyyaml = "*" [package.extras] @@ -732,14 +1351,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["docs"] files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -747,63 +1366,65 @@ six = ">=1.5" [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -823,14 +1444,14 @@ pyyaml = "*" [[package]] name = "requests" -version = "2.32.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["docs"] +groups = ["main", "docs"] files = [ - {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, - {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -843,18 +1464,246 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "75.6.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.7.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (>=1.12,<1.14)", "pytest-mypy"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["docs"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "smart-open" +version = "7.0.5" +description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +optional = false +python-versions = "<4.0,>=3.7" +groups = ["main"] +files = [ + {file = "smart_open-7.0.5-py3-none-any.whl", hash = "sha256:8523ed805c12dff3eaa50e9c903a6cb0ae78800626631c5fe7ea073439847b89"}, + {file = "smart_open-7.0.5.tar.gz", hash = "sha256:d3672003b1dbc85e2013e4983b88eb9a5ccfd389b0d4e5015f39a9ee5620ec18"}, +] + +[package.dependencies] +wrapt = "*" + +[package.extras] +all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests", "zstandard"] +azure = ["azure-common", "azure-core", "azure-storage-blob"] +gcs = ["google-cloud-storage (>=2.6.0)"] +http = ["requests"] +s3 = ["boto3"] +ssh = ["paramiko"] +test = ["awscli", "azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "numpy", "paramiko", "pyopenssl", "pytest", "pytest-benchmark", "pytest-rerunfailures", "requests", "responses", "zstandard"] +webhdfs = ["requests"] +zst = ["zstandard"] + +[[package]] +name = "spacy" +version = "3.8.2" +description = "Industrial-strength Natural Language Processing (NLP) in Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "spacy-3.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:795e74493036dda0a576093b797a4fdc1aaa83d66f6c9af0e5b6b1c640dc2222"}, + {file = "spacy-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d432e78fe2a7424aba9a741db07ce58487d3b74fae4e20a779142828e61bc402"}, + {file = "spacy-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536a8ba17359540de502285934bf357805d978128d7bd5e84ba53d28b32a0ffb"}, + {file = "spacy-3.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6a0d0d39baa1cb9f5bb82874cbe1067bf494f76277a383f1f7b29f7a855d41a9"}, + {file = "spacy-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:9dcfcfda558b3e47946b2041c7a4203b78e542d0de20997a7c0a6d11b58b2522"}, + {file = "spacy-3.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e5d48918028cff6d69d9915dad64f0e32ebd5f1e4f1fa81a2e17e56a6f61e05"}, + {file = "spacy-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:455f845b88ed795d7e595070ee84b65b3ea382357811e09fc744789a20b7b5f7"}, + {file = "spacy-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05d8a4cbfdb90049053564790a0d12fa790c9471580cb6a1f8bdc2b6e74703dd"}, + {file = "spacy-3.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e3c3e67f786f1410d08420ffcaba0f80dc58387ab6172dcdac1a73353d3a85c7"}, + {file = "spacy-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cfe0c4558f635c67677e36d9a315f51d51f824870589c4846c95e880042a2ceb"}, + {file = "spacy-3.8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0ce56f3c46dd4cebb5aaa3a40966e813b7fc6a540d547788a7d00cca10cd60a9"}, + {file = "spacy-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09faa873cf23d5136b1c1ce6378054a885f650fda96e1655a3ab49e2e7fdd15b"}, + {file = "spacy-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e992a11de9b727c61288541945c0ffc37ed998aca76bfd557937c2c195d7d4"}, + {file = "spacy-3.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be962a8188fb20d6c2065e1e865d1799ebbf544c1af67ab8c75cb279bf5448c7"}, + {file = "spacy-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:04546c5f5ed607387d4e9ecf57614e90c5784866a10a3c6dbe5b06e9b18a2f29"}, + {file = "spacy-3.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c5fb8b804ebf1c2791b384d61391e9d0227bcfdecd6c861291690813b8a6eb1"}, + {file = "spacy-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3647233b2454e8e7bae94232563c9bff849db9e26bf61ef51122ef723de009fe"}, + {file = "spacy-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca20e2d9b4aeaedd7068d6419762d66cfad82bc8b1e63e36714601686d67f163"}, + {file = "spacy-3.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be3aa3e7d456627acbcb7f585156ee463c01d006a07aeb20b43a8543a02cd047"}, + {file = "spacy-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:54c63d31ef410ebb5b0fd72729afaf50f876bf2bc29f73c6c5fc3676ae4158a1"}, + {file = "spacy-3.8.2.tar.gz", hash = "sha256:4b37ebd25ada4059b0dc9e0893e70dde5df83485329a068ef04580e70892a65d"}, +] + +[package.dependencies] +catalogue = ">=2.0.6,<2.1.0" +cymem = ">=2.0.2,<2.1.0" +jinja2 = "*" +langcodes = ">=3.2.0,<4.0.0" +murmurhash = ">=0.28.0,<1.1.0" +numpy = {version = ">=1.19.0", markers = "python_version >= \"3.9\""} +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +setuptools = "*" +spacy-legacy = ">=3.0.11,<3.1.0" +spacy-loggers = ">=1.0.0,<2.0.0" +srsly = ">=2.4.3,<3.0.0" +thinc = ">=8.3.0,<8.4.0" +tqdm = ">=4.38.0,<5.0.0" +typer = ">=0.3.0,<1.0.0" +wasabi = ">=0.9.1,<1.2.0" +weasel = ">=0.1.0,<0.5.0" + +[package.extras] +apple = ["thinc-apple-ops (>=1.0.0,<2.0.0)"] +cuda = ["cupy (>=5.0.0b4,<13.0.0)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0,<13.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4,<13.0.0)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4,<13.0.0)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4,<13.0.0)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4,<13.0.0)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4,<13.0.0)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4,<13.0.0)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4,<13.0.0)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4,<13.0.0)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4,<13.0.0)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4,<13.0.0)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4,<13.0.0)"] +cuda11x = ["cupy-cuda11x (>=11.0.0,<13.0.0)"] +cuda12x = ["cupy-cuda12x (>=11.5.0,<13.0.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4,<13.0.0)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4,<13.0.0)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4,<13.0.0)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4,<13.0.0)"] +ja = ["sudachidict-core (>=20211220)", "sudachipy (>=0.5.2,!=0.6.1)"] +ko = ["natto-py (>=0.9.0)"] +lookups = ["spacy-lookups-data (>=1.0.3,<1.1.0)"] +th = ["pythainlp (>=2.0)"] +transformers = ["spacy-transformers (>=1.1.2,<1.4.0)"] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +description = "Legacy registered functions for spaCy backwards compatibility" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774"}, + {file = "spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f"}, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +description = "Logging utilities for SpaCy" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24"}, + {file = "spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645"}, +] + +[[package]] +name = "srsly" +version = "2.4.8" +description = "Modern high-performance serialization utilities for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "srsly-2.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:17f3bcb418bb4cf443ed3d4dcb210e491bd9c1b7b0185e6ab10b6af3271e63b2"}, + {file = "srsly-2.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b070a58e21ab0e878fd949f932385abb4c53dd0acb6d3a7ee75d95d447bc609"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98286d20014ed2067ad02b0be1e17c7e522255b188346e79ff266af51a54eb33"}, + {file = "srsly-2.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18685084e2e0cc47c25158cbbf3e44690e494ef77d6418c2aae0598c893f35b0"}, + {file = "srsly-2.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:980a179cbf4eb5bc56f7507e53f76720d031bcf0cef52cd53c815720eb2fc30c"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5472ed9f581e10c32e79424c996cf54c46c42237759f4224806a0cd4bb770993"}, + {file = "srsly-2.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50f10afe9230072c5aad9f6636115ea99b32c102f4c61e8236d8642c73ec7a13"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c994a89ba247a4d4f63ef9fdefb93aa3e1f98740e4800d5351ebd56992ac75e3"}, + {file = "srsly-2.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7ed4a0c20fa54d90032be32f9c656b6d75445168da78d14fe9080a0c208ad"}, + {file = "srsly-2.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:7a919236a090fb93081fbd1cec030f675910f3863825b34a9afbcae71f643127"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7583c03d114b4478b7a357a1915305163e9eac2dfe080da900555c975cca2a11"}, + {file = "srsly-2.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ccdd2f6db824c31266aaf93e0f31c1c43b8bc531cd2b3a1d924e3c26a4f294"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72d2974f91aee652d606c7def98744ca6b899bd7dd3009fd75ebe0b5a51034"}, + {file = "srsly-2.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a60c905fd2c15e848ce1fc315fd34d8a9cc72c1dee022a0d8f4c62991131307"}, + {file = "srsly-2.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:e0b8d5722057000694edf105b8f492e7eb2f3aa6247a5f0c9170d1e0d074151c"}, + {file = "srsly-2.4.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:196b4261f9d6372d1d3d16d1216b90c7e370b4141471322777b7b3c39afd1210"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4750017e6d78590b02b12653e97edd25aefa4734281386cc27501d59b7481e4e"}, + {file = "srsly-2.4.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa034cd582ba9e4a120c8f19efa263fcad0f10fc481e73fb8c0d603085f941c4"}, + {file = "srsly-2.4.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5a78ab9e9d177ee8731e950feb48c57380036d462b49e3fb61a67ce529ff5f60"}, + {file = "srsly-2.4.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:087e36439af517e259843df93eb34bb9e2d2881c34fa0f541589bcfbc757be97"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad141d8a130cb085a0ed3a6638b643e2b591cb98a4591996780597a632acfe20"}, + {file = "srsly-2.4.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d05367b2571c0d08d00459636b951e3ca2a1e9216318c157331f09c33489d3"}, + {file = "srsly-2.4.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3fd661a1c4848deea2849b78f432a70c75d10968e902ca83c07c89c9b7050ab8"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec37233fe39af97b00bf20dc2ceda04d39b9ea19ce0ee605e16ece9785e11f65"}, + {file = "srsly-2.4.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2fd4bc081f1d6a6063396b6d97b00d98e86d9d3a3ac2949dba574a84e148080"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7347cff1eb4ef3fc335d9d4acc89588051b2df43799e5d944696ef43da79c873"}, + {file = "srsly-2.4.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9dc1da5cc94d77056b91ba38365c72ae08556b6345bef06257c7e9eccabafe"}, + {file = "srsly-2.4.8-cp38-cp38-win_amd64.whl", hash = "sha256:dc0bf7b6f23c9ecb49ec0924dc645620276b41e160e9b283ed44ca004c060d79"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff8df21d00d73c371bead542cefef365ee87ca3a5660de292444021ff84e3b8c"}, + {file = "srsly-2.4.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ac3e340e65a9fe265105705586aa56054dc3902789fcb9a8f860a218d6c0a00"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d1733f4275eff4448e96521cc7dcd8fdabd68ba9b54ca012dcfa2690db2644"}, + {file = "srsly-2.4.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be5b751ad88fdb58fb73871d456248c88204f213aaa3c9aab49b6a1802b3fa8d"}, + {file = "srsly-2.4.8-cp39-cp39-win_amd64.whl", hash = "sha256:822a38b8cf112348f3accbc73274a94b7bf82515cb14a85ba586d126a5a72851"}, + {file = "srsly-2.4.8.tar.gz", hash = "sha256:b24d95a65009c2447e0b49cda043ac53fecf4f09e358d87a57446458f91b8a91"}, ] +[package.dependencies] +catalogue = ">=2.0.3,<2.1.0" + [[package]] name = "syrupy" version = "4.9.1" @@ -870,28 +1719,139 @@ files = [ [package.dependencies] pytest = ">=7.0.0,<9.0.0" +[[package]] +name = "thinc" +version = "8.3.2" +description = "A refreshing functional take on deep learning, compatible with your favorite libraries" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "thinc-8.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6af5b1b57fb1874079f7e84cd99c983c3dcb234a55845d8585d7e066b09755fb"}, + {file = "thinc-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f8b753b63714d38f36e951241466c650afe3177b0c8b220e180ebf4888f09f5e"}, + {file = "thinc-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d471e1261e5e650f93cfae9880928c2ee68ad0426656f02da4490dd24716a93b"}, + {file = "thinc-8.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b46786063787a60a0d732a5d43d0196f632d3d35780c8fe1232d1378b1b5980"}, + {file = "thinc-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:f96c274c4119c92fb8fd8a708381080d47ad92994ef3041c791ed6d4b5c27761"}, + {file = "thinc-8.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:12e998780f40d36d4d5f3b760ef60ac60637643f2965ebe1948801ba44261a03"}, + {file = "thinc-8.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54a5411daaca1718a73982767b714c1d0a5e142de73c916367baf1c13d79e8f0"}, + {file = "thinc-8.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed88275031dcaadd85d3deeb8eb12d1ec0ee6b4679e24cc893c81a30409ac4ee"}, + {file = "thinc-8.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef0868e55108f05300f4508e6896ae4e9492f3415220e3da65579f693225816e"}, + {file = "thinc-8.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:813942d59881c4e4165ce95fef37ba30ce3366dac43289697d13a952a8208854"}, + {file = "thinc-8.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bce8ca6a62ab82f4595210ba7f18bbdb6e33561277c59060f2f04bdb93ac4fbc"}, + {file = "thinc-8.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b014a282e9ea6330a678b472d74f479c7a38168cbf570bdc740e50d960dd78a1"}, + {file = "thinc-8.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80384c228ac6bbaf4ab7f7c9ca4a53c6053f2fb37b2b50c4730b9057f07e9fd"}, + {file = "thinc-8.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ae309b0788478984eafeac4e3c33a2de84a6ea251fd1e3528d8018d4b4347247"}, + {file = "thinc-8.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe8dac2749db23f8ebf09d7a7f29e1b99d67e7d7b183e106aa2b6c9b570f3015"}, + {file = "thinc-8.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4b1e4149a3bfdeb308cee4b53b07d234e5b35495a7f35241b80acf7cb4a33d3"}, + {file = "thinc-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a6b507b1fecd1771fc448aa27dc42d024e5799d10f1ddad6abc6353ae72ef540"}, + {file = "thinc-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4edb20939c3a157beb386aee221a5e1bbfb7ffb90d63d22c80047ca0fa4d026d"}, + {file = "thinc-8.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d701de93d6d6bb029d24088d7f8cb8200f486658fd08dd859767b5eda6eba268"}, + {file = "thinc-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:2977c4811b7612984ded795dce182419b9f3058a1a55c191f75024ec2f4cb218"}, + {file = "thinc-8.3.2.tar.gz", hash = "sha256:3e8ef69eac89a601e11d47fc9e43d26ffe7ef682dcf667c94ff35ff690549aeb"}, +] + +[package.dependencies] +blis = ">=1.0.0,<1.1.0" +catalogue = ">=2.0.4,<2.1.0" +confection = ">=0.0.1,<1.0.0" +cymem = ">=2.0.2,<2.1.0" +murmurhash = ">=1.0.2,<1.1.0" +numpy = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3.9\""} +packaging = ">=20.0" +preshed = ">=3.0.2,<3.1.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +setuptools = "*" +srsly = ">=2.4.0,<3.0.0" +wasabi = ">=0.8.1,<1.2.0" + +[package.extras] +apple = ["thinc-apple-ops (>=1.0.0,<2.0.0)"] +cuda = ["cupy (>=5.0.0b4)"] +cuda-autodetect = ["cupy-wheel (>=11.0.0)"] +cuda100 = ["cupy-cuda100 (>=5.0.0b4)"] +cuda101 = ["cupy-cuda101 (>=5.0.0b4)"] +cuda102 = ["cupy-cuda102 (>=5.0.0b4)"] +cuda110 = ["cupy-cuda110 (>=5.0.0b4)"] +cuda111 = ["cupy-cuda111 (>=5.0.0b4)"] +cuda112 = ["cupy-cuda112 (>=5.0.0b4)"] +cuda113 = ["cupy-cuda113 (>=5.0.0b4)"] +cuda114 = ["cupy-cuda114 (>=5.0.0b4)"] +cuda115 = ["cupy-cuda115 (>=5.0.0b4)"] +cuda116 = ["cupy-cuda116 (>=5.0.0b4)"] +cuda117 = ["cupy-cuda117 (>=5.0.0b4)"] +cuda11x = ["cupy-cuda11x (>=11.0.0)"] +cuda12x = ["cupy-cuda12x (>=11.5.0)"] +cuda80 = ["cupy-cuda80 (>=5.0.0b4)"] +cuda90 = ["cupy-cuda90 (>=5.0.0b4)"] +cuda91 = ["cupy-cuda91 (>=5.0.0b4)"] +cuda92 = ["cupy-cuda92 (>=5.0.0b4)"] +datasets = ["ml-datasets (>=0.2.0,<0.3.0)"] +mxnet = ["mxnet (>=1.5.1,<1.6.0)"] +tensorflow = ["tensorflow (>=2.0.0,<2.6.0)"] +torch = ["torch (>=1.6.0)"] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typer" +version = "0.15.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, + {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" -groups = ["docs"] +groups = ["main", "docs"] files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -900,47 +1860,163 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wasabi" +version = "1.1.3" +description = "A lightweight console printing and formatting toolkit" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c"}, + {file = "wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\" and python_version >= \"3.7\""} + [[package]] name = "watchdog" -version = "3.0.0" +version = "6.0.0" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, - {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, - {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, - {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, - {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, - {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, - {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, - {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, - {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "weasel" +version = "0.4.1" +description = "Weasel: A small and easy workflow system" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "weasel-0.4.1-py3-none-any.whl", hash = "sha256:24140a090ea1ac512a2b2f479cc64192fd1d527a7f3627671268d08ed5ac418c"}, + {file = "weasel-0.4.1.tar.gz", hash = "sha256:aabc210f072e13f6744e5c3a28037f93702433405cd35673f7c6279147085aa9"}, +] + +[package.dependencies] +cloudpathlib = ">=0.7.0,<1.0.0" +confection = ">=0.0.4,<0.2.0" +packaging = ">=20.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" +requests = ">=2.13.0,<3.0.0" +smart-open = ">=5.2.1,<8.0.0" +srsly = ">=2.4.3,<3.0.0" +typer = ">=0.3.0,<1.0.0" +wasabi = ">=0.9.1,<1.2.0" + +[[package]] +name = "wrapt" +version = "1.17.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, +] + [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "5d320818255eb1095176894ba210c5517fd6528950c648c73b80eee9806e64d8" +content-hash = "030176ee2f197b0dd46fa5e4bede1a4e4ffd9bd1da68e4c466ef9590c3f153b0" diff --git a/pyproject.toml b/pyproject.toml index e00a2098..d6816ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,9 @@ safe-ds-stubgen = "safeds_stubgen.main:main" [tool.poetry.dependencies] python = "^3.11" mypy = "^1.6.1" +astroid = "^3.0.0" griffe = ">=0.47.0,<0.49" +spacy = "^3.5.1" [tool.poetry.group.dev.dependencies] pytest = ">=7.4.3,<9.0.0" diff --git a/src/safeds_stubgen/api_analyzer/__init__.py b/src/safeds_stubgen/api_analyzer/__init__.py index 294452ed..984af8dc 100644 --- a/src/safeds_stubgen/api_analyzer/__init__.py +++ b/src/safeds_stubgen/api_analyzer/__init__.py @@ -17,6 +17,8 @@ VarianceKind, WildcardImport, ) +from safeds_stubgen.api_analyzer._extract_boundary_values import extract_boundary +from safeds_stubgen.api_analyzer._extract_valid_values import extract_valid_literals from ._ast_visitor import result_name_generator from ._get_api import get_api from ._mypy_helpers import get_classdef_definitions, get_funcdef_definitions, get_mypyfile_definitions @@ -36,6 +38,7 @@ TypeVarType, UnionType, UnknownType, + EnumType ) __all__ = [ @@ -46,6 +49,9 @@ "Class", "DictType", "Enum", + "EnumType", + "extract_boundary", + "extract_valid_literals", "FinalType", "Function", "ListType", diff --git a/src/safeds_stubgen/api_analyzer/_api.py b/src/safeds_stubgen/api_analyzer/_api.py index 7b8df4c3..5964690c 100644 --- a/src/safeds_stubgen/api_analyzer/_api.py +++ b/src/safeds_stubgen/api_analyzer/_api.py @@ -6,6 +6,8 @@ from enum import Enum as PythonEnum from typing import TYPE_CHECKING, Any +from safeds_stubgen.api_analyzer._types import NamedType + if TYPE_CHECKING: from pathlib import Path @@ -36,7 +38,7 @@ def ensure_file_exists(file: Path) -> None: class API: - def __init__(self, distribution: str, package: str, version: str) -> None: + def __init__(self, distribution: str, package: str, version: str, path_to_package: str) -> None: self.distribution: str = distribution self.package: str = package self.version: str = version @@ -49,6 +51,7 @@ def __init__(self, distribution: str, package: str, version: str) -> None: self.attributes_: dict[str, Attribute] = {} self.parameters_: dict[str, Parameter] = {} self.reexport_map: dict[str, set[Module]] = defaultdict(set) + self.path_to_package: str = path_to_package def add_module(self, module: Module) -> None: self.modules[module.id] = module @@ -56,8 +59,22 @@ def add_module(self, module: Module) -> None: def add_class(self, class_: Class) -> None: self.classes[class_.id] = class_ + def create_astroid_module_path(self, path_str: str) -> str: + package_name = self.package + module_path_list = path_str.split(".") + index_to_split = module_path_list.index(package_name) + module_path = module_path_list[index_to_split:] + correct_module_path = ".".join(module_path) + return correct_module_path + + def create_astroid_id(self, function: Function) -> str: + correct_module_path = self.create_astroid_module_path(function.module_id_which_contains_def) + full_id = function.id.split("/") + id = full_id[-1] + return f"{correct_module_path}.{id}.{function.line}.{function.column}" + def add_function(self, function: Function) -> None: - self.functions[function.id] = function + self.functions[self.create_astroid_id(function)] = function def add_enum(self, enum: Enum) -> None: self.enums[enum.id] = enum @@ -168,6 +185,7 @@ class Class: id: str name: str superclasses: list[str] + subclasses: list[str] is_public: bool docstring: ClassDocstring constructor: Function | None = None @@ -235,8 +253,12 @@ def to_dict(self) -> dict[str, Any]: @dataclass class Function: id: str + module_id_which_contains_def: str + line: int + column: int name: str docstring: FunctionDocstring + body: Body is_public: bool is_static: bool is_class_method: bool @@ -246,6 +268,7 @@ class Function: results: list[Result] = field(default_factory=list) reexported_by: list[Module] = field(default_factory=list) parameters: list[Parameter] = field(default_factory=list) + closures: dict[str, Function] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { @@ -259,8 +282,42 @@ def to_dict(self) -> dict[str, Any]: "results": [result.id for result in self.results], "reexported_by": [module.id for module in self.reexported_by], "parameters": [parameter.id for parameter in self.parameters], + # "closures": [closure.to_dict for closure in self.closures], } +@dataclass +class Body: + line: int + column: int + end_line: int | None + end_column: int | None + call_references: dict[str, CallReference] = field(default_factory=dict) + +@dataclass +class CallReference: + receiver: CallReceiver + function_name: str + line: int + column: int + possibly_referenced_functions: list[Function] = field(default_factory=list) + isSuperCallRef: bool = False + reason_for_no_found_functions: str = "" + fallbackToSignatureCheck: bool = True + +@dataclass +class CallReceiver: + type: Any | NamedType + full_name: str + path_to_call_reference: list[str] + found_classes: list[Class] + isFromParameter: bool = False + typeThroughTypeHint: bool = False + typeThroughDocString: bool = False + typeThroughInference: bool = False + typeOutsideOfPackage: bool = False + decrease: int = 0 + increase: int = 0 + missingTypesWhileFindingFunction: bool = False class UnknownValue: pass diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index de871c24..aed5b1ec 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -5,7 +5,7 @@ from copy import deepcopy from itertools import zip_longest from types import NoneType -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import mypy.nodes as mp_nodes import mypy.types as mp_types @@ -19,6 +19,9 @@ from ._api import ( API, Attribute, + Body, + CallReceiver, + CallReference, Class, Enum, EnumInstance, @@ -69,6 +72,8 @@ def __init__( self.mypy_file: mp_nodes.MypyFile | None = None # We gather type var types used as a parameter type in a function self.type_var_types: set[sds_types.TypeVarType] = set() + self.current_module_id = "" + def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: self.mypy_file = node @@ -121,6 +126,12 @@ def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: # the __init__.py file we set the name to __init__ name = "__init__" if is_package else node.name + package_name = self.api.package + module_path_list = node.fullname.split(".") + index_to_split = module_path_list.index(package_name) + module_path = module_path_list[index_to_split:] + correct_module_path = ".".join(module_path) + self.current_module_id = correct_module_path # Remember module, so we can later add classes and global functions module = Module( id_=id_, @@ -139,7 +150,7 @@ def leave_moduledef(self, _: mp_nodes.MypyFile) -> None: module = self.__declaration_stack.pop() if not isinstance(module, Module): # pragma: no cover raise AssertionError("Imbalanced push/pop on stack") # noqa: TRY004 - + self.current_module_id = "" self.api.add_module(module) def enter_classdef(self, node: mp_nodes.ClassDef) -> None: @@ -219,7 +230,13 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: # Check if the superclass name is an alias and find the real name if superclass_name in self.aliases: _, superclass_alias_qname = self._find_alias(superclass_name) - superclass_qname = superclass_alias_qname if superclass_alias_qname else superclass_qname + if superclass_alias_qname: + superclass_qname = superclass_alias_qname + else: + if superclass_qname: # pragma: no cover + superclass_qname = superclass_qname + else: + superclass_qname = superclass_qname # pragma: no cover superclasses.append(superclass_qname) @@ -241,6 +258,7 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: id=id_, name=node.name, superclasses=superclasses, + subclasses=[], # will be updated after api generation is completed is_public=self._is_public(node.name, node.fullname), docstring=docstring, reexported_by=reexported_by, @@ -264,14 +282,14 @@ def leave_classdef(self, _: mp_nodes.ClassDef) -> None: def enter_funcdef(self, node: mp_nodes.FuncDef) -> None: name = node.name - function_id = self._create_id_from_stack(name) + function_id = self._create_id_from_stack(name) # function id is path is_public = self._is_public(name, node.fullname) is_static = node.is_static - + if name == "global_func_inside_of_lambda_with_map_should_be_pure_but_impure": + pass # Get docstring docstring = self.docstring_parser.get_function_documentation(node) - # Function args & TypeVar parameters: list[Parameter] = [] type_var_types: list[sds_types.TypeVarType] = [] @@ -347,12 +365,42 @@ def enter_funcdef(self, node: mp_nodes.FuncDef) -> None: reexported_by = get_reexported_by(qname=node.fullname, reexport_map=self.api.reexport_map) # Sort for snapshot tests reexported_by.sort(key=lambda x: x.id) - + + parameter_dict = {parameter.name: parameter for parameter in parameters} + + # analyze body for types of receivers of call references + closures: dict[str, Function] = self._extract_closures(node.body, parameter_dict) + call_references: dict[str, CallReference] = {} + try: + function_body = self.extract_body_info(node.body, parameter_dict, call_references) + except RecursionError: # pragma: no cover + # catch Recursion error for sklearn lib, as there are bodies with extremely nested structures, which leads to a recursion error + if node.body is not None: + function_body = Body( + line=node.body.line, + end_line=node.body.end_line, + column=node.body.column, + end_column=node.body.end_column, + call_references=call_references + ) + else: + function_body = Body( + line=-1, + end_line=-1, + column=-1, + end_column=-1, + call_references=call_references + ) + # Create and add Function to stack function = Function( id=function_id, + module_id_which_contains_def=self.current_module_id, + line=node.line, + column=node.column, name=name, docstring=docstring, + body=function_body, is_public=is_public, is_static=is_static, is_class_method=node.is_class, @@ -362,6 +410,7 @@ def enter_funcdef(self, node: mp_nodes.FuncDef) -> None: parameters=parameters, type_var_types=type_var_types, result_docstrings=result_docstrings, + closures=closures, ) self.__declaration_stack.append(function) @@ -499,6 +548,885 @@ def leave_assignmentstmt(self, _: mp_nodes.AssignmentStmt) -> None: # ############################## Utilities ############################## # + # #### Function body analysis utilities + + def _extract_closures(self, body_block: mp_nodes.Block, parameter_of_func: dict[str, Parameter]) -> dict[str, Function]: + """ + extracts all closures so that references to these closures inside of the body block can be found. + + Parameters + ---------- + body_block : mp_nodes.Block | None + Holds info about the current block of code, at first, this is the whole function body + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + + Returns + ---------- + dict[str, Function] + Contains info about the all closures inside of the body block + """ + closures: dict[str, Function] = {} + statements = body_block.body + for statement in statements: + if isinstance(statement, mp_nodes.FuncDef): + closure = self._extract_closure(statement, parameter_of_func) + closures[closure.name] = closure + else: + for member_name in dir(statement): + if not member_name.startswith("__"): + member = getattr(statement, member_name) + if isinstance(member, mp_nodes.FuncDef): # pragma: no cover + closure = self._extract_closure(member, parameter_of_func) + closures[closure.name] = closure + elif isinstance(member, mp_nodes.Block): + closures.update(self._extract_closures(member, parameter_of_func)) + elif isinstance(member, list) and len(member) != 0: + if isinstance(member[0], mp_nodes.Block): + for body in member: + closures.update(self._extract_closures(body, parameter_of_func)) + else: + pass + else: + pass + return closures + + def _extract_closure(self, node: mp_nodes.FuncDef, parameter_of_func: dict[str, Parameter]) -> Function: + """ + extracts a closure + + Parameters + ---------- + node : mp_nodes.FuncDef + Holds info about the current block of code, at first, this is the whole function body + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + + Returns + ---------- + Function + Contains info about a closure inside of the body block + """ + name = node.name + function_id = self._create_id_from_stack(name) # function id is path + + is_public = self._is_public(name, node.fullname) + is_static = node.is_static + + # Get docstring + docstring = self.docstring_parser.get_function_documentation(node) + # Function args & TypeVar + parameters: list[Parameter] = [] + type_var_types: list[sds_types.TypeVarType] = [] + # Reset the type_var_types list + self.type_var_types = set() + if getattr(node, "arguments", None) is not None: + parameters = self._parse_parameter_data(node, function_id) + + if self.type_var_types: # pragma: no cover + type_var_types = list(self.type_var_types) + # Sort for the snapshot tests + type_var_types.sort(key=lambda x: x.name) + + # Check docstring parameter types vs code parameter type hint + for i, parameter in enumerate(parameters): + code_type = parameter.type + doc_type = parameter.docstring.type + + if ( + code_type is not None + and doc_type is not None + and code_type != doc_type + and self.type_source_warning == TypeSourceWarning.WARN + ): # pragma: no cover + msg = f"Different type hint and docstring types for '{function_id}'." + logging.info(msg) + + if doc_type is not None and ( + code_type is None or self.type_source_preference == TypeSourcePreference.DOCSTRING + ): + parameters[i] = dataclasses.replace( + parameter, + is_optional=parameter.docstring.default_value != "", + default_value=parameter.docstring.default_value, + type=doc_type, + ) # pragma: no cover + + # Create results and result docstrings + result_docstrings = self.docstring_parser.get_result_documentation(node.fullname) + results_code = self._parse_results(node, function_id, result_docstrings) + + # Check docstring return type vs code return type hint + i = 0 + for result_type, result_doc in zip_longest(results_code, result_docstrings, fillvalue=None): # pragma: no cover + if result_doc is None: + break + + result_doc_type = result_doc.type + + if ( + result_type is not None + and result_doc_type is not None + and result_type != result_doc_type + and self.type_source_warning == TypeSourceWarning.WARN + ): + msg = f"Different type hint and docstring types for the result of '{function_id}'." + logging.info(msg) + + if result_doc_type is not None: + if result_type is None: + # Add missing returns + result_name = result_doc.name if result_doc.name else f"result_{i + 1}" + new_result = Result(type=result_doc_type, name=result_name, id=f"{function_id}/{result_name}") + results_code.append(new_result) + + elif self.type_source_preference == TypeSourcePreference.DOCSTRING: + # Overwrite the type with the docstring type if preference is set, else prefer the code (default) + results_code[i] = dataclasses.replace(results_code[i], type=result_doc_type) + + i += 1 + + # Get reexported data + reexported_by = get_reexported_by(qname=node.fullname, reexport_map=self.api.reexport_map) + # Sort for snapshot tests + reexported_by.sort(key=lambda x: x.id) + parameter_dict = {parameter.name: parameter for parameter in parameters} + parameter_of_func.update(parameter_dict) + + # limited to depth 1, can be extended by uncommenting + # closures: dict[str, Function] = self._extract_closures(node.body, parameter_of_func) + # function_body = self._extract_body_info(node.body, parameter_of_func, {}) + closures: dict[str, Function] = {} + function_body = Body( + line=-1, + end_line=-1, + column=-1, + end_column=-1, + call_references={} + ) + + function = Function( + id=function_id, + module_id_which_contains_def=self.current_module_id, + line=node.line, + column=node.column, + name=name, + docstring=docstring, + body=function_body, + is_public=is_public, + is_static=is_static, + is_class_method=node.is_class, + is_property=node.is_property, + results=results_code, + reexported_by=reexported_by, + parameters=parameters, + type_var_types=type_var_types, + result_docstrings=result_docstrings, + closures=closures, + ) + return function + + def extract_body_info(self, body_block: mp_nodes.Block | None, parameter_of_func: dict[str, Parameter], call_references: dict[str, CallReference]) -> Body: + """ + Entry point of body extraction + + Searches recursively for members of type mp_nodes.Block or mp_nodes.Expression + For Block, this function is called again and for expression, _traverse_expr + is called. + A call_reference dictionary is passed along to store found call references. + + Parameters + ---------- + body_block : mp_nodes.Block | None + Holds info about the current block of code, at first, this is the whole function body + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + call_references : dict[str, CallReference] + Stores all found call references and is passed along the recursion + + Returns + ---------- + body + Contains info about the function body and especially the call references. This body info + is then stored in Function class + """ + if body_block is None: # pragma: no cover + return Body( + line=-1, + end_line=-1, + column=-1, + end_column=-1, + call_references=call_references + ) + statements = body_block.body + for statement in statements: + for member_name in dir(statement): + if not member_name.startswith("__"): + member = getattr(statement, member_name) + # what about patterns? + if isinstance(member, mp_nodes.Block): + self.extract_body_info(member, parameter_of_func, call_references) + elif isinstance(member, mp_nodes.Expression | mp_nodes.Lvalue): + self.traverse_expr(member, parameter_of_func, call_references) + elif isinstance(member, list) and len(member) != 0: + if isinstance(member[0], mp_nodes.Block): + for body in member: + self.extract_body_info(body, parameter_of_func, call_references) + elif isinstance(member[0], mp_nodes.Expression | mp_nodes.Lvalue): + for expr in member: + self.traverse_expr(expr, parameter_of_func, call_references) + elif isinstance(member[0], list) and len(member[0]) > 0 and isinstance(member[0][0], mp_nodes.Expression): + # generator expression member condlist + for condlist in member: # pragma: no cover + for cond in condlist: + self.traverse_expr(cond, parameter_of_func, call_references) + else: + pass + else: + pass + + return Body( + line=body_block.line, + end_line=body_block.end_line, + column=body_block.column, + end_column=body_block.end_column, + call_references=call_references + ) + + def traverse_expr(self, expr: mp_nodes.Expression | None, parameter_of_func: dict[str, Parameter], call_references: dict[str, CallReference]) -> None: + """ + Entry point of expression extraction + + Searches recursively for members of type mp_nodes.Expression. + Once a call expression is found, another recursion is started, in order to get the type of the + receiver of the call reference. + A call_reference dictionary is passed along to store found call references. + + Parameters + ---------- + expr : mp_nodes.Expression | None + Holds info about the current examined expression + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + call_references : dict[str, CallReference] + Stores all found call references and is passed along the recursion + """ + if isinstance(expr, mp_nodes.CallExpr): + self.traverse_callExpr(expr, [], parameter_of_func, call_references) + return + + if isinstance(expr, mp_nodes.LambdaExpr): + # lambda expressions have a method called expr to get the body of the lamda function + self.traverse_expr(expr.expr(), parameter_of_func, call_references) + + for member_name in dir(expr): + if not member_name.startswith("__") and member_name != "expanded": # expanded stores function itself which leads to infinite recursion + try: + member = getattr(expr, member_name, None) + if isinstance(member, mp_nodes.Expression): + self.traverse_expr(member, parameter_of_func, call_references) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], mp_nodes.Expression): + for expr_of_member in member: + self.traverse_expr(expr_of_member, parameter_of_func, call_references) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], tuple) and len(member[0]) == 2 and isinstance(member[0][1], mp_nodes.Expression): + for tuple_item in member: + for tuple_expr in tuple_item: + if tuple_expr is None: + continue # pragma: no cover + self.traverse_expr(tuple_expr, parameter_of_func, call_references) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], list) and len(member[0]) > 0 and isinstance(member[0][0], mp_nodes.Expression): + # generator expression member condlist + for condlist in member: + for cond in condlist: + self.traverse_expr(cond, parameter_of_func, call_references) + else: + pass + + except AttributeError as err: # fix AttributeError: 'IntExpr' object has no attribute 'operators' # pragma: no cover + logging.warning(f"Member not found with member name: {member_name}, expr: {expr}, error: {err}") + + def traverse_callExpr(self, expr: mp_nodes.CallExpr, path: list[str], parameter_of_func: dict[str, Parameter], call_references: dict[str, CallReference]) -> None: + """ + Entry point of call expression extraction, but also handles nested calls + + A call reference has three attributes, that need to be examined. + - expr.callee: this represents the part of the call reference which comes before "()" so this is the "callee()" + and to find the receiver of this call reference, we need to handle this case separately + - expr.analyzed: is of type Expression and therefore needs to be examined by traverse_expr() + - expr.args: is of type list[Expression] and therefore needs to be examined by traverse_expr() as well + + Parameters + ---------- + expr : mp_nodes.CallExpr + Holds info about the current examined call expression + path : list[str] + A call reference can have a nested receiver, like this for example "receiver.attribute[0].correct_receiver.call() + mypy only stores node info of the receiver at the start of the call expression, so the path is used to store + the names of the attributes or methods, that lead to the call reference + Later in _get_api.py, once the info about all classes is retrieved, the path can be used to find the type + of the correct_receiver + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + call_references : dict[str, CallReference] + Stores all found call references and is passed along the recursion + """ + # start search for type + pathCopy = path.copy() + pathCopy.append("()") + self.traverse_callee(expr.callee, pathCopy, parameter_of_func, call_references) + if expr.analyzed is not None: + self.traverse_expr(expr.analyzed, parameter_of_func, call_references) # pragma: no cover + for arg in expr.args: + self.traverse_expr(arg, parameter_of_func, call_references) + + def traverse_callee(self, expr: mp_nodes.Expression, path: list[str], parameter_of_func: dict[str, Parameter], call_references: dict[str, CallReference]) -> None: + """ + A call reference was found and this function tries to retrieve the type of the receiver of the call + + There are different termination conditions, which this function tries to find. + + condition 1: instance.(...).call_reference() # instance is of type class with member that leads to call_reference + condition 2: func().(...).call_reference() # func() -> Class with member that leads to the call_reference + condition 3: list[0].(...).call_reference() # list[Class], tuple or dict with Class having a member that leads to the call_reference + etc. + But there can also be nested combinations of those conditions. + + If there is no condition to be found, then, all members are searched, whether they are of type expression + + Parameters + ---------- + expr : mp_nodes.Expression + Holds info about the current examined expression + path : list[str] + A call reference can have a nested receiver, like this for example "receiver.attribute[0].correct_receiver.call() + mypy only stores node info of the receiver at the start of the call expression, so the path is used to store + the names of the attributes or methods, that lead to the call reference + Later in _get_api.py, once the info about all classes is retrieved, the path can be used to find the type + of the correct_receiver + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + call_references : dict[str, CallReference] + Stores all found call references and is passed along the recursion + """ + pathCopy = path.copy() + if hasattr(expr, "name"): + pathCopy.append(expr.name) # type: ignore + if isinstance(expr, mp_nodes.IndexExpr): + if isinstance(expr.index, mp_nodes.IntExpr): + key = expr.index.value + pathCopy.append(f"[{str(key)}]") + else: + pathCopy.append("[]") + + # termination conditions + + # condition: callref() + if isinstance(expr, mp_nodes.NameExpr): # here we have no member expression just call ref + self.extract_call_reference_data_from_node(expr, expr.node, pathCopy, parameter_of_func, call_references) + return + + # condition: receiver.member() + elif isinstance(expr, mp_nodes.MemberExpr): + if isinstance(expr.expr, mp_nodes.NameExpr): + pathCopy.append(expr.expr.name) + self.extract_call_reference_data_from_node(expr, expr.expr.node, pathCopy, parameter_of_func, call_references) + return + + # condition: super().__init__() etc + elif isinstance(expr, mp_nodes.SuperExpr): + if isinstance(expr.info, mp_nodes.TypeInfo): + class_that_calls_super = expr.info.fullname + pathCopy.append("()") + pathCopy.append("super") + + self._set_call_reference( + expr=expr, + type=class_that_calls_super, + path=pathCopy, + call_references=call_references, + is_super=True + ) + return + else: + pass # pragma: no cover + + # condition 2: func().(...).call_reference() # func() -> Class with member that leads to the call_reference + elif isinstance(expr, mp_nodes.CallExpr): + if isinstance(expr.callee, mp_nodes.NameExpr): + pathCopy.append("()") + pathCopy.append(expr.callee.name) + self.extract_call_reference_data_from_node(expr, expr.callee.node, pathCopy, parameter_of_func, call_references) + # maybe add check if call reference could not be extracted + for arg in expr.args: + self.traverse_expr(arg, parameter_of_func, call_references) # pragma: no cover + # this is another call ref that needs to be extracted + self.traverse_callExpr(expr, [], parameter_of_func, call_references) + return + # find final receiver + pathCopy.append("()") + self.traverse_callee(expr.callee, pathCopy, parameter_of_func, call_references) + # start finding info of another call expr + newPath: list[str] = [] + self.traverse_callExpr(expr, newPath, parameter_of_func, call_references) + return + + # condition 3: list[0].(...).call_reference() # list[Class] or tuple with Class having a member that leads to the call_reference + # also for tuple and dict + # here we can also have nested types that ultimately lead to Class being used + elif isinstance(expr, mp_nodes.IndexExpr): + if isinstance(expr.base, mp_nodes.NameExpr): + # add [] to path is already done above + pathCopy.append(expr.base.name) + self.extract_call_reference_data_from_node(expr, expr.base.node, pathCopy, parameter_of_func, call_references) + self.traverse_expr(expr.index, parameter_of_func, call_references) + return + elif isinstance(expr, mp_nodes.OpExpr): + pathCopy.append(f"${expr.op}$") + self.extract_call_reference_data_from_node(expr, expr.method_type, pathCopy, parameter_of_func, call_references) + self.traverse_expr(expr.left, parameter_of_func, call_references) + self.traverse_expr(expr.right, parameter_of_func, call_references) + return + else: + pass + + found_expression = False + for member_name in dir(expr): + if not member_name.startswith("__"): + member = getattr(expr, member_name, None) + if isinstance(member, mp_nodes.Expression): + self.traverse_callee(member, pathCopy, parameter_of_func, call_references) + found_expression = True + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], mp_nodes.Expression): # pragma: no cover + for expr_of_member in member: + self.traverse_callee(expr_of_member, pathCopy, parameter_of_func, call_references) + found_expression = True + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], tuple) and len(member[0]) == 2 and isinstance(member[0][1], mp_nodes.Expression): # pragma: no cover + for tuple_item in member: + for tuple_expr in tuple_item: + if tuple_expr is None: + continue + self.traverse_callee(tuple_expr, pathCopy, parameter_of_func, call_references) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], list) and len(member[0]) > 0 and isinstance(member[0][0], mp_nodes.Expression): # pragma: no cover + # generator expression member condlist + for condlist in member: + for cond in condlist: + self.traverse_callee(cond, pathCopy, parameter_of_func, call_references) + found_expression = True + else: + pass + if not found_expression: # so expr is the final expression and the receiver of the call + pathCopy.append("not_implemented") + self.extract_call_reference_data_from_node(expr, "None", pathCopy, parameter_of_func, call_references) + + def extract_call_reference_data_from_node( + self, + expr: mp_nodes.Expression, + node: mp_nodes.SymbolNode | mp_types.Type | str | None, + path: list[str], + parameter_of_func: dict[str, Parameter], + call_references: dict[str, CallReference] + ) -> None: + """ + Helper function to extract typeinfo and to set the callreference + + Parameters + ---------- + expr : mp_nodes.Expression + Holds info about the current examined expression + node : mp_nodes.SymbolNode | mp_types.Type | str | None + The Node that contains type info + path : list[str] + A call reference can have a nested receiver, like this for example "receiver.attribute[0].correct_receiver.call() + mypy only stores node info of the receiver at the start of the call expression, so the path is used to store + the names of the attributes or methods, that lead to the call reference + Later in _get_api.py, once the info about all classes is retrieved, the path can be used to find the type + of the correct_receiver + parameter_of_func : dict[str, Parameter] + Contains the parameter of the function which the body belongs to, can be used if mypy has no + type info about the parameter + call_references : dict[str, CallReference] + Stores all found call references and is passed along the recursion + """ + possible_reason_for_no_found_functions = f"{str(expr)} " + if node is None: # pragma: no cover + possible_reason_for_no_found_functions += "Type node is none " + call_receiver_type_none = "None" + self._set_call_reference( + expr=expr, + type=call_receiver_type_none, + path=path, + call_references=call_references + ) + elif isinstance(node, str): + possible_reason_for_no_found_functions += "Mypy Node is a string " + if node == "None": + possible_reason_for_no_found_functions += f"There is no end condition for {str(expr)} " + + call_receiver_type_str = node + self._set_call_reference( + expr=expr, + type=call_receiver_type_str, + path=path, + call_references=call_references, + typeThroughInference=True + ) + elif isinstance(node, mp_types.Type): + call_receiver_type_type: mp_types.Type | str = node + possible_reason_for_no_found_functions += "Mypy Node is a mp_types.Type " + + if isinstance(call_receiver_type_type, mp_types.AnyType): + possible_reason_for_no_found_functions += "Type is Any " + if call_receiver_type_type.missing_import_name is not None: + call_receiver_type_type = call_receiver_type_type.missing_import_name # pragma: no cover + else: + possible_reason_for_no_found_functions += "No missing import name " + abstact_type = self.mypy_type_to_abstract_type(node) + typeThroughInference = not isinstance(call_receiver_type_type, mp_types.AnyType) or (isinstance(call_receiver_type_type, mp_types.AnyType) and call_receiver_type_type.missing_import_name is not None) + + self._set_call_reference( + expr=expr, + type=abstact_type, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=typeThroughInference + ) + return + elif isinstance(node, mp_nodes.FuncDef) and len(path) == 2: + # here a global function is referenced that is in the same module + call_receiver_type_funcdef = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_funcdef, + path=path, + call_references=call_references, + typeThroughInference=True + ) + return + elif isinstance(node, mp_nodes.FuncDef): + call_receiver_type_funcdef_full: Any = None + possible_reason_for_no_found_functions += "" + typeThroughTypeHint = False + typeThroughDocString = False + typeThroughInference = False + isFromParameter = False + parameter_type = None + parameter = parameter_of_func.get(node.fullname) + if parameter is not None and (parameter.type is not None or parameter.docstring.type is not None): # pragma: no cover + if parameter.type is not None: + parameter_type = parameter.type + elif parameter.docstring.type is not None: + parameter_type = parameter.docstring.type + + isFromParameter = True + typeThroughTypeHint = parameter.type is not None + typeThroughDocString = parameter.docstring.type is not None + if node.type is not None: # pragma: no cover + call_receiver_type_funcdef_full = self.mypy_type_to_abstract_type(node.type.ret_type) # type: ignore + if isinstance(call_receiver_type_funcdef_full, mp_types.AnyType): + possible_reason_for_no_found_functions += "Type is Any " + if call_receiver_type_funcdef_full.missing_import_name is not None: + call_receiver_type_funcdef_full = call_receiver_type_funcdef_full.missing_import_name + else: + possible_reason_for_no_found_functions += "No missing import name " + call_receiver_type_funcdef_full = node.fullname if parameter_type is None else parameter_type + + typeThroughInference = not isinstance(node.type.ret_type, mp_types.AnyType) or (isinstance(node.type.ret_type, mp_types.AnyType) and node.type.ret_type.missing_import_name is not None) # type: ignore + if isinstance(node.type.ret_type, sds_types.NamedType) and node.type.ret_type.name == "Any": # type: ignore + typeThroughInference = False + + else: # pragma: no cover + possible_reason_for_no_found_functions += "Node.type was None for FuncDef" + call_receiver_type_funcdef_full = node.fullname if parameter_type is None else parameter_type + + self._set_call_reference( + expr=expr, + type=call_receiver_type_funcdef_full, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughDocString=typeThroughDocString, + typeThroughInference=typeThroughInference, + typeThroughTypeHint=typeThroughTypeHint, + isFromParameter=isFromParameter + ) + return + elif isinstance(node, mp_nodes.Var): + possible_reason_for_no_found_functions += "" + typeThroughTypeHint = False + typeThroughDocString = False + typeThroughInference = False + isFromParameter = False + parameter_type = None + parameter = parameter_of_func.get(node.fullname) + if parameter is not None and (parameter.type is not None or parameter.docstring.type is not None): + if parameter.type is not None: + parameter_type = parameter.type + elif parameter.docstring.type is not None: # pragma: no cover + parameter_type = parameter.docstring.type + + isFromParameter = True + typeThroughTypeHint = parameter.type is not None + typeThroughDocString = parameter.docstring.type is not None + if node.type is not None: + call_receiver_type_var: Any = self.mypy_type_to_abstract_type(node.type) + if isinstance(node.type, mp_types.AnyType): + # analyzing static methods, mypy sets the type as Any but with the fullname we can retrieve the type + possible_reason_for_no_found_functions += "Type is Any " + if node.type.missing_import_name is not None: + call_receiver_type_var = node.type.missing_import_name # pragma: no cover + else: + possible_reason_for_no_found_functions += "No missing import name " + call_receiver_type_var = node.fullname if parameter_type is None else parameter_type + + typeThroughInference = not isinstance(node.type, mp_types.AnyType) or (isinstance(node.type, mp_types.AnyType) and node.type.missing_import_name is not None) + if isinstance(node.type, sds_types.NamedType) and node.type.name == "Any": + typeThroughInference = False # pragma: no cover + else: + possible_reason_for_no_found_functions += "Node.type was None for Var " + call_receiver_type_var = node.fullname if parameter_type is None else parameter_type + + self._set_call_reference( + expr=expr, + type=call_receiver_type_var, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughDocString=typeThroughDocString, + typeThroughInference=typeThroughInference, + typeThroughTypeHint=typeThroughTypeHint, + isFromParameter=isFromParameter + ) + return + elif isinstance(node, mp_nodes.TypeAlias): # pragma: no cover + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.TypeAlias " + call_receiver_type_type_alias: Any = node.target + if isinstance(call_receiver_type_type_alias, mp_types.AnyType): + if call_receiver_type_type_alias.missing_import_name is not None: + call_receiver_type_type_alias = call_receiver_type_type_alias.missing_import_name + else: + call_receiver_type_type_alias = node.fullname + typeThroughInference = not isinstance(call_receiver_type_type_alias, mp_types.AnyType) or (isinstance(call_receiver_type_type_alias, mp_types.AnyType) and call_receiver_type_type_alias.missing_import_name is not None) + self._set_call_reference( + expr=expr, + type=call_receiver_type_type_alias, + path=path, + call_references=call_references, + typeThroughInference=typeThroughInference, + ) + return + elif isinstance(node, mp_nodes.Decorator): # pragma: no cover + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.Decorator " + call_receiver_type_decorator = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_decorator, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=True, + ) + return + elif isinstance(node, mp_nodes.TypeVarLikeExpr): # pragma: no cover + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.TypeVarLikeExpr " + call_receiver_type_typeVarLikeExpr = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_typeVarLikeExpr, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=True, + ) + return + elif isinstance(node, mp_nodes.PlaceholderNode): # pragma: no cover + return + elif isinstance(node, mp_nodes.OverloadedFuncDef): + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.OverloadedFuncDef " + + call_receiver_type_overloadedFuncdef = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_overloadedFuncdef, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=True, + ) + return + elif isinstance(node, mp_nodes.TypeInfo): + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.TypeInfo " + call_receiver_type_typeInfo = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_typeInfo, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=True, + ) + return + elif isinstance(node, mp_nodes.MypyFile): + possible_reason_for_no_found_functions += "Mypy Node is a mp_nodes.MypyFile " + call_receiver_type_mypyFile = node.fullname + self._set_call_reference( + expr=expr, + type=call_receiver_type_mypyFile, + path=path, + call_references=call_references, + possible_reason_for_no_found_functions=possible_reason_for_no_found_functions, + typeThroughInference=True, + ) + return + else: + return # pragma: no cover + + def _get_named_types_from_nested_type(self, nested_type: AbstractType) -> list[sds_types.NamedType | sds_types.NamedSequenceType] | None: # pragma: no cover + """ + Iterates through a nested type recursively, to find all NamedTypes + + Parameters + ---------- + nested_type : AbstractType + Abstract class for types + + Returns + ---------- + type : list[NamedType] | None + """ + if isinstance(nested_type, sds_types.NamedType): + return [nested_type] + elif isinstance(nested_type, sds_types.ListType): + if len(nested_type.types) == 0: + return None + return self._get_named_types_from_nested_type(nested_type.types[0]) # a list can only have one type + elif isinstance(nested_type, sds_types.NamedSequenceType): + if len(nested_type.types) == 0: + return None + return [nested_type] + elif isinstance(nested_type, sds_types.DictType): + return self._get_named_types_from_nested_type(nested_type.value_type) + elif isinstance(nested_type, sds_types.SetType): + if len(nested_type.types) == 0: + return None + return self._get_named_types_from_nested_type(nested_type.types[0]) # a set can only have one type + elif isinstance(nested_type, sds_types.FinalType): + return self._get_named_types_from_nested_type(nested_type.type_) + elif isinstance(nested_type, sds_types.CallableType): + return self._get_named_types_from_nested_type(nested_type.return_type) + elif isinstance(nested_type, sds_types.UnionType): + result = [] + for type in nested_type.types: + extracted_types = self._get_named_types_from_nested_type(type) + if extracted_types is None: + continue + result.extend(extracted_types) + return result + elif isinstance(nested_type, sds_types.TupleType): + result = [] + for type in nested_type.types: + extracted_types = self._get_named_types_from_nested_type(type) + if extracted_types is None: + continue + result.extend(extracted_types) + return result + + for member_name in dir(nested_type): + if not member_name.startswith("__"): + member = getattr(nested_type, member_name) + if isinstance(member, sds_types.AbstractType): + return self._get_named_types_from_nested_type(member) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], sds_types.AbstractType): + types: list[sds_types.NamedType | sds_types.NamedSequenceType] = [] + for type in member: + named_type = self._get_named_types_from_nested_type(type) + if named_type is not None: + types.extend(named_type) + return list(filter(lambda type: not type.qname.startswith("builtins"), list(set(types)))) + return None + + def _set_call_reference(self, + expr: mp_nodes.Expression, + type: Any | list[sds_types.NamedType | sds_types.NamedSequenceType], # can also be List of types for union type + path: list[str], + call_references: dict[str, CallReference], + is_super: bool = False, + possible_reason_for_no_found_functions: str = "", + typeThroughTypeHint: bool = False, + typeThroughDocString: bool = False, + typeThroughInference: bool = False, + isFromParameter: bool = False, + ) -> None: + """ + Helper function, to set a callreference into the call_references dictionary + + Parameters + ---------- + expr : mp_nodes.Expression + Current expression, will be used to get the line and column + full_name : str + The full name of the call_reference, is also used as id + type : Any | list[sds_types.NamedType | sds_types.NamedSequenceType] + The type of the receiver, if type Any, then its the type from mypy and if NamedType then from parameter + path : list[str] + The path from the receiver to the call reference + call_references : dict[str, CallReference] + Dictionary of found call references + """ + try: + function_name = list(filter(lambda part: part != "()" and part != "[]", path))[0] + except IndexError as error: # pragma: no cover + print(error) + return + full_name = "" + if isinstance(type, list) and len(type) == 1 and (isinstance(type[0], sds_types.NamedType) or isinstance(type[0], sds_types.NamedSequenceType)): # pragma: no cover + full_name = type[0].qname + type = type[0] + elif isinstance(type, list) and len(type) > 1 and (isinstance(type[0], sds_types.NamedType) or isinstance(type[0], sds_types.NamedSequenceType)): # pragma: no cover + full_name = "+".join(list(map(lambda x: x.qname, type))) + elif isinstance(type, sds_types.NamedType): # pragma: no cover + full_name = type.qname + elif hasattr(type, "type"): # pragma: no cover + full_name = type.type.fullname # type: ignore + elif hasattr(type, "fullname"): # pragma: no cover + full_name = type.fullname # type: ignore + elif hasattr(type, "name"): # pragma: no cover + full_name = type.name # type: ignore + elif isinstance(type, str): # pragma: no cover + full_name = type + + if isinstance(full_name, mp_types.NoneType): + full_name = "None" # pragma: no cover + if not isinstance(full_name, str): + full_name = "" # pragma: no cover + call_receiver = CallReceiver( + full_name=full_name, + type=type, + path_to_call_reference=path, + found_classes=[], + typeThroughTypeHint=typeThroughTypeHint, + typeThroughDocString=typeThroughDocString, + typeThroughInference=typeThroughInference, + isFromParameter=isFromParameter, + ) # found Classes will later be found + call_reference = CallReference( + column=expr.column, + line=expr.line, + receiver=call_receiver, + function_name=function_name, + isSuperCallRef=is_super, + reason_for_no_found_functions=possible_reason_for_no_found_functions + ) + id = f"{function_name}.{expr.line}.{expr.column}" + if call_references.get(id) is None: + call_references[id] = call_reference + # #### Result utilities def _parse_results( @@ -747,7 +1675,7 @@ def _create_inferred_results( function has the following returns "return 42" and "return True, 1.2" we would have to group the integer and boolean as "result_1: Union[int, bool]" and the float number as "result_2: Union[float, None]". - Paramters + Parameters --------- ret_type: An object representing a tuple with all inferred types. @@ -775,7 +1703,7 @@ def _create_inferred_results( if type__ not in result_array[i]: result_array[i].append(type__) - longest_inner_list = max(len(result_array[i]), longest_inner_list) + longest_inner_list = max(longest_inner_list, len(result_array[i])) else: result_array.append([type__]) @@ -1230,8 +2158,8 @@ def mypy_type_to_abstract_type( and "." in unanalyzed_type.name and unanalyzed_type.name.startswith(missing_import_name) ): - name = unanalyzed_type.name.split(".")[-1] - qname = unanalyzed_type.name.replace(missing_import_name, qname) + name = unanalyzed_type.name.split(".")[-1] # pragma: no cover + qname = unanalyzed_type.name.replace(missing_import_name, qname) # pragma: no cover if not qname: # pragma: no cover logging.info("Could not parse a type, added unknown type instead.") @@ -1452,18 +2380,12 @@ def _check_publicity_in_reexports(self, name: str, qname: str, parent: Module | continue # If the whole module was reexported we have to check if the name or alias is intern - if module_is_reexported: + if module_is_reexported and not_internal and (isinstance(parent, Module) or parent.is_public): # Check the wildcard imports of the source for wildcard_import in reexport_source.wildcard_imports: - if ( - ( - (is_from_same_package and wildcard_import.module_name == module_name) - or (is_from_another_package and wildcard_import.module_name == module_qname) - ) - and not_internal - and (isinstance(parent, Module) or parent.is_public) - ): + if ((is_from_same_package and wildcard_import.module_name == module_name) + or (is_from_another_package and wildcard_import.module_name == module_qname)): return True # Check the qualified imports of the source @@ -1474,11 +2396,9 @@ def _check_publicity_in_reexports(self, name: str, qname: str, parent: Module | if ( qualified_import.qualified_name in {module_name, module_qname} and ( - (qualified_import.alias is None and not_internal) + qualified_import.alias is None or (qualified_import.alias is not None and not is_internal(qualified_import.alias)) ) - and not_internal - and (isinstance(parent, Module) or parent.is_public) ): # If the module name or alias is not internal, check if the parent is public return True diff --git a/src/safeds_stubgen/api_analyzer/_extract_boundary_values.py b/src/safeds_stubgen/api_analyzer/_extract_boundary_values.py new file mode 100644 index 00000000..696ffdef --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/_extract_boundary_values.py @@ -0,0 +1,842 @@ +"""Boundary Extractor.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, TypeAlias + +from numpy import inf +from spacy.matcher import Matcher +from spacy.language import Language +from spacy.tokens import Doc, Span +from spacy import load +from spacy.cli.download import download + +from ._types import BoundaryType + +if TYPE_CHECKING: + from spacy.tokens import Doc, Span + + +def load_language(name: str) -> Language: + """ + Safely load a Spacy language model. + + Parameters + ---------- + name: str + The name of the language model to load. + + Returns + ------- + spacy.Language + The loaded language model. + """ + try: + return load(name) + except OSError: # pragma: no cover + download(name) + return load(name) + + + + +_Numeric: TypeAlias = int | float + + +@dataclass +class BoundaryList: + _boundaries: set[BoundaryType] = field(default_factory=set[BoundaryType]) + + def add_boundary(self, match_label: str, type_: str, match_string: Span | None = None) -> None: + """Add a boundary according to the matched rule. + + Parameters + ---------- + match_label + Label of the matched rule + type_ + Base type of the boundary to be created + match_string + Span containing the string matched by the corresponding rule. + This parameter is not required for every rule. + + """ + match match_label: + case "BOUNDARY_NON_POSITIVE": + self._boundaries.add(_create_non_positive_boundary(type_)) + case "BOUNDARY_POSITIVE": + self._boundaries.add(_create_positive_boundary(type_)) + case "BOUNDARY_NON_NEGATIVE": + self._boundaries.add(_create_non_negative_boundary(type_)) + case "BOUNDARY_NEGATIVE": + self._boundaries.add(_create_negative_boundary(type_)) # pragma: no cover + case "BOUNDARY_BETWEEN": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + self._boundaries.add(_create_between_boundary(match_string, type_)) + case "BOUNDARY_INTERVAL": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + boundary = _create_interval_boundary(match_string, type_) + if boundary is not None: + self._boundaries.add(boundary) + case "BOUNDARY_AT_LEAST": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + self._boundaries.add(_create_at_least_boundary(match_string, type_)) + case "BOUNDARY_INTERVAL_RELATIONAL": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + self._boundaries.add(_create_interval_relational_boundary(match_string, type_)) + case "BOUNDARY_TYPE_REL_VAL": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + self._boundaries.add(_create_type_rel_val_boundary(match_string, type_)) + case "BOUNDARY_INTERVAL_IN_BRACKETS": + if match_string is None: + raise TypeError(f"match_string can't be None if match_label is: {match_label}") # pragma: no cover + boundary = _create_interval_in_brackets_boundary(match_string, type_) + if boundary is not None: + self._boundaries.add(boundary) + + def get_boundaries(self) -> set[BoundaryType]: + return self._boundaries + + +type_funcs = {"float": float, "int": int} + +_nlp = load_language("en_core_web_sm") +_matcher = Matcher(_nlp.vocab) + +_rel_ops = {"ORTH": {"IN": ["GT$", "LT$", "GEQ$", "LEQ$"]}} + +_boundary_type = {"LOWER": {"IN": ["float", "int"]}} + +_boundary_type_rel_val = [_boundary_type, _rel_ops, {"LIKE_NUM": True}] + +_boundary_rel_interval = [{"LIKE_NUM": True}, _rel_ops, {}, _rel_ops, {"LIKE_NUM": True}] + +_boundary_and_rel_interval = [ + _rel_ops, + {"LIKE_NUM": True}, + {"ORTH": {"IN": ["and", "or"]}}, + _rel_ops, + {"LIKE_NUM": True}, +] + +_boundary_at_least = [{"LOWER": "at"}, {"LOWER": "least"}, {"LIKE_NUM": True}] + +_boundary_min = [{"LOWER": "min"}, {"ORTH": "."}, {"LIKE_NUM": True}] + +_boundary_interval = [ + {"LOWER": {"IN": ["in", "within"]}}, + {"LOWER": "the", "OP": "?"}, + {"LOWER": {"IN": ["range", "interval"]}, "OP": "?"}, + {"LOWER": "of", "OP": "?"}, + {"ORTH": {"IN": ["(", "["]}}, + {}, + {"ORTH": ","}, + {}, + {"ORTH": {"IN": [")", "]"]}}, +] + + +_boundary_value_in = [ + {"LOWER": {"FUZZY": "value"}}, + {"LOWER": {"IN": ["is", "in"]}}, + {"ORTH": {"IN": ["(", "["]}}, + {}, + {"ORTH": ","}, + {}, + {"ORTH": {"IN": [")", "]"]}}, +] + + +_boundary_non_negative = [ + {"LOWER": {"IN": ["non", "not"]}}, + {"ORTH": {"IN": ["-", "_"]}, "OP": "?"}, + {"LOWER": "negative"}, +] + +_boundary_positive = [{"LOWER": "strictly", "OP": "?"}, {"LOWER": "positive"}] + +_boundary_non_positive = [ + {"LOWER": {"IN": ["non", "not"]}}, + {"ORTH": {"IN": ["-", "_"]}, "OP": "?"}, + {"LOWER": "positive"}, +] + +_boundary_negative = [{"LOWER": "strictly", "OP": "?"}, {"LOWER": "negative"}] + +_boundary_between = [ + {"LOWER": "strictly", "OP": "?"}, + {"LOWER": "between"}, + {"LIKE_NUM": True}, + {"LOWER": "and"}, + {"LIKE_NUM": True}, +] + +_boundary_interval_in_brackets = [ + _boundary_type, + {"ORTH": "("}, + {"ORTH": {"IN": ["(", "["]}}, + {}, + {"ORTH": ","}, + {}, + {"ORTH": {"IN": [")", "]"]}}, + {"ORTH": ")"}, +] + + +def _check_negative_pattern( + matcher: Matcher, # noqa: ARG001 + doc: Doc, # noqa: ARG001 + i: int, + matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Delete the BOUNDARY_NEGATIVE match if the BOUNDARY_NON_NEGATIVE rule has already been detected. + + Parameters + ---------- + matcher + Parameter is ignored. + doc + Parameter is ignored. + i + Index of the match that was recognized by the rule. + + matches + List of matches found by the matcher + + """ + previous_id, _, _ = matches[i - 1] + if _nlp.vocab.strings[previous_id] == "BOUNDARY_NON_NEGATIVE": + matches.remove(matches[i]) + + return None + + +def _check_positive_pattern( + matcher: Matcher, # noqa: ARG001 + doc: Doc, # noqa: ARG001 + i: int, + matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Delete the BOUNDARY_POSITIVE match if the BOUNDARY_NON_POSITIVE rule has already been detected. + + Parameters + ---------- + matcher + Parameter is ignored. + doc + Parameter is ignored. + i + Index of the match that was recognized by the rule. + + matches + List of matches found by the matcher + + """ + previous_id, _, _ = matches[i - 1] + if _nlp.vocab.strings[previous_id] == "BOUNDARY_NON_POSITIVE": + matches.remove(matches[i]) + + return None + + +def _check_interval_relational_pattern( + matcher: Matcher, # noqa: ARG001 + doc: Doc, # noqa: ARG001 + i: int, + matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Delete the BOUNDARY_TYPE_REL_VAL match if the BOUNDARY_INTERVAL_RELATIONAL rule has been detected. + + Parameters + ---------- + matcher + Parameter is ignored. + doc + Parameter is ignored. + i + Index of the match that was recognized by the rule. + + matches + List of matches found by the matcher + + """ + previous_id, _, _ = matches[i - 1] + if _nlp.vocab.strings[previous_id] == "BOUNDARY_TYPE_REL_VAL": + matches.remove(matches[i - 1]) # pragma: no cover + + return None + + +def _check_interval( + matcher: Matcher, # noqa: ARG001 + doc: Doc, # noqa: ARG001 + i: int, + matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Delete the BOUNDARY_INTERVAL match if the BOUNDARY_INTERVAL rule has been already detected. + + Parameters + ---------- + matcher + Parameter is ignored. + doc + Parameter is ignored. + i + Index of the match that was recognized by the rule. + + matches + List of matches found by the matcher + + """ + previous_id, _, _ = matches[i - 1] + if _nlp.vocab.strings[previous_id] == "BOUNDARY_INTERVAL" and (len(matches) > 1): + matches.remove(matches[i - 1]) + + return None + + +_matcher.add("BOUNDARY_AT_LEAST", [_boundary_at_least, _boundary_min]) +_matcher.add("BOUNDARY_INTERVAL", [_boundary_interval, _boundary_value_in], on_match=_check_interval) +_matcher.add("BOUNDARY_POSITIVE", [_boundary_positive], on_match=_check_positive_pattern) +_matcher.add("BOUNDARY_NON_NEGATIVE", [_boundary_non_negative]) +_matcher.add("BOUNDARY_NEGATIVE", [_boundary_negative], on_match=_check_negative_pattern) +_matcher.add("BOUNDARY_NON_POSITIVE", [_boundary_non_positive]) +_matcher.add("BOUNDARY_BETWEEN", [_boundary_between], greedy="LONGEST") +_matcher.add( + "BOUNDARY_INTERVAL_RELATIONAL", + [_boundary_rel_interval, _boundary_and_rel_interval], + on_match=_check_interval_relational_pattern, +) +_matcher.add("BOUNDARY_TYPE", [[_boundary_type]]) +_matcher.add("BOUNDARY_TYPE_REL_VAL", [_boundary_type_rel_val]) +_matcher.add("BOUNDARY_INTERVAL_IN_BRACKETS", [_boundary_interval_in_brackets]) + + +def _get_type_value(type_: str, value: _Numeric | str) -> _Numeric: + """Transform the passed value to the value matching type_. + + Parameters + ---------- + type_ + Type to be transformed to. + value + Value to be transformed. + + Returns + ------- + Numeric + Transformed value. + """ + numbers = { + "zero": 0, + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + } + if isinstance(value, str) and value.lower() in numbers: + value = numbers[value] # pragma: no cover + + return type_funcs[type_.lower()](value) + + +def _create_non_positive_boundary(type_: str) -> BoundaryType: + """Create a BoundaryType with predefined extrema. + + Create a BoundaryType that describes the non-positive value range of the given type. + + Parameters + ---------- + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + return BoundaryType( + type_, + min=BoundaryType.NEGATIVE_INFINITY, + max=_get_type_value(type_, 0), + min_inclusive=False, + max_inclusive=True, + ) + + +def _create_positive_boundary(type_: str) -> BoundaryType: + """Create a BoundaryType with predefined extrema. + + Create a BoundaryType that describes the positive value range of the given type. + + Parameters + ---------- + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + return BoundaryType( + type_, + min=_get_type_value(type_, 0), + max=BoundaryType.INFINITY, + min_inclusive=False, + max_inclusive=False, + ) + + +def _create_non_negative_boundary(type_: str) -> BoundaryType: + """Create a BoundaryType with predefined extrema. + + Create a BoundaryType that describes the non-negative value range of the given type. + + Parameters + ---------- + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + return BoundaryType( + type_, + min=_get_type_value(type_, 0), + max=BoundaryType.INFINITY, + min_inclusive=True, + max_inclusive=False, + ) + + +def _create_negative_boundary(type_: str) -> BoundaryType: + """Create a BoundaryType with predefined extrema. + + Create a BoundaryType that describes the negative value range of the given type. + + Parameters + ---------- + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + # return type_, ("negative infinity", False), (_get_type_value(type_, 0), False) + return BoundaryType( + type_, + min=BoundaryType.NEGATIVE_INFINITY, + max=_get_type_value(type_, 0), + min_inclusive=False, + max_inclusive=False, + ) # pragma: no cover + + +def _create_between_boundary(match_string: Span, type_: str) -> BoundaryType: + """Create a BoundaryType with individual extrema. + + Create a BoundaryType whose extrema are extracted from the passed match string. + + Parameters + ---------- + match_string + Match string containing the extrema of the value range. + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + values = [] + min_incl = True + max_incl = True + for token in match_string: + if token.text == "strictly": + min_incl = False + max_incl = False + if token.like_num: + values.append(_get_type_value(type_, token.text)) + return BoundaryType(type_, min=min(values), max=max(values), min_inclusive=min_incl, max_inclusive=max_incl) + + +def _create_at_least_boundary(match_string: Span, type_: str) -> BoundaryType: + """Create a BoundaryType with individual minimum. + + Create a BoundaryType whose minimum is extracted from the passed match string. + + Parameters + ---------- + match_string + Match string containing the minimum of the value range. + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + value: _Numeric = 0 + for token in match_string: + if token.like_num: + value = _get_type_value(type_, token.text) + return BoundaryType(type_, min=value, max=BoundaryType.INFINITY, min_inclusive=True, max_inclusive=False) + + +def _create_interval_boundary(match_string: Span, type_: str) -> BoundaryType | None: + """Create a BoundaryType with individual extrema. + + Create a BoundaryType whose extrema are extracted from the passed match string. + + Parameters + ---------- + match_string + Match string containing the extrema of the value range. + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + scientific_notation = r"[+\-]?\d+[eE][+\-]?\d+" + values = [] + brackets = [] + for token in match_string: + if token.text in ["(", "[", ")", "]"]: + brackets.append(token.text) + if token.like_num: + values.append(_get_type_value(type_, token.text)) + + if token.text in ["inf", "infty", "infinty"]: + values.append(inf) + elif token.text in ["negative inf", "negative infty", "negative infinity"]: + values.append(-inf) # pragma: no cover + elif re.match(scientific_notation, token.text) is not None: + values.append(float(token.text)) + + if len(values) != 2: + return None # pragma: no cover + + type_func = type_funcs[type_] + + if -inf in values: # pragma: no cover + minimum = BoundaryType.NEGATIVE_INFINITY + min_incl = False + else: + minimum = type_func(min(values)) + min_incl = brackets[0] == "[" + + if inf in values: + maximum = BoundaryType.INFINITY + max_incl = False + else: + maximum = type_func(max(values)) + max_incl = brackets[1] == "]" + + return BoundaryType(type_, min=minimum, max=maximum, min_inclusive=min_incl, max_inclusive=max_incl) + + +def _create_interval_relational_boundary(match_string: Span, type_: str) -> BoundaryType: + """Create a BoundaryType with individual extrema. + + Create a BoundaryType whose extrema are extracted from the passed match string. + + Parameters + ---------- + match_string + Match string containing the extrema of the value range. + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + relational_ops = [] + values = [] + and_or_found = False + + for token in match_string: + if token.like_num: + values.append(token.text) + else: + match token.text: + case "LT$": + relational_ops.append("<") + case "GT$": + relational_ops.append(">") + case "LEQ$": + relational_ops.append("<=") + case "GEQ$": + relational_ops.append(">=") + case "and" | "or": + and_or_found = True + + type_func = type_funcs[type_] + + minimum = type_func(min(values)) + maximum = type_func(max(values)) + + if not and_or_found: + min_incl = (relational_ops[0] == "<=") or (relational_ops[1] == ">=") + max_incl = (relational_ops[1] == "<=") or (relational_ops[0] == ">=") + else: + min_incl = ">=" in relational_ops + max_incl = "<=" in relational_ops + + return BoundaryType(type_, min=minimum, max=maximum, min_inclusive=min_incl, max_inclusive=max_incl) + + +def _create_type_rel_val_boundary(match_string: Span, type_: str) -> BoundaryType: + """Create a BoundaryType with individual minimum or maximum. + + Create a BoundaryType whose minimum or maximum is extracted from the passed match string. + + Parameters + ---------- + match_string + Match string containing the extrema of the value range. + type_ + Base type of Boundary + + Returns + ------- + BoundaryType + + """ + val: _Numeric = 0 + min_: _Numeric | str = 0 + max_: _Numeric | str = 0 + + rel_op = "" + type_func = type_funcs[type_] + min_incl = False + max_incl = False + + for token in match_string: + if token.like_num: + val = type_func(token.text) + else: + match token.text: + case "LT$": + rel_op = "<" + case "GT$": + rel_op = ">" + case "LEQ$": + rel_op = "<=" + case "GEQ$": + rel_op = ">=" + + # type (< | <=) val + if rel_op in ["<", "<="]: + min_ = BoundaryType.NEGATIVE_INFINITY + max_ = val + if rel_op == "<=": + max_incl = True + + # type (> | >=) val + elif rel_op in [">", ">="]: + min_ = val + max_ = BoundaryType.INFINITY + if rel_op == ">=": + min_incl = True + + return BoundaryType(type_, min=min_, max=max_, min_inclusive=min_incl, max_inclusive=max_incl) + + +def _create_interval_in_brackets_boundary(match_string: Span, type_: str) -> BoundaryType | None: + span_ = match_string[2:-1] + + return _create_interval_boundary(span_, type_) + + +def _analyze_matches(matches: list[tuple[str, Span]], boundaries: BoundaryList) -> None: + """Analyze the passed match list for boundaries to be created. + + Parameters + ---------- + matches + Matches found by spaCy Matcher. + + boundaries + BoundaryList object that creates and contains the matching boundary objects. + + """ + type_id = 0 + other_id = 0 + processed_matches: list[dict[str, (int | str | Span)]] = [] + found_type = False + # Assignment of the found boundaries to the corresponding data type + for match_label, match_string in matches: + if match_label == "BOUNDARY_TYPE": + if found_type: + other_id += 1 + processed_matches.append({"id": type_id, "match_label": match_label, "match_string": match_string}) + type_id += 1 + found_type = True + + else: + processed_matches.append({"id": other_id, "match_label": match_label, "match_string": match_string}) + other_id += 1 + if found_type: + found_type = False + + # Creation of the matching BoundaryTypes + for i in range(max(type_id, other_id)): + same_id = [match for match in processed_matches if match["id"] == i] + + if len(same_id) == 2: + type_: str = "" + match_string = None + match_label = "" + + for match in same_id: + if match["match_label"] == "BOUNDARY_TYPE": + type_ = match["match_string"].text # type: ignore + else: + match_label = match["match_label"] # type: ignore + match_string = match["match_string"] # type: ignore + + boundaries.add_boundary(match_label, type_, match_string) # type: ignore + + +def _preprocess_docstring(docstring: str) -> str: + """ + Preprocess docstring to make it easier to parse. + + 1. Encode relational operators + 2. Remove back ticks + 3. Transform multiple whitespaces to one + + Parameters + ---------- + docstring + The docstring to be processed. + + Returns + ------- + str + The processed docstring. + """ + docstring = re.sub(r">=", "GEQ$", docstring) + docstring = re.sub(r"<=", "LEQ$", docstring) + docstring = re.sub(r"<", "LT$", docstring) + docstring = re.sub(r">", "GT$", docstring) + + docstring = re.sub(r"`", "", docstring) + return re.sub(r"\s+", " ", docstring) + + +def extract_boundary(description: str, type_string: str) -> set[BoundaryType]: + """Extract valid BoundaryTypes. + + Extract valid BoundaryTypes described by predefined rules. + + Parameters + ---------- + description + Description string of the parameter to be examined. + + type_string + Type string of the parameter to be examined. + + Returns + ------- + set[BoundaryType] + A set containing valid BoundaryTypes. + """ + boundaries = BoundaryList() + + type_string = _preprocess_docstring(type_string) + type_doc = _nlp(type_string) + + type_matches = _matcher(type_doc) + type_matches = [(_nlp.vocab.strings[match_id], type_doc[start:end]) for match_id, start, end in type_matches] + + description_preprocessed = _preprocess_docstring(description) + description_doc = _nlp(description_preprocessed) + + desc_matches: list[tuple[str, Span]] = [] + for sent in description_doc.sents: + d_matches = _matcher(sent) + d_matches = [(_nlp.vocab.strings[match_id], sent[start:end]) for match_id, start, end in d_matches] + desc_matches.extend(d_matches) + + if type_matches: + type_list: list[str] = [] # Possible numeric data types that may be used with the parameter to be examined. + restriction_list: list[tuple[str, Span]] = [] # Restrictions of the type such as non-negative + match_label = "" + + for match in type_matches: + if match[0] == "BOUNDARY_TYPE": + type_list.append(match[1].text) + else: + restriction_list.append(match) + + type_length = len(type_list) + + # If the length of the found types is 1, the boundary type is described only in the type string + # and the value range only in the description string. + + if type_length == 1: + type_text = type_list[0] + match_string: Span | None = None + + if len(restriction_list) == 1: + match_label = restriction_list[0][0] + match_string = restriction_list[0][1] + + # Checking the description for boundaries if no restriction was found in the type string + elif len(desc_matches) > 0: + match_label, match_string = desc_matches[0] + for m_label, m_string in desc_matches: + if m_label == "BOUNDARY_INTERVAL": + match_label = m_label + match_string = m_string + break + + if match_label == "BOUNDARY_TYPE" and len(desc_matches) > 1: + type_text = match_string.text + match_label, match_string = desc_matches[1] + + boundaries.add_boundary(match_label, type_text, match_string) + + elif type_length > 1: + found_type_rel_val = any(match[0] == "BOUNDARY_TYPE_REL_VAL" for match in type_matches) + + if found_type_rel_val: + _analyze_matches(type_matches, boundaries) + else: + _analyze_matches(desc_matches, boundaries) + + return boundaries.get_boundaries() diff --git a/src/safeds_stubgen/api_analyzer/_extract_valid_values.py b/src/safeds_stubgen/api_analyzer/_extract_valid_values.py new file mode 100644 index 00000000..55fd1812 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/_extract_valid_values.py @@ -0,0 +1,526 @@ +"""Enumeration/valid values Extractor""" + +import re +from dataclasses import dataclass +from typing import Any + +from spacy.language import Language +from spacy.matcher import Matcher +from spacy.tokens import Doc, Span +from spacy import load +from spacy.cli.download import download + + +def load_language(name: str) -> Language: + """ + Safely load a Spacy language model. + + Parameters + ---------- + name: str + The name of the language model to load. + + Returns + ------- + spacy.Language + The loaded language model. + """ + try: + return load(name) + except OSError: # pragma: no cover + download(name) + return load(name) + + +_quotes = {"ORTH": {"IN": ["'", '"', "`"]}} +_quotes_without_backticks = {"ORTH": {"IN": ["'", '"']}} +_quotes_at_least_one = {"ORTH": {"IN": ["'", '"', "`"]}, "OP": "+"} +_quotes_optional = {"ORTH": {"IN": ["'", '"', "`"]}, "OP": "?"} + +_enum_valid_values_are = [{"LOWER": {"IN": ["valid", "possible", "accepted"]}}, {"LOWER": "values"}, {"LOWER": "are"}] + +_enum_when_set_to = [{"LOWER": "when"}, {"LOWER": "set"}, {"LOWER": "to"}] + +_enum_if_listing = [ + {"ORTH": "If"}, + _quotes_at_least_one, + {"OP": "{1,1}"}, + _quotes_at_least_one, + {"ORTH": {"IN": [",", ":"]}}, +] + +_enum_if_special_vals = [ + {"ORTH": "If"}, + _quotes_optional, + {"LOWER": {"IN": ["false", "true", "none"]}}, + _quotes_optional, +] + +_enum_hyphened_single_val = [ + {"ORTH": "-"}, + _quotes_at_least_one, + {}, + _quotes_at_least_one, + {"ORTH": {"IN": [",", ":"]}, "OP": "?"}, +] + +_enum_hyphened_special_vals = [ + {"ORTH": "-"}, + _quotes_optional, + {"LOWER": {"IN": ["false", "true", "none"]}}, + _quotes_optional, +] + +_enum_type_curly = [{"ORTH": "{"}, {"OP": "+"}, {"ORTH": "}"}] + +_enum_str = [{"LOWER": "of", "OP": "?"}, {"LOWER": "str"}] +_enum_bool = [{"LOWER": {"IN": ["bool", "false", "true"]}}] + +_enum_single_val_bool_none = [{"ORTH": {"IN": ["True", "False", "None"]}}, {"ORTH": ":", "OP": "?"}] + +_enum_single_val_quoted = [ + _quotes_without_backticks, + {"ORTH": {"NOT_IN": ["'", "`", '"']}, "OP": "+"}, + _quotes_without_backticks, +] + +_enum_string_inputs_supported = [ + {"LOWER": "string"}, + {"LOWER": "inputs"}, + {"ORTH": ","}, + {"OP": "+"}, + {"LOWER": "are"}, + {"LOWER": "supported"}, +] + +_enum_single_val_respective = [ + {"LOWER": {"IN": ["resp.", "respective"]}}, +] + + +@dataclass +class MatcherConfiguration: + _nlp: Language | None = None + _descr_matcher: Matcher | None = None + _type_matcher: Matcher | None = None + + # Rules to be checked + single_val_type_string: bool = True + when_set_to: bool = True + valid_values_are: bool = True + type_curly: bool = True + if_listings: bool = True + single_vals: bool = True + string_inputs: bool = True + hyphened_single: bool = True + + def __post_init__(self) -> None: + self._nlp = load_language("en_core_web_sm") + self._descr_matcher = Matcher(self._nlp.vocab) + self._type_matcher = Matcher(self._nlp.vocab) + + self._type_matcher.add("ENUM_STR", [_enum_str], greedy="LONGEST") + self._type_matcher.add("ENUM_BOOL", [_enum_bool]) + + if self.when_set_to: + self._descr_matcher.add("ENUM_SINGLE_VAL_WHEN", [_enum_when_set_to], on_match=_extract_single_value) + if self.if_listings: + self._descr_matcher.add( + "ENUM_SINGLE_VAL_IF", + [_enum_if_listing, _enum_if_special_vals], + on_match=_extract_single_value, + greedy="FIRST", + ) + if self.valid_values_are: + self._descr_matcher.add("ENUM_VALID_VALUES_ARE", [_enum_valid_values_are], on_match=_extract_list) + if self.type_curly: + self._type_matcher.add("ENUM_TYPE_CURLY", [_enum_type_curly], on_match=_extract_list) + if self.single_vals: + self._type_matcher.add( + "ENUM_TYPE_SINGLE_VALS", + [_enum_single_val_quoted, _enum_single_val_bool_none], + on_match=_extract_indented_single_value, + greedy="FIRST", + ) + if self.string_inputs: + self._descr_matcher.add("ENUM_STRING_INPUTS", [_enum_string_inputs_supported], on_match=_extract_list) + if self.hyphened_single: + self._descr_matcher.add( + "ENUM_HYPHENED_SINGLE", + [_enum_hyphened_special_vals, _enum_hyphened_single_val], + on_match=_extract_single_value, + greedy="FIRST", + ) + + def get_descr_matcher(self) -> Matcher: + if self._descr_matcher is None: # pragma: no cover + self.__post_init__() + assert self._descr_matcher is not None + return self._descr_matcher + else: + return self._descr_matcher + + def get_type_matcher(self) -> Matcher: + if self._type_matcher is None: # pragma: no cover + self.__post_init__() + assert self._type_matcher is not None + return self._type_matcher + else: + return self._type_matcher + + def get_nlp(self) -> Language: + if self._nlp is None: # pragma: no cover + self.__post_init__() + assert self._nlp is not None + return self._nlp + else: + return self._nlp + + +_extracted: list[str] = [] + + +def _merge_with_last_value_in_list(value_list: list[str], merge_value: str) -> None: + """Merge the last value of a list with the passed merge_value. + + Parameters + ---------- + value_list + list of string values + merge_value + value to be merged + + """ + if merge_value in ["-", "_"] or value_list[-1][-1] in ["-", "_"]: + value_list[-1] += merge_value + else: + value_list[-1] += " " + merge_value + + +def _extract_list( + nlp_matcher: Matcher, # noqa: ARG001 + doc: Doc, + i: int, + nlp_matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Extract the first collection of valid string values that occurs after the matched string. + + Parameters + ---------- + nlp_matcher + Parameter is ignored. + doc + Doc object that is checked for the active rules. + i + Parameter is ignored. + nlp_matches + List of matches found by the matcher + + """ + found_list = False + quotes = ["'", "`", '"'] + seperators_opener = [",", "[", "{", "or", "and"] + ex = [] + quote_start = False + last_token = False + value = "" + + match_ = nlp_matches[i] + end = match_[2] + start = match_[1] + + label = MATCHER_CONFIG.get_nlp().vocab.strings[match_[0]] + + if label == "ENUM_STRING_INPUTS": + end = start + 2 + elif label == "ENUM_TYPE_CURLY": + end = start + + for token in doc[end:]: + if token.text in ["[", "{"]: + found_list = True + continue + elif token.text in ["]", "}"]: + break + + if found_list and token.text not in seperators_opener and token.text not in quotes: + first_left_nbor = token.nbor(-1).text + second_left_nbor = "'" + + if token.i > 1: + second_left_nbor = token.nbor(-2).text + + if token.text in ["True", "False", "bool"]: + ex.append("True") + ex.append("False") + elif token.text == "None": + ex.append("None") + + elif ( + (first_left_nbor in quotes and second_left_nbor not in seperators_opener) + or (first_left_nbor not in seperators_opener and first_left_nbor not in quotes) + and ex + ): + _merge_with_last_value_in_list(ex, token.text) + + elif token.nbor(-1).text in quotes or token.nbor(1).text in quotes: + ex.append(token.text) + + elif not found_list: + if token.text in ["or", "and"]: + last_token = True + + if token.text in quotes: + if quote_start: + ex.append(value) + value = "" + + if last_token and quote_start: + break + + quote_start = not quote_start + continue + + if quote_start: + value += token.text + + if not quote_start and token.text in ["True", "False", "None"]: + ex.append(token.text) + + if last_token: + break # pragma: no cover + + ex = ['"' + x + '"' if x not in ["True", "False", "None"] else x for x in ex] + _extracted.extend(ex) + + return None + + +def _extract_single_value( + nlp_matcher: Matcher, # noqa: ARG001 + doc: Doc, + i: int, + nlp_matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Extract the first value that occurs after the matched string. + + Parameters + ---------- + nlp_matcher + Parameter is ignored. + doc + Doc object that is checked for the active rules. + i + Index of the match that was recognized by the rule. + nlp_matches + List of matches found by the matcher. + + """ + text = "" + match_id, start, end = nlp_matches[i] + + match_label = MATCHER_CONFIG.get_nlp().vocab.strings[match_id] + if match_label in ["ENUM_SINGLE_VAL_IF", "ENUM_HYPHENED_SINGLE"]: + next_token_idx = start + 1 + next_token = doc[next_token_idx] + quotes = ["'", '"', "`"] + + if next_token.text in quotes and next_token.nbor(1).text in quotes: + end = next_token_idx + 1 + else: + end = next_token_idx + + else: + next_token = doc[end] + + if next_token.text in ["True", "False", "bool"]: + _extracted.append("True") + _extracted.append("False") + elif next_token.text == "None": + _extracted.append("None") + elif next_token.text in ["'", '"', "`"]: + for token in doc[end + 1 :]: + if token.text in ["'", '"', "`"]: + break + else: + text += token.text + + if text == "None": + _extracted.append("None") + elif text in ["False", "True"]: + _extracted.append("False") + _extracted.append("True") + elif text in ["int", "float"]: + return None + elif text != "": + _extracted.append('"' + text + '"') + + return None + + +def _extract_indented_single_value( + nlp_matcher: Matcher, # noqa: ARG001 + doc: Doc, + i: int, + nlp_matches: list[tuple[Any, ...]], +) -> Any | None: + """on-match function for the spaCy Matcher. + + Extract the standalone indented values. + + Parameters + ---------- + nlp_matcher + Parameter is ignored. + doc + Doc object that is checked for the active rules. + i + Index of the match that was recognized by the rule. + nlp_matches + List of matches found by the matcher. + + """ + _, start, end = nlp_matches[i] + if end - start == 1: + value = doc[start:end] + else: + value = doc[start : end - 1] + + value = value.text + + if value[0] in ["'", "`", '"']: + value = re.sub(r"['`]", '"', value) + if value[-1] != '"': + value = value + '"' + + _extracted.append(value) + + return None + + +def _nlp_matches_to_readable_matches( + nlp_matches: list[tuple[int, int, int]], + nlp_: Language, + doc_: Doc, +) -> list[tuple[str, Span]]: + """Transform the matches list into a readable list. + + Parameters + ---------- + nlp_matches + list of spaCy matches + nlp_ + spaCy natural language pipeline + doc_ + Doc object that is checked for the active rules. + + """ + return [(nlp_.vocab.strings[match_id], doc_[start:end]) for match_id, start, end in nlp_matches] + + +def _preprocess_docstring(docstring: str, is_type_string: bool = False) -> str: + """ + Preprocess docstring to make it easier to parse. + + Transform multiple back ticks to one back tick and replace multiple whitespaces with one whitespace if the + docstrng to be processed is a type string. + + Parameters + ---------- + docstring + The docstring to be processed. + + Returns + ------- + str + The processed docstring. + """ + docstring = re.sub(r"`+", "`", docstring) + + if is_type_string: + docstring = re.sub(r"\s+", " ", docstring) + + return docstring + + +def extract_valid_literals(description: str, type_string: str) -> set[str]: + """Extract all valid literals. + + Parameters + ---------- + description + Description string of the parameter to be examined. + type_string + Type string of the prameter to be examined. + + + Returns + ------- + set[str] + Set of extracted literals. + + """ + _extracted.clear() + + nlp = MATCHER_CONFIG.get_nlp() + descr_matcher = MATCHER_CONFIG.get_descr_matcher() + type_matcher = MATCHER_CONFIG.get_type_matcher() + + type_match_labels = [] + + none_and_bool = {"False", "None", "True"} + + description = _preprocess_docstring(description) + desc_doc = nlp.make_doc(" ".join(description.split())) + + type_string = _preprocess_docstring(type_string, is_type_string=True) + type_doc = nlp.make_doc(type_string) + + descr_matcher(desc_doc) + + type_matches = type_matcher(type_doc) + type_matches = _nlp_matches_to_readable_matches(type_matches, nlp, type_doc) + + if type_matches: + type_match_labels = [match_label for match_label, _ in type_matches] + + if "ENUM_BOOL" in type_match_labels: + _extracted.append("True") + _extracted.append("False") + + for match_label, match_span in type_matches: + if match_label == "ENUM_TYPE_SINGLE_VALS" and "ENUM_TYPE_CURLY" not in type_match_labels: + substituted_string = re.sub(r"['`]+", '"', match_span.text) + _extracted.append(substituted_string) + values_to_be_removed = [] + for val in _extracted: + if val in ["True", "False"] and "ENUM_BOOL" not in type_match_labels: + values_to_be_removed.append(val) + if val[0] == '"' and not val[1:-1].isalpha(): + for c in val[1:-1]: + if c in ["!", "§", "$", "%", "&", "/", "=", "?", "*", "~"]: + _extracted.remove(val) + break + + for val in values_to_be_removed: + _extracted.remove(val) + + extracted_set = set(_extracted) + + is_enum_str = False + for label, match_span in type_matches: + if label == "ENUM_STR" and match_span.text != "of str": + is_enum_str = True + break + + if is_enum_str and not extracted_set.difference(none_and_bool): + extracted_set.add("unlistable_str") + + return extracted_set + + +MATCHER_CONFIG = MatcherConfiguration() diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index 1930fb92..19cb92af 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -2,7 +2,7 @@ import logging from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, get_args, Union import mypy.build as mypy_build import mypy.main as mypy_main @@ -10,9 +10,10 @@ from mypy import types as mypy_types from safeds_stubgen.api_analyzer._type_source_enums import TypeSourcePreference, TypeSourceWarning +from safeds_stubgen.api_analyzer._types import AbstractType, CallableType, DictType, FinalType, ListType, NamedSequenceType, NamedType, SetType, TupleType, UnionType from safeds_stubgen.docstring_parsing import DocstringStyle, create_docstring_parser -from ._api import API +from ._api import API, Attribute, CallReference, Function, Class, Module, QualifiedImport from ._ast_visitor import MyPyAstVisitor from ._ast_walker import ASTWalker from ._package_metadata import distribution, distribution_version @@ -27,6 +28,7 @@ def get_api( is_test_run: bool = False, type_source_preference: TypeSourcePreference = TypeSourcePreference.CODE, type_source_warning: TypeSourceWarning = TypeSourceWarning.WARN, + old_purity_analysis: bool = False ) -> API: """Parse a given code package with Mypy, walk the Mypy AST and create an API object.""" init_roots = _get_nearest_init_dirs(root) @@ -70,7 +72,10 @@ def get_api( aliases = _get_aliases(result_types=build_result.types, package_name=package_name) # Setup api walker - api = API(distribution=dist, package=package_name, version=dist_version) + path_to_package = "" + if is_test_run and package_name not in ["safeds", "matplotlib", "pandas", "sklearn", "seaborn"]: + path_to_package = f"tests/data/{dist}" + api = API(distribution=dist, package=package_name, version=dist_version, path_to_package=path_to_package) docstring_parser = create_docstring_parser(style=docstring_style, package_path=root) callable_visitor = MyPyAstVisitor( docstring_parser=docstring_parser, @@ -84,8 +89,13 @@ def get_api( for tree in mypy_asts: walker.walk(tree=tree) - return callable_visitor.api + api = callable_visitor.api + if not old_purity_analysis: + _update_class_subclass_relation(api) + _find_correct_type_by_path_to_call_reference(api) + _find_all_referenced_functions_for_all_call_references(api) + return api def _get_nearest_init_dirs(root: Path) -> list[Path]: """Check for the nearest directory with an __init__.py file. @@ -219,3 +229,890 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: aliases[name].add(fullname) return aliases + +# ############### Functions for finding referenced functions #################### + +def _update_class_subclass_relation(api: API) -> None: + """ + For each class, updates each superclass by appending the id of the class to subclasses list of the superclass + + Parameters + ---------- + api : API + Stores api data of analyzed package + """ + for class_def in api.classes.values(): + super_classes: list[Class] = [] + for super_class_id in class_def.superclasses: + class_to_append = _get_class_by_id(api, super_class_id) + if class_to_append is None: # super class imported from outside of the analyzed package + continue + super_classes.append(class_to_append) + for super_class in super_classes: + super_class.subclasses.append(class_def.id) + +def _find_attribute_in_class_and_super_classes(api: API, attribute_name: str, current_class: Class, visited_classes: list[str]) -> Attribute | None: + attributes = list(filter(lambda attribute: attribute.name == attribute_name, current_class.attributes)) + visited_classes.append(current_class.id) + if len(attributes) != 1: + # look in superclass + for super_class_name in current_class.superclasses: + super_class = _get_class_by_id(api, super_class_name) + if super_class is None: + continue # pragma: no cover + if super_class.id in visited_classes: + continue # pragma: no cover + attribute = _find_attribute_in_class_and_super_classes(api, attribute_name, super_class, visited_classes) + if attribute is not None: + return attribute + return None + attribute = attributes[0] + return attribute + +def _find_method_in_class_and_super_classes(api: API, method_name: str, current_class: Class, visited_classes: list[str]) -> Function | None: + methods = list(filter(lambda method: method.name == method_name, current_class.methods)) + visited_classes.append(current_class.id) + if method_name == "__init__" and current_class.constructor is not None: + methods = [current_class.constructor] # pragma: no cover + if len(methods) != 1: + # look in superclass + for super_class_name in current_class.superclasses: + super_class = _get_class_by_id(api, super_class_name) + if super_class is None: + continue # pragma: no cover + if super_class.id in visited_classes: + continue # pragma: no cover + method = _find_method_in_class_and_super_classes(api, method_name, super_class, visited_classes) + if method is not None: + return method + return None # pragma: no cover + method = methods[0] + return method + +def _find_correct_type_by_path_to_call_reference(api: API) -> None: + """ + Call references can be nested, for example: "instance.attribute.method()" or "instance.method().method2()" etc. + Therefore the call_reference type stores a path of the names to the call. The path is of type: list[str] + + For each call reference of each function, the correct class is attempted to be found, + by iterating through the path which is stored in the call_reference class. + For each string of the path, it is first assumed, that the string is the name of an attribute of the + current class. If there is no attribute with the string of the path as name, then it is assumed + that the string is the name of a method. For either attribute or method, we try to find the next + class along the path, until the the end of the path to the call reference is reached. + + Parameters: + ---------- + api : API + Stores api data of analyzed package + """ + for function in api.functions.values(): + for call_reference in function.body.call_references.values(): + type = call_reference.receiver.type + if isinstance(type, str): # closures, global func or imported func which is called like this: func() + global_function_directly_imported = _get_function(api, type.split(".")[-1], function) + if global_function_directly_imported is not None and len(call_reference.receiver.path_to_call_reference) == 2: + call_reference.possibly_referenced_functions.append(global_function_directly_imported) + + if not call_reference.isSuperCallRef: + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, type, call_reference.receiver.path_to_call_reference, 0) + else: + # here we have a super() call so we need to get the super classes + classes, _, _ = _get_classes_and_modules_of_type(api, type, call_reference, function) + for found_class in classes: + super_classes: list[Class] = [] + if not isinstance(found_class, Class): # pragma: no cover + found_class = _get_class_by_id(api, found_class) + if found_class is None: + call_reference.reason_for_no_found_functions += f"the found class of a super call was not a class: {str(found_class)} | " + continue + for super_class_id in found_class.superclasses: + found_super_class = _get_class_by_id(api, super_class_id) + if found_super_class is not None: + super_classes.append(found_super_class) + elif found_super_class is None and super_class_id.startswith("builtins."): + # builtin superclasses are handled in resolve_references + call_reference.receiver.type = super_class_id + call_reference.receiver.full_name = super_class_id + for super_class in super_classes: + prev_found_classes_length = len(call_reference.receiver.found_classes) + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, super_class, call_reference.receiver.path_to_call_reference, 0) + if prev_found_classes_length < len(call_reference.receiver.found_classes): + break # for super types we can only take the first class where we found correct types + +def _find_correct_types_by_path_to_call_reference_recursively(api: API, call_reference: CallReference, type_of_receiver: Class | Any, path: list[str], depth: int) -> None: + """ + Recursive helper function of _find_correct_type_by_path_to_call_reference + + Parameters: + ---------- + api : API + Stores api data of analyzed package + call_reference : CallReference + The current call reference to find the correct type for + type_of_receiver : Class | Any, + Current type of receiver, used for recursion + path : list[str] + The path is traversed during recursion and gets smaller each time until its length is 0 + depth : int + stores the depth of the recursion + """ + if len(path) == 0: + return + path_copy = path.copy() + part = path_copy.pop() + + if type_of_receiver is None: + return # pragma: no cover + if depth == 0: + # first part of path is a variable name etc so we can skip + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, type_of_receiver, path_copy, depth + 1) + return + if part == "()": + # return type is found below in except KeyError block or module part + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, type_of_receiver, path_copy, depth + 1) + return + if part.startswith("[") and part.endswith("]"): + str_key = part.removeprefix("[").removesuffix("]") + key = int(str_key) if str_key != "" else None + _, _, next_types = _get_classes_and_modules_of_type(api, type_of_receiver, call_reference, None, key, True) + for next_type in next_types: + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, next_type, path_copy, depth + 1) + return + + if isinstance(type_of_receiver, Module): + # look for part in global functions and classes + global_function_name = part + class_name = part + + global_function = None + for global_func in type_of_receiver.global_functions: + if global_func.id == f"{type_of_receiver.id}/{global_function_name}": + global_function = global_func + break + if global_function is not None: + is_last_method = all(item == "()" for item in path_copy) # maybe we need to check for [] as well + if is_last_method: # here we have "method()" or "method()()" + call_reference.possibly_referenced_functions.append(global_function) + return + + if len(global_function.results) != 0: # pragma: no cover + for result in global_function.results: + if result.type is None: + print(f"Result {result.name} has type None") + continue + + # get NamedType from result + types = _get_named_types_from_nested_type(result.type) # will find the type of expressions like "method()[0]" + if types is None: + print("NamedType not found") + continue + + for type in types: + if type.qname == "builtins.None": + continue + found_class = _get_class_by_id(api, type.qname) + if found_class is None: + # if one type cannot be found then this call ref should be impure as the function which could not be found could be impure + call_reference.reason_for_no_found_functions += f"Class with name {type.qname} not found at module path part {part} no fallback | " + call_reference.fallbackToSignatureCheck = False + break + type_of_receiver = found_class + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, type, path_copy, depth + 1) + + class_from_module = None + for class_from_mod in type_of_receiver.classes: + if class_from_mod.id == f"{type_of_receiver.id}/{class_name}": + class_from_module = class_from_mod + break + if class_from_module is not None: + is_last_method = all(item == "()" for item in path_copy) + if is_last_method: # here we have "method()" or "method()()" # pragma: no cover + if class_from_module.constructor is not None: + call_reference.possibly_referenced_functions.append(class_from_module.constructor) + return + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, class_from_module, path_copy, depth + 1) + + if class_from_module is None and global_function is None: # pragma: no cover + call_reference.reason_for_no_found_functions += f"Module {type_of_receiver.name} has no class nor global function of name {part} no fallback | " + call_reference.fallbackToSignatureCheck = False + + return + + if not isinstance(type_of_receiver, Class): # as from here on, type_of_receiver needs to be of type Class + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_of_receiver, call_reference) + for found_class in found_classes: + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, found_class, path, depth) + for found_module in found_modules: + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, found_module, path, depth) + return + + try: # assume the part of the path is a name of a member + attribute_name = part + attribute = _find_attribute_in_class_and_super_classes(api, attribute_name, type_of_receiver, []) + if attribute is None: + raise KeyError() + type_of_attribute = attribute.type + if type_of_attribute is None: # pragma: no cover + print("missing type info!") + call_reference.reason_for_no_found_functions += f"Missing type info of: {str(attribute_name)}, at path part {part} | " + return + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, type_of_attribute, path_copy, depth + 1) + return + + except KeyError: # current part of path was not a member so we assume its a method + is_last_method = all(item == "()" for item in path_copy) # maybe we need to check for [] as well + if is_last_method: # here we have "method()" or "method()()" + if type_of_receiver is not None and isinstance(type_of_receiver, Class): + call_reference.receiver.found_classes.append(type_of_receiver) + return + else: # here we have something like this "method1().method2()" or "method1().member.method2()" or "method()[0].member.method2()" or "method()()" + method_name = part + method = _find_method_in_class_and_super_classes(api, method_name, type_of_receiver, []) + if method is None: # pragma: no cover + call_reference.reason_for_no_found_functions += f"Method {method_name} and Attribute {attribute_name} not found in class {type_of_receiver.name} and superclasses, at path part {part} | " + return + if len(method.results) == 0: # pragma: no cover + call_reference.reason_for_no_found_functions += f"The found method has no result {str(method.name)} and is not the last in path, at path part {part} | " + return + result = method.results[0] # in this case there can only be one result + if result.type is None: # pragma: no cover + call_reference.reason_for_no_found_functions += f"Result: {result.name} has type None, at path part {part} | " + return + _find_correct_types_by_path_to_call_reference_recursively(api, call_reference, result.type, path_copy, depth + 1) + return + +def _find_all_referenced_functions_for_all_call_references(api: API) -> None: + """ + Once the correct receiver type was found in _find_correct_type_by_path_to_call_reference, + all possibly referenced functions for each call references need to be found by this function. + + For that, found class of each call reference is accessed, if its None, the possibly referenced + functions for this call reference will be empty. If they are empty, the purity analysis will + fall back to using a list of all functions with the same name as possibly referenced functions. + + The possibly referenced functions can also be functions of the subclasses or superclasses. + Therefore we also need to check the superclasses, if the type of the receiver doesnt have the + function. If the type has subtypes, we need to search for the function in each subtype. + + Parameters: + ---------- + api : API + Stores api data of analyzed package + """ + for function in api.functions.values(): + for call_reference in function.body.call_references.values(): + if call_reference.receiver.found_classes is None or len(call_reference.receiver.found_classes) == 0: + continue + else: + current_classes = call_reference.receiver.found_classes + + for current_class in current_classes: + # check if specified class has method with name of callreference + specified_class_has_method = False + referenced_functions: list[Function] = [] + methods = current_class.methods + if call_reference.function_name == "__init__" and current_class.constructor is not None: + specified_class_has_method = True + if not specified_class_has_method: + for method in methods: + if method.name == call_reference.function_name: + specified_class_has_method = True + break # as there can only be one method of that name in a python class + + _get_referenced_functions_from_class_and_subclasses( + api, + call_reference, + current_class.id, + [], + referenced_functions + ) + + if not specified_class_has_method: # then python will look for method in super but so we have to do that as well + # find function in superclasses but only first appearance as python will also only call the first appearance + super_classes: list[Class] = [] + for super_class_id in current_class.superclasses: + super_class = _get_class_by_id(api, super_class_id) + if super_class is not None: + super_classes.append(super_class) + _get_referenced_function_from_super_classes( + api, + call_reference, + super_classes, + referenced_functions, + [] + ) + + call_reference.possibly_referenced_functions.extend(referenced_functions) + +def _get_referenced_function_from_super_classes( + api: API, + call_reference: CallReference, + super_classes: list[Class], + referenced_functions: list[Function], + visited_classes: list[str], +) -> None: + """ + finds the first referenced function in super classes recursively + + Parameters + ---------- + api : API + Stores api data of analyzed package + call_reference : CallReference + The call reference + super_classes : list[Class] + The super classes of the type of the receiver of the super class or of those super classes + referenced_functions : list[Function] + Passed along recursion to store all possibly referenced functions + visited_classes : list[str] + stores visited classes to prevent loops + """ + for super_class in super_classes: + if super_class.id in visited_classes: + continue # pragma: no cover + visited_classes.append(super_class.id) + found_method = False + if call_reference.function_name == "__init__" and super_class.constructor is not None: # pragma: no cover + referenced_functions.append(super_class.constructor) + found_method = True + break + for method in super_class.methods: + if method.name == call_reference.function_name: + referenced_functions.append(method) + found_method = True + break + if found_method: + break + else: + try: + next_super_classes: list[Class] = [] + for next_super_class_id in super_class.superclasses: + next_super_class = _get_class_by_id(api, next_super_class_id) + if next_super_class is not None: + next_super_classes.append(next_super_class) + except KeyError as error: # pragma: no cover + print(error) + _get_referenced_function_from_super_classes( + api, + call_reference, + next_super_classes, + referenced_functions, + visited_classes + ) + +def _get_referenced_functions_from_class_and_subclasses( + api: API, + call_reference: CallReference, + current_class_id: str, + visited_classes: list[str], + referenced_functions: list[Function] +) -> None: + """ + Finds all additional function defs with same name in sub classes recursively, as they could also be called + + Parameters + ---------- + api : API + Stores api data of analyzed package + call_reference : CallReference + The call reference + current_class_id : str + The id of the current class, which has the sub classes + visited_classes : list[str] + Stores visited class ids, so that we dont visit classes twice, during recursion + referenced_functions : list[Function] + Passed along recursion to store all possibly referenced functions + """ + # find all additional function defs with same name in sub classes as they could also be called + if current_class_id in visited_classes: + return # pragma: no cover + current_class = api.classes[current_class_id] + visited_classes.append(current_class_id) + methods = current_class.methods + if call_reference.function_name == "__init__" and current_class.constructor is not None: + referenced_functions.append(current_class.constructor) + else: + for method in methods: + if method.name == call_reference.function_name: + referenced_functions.append(method) + break # as there can only be one method of that name in a python class + + if len(current_class.subclasses) != 0: + for subclass in current_class.subclasses: + _get_referenced_functions_from_class_and_subclasses( + api, + call_reference, + subclass, + visited_classes, + referenced_functions + ) + + +# ################ Utilities for finding referenced functions ####################### + +def _get_named_types_from_nested_type(nested_type: AbstractType) -> list[NamedType | NamedSequenceType] | None: # pragma: no cover + """ + Iterates through a nested type recursively, to find a NamedType + + Parameters + ---------- + nested_type : AbstractType + Abstract class for types + + Returns + ---------- + type : list[NamedType] | None + """ + if isinstance(nested_type, NamedType): + return [nested_type] + elif isinstance(nested_type, ListType): + if len(nested_type.types) == 0: + return None + return _get_named_types_from_nested_type(nested_type.types[0]) # a list can only have one type + elif isinstance(nested_type, NamedSequenceType): + if len(nested_type.types) == 0: + return None + return [nested_type] + elif isinstance(nested_type, DictType): + return _get_named_types_from_nested_type(nested_type.value_type) + elif isinstance(nested_type, SetType): + if len(nested_type.types) == 0: + return None + return _get_named_types_from_nested_type(nested_type.types[0]) # a set can only have one type + elif isinstance(nested_type, FinalType): + return _get_named_types_from_nested_type(nested_type.type_) + elif isinstance(nested_type, CallableType): + return _get_named_types_from_nested_type(nested_type.return_type) + elif isinstance(nested_type, UnionType): + result = [] + for type in nested_type.types: + extracted_types = _get_named_types_from_nested_type(type) + if extracted_types is None: + continue + result.extend(extracted_types) + return result + elif isinstance(nested_type, TupleType): + result = [] + for type in nested_type.types: + extracted_types = _get_named_types_from_nested_type(type) + if extracted_types is None: + continue + result.extend(extracted_types) + return result + + for member_name in dir(nested_type): + if not member_name.startswith("__"): + member = getattr(nested_type, member_name) + if isinstance(member, AbstractType): + return _get_named_types_from_nested_type(member) + elif isinstance(member, list) and len(member) > 0 and isinstance(member[0], AbstractType): + types: list[NamedType | NamedSequenceType] = [] + for type in member: + named_type = _get_named_types_from_nested_type(type) + if named_type is not None: + types.extend(named_type) + return list(filter(lambda type: not type.qname.startswith("builtins"), list(set(types)))) + return None + +def _get_class_by_id(api: API, class_id: str) -> Class | None: + """ + Helper function, to retrieve the class from API by id + + Parameters + ---------- + api : API + Abstract class for types + class_id : str + The class id to search for + + Returns + ---------- + class : Class | None + returns None if no class is found + """ + class_name = class_id.split(".")[-1] + correct_id = "/".join(class_id.split(".")) + result_class: Class | None = api.classes.get(correct_id, None) + if result_class is not None: + return result_class + if correct_id.startswith(api.path_to_package): + result_class = api.classes.get(correct_id, None) + else: + correct_id = api.path_to_package + correct_id + result_class = api.classes.get(correct_id, None) + if result_class is not None: + return result_class # pragma: no cover + + correct_id = "/".join(class_id.split(".")) + if not correct_id.startswith(api.path_to_package): + correct_id = api.path_to_package + correct_id + found_id = False + for key in api.reexport_map.keys(): + if key.endswith(f".{class_name}"): + for module in api.reexport_map[key]: + module_id = module.id + if module_id in correct_id: + correct_id = "/".join(f"{module_id}/{key}".split(".")) + found_id = True + break + if len(api.reexport_map[key]) == 1: # pragma: no cover + correct_id = "/".join(f"{module_id}/{key}".split(".")) + found_id = True + break + if found_id: + break + if correct_id.startswith(api.path_to_package): + result_class = api.classes.get(correct_id, None) + else: # pragma: no cover + correct_id = api.path_to_package + correct_id + result_class = api.classes.get(correct_id, None) + return result_class + +def _get_module_by_id(api: API, module_id: str) -> Module | None: + """ + Helper function, to retrieve the module from API by id + + Parameters + ---------- + api : API + Abstract class for types + module_id : str + The class id to search for + + Returns + ---------- + class : Module | None + returns None if no module is found + """ + module_name = module_id.split(".")[-1] + correct_id = "/".join(module_id.split(".")) + result_module: Module | None = api.modules.get(correct_id, None) + if result_module is not None: + return result_module + if correct_id.startswith(api.path_to_package): + result_module = api.modules.get(correct_id, None) + else: + correct_id = api.path_to_package + correct_id + result_module = api.modules.get(correct_id, None) + if result_module is not None: + return result_module + + correct_id = "/".join(module_id.split(".")) + if not correct_id.startswith(api.path_to_package): + correct_id = api.path_to_package + correct_id + found_id = False + for key in api.reexport_map.keys(): # pragma: no cover + if key.endswith(f".{module_name}"): + for module in api.reexport_map[key]: + module_id = module.id + if module_id in correct_id: + correct_id = "/".join(f"{module_id}/{key}".split(".")) + found_id = True + break + if len(api.reexport_map[key]) == 1: + correct_id = "/".join(f"{module_id}/{key}".split(".")) + found_id = True + break + if found_id: + break + if correct_id.startswith(api.path_to_package): + result_module = api.modules.get(correct_id, None) + else: # pragma: no cover + correct_id = api.path_to_package + correct_id + result_module = api.modules.get(correct_id, None) + return result_module + +def _get_function(api: API, function_name: str, function: Function, imported_module_name: str | None = None) -> Function | None: + """ + Helper function, to check if the function references a closure + + Parameters + ---------- + api : API + Abstract class for types + function_name : str + The name of the call reference to search for + function : Function + The function which contains the call ref + imported_module_name : str | None + Imported Module name from mypy ast if available + + Returns + ---------- + function: Function | None + returns None if no function is found + """ + closure = function.closures.get(function_name, None) + if closure is not None: + return closure + global_function = _get_global_function(api, function_name, function, imported_module_name) + return global_function + +def _get_global_function(api: API, function_name: str, function: Function, imported_module_name: str | None = None) -> Function | None: + """ + Helper function, to check if the function references a global function of the current module + + Parameters + ---------- + api : API + Abstract class for types + function_name : str + The name of the call reference to search for + function : Function + The function which contains the call ref + imported_module_name : str | None + Imported Module name from mypy ast if available + + Returns + ---------- + function: Function | None + returns None if no function is found + """ + # get module + module = _get_module_by_id(api, function.module_id_which_contains_def) + if module is None: + return None # pragma: no cover + + global_functions = module.global_functions + found_global_function: Function | None = None + for global_function in global_functions: + if function_name == global_function.name: + found_global_function = global_function + break + if found_global_function is None: + # there was no global function with given name in current module, so we look in imported modules + found_global_function = _get_imported_global_function(api, function_name, function, imported_module_name) + if found_global_function is None: + return None + + return found_global_function + +def _get_imported_global_function(api: API, imported_function_name: str, function: Function, imported_module_name: str | None = None) -> Function | None: + """ + Helper function, to check if the function references an imported global function + + Parameters + ---------- + api : API + Abstract class for types + function_name : str + The name of the call reference to search for + function : Function + The function which contains the call ref + imported_module_name : str | None + Imported Module name from mypy ast if available + + Returns + ---------- + function: Function | None + returns None if no function is found + """ + # get module + module = _get_module_by_id(api, function.module_id_which_contains_def) + if module is None: + return None # pragma: no cover + + imports = module.qualified_imports + found_import: QualifiedImport | None = None + for qualified_import in imports: + alias = qualified_import.alias + qname = qualified_import.qualified_name + if imported_function_name == qname.split(".")[-1] or imported_function_name == alias or imported_function_name == qname: + found_import = qualified_import + break + if imported_module_name is not None: # pragma: no cover + if imported_module_name.split(".")[-1] == qname.split(".")[-1]: + found_import = qualified_import + break + if imported_module_name.split(".")[-1] == alias: + found_import = qualified_import + break + + if found_import is None: + return None + + imported_module = _get_module_by_id(api, ".".join(found_import.qualified_name.split(".")[:-1])) + if imported_module is None: + if imported_module_name is not None: + imported_module = _get_module_by_id(api, imported_module_name) # pragma: no cover + if imported_module is None and "." not in found_import.qualified_name: # then function is imported like this from . import global_function + # then __init__.py is the module that contains the imported function + imported_module = _get_module_by_id(api, ".".join(function.module_id_which_contains_def.split(".")[:-1])) + if imported_module is None: + return None + + + global_functions = imported_module.global_functions + found_global_function: Function | None = None + for global_function in global_functions: + if imported_function_name == global_function.name: + found_global_function = global_function + break + if found_global_function is None: + return None + + return found_global_function + +def _get_classes_and_modules_of_type( + api: API, + type_to_analyze: Any, + call_reference: CallReference, + function: Function | None = None, + key: int | None = None, + return_next_type_only: bool = False +) -> tuple[list[Class], list[Module], list[Any]]: # pragma: no cover + """ + Helper function, to get the modules and classes of a given type + + Parameters + ---------- + api : API + Abstract class for types + type_to_analyze : Any + The type of the receiver to find the classes and modules for + function : Function | None + The function which contains the call ref + key : int | None + Key of index expression like this array[index], this helps to get the correct type for tuple types + return_next_type_only : bool + Set to true, if no recursion, so this function will only return the next type of a nested type + + Returns + ---------- + tuple[list[Class], list[Module], list[Any]] + """ + modules: list[Module] = [] + classes: list[Class] = [] + if isinstance(type_to_analyze, list): + if return_next_type_only: + return ([], [], [t for t in type_to_analyze]) + for t in type_to_analyze: + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, t, call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + + elif isinstance(type_to_analyze, mypy_types.TupleType): + if key is None: + if return_next_type_only: + return ([], [], [t for t in type_to_analyze.items]) + for type in type_to_analyze.items: + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type, call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + else: + if return_next_type_only: + return ([], [], [type_to_analyze.items[int(key)]]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze.items[int(key)], call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif isinstance(type_to_analyze, TupleType): + if key is None: + if return_next_type_only: + return ([], [], [t for t in type_to_analyze.types]) + for abst_type in type_to_analyze.types: + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, abst_type, call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + else: + if return_next_type_only: + return ([], [], [type_to_analyze.types[int(key)]]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze.types[int(key)], call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif isinstance(type_to_analyze, ListType): + if return_next_type_only: + return ([], [], [t for t in type_to_analyze.types]) + for abst_type in type_to_analyze.types: + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, abst_type, call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif isinstance(type_to_analyze, DictType): + if return_next_type_only: + return ([], [], [type_to_analyze.value_type]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze.value_type, call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif hasattr(type_to_analyze, "args") and len(type_to_analyze.args) == 1: # type: ignore + if return_next_type_only: + return ([], [], [type_to_analyze.args[0]]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze.args[0], call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif hasattr(type_to_analyze, "args") and len(type_to_analyze.args) == 2: # type: ignore + if return_next_type_only: + return ([], [], [type_to_analyze.args[1]]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze.args[1], call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + elif isinstance(type_to_analyze, dict): + if return_next_type_only: + return ([], [], [type_to_analyze[1]]) + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, type_to_analyze[1], call_reference, function) + classes.extend(found_classes) + modules.extend(found_modules) + + + elif isinstance(type_to_analyze, NamedType) or isinstance(type_to_analyze, NamedSequenceType): + class_of_receiver = _get_class_by_id(api, type_to_analyze.qname) + if class_of_receiver is None: + call_reference.reason_for_no_found_functions += f"no class with qname: {type_to_analyze.qname} no fallback | " + call_reference.fallbackToSignatureCheck = False + else: + classes.append(class_of_receiver) + elif isinstance(type_to_analyze, AbstractType): + types = _get_named_types_from_nested_type(type_to_analyze) + if types is not None: + for t in types: + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, t, call_reference, function, None, return_next_type_only) + classes.extend(found_classes) + modules.extend(found_modules) + elif hasattr(type_to_analyze, "__origin__") and type_to_analyze.__origin__ is Union: + for t in get_args(type_to_analyze): + found_classes, found_modules, _ = _get_classes_and_modules_of_type(api, t, call_reference, function, None, return_next_type_only) + classes.extend(found_classes) + modules.extend(found_modules) + elif hasattr(type_to_analyze, "type") and api.classes.get("/".join(type_to_analyze.type.fullname.split(".")), None) is not None: + class_of_receiver = _get_class_by_id(api, type_to_analyze.type.fullname) + if class_of_receiver is None: + call_reference.reason_for_no_found_functions += f"no class with type.fullname: {type_to_analyze.type.fullname} no fallback | " + call_reference.fallbackToSignatureCheck = False + else: + classes.append(class_of_receiver) + elif hasattr(type_to_analyze, "fullname") and api.classes.get("/".join(type_to_analyze.fullname.split(".")), None) is not None: + class_of_receiver = _get_class_by_id(api, type_to_analyze.fullname) + if class_of_receiver is None: + call_reference.reason_for_no_found_functions += f"no class with fullname: {type_to_analyze.fullname} no fallback | " + call_reference.fallbackToSignatureCheck = False + else: + classes.append(class_of_receiver) + elif isinstance(type_to_analyze, str): # super() or static method or imported global function + class_of_receiver = _get_class_by_id(api, type_to_analyze) + imported_module = _get_module_by_id(api, type_to_analyze) + + if class_of_receiver is not None: + classes.append(class_of_receiver) + + if imported_module is not None: + modules.append(imported_module) + + if function is not None: + global_function_directly_imported = _get_function(api, type_to_analyze.split(".")[-1], function) + if global_function_directly_imported is not None and len(call_reference.receiver.path_to_call_reference) == 2: + call_reference.possibly_referenced_functions.append(global_function_directly_imported) + elif isinstance(type_to_analyze, mypy_types.AnyType): + missing_import_name = type_to_analyze.missing_import_name + if missing_import_name is not None: + class_of_receiver = _get_class_by_id(api, missing_import_name) + if class_of_receiver is None: + call_reference.reason_for_no_found_functions += "anytype and missing_import_name class not found no fallback | " + call_reference.fallbackToSignatureCheck = False + else: + classes.append(class_of_receiver) + else: + call_reference.reason_for_no_found_functions += "anytype | " + else: # type is tuple or dict + call_reference.reason_for_no_found_functions += f"no case for this: {str(type_to_analyze)} | " + + return classes, modules, [] + \ No newline at end of file diff --git a/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index 655e6530..d6728de9 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -2,8 +2,9 @@ from abc import ABCMeta, abstractmethod from collections import Counter -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from dataclasses import dataclass, field +import re +from typing import TYPE_CHECKING, Any, ClassVar if TYPE_CHECKING: from collections.abc import Sequence @@ -112,6 +113,162 @@ def __hash__(self) -> int: return hash(frozenset([self.name, self.qname, *self.types])) +@dataclass(frozen=True) +class EnumType(AbstractType): + values: frozenset[str] = field(default_factory=frozenset) + full_match: str = field(default="", compare=False) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> EnumType: + return EnumType(d["values"]) # pragma: no cover + + @classmethod + def from_string(cls, string: str) -> EnumType | None: # pragma: no cover + def remove_backslash(e: str) -> str: + e = e.replace(r"\"", '"') + return e.replace(r"\'", "'") + + enum_match = re.search(r"{(.*?)}", string) + if enum_match: + quotes = "'\"" + values = set() + enum_str = enum_match.group(1) + value = "" + inside_value = False + curr_quote = None + for i, char in enumerate(enum_str): + if char in quotes and (i == 0 or (i > 0 and enum_str[i - 1] != "\\")): + if not inside_value: + inside_value = True + curr_quote = char + elif inside_value: + if curr_quote == char: + inside_value = False + curr_quote = None + values.add(remove_backslash(value)) + value = "" + else: + value += char + elif inside_value: + value += char + + return EnumType(frozenset(values), enum_match.group(0)) + + return None + + def update(self, enum: EnumType) -> EnumType: # pragma: no cover + values = set(self.values) + values.update(enum.values) + return EnumType(frozenset(values)) + + def to_dict(self) -> dict[str, Any]: + return {"kind": self.__class__.__name__, "values": set(self.values)} # pragma: no cover + + +@dataclass(frozen=True) +class BoundaryType(AbstractType): + NEGATIVE_INFINITY: ClassVar = "NegativeInfinity" + INFINITY: ClassVar = "Infinity" + + base_type: str + min: float | int | str + max: float | int | str + min_inclusive: bool + max_inclusive: bool + + full_match: str = field(default="", compare=False) + + @classmethod + def _is_inclusive(cls, bracket: str) -> bool: # pragma: no cover + if bracket in ("(", ")"): + return False + if bracket in ("[", "]"): + return True + raise ValueError(f"{bracket} is not one of []()") + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> BoundaryType: # pragma: no cover + return BoundaryType( + d["base_type"], + d["min"], + d["max"], + d["min_inclusive"], + d["max_inclusive"], + ) + + @classmethod + def from_string(cls, string: str) -> BoundaryType | None: # pragma: no cover + pattern = r"""(?Pfloat|int)?[ ] # optional base type of either float or int + (in|of)[ ](the[ ])?(range|interval)[ ](of[ ])? + # 'in' or 'of', optional 'the', 'range' or 'interval', optional 'of' + `?(?P[\[(])(?P[-+]?\d+(.\d*)?|negative_infinity),[ ] # left side of the range + (?P[-+]?\d+(.\d*)?|infinity)(?P[\])])`?""" # right side of the range + match = re.search(pattern, string, re.VERBOSE) + + if match is not None: + base_type = match.group("base_type") + if base_type is None: + base_type = "float" + + min_value: str | int | float = match.group("min") + if min_value != "negative_infinity": + if base_type == "int": + min_value = int(min_value) + else: + min_value = float(min_value) + else: + min_value = BoundaryType.NEGATIVE_INFINITY + + max_value: str | int | float = match.group("max") + if max_value != "infinity": + if base_type == "int": + max_value = int(max_value) + else: + max_value = float(max_value) + else: + max_value = BoundaryType.INFINITY + + min_bracket = match.group("min_bracket") + max_bracket = match.group("max_bracket") + min_inclusive = BoundaryType._is_inclusive(min_bracket) + max_inclusive = BoundaryType._is_inclusive(max_bracket) + + return BoundaryType( + base_type=base_type, + min=min_value, + max=max_value, + min_inclusive=min_inclusive, + max_inclusive=max_inclusive, + full_match=match.group(0), + ) + + return None + + def __eq__(self, __o: object) -> bool: + if isinstance(__o, BoundaryType): + eq = ( + self.base_type == __o.base_type + and self.min == __o.min + and self.min_inclusive == __o.min_inclusive + and self.max == __o.max + ) + if eq: + if self.max == BoundaryType.INFINITY: + return True + return self.max_inclusive == __o.max_inclusive + return False # pragma: no cover + + def to_dict(self) -> dict[str, Any]: + return { + "kind": self.__class__.__name__, + "base_type": self.base_type, + "min": self.min, + "max": self.max, + "min_inclusive": self.min_inclusive, + "max_inclusive": self.max_inclusive, + } + + @dataclass(frozen=True) class UnionType(AbstractType): types: Sequence[AbstractType] diff --git a/src/safeds_stubgen/api_analyzer/cli/_cli.py b/src/safeds_stubgen/api_analyzer/cli/_cli.py index c30442d7..dd40c3f6 100644 --- a/src/safeds_stubgen/api_analyzer/cli/_cli.py +++ b/src/safeds_stubgen/api_analyzer/cli/_cli.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from safeds_stubgen.api_analyzer.purity_analysis._infer_purity import get_purity_results from safeds_stubgen.api_analyzer import TypeSourcePreference, TypeSourceWarning, get_api from safeds_stubgen.stubs_generator import StubsStringGenerator, create_stub_files, generate_stub_data @@ -25,6 +26,7 @@ def cli() -> None: convert_identifiers=args.naming_convert, type_source_preference=args.type_source_preference, type_source_warning=args.show_type_source_warning, + old_purity_analysis=args.old_purity_analysis, ) @@ -90,6 +92,13 @@ def _get_args() -> argparse.Namespace: required=False, default=TypeSourceWarning.WARN.name, ) + parser.add_argument( + "-old", + "--old_purity_analysis", + help="Set this flag to run the old purity analysis.", + required=False, + action="store_true", + ) return parser.parse_args() @@ -102,6 +111,7 @@ def _run_stub_generator( convert_identifiers: bool, type_source_preference: TypeSourcePreference, type_source_warning: TypeSourceWarning, + old_purity_analysis: bool = False, ) -> None: """ Create API data of a package and Safe-DS stub files. @@ -122,13 +132,27 @@ def _run_stub_generator( is_test_run=is_test_run, type_source_preference=type_source_preference, type_source_warning=type_source_warning, + old_purity_analysis=old_purity_analysis ) + # Create an API file out_file_api = out_dir_path.joinpath(f"{src_dir_path.stem}__api.json") api.to_json_file(out_file_api) + + api_purity = get_purity_results( + src_dir_path, api_data=api, + test_run=is_test_run, + old_purity_analysis=old_purity_analysis, + ) + + out_file_api_purity = out_dir_path.joinpath(f"{src_dir_path.stem}__api_purity.json") + api_purity.to_json_file( + out_file_api_purity, + shorten=True + ) # Shorten is set to True by default, therefore the results will only contain the count of each reason. # Generate the stub data - stubs_generator = StubsStringGenerator(api=api, convert_identifiers=convert_identifiers) + stubs_generator = StubsStringGenerator(api=api, purity_api=api_purity, convert_identifiers=convert_identifiers) stub_data = generate_stub_data(stubs_generator=stubs_generator, out_path=out_dir_path) # Create the stub files create_stub_files(stubs_generator=stubs_generator, stubs_data=stub_data, out_path=out_dir_path) diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/__init__.py b/src/safeds_stubgen/api_analyzer/purity_analysis/__init__.py new file mode 100644 index 00000000..a04dee43 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/__init__.py @@ -0,0 +1,17 @@ +"""Analyze the purity of a library's API.""" + +from ._build_call_graph import ( + build_call_graph, +) +from ._get_module_data import ( + get_module_data, +) +from ._infer_purity import ( + get_purity_results, + infer_purity, +) +from ._resolve_references import ( + resolve_references, +) + +__all__ = ["get_module_data", "resolve_references", "infer_purity", "build_call_graph", "get_purity_results"] diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/_astroid_ast_walker.py b/src/safeds_stubgen/api_analyzer/purity_analysis/_astroid_ast_walker.py new file mode 100644 index 00000000..2ff47276 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/_astroid_ast_walker.py @@ -0,0 +1,61 @@ +from collections.abc import Callable +from typing import Any + +import astroid + +_EnterAndLeaveFunctions = tuple[ + Callable[[astroid.NodeNG], None] | None, + Callable[[astroid.NodeNG], None] | None, +] + + +class ASTWalker: + """A walker visiting an abstract syntax tree in preorder. + + The following methods get called: + + * enter_ on entering a node, where class name is the class of the node in lower case. + * leave_ on leaving a node, where class name is the class of the node in lower case. + """ + + def __init__(self, handler: Any) -> None: + self._handler = handler + self._cache: dict[type, _EnterAndLeaveFunctions] = {} + + def walk(self, node: astroid.NodeNG) -> None: + self.__walk(node, set()) + + def __walk(self, node: astroid.NodeNG, visited_nodes: set[astroid.NodeNG]) -> None: + if node in visited_nodes: + raise AssertionError("Node visited twice") # pragma: no cover + visited_nodes.add(node) + + self.__enter(node) + for child_node in node.get_children(): + self.__walk(child_node, visited_nodes) + self.__leave(node) + + def __enter(self, node: astroid.NodeNG) -> None: + method = self.__get_callbacks(node)[0] + if method is not None: + method(node) + + def __leave(self, node: astroid.NodeNG) -> None: + method = self.__get_callbacks(node)[1] + if method is not None: + method(node) + + def __get_callbacks(self, node: astroid.NodeNG) -> _EnterAndLeaveFunctions: + klass = node.__class__ + methods = self._cache.get(klass) + + if methods is None: + handler = self._handler + class_name = klass.__name__.lower() + enter_method = getattr(handler, f"enter_{class_name}", getattr(handler, "enter_default", None)) + leave_method = getattr(handler, f"leave_{class_name}", getattr(handler, "leave_default", None)) + self._cache[klass] = (enter_method, leave_method) + else: + enter_method, leave_method = methods + + return enter_method, leave_method diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/_build_call_graph.py b/src/safeds_stubgen/api_analyzer/purity_analysis/_build_call_graph.py new file mode 100644 index 00000000..c9646642 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/_build_call_graph.py @@ -0,0 +1,411 @@ +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + Builtin, + CallGraphForest, + CallGraphNode, + ClassScope, + CombinedCallGraphNode, + CombinedSymbol, + Import, + ImportedCallGraphNode, + NodeID, + Parameter, + Reasons, + Symbol, + UnknownProto, +) + + +class CallGraphBuilder: + """Class for building a call graph. + + This class is used to build a call graph forest for a module from a dict of Reasons. + + Attributes + ---------- + classes : + Classnames in the module as key and their corresponding ClassScope instance as value. + raw_reasons : + The raw reasons for impurity for all functions. + Keys are the ids of the functions. + call_graph_forest : + The call graph forest for the given functions. + visited : + A set of all visited nodes. + + Parameters + ---------- + classes : + Classnames in the module as key and their corresponding ClassScope instance as value. + raw_reasons : + The raw reasons for impurity for all functions. + Keys are the ids of the functions. + """ + + def __init__( + self, + classes: dict[str, ClassScope], + raw_reasons: dict[NodeID, Reasons], + ) -> None: + self.classes = classes + self.raw_reasons = raw_reasons + self.call_graph_forest = CallGraphForest() + self.visited: set[NodeID] = set() + + self._build_call_graph_forest() + + def _build_call_graph_forest(self) -> CallGraphForest: + """Build the call graph forest. + + Build the call graph forest for the functions of a given module. + + Returns + ------- + call_graph_forest : + The call graph forest for the given functions. + """ + # Prepare the classes for the call graph. + self._prepare_classes() + + # Create a new CallGraphNode for each function and add it to the forest. + for reason in self.raw_reasons.values(): + # Check if the CallGraphNode is already in the forest and has no calls left to deal with. + if ( + self.call_graph_forest.has_graph(reason.id) + and not self.call_graph_forest.get_graph(reason.id).reasons.calls + ): + continue + + # Build the call graph for the function and add it to the forest. + self._built_call_graph(reason) + + # Handle cycles in the call graph. + self._handle_cycles() + + return self.call_graph_forest + + def _prepare_classes(self) -> None: + """Prepare the classes of the module. + + Adds the classes of the module to the call graph. + Since classes can be called (initialized) like functions, + they need to be added to the call graph forest to propagate the information from the init function + (which is indirectly invoked by the class call). + This is done by creating a new CallGraphNode for each class + and adding it to the forest before the call graph is built. + """ + for klass in self.classes.values(): + # Create a new CallGraphNode for each class and add it to the forest. + class_cgn = CallGraphNode(symbol=klass.symbol, reasons=Reasons(klass.symbol.id)) + # If the class has a __new__, __init__ or __post_init__ function, add it to the class node as a child. + # Also add the init function to the forest if it is not already there. + if klass.new_function: + new_cgn = CallGraphNode( + symbol=klass.new_function.symbol, + reasons=self.raw_reasons[klass.new_function.symbol.id], + ) + self.call_graph_forest.add_graph(klass.new_function.symbol.id, new_cgn) + class_cgn.add_child(new_cgn) + if klass.init_function: + init_cgn = CallGraphNode( + symbol=klass.init_function.symbol, + reasons=self.raw_reasons[klass.init_function.symbol.id], + ) + self.call_graph_forest.add_graph(klass.init_function.symbol.id, init_cgn) + class_cgn.add_child(init_cgn) + if klass.post_init_function: + post_init_cgn = CallGraphNode( + symbol=klass.post_init_function.symbol, + reasons=self.raw_reasons[klass.post_init_function.symbol.id], + ) + self.call_graph_forest.add_graph(klass.post_init_function.symbol.id, post_init_cgn) + class_cgn.add_child(post_init_cgn) + + # Add the class to the forest. + self.call_graph_forest.add_graph(klass.symbol.id, class_cgn) + + def _built_call_graph(self, reason: Reasons) -> None: + """Build the call graph for a function. + + Recursively builds the call graph for a function and adds it to the forest. + The order in which the functions are handled does not matter, + since the functions will set the pointers to the children if needed. + + Parameters + ---------- + reason : + The raw reasons of the function. + """ + # If the node has already been visited, return + if reason.id in self.visited: + return + + # Mark the current node as visited + self.visited.add(reason.id) + + # If the node is already inside the forest and does not have any calls left, it is considered to be finished. + if self.call_graph_forest.has_graph(reason.id) and not reason.calls: + return # pragma: no cover + # If the node is already inside the forest but still has calls left, it needs to be updated. + if self.call_graph_forest.has_graph(reason.id): + cgn = self.call_graph_forest.get_graph(reason.id) + # Create a new node and add it to the forest. + else: + cgn = CallGraphNode( + symbol=reason.function_scope.symbol, # type: ignore[union-attr] # function_scope is never None here + reasons=reason, + ) + self.call_graph_forest.add_graph(reason.id, cgn) + + # The node has calls, which need to be added to the forest and to the children of the current node. + # They are sorted to ensure a deterministic order of the children (especially but not only for testing). + sorted_calls = sorted(cgn.reasons.calls, key=lambda x: x.id) + for call in sorted_calls: + if call in self.call_graph_forest.get_graph(reason.id).reasons.calls: + self.call_graph_forest.get_graph(reason.id).reasons.calls.remove(call) + if isinstance(call, Builtin): + builtin_cgn = CallGraphNode(symbol=call, reasons=Reasons(call.id)) + self.call_graph_forest.get_graph(reason.id).add_child(builtin_cgn) + + # Check if the called child function is already in the forest and has no calls left to deal with. + elif ( + self.call_graph_forest.has_graph(call.id) + and not self.call_graph_forest.get_graph(call.id).reasons.calls + ): + # Add the child to the children of the current node since it doesn't need further handling. + self.call_graph_forest.get_graph(reason.id).add_child(self.call_graph_forest.get_graph(call.id)) + + # Check if the node was declared inside the current module. + elif call.id not in self.raw_reasons: + self._handle_unknown_call(call, reason) + + # Build the call graph for the child function and add it to the children of the current node. + else: + self._built_call_graph(self.raw_reasons[call.id]) + self.call_graph_forest.get_graph(reason.id).add_child(self.call_graph_forest.get_graph(call.id)) + + def _handle_unknown_call(self, call: Symbol, reason: Reasons) -> None: + """Handle unknown calls. + + Deal with unknown calls and add them to the forest. + Unknown calls are calls of unknown code, calls of imported code, or calls of parameters. + If the call references an imported function, it is represented as ImportedCallGraphNode in the forest. + + Parameters + ---------- + call : + The call that is unknown. + reason : + The reason of the function that contains the unknown call. + """ + # Deal with the case that the call calls an imported function. + if isinstance(call, Import): + imported_cgn = ImportedCallGraphNode( + symbol=call, + reasons=Reasons(id=call.id), + ) + self.call_graph_forest.add_graph(call.id, imported_cgn) + self.call_graph_forest.get_graph(reason.id).add_child(self.call_graph_forest.get_graph(call.id)) + + # If the call was used as a member of an MemberAccessValue, it needs to be removed from the unknown_calls. + # This is due to the improved analysis that can determine the module through the receiver of that call. + # Hence, the call is handled as a call of an imported function and not as an unknown_call + # when inferring the purity later. + for unknown_call in self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls.copy().values(): + if unknown_call.symbol.node == call.call: + ( + self.call_graph_forest.get_graph(reason.id).reasons.remove_unknown_call( + NodeID.calc_node_id(call.call), + ) + ) + + # Deal with the case that the call calls a function parameter. + elif isinstance(call, Parameter): + self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls[call.id] = UnknownProto( + symbol=call, + origin=reason.function_scope.symbol if reason.function_scope else None, + ) + + else: + self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls[call.id] = UnknownProto( + symbol=call, + origin=reason.function_scope.symbol if reason.function_scope else None, + ) + + def _handle_cycles(self, removed_nodes: set[NodeID] | None = None) -> None: + """Handle cycles in the call graph. + + Handles cycles within the call graph. + Iterates over the forest and checks for cycles in each tree. + If a cycle is found, it is contracted into a single node and all pointers are updated respectively. + Stores all nodes that have been removed (=nodes inside combined nodes) in the removed_nodes set. + + Parameters + ---------- + removed_nodes : + A set of all removed nodes. + If not given, a new set is created. + """ + if removed_nodes is None: + removed_nodes = set() + for graph in self.call_graph_forest.graphs.copy().values(): + if graph.symbol.id in removed_nodes or all( + child.symbol.id in removed_nodes for child in graph.children.values() + ): + continue + + cycle = self._test_cgn_for_cycles(graph) + if cycle: + self._contract_cycle(cycle) + removed_nodes.update(cycle.keys()) + + def _test_cgn_for_cycles( + self, + cgn: CallGraphNode, + visited_nodes: set[CallGraphNode] | None = None, + path: list[NodeID] | None = None, + ) -> dict[NodeID, CallGraphNode]: + """Test for cycles in the call graph. + + Recursively traverses the call graph and checks for cycles. + It uses a DFS approach to traverse the graph. + If a cycle is found, a dict of all nodes in the cycle is returned. + + Parameters + ---------- + cgn : + The current node in the graph that is visited. + visited_nodes : + A set of all visited nodes. + path : + A list of all nodes in the current path. + + Returns + ------- + cycle : + Dict of all nodes in the cycle. + Keys are the NodeIDs of the nodes. + Returns an empty dict if no cycle is found. + """ + # If the visited_nodes set is not given, create a new one. + if visited_nodes is None: + visited_nodes = set() + # If the path list is not given, create a new one. + if path is None: + path = [] + + # If the current node is already in the path, a cycle is found. + if cgn.symbol.id in path: + cut_path = path[path.index(cgn.symbol.id) :] + return {node_id: self.call_graph_forest.get_graph(node_id) for node_id in cut_path} + + # If a node has no children, it is a leaf node, and an empty list is returned. + if not cgn.children: + return {} + + # Mark the current node as visited. + visited_nodes.add(cgn) + path.append(cgn.symbol.id) + + cycle: dict[NodeID, CallGraphNode] = {} + + # Check for cycles in children. + for child in cgn.children.values(): + cycle = self._test_cgn_for_cycles(child, visited_nodes, path) + if cycle: + return cycle + # Remove the current node from the path when backtracking. + path.pop() + + return cycle + + def _contract_cycle(self, cycle: dict[NodeID, CallGraphNode]) -> None: + """Contract a cycle in the call graph. + + Contracts a cycle in the call graph into a single node. + Therefore, creates a new CombinedCallGraphNode out of all nodes in the cycle and adds it to the forest. + + Parameters + ---------- + cycle : + A dict of all nodes in the cycle. + Keys are the NodeIDs of the CallGraphNodes. + """ + # Create the new combined node. + combined_name = "+".join(sorted(c.__str__() for c in cycle)) + module = ( + next(iter(cycle.values())).symbol.node.root().name + if (next(iter(cycle.values())).symbol.node and next(iter(cycle.values())).symbol.node.root().name != "") + else None + ) + combined_id = NodeID(module, combined_name) + combined_reasons = Reasons(id=combined_id).join_reasons_list([node.reasons for node in cycle.values()]) + combined_cgn = CombinedCallGraphNode( + symbol=CombinedSymbol( + node=None, + id=combined_id, + name=combined_name, + ), + reasons=combined_reasons, + ) + combines: dict[NodeID, CallGraphNode] = {} + # Check if the combined node is already in the forest. + if self.call_graph_forest.has_graph(combined_cgn.symbol.id): + return # pragma: no cover + + # Find all other calls (calls that are not part of the cycle) and remove all nodes in the cycle from the forest. + for node in cycle.values(): + for child in node.children.values(): + if child.symbol.id not in cycle and not combined_cgn.has_child(child.symbol.id): + combined_cgn.add_child(child) + self.call_graph_forest.delete_graph(node.symbol.id) + + if isinstance(node, CombinedCallGraphNode): + combines.update(node.combines) + else: + combines[node.symbol.id] = node + combined_cgn.combines = combines + + # Add the combined node to the forest. + self.call_graph_forest.add_graph(combined_id, combined_cgn) + + # Set all pointers from nodes calling the nodes in the cycle to the combined node. + self._update_pointers(cycle, combined_cgn) + + def _update_pointers(self, cycle: dict[NodeID, CallGraphNode], combined_node: CombinedCallGraphNode) -> None: + """Replace all pointers to nodes inside the cycle with pointers to the combined node. + + Traverses the tree and replaces all pointers to nodes in the cycle with pointers to the combined node. + + Parameters + ---------- + cycle : + A dict of all nodes in the cycle. + Keys are the NodeIDs of the nodes. + combined_node : + The combined node that replaces all nodes in the cycle. + """ + for graph in self.call_graph_forest.graphs.values(): + for child in graph.children.copy().values(): + if child.symbol.id in cycle: + graph.delete_child(child.symbol.id, store_it=True) + graph.add_child(combined_node) + + +def build_call_graph(classes: dict[str, ClassScope], raw_reasons: dict[NodeID, Reasons], module_name: str = "") -> CallGraphForest: + """Build the call graph forest for the given classes and reasons. + + Parameters + ---------- + classes : + Classnames in the module as key and their corresponding ClassScope instance as value. + raw_reasons : + The raw reasons for impurity for all functions. + Keys are the ids of the functions. + + Returns + ------- + call_graph_forest : + The call graph forest for the given functions. + """ + forest = CallGraphBuilder(classes, raw_reasons).call_graph_forest + return forest diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/_get_module_data.py b/src/safeds_stubgen/api_analyzer/purity_analysis/_get_module_data.py new file mode 100644 index 00000000..2a54684e --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/_get_module_data.py @@ -0,0 +1,1236 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +import astroid + +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + BUILTIN_CLASSSCOPES, + ClassScope, + ClassVariable, + FunctionScope, + GlobalVariable, + Import, + InstanceVariable, + LocalVariable, + MemberAccess, + MemberAccessTarget, + MemberAccessValue, + ModuleData, + NodeID, + Parameter, + ParameterKind, + Reference, + Scope, + Symbol, +) +from ._astroid_ast_walker import ASTWalker + +_ComprehensionType = astroid.ListComp | astroid.DictComp | astroid.SetComp | astroid.GeneratorExp + + +@dataclass +class ModuleDataBuilder: + """ + A ModuleDataBuilder instance is used to find all information relevant for the purity analysis of a module. + + It must be handed to an ASTWalker instance to collect all information. + After the ASTWalker has walked the AST, + the ModuleDataBuilder instance contains all information relevant to the purity analysis of the module. + + Attributes + ---------- + current_node_stack : + Stack of nodes that are currently visited by the ASTWalker. + The last node in the stack is the current node. + It Is only used while walking the AST. + current_function_def : + Stack of FunctionScopes that are currently visited by the ASTWalker. + The top of the stack is the current function definition. + It is only used while walking the AST. + children : + All found children nodes are stored in children until their scope is determined. + After the AST is completely walked, the resulting "Module"- Scope is stored in children. + (children[0]) + targets : + All found targets are stored in targets until their scope is determined. + values : + All found names are stored in names until their scope is determined. + It Is only used while walking the AST. + calls : + All calls found on function level are stored in calls until their scope is determined. + It Is only used while walking the AST. + classes : + Classnames in the module as key and their corresponding ClassScope instance as value. + functions : + Function names in the module as key and a list of their corresponding FunctionScope instances as value. + global_variables : + All global variables and their corresponding Scope instance. + imports : + All imports and their corresponding Import instance. + """ + + current_node_stack: list[Scope] = field(default_factory=list) + current_function_def: list[FunctionScope] = field(default_factory=list) + children: list[Scope] = field(default_factory=list) + targets: list[Symbol] = field(default_factory=list) + values: list[Reference] = field(default_factory=list) + calls: list[Reference] = field(default_factory=list) + classes: dict[str, ClassScope] = field(default_factory=dict) + functions: dict[str, list[FunctionScope]] = field(default_factory=dict) + global_variables: dict[str, Scope] = field(default_factory=dict) + imports: dict[str, Import] = field(default_factory=dict) + + @staticmethod + def has_assignattr_parent(node: astroid.Attribute) -> bool: + """Check if any parent of the given node is an AssignAttr node. + + Since astroid generates an Attribute node for every attribute access, + and it is possible to have nested attribute accesses, + it is possible that the direct parent is not an AssignAttr node. + In this case, check if any parent of the given node is an AssignAttr node. + + Parameters + ---------- + node : + The node whose parents are to be checked. + + Returns + ------- + bool + True if any parent of the given node is an AssignAttr node, False otherwise. + True means that the given node is a target node, False means that the given node is a value node. + """ + current_node = node + while current_node is not None: + if isinstance(current_node, astroid.AssignAttr): + return True + current_node = current_node.parent + return False + + def get_symbol(self, node: astroid.NodeNG, current_scope: astroid.NodeNG | None) -> Symbol: + """Get the symbol of a node. + + It matches the current scope of the node and returns the corresponding symbol for the given node. + This function can not handle Import and ImportFrom nodes since they need special treatment. + + Parameters + ---------- + node : + The node whose symbol is to be determined. + current_scope : + The current scope of the node (is None if the node is the module node). + """ + match current_scope: + case astroid.Module() | None: + if isinstance( + node, + _ComprehensionType | astroid.Lambda | astroid.Try, + ) and not isinstance(node, astroid.FunctionDef): + return GlobalVariable(node=node, id=NodeID.calc_node_id(node), name=node.__class__.__name__) + return GlobalVariable(node=node, id=NodeID.calc_node_id(node), name=node.name) + + case astroid.ClassDef(): + # Functions inside a class are defined as class variables if they are defined in the class scope. + # if isinstance(node, astroid.FunctionDef): + # return LocalVariable(node=node, id=_NodeID.calc_node_id(node), name=node.name) + if isinstance( + node, + _ComprehensionType | astroid.Lambda | astroid.Try, + ) and not isinstance(node, astroid.FunctionDef): + return ClassVariable( + node=node, + id=NodeID.calc_node_id(node), + name=node.__class__.__name__, + klass=current_scope, + ) + return ClassVariable(node=node, id=NodeID.calc_node_id(node), name=node.name, klass=current_scope) + + case astroid.FunctionDef(): + # Find instance variables (in the constructor) + if ( + isinstance(current_scope, astroid.FunctionDef) + and isinstance(node, MemberAccessTarget) + and current_scope.name == "__init__" + ): + return InstanceVariable( + node=node, + id=NodeID.calc_node_id(node), + name=node.member, + klass=current_scope.parent, + ) + + # Find parameters + if ( + isinstance(node, astroid.AssignName) + and isinstance(self.current_node_stack[-1], FunctionScope) + and node.name in self.current_node_stack[-1].parameters + ): + return Parameter(node=node, id=NodeID.calc_node_id(node), name=node.name) + + # Special cases for nodes inside functions that are defined as LocalVariables but which do not have a name + if isinstance(node, _ComprehensionType | astroid.Lambda | astroid.Try): + return LocalVariable(node=node, id=NodeID.calc_node_id(node), name=node.__class__.__name__) + + if ( + isinstance(node, astroid.Name | astroid.AssignName) + and node.name in node.root().globals + and node.name not in current_scope.locals + ): + return GlobalVariable(node=node, id=NodeID.calc_node_id(node), name=node.name) + + return LocalVariable(node=node, id=NodeID.calc_node_id(node), name=node.name) + + case ( + astroid.Lambda() | astroid.ListComp() | astroid.DictComp() | astroid.SetComp() | astroid.GeneratorExp() + ): + # This deals with the case where a lambda function has parameters + if isinstance(node, astroid.AssignName) and isinstance(node.parent, astroid.Arguments): + return Parameter(node=node, id=NodeID.calc_node_id(node), name=node.name) + # This deals with global variables that are used inside a lambda + if isinstance(node, astroid.AssignName) and node.name in self.global_variables: + return GlobalVariable(node=node, id=NodeID.calc_node_id(node), name=node.name) + return LocalVariable( + node=node, + id=NodeID.calc_node_id(node), + name=node.name if hasattr(node, "name") else "None", + ) + + case ( + astroid.Try() + ): # TODO: can we summarize Lambda and ListComp here? -> only if nodes in try except are not global + return LocalVariable( + node=node, + id=NodeID.calc_node_id(node), + name=node.name if hasattr(node, "name") else "None", + ) + + # This line is a fallback but should never be reached + return Symbol(node=node, id=NodeID.calc_node_id(node), name=node.name) # pragma: no cover + + def _detect_scope(self, current_node: astroid.NodeNG) -> None: + """ + Detect the scope of the given node. + + Detecting the scope of a node means finding the node in the scope tree that defines the scope of the given node. + The scope of a node is defined by the parent node in the scope tree. + This function is called when the ASTWalker leaves a node. + It also adds classes and functions to their corresponding dicts + while dealing with the construction of the corresponding Scope instance (FunctionsScope, ClassScope). + + Parameters + ---------- + current_node : + The node whose scope is to be determined. + """ + outer_scope_children: list[Scope] = [] + inner_scope_children: list[Scope] = [] + # This is only the case when the module is left: every child must be in the inner scope (=module scope). + # This speeds up the process of finding the scope of the children. + if isinstance(current_node, astroid.Module): + inner_scope_children = self.children + # Look at a nodes' parent node to determine if it is in the scope of the current node. + else: + for child in self.children: + if ( + child.parent is not None and child.parent.symbol.node != current_node + ): # Check if the child is in the scope of the current node. + outer_scope_children.append(child) # Add the child to the outer scope. + else: + inner_scope_children.append(child) # Add the child to the inner scope. + + self.current_node_stack[-1].children = inner_scope_children # Set the children of the current node. + self.children = outer_scope_children # Keep the children that are not in the scope of the current node. + self.children.append(self.current_node_stack[-1]) # Add the current node to the children. + + # TODO: ideally this should not be part of detect_scope since it is just called when we leave the corresponding node + # Analyze the current node regarding class exclusive property's. + if isinstance(current_node, astroid.ClassDef): + self._analyze_class(current_node) + + # Analyze the current node regarding function exclusive property's. + if isinstance(current_node, astroid.FunctionDef): + self._analyze_function(current_node) + + # Analyze the current node regarding lambda exclusive property's. + if isinstance(current_node, astroid.Lambda): + self._analyze_lambda(current_node) + + self.current_node_stack.pop() # Remove the current node from the stack. + + def _analyze_class(self, current_node: astroid.ClassDef) -> None: + """Analyze a ClassDef node. + + This is called while the scope of a node is detected. + It must only be called when the current node is of type ClassDef. + This adds the ClassScope to the classes dict and adds all class variables and instance variables to their dicts. + + Parameters + ---------- + current_node : + The node to analyze. + """ + if not isinstance(current_node, astroid.ClassDef): + return # pragma: no cover + # Add classdef to the classes dict. + if isinstance(self.current_node_stack[-1], ClassScope): + self.classes[current_node.name] = self.current_node_stack[-1] + + # Add class variables to the class_variables dict. + for child in self.current_node_stack[-1].children: + if ( + isinstance(child.symbol, ClassVariable) + and isinstance(self.current_node_stack[-1], ClassScope) + and hasattr(self.current_node_stack[-1], "class_variables") + ): + self.current_node_stack[-1].class_variables.setdefault(child.symbol.name, []).append(child.symbol) + + def _analyze_function(self, current_node: astroid.FunctionDef) -> None: + """Analyze a FunctionDef node. + + This is called while the scope of a node is detected. + It must only be called when the current node is of type FunctionDef. + Add the FunctionScope to the functions' dict. + Add all targets, values and calls that are collected inside the function to the FunctionScope instance. + + Parameters + ---------- + current_node : + The node to analyze. + """ + if not isinstance(current_node, astroid.FunctionDef): + return # pragma: no cover + # Extend the dict of functions with the current node or create + # a new dict entry with the list containing the current node + # if the function name is already in the dict + if current_node.name in self.functions: + self.functions[current_node.name].append(self.current_function_def[-1]) + else: # better for readability + self.functions[current_node.name] = [self.current_function_def[-1]] + + # If the function is the constructor of a class, analyze it to find the instance variables of the class. + if current_node.name in ("__init__", "__new__", "__post_init__"): + self._analyze_constructor(current_node.name) + + # Add all calls that are used inside the function body to its calls' dict. + if self.calls: + for call in self.calls: + self.functions[current_node.name][-1].call_references.setdefault(call.name, []).append(call) + self.calls = [] + + # Add all targets that are used inside the function body to its targets' dict. + if self.targets: + parent_targets = [] + for target in self.targets: + if self.find_first_parent_function(target.node) == self.current_function_def[-1].symbol.node: + self.current_function_def[-1].target_symbols.setdefault(target.name, []).append(target) + self.targets = [] + else: + parent_targets.append(target) + if parent_targets: + self.targets = parent_targets + + # Add all values that are used inside the function body to its values' dict. + if self.values: + for value in self.values: + if self.find_first_parent_function(value.node) == self.current_function_def[-1].symbol.node: + self.current_function_def[-1].value_references.setdefault(value.name, []).append(value) + self.values = [] + + def _analyze_lambda(self, current_node: astroid.Lambda) -> None: + """Analyze a Lambda node. + + This is called while the scope of a node is detected. + It must only be called when the current node is of type Lambda. + Add the Lambda FunctionScope to the functions' dict if the lambda function is assigned a name. + Add all values and calls that are collected inside the lambda to the Lambda FunctionScope instance. + Also add these values to the surrounding scope if it is of type FunctionScope. + This is due to the fact that lambda functions define a scope themselves + and otherwise the values would be lost. + """ + if not isinstance(current_node, astroid.Lambda): + return # pragma: no cover + + # Add lambda functions that are assigned to a name (and therefor are callable) to the functions' dict. + if isinstance(current_node, astroid.Lambda) and isinstance(current_node.parent, astroid.Assign): + # Make sure there is no AttributeError because of the inconsistent names in the astroid API. + if isinstance(current_node.parent.targets[0], astroid.AssignAttr): + node_name = current_node.parent.targets[0].attrname + elif isinstance(current_node.parent.targets[0], astroid.AssignName): + node_name = current_node.parent.targets[0].name + else: + node_name = "Lambda" # pragma: no cover + # If the Lambda function is assigned to a name, it can be called just as a normal function. + # Since Lambdas normally do not have names, they need to be assigned manually. + self.current_function_def[-1].symbol.name = node_name + self.current_function_def[-1].symbol.node.name = node_name + self.current_function_def[-1].symbol.id.name = node_name + + # Extend the dict of functions with the current node or create a new list with the current node. + self.functions.setdefault(node_name, []).append(self.current_function_def[-1]) + + # Add all targets that are used inside the function body to its targets' dict. + if self.targets: + for target in self.targets: + self.current_function_def[-1].target_symbols.setdefault(target.name, []).append(target) + self.targets = [] + + # Add all values that are used inside the function body to its values' dict. + if self.values: + for value in self.values: + self.current_function_def[-1].value_references.setdefault(value.name, []).append(value) + self.values = [] + + # Add all calls that are used inside the function body to its calls' dict. + if self.calls: + for call in self.calls: + self.functions[current_node.name][-1].call_references.setdefault(call.name, []).append(call) + self.calls = [] + + # Lambda Functions that have no name are hard to deal with when building the call graph. Therefore, + # add all of their targets/values/calls to the parent function to indirectly add the needed impurity info + # to the parent function. From here, assume that lambda functions are only used inside a function body + # (other cases would be irrelevant for function purity anyway). + # Anyway, all names in the lambda function are of local scope. + # Therefore, assign a FunctionScope instance with the name 'Lambda' to represent that. + if ( + isinstance(current_node, astroid.Lambda) + and not isinstance(current_node, astroid.FunctionDef) + and isinstance(current_node.parent, astroid.Call | astroid.Expr) + # Call deals with: (lambda x: x+1)(2) and Expr deals with: lambda x: x+1 + ): + # Add all targets that are used inside the function body to its targets' dict. + if self.targets: + for target in self.targets: + self.current_function_def[-1].target_symbols.setdefault(target.name, []).append(target) + self.targets = [] + + # Add all values that are used inside the lambda body to its parent function values' dict. + if ( + self.values + and len(self.current_function_def) >= 2 + and isinstance(self.current_node_stack[-2], FunctionScope) + ): + for value in self.values: + if value.name not in self.current_function_def[-1].parameters: + self.current_function_def[-2].value_references.setdefault(value.name, []).append(value) + + # Add the values to the Lambda FunctionScope. + if ( + self.values + and isinstance(self.current_function_def[-1], FunctionScope) + and isinstance(self.current_function_def[-1].symbol.node, astroid.Lambda) + ): + for value in self.values: + self.current_function_def[-1].value_references.setdefault(value.name, []).append(value) + self.values = [] + + # Add all calls that are used inside the lambda body to its parent function calls' dict. + if ( + self.calls + and len(self.current_function_def) >= 2 + and isinstance(self.current_function_def[-2], FunctionScope) + ): + for call in self.calls: + if call.name not in self.current_function_def[-2].call_references: + self.current_function_def[-2].call_references[call.name] = [call] + else: + self.current_function_def[-2].call_references[call.name].append(call) # pragma: no cover + + # Add the calls to the Lambda FunctionScope. + if ( + self.calls + and isinstance(self.current_function_def[-1], FunctionScope) + and isinstance(self.current_function_def[-1].symbol.node, astroid.Lambda) + ): + for call in self.calls: + if call.name not in self.current_function_def[-1].call_references: + self.current_function_def[-1].call_references[call.name] = [call] + else: + self.current_function_def[-1].call_references[call.name].append(call) # pragma: no cover + self.calls = [] + + # Add all globals that are used inside the Lambda to the parent function globals list. + if self.current_function_def[-1].globals_used: + for glob_name, glob_def_list in self.current_function_def[-1].globals_used.items(): + if ( + len(self.current_function_def) >= 2 + and glob_name not in self.current_function_def[-2].globals_used + ): + self.current_function_def[-2].globals_used[glob_name] = glob_def_list + else: + for glob_def in glob_def_list: + if ( + len(self.current_function_def) >= 2 + and glob_def not in self.current_function_def[-2].globals_used[glob_name] + ): + self.current_function_def[-2].globals_used[glob_name].append(glob_def) # pragma: no cover + + def _analyze_constructor(self, function_name: str) -> None: + """Analyze the constructor of a class. + + The constructor of a class is a special function called when an instance of the class is created. + This function must only be called when the name of the FunctionDef node is `__init__`. + """ + if function_name == "__init__": + # Add instance variables to the instance_variables list of the class. + for child in self.current_function_def[-1].children: + if ( + isinstance(child.symbol, InstanceVariable) + and isinstance(self.current_function_def[-1].parent, ClassScope) + and hasattr(self.current_function_def[-1].parent, "instance_variables") + ): + self.current_function_def[-1].parent.instance_variables.setdefault(child.symbol.name, []).append( + child.symbol, + ) + + # Add __init__ function to ClassScope. + if isinstance(self.current_function_def[-1].parent, ClassScope) and hasattr( + self.current_function_def[-1].parent, + "init_function", + ): + self.current_function_def[-1].parent.init_function = self.current_function_def[-1] + elif function_name == "__new__": + # Add __new__ function to ClassScope. + if isinstance(self.current_function_def[-1].parent, ClassScope) and hasattr( + self.current_function_def[-1].parent, + "new_function", + ): + self.current_function_def[-1].parent.new_function = self.current_function_def[-1] + elif function_name == "__post_init__": + # Add __post_init__ function to ClassScope. + if isinstance(self.current_function_def[-1].parent, ClassScope) and hasattr( + self.current_function_def[-1].parent, + "post_init_function", + ): + self.current_function_def[-1].parent.post_init_function = self.current_function_def[-1] + + def find_first_parent_function(self, node: astroid.NodeNG | MemberAccess) -> astroid.NodeNG: + """Find the first parent of a call node that is a function. + + Parameters + ---------- + node : + The node to start the search from. + + Returns + ------- + astroid.NodeNG + The first parent of the node that is a function. + If the parent is a module, return the module. + """ + if isinstance(node, MemberAccess): + node = node.node # This assures that the node to calculate the parent function exists. + if isinstance(node.parent, astroid.FunctionDef | astroid.Lambda | astroid.Module | None): + return node.parent + return self.find_first_parent_function(node.parent) + + def handle_arg(self, node: astroid.AssignName, kind: ParameterKind) -> None: + """Handle an argument node. + + This function is called when a vararg or a kwarg parameter is found inside an Argument node. + This is needed because astroid does not generate a symbol for these nodes. + Therefore, create one manually and add it to the parameters' dict. + + Parameters + ---------- + node : + The node that is to be handled. + kind : + The kind of the parameter. + """ + scope_node = Scope( + _symbol=Parameter(node, NodeID.calc_node_id(node), node.name, kind=kind), + _children=[], + _parent=self.current_node_stack[-1], + ) + self.targets.append(scope_node.symbol) + self.children.append(scope_node) + self.add_arg_to_function_scope_parameters(node, kind) + + def add_arg_to_function_scope_parameters(self, argument: astroid.AssignName, kind: ParameterKind) -> None: + """Add an argument to the parameters dict of the current function scope. + + Parameters + ---------- + argument : + The argument node to add to the parameter dict. + kind : + The kind of the parameter. + """ + if isinstance(self.current_node_stack[-1], FunctionScope): + self.current_node_stack[-1].parameters[argument.name] = Parameter( + argument, + NodeID.calc_node_id(argument), + argument.name, + kind=kind, + ) + + def is_annotated(self, node: astroid.NodeNG | MemberAccess, found_annotation_node: bool) -> bool: + """Check if the Name node is a type hint. + + Parameters + ---------- + node : + The node to check. + found_annotation_node : + A bool that indicates if an annotation node is found. + + Returns + ------- + bool + True if the node is a type hint, False otherwise. + """ + # Condition that checks if an annotation node is found. + # This can be extended by all nodes indicating a type hint. + if isinstance(node, astroid.Arguments | astroid.AnnAssign): + return True + + # This checks if the node is used as a return type + if isinstance(node.parent, astroid.FunctionDef) and node.parent.returns and node == node.parent.returns: + return True + + # Return the current bool if an assignment node is found. + # This is the case when there are no more nested nodes that could contain a type hint property. + if isinstance(node, astroid.Assign) or found_annotation_node: + return found_annotation_node + + # Check the parent of the node for annotation types. + elif node.parent is not None: + return self.is_annotated(node.parent, found_annotation_node=found_annotation_node) + + return found_annotation_node + + # TODO: this lookup could be more efficient if we would add all global nodes to the dict when 'enter_module' is called + # we than can be sure that all globals are detected already and we do not need to traverse the tree + def check_if_global(self, name: str, node: astroid.NodeNG) -> list[astroid.AssignName] | None: + """ + Check if a name is a global variable. + + Checks if a name is a global variable inside the root of the given node + and return its assignment node if it is a global variable. + + Parameters + ---------- + name : + The variable name to check. + node : + The node whose root is to be checked. + + Returns + ------- + list[astroid.AssignName] | None + The symbol of the global variable if it exists, None otherwise. + """ + if not isinstance(node, astroid.Module): + return self.check_if_global(name, node.parent) + elif isinstance(node, astroid.Module) and name in node.globals: + # The globals() dict contains all assignments of the node with this name + # (this includes assignments in other scopes). + # Only add the assignments of the nodes which are assigned on module scope (true global variables). + return [ + node for node in node.globals[name] if isinstance(self.find_first_parent_function(node), astroid.Module) + ] + return None + + def find_base_classes(self, node: astroid.ClassDef) -> list[ClassScope]: + """Find a list of all base classes of the given class. + + If a class has no base classes, an empty list is returned. + + Parameters + ---------- + node : + The class whose base classes are to be found. + + Returns + ------- + list[ClassScope] + A list of all base classes of the given class if it has any, else an empty list. + """ + base_classes: list[ClassScope] = [] + for base in node.bases: + if isinstance(base, astroid.Name): + if base.name in self.classes: + base_classes.append(self.classes[base.name]) + elif base.name in BUILTIN_CLASSSCOPES: + base_classes.append(BUILTIN_CLASSSCOPES[base.name]) + + return base_classes + + def enter_module(self, node: astroid.Module) -> None: + """ + Enter a module node. + + The module node is the root node of the AST, so it has no parent (parent is None). + The module node is also the first node that is visited, so the current_node_stack is empty before entering the module node. + + Parameters + ---------- + node : + The module node to enter. + """ + self.current_node_stack.append( + Scope(_symbol=self.get_symbol(node, None), _children=[], _parent=None), + ) + + def leave_module(self, node: astroid.Module) -> None: + self._detect_scope(node) + + def enter_classdef(self, node: astroid.ClassDef) -> None: + self.current_node_stack.append( + ClassScope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + instance_variables={}, + class_variables={}, + super_classes=self.find_base_classes(node), + ), + ) + + def leave_classdef(self, node: astroid.ClassDef) -> None: + self._detect_scope(node) + + def enter_functiondef(self, node: astroid.FunctionDef) -> None: + if node.decorators: + for decorator in node.decorators.nodes: + if isinstance(decorator, astroid.Name) and decorator.name == "overload": + return + elif isinstance(decorator, astroid.Name) and decorator.name == "property": + if isinstance(self.current_node_stack[-1], ClassScope) and hasattr( + self.current_node_stack[-1], + "instance_variables", + ): + self.current_node_stack[-1].instance_variables.setdefault(node.name, []).append( + InstanceVariable( + node=node, + id=NodeID.calc_node_id(node), + name=node.name, + klass=self.current_node_stack[-1].symbol.node, + ), + ) + + self.current_node_stack.append( + FunctionScope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + self.current_function_def.append(self.current_node_stack[-1]) # type: ignore[arg-type] + # The current_node_stack[-1] is always of type FunctionScope here. + + def leave_functiondef(self, node: astroid.FunctionDef) -> None: + if node.decorators: + for decorator in node.decorators.nodes: + if isinstance(decorator, astroid.Name) and decorator.name == "overload": + return + + self._detect_scope(node) + self.current_function_def.pop() + + def enter_asyncfunctiondef(self, node: astroid.AsyncFunctionDef) -> None: + self.current_node_stack.append( + FunctionScope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + self.current_function_def.append(self.current_node_stack[-1]) # type: ignore[arg-type] + # The current_node_stack[-1] is always of type FunctionScope here. + + def leave_asyncfunctiondef(self, node: astroid.AsyncFunctionDef) -> None: + self._detect_scope(node) + self.current_function_def.pop() + + def enter_lambda(self, node: astroid.Lambda) -> None: + self.current_node_stack.append( + FunctionScope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + self.current_function_def.append(self.current_node_stack[-1]) # type: ignore[arg-type] + # The current_node_stack[-1] is always of type FunctionScope here. + + def leave_lambda(self, node: astroid.Lambda) -> None: + self._detect_scope(node) + # self.cleanup_globals(self.current_node_stack[-1]) + self.current_function_def.pop() + + def enter_listcomp(self, node: astroid.ListComp) -> None: + self.current_node_stack.append( + Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + + def leave_listcomp(self, node: astroid.ListComp) -> None: + self._detect_scope(node) + + def enter_dictcomp(self, node: astroid.DictComp) -> None: + self.current_node_stack.append( + Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + + def leave_dictcomp(self, node: astroid.DictComp) -> None: + self._detect_scope(node) + + def enter_setcomp(self, node: astroid.SetComp) -> None: + self.current_node_stack.append( + Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + + def leave_setcomp(self, node: astroid.SetComp) -> None: + self._detect_scope(node) + + def enter_generatorexp(self, node: astroid.GeneratorExp) -> None: + self.current_node_stack.append( + Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + + def leave_generatorexp(self, node: astroid.DictComp) -> None: + self._detect_scope(node) + + def enter_try(self, node: astroid.Try) -> None: + self.current_node_stack.append( + Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=self.current_node_stack[-1], + ), + ) + + def leave_try(self, node: astroid.Try) -> None: + self._detect_scope(node) + + def enter_arguments(self, node: astroid.Arguments) -> None: + if node.args: + for arg in node.args: + kind = ParameterKind.POSITIONAL_OR_KEYWORD + self.add_arg_to_function_scope_parameters(arg, kind) + if node.kwonlyargs: + for arg in node.kwonlyargs: + kind = ParameterKind.KEYWORD_ONLY + self.add_arg_to_function_scope_parameters(arg, kind) + if node.posonlyargs: + for arg in node.posonlyargs: + kind = ParameterKind.POSITIONAL_ONLY + self.add_arg_to_function_scope_parameters(arg, kind) + if node.vararg: + kind = ParameterKind.VAR_POSITIONAL + constructed_node = astroid.AssignName( + name=node.vararg, + parent=node, + lineno=( + node.parent.lineno + if isinstance(node.parent, astroid.FunctionDef) and not node.parent.decorators + else ( + node.parent.lineno + len(node.parent.decorators.nodes) + if isinstance(node.parent, astroid.FunctionDef) + else node.parent.lineno + ) + ), + col_offset=node.parent.col_offset, + end_col_offset=node.parent.end_col_offset, + end_lineno=node.parent.end_lineno + ) + self.handle_arg(constructed_node, kind) + # TODO: col_offset is not correct: it should be the col_offset of the vararg/(kwarg) node which is not + # collected by astroid + if node.kwarg: + kind = ParameterKind.VAR_KEYWORD + constructed_node = astroid.AssignName( + name=node.kwarg, + parent=node, + lineno=( + node.parent.lineno + if isinstance(node.parent, astroid.FunctionDef) and not node.parent.decorators + else ( + node.parent.lineno + len(node.parent.decorators.nodes) + if isinstance(node.parent, astroid.FunctionDef) + else node.parent.lineno + ) + ), + col_offset=node.parent.col_offset, + end_col_offset=node.parent.end_col_offset, + end_lineno=node.parent.end_lineno + ) + self.handle_arg(constructed_node, kind) + + def enter_name(self, node: astroid.Name) -> None: + # Do not add names of decorators as values, since are not needed. + if isinstance(node.parent, astroid.Decorators) or isinstance(node.parent.parent, astroid.Decorators): + return + # Since astroid also uses Name nodes inside AssignAttr nodes, the following condition checks for that. + # These names are not added to the values' list because they are not used as values. + # Add the name to the targets dict to determine the scope of the name later. + elif isinstance(node.parent, astroid.AssignAttr): + self.targets.append(Symbol(node, NodeID.calc_node_id(node), node.name)) + + # Add the name to the values' list to determine the scope of the name later. + # Some cases need to be filtert out because they are not defined as values. + func_def = self.find_first_parent_function(node) + + if self.current_function_def and func_def == self.current_function_def[-1].symbol.node: + # Exclude propagation to a function scope if the scope within that function defines a local scope itself. + # e.g. ListComp, SetComp, DictComp, GeneratorExp + # Except the name node is a global variable, than it must be propagated to the function scope. + # TODO: dazu zählen ListComp, Lambda, TryExcept??, TryFinally?? + if ( + isinstance(self.current_node_stack[-1].symbol.node, _ComprehensionType) + and node.name not in self.global_variables + ): + return + + # Deal with other cases that need to be excluded. + if isinstance(node, astroid.Name): + # Do not add the names, that astroid used inside AssignAttr. + if isinstance(node.parent, astroid.AssignAttr): + return + + # Do not add the name of a function as a value. + if isinstance(node.parent, astroid.Call): + if isinstance(node.parent.func, astroid.Attribute): + if node.parent.func.attrname == node.name: + return # pragma: no cover + elif isinstance(node.parent.func, astroid.Name): + if node.parent.func.name == node.name: + return + + # Check if the Name belongs to a type hint, if so do not add it. + if self.is_annotated(node, found_annotation_node=False): + return + + # If none of the conditions above is true, the name node is added to the values' list. + reference = Reference(node, NodeID.calc_node_id(node), node.name) + if reference not in self.values: + self.values.append(reference) + + # Add the name to the globals list of the surrounding function if it is a variable of global scope. + global_node_defs = self.check_if_global(node.name, node) + if global_node_defs is not None: + # It is possible that a variable has more than one global assignment, + # particularly in cases where the variable is depending on a condition. + # Since this can only be determined at runtime, add all global assignments to the list. + for global_node_def in global_node_defs: + # Propagate global variables in Comprehension type to + # the surrounding function if it is a global variable. + if isinstance(global_node_def, astroid.AssignName) and ( + isinstance(self.current_node_stack[-1], FunctionScope) + or isinstance(self.current_node_stack[-1].symbol.node, _ComprehensionType | astroid.Lambda) + ): + # Create a new dict entry for a global variable (by name). + if node.name not in self.current_function_def[-1].globals_used: + symbol = self.get_symbol(global_node_def, self.current_function_def[-1].symbol.node) + if isinstance(symbol, GlobalVariable): + self.current_function_def[-1].globals_used[node.name] = [symbol] + # If the name of the global variable already exists, + # add the new declaration to the list (redeclaration). + else: + symbol = self.get_symbol(global_node_def, self.current_function_def[-1].symbol.node) + if symbol not in self.current_function_def[-1].globals_used[node.name] and isinstance( + symbol, + GlobalVariable, + ): + self.current_function_def[-1].globals_used[node.name].append(symbol) # pragma: no cover + return + + def enter_assignname(self, node: astroid.AssignName) -> None: + # Lambda assignments will not be added to the target_nodes dict because they are handled as functions. + if isinstance(node.parent, astroid.Assign) and isinstance(node.parent.value, astroid.Lambda): + return + + # The following nodes are added to the target_nodes dict, + # because they are real assignments and therefore targets. + if isinstance( + node.parent, + astroid.Assign + | astroid.Arguments + | astroid.AssignAttr + | astroid.Attribute + | astroid.AugAssign + | astroid.AnnAssign + | astroid.Return + | astroid.Compare + | astroid.For + | astroid.Tuple + | astroid.NamedExpr + | astroid.Starred + | astroid.Comprehension + | astroid.With, + ): + # Only add assignments if they are inside a function, or if they are inside a try-except block. + # Nodes inside try-except will be propagated to the next function scope. + if ( + isinstance(self.current_node_stack[-1], FunctionScope) + or isinstance(self.current_node_stack[-1].symbol.node, astroid.Try) + and self.current_function_def + and self.find_first_parent_function(node) == self.current_function_def[-1].symbol.node + ): + self.targets.append(self.get_symbol(node, self.current_node_stack[-1].symbol.node)) + + # The following nodes are no real target nodes, but astroid generates an AssignName node for them. + # They still need to be added to the children of the current scope. + if isinstance( + node.parent, + astroid.Assign + | astroid.Arguments + | astroid.AssignAttr + | astroid.Attribute + | astroid.AugAssign + | astroid.AnnAssign + | astroid.Tuple + | astroid.For + | astroid.NamedExpr + | astroid.Starred + | astroid.Comprehension + | astroid.ExceptHandler + | astroid.With, + ): + parent = self.current_node_stack[-1] + scope_node = Scope( + _symbol=self.get_symbol(node, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=parent, + ) + self.children.append(scope_node) + + # Detect global assignments and add them to the global_variables dict. + if isinstance(node.root(), astroid.Module) and node.name in node.root().globals: + self.global_variables[node.name] = scope_node + + def enter_assignattr(self, node: astroid.AssignAttr) -> None: + parent = self.current_node_stack[-1] + member_access = MemberAccessTarget.construct_member_access_target(node) + scope_node = Scope( + _symbol=self.get_symbol(member_access, self.current_node_stack[-1].symbol.node), + _children=[], + _parent=parent, + ) + self.children.append(scope_node) + + if isinstance(self.current_node_stack[-1], FunctionScope): + self.targets.append(Symbol(member_access, NodeID.calc_node_id(member_access), member_access.name)) + + def enter_attribute(self, node: astroid.Attribute) -> None: + # Do not handle names used in decorators since this would be to complex for now. + if isinstance(node.parent, astroid.Decorators): + return + + # Astroid generates an Attribute node for every attribute access. + # Check if the attribute access is a target or a value. + # Subscript deals with assignments to a dictionary. + if ( + isinstance(node.parent, astroid.AssignAttr) + or isinstance(node.parent, astroid.Subscript) + and not isinstance(node.parent.parent, astroid.Arguments) + or self.has_assignattr_parent(node) + ): + member_access = MemberAccessTarget.construct_member_access_target(node) + if isinstance(node.expr, astroid.Name) and isinstance(self.current_node_stack[-1], FunctionScope): + self.targets.append(Symbol(member_access, NodeID.calc_node_id(member_access), member_access.name)) + else: + member_access = MemberAccessValue.construct_member_access_value(node) + + if isinstance(member_access, MemberAccessTarget): + if isinstance(self.current_node_stack[-1], FunctionScope): + self.targets.append(Symbol(member_access, NodeID.calc_node_id(member_access), member_access.name)) + elif isinstance(member_access, MemberAccessValue): + # Ignore type annotations because they are not relevant for purity. + if self.is_annotated(member_access.node, found_annotation_node=False): + return + + reference = Reference(member_access, NodeID.calc_node_id(member_access), member_access.name) + self.values.append(reference) + + def enter_global(self, node: astroid.Global) -> None: + """Enter a global node. + + Global nodes are used to declare global variables inside a function. + Collect all these global variable usages and add them to the globals_used dict of that FunctionScope. + """ + for name in node.names: + global_node_defs = self.check_if_global(name, node) + if global_node_defs: + # It is possible that a variable has more than one global assignment, + # particularly in cases where the variable is depending on a condition. + # Since this can only be determined at runtime, add all global assignments to the list. + for global_node_def in global_node_defs: + if isinstance(global_node_def, astroid.AssignName) and isinstance( + self.current_node_stack[-1], + FunctionScope, + ): + symbol = self.get_symbol(global_node_def, self.current_node_stack[-1].symbol.node) + if isinstance(symbol, GlobalVariable) and hasattr(self.current_node_stack[-1], "globals_used"): + self.current_node_stack[-1].globals_used.setdefault(name, []).append(symbol) + + def enter_call(self, node: astroid.Call) -> None: + if isinstance(node.func, astroid.Name | astroid.Attribute): + if isinstance(node.func, astroid.Attribute): + call_name = node.func.attrname + else: + call_name = node.func.name + + call_reference = Reference(node, NodeID.calc_node_id(node), call_name) + # Add the call node to the calls of the parent scope if it is of type FunctionScope. + if isinstance(self.current_node_stack[-1], FunctionScope): + self.calls.append(call_reference) + else: # noqa: PLR5501 + # Add the call node to the calls of the last function definition to ensure it is considered + # in the call graph since it would otherwise be lost in the (local) Scope of the Comprehension. + if ( + isinstance( + self.current_node_stack[-1].symbol.node, + _ComprehensionType | astroid.Try, + ) + and self.current_function_def + ): + self.current_function_def[-1].call_references.setdefault(call_name, []).append(call_reference) + + # This deals with cases where a nested call calls the result of another call. + # Like: fun(1)(2)(3), where fun1 returns a function. + elif isinstance(node.func, astroid.Call): + call_reference = Reference(node, NodeID.calc_node_id(node), "UNKNOWN") + # Add the call node to the calls of the parent scope if it is of type FunctionScope. + if isinstance(self.current_node_stack[-1], FunctionScope): + self.calls.append(call_reference) + else: # noqa: PLR5501 + # Add the call node to the calls of the last function definition to ensure it is considered + # in the call graph since it would otherwise be lost in the (local) Scope of the Comprehension. + if ( + isinstance(self.current_node_stack[-1].symbol.node, _ComprehensionType) + and self.current_function_def + ): # pragma: no cover + self.current_function_def[-1].call_references.setdefault("UNKNOWN", []).append(call_reference) + + def enter_import(self, node: astroid.Import) -> None: + parent = self.current_node_stack[-1] + symbols: dict[str, Import] = {} + for name_tuple in node.names: + module = name_tuple[0] + alias = name_tuple[1] + if alias and isinstance(alias, str): + import_symbol = Import( + node=node, + id=NodeID(node.root().name, module, node.lineno, node.col_offset), + # Do not use NodeID.calc_node_id here because it would use the wrong name as node name. + name=module, + module=module, + alias=alias, + ) + symbols[import_symbol.alias] = import_symbol # type: ignore[index] # It is checked, that alias is str. + else: + import_symbol = Import( + node=node, + id=NodeID(node.root().name, module, node.lineno, node.col_offset), + # Do not use NodeID.calc_node_id here because it would use the wrong name as node name. + name=module, + module=module, + ) + symbols[import_symbol.name] = import_symbol + scope_node = Scope( + _symbol=import_symbol, + _children=[], + _parent=parent, + ) + self.children.append(scope_node) + + self.imports.update(symbols) + + def enter_importfrom(self, node: astroid.ImportFrom) -> None: + parent = self.current_node_stack[-1] + symbols: dict[str, Import] = {} + for name_tuple in node.names: + module = node.modname + name = name_tuple[0] + alias = name_tuple[1] + if alias and isinstance(alias, str): + import_symbol = Import( + node=node, + id=NodeID(node.root().name, name, node.lineno, node.col_offset), + # Do not use NodeID.calc_node_id here because it would use the wrong name as node name. + name=name, + module=module, + alias=alias, + ) + symbols[import_symbol.alias] = import_symbol # type: ignore[index] # It is checked, that alias is str. + else: + import_symbol = Import( + node=node, + id=NodeID(node.root().name, name, node.lineno, node.col_offset), + # Do not use NodeID.calc_node_id here because it would use the wrong name as node name. + name=name, + module=module, + ) + symbols[import_symbol.name] = import_symbol + scope_node = Scope( + _symbol=import_symbol, + _children=[], + _parent=parent, + ) + self.children.append(scope_node) + + self.imports.update(symbols) + + +def get_module_data(code: str, module_name: str = "", path: str | None = None) -> ModuleData: + """Get the module data of the given code. + + To get the module data of the given code, the code is parsed into an AST and then walked by an ASTWalker. + The ModuleDataBuilder detects the scope of each node and builds a scope tree. + The ModuleDataBuilder also collects all classes, functions, global variables, value nodes, target nodes, parameters, + function calls, and function references. + + Parameters + ---------- + code : + The source code of the module whose module data is to be found. + module_name : + The name of the module, by default "". + path : + The path of the module, by default None. + + Returns + ------- + ModuleData + The module data of the given module. + + Raises + ------ + ValueError + If the code has invalid syntax. + """ + module_data_handler = ModuleDataBuilder() + walker = ASTWalker(module_data_handler) + try: + module = astroid.parse(code, module_name, path) + except astroid.AstroidSyntaxError as e: # pragma: no cover + raise ValueError(f"Invalid syntax in code: {e}") from e + walker.walk(module) + + scope = module_data_handler.children[0] # Get the children of the root node, which are the scopes of the module + + return ModuleData( + scope=scope, + classes=module_data_handler.classes, + functions=module_data_handler.functions, + imports=module_data_handler.imports, + ) diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/_infer_purity.py b/src/safeds_stubgen/api_analyzer/purity_analysis/_infer_purity.py new file mode 100644 index 00000000..1ac2075b --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/_infer_purity.py @@ -0,0 +1,720 @@ +from __future__ import annotations + +from pathlib import Path + +import astroid + +from safeds_stubgen.api_analyzer._api import API +from safeds_stubgen.api_analyzer.purity_analysis import get_module_data +from safeds_stubgen.api_analyzer.purity_analysis._resolve_references import resolve_references +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + BUILTIN_FUNCTIONS, + BUILTIN_SPECIALS, + OPEN_MODES, + APIPurity, + Builtin, + BuiltinOpen, + CallGraphForest, + CallGraphNode, + CallOfParameter, + CombinedCallGraphNode, + FileRead, + FileWrite, + Import, + ImportedCallGraphNode, + Impure, + ImpurityReason, + NativeCall, + NodeID, + OpenMode, + PackageData, + Parameter, + ParameterAccess, + Pure, + PurityResult, + Reasons, + Reference, + StringLiteral, + UnknownCall, + UnknownClassInit, + UnknownFunctionCall, +) + +def _is_test_file(posix_path: str) -> bool: + return "/test/" in posix_path or "/tests/" in posix_path + +class PurityAnalyzer: + """ + The PurityAnalyzer class. + + This class is used to analyze the purity of a given module. + It uses the infer_purity function to determine the purity of the functions in a module. + + Attributes + ---------- + module_id : + The ID of the module to analyze. + visited_nodes : + A set of all nodes that have been visited during the analysis. + call_graph_forest : + The call graph forest of the module. + current_purity_results : + The purity results of the functions in the module. + separated_nodes : + If the module has cycles, they will be found by the CallGraphBuilder and combined to a single node. + Since these combined nodes are not part of the module but needed for the analysis, + their purity results will be propagated to the original nodes during the analysis. + This attribute stores the original nodes inside after the combined node was analyzed. + cached_module_results : + The results of all previously analyzed modules. + The key is the NodeID of the module, + the value is a dictionary of the purity results of the functions in the module. + After the analysis of the module, the results are saved in this dictionary. + All imported modules are saved in this dictionary too for further runtime reduction. + api_data : + API Data generated by api_analyzer for improved purity analysis + + Parameters + ---------- + api_data : + API Data generated by api_analyzer for improved purity analysis + code : + The source code of the module. + If None is provided, the package data must be provided (or else an exception is raised). + module_name : + The name of the module. + path : + The path of the module. + results : + The results of all previously analyzed modules. + The key is the NodeID of the module, + the value is a dictionary of the purity results of the functions in the module. + package_data : + The module data of all modules the package. + If provided, the references are resolved with the package data, else the module data is collected first. + It is used for the inference of the purity between modules in the package. + """ + + def __init__( + self, + api_data: API | None, + code: str | None, + module_name: str = "", + path: str | None = None, + results: dict[NodeID, dict[NodeID, PurityResult]] | None = None, + package_data: PackageData | None = None, + old_purity_analysis: bool = False, + ) -> None: + if code is None and not package_data: + raise ValueError("The code and package data are None.") # pragma: no cover + elif package_data: + references = resolve_references(code, api_data, module_name, path, package_data, old_purity_analysis=old_purity_analysis) # type: ignore[arg-type] # code is not None, so the type is correct. + else: + references = resolve_references(code, api_data, module_name, path, old_purity_analysis=old_purity_analysis) # type: ignore[arg-type] # code is not None, so the type is correct. + if references.call_graph_forest is None: + raise ValueError("The call graph forest is empty.") # pragma: no cover + + self.module_id = references.module_id + if self.module_id is None: + raise ValueError("The module ID is None.") # pragma: no cover + + self.visited_nodes: set[NodeID] = set() + self.call_graph_forest: CallGraphForest = references.call_graph_forest + self.current_purity_results: dict[NodeID, dict[NodeID, PurityResult]] = {self.module_id: {}} + self.separated_nodes: dict[NodeID, CallGraphNode] = {} + self.cached_module_results: dict[NodeID, dict[NodeID, PurityResult]] = results if results else {} + self.api_data = api_data + self.old_purity_analysis = old_purity_analysis + self._analyze_purity() + + @staticmethod + def _handle_open_like_functions(call: astroid.Call) -> PurityResult: + """Check open-like function for impurity. + + This includes functions like open, read, readline, readlines, write, writelines. + + Parameters + ---------- + call : + The call to check. + + Returns + ------- + PurityResult + The purity result of the function. + + """ + if not isinstance(call, astroid.Call): + raise TypeError(f"Expected astroid.Call, got {call.__class__.__name__}") from None + + func_ref_node_func_name = call.func.attrname if isinstance(call.func, astroid.Attribute) else call.func.name + + # Check if the function is open + if func_ref_node_func_name == "open": + open_mode_str = "r" + open_mode = None + + if not call.args: + return Impure({FileRead(StringLiteral("UNKNOWN")), FileWrite(StringLiteral("UNKNOWN"))}) # pragma: no cover + + # Check if a mode is set and if the value is a string literal + if len(call.args) >= 2 and isinstance(call.args[1], astroid.Const): + if call.args[1].value in OPEN_MODES: + open_mode_str = call.args[1].value + # Exclude the case where the mode is a variable since it cannot be determined in this case, + # therefore, set it to be the worst case (read and write). + elif len(call.args) == 2 and not isinstance(call.args[1], astroid.Const): + open_mode = OpenMode.READ_WRITE + + # Check if the file name is a variable or a string literal + file_var = call.args[0].name if isinstance(call.args[0], astroid.Name) else None + file_str = call.args[0].value if not file_var and hasattr(call.args[0], "value") else None # type: ignore # hasattr checks for value attribute + + # The file name is a variable + if file_var: + open_mode = open_mode or OPEN_MODES[open_mode_str] + # if open_mode is OpenMode.READ: + # return Impure(reasons={FileRead(source=ParameterAccess(file_var))}) + # elif open_mode is OpenMode.WRITE: + # return Impure(reasons={FileWrite(source=ParameterAccess(file_var))}) + # else: + # return Impure(reasons={FileRead(source=ParameterAccess(file_var)), FileWrite(source=ParameterAccess(file_var))}) + return Impure( + ( + {FileRead(source=ParameterAccess(file_var))} + if open_mode is OpenMode.READ + else ( + {FileWrite(source=ParameterAccess(file_var))} + if open_mode is OpenMode.WRITE + else { + FileRead(source=ParameterAccess(file_var)), + FileWrite(source=ParameterAccess(file_var)), + } + ) + ), + ) + + # The file name is a string literal + elif file_str: + open_mode = OPEN_MODES[open_mode_str] + return Impure( + ( + {FileRead(StringLiteral(file_str))} + if open_mode is OpenMode.READ + else ( + {FileWrite(StringLiteral(file_str))} + if open_mode is OpenMode.WRITE + else {FileRead(StringLiteral(file_str)), FileWrite(StringLiteral(file_str))} + ) + ), + ) + else: + return Impure({FileRead(StringLiteral("UNKNOWN")), FileWrite(StringLiteral("UNKNOWN"))}) # pragma: no cover + else: + return Pure() + + @staticmethod + def _get_impurity_result(reasons: Reasons) -> PurityResult: + """ + Get the reasons for impurity from the reasons. + + Given a Reasons object, this function transforms the collected reasons + from a Reasons object to a set of ImpurityReason and returns a PurityResult. + If any ImpurityReason is found in the reasons, the function returns an Impure result. + If no ImpurityReason is found, the function returns a Pure result. + If any unknown calls are found, the function returns an Unknown result. + + Parameters + ---------- + reasons : + The node to process containing the raw reasons for impurity collected. + + Returns + ------- + PurityResult + The impurity result of the function (Pure, Impure or Unknown). + """ + impurity_reasons: set[ImpurityReason] = set() + + # If no reasons are found, the function is pure. + if not reasons.reads_from and not reasons.writes_to and not reasons.unknown_calls: + return Pure() + + # Check if the function has any non-local variable writes. + if reasons.writes_to: + for write in reasons.writes_to.values(): + impurity_reasons.add(write) + + # Check if the function has any non-local variable reads. + if reasons.reads_from: + for read in reasons.reads_from.values(): + # Check if the read reads from an imported module. + if isinstance(read.symbol, Import): + if read.symbol.inferred_node: + # If the inferred node is a function, it must be analyzed to determine its purity. + if isinstance(read.symbol.inferred_node, astroid.FunctionDef): + impurity_reasons.add( + UnknownCall( + UnknownFunctionCall(call=read.symbol.call, inferred_def=read.symbol.inferred_node), + ), + ) # pragma: no cover + elif isinstance(read.symbol.inferred_node, astroid.ClassDef): + impurity_reasons.add( + UnknownCall( + UnknownClassInit(call=read.symbol.call, inferred_def=read.symbol.inferred_node), + ), + ) # pragma: no cover + # If the inferred node is a module, it will not count towards the impurity of the function. + # If this was added, nearly anything would be impure. + # Also, since the imported symbols are analyzed in much more detail, this can be omitted. + elif isinstance(read.symbol.inferred_node, astroid.Module): + pass + # Default case for symbols that could not be inferred. + else: # TODO: what type of nodes are allowed here? + impurity_reasons.add(read) + + else: + raise ValueError(f"Imported node {read.symbol.name} has no inferred node.") from None # pragma: no cover + + else: + impurity_reasons.add(read) + + # Check if the function has any unknown calls. + if reasons.unknown_calls: + for unknown_call in reasons.unknown_calls.values(): + # Handle calls of code where no definition was found. + if isinstance(unknown_call.symbol, Reference): + # This checks special cases of unknown calls. + # These are cases where a function is not a true builtin, but also not a user-defined function. + # Cases like dict.pop(), list.remove(), set.union(), etc. + if unknown_call.symbol.name in BUILTIN_SPECIALS: + pass + else: + impurity_reasons.add( + UnknownCall( + expression=UnknownFunctionCall(call=unknown_call.symbol.node), + origin=unknown_call.origin, + ), + ) + # Handle parameter calls + elif isinstance(unknown_call.symbol, Parameter): + impurity_reasons.add( + CallOfParameter( + expression=ParameterAccess(unknown_call.symbol), + origin=unknown_call.origin, + ), + ) + # Do not handle imported calls here since they are handled separately. + elif isinstance(unknown_call, Import): + pass # pragma: no cover + + if impurity_reasons: + return Impure(impurity_reasons) + return Pure() + + def _process_imported_node(self, imported_node: ImportedCallGraphNode) -> PurityResult: + """Process an imported node. + + Since imported nodes are not part of the module, they need to be analyzed separately. + Therefore, the inferred node of the imported node is analyzed to determine its purity. + If the module can be determined, a purity analysis is run on the imported module. + If the module cannot be determined, + or the function def is not found inside the module, the function is impure. + Since it is possible that a module is used more than once, + the results are cached after the first time analyzing the module. + + Parameters + ---------- + imported_node : + The imported node to process. + + Returns + ------- + PurityResult + The purity result of the imported node. + """ + # Check if the reference was resolved and the symbol has an inferred node. + if imported_node.symbol.inferred_node is None: + return Impure( + { + UnknownCall( + expression=UnknownFunctionCall(call=imported_node.symbol.call), + origin=imported_node.symbol, + ), + }, + ) # pragma: no cover + + imported_module = imported_node.symbol.inferred_node.root() + # Some imported modules are not written in python. Their purity cannot be analyzed. + if not imported_module.path: + return Impure( + { + NativeCall( + expression=UnknownFunctionCall( + call=imported_node.symbol.call, + inferred_def=( + imported_node.symbol.inferred_node + if isinstance(imported_node.symbol.inferred_node, astroid.FunctionDef) + else None + ), + ), + origin=imported_node.symbol, + ), + }, + ) + # Check if the imported module is actually a module. + if not isinstance(imported_module, astroid.Module): + return Impure( + { + UnknownCall( + expression=UnknownFunctionCall(call=imported_node.symbol.call), + origin=imported_node.symbol, + ), + }, + ) # pragma: no cover + + # Calculate the ID of the imported module. + imported_module_id = NodeID.calc_node_id(imported_module) + inferred_node_id = NodeID.calc_node_id(imported_node.symbol.inferred_node) + + # Check if the imported module has already been analyzed. + # Check if the purity result for the inferred node is available in the cache. + if ( + imported_module_id in self.cached_module_results + and inferred_node_id in self.cached_module_results[imported_module_id] + ): + return self.cached_module_results[imported_module_id].get(inferred_node_id) # type: ignore[return-value] + + # Check if the imported module is currently being analyzed to prevent recursion. + elif ( + imported_module_id in self.cached_module_results + and inferred_node_id not in self.cached_module_results[imported_module_id] + ): + # The module is being analyzed, return an impure result to break the recursion. + return Impure( + { + UnknownCall( + expression=UnknownFunctionCall(call=imported_node.symbol.call), + origin=imported_node.symbol, + ), + }, + ) # pragma: no cover + + # Mark the imported module as being analyzed. + elif imported_module_id not in self.cached_module_results: + self.cached_module_results.update({imported_module_id: {}}) + + # Get the source code of the imported module. + with imported_module.stream() as s: + source_code = s.read() + s.close() + try: + source_code = source_code.decode("utf-8") + except UnicodeDecodeError: + return Impure( + { + UnknownCall( + expression=UnknownFunctionCall(call=imported_node.symbol.call), + origin=imported_node.symbol, + ), + }, + ) + + # Analyze the purity of the imported module. + purity_result_imported_module = infer_purity( + code=source_code, + api_data=self.api_data, + module_name=imported_module.name, + path=imported_module.path[0], + results=self.cached_module_results, + old_purity_analysis=self.old_purity_analysis + ) + + # Update the cache with the purity results of the imported module. + self.cached_module_results.update(purity_result_imported_module) + + # Check if the purity result for the inferred node is available in the cache. + if inferred_node_id in self.cached_module_results[imported_module_id]: + return self.cached_module_results[imported_module_id].get(inferred_node_id) # type: ignore[return-value] + # If the inferred_node cannot be found in the result, return an unknown call. + else: # pragma: no cover + if isinstance(imported_node.symbol.inferred_node, astroid.ClassDef): + return Impure( + { + UnknownCall( + expression=UnknownClassInit( + call=imported_node.symbol.call, + inferred_def=imported_node.symbol.inferred_node, + ), + origin=imported_node.symbol, + ), + }, + ) + return Impure( + { + UnknownCall( + expression=UnknownFunctionCall( + call=imported_node.symbol.call, + inferred_def=( + imported_node.symbol.inferred_node if imported_node.symbol.inferred_node else None + ), + ), + origin=imported_node.symbol, + ), + }, + ) + + def _process_node(self, node: CallGraphNode) -> PurityResult: + """Process a node in the call graph. + + Process a node in the call graph to determine the purity of the function. + Therefore, recursively process the children of the node (if any) and propagate the results afterward. + First check if the purity of the function is already determined. + Works with builtin functions and combined function nodes. + + Parameters + ---------- + node : + The node to process. + + Returns + ------- + PurityResult + The purity result of the function node (combined with the results of its children). + """ + # Check the forest if the purity of the function is already determined + if node.is_inferred(): + # The purity of the function is determined already. + return node.reasons.result # type: ignore[return-value] # It is checked before that the result is not None. + elif ( + self.module_id in self.cached_module_results + and node.symbol.id in self.cached_module_results[self.module_id] + ): + return self.cached_module_results[self.module_id].get(node.symbol.id) # type: ignore[return-value] # pragma: no cover + elif node.symbol.id in self.visited_nodes and not isinstance(node.symbol, Builtin | BuiltinOpen): + return Impure( + {UnknownCall(expression=UnknownFunctionCall(), origin=node.symbol)}, + ) # TODO: find better return value # pragma: no cover + + self.visited_nodes.add(node.symbol.id) + + # Check if the node is a builtin function. + if isinstance(node.symbol, Builtin | BuiltinOpen): + if isinstance(node.symbol, BuiltinOpen): + result = self._handle_open_like_functions(node.symbol.call) + elif node.symbol.name in BUILTIN_FUNCTIONS: + result = BUILTIN_FUNCTIONS[node.symbol.name] + else: + result = Impure({UnknownCall(UnknownFunctionCall(call=node.symbol.call))}) # pragma: no cover + # Add the origin to the reasons if it is not set yet. + # Also add the caller of a builtin function to the origin (for better traceability). + if isinstance(result, Impure): + for reason in result.reasons: + if hasattr(reason, "origin") and reason.origin is None: + caller = None + parent = node.symbol.call.parent + while not caller: + if parent is None: + break # pragma: no cover + if isinstance(parent, astroid.FunctionDef): + caller = parent + else: + parent = parent.parent + + reason.origin = node.symbol + if caller: + reason.origin.id.name = reason.origin.id.name + " @ " + str(NodeID.calc_node_id(caller)) + return result + + # The purity of the node is not determined yet, but the node has children. + # Check their purity first and propagate the results afterward. + if not node.is_leaf(): + purity_result_children: PurityResult = Pure() + for child in node.children.values(): + # Check imported nodes separately. + if isinstance(child, ImportedCallGraphNode): + purity_result_child = self._process_imported_node(child) + # Check combined nodes separately. + elif isinstance(child, CombinedCallGraphNode): + purity_result_child = self._process_node(child) + self.separated_nodes.update(child.separate()) + else: + purity_result_child = self._process_node(child) + # Combine the reasons of all children. + purity_result_children = purity_result_children.update(purity_result_child) + + node.reasons.result = self._get_impurity_result(node.reasons) + node.reasons.result = node.reasons.result.update(purity_result_children) + + # The purity of the node is not determined yet, and it has no children. + # Therefore, it is possible to check its (reasons for) impurity directly. + # TODO: what about combined nodes here? Add testcase for that! + else: + node.reasons.result = self._get_impurity_result(node.reasons) + + return node.reasons.result + + def _analyze_purity(self) -> None: + """ + Analyze the purity of the module. + + While traversing the forest, it saves the purity results in the purity_results attribute. + """ + # Check if the module was already analyzed and the results are cached. + # if self.module_id in self.cached_module_results: + # self.current_purity_results[self.module_id] = self.cached_module_results[self.module_id] + # return + + # The purity of the module is not determined yet, so all graphs in the forest need to be analyzed. + for graph in self.call_graph_forest.graphs.values(): + if isinstance(graph, CombinedCallGraphNode): + self._process_node(graph) + self.separated_nodes.update(graph.separate()) + elif isinstance(graph, ImportedCallGraphNode): + pass + elif isinstance(graph, CallGraphNode): + purity_result = self._process_node(graph) + if isinstance(graph.symbol.node, astroid.ClassDef): + purity_result.is_class = True + + self.current_purity_results[self.module_id].update({graph.symbol.id: purity_result}) # type: ignore[index] # self.module_id is never None here, since an exception is raised before. + + if self.separated_nodes: + for func_id, graph in self.separated_nodes.items(): + if graph.reasons.result is None: + raise ValueError(f"The purity of the combined node {func_id} is not inferred.") # pragma: no cover + self.current_purity_results[self.module_id].update({func_id: graph.reasons.result}) # type: ignore[index] # self.module_id is never None here, since an exception is raised before. + + +def infer_purity( + code: str | None, + api_data: API | None = None, + module_name: str = "", + path: str | None = None, + results: dict[NodeID, dict[NodeID, PurityResult]] | None = None, + package_data: PackageData | None = None, + old_purity_analysis: bool = False, +) -> dict[NodeID, dict[NodeID, PurityResult]]: + """ + Infer the purity of functions. + + Given the code of a module, this function infers the purity of the functions inside a module. + It uses the PurityAnalyzer to determine the purity of the functions in a module. + + Parameters + ---------- + code : + The source code of the module. + If None is provided, the package data must be provided (or else an exception is raised). + module_name : + The name of the module, by default "". + path : + The path of the module, by default None. + results : + The results of all previously analyzed modules. + The key is the NodeID of the module, the value is a dictionary of the purity results of the functions in the module. + After the analysis of the module, the results are saved in this dictionary. + All imported modules are saved in this dictionary too for further runtime reduction. + Is None if no results are available. + package_data : + The module data of all modules the package. + If provided, the references are resolved with the package data, else the module data is collected first. + It is used for the inference of the purity between modules in the package. + + Returns + ------- + purity_results : + The purity results of the functions in the module. + The key is the NodeID of the module, the value is a dictionary of the purity results of the functions in the module. + """ + purity_analyzer = PurityAnalyzer(api_data, code, module_name, path, results, package_data, old_purity_analysis) + return purity_analyzer.current_purity_results + + +def get_purity_results( + src_dir_path: Path, + api_data: API, + test_run: bool = False, + old_purity_analysis: bool = False, +) -> APIPurity: + """Get the purity results of a package. + + This function is the entry to the purity analysis of a package. + It iterates over all modules in the package and infers the purity of the functions in the modules. + + Parameters + ---------- + src_dir_path : + The path of the source directory of the package. + + Returns + ------- + APIPurity + The purity results of the package. + """ + modules = list(src_dir_path.glob("**/*.py")) + module_names: list[str] = [] + package_purity = APIPurity() + package_data = PackageData(src_dir_path.stem) + + for module in modules: + posix_path = Path(module).as_posix() + + if _is_test_file(posix_path) and not test_run: + continue # pragma: no cover + + module_name = __module_name(src_dir_path, Path(module)) + module_names.append(module_name) + # Prepare the module data for all modules of the package. + with module.open("r", encoding="utf-8") as file: + code = file.read() + package_data.modules.update({module_name: (posix_path, get_module_data(code, module_name, posix_path))}) + + # Analyze the complete package. + package_data.combine_modules() + package_purity_results = infer_purity( + code=None, + api_data=api_data, + results=package_purity.purity_results, + package_data=package_data, + old_purity_analysis=old_purity_analysis, + ) + + # Group the results by file name. + sorted_module_purity_results: dict[NodeID, dict[NodeID, PurityResult]] = {} + for values in package_purity_results.values(): + for k, v in values.items(): + sorted_module_purity_results.setdefault( + NodeID(None, "UNKNOWN" if k.module is None else k.module), + {}, + ).update({k: v}) + + # Add back empty files. + for mod in module_names: + if NodeID(None, mod) not in sorted_module_purity_results: + sorted_module_purity_results[NodeID(None, mod)] = {} + + # Sort the functions by line number to make the results more readable. + sorted_module_purity_results = { + key: dict( + sorted( + value.items(), + key=lambda item: item[0].line if item[0] is not None and item[0].line is not None else float("inf"), + ), + ) + for key, value in sorted_module_purity_results.items() + } + + package_purity.purity_results.update(sorted_module_purity_results) + + # Clean the purity results by removing all modules that are not part of the package. + for module_id in package_purity.purity_results.copy(): + if module_id.name not in module_names: + package_purity.purity_results.pop(module_id) + + return package_purity + + +def __module_name(root: Path, file: Path) -> str: + relative_path = file.relative_to(root.parent).as_posix() + return str(relative_path).replace(".py", "").replace("/", ".") diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/_resolve_references.py b/src/safeds_stubgen/api_analyzer/purity_analysis/_resolve_references.py new file mode 100644 index 00000000..5c0ddab8 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/_resolve_references.py @@ -0,0 +1,1039 @@ +from __future__ import annotations + +import builtins +import contextlib +import dataclasses + +import astroid +from astroid.helpers import safe_infer + +from safeds_stubgen.api_analyzer._api import API, Function +from safeds_stubgen.api_analyzer.purity_analysis import build_call_graph, get_module_data +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + Builtin, + BuiltinOpen, + ClassScope, + ClassVariable, + FunctionScope, + GlobalVariable, + Import, + InstanceVariable, + MemberAccessTarget, + MemberAccessValue, + ModuleAnalysisResult, + NodeID, + NonLocalVariableRead, + NonLocalVariableWrite, + PackageData, + ParameterKind, + Reasons, + Reference, + ReferenceNode, + Symbol, + TargetReference, + UnknownProto, + ValueReference, +) + +_BUILTINS = dir(builtins) + + +class ReferenceResolver: + """Class to resolve all references in a module. + + Attributes + ---------- + functions : + The functions of the module. + classes : + The classes of the module. + imports : + The imports of the module. + module_analysis_result : + The result of the reference resolving. + package_data_is_provided : + True if package data is given, False otherwise. + api_data : + API Data generated by api_analyzer for improved purity analysis + + Parameters + ---------- + api_data : + API Data generated by api_analyzer for improved purity analysis + code : + The code of the module. + module_name : + The name of the module if any. + path : + The path of the module if any. + package_data : + The module data of all modules the package. + If provided, the references are resolved with the package data, else the module data is collected first. + It is used for the inference of the purity between modules in the package. + """ + + functions: dict[str, list[FunctionScope]] + classes: dict[str, ClassScope] + imports: dict[str, Import] + module_analysis_result: ModuleAnalysisResult = ModuleAnalysisResult() + package_data_is_provided: bool = False + api_data: API | None + old_purity_analysis: bool + + def __init__( + self, + api_data: API | None, + code: str, + module_name: str = "", + path: str | None = None, + package_data: PackageData | None = None, + old_purity_analysis: bool = False, + ): + # Check if the module is part of a package and if the package data is given. + if package_data and package_data.combined_module: + self.package_data_is_provided = True + module_data = package_data.combined_module + self.module_analysis_result.module_id = module_data.scope.symbol.id + else: + # Initialize the Class by getting the module data for the given (module) code. + try: + module_data = get_module_data(code, module_name, path) + self.module_analysis_result.module_id = module_data.scope.symbol.id + except ValueError: # pragma: no cover + return # TODO: add error message to result? + self.functions = module_data.functions + self.classes = module_data.classes + self.imports = module_data.imports + self.api_data = api_data + self.old_purity_analysis = old_purity_analysis + + # Resolve the references for the module. + self.module_analysis_result.classes = self.classes + resolved_references, raw_reasons = self._resolve_references() + self.module_analysis_result.resolved_references = resolved_references + self.module_analysis_result.raw_reasons = raw_reasons + self.module_analysis_result.call_graph_forest = build_call_graph( + self.classes, + self.module_analysis_result.raw_reasons, + ) + + @staticmethod + def is_function_of_class(function: astroid.FunctionDef, klass: ClassScope) -> bool: + """Check if a function is a method of a class. + + Parameters + ---------- + function : + The function to check. + klass : + The class to check. + + Returns + ------- + bool + True if the function is a method of the class, False otherwise. + """ + parent = function + while not isinstance(parent, astroid.Module | None): + if isinstance(parent, astroid.ClassDef) and parent == klass.symbol.node: + return True + elif isinstance(parent, astroid.ClassDef): + return False + parent = parent.parent + return False # pragma: no cover + + @staticmethod + def merge_dicts( + d1: dict[str, list[ReferenceNode]], + d2: dict[str, list[ReferenceNode]], + ) -> dict[str, list[ReferenceNode]]: + """Merge two dicts of lists of ReferenceNodes. + + Parameters + ---------- + d1 : + The first dict. + d2 : + The second dict. + + Returns + ------- + d3 : + The merged dict. + """ + d3 = d1.copy() + for key, value in d2.items(): + if key in d3: + d3[key].extend(value) + else: + d3[key] = value + return d3 + + @staticmethod + def compare_amount_of_parameters(function: FunctionScope, call: astroid.Call) -> bool: + """Compare the parameters of a function with the arguments of a call. + + To precisely determine the referenced symbols of a call, + the parameters of the function are compared with the arguments of the call. + A parameter is the variable listed inside the parentheses in the function definition. + An argument is the value sent to the function when it is called. + + Parameters + ---------- + function : + The function to compare. + call : + The call to compare. + + Returns + ------- + bool + True if the parameters of the function match the arguments of the call, False otherwise. + """ + if function.parameters: + argument_node: astroid.Arguments | None = ( + next(iter(function.parameters.values())).node.parent + if (isinstance(next(iter(function.parameters.values())).node.parent, astroid.Arguments)) + else None + ) + + # Get the parameters of the function and group them by kind. + star_param = [ + f + for f in function.parameters.values() + if f.kind in (ParameterKind.VAR_POSITIONAL, ParameterKind.VAR_KEYWORD) + ] + keyword_param = [ + f + for f in function.parameters.values() + if f.kind in (ParameterKind.KEYWORD_ONLY, ParameterKind.VAR_KEYWORD) + ] + positional_param = [ + f + for f in function.parameters.values() + if f.kind in (ParameterKind.POSITIONAL_ONLY, ParameterKind.POSITIONAL_OR_KEYWORD) + ] + + if isinstance(function.parent, ClassScope): + positional_param = [f for f in positional_param if f.name not in ("self", "cls")] + + # Get the arguments of the call and group them by kind. + positional_or_keyword_args = call.args + keyword_args = call.keywords + + # If no asterisk args are present, ... + if not star_param: + # ... the number of call arguments must be less or equal to the number of function parameters. + if len(positional_or_keyword_args + keyword_args) > len(positional_param) + len(keyword_param): + return False + # ... and no default values are present, + # the number of call arguments must be equal to the number of function parameters. + elif argument_node and not argument_node.defaults: + if len(positional_or_keyword_args + keyword_args) < len(positional_param) + len(keyword_param): + return False + + # If the call has keyword arguments and the function has no keyword parameters, return False. + if not keyword_param and keyword_args: + # Since it is possible to declare positional arguments as keyword arguments, + # it also needs to be checked if the keyword arguments are in the function parameters. + keyword_args_names = [keyword.arg for keyword in keyword_args] + keyword_args_names_matching_positional_parameter_names = [ + key for key in keyword_args_names if key in function.parameters + ] + if not keyword_args_names_matching_positional_parameter_names: + return False + + # Check if all used keyword arguments are in the functions keyword parameters. + if not all(key in function.parameters for key in keyword_args_names): + return False + + return True + else: + return True + + def _get_function_scopes_by_call_reference( + self, + func: FunctionScope, + call_reference: Reference, + ) -> list[FunctionScope]: + """ Get list of FunctionScopes by accessing the api which was generated by the api_analyzer + + The api of the api_analyzer stores all functions and each function contains info about their + body. The body contains info about each call reference and for each call reference, each + possibly referenced function is stored. This information is used to reduce the list of + FunctionScopes. + + Parameters + --------- + func : FunctionScope + Holds information about the Function in which the call reference is made + call_reference : Reference + a call reference of the function "func" + + Returns + --------- + list[FunctionScope] + """ + node_id = func.symbol.id + function_id = f"{node_id.module}.{node_id.name}.{node_id.line}.{node_id.col}" + function_defs = self.functions.get(call_reference.name, None) + if function_defs is None: # the call reference references functions out side of the package, can not happen, as before this function is called, this is checked + return [] # pragma: no cover + + if self.old_purity_analysis: # pragma: no cover + result = self._reduce_function_defs_by_parameter_comparison(function_defs, call_reference) + return result + + # api analyzer didn't provide any data at all, this shouldn't happen + if self.api_data is None: + result = self._reduce_function_defs_by_parameter_comparison(function_defs, call_reference) + return result + + function_api = self.api_data.functions.get(function_id) + # api_analyzer doesn't has function which contains the call ref, could be a bug of api analyzer + if function_api is None or self.api_data.functions[function_id].body is None: + result = self._reduce_function_defs_by_parameter_comparison(function_defs, call_reference) + return result + + # try to get call_reference from api + call_reference_id = f"{call_reference.name}.{call_reference.id.line}.{call_reference.id.col}" + call_reference_api = function_api.body.call_references.get(call_reference_id, None) + + # if call reference is none then this call reference could not be found + if call_reference_api is None: + result = self._reduce_function_defs_by_parameter_comparison(function_defs, call_reference) + return result + + possibly_referenced_functions = call_reference_api.possibly_referenced_functions + + # here we reference a function of builtins through super(), so we access a builtin class and the purity of those is stored in a lookup table + if call_reference_api.isSuperCallRef and call_reference_api.receiver.full_name.startswith("builtins.") and len(possibly_referenced_functions) == 0: + return [] + + # for builtins we dont need to find possibly referenced functions by name so we return [] + # builtins are handled later + if call_reference_api.receiver.full_name.startswith("builtins.") and len(call_reference_api.receiver.path_to_call_reference) == 2 and len(possibly_referenced_functions) == 0: + return [] # pragma: no cover + + # no found functions, due to missing types etc, could be a bug of type aware purity analysis or there is actually no type available + if len(possibly_referenced_functions) == 0: + if call_reference_api.fallbackToSignatureCheck: + call_reference_api.receiver.missingTypesWhileFindingFunction = True + result = self._reduce_function_defs_by_parameter_comparison(function_defs, call_reference) + return result + call_reference_api.receiver.typeOutsideOfPackage = True + return [] + + list_of_function_ids: list[str] = list(map(lambda api_func: self._get_id_from_api_function(api_func), possibly_referenced_functions)) + reduced_function_defs = [function_d for function_d in function_defs if self._get_id_from_nodeId(function_d.symbol.id) in list_of_function_ids] + + return reduced_function_defs + + def _reduce_function_defs_by_parameter_comparison(self, function_defs: list[FunctionScope], call_reference: Reference) -> list[FunctionScope]: + """ Helper function for _get_function_scopes_by_call_reference + + Once we dont have necessary data from the api, we have to fall back to reducing the list of functionScopes by + parameter comparison. + + Parameters + --------- + function_defs : list[FunctionScope] + call_reference : Reference + + Returns + --------- + list[FunctionScope] + """ + function_defs = [function_d for function_d in function_defs if self.compare_amount_of_parameters(function_d, call_reference.node)] # type: ignore[union-attr] + return function_defs + + def _get_id_from_nodeId(self, nodeId: NodeID) -> str: + """ Helper function for _get_function_scopes_by_call_reference + + Mypy and Astorid generate different ids, so we need to convert them + + Parameters + --------- + nodeId : NodeID + + Returns + --------- + str + """ + return f"{nodeId.module}.{nodeId.name}.{nodeId.line}.{nodeId.col}" + + def _get_id_from_api_function(self, function: Function) -> str: + """ Helper function for _get_function_scopes_by_call_reference + + Mypy and Astorid generate different ids, so we need to convert them + Parameters + --------- + function : Function + + Returns + --------- + str + """ + return f"{function.module_id_which_contains_def}.{function.name}.{function.line}.{function.column}" + + def _find_referenced_functions( + self, + call_reference: Reference, + function: FunctionScope, + ) -> ValueReference: + """Find all references for a function call. + + This function finds all referenced Symbols for a call reference. + A reference for a call node can be either a FunctionDef or a ClassDef node. + Also analyze builtins calls and calls of function parameters. + + Parameters + ---------- + call_reference : + The call reference which should be analyzed. + function : + The function in which the call is made. + + Returns + ------- + ValueReference + A ValueReference for the given call reference. + This contains all referenced symbols for the call reference. + """ + if not isinstance(call_reference, Reference): + raise TypeError(f"call is not of type Reference, but of type {type(call_reference)}") # pragma: no cover + + result_value_reference = ValueReference(call_reference, function, []) + + # Find functions that are called. but only those which are defined in the package + if call_reference.name in self.functions: + function_def = self._get_function_scopes_by_call_reference(function, call_reference) + function_symbols = [func.symbol for func in function_def if function_def] # type: ignore[union-attr] + + # "None" is not iterable, but it is checked before + class_iterator = function.symbol.node + klass = None + while class_iterator: + if isinstance(class_iterator, astroid.ClassDef) and class_iterator.name is not None: + klass = self.classes.get(class_iterator.name) + break + if isinstance(class_iterator, astroid.Module): + break + class_iterator = class_iterator.parent + + if klass and klass.super_classes: + res = [] + for sup in klass.super_classes: + for func in sup.class_variables.values(): + for f in func: + if f.name == call_reference.name: + res.append(f) + # seems to find builtins, for __init__ + result_value_reference.referenced_symbols.extend(res) + else: + result_value_reference.referenced_symbols.extend(function_symbols) + + # Find classes that are called (initialized). + elif call_reference.name in self.classes: + class_def = self.classes.get(call_reference.name) + if class_def: + result_value_reference.referenced_symbols.append(class_def.symbol) + + # Find builtins that are called, this includes open-like functions. + # Because the parameters of the call node are relevant for the analysis, they are added to the (Builtin) Symbol. + if call_reference.name in _BUILTINS or call_reference.name in ( + "open", + "read", + "readline", + "readlines", + "write", + "writelines", + "close", + ): + # Construct an artificial FunctionDef node for the builtin function. + assert isinstance(call_reference.node, astroid.Call) + builtin_function = astroid.FunctionDef( + name=( + ( + call_reference.node.func.attrname + if isinstance(call_reference.node.func, astroid.Attribute) + else call_reference.node.func.name + ) + if isinstance(call_reference.node.func, astroid.Attribute | astroid.Name) + else None + ), + lineno=call_reference.node.lineno, + end_lineno=call_reference.node.end_lineno, + col_offset=call_reference.node.col_offset, + end_col_offset=call_reference.node.end_col_offset, + parent=None + ) + builtin_call = Builtin( + node=builtin_function, + id=NodeID("BUILTIN", call_reference.name), + name=call_reference.name, + call=call_reference.node, + ) + if call_reference.name in ("open", "read", "readline", "readlines", "write", "writelines", "close"): + builtin_call = BuiltinOpen( + node=builtin_function, + id=NodeID("BUILTIN", call_reference.name), + name=call_reference.name, + call=call_reference.node, + ) + result_value_reference.referenced_symbols.append(builtin_call) + + # Find function parameters that are called (passed as arguments), like: + # def f(a): + # a() + # It is not possible to analyze this any further before runtime, so they will later be marked as unknown. + if call_reference.name in function.parameters: # callbacks + param = function.parameters[call_reference.name] + result_value_reference.referenced_symbols.append(param) + + # Find imported functions or classes that are called for ImportFrom nodes. + if call_reference.name in self.imports: + import_def = self.imports.get(call_reference.name) + inferred_node_def = safe_infer(call_reference.node.func) # type: ignore + if not inferred_node_def: + with contextlib.suppress(astroid.InferenceError): + inferred_node_def = next(call_reference.node.func.infer()) + if not isinstance(inferred_node_def, astroid.FunctionDef | astroid.ClassDef): + # These cases will be added to the unknown calls since they do not have any referenced_symbols. + pass + else: + specified_import_def = dataclasses.replace( + import_def, # type: ignore[type-var] # import def is not None. + inferred_node=inferred_node_def, + call=call_reference.node, + ) + if specified_import_def: + result_value_reference.referenced_symbols.append(specified_import_def) + + return result_value_reference + + def _find_value_references( + self, + value_reference: Reference, + function: FunctionScope, + ) -> ValueReference: + """Find all references for a value node. + + This functions finds all referenced Symbols for a value reference. + A reference for a value node can be a GlobalVariable, a LocalVariable, + a Parameter, a ClassVariable or an InstanceVariable. + It Also deals with the case where a class or a function is used as a value. + + Parameters + ---------- + value_reference : + The value reference which should be analyzed. + function : + The function in which the value is used. + + Returns + ------- + ValueReference + A ValueReference for the given value reference. + This contains all referenced symbols for the value reference. + """ + if not isinstance(value_reference, Reference): + raise TypeError(f"call is not of type Reference, but of type {type(value_reference)}") # pragma: no cover + + result_value_reference = ValueReference(value_reference, function, []) + + # Find local variables that are referenced. + if value_reference.name in function.target_symbols and value_reference.name not in function.parameters: + symbols = function.target_symbols[value_reference.name] + # Check if all symbols are refined (refined means that they are of any subtyp of Symbol) + if any(isinstance(symbol, Symbol) for symbol in symbols): + # This currently is mostly the case for ClassVariables and InstanceVariables that are used as targets + missing_refined = [symbol for symbol in symbols if type(symbol) is Symbol] + + # Because the missing refined symbols are added separately below, + # remove the unrefined symbols from the list to avoid duplicates. + symbols = list(set(symbols) - set(missing_refined)) + + for symbol in missing_refined: + if isinstance(symbol.node, MemberAccessTarget): + for klass in self.classes.values(): + if klass.class_variables: + if value_reference.node.member in klass.class_variables: + symbols.append( + ClassVariable(symbol.node, symbol.id, symbol.node.member, klass.symbol.node), + ) + if klass.instance_variables: + if value_reference.node.member in klass.instance_variables: + symbols.append( + InstanceVariable(symbol.node, symbol.id, symbol.node.member, klass.symbol.node), + ) + + # Only add symbols that are defined before the value is used. + for symbol in symbols: + if ( + symbol.id.line is None + or value_reference.id.line is None + or symbol.id.line <= value_reference.id.line + ): + result_value_reference.referenced_symbols.append(symbol) + + # Find parameters that are referenced. + if value_reference.name in function.parameters: + local_symbols = [function.parameters[value_reference.name]] + result_value_reference.referenced_symbols.extend(local_symbols) + + # Find global variables that are referenced. + if value_reference.name in function.globals_used: + global_symbols = function.globals_used[value_reference.name] # type: ignore[assignment] + # globals_used contains GlobalVariable instances, which are a subtype of Symbol. + result_value_reference.referenced_symbols.extend(global_symbols) + + # Find functions that are referenced (as value). + if value_reference.name in self.functions: + function_def = self.functions.get(value_reference.name) + if function_def: + function_symbols = [func.symbol for func in function_def if function_def] + result_value_reference.referenced_symbols.extend(function_symbols) + + # Find classes that are referenced (as value). + if value_reference.name in self.classes: + class_def = self.classes.get(value_reference.name) + if class_def: + result_value_reference.referenced_symbols.append(class_def.symbol) + + # Find imported modules that are referenced (as value) for Import. + # Find symbols that are referenced for ImportFrom. + if not isinstance(value_reference.node, MemberAccessValue) and value_reference.name in self.imports: + import_def = self.imports.get(value_reference.name) + inferred_node_def = safe_infer(value_reference.node) + if not inferred_node_def: + with contextlib.suppress(astroid.InferenceError): + inferred_node_def = next(value_reference.node.infer()) + if not inferred_node_def: + pass # pragma: no cover + else: + specified_import_def = dataclasses.replace( + import_def, # type: ignore # import def is not None + inferred_node=inferred_node_def, # type: ignore[type-var] # import def is not None. + ) + specified_import_def.id.name = specified_import_def.id.name + "." + specified_import_def.name # type: ignore[union-attr] # specified_import_def is not None. + + if specified_import_def: + result_value_reference.referenced_symbols.append(specified_import_def) + + # Find class and instance variables that are referenced. + if isinstance(value_reference.node, MemberAccessValue): + for klass in self.classes.values(): + if klass.class_variables: + if ( + value_reference.node.member in klass.class_variables + and value_reference.node.member not in function.call_references + ): + result_value_reference.referenced_symbols.extend( + klass.class_variables[value_reference.node.member], + ) + if klass.instance_variables: + if ( + value_reference.node.member in klass.instance_variables + and value_reference.node.member not in function.call_references + ): + result_value_reference.referenced_symbols.extend( + klass.instance_variables[value_reference.node.member], + ) + + # Find imported symbols that are referenced (as member of a MemberAccessValue). + + # Also deal with the case that the member is a call here, which at first is not intuitive + # (not imported function calls, where the member is a call, are treated as calls). + # On the other hand, dealing with imported calls as members when the references for function calls + # are resolved is much more effort and would require to change the data structure. + # Therefore, all calls of imported functions are handled as MemberAccessValue. + # Because of this, a check at the point where the referenced_symbols are added to the raw_reasons is needed. + try: + if value_reference.node.receiver is None: + receiver_name = "UNKNOWN" + elif isinstance(value_reference.node.receiver, astroid.Attribute): + receiver_name = value_reference.node.receiver.attrname + elif isinstance(value_reference.node.receiver, astroid.Call) and hasattr( + value_reference.node.receiver.func, + "name", + ): + receiver_name = value_reference.node.receiver.func.name # type: ignore # hasattr() ensures that func has .name # pragma: no cover + else: + receiver_name = value_reference.node.receiver.name # type: ignore # try catch ensures that there wont be an error + except AttributeError: # pragma: no cover + receiver_name = "UNKNOWN" + + # In references imported via "import" statements, the symbols of the imported module are not known yet. + # The symbol is accessed via its name, which is of type MemberAccessValue. + # At this point, only the receiver(=module name) is saved in the imports' dict. + # This means that the symbol for the member needs to be inferred from the module and added to the list + # of referenced symbols. + if receiver_name in self.imports: + # But first, it can be checked if the receiver is a class of the package (if one is provided). + # In this case, there is no need to use astroid to determine the function or class. + was_found = False + if self.package_data_is_provided and receiver_name in self.classes: + class_def = self.classes.get(receiver_name) # type: ignore # code above ensures that receiver_name is of type str + if class_def: + if value_reference.node.member in class_def.class_variables: + was_found = True + for symbol in class_def.class_variables[value_reference.node.member]: + result_value_reference.referenced_symbols.append(symbol) + + # Since the symbol is not part of the package, it needs to be inferred from the imported module. + import_def = self.imports.get(receiver_name) # type: ignore # code above ensures that receiver_name is of type str + if import_def and value_reference.node.node is not None and not was_found: + # Use astroid to infer the symbol of the member from the module. + inferred_node_def = safe_infer( + value_reference.node.node, + ) # TODO: what if node is a MemberAccessValue? + if not inferred_node_def: + with contextlib.suppress(astroid.InferenceError): + inferred_node_def = next(value_reference.node.node.infer()) + if not inferred_node_def: + pass # pragma: no cover + + else: + # Overcome the problem, that the import symbol object is the same for all possible functions and + # classes that are imported from one module. + # Therefore, copy the original import node and define a new one for one specific function or class. + # This means that every function or class imported from a module has its own import node. + specified_import_def = dataclasses.replace( + import_def, + name=value_reference.node.member, + inferred_node=inferred_node_def, + ) + specified_import_def.id.name = specified_import_def.id.name + "." + specified_import_def.name + + # If the member is a call, add the call node to the specified_import_def as fallback for the case + # that the purity of the called function cannot be inferred. + if isinstance(value_reference.node.node.parent, astroid.Call): + specified_import_def.call = value_reference.node.node.parent + + result_value_reference.referenced_symbols.append(specified_import_def) + + return result_value_reference + + def _find_target_references( + self, + target_reference: Symbol, + function: FunctionScope, + ) -> TargetReference: + """Find all references for a target node. + + This functions finds all referenced Symbols for a target reference. + TargetReferences occur whenever a Symbol is reassigned. + A reference for a target node can be a GlobalVariable, a LocalVariable, a ClassVariable or an InstanceVariable. + It Also deals with the case where a class is used as a target. + + Parameters + ---------- + target_reference : + The target reference which should be analyzed. + function : + The function in which the value is used. + + Returns + ------- + TargetReference + A TargetReference for the given target reference. + This contains all referenced symbols for the value reference. + """ + if not isinstance(target_reference, Symbol): + raise TypeError(f"call is not of type Reference, but of type {type(target_reference)}") # pragma: no cover + + result_target_reference = TargetReference(target_reference, function, []) + + # Find local variables that are referenced. + if target_reference.name in function.target_symbols: + # Only check for symbols that are defined before the current target_reference. + local_symbols = function.target_symbols[target_reference.name][ + : function.target_symbols[target_reference.name].index(target_reference) + ] + result_target_reference.referenced_symbols.extend(local_symbols) + + # Find global variables that are referenced. + if target_reference.name in function.globals_used: + global_symbols = function.globals_used[target_reference.name] + result_target_reference.referenced_symbols.extend(global_symbols) + + # Find classes that are referenced (as value). + if target_reference.name in self.classes: + class_def = self.classes.get(target_reference.name) + if class_def: + result_target_reference.referenced_symbols.append(class_def.symbol) + + # Find class and instance variables that are referenced. + if isinstance(target_reference.node, MemberAccessTarget): + for klass in self.classes.values(): + if klass.class_variables: + if target_reference.node.member in klass.class_variables: + # Do not add class variables from other classes + if target_reference.node.receiver is not None: + if ( + function.symbol.name == "__init__" + and function.parent != klass + or isinstance(target_reference.node.receiver, astroid.Name) + and target_reference.node.receiver.name == "self" + and function.parent != klass + or isinstance(target_reference.node.receiver, astroid.Attribute) + and target_reference.node.receiver.attrname == "self" + and function.parent != klass + ): + continue + # Do not add functions that are not of the current class (or superclass). + if function.symbol.name not in klass.class_variables or not self.is_function_of_class( + function.symbol.node, + klass, + ): + # Collect all functions of superclasses for the current klass instance. + super_functions = [] + for sup in klass.super_classes: + for class_var_list in sup.class_variables.values(): + for var in class_var_list: + if isinstance(var.node, astroid.FunctionDef): + super_functions.append(var.node.name) + + # Make an exception for global functions and functions of superclasses. + # Also check if the function was overwritten in the current class. + if ( + isinstance(function.symbol, GlobalVariable) + or function.symbol.name in super_functions + and function.symbol.name not in klass.class_variables + ): + pass + else: + continue + + result_target_reference.referenced_symbols.extend( + klass.class_variables[target_reference.node.member], + ) + if klass.instance_variables: + if ( + target_reference.node.member in klass.instance_variables + and target_reference.node != klass.instance_variables[target_reference.node.member][0].node + ): # This excludes the case where the instance variable is assigned + result_target_reference.referenced_symbols.extend( + klass.instance_variables[target_reference.node.member], + ) + + # Find imported symbols that are referenced (as member of a MemberAccessTarget). + # Astroids safe_infer methode will get the value of the assignment in the MemberAccessTarget node. + # However, it is possible to detect write to an imported symbol which should be enough to ensure impurity. + receiver_name: str | None = None + if isinstance(target_reference.node.receiver, astroid.Attribute): + receiver_name = target_reference.node.receiver.attrname # pragma: no cover + elif isinstance(target_reference.node.receiver, astroid.Name): + receiver_name = target_reference.node.receiver.name + + if receiver_name is not None and receiver_name in self.imports: + import_def = self.imports.get(receiver_name) + if import_def: + specified_import_def = dataclasses.replace(import_def, name=target_reference.node.member) + result_target_reference.referenced_symbols.append(specified_import_def) + + return result_target_reference + + def _resolve_references(self) -> tuple[dict[str, list[ReferenceNode]], dict[NodeID, Reasons]]: + """ + Resolve all references in a module. + + This function is the entry point for the reference resolving. + It calls all other functions that are needed to resolve the references. + First, get the module data for the given (module) code. + Then call the functions to find all call, target and value references in the module. + + Returns + ------- + tuple[dict[NodeID, list[ReferenceNode]], dict[NodeID, Reasons]] + The resolved references and the raw reasons for the functions. + """ + raw_reasons: dict[NodeID, Reasons] = {} + call_references: dict[str, list[ReferenceNode]] = {} + value_references: dict[str, list[ReferenceNode]] = {} + target_references: dict[str, list[ReferenceNode]] = {} + # The call_references value is a list because the analysis analyzes the functions by name, + # therefor a call can reference more than one function. + # In the future, it is possible to differentiate between calls with the same name. + # This could be done by further specifying the call_references for a function (by analyzing the signature, etc.) + # If it is analyzed with 100% certainty, it is possible to remove the list and use a single ValueReference. + amount_of_call_refs = 0 + for function_list in self.functions.values(): # functions are stored by dict, with the name as key + # iterate over all functions with the same name + for function in function_list: # here we iterate over the functions with same name + # Collect the reasons while iterating over the functions, so there is no need to iterate over them again. + raw_reasons[function.symbol.id] = Reasons(function.symbol.id, function) + + # Check if the function has call_references (References from a call to the function definition itself). + if function.call_references: + for call_list in function.call_references.values(): + for call_reference in call_list: + amount_of_call_refs += 1 + + call_references_result: ReferenceNode + call_references_result = self._find_referenced_functions( + call_reference, + function, # this is the function in which a reference is made + ) + + # If referenced symbols are found, + # add them to the list of symbols in the dict by the name of the node. + # If the name does not yet exist, create a new list with the reference. + if call_references_result.referenced_symbols: + call_references.setdefault(call_references_result.node.name, []).append( + call_references_result, + ) + + # Add the referenced symbols to the calls of the raw_reasons dict for this function + for referenced_symbol in call_references_result.referenced_symbols: + # if isinstance( + # referenced_symbol, + # GlobalVariable | ClassVariable | Builtin | BuiltinOpen | Import + # ): + if referenced_symbol not in raw_reasons[function.symbol.id].calls: + raw_reasons[function.symbol.id].calls.add(referenced_symbol) # pm: for each function inside of the functionScope we add the referenced call to reasons + # If no referenced symbols are found, add the call to the list of unknown_calls + # of the raw_reasons dict for this function + elif call_references_result.node not in raw_reasons[function.symbol.id].unknown_calls: + raw_reasons[function.symbol.id].unknown_calls[call_references_result.node.id] = ( + UnknownProto(symbol=call_references_result.node, origin=function.symbol) + ) + + # Check if the function has value_references (References from a value node to a target node). + if function.value_references: + for value_list in function.value_references.values(): + for value_reference in value_list: + value_reference_result: ReferenceNode + value_reference_result = self._find_value_references( + value_reference, + function, + ) + + # If referenced symbols are found, + # add them to the list of symbols in the dict by the name of the node. + # If the name does not yet exist, create a new list with the reference. + if value_reference_result.referenced_symbols: + value_references.setdefault(value_reference_result.node.name, []).append( + value_reference_result, + ) + + # Add the referenced symbols to the reads_from of the raw_reasons dict for this function + for referenced_symbol in value_reference_result.referenced_symbols: + if isinstance(referenced_symbol, GlobalVariable | ClassVariable | InstanceVariable): + # Since classes and functions are defined as immutable + # reading from them is not a reason for impurity. + # There is an exception to this rule for functions + # that are decorated with a '@property' decorator. These functions define an + # instance variable as a property, which can be read from. + if isinstance(referenced_symbol.node, astroid.ClassDef | astroid.FunctionDef): + if ( + isinstance(referenced_symbol.node, astroid.FunctionDef) + and "builtins.property" in referenced_symbol.node.decoratornames() + and isinstance(referenced_symbol, InstanceVariable) + ): + pass + else: + continue + # Add the referenced symbol to the list of symbols whom are read from. + if referenced_symbol.id not in raw_reasons[function.symbol.id].reads_from: + raw_reasons[function.symbol.id].reads_from[referenced_symbol.id] = ( + NonLocalVariableRead(symbol=referenced_symbol, origin=function.symbol) + ) + elif isinstance(referenced_symbol, Import): + # Since calls of imported functions are treated within _find_value_references + # as MemberAccessValue, they need to be added to the calls of the raw_reasons dict + # instead of the reads_from. + if isinstance( + referenced_symbol.inferred_node, + astroid.FunctionDef | astroid.ClassDef, + ): + if referenced_symbol not in raw_reasons[function.symbol.id].calls: + raw_reasons[function.symbol.id].calls.add(referenced_symbol) + else: # noqa: PLR5501 + if referenced_symbol.id not in raw_reasons[function.symbol.id].reads_from: + raw_reasons[function.symbol.id].reads_from[referenced_symbol.id] = ( + NonLocalVariableRead( + symbol=referenced_symbol, + origin=function.symbol, + ) + ) + # If no referenced symbols are found, add the call to the list of unknown_calls + # of the raw_reasons dict for this function + elif value_reference_result.node not in raw_reasons[ + function.symbol.id + ].unknown_calls and isinstance(value_reference_result.node.node, astroid.Call): + raw_reasons[function.symbol.id].unknown_calls[value_reference_result.node.id] = ( + UnknownProto(symbol=value_reference_result.node, origin=function.symbol) + ) # pragma: no cover + + # Check if the function has target_references (References from a target node to another target node). + if function.target_symbols: + for target_list in function.target_symbols.values(): + for target_reference in target_list: + target_reference_result: ReferenceNode + target_reference_result = self._find_target_references( + target_reference, + function, + ) + + # If referenced symbols are found, + # add them to the list of symbols in the dict by the name of the node. + # If the name does not yet exist, create a new list with the reference. + if target_reference_result.referenced_symbols: + target_references.setdefault(target_reference_result.node.name, []).append( + target_reference_result, + ) + + # Add the referenced symbols to the writes_to of the raw_reasons dict for this function + for referenced_symbol in target_reference_result.referenced_symbols: + if isinstance( + referenced_symbol, + GlobalVariable | ClassVariable | InstanceVariable | Import, + ): + # Since classes and functions are defined as immutable, + # writing to them is not a reason for impurity. + # Also, it is not common to do so anyway. + if isinstance(referenced_symbol.node, astroid.ClassDef | astroid.FunctionDef): + continue + # Add the referenced symbol to the list of symbols whom are written to. + if referenced_symbol not in raw_reasons[function.symbol.id].writes_to: + raw_reasons[function.symbol.id].writes_to[referenced_symbol.id] = ( + NonLocalVariableWrite(symbol=referenced_symbol, origin=function.symbol) + ) + + name_references: dict[str, list[ReferenceNode]] = self.merge_dicts(value_references, target_references) + resolved_references: dict[str, list[ReferenceNode]] = self.merge_dicts(call_references, name_references) + + return resolved_references, raw_reasons + + +def resolve_references( + code: str, + api_data: API | None = None, + module_name: str = "", + path: str | None = None, + package_data: PackageData | None = None, + old_purity_analysis: bool = False, +) -> ModuleAnalysisResult: + """Resolve all references in a module. + + Parameters + ---------- + code : + The code of the module. + module_name : + The name of the module if any. + path : + The path of the module if any. + package_data : + The module data of all modules the package. + If provided, the references are resolved with the package data, else the module data is collected first. + It is used for the inference of the purity between modules in the package. + + Returns + ------- + ModuleAnalysisResult + The result of the reference resolving. + """ + return ReferenceResolver(api_data, code, module_name, path, package_data, old_purity_analysis).module_analysis_result diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/__init__.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/__init__.py new file mode 100644 index 00000000..76fd8ccd --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/__init__.py @@ -0,0 +1,123 @@ +"""Data model for purity analysis.""" + +from safeds_stubgen.api_analyzer.purity_analysis.model._call_graph import ( + CallGraphForest, + CallGraphNode, + CombinedCallGraphNode, + ImportedCallGraphNode, +) +from safeds_stubgen.api_analyzer.purity_analysis.model._module_data import ( + Builtin, + BuiltinOpen, + ClassScope, + ClassVariable, + CombinedSymbol, + FunctionScope, + GlobalVariable, + Import, + InstanceVariable, + LocalVariable, + MemberAccess, + MemberAccessTarget, + MemberAccessValue, + ModuleData, + NodeID, + PackageData, + Parameter, + ParameterKind, + Reference, + Scope, + Symbol, + UnknownSymbol, +) +from safeds_stubgen.api_analyzer.purity_analysis.model._purity import ( + APIPurity, + CallOfParameter, + Expression, + FileRead, + FileWrite, + Impure, + ImpurityReason, + NativeCall, + NonLocalVariableRead, + NonLocalVariableWrite, + OpenMode, + ParameterAccess, + Pure, + PurityResult, + StringLiteral, + UnknownCall, + UnknownClassInit, + UnknownFunctionCall, + UnknownProto, +) +from safeds_stubgen.api_analyzer.purity_analysis.model._purity_builtins import ( + BUILTIN_CLASSSCOPES, + BUILTIN_FUNCTIONS, + BUILTIN_SPECIALS, + OPEN_MODES, +) +from safeds_stubgen.api_analyzer.purity_analysis.model._reference import ( + ModuleAnalysisResult, + Reasons, + ReferenceNode, + TargetReference, + ValueReference, +) + +__all__ = [ + "ModuleAnalysisResult", + "ModuleData", + "Scope", + "ClassScope", + "FunctionScope", + "MemberAccess", + "MemberAccessTarget", + "MemberAccessValue", + "NodeID", + "Symbol", + "Parameter", + "LocalVariable", + "GlobalVariable", + "ClassVariable", + "InstanceVariable", + "Import", + "Builtin", + "ReferenceNode", + "Expression", + "ParameterAccess", + "StringLiteral", + "ImpurityReason", + "NonLocalVariableRead", + "NonLocalVariableWrite", + "FileRead", + "FileWrite", + "PurityResult", + "Pure", + "Impure", + "Reasons", + "CallGraphForest", + "OpenMode", + "NativeCall", + "UnknownCall", + "CallOfParameter", + "Reference", + "TargetReference", + "ValueReference", + "APIPurity", + "BuiltinOpen", + "CallGraphNode", + "CombinedCallGraphNode", + "CombinedSymbol", + "UnknownFunctionCall", + "UnknownClassInit", + "ImportedCallGraphNode", + "BUILTIN_CLASSSCOPES", + "BUILTIN_FUNCTIONS", + "OPEN_MODES", + "UnknownSymbol", + "BUILTIN_SPECIALS", + "PackageData", + "ParameterKind", + "UnknownProto", +] diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/_call_graph.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_call_graph.py new file mode 100644 index 00000000..5e3069b7 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_call_graph.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from safeds_stubgen.api_analyzer.purity_analysis.model._module_data import ( + Import, + NodeID, + Symbol, + ) + from safeds_stubgen.api_analyzer.purity_analysis.model._reference import Reasons + + +@dataclass +class CallGraphForest: + """Class for call graph forests. + + A call graph forest represents a collection of call graph trees. + + Attributes + ---------- + graphs : + The dictionary of call graph trees. + The key is the name of the tree, the value is the root CallGraphNode of the tree. + """ + + graphs: dict[NodeID, CallGraphNode] = field(default_factory=dict) + + def add_graph(self, graph_id: NodeID, graph: CallGraphNode) -> None: + """Add a call graph tree to the forest. + + Parameters + ---------- + graph_id : + The NodeID of the tree node. + graph : + The root of the tree. + """ + # if graph_id in self.forest: + # raise ValueError(f"Graph with id {graph_id} already exists inside the call graph.") + self.graphs[graph_id] = graph + + def get_graph(self, graph_id: NodeID) -> CallGraphNode: + """Get a call graph tree from the forest. + + Parameters + ---------- + graph_id : + The NodeID of the tree node to get. + + Raises + ------ + KeyError + If the graph_id is not in the forest. + """ + result = self.graphs.get(graph_id) + if result is None: + raise KeyError(f"Graph with id {graph_id} not found inside the call graph.") # pragma: no cover + return result + + def has_graph(self, graph_id: NodeID) -> bool: + """Check if the forest contains a call graph tree with the given NodeID. + + Parameters + ---------- + graph_id : + The NodeID of the tree to check for. + + Returns + ------- + bool + True if the forest contains a tree with the given NodeID, False otherwise. + """ + return graph_id in self.graphs + + def delete_graph(self, graph_id: NodeID) -> None: + """Delete a call graph tree from the forest. + + Parameters + ---------- + graph_id : + The NodeID of the tree to delete. + """ + del self.graphs[graph_id] + + +@dataclass +class CallGraphNode: + """Class for call graph nodes. + + A call graph node represents a function in the call graph. + + Attributes + ---------- + symbol : + The symbol of the function that the node represents. + reasons : + The raw Reasons for the node. + After the call graph is built, this only contains reads_from and writes_to as well as unknown_calls. + children : + The set of children of the node, (i.e., the set of nodes that this node calls) + """ + + symbol: Symbol + reasons: Reasons + children: dict[NodeID, CallGraphNode] = field(default_factory=dict) + removed_children: dict[NodeID, CallGraphNode] = field(default_factory=dict) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.symbol.id}" + + def __repr__(self) -> str: + return f"{self.symbol.name}: {id(self)}" # pragma: no cover + + def add_child(self, child: CallGraphNode) -> None: + """Add a child to the node. + + Parameters + ---------- + child : + The child to add. + """ + self.children[child.symbol.id] = child + + def get_child(self, child_id: NodeID) -> CallGraphNode: # pragma: no cover + """Get a child from the node. + + Parameters + ---------- + child_id : + The NodeID of the child to get. + + Raises + ------ + KeyError + If the child_id is not in the children. + """ + result = self.children.get(child_id) + if result is None: + raise KeyError(f"Child with id {child_id} not found inside the call graph.") + return result + + def has_child(self, child_id: NodeID) -> bool: + """Check if the node has a child with the given NodeID. + + Parameters + ---------- + child_id : + The NodeID of the child to check for. + + Returns + ------- + bool + True if the node has a child with the given NodeID, False otherwise. + """ + return child_id in self.children + + def delete_child(self, child_id: NodeID, store_it: bool = False) -> None: + """Delete a child from the node. + + Parameters + ---------- + child_id : + The NodeID of the child to delete. + """ + if store_it: + child_to_store = self.children[child_id] + self.removed_children[child_id] = child_to_store + del self.children[child_id] + + def is_leaf(self) -> bool: + """Check if the node is a leaf node. + + Returns + ------- + bool + True if the node is a leaf node, False otherwise. + """ + return len(self.children) == 0 + + def is_inferred(self) -> bool: + """Check if the result is already inferred.""" + return self.reasons.result is not None + + +@dataclass +class CombinedCallGraphNode(CallGraphNode): + """Class for combined call graph nodes. + + A CombinedCallGraphNode represents a combined cycle of functions in the call graph. + + Attributes + ---------- + combines : + A dictionary of all nodes that are combined into this node. + This is later used for transferring the reasons of the combined node to the original nodes. + """ + + combines: dict[NodeID, CallGraphNode] = field(default_factory=dict) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.symbol.id}" + + def __repr__(self) -> str: + return f"{self.symbol.name}: {id(self)}" # pragma: no cover + + def separate(self) -> dict[NodeID, CallGraphNode]: + """Separate the node. + + After the purity of a combined node is inferred, + the reasons of the combined node are transferred to the original nodes. + + Returns + ------- + dict[NodeID, CallGraphNode] + The original nodes with the transferred reasons. + """ + original_nodes: dict[NodeID, CallGraphNode] = {} + for node_id, node in self.combines.items(): + original_nodes[node_id] = node + original_nodes[node_id].reasons.result = self.reasons.result + + return original_nodes + + +@dataclass +class ImportedCallGraphNode(CallGraphNode): + """Class for imported call graph nodes. + + This class represents functions that are imported from other modules in the call graph. + """ + + symbol: Import + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.symbol.id}" # pragma: no cover + + def __repr__(self) -> str: + return f"{self.symbol.name}: {id(self)}" # pragma: no cover diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/_module_data.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_module_data.py new file mode 100644 index 00000000..5bfdc357 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_module_data.py @@ -0,0 +1,821 @@ +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import TYPE_CHECKING + +import astroid + +if TYPE_CHECKING: + from collections.abc import Iterator + + +@dataclass +class ModuleData: + """ + Contains all data collected for a module. + + Attributes + ---------- + scope : + The module's scope, this contains all child scopes. + classes : + All classes and their ClassScope. + functions : + All functions and a list of their FunctionScopes. + The value is a list since there can be multiple functions with the same name. + imports : + All imported symbols. + """ + + scope: Scope + classes: dict[str, ClassScope] + functions: dict[str, list[FunctionScope]] + imports: dict[str, Import] + + +@dataclass +class PackageData: + """ + Contains all data collected for a package. + + Attributes + ---------- + package_name : + The name of the package. + modules : + All modules and their ModuleData. + The key is the name of the module. + The value is a tuple of the path to the module and the ModuleData. + combined_module : ModuleData + The combined ModuleData of all modules in the package. + """ + + package_name: str + modules: dict[str, tuple[str, ModuleData]] = field(default_factory=dict) + combined_module: ModuleData | None = field(default=None) + + def combine_modules(self) -> None: + """Combine the data of all modules into one ModuleData. + + Combines the data of all modules in the package into one ModuleData. + The scope of the new ModuleData is of type UnkownSymbol, and the children are the scopes of the modules. + The classes, functions, and imports are combined into one dict each. + + Returns + ------- + ModuleData + The combined ModuleData. + """ + combined_module = ModuleData( + scope=Scope(UnknownSymbol(id=NodeID(None, self.package_name)), [], None), + classes={}, + functions={}, + imports={}, + ) + + for module_data in self.modules.values(): + combined_module.scope.children.extend(module_data[1].scope) + combined_module.classes.update(module_data[1].classes) + for name, functions in module_data[1].functions.items(): + combined_module.functions.setdefault(name, []).extend(functions) + combined_module.imports.update(module_data[1].imports) + + self.combined_module = combined_module + + +@dataclass +class MemberAccess(astroid.NodeNG): + """Represents a member access. + + Superclass for MemberAccessTarget and MemberAccessValue. + Represents a member access, e.g. `a.b` or `a.b.c`. + + Attributes + ---------- + node : + The original node that represents the member access. + Needed as fallback when determining the parent node if the receiver is None. + receiver : + The receiver is the node that is accessed, it can be nested, e.g. `a` in `a.b` or `a.b` in `a.b.c`. + The receiver can be nested. + Is None if the receiver is not of type Name, Call or Attribute + member : + The member is the name of the node that accesses the receiver, e.g. `b` in `a.b`. + parent : astroid.NodeNG | None + The parent node of the member access. + name : + The name of the member access, e.g. `a.b`. + Is set in __post_init__, after the member access has been created. + If the MemberAccess is nested, the name of the receiver will be set to "UNKNOWN" since it is hard to determine + correctly for all possible cases, and we do not need it for the analysis. + """ + + node: astroid.Attribute | astroid.AssignAttr + receiver: MemberAccess | astroid.NodeNG | None + member: str + parent: astroid.NodeNG | None = field(default=None) + name: str = field(init=False) + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" # pragma: no cover + + def __post_init__(self) -> None: + if isinstance(self.receiver, astroid.AssignAttr | astroid.Attribute): + self.name = f"{self.receiver.attrname}.{self.member}" + elif isinstance(self.receiver, astroid.Name): + self.name = f"{self.receiver.name}.{self.member}" + else: + self.name = f"UNKNOWN.{self.member}" + + +@dataclass +class MemberAccessTarget(MemberAccess): + """Represents a member access target. + + Member access target is a member access written to, e.g. `a.b` in `a.b = 1`. + """ + + node: astroid.AssignAttr + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + @classmethod + def construct_member_access_target(cls, node: astroid.Attribute | astroid.AssignAttr) -> MemberAccessTarget: + """Construct a MemberAccessTarget node. + + Construct a MemberAccessTarget node from an Attribute or AssignAttr node. + The receiver is the node that is accessed, and the member is the node that accesses the receiver. + The receiver can be nested. + + Parameters + ---------- + node : + The node to construct the MemberAccessTarget node from. + + Returns + ------- + MemberAccessTarget + The constructed MemberAccessTarget node. + """ + receiver = node.expr + member = node.attrname + + try: + if isinstance(receiver, astroid.Name): + return MemberAccessTarget(node=node, receiver=receiver, member=member) + elif isinstance(receiver, astroid.Call): + return MemberAccessTarget(node=node, receiver=receiver.func, member=member) + elif isinstance(receiver, astroid.Attribute): + return MemberAccessTarget( + node=node, + receiver=cls.construct_member_access_target(receiver), + member=member, + ) + else: + return MemberAccessTarget(node=node, receiver=None, member=member) # pragma: no cover + # Since it is tedious to add testcases for this function, ignore the coverage for now + except TypeError as err: # pragma: no cover + raise TypeError(f"Unexpected node type {type(node)}") from err # pragma: no cover + + +@dataclass +class MemberAccessValue(MemberAccess): + """Represents a member access value. + + Member access value is a member access read from, e.g. `a.b` in `print(a.b)`. + """ + + node: astroid.Attribute + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + @classmethod + def construct_member_access_value(cls, node: astroid.Attribute) -> MemberAccessValue: + """Construct a MemberAccessValue node. + + Construct a MemberAccessValue node from an Attribute node. + The receiver is the node that is accessed, and the member is the node that accesses the receiver. + The receiver can be nested. + + Parameters + ---------- + node : + The node to construct the MemberAccessValue node from. + + Returns + ------- + MemberAccessValue + The constructed MemberAccessValue node. + """ + receiver = node.expr + member = node.attrname + + try: + if isinstance(receiver, astroid.Name): + return MemberAccessValue(node=node, receiver=receiver, member=member) + elif isinstance(receiver, astroid.Call): + return MemberAccessValue(node=node, receiver=receiver.func, member=member) + elif isinstance(receiver, astroid.Attribute): + return MemberAccessValue(node=node, receiver=cls.construct_member_access_value(receiver), member=member) + else: + return MemberAccessValue(node=node, receiver=None, member=member) + # Since it is tedious to add testcases for this function, ignore the coverage for now + except TypeError as err: # pragma: no cover + raise TypeError(f"Unexpected node type {type(node)}") from err # pragma: no cover + + +@dataclass +class NodeID: + """Represents an id of a node. + + Attributes + ---------- + module : + The module of the node. + Is None for combined nodes. + name : + The name of the node. + line : + The line of the node in the source code. + Is None for combined nodes, builtins or any other node that do not have a line. + col : + The column of the node in the source code. + Is None for combined nodes, builtins or any other node that do not have a line. + """ + + module: str | None + name: str + line: int | None = None + col: int | None = None + + def __str__(self) -> str: + if self.module is not None: + if self.line is not None and self.col is not None: + return f"{self.module}.{self.name}.{self.line}.{self.col}" + else: + return f"{self.module}.{self.name}" + elif self.line is not None and self.col is not None: + if self.line == 0 and self.col == 0: + return f"{self.name}" + return f"{self.name}.{self.line}.{self.col}" # pragma: no cover + else: + return f"{self.name}" + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NodeID): # pragma: no cover + if isinstance(other, Symbol): + return self == other.id + raise NotImplementedError(f"Cannot compare NodeID with {type(other)}") + return ( + self.module == other.module + and self.name == other.name + and self.line == other.line + and self.col == other.col + ) + + def __lt__(self, other: NodeID) -> bool: + if not isinstance(other, NodeID): + raise TypeError(f"Cannot compare NodeID with {type(other)}") # pragma: no cover + + if self.line is None: + if other.line is None: + return False # Both lines are None, consider them equal + return True # self.line is None, other.line is not, so other is greater + elif other.line is None: + return False # other.line is None, self.line is not, so self is greater + + if self.line != other.line and self.line is not None and other.line is not None: + return self.line < other.line + + if self.col != other.col and self.col is not None and other.col is not None: + return self.col < other.col # pragma: no cover + + # If both line and column are equal, compare by name, + return self.name < other.name + + @classmethod + def calc_node_id( + cls, + node: ( + astroid.NodeNG + | astroid.Module + | astroid.ClassDef + | astroid.FunctionDef + | astroid.AssignName + | astroid.Name + | astroid.AssignAttr + | astroid.Import + | astroid.ImportFrom + | astroid.Call + | astroid.Lambda + | astroid.ListComp + | MemberAccess + ), + ) -> NodeID: + """Calculate the NodeID of the given node. + + The NodeID is calculated by using the name of the module, the name of the node, the line number and the column offset. + The NodeID is used to identify nodes in the module. + + Parameters + ---------- + node : + + Returns + ------- + NodeID + The NodeID of the given node. + """ + if isinstance(node, MemberAccess): + module = node.node.root().name + else: + module = node.root().name + + match node: + case astroid.Module(): + return NodeID(None, node.name, 0, 0) + case astroid.ClassDef(): + return NodeID(module, node.name, node.lineno, node.col_offset) + case astroid.FunctionDef(): + try: # workaround for "AttributeError: 'FunctionDef' object has no attribute 'decorators'", so that node.fromlineno can be called + node.decorators + except AttributeError: + node.postinit(astroid.Arguments(None, None, astroid.NodeNG(node.lineno, node.col_offset, None, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset)), [], None) + return NodeID(module, node.name, node.fromlineno, node.col_offset) + case astroid.AssignName(): + return NodeID(module, node.name, node.lineno, node.col_offset) + case astroid.Name(): + return NodeID(module, node.name, node.lineno, node.col_offset) + case MemberAccess(): + return NodeID(module, node.name, node.node.lineno, node.node.col_offset) + case astroid.Import(): # TODO: we need a special treatment for imports and import from + return NodeID(module, node.names[0][0], node.lineno, node.col_offset) # pragma: no cover + case astroid.ImportFrom(): + return NodeID(module, node.names[0][1], node.lineno, node.col_offset) # pragma: no cover + case astroid.AssignAttr(): + return NodeID(module, node.attrname, node.lineno, node.col_offset) # pragma: no cover + case astroid.Call(): + # Make sure there is no AttributeError because of the inconsistent names in the astroid API. + if isinstance(node.func, astroid.Attribute): + return NodeID(module, node.func.attrname, node.lineno, node.col_offset) + elif isinstance(node.func, astroid.Name): + return NodeID(module, node.func.name, node.lineno, node.col_offset) + else: + return NodeID(module, "UNKNOWN", node.lineno, node.col_offset) + case astroid.Lambda(): + if isinstance(node.parent, astroid.Assign) and node.name != "LAMBDA": + return NodeID(module, node.name, node.lineno, node.col_offset) + return NodeID(module, "LAMBDA", node.lineno, node.col_offset) + case astroid.ListComp(): + return NodeID(module, "LIST_COMP", node.lineno, node.col_offset) + case astroid.NodeNG(): + return NodeID(module, node.as_string(), node.lineno, node.col_offset) + case _: # pragma: no cover + raise ValueError(f"Node type {node.__class__.__name__} is not supported yet.") + + +@dataclass +class Symbol(ABC): + """Represents a node that defines a Name. + + A Symbol is a node that defines a Name, e.g. a function, a class, a variable, etc. + It can be referenced by another node. + + Attributes + ---------- + node : + The node that defines the symbol. + id : + The id of that node. + name : + The name of the symbol (for easier access). + """ + + node: astroid.ClassDef | astroid.FunctionDef | astroid.AssignName | MemberAccessTarget + id: NodeID + name: str + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}.line{self.id.line}" + + def __hash__(self) -> int: + return hash(str(self)) + + +@dataclass +class UnknownSymbol(Symbol): + """Represents an unknown symbol. + + An unknown symbol is used to represent a symbol that could not be determined. + It is used as a placeholder for symbols that could not be determined during the analysis. + + Attributes + ---------- + node : + """ + + node: None = None + id: NodeID | None = None # type: ignore[assignment] + name: str = "UNKNOWN" + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" # pragma: no cover + + +@dataclass +class Parameter(Symbol): + """Represents a parameter of a function.""" + + node: astroid.AssignName + kind: ParameterKind | None = None + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}.line{self.id.line}" + + +@dataclass +class LocalVariable(Symbol): + """Represents a local variable.""" + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}.line{self.id.line}" + + +@dataclass +class GlobalVariable(Symbol): + """Represents a global variable.""" + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}.line{self.id.line}" + + +@dataclass +class ClassVariable(Symbol): + """Represents a class variable. + + Attributes + ---------- + klass : + The class that defines the class variable. + """ + + klass: astroid.ClassDef | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + if self.klass is None: + return f"{self.__class__.__name__}.UNKNOWN_CLASS.{self.name}.line{self.id.line}" # pragma: no cover + return f"{self.__class__.__name__}.{self.klass.name}.{self.name}.line{self.id.line}" + + +@dataclass +class InstanceVariable(Symbol): + """Represents an instance variable. + + Attributes + ---------- + klass : + The class that defines the instance variable. + """ + + klass: astroid.ClassDef | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + if self.klass is None: + return f"{self.__class__.__name__}.UNKNOWN_CLASS.{self.name}.line{self.id.line}" # pragma: no cover + return f"{self.__class__.__name__}.{self.klass.name}.{self.name}.line{self.id.line}" + + +@dataclass +class Import(Symbol): + """Represents an import. + + Attributes + ---------- + node : + The node that defines the import. + name : + The name of the symbol that is imported if any is given. + Else it is equal to the module name. + module : + The name of the module that is imported. + alias : + If the node is of type Import alias is the alias name for the module name if any is given. + If the node is of type ImportFrom alias is the alias name for the name of the symbol if any is given. + inferred_node : + When the import is used as a reference (or a symbol) + the inferred_node is the node of the used reference (or symbol) in the original module. + It was inferred by the reference analysis by using astroids safe_infer method. + If the method could not infer the node, the inferred_node is None. + call : + The original call node as fallback for the case, that the purity of the inferred_node cannot be inferred. + Only is set if the symbol represents a call. + """ + + node: astroid.ImportFrom | astroid.Import + module: str + alias: str | None = None + inferred_node: astroid.NodeNG | None = None + call: astroid.Call | None = None + + def __str__(self) -> str: + if isinstance(self.node, astroid.ImportFrom): + if self.name: + return f"{self.__class__.__name__}.{self.module}.{self.name}.line{self.id.line}" + return f"{self.__class__.__name__}.{self.module}.line{self.id.line}" # pragma: no cover + else: + if self.name != self.module: + return f"{self.__class__.__name__}.{self.module}.{self.name}.line{self.id.line}" + return f"{self.__class__.__name__}.{self.module}.line{self.id.line}" + + def __hash__(self) -> int: + return hash(str(self)) + + +@dataclass +class Builtin(Symbol): + """Represents a builtin (function). + + Attributes + ---------- + call : + The call node of the function. + """ + + call: astroid.Call + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + def __hash__(self) -> int: + return hash(str(self)) + + +@dataclass +class BuiltinOpen(Builtin): + """Represents the builtin open like function. + + When dealing with open-like functions the call node is needed to determine the file path. + + Attributes + ---------- + call : + The call node of the open-like function. + """ + + call: astroid.Call + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + def __hash__(self) -> int: + return hash(str(self)) + + +@dataclass +class CombinedSymbol(Symbol): + """Represents a combined symbol. + + A combined symbol is used to represent a combined node in the call graph. + Since the node for a combined node does not exist, it is set to None. + + + Attributes + ---------- + node : + + """ + + node: None + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" # pragma: no cover + + +@dataclass +class Reference: + """Represents a node that references a Name. + + A Reference is a node that references a Name, + e.g., a function call, a variable read, etc. + + + Attributes + ---------- + node : + The node that defines the symbol. + id : + The id of that node. + name : + The name of the symbol (for easier access). + """ + + node: astroid.Call | astroid.Name | MemberAccessValue + id: NodeID + name: str + + def __str__(self) -> str: + if self.id is None: + return f"{self.__class__.__name__}.{self.name}" # pragma: no cover + return f"{self.__class__.__name__}.{self.name}.line{self.id.line}" + + def __hash__(self) -> int: + return hash(str(self)) + + +@dataclass +class Scope: + """Represents a node in the scope tree. + + The scope tree is a tree that represents the scope of a module. It is used to determine the scope of a reference. + On the top level, there is a ScopeNode for the module. Each Scope has a list of children, which are the nodes + that are defined in the scope of the node. Each Scope also has a reference to its parent node. + + Attributes + ---------- + _symbol : + The symbol that defines the scope. + _children : + The list of Scope or ClassScope instances that are defined in the scope of the Symbol node. + Is None if the node is a leaf node. + _parent : + The parent node in the scope tree, there is None if the node is the root node. + """ + + _symbol: Symbol + _children: list[Scope] = field(default_factory=list) + _parent: Scope | None = None + + def __iter__(self) -> Iterator[Scope | ClassScope]: + yield self + + def __next__(self) -> Scope | ClassScope: + return self # pragma: no cover + + def __str__(self) -> str: + return f"{self.symbol.name}.line{self.symbol.id.line}" # pragma: no cover + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + @property + def symbol(self) -> Symbol: + """Symbol : The symbol that defines the scope.""" + return self._symbol + + @symbol.setter + def symbol(self, new_symbol: Symbol) -> None: # pragma: no cover + if not isinstance(new_symbol, Symbol): + raise TypeError("Invalid node type.") + self._symbol = new_symbol + + @property + def children(self) -> list[Scope | ClassScope]: + """list[Scope | ClassScope] : Children of the scope. + + The list of Scope or ClassScope instances that are defined in the scope of the Symbol node. + Is None if the node is a leaf node. + """ + return self._children + + @children.setter + def children(self, new_children: list[Scope | ClassScope]) -> None: + if not isinstance(new_children, list): + raise TypeError("Children must be a list.") # pragma: no cover + self._children = new_children + + @property + def parent(self) -> Scope | None: + """Scope | ClassScope | None : Parent of the scope. + + The parent node in the scope tree. + Is None if the node is the root node. + """ + return self._parent + + @parent.setter + def parent(self, new_parent: Scope | None) -> None: # pragma: no cover + if not isinstance(new_parent, Scope | None): + raise TypeError("Invalid parent type.") + self._parent = new_parent + + def get_module_scope(self) -> Scope: # pragma: no cover + """Return the module scope. + + Gets the module scope for each scope in the scope tree. + The module scope is the root node of the scope tree. + + Returns + ------- + Scope + The module scope. + """ + if self.parent is None: + return self + return self.parent.get_module_scope() + + +@dataclass +class ClassScope(Scope): + """Represents a Scope that defines the scope of a class. + + Attributes + ---------- + class_variables : + The name of the class variable and a list of its Symbols (which represent a declaration). + There can be multiple declarations of the same class variable, e.g. `a = 1` and `a = 2` + since we cannot determine which one is used since we do not analyze the control flow. + Also, it is impossible to distinguish between a declaration and a reassignment. + instance_variables : + The name of the instance variable and a list of its Symbols (which represent a declaration). + init_function : + The init function of the class if it exists else None. + super_classes : + The list of superclasses of the class if any. + """ + + class_variables: dict[str, list[Symbol]] = field(default_factory=dict) + instance_variables: dict[str, list[Symbol]] = field(default_factory=dict) + new_function: FunctionScope | None = None + init_function: FunctionScope | None = None + post_init_function: FunctionScope | None = None + super_classes: list[ClassScope] = field(default_factory=list) + + +@dataclass +class FunctionScope(Scope): + """Represents a Scope that defines the scope of a function. + + Attributes + ---------- + target_symbols : + The dict of all target nodes used inside the corresponding function. + Target nodes are specified as all nodes that can be written to and which can be represented as a Symbol. + This includes assignments, parameters, + value_references : + The dict of all value nodes used inside the corresponding function. + call_references : + The dict of all function calls inside the corresponding function. + The key is the name of the call node, the value is a list of all References of call nodes with that name. + parameters : + The parameters of the function. + globals_used : + The global variables used inside the function. + It stores the globally assigned nodes (Assignment of the used variable). + """ + + target_symbols: dict[str, list[Symbol]] = field(default_factory=dict) + value_references: dict[str, list[Reference]] = field(default_factory=dict) + call_references: dict[str, list[Reference]] = field(default_factory=dict) + parameters: dict[str, Parameter] = field(default_factory=dict) + globals_used: dict[str, list[GlobalVariable]] = field(default_factory=dict) + + def remove_call_reference_by_id(self, call_id: str) -> None: + """Remove a call node by name. + + Removes a call node from the dict of call nodes by name. + This is used to remove cyclic calls from the dict of call nodes after the call graph has been built. + + Parameters + ---------- + call_id : + The name of the call node to remove. + """ + self.call_references.pop(call_id, None) # pragma: no cover + + +class ParameterKind(Enum): + """Represents the kind of a parameter.""" + + POSITIONAL_ONLY = auto() + POSITIONAL_OR_KEYWORD = auto() + VAR_POSITIONAL = auto() + KEYWORD_ONLY = auto() + VAR_KEYWORD = auto() diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity.py new file mode 100644 index 00000000..c38cd083 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity.py @@ -0,0 +1,697 @@ +from __future__ import annotations + +import json +import typing +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import TYPE_CHECKING, Any + +import astroid + +from safeds_stubgen.api_analyzer.purity_analysis.model._module_data import ( + MemberAccessValue, + NodeID, + Reference, + Symbol, + UnknownSymbol, +) + +def ensure_file_exists(file: Path) -> None: + """ + Create a file and all parent directories if they don't exist already. + + Parameters + ---------- + file: Path + The file path. + """ + file.parent.mkdir(parents=True, exist_ok=True) + file.touch(exist_ok=True) + +if TYPE_CHECKING: + from pathlib import Path + + from model import ( + ClassVariable, + GlobalVariable, + Import, + InstanceVariable, + Parameter, + ) + + +class PurityResult(ABC): + """Superclass for purity results. + + Purity results are either pure, impure or unknown. + + is_class : + Whether the result is for a class or not. + """ + + is_class: bool = False + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + @abstractmethod + def to_dict(self, shorten: bool = False) -> dict[str, Any]: + pass # pragma: no cover + + @abstractmethod + def update(self, other: PurityResult | None) -> PurityResult: + """Update the current result with another result.""" + + +@dataclass +class Pure(PurityResult): + """Class for pure results. + + A function is pure if it has no (External-, Internal-)Read nor (External-, Internal-)Write side effects. + A pure function must also have no unknown reasons. + + Attributes + ---------- + is_class : + Whether the result is for a class or not. + """ + + is_class: bool = False + + def update(self, other: PurityResult | None) -> PurityResult: + """Update the current result with another result. + + Parameters + ---------- + other : + The result to update with. + + Returns + ------- + PurityResult + The updated result. + + Raises + ------ + TypeError + If the result cannot be updated with the given result. + """ + if other is None: + return self.clone() # pragma: no cover + elif isinstance(self, Pure): + if isinstance(other, Pure): + return self.clone() + elif isinstance(other, Impure): + return other.clone() + elif isinstance(self, Impure): # pragma: no cover + if isinstance(other, Pure): + return self.clone() + elif isinstance(other, Impure): + return Impure(reasons=self.reasons | other.reasons).clone() + + raise TypeError(f"Cannot update {self} with {other}") # pragma: no cover + + @staticmethod + def clone() -> Pure: + return Pure() + + def to_dict(self, shorten: bool = False) -> dict[str, Any]: # noqa: ARG002 + return {"purity": self.__class__.__name__} + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.__class__.__name__}" # pragma: no cover + + +@dataclass +class Impure(PurityResult): + """Class for impure results. + + A function is impure if it has at least one + (File-, NonLocalVariable-)Read OR (File-, NonLocalVariable-)Write side effect. + An impure function must also have no unknown reasons. + + Be aware that a function can be impure because of multiple reasons. + Also, Impure != Pure since: not Pure would mean a function is either unknown or has at least one + (File-, NonLocalVariable-)Read OR (File-, NonLocalVariable-)Write side effect. + + Attributes + ---------- + reasons : + The reasons why the function is impure. + is_class : + Whether the result is for a class or not. + """ + + reasons: set[ImpurityReason] + is_class: bool = False + + def update(self, other: PurityResult | None) -> PurityResult: + """Update the current result with another result. + + Parameters + ---------- + other : + The result to update with. + + Returns + ------- + PurityResult + The updated result. + + Raises + ------ + TypeError + If the result cannot be updated with the given result. + """ + if other is None: + return self.clone() # pragma: no cover + elif isinstance(self, Pure): # pragma: no cover + if isinstance(other, Pure): + return self.clone() + elif isinstance(other, Impure): + return other.clone() + elif isinstance(self, Impure): + if isinstance(other, Pure): + return self.clone() + elif isinstance(other, Impure): + return Impure(reasons=self.reasons | other.reasons).clone() + raise TypeError(f"Cannot update {self} with {other}") # pragma: no cover + + def clone(self) -> Impure: + return Impure(reasons=self.reasons.copy()) + + def to_dict(self, shorten: bool = False) -> dict[str, Any]: + seen = set() + non_local_variable_reads = [] + non_local_variable_writes = [] + file_reads = [] + file_writes = [] + unknown_calls = [] + native_calls = [] + parameter_calls = [] + for reason in self.reasons: + if str(reason) not in seen: + seen.add(str(reason)) + match reason: + case NonLocalVariableRead(): + non_local_variable_reads.append(reason.to_dict()) + case NonLocalVariableWrite(): + non_local_variable_writes.append(reason.to_dict()) + case FileRead(): + file_reads.append(reason.to_dict()) + case FileWrite(): + file_writes.append(reason.to_dict()) + case UnknownCall(): + unknown_calls.append(reason.to_dict()) + case NativeCall(): # pragma: no cover + native_calls.append(reason.to_dict()) + case CallOfParameter(): # pragma: no cover + parameter_calls.append(reason.to_dict()) + case _: # pragma: no cover + raise TypeError(f"Unknown reason type: {reason}") + if not shorten: + combined_reasons: dict[str, Any] = { + "NonLocalVariableRead": non_local_variable_reads, + "NonLocalVariableWrite": non_local_variable_writes, + "FileRead": file_reads, + "FileWrite": file_writes, + "UnknownCall": unknown_calls, + "NativeCall": native_calls, + "CallOfParameter": parameter_calls, + } + else: + combined_reasons = { + "NonLocalVariableRead": len(non_local_variable_reads), + "NonLocalVariableWrite": len(non_local_variable_writes), + "FileRead": len(file_reads), + "FileWrite": len(file_writes), + "UnknownCall": len(unknown_calls), + "NativeCall": len(native_calls), + "CallOfParameter": len(parameter_calls), + } + return { + "purity": self.__class__.__name__, + "reasons": {reason: value for reason, value in combined_reasons.items() if value}, + } + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.__class__.__name__}" # pragma: no cover + + +class ImpurityReason(ABC): # this is just a base class, and it is important that it cannot be instantiated + """Superclass for impurity reasons. + + If a function is impure it is because of one or more impurity reasons. + """ + + @abstractmethod + def __str__(self) -> str: + pass # pragma: no cover + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + @abstractmethod + def to_dict(self) -> dict[str, Any]: + pass # pragma: no cover + + +class Read(ImpurityReason, ABC): + """Superclass for read type impurity reasons.""" + + +@dataclass +class NonLocalVariableRead(Read): + """Class for internal variable reads (GlobalVariable / global Fields). + + Attributes + ---------- + symbol : + The symbol that is read. + origin : + The origin of the read. + """ + + symbol: GlobalVariable | ClassVariable | InstanceVariable | Import | UnknownSymbol + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.symbol.__class__.__name__}.{self.symbol.name}" + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) + return { + "origin": f"{origin}", + "reason": f"{self.symbol.__class__.__name__}.{self.symbol.name}", + } + + +@dataclass +class FileRead(Read): + """Class for external variable reads (File / Database). + + Attributes + ---------- + source : + The source of the read. + origin : + The origin of the read. + """ + + source: Expression + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + if isinstance(self.source, Expression): + return f"{self.__class__.__name__}: {self.source.__str__()}" + return f"{self.__class__.__name__}: UNKNOWN EXPRESSION" # pragma: no cover + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) + return { + "origin": f"{origin}", + "reason": f"{self.source.__str__()}", + } + + +class Write(ImpurityReason, ABC): + """Superclass for write type impurity reasons.""" + + +@dataclass +class NonLocalVariableWrite(Write): + """Class for internal variable writes (GlobalVariable / global Fields). + + Attributes + ---------- + symbol : + The symbol that is written to. + origin : + The origin of the write. + """ + + symbol: GlobalVariable | ClassVariable | InstanceVariable | Import | UnknownSymbol + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.symbol.__class__.__name__}.{self.symbol.name}" + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) + return { + "origin": f"{origin}", + "reason": f"{self.symbol.__class__.__name__}.{self.symbol.name}", + } + + +@dataclass +class FileWrite(Write): + """Class for external variable writes (File / Database). + + Attributes + ---------- + source : + The source of the write. + origin : + The origin of the write. + """ + + source: Expression + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + if isinstance(self.source, Expression): + return f"{self.__class__.__name__}: {self.source.__str__()}" + return f"{self.__class__.__name__}: UNKNOWN EXPRESSION" # pragma: no cover + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) + return { + "origin": f"{origin}", + "reason": f"{self.source.__str__()}", + } + + +class Unknown(ImpurityReason, ABC): + """Superclass for unknown type impurity reasons.""" + + +@dataclass +class UnknownProto(Unknown): + """Class for UnknownCalls which are not fully determined. + + Attributes + ---------- + symbol : + The symbol or reference object which is not fully determined. + origin : + The origin of the unknown call. + """ + + symbol: Symbol | Reference + origin: Symbol | NodeID | None = field(default=None) # TODO: remove NodeID + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.symbol.__class__.__name__}.{self.symbol.name}" # pragma: no cover + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) # pragma: no cover + return { + "origin": f"{origin}", + "reason": f"{self.symbol.name}", + } # pragma: no cover + + +@dataclass +class UnknownCall(Unknown): + """Class for calling unknown code. + + Since we cannot analyze unknown code, we mark it as unknown. + + Attributes + ---------- + expression : + The expression that is called. + origin : + The origin of the call. + """ + + expression: Expression + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.expression.__str__()}" + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) # pragma: no cover + return { + "origin": f"{origin}", + "reason": f"{self.expression.__str__()}", + } # pragma: no cover + + +@dataclass +class NativeCall(Unknown): # ExternalCall + """Class for calling native code. + + Since we cannot analyze native code, we mark it as unknown. + + Attributes + ---------- + expression : + The expression that is called. + origin : + The origin of the call. + """ + + expression: Expression + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.expression.__str__()}" + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) # pragma: no cover + return { + "origin": f"{origin}", + "reason": f"{self.expression.__str__()}", + } # pragma: no cover + + +@dataclass +class CallOfParameter(Unknown): # ParameterCall + """Class for parameter calls. + + Since we cannot analyze parameter calls, we mark it as unknown. + A parameter call is a call of a function that is passed as a parameter to another function. + E.g., def f(x): + x() + The call of x() is a parameter call only known at runtime. + + Attributes + ---------- + expression : + The expression that is called. + origin : + The origin of the call. + """ + + expression: Expression + origin: Symbol | NodeID | None = field(default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self.expression.__str__()}" + + def to_dict(self) -> dict[str, Any]: + origin = ( + self.origin.id if isinstance(self.origin, Symbol) else (self.origin if self.origin is not None else None) + ) # pragma: no cover + return { + "origin": f"{origin}", + "reason": f"{self.expression.__str__()}", + } # pragma: no cover + + +class Expression(ABC): # this is just a base class, and it is important that it cannot be instantiated + """Superclass for expressions. + + Expressions are used to represent code. + """ + + @abstractmethod + def __str__(self) -> str: + pass # pragma: no cover + + +@dataclass +class ParameterAccess(Expression): + """Class for function parameter access. + + Attributes + ---------- + parameter : + The parameter that is accessed. + """ + + parameter: Parameter + + def __str__(self) -> str: + if isinstance(self.parameter, str): + return f"{self.__class__.__name__}.{self.parameter}" + return f"{self.__class__.__name__}.{self.parameter.name}" + + +@dataclass +class StringLiteral(Expression): + """Class for string literals. + + Attributes + ---------- + value : + The name of the string literal. + """ + + value: str + + def __str__(self) -> str: + return f"StringLiteral.{self.value}" + + +@dataclass +class UnknownFunctionCall(Expression): + """Class for unknown function calls. + + Attributes + ---------- + call : + The call node. + inferred_def : + The inferred function definition for the call if it is known. + name : + The name of the call. + """ + + call: astroid.Call | None = None + inferred_def: astroid.FunctionDef | None = None + name: str = field(init=False) + + def __post_init__(self) -> None: + if self.inferred_def is not None: + self.name = f"{self.inferred_def.root().name}.{self.inferred_def.name}" + elif self.call is None: + self.name = "UNKNOWN" # pragma: no cover + elif isinstance(self.call, MemberAccessValue): + self.name = self.call.name # pragma: no cover + elif isinstance(self.call.func, astroid.Attribute): + self.name = self.call.func.attrname + elif isinstance(self.call.func, astroid.Name): + self.name = self.call.func.name + else: + self.name = "UNKNOWN" + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +@dataclass +class UnknownClassInit(Expression): + """Class for unknown class initializations. + + Attributes + ---------- + call : + The call node. + inferred_def : + The inferred class definition for the call if it is known. + name : + The name of the call. + """ + + call: astroid.Call + inferred_def: astroid.ClassDef | None = None + name: str = field(init=False) + + def __post_init__(self) -> None: # pragma: no cover + if self.inferred_def is not None: + self.name = f"{self.inferred_def.root().name}.{self.inferred_def.name}" + elif isinstance(self.call.func, astroid.Attribute): + self.name = self.call.func.attrname + else: + self.name = self.call.func.name + + def __str__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" # pragma: no cover + + +class APIPurity: + """Class for API purity. + + The API purity is used to represent the purity result of an API. + + Attributes + ---------- + purity_results : dict[NodeID, dict[NodeID, PurityResult]] + The purity results of all functions of the API. + The key is the NodeID of the module, + the value is a dictionary of the purity results of the functions in the module. + """ + + purity_results: typing.ClassVar[dict[NodeID, dict[NodeID, PurityResult]]] = {} + + def to_json_file(self, path: Path, shorten: bool = True) -> None: + ensure_file_exists(path) + with path.open("w") as f: + json.dump(self.to_dict(shorten), f, indent=2) + + def to_dict(self, shorten: bool = False) -> dict[str, Any]: + return { + module_name.__str__(): { + function_id.__str__(): purity.to_dict(shorten) + for function_id, purity in purity_result.items() + if not purity.is_class + } + for module_name, purity_result in self.purity_results.items() + } + + +class OpenMode(Enum): + """Enum for open modes. + + Attributes + ---------- + READ : OpenMode + Read mode. + WRITE : OpenMode + Write mode. + READ_WRITE : OpenMode + Read and write mode. + """ + + READ = auto() + WRITE = auto() + READ_WRITE = auto() diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity_builtins.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity_builtins.py new file mode 100644 index 00000000..8a74bd6e --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_purity_builtins.py @@ -0,0 +1,2079 @@ +import astroid + +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + ClassScope, + ClassVariable, + FileRead, + FileWrite, + GlobalVariable, + Impure, + NodeID, + NonLocalVariableRead, + NonLocalVariableWrite, + OpenMode, + Pure, + PurityResult, + StringLiteral, + UnknownSymbol, +) + +BUILTIN_FUNCTIONS: dict[str, PurityResult] = { # all errors and warnings are pure + "ArithmeticError": Pure(), + "AssertionError": Pure(), + "AttributeError": Pure(), + "BaseException": Impure(set()), + "BaseExceptionGroup": Impure(set()), + "BlockingIOError": Pure(), + "BrokenPipeError": Pure(), + "BufferError": Pure(), + "BytesWarning": Pure(), + "ChildProcessError": Pure(), + "ConnectionAbortedError": Pure(), + "ConnectionError": Pure(), + "ConnectionRefusedError": Pure(), + "ConnectionResetError": Pure(), + "DeprecationWarning": Pure(), + "EOFError": Pure(), + "Ellipsis": Impure(set()), + "EncodingWarning": Pure(), + "EnvironmentError": Pure(), + "Exception": Impure(set()), + "ExceptionGroup": Impure(set()), + "False": Pure(), + "FileExistsError": Pure(), + "FileNotFoundError": Pure(), + "FloatingPointError": Pure(), + "FutureWarning": Pure(), + "GeneratorExit": Impure(set()), + "IOError": Pure(), + "ImportError": Pure(), + "ImportWarning": Pure(), + "IndentationError": Pure(), + "IndexError": Pure(), + "InterruptedError": Pure(), + "IsADirectoryError": Pure(), + "KeyError": Pure(), + "KeyboardInterrupt": Impure(set()), + "LookupError": Pure(), + "MemoryError": Pure(), + "ModuleNotFoundError": Pure(), + "NameError": Pure(), + "None": Impure(set()), + "NotADirectoryError": Pure(), + "NotImplemented": Impure(set()), + "NotImplementedError": Pure(), + "OSError": Pure(), + "OverflowError": Pure(), + "PendingDeprecationWarning": Pure(), + "PermissionError": Pure(), + "ProcessLookupError": Pure(), + "RecursionError": Pure(), + "ReferenceError": Pure(), + "ResourceWarning": Pure(), + "RuntimeError": Pure(), + "RuntimeWarning": Pure(), + "StopAsyncIteration": Impure(set()), + "StopIteration": Impure(set()), + "SyntaxError": Pure(), + "SyntaxWarning": Pure(), + "SystemError": Pure(), + "SystemExit": Impure(set()), + "TabError": Pure(), + "TimeoutError": Pure(), + "True": Pure(), + "TypeError": Pure(), + "UnboundLocalError": Pure(), + "UnicodeDecodeError": Pure(), + "UnicodeEncodeError": Pure(), + "UnicodeError": Pure(), + "UnicodeTranslateError": Pure(), + "UnicodeWarning": Pure(), + "UserWarning": Pure(), + "ValueError": Pure(), + "Warning": Pure(), + "WindowsError": Pure(), + "ZeroDivisionError": Pure(), + "__build_class__": Impure(set()), + "__debug__": Impure(set()), + "__doc__": Impure(set()), + "__import__": Impure(set()), + "__loader__": Impure(set()), + "__name__": Impure(set()), + "__package__": Impure(set()), + "__spec__": Impure(set()), + "abs": Pure(), + "aiter": Pure(), + "all": Pure(), + "anext": Pure(), + "any": Pure(), + "ascii": Pure(), + "bin": Pure(), + "bool": Pure(), + "breakpoint": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + FileRead(StringLiteral("UNKNOWN")), + FileWrite(StringLiteral("UNKNOWN")), + }, + ), + "bytearray": Pure(), + "bytes": Pure(), + "callable": Pure(), + "chr": Pure(), + "classmethod": Pure(), + "compile": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + FileRead(StringLiteral("UNKNOWN")), + FileWrite(StringLiteral("UNKNOWN")), + }, + ), # Can execute arbitrary code + "complex": Pure(), + "delattr": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + }, + ), # Can modify objects + "dict": Pure(), + "dir": Pure(), + "divmod": Pure(), + "enumerate": Pure(), + "eval": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + FileRead(StringLiteral("UNKNOWN")), + FileWrite(StringLiteral("UNKNOWN")), + }, + ), # Can execute arbitrary code + "exec": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + FileRead(StringLiteral("UNKNOWN")), + FileWrite(StringLiteral("UNKNOWN")), + }, + ), # Can execute arbitrary code + "filter": Pure(), + "float": Pure(), + "format": Pure(), + "frozenset": Pure(), + "getattr": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + }, + ), # Can raise exceptions or interact with external resources + "globals": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + }, + ), # May interact with external resources + "hasattr": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + }, + ), # Calls the getattr function + "hash": Pure(), + "help": Impure({FileWrite(StringLiteral("stdout"))}), # Interacts with external resources + "hex": Pure(), + "id": Pure(), + "input": Impure({FileRead(StringLiteral("stdin"))}), # Reads user input + "int": Pure(), + "isinstance": Pure(), + "issubclass": Pure(), + "iter": Pure(), + "len": Pure(), + "list": Pure(), + "locals": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + }, + ), # May interact with external resources + "map": Pure(), + "max": Pure(), + "memoryview": Pure(), + "min": Pure(), + "next": Pure(), + "object": Pure(), + "oct": Pure(), + "ord": Pure(), + "pow": Pure(), + "print": Impure({FileWrite(StringLiteral("stdout"))}), + "property": Pure(), + "range": Pure(), + "repr": Pure(), + "reversed": Pure(), + "round": Pure(), + "set": Pure(), + "setattr": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + }, + ), # Can modify objects + "slice": Pure(), + "sorted": Pure(), + "staticmethod": Pure(), + "str": Pure(), + "sum": Pure(), + "super": Pure(), + "tuple": Pure(), + "type": Pure(), + "vars": Impure( + { + NonLocalVariableRead(UnknownSymbol()), + NonLocalVariableWrite(UnknownSymbol()), + }, + ), # May interact with external resources + "zip": Pure(), +} + +OPEN_MODES = { + "": OpenMode.READ, + "r": OpenMode.READ, + "rb": OpenMode.READ, + "rt": OpenMode.READ, + "w": OpenMode.WRITE, + "wb": OpenMode.WRITE, + "wt": OpenMode.WRITE, + "a": OpenMode.WRITE, + "ab": OpenMode.WRITE, + "at": OpenMode.WRITE, + "x": OpenMode.WRITE, + "xb": OpenMode.WRITE, + "xt": OpenMode.WRITE, + "r+": OpenMode.READ_WRITE, + "rb+": OpenMode.READ_WRITE, + "w+": OpenMode.READ_WRITE, + "wb+": OpenMode.READ_WRITE, + "a+": OpenMode.READ_WRITE, + "ab+": OpenMode.READ_WRITE, + "x+": OpenMode.READ_WRITE, + "xb+": OpenMode.READ_WRITE, + "r+b": OpenMode.READ_WRITE, + "rb+b": OpenMode.READ_WRITE, + "w+b": OpenMode.READ_WRITE, + "wb+b": OpenMode.READ_WRITE, + "a+b": OpenMode.READ_WRITE, + "ab+b": OpenMode.READ_WRITE, + "x+b": OpenMode.READ_WRITE, + "xb+b": OpenMode.READ_WRITE, +} + + +BUILTIN_CLASSSCOPES = { + "BaseException": ClassScope( + GlobalVariable( + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "BaseException", 859, 0), + "BaseException", + ), + [], + None, + { + "add_note": [ + ClassVariable( + astroid.FunctionDef(name="add_note", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "add_note", 861, 4), + "add_note", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "with_traceback": [ + ClassVariable( + astroid.FunctionDef(name="with_traceback", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "with_traceback", 868, 4), + "with_traceback", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__delattr__": [ + ClassVariable( + astroid.FunctionDef(name="__delattr__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__delattr__", 875, 4), + "__delattr__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__getattribute__": [ + ClassVariable( + astroid.FunctionDef(name="__getattribute__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__getattribute__", 879, 4), + "__getattribute__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 883, 4), + "__init__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 886, 4), + "__new__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__reduce__": [ + ClassVariable( + astroid.FunctionDef(name="__reduce__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__reduce__", 891, 4), + "__reduce__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__repr__": [ + ClassVariable( + astroid.FunctionDef(name="__repr__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__repr__", 894, 4), + "__repr__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__setattr__": [ + ClassVariable( + astroid.FunctionDef(name="__setattr__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__setattr__", 898, 4), + "__setattr__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__setstate__": [ + ClassVariable( + astroid.FunctionDef(name="__setstate__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__setstate__", 902, 4), + "__setstate__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 905, 4), + "__str__", + astroid.ClassDef(name="BaseException", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "Exception": ClassScope( + GlobalVariable(astroid.ClassDef(name="Exception", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "Exception", 925, 0), "Exception"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 927, 4), + "__init__", + astroid.ClassDef(name="Exception", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 930, 4), + "__new__", + astroid.ClassDef(name="Exception", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ArithmeticError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ArithmeticError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ArithmeticError", 936, 0), + "ArithmeticError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 938, 4), + "__init__", + astroid.ClassDef(name="ArithmeticError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 941, 4), + "__new__", + astroid.ClassDef(name="ArithmeticError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "AssertionError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="AssertionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "AssertionError", 947, 0), + "AssertionError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 949, 4), + "__init__", + astroid.ClassDef(name="AssertionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 952, 4), + "__new__", + astroid.ClassDef(name="AssertionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "AttributeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="AttributeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "AttributeError", 958, 0), + "AttributeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 960, 4), + "__init__", + astroid.ClassDef(name="AttributeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 963, 4), + "__str__", + astroid.ClassDef(name="AttributeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "BaseExceptionGroup": ClassScope( + GlobalVariable( + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "BaseExceptionGroup", 975, 0), + "BaseExceptionGroup", + ), + [], + None, + { + "derive": [ + ClassVariable( + astroid.FunctionDef(name="derive", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "derive", 977, 4), + "derive", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "split": [ + ClassVariable( + astroid.FunctionDef(name="split", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "split", 980, 4), + "split", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "subgroup": [ + ClassVariable( + astroid.FunctionDef(name="subgroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "subgroup", 983, 4), + "subgroup", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__class_getitem__": [ + ClassVariable( + astroid.FunctionDef(name="__class_getitem__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__class_getitem__", 986, 4), + "__class_getitem__", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 990, 4), + "__init__", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 993, 4), + "__new__", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 998, 4), + "__str__", + astroid.ClassDef(name="BaseExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "WindowsError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="WindowsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "WindowsError", 1010, 0), + "WindowsError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 1012, 4), + "__init__", + astroid.ClassDef(name="WindowsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 1015, 4), + "__new__", + astroid.ClassDef(name="WindowsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__reduce__": [ + ClassVariable( + astroid.FunctionDef(name="__reduce__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__reduce__", 1020, 4), + "__reduce__", + astroid.ClassDef(name="WindowsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 1023, 4), + "__str__", + astroid.ClassDef(name="WindowsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "BlockingIOError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="BlockingIOError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "BlockingIOError", 1055, 0), + "BlockingIOError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 1057, 4), + "__init__", + astroid.ClassDef(name="BlockingIOError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ConnectionError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ConnectionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ConnectionError", 1450, 0), + "ConnectionError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 1452, 4), + "__init__", + astroid.ClassDef(name="ConnectionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "BrokenPipeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="BrokenPipeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "BrokenPipeError", 1456, 0), + "BrokenPipeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 1458, 4), + "__init__", + astroid.ClassDef(name="BrokenPipeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "BufferError": ClassScope( + GlobalVariable(astroid.ClassDef(name="BufferError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "BufferError", 1462, 0), "BufferError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 1464, 4), + "__init__", + astroid.ClassDef(name="BufferError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 1467, 4), + "__new__", + astroid.ClassDef(name="BufferError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "Warning": ClassScope( + GlobalVariable(astroid.ClassDef(name="Warning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "Warning", 2688, 0), "Warning"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2690, 4), + "__init__", + astroid.ClassDef(name="Warning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 2693, 4), + "__new__", + astroid.ClassDef(name="Warning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "BytesWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="BytesWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "BytesWarning", 2699, 0), + "BytesWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2704, 4), + "__init__", + astroid.ClassDef(name="BytesWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 2707, 4), + "__new__", + astroid.ClassDef(name="BytesWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ChildProcessError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ChildProcessError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ChildProcessError", 2713, 0), + "ChildProcessError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2715, 4), + "__init__", + astroid.ClassDef(name="ChildProcessError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ConnectionAbortedError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ConnectionAbortedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ConnectionAbortedError", 2903, 0), + "ConnectionAbortedError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2905, 4), + "__init__", + astroid.ClassDef(name="ConnectionAbortedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ConnectionRefusedError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ConnectionRefusedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ConnectionRefusedError", 2909, 0), + "ConnectionRefusedError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2911, 4), + "__init__", + astroid.ClassDef(name="ConnectionRefusedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ConnectionResetError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ConnectionResetError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ConnectionResetError", 2915, 0), + "ConnectionResetError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2917, 4), + "__init__", + astroid.ClassDef(name="ConnectionResetError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "DeprecationWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="DeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "DeprecationWarning", 2921, 0), + "DeprecationWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 2923, 4), + "__init__", + astroid.ClassDef(name="DeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 2926, 4), + "__new__", + astroid.ClassDef(name="DeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "EncodingWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="EncodingWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "EncodingWarning", 3111, 0), + "EncodingWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3113, 4), + "__init__", + astroid.ClassDef(name="EncodingWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3116, 4), + "__new__", + astroid.ClassDef(name="EncodingWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "EOFError": ClassScope( + GlobalVariable(astroid.ClassDef(name="EOFError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "EOFError", 3165, 0), "EOFError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3167, 4), + "__init__", + astroid.ClassDef(name="EOFError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3170, 4), + "__new__", + astroid.ClassDef(name="EOFError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ExceptionGroup": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ExceptionGroup", 3176, 0), + "ExceptionGroup", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3178, 4), + "__init__", + astroid.ClassDef(name="ExceptionGroup", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "FileExistsError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="FileExistsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "FileExistsError", 3186, 0), + "FileExistsError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3188, 4), + "__init__", + astroid.ClassDef(name="FileExistsError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "FileNotFoundError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="FileNotFoundError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "FileNotFoundError", 3192, 0), + "FileNotFoundError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3194, 4), + "__init__", + astroid.ClassDef(name="FileNotFoundError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "FloatingPointError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="FloatingPointError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "FloatingPointError", 3463, 0), + "FloatingPointError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3465, 4), + "__init__", + astroid.ClassDef(name="FloatingPointError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3468, 4), + "__new__", + astroid.ClassDef(name="FloatingPointError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "FutureWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="FutureWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "FutureWarning", 3631, 0), + "FutureWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3636, 4), + "__init__", + astroid.ClassDef(name="FutureWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3639, 4), + "__new__", + astroid.ClassDef(name="FutureWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "GeneratorExit": ClassScope( + GlobalVariable( + astroid.ClassDef(name="GeneratorExit", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "GeneratorExit", 3645, 0), + "GeneratorExit", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3647, 4), + "__init__", + astroid.ClassDef(name="GeneratorExit", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3650, 4), + "__new__", + astroid.ClassDef(name="GeneratorExit", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ImportError": ClassScope( + GlobalVariable(astroid.ClassDef(name="ImportError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "ImportError", 3656, 0), "ImportError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3658, 4), + "__init__", + astroid.ClassDef(name="ImportError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__reduce__": [ + ClassVariable( + astroid.FunctionDef(name="__reduce__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__reduce__", 3661, 4), + "__reduce__", + astroid.ClassDef(name="ImportError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 3664, 4), + "__str__", + astroid.ClassDef(name="ImportError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ImportWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ImportWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ImportWarning", 3679, 0), + "ImportWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3681, 4), + "__init__", + astroid.ClassDef(name="ImportWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3684, 4), + "__new__", + astroid.ClassDef(name="ImportWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "SyntaxError": ClassScope( + GlobalVariable(astroid.ClassDef(name="SyntaxError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "SyntaxError", 3690, 0), "SyntaxError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3692, 4), + "__init__", + astroid.ClassDef(name="SyntaxError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 3695, 4), + "__str__", + astroid.ClassDef(name="SyntaxError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "IndentationError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="IndentationError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "IndentationError", 3725, 0), + "IndentationError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3727, 4), + "__init__", + astroid.ClassDef(name="IndentationError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "LookupError": ClassScope( + GlobalVariable(astroid.ClassDef(name="LookupError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "LookupError", 3731, 0), "LookupError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3733, 4), + "__init__", + astroid.ClassDef(name="LookupError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3736, 4), + "__new__", + astroid.ClassDef(name="LookupError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "IndexError": ClassScope( + GlobalVariable(astroid.ClassDef(name="IndexError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "IndexError", 3742, 0), "IndexError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3744, 4), + "__init__", + astroid.ClassDef(name="IndexError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3747, 4), + "__new__", + astroid.ClassDef(name="IndexError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "InterruptedError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="InterruptedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "InterruptedError", 3753, 0), + "InterruptedError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3755, 4), + "__init__", + astroid.ClassDef(name="InterruptedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "IsADirectoryError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="IsADirectoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "IsADirectoryError", 3759, 0), + "IsADirectoryError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3761, 4), + "__init__", + astroid.ClassDef(name="IsADirectoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "KeyboardInterrupt": ClassScope( + GlobalVariable( + astroid.ClassDef(name="KeyboardInterrupt", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "KeyboardInterrupt", 3765, 0), + "KeyboardInterrupt", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3767, 4), + "__init__", + astroid.ClassDef(name="KeyboardInterrupt", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 3770, 4), + "__new__", + astroid.ClassDef(name="KeyboardInterrupt", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "KeyError": ClassScope( + GlobalVariable(astroid.ClassDef(name="KeyError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "KeyError", 3776, 0), "KeyError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3778, 4), + "__init__", + astroid.ClassDef(name="KeyError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 3781, 4), + "__str__", + astroid.ClassDef(name="KeyError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "MemoryError": ClassScope( + GlobalVariable(astroid.ClassDef(name="MemoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "MemoryError", 3997, 0), "MemoryError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 3999, 4), + "__init__", + astroid.ClassDef(name="MemoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4002, 4), + "__new__", + astroid.ClassDef(name="MemoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ModuleNotFoundError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ModuleNotFoundError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ModuleNotFoundError", 4174, 0), + "ModuleNotFoundError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4176, 4), + "__init__", + astroid.ClassDef(name="ModuleNotFoundError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "NameError": ClassScope( + GlobalVariable(astroid.ClassDef(name="NameError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "NameError", 4180, 0), "NameError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4182, 4), + "__init__", + astroid.ClassDef(name="NameError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 4185, 4), + "__str__", + astroid.ClassDef(name="NameError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "NotADirectoryError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="NotADirectoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "NotADirectoryError", 4194, 0), + "NotADirectoryError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4196, 4), + "__init__", + astroid.ClassDef(name="NotADirectoryError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "RuntimeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="RuntimeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "RuntimeError", 4200, 0), + "RuntimeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4202, 4), + "__init__", + astroid.ClassDef(name="RuntimeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4205, 4), + "__new__", + astroid.ClassDef(name="RuntimeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "NotImplementedError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="NotImplementedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "NotImplementedError", 4211, 0), + "NotImplementedError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4213, 4), + "__init__", + astroid.ClassDef(name="NotImplementedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4216, 4), + "__new__", + astroid.ClassDef(name="NotImplementedError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "OverflowError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="OverflowError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "OverflowError", 4222, 0), + "OverflowError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4224, 4), + "__init__", + astroid.ClassDef(name="OverflowError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4227, 4), + "__new__", + astroid.ClassDef(name="OverflowError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "PendingDeprecationWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="PendingDeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "PendingDeprecationWarning", 4233, 0), + "PendingDeprecationWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4238, 4), + "__init__", + astroid.ClassDef(name="PendingDeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4241, 4), + "__new__", + astroid.ClassDef(name="PendingDeprecationWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "PermissionError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="PermissionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "PermissionError", 4247, 0), + "PermissionError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4249, 4), + "__init__", + astroid.ClassDef(name="PermissionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ProcessLookupError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ProcessLookupError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ProcessLookupError", 4253, 0), + "ProcessLookupError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4255, 4), + "__init__", + astroid.ClassDef(name="ProcessLookupError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "RecursionError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="RecursionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "RecursionError", 4480, 0), + "RecursionError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4482, 4), + "__init__", + astroid.ClassDef(name="RecursionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4485, 4), + "__new__", + astroid.ClassDef(name="RecursionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ReferenceError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ReferenceError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ReferenceError", 4491, 0), + "ReferenceError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4493, 4), + "__init__", + astroid.ClassDef(name="ReferenceError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4496, 4), + "__new__", + astroid.ClassDef(name="ReferenceError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ResourceWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ResourceWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ResourceWarning", 4502, 0), + "ResourceWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4504, 4), + "__init__", + astroid.ClassDef(name="ResourceWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4507, 4), + "__new__", + astroid.ClassDef(name="ResourceWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "RuntimeWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="RuntimeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "RuntimeWarning", 4548, 0), + "RuntimeWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4550, 4), + "__init__", + astroid.ClassDef(name="RuntimeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4553, 4), + "__new__", + astroid.ClassDef(name="RuntimeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "StopAsyncIteration": ClassScope( + GlobalVariable( + astroid.ClassDef(name="StopAsyncIteration", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "StopAsyncIteration", 4914, 0), + "StopAsyncIteration", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4916, 4), + "__init__", + astroid.ClassDef(name="StopAsyncIteration", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 4919, 4), + "__new__", + astroid.ClassDef(name="StopAsyncIteration", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "StopIteration": ClassScope( + GlobalVariable( + astroid.ClassDef(name="StopIteration", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "StopIteration", 4925, 0), + "StopIteration", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 4927, 4), + "__init__", + astroid.ClassDef(name="StopIteration", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "SyntaxWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="SyntaxWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "SyntaxWarning", 5593, 0), + "SyntaxWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5595, 4), + "__init__", + astroid.ClassDef(name="SyntaxWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5598, 4), + "__new__", + astroid.ClassDef(name="SyntaxWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "SystemError": ClassScope( + GlobalVariable(astroid.ClassDef(name="SystemError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "SystemError", 5604, 0), "SystemError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5611, 4), + "__init__", + astroid.ClassDef(name="SystemError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5614, 4), + "__new__", + astroid.ClassDef(name="SystemError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "SystemExit": ClassScope( + GlobalVariable(astroid.ClassDef(name="SystemExit", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "SystemExit", 5620, 0), "SystemExit"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5622, 4), + "__init__", + astroid.ClassDef(name="SystemExit", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "TabError": ClassScope( + GlobalVariable(astroid.ClassDef(name="TabError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "TabError", 5630, 0), "TabError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5632, 4), + "__init__", + astroid.ClassDef(name="TabError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "TimeoutError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="TimeoutError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "TimeoutError", 5636, 0), + "TimeoutError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5638, 4), + "__init__", + astroid.ClassDef(name="TimeoutError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "TypeError": ClassScope( + GlobalVariable(astroid.ClassDef(name="TypeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "TypeError", 5853, 0), "TypeError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5855, 4), + "__init__", + astroid.ClassDef(name="TypeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5858, 4), + "__new__", + astroid.ClassDef(name="TypeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnboundLocalError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnboundLocalError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnboundLocalError", 5864, 0), + "UnboundLocalError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5866, 4), + "__init__", + astroid.ClassDef(name="UnboundLocalError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ValueError": ClassScope( + GlobalVariable(astroid.ClassDef(name="ValueError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "ValueError", 5870, 0), "ValueError"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5872, 4), + "__init__", + astroid.ClassDef(name="ValueError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5875, 4), + "__new__", + astroid.ClassDef(name="ValueError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnicodeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnicodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnicodeError", 5881, 0), + "UnicodeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5883, 4), + "__init__", + astroid.ClassDef(name="UnicodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5886, 4), + "__new__", + astroid.ClassDef(name="UnicodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnicodeDecodeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnicodeDecodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnicodeDecodeError", 5892, 0), + "UnicodeDecodeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5894, 4), + "__init__", + astroid.ClassDef(name="UnicodeDecodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5897, 4), + "__new__", + astroid.ClassDef(name="UnicodeDecodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 5902, 4), + "__str__", + astroid.ClassDef(name="UnicodeDecodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnicodeEncodeError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnicodeEncodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnicodeEncodeError", 5923, 0), + "UnicodeEncodeError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5925, 4), + "__init__", + astroid.ClassDef(name="UnicodeEncodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5928, 4), + "__new__", + astroid.ClassDef(name="UnicodeEncodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 5933, 4), + "__str__", + astroid.ClassDef(name="UnicodeEncodeError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnicodeTranslateError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnicodeTranslateError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnicodeTranslateError", 5954, 0), + "UnicodeTranslateError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5956, 4), + "__init__", + astroid.ClassDef(name="UnicodeTranslateError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5959, 4), + "__new__", + astroid.ClassDef(name="UnicodeTranslateError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__str__": [ + ClassVariable( + astroid.FunctionDef(name="__str__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__str__", 5964, 4), + "__str__", + astroid.ClassDef(name="UnicodeTranslateError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UnicodeWarning": ClassScope( + GlobalVariable( + astroid.ClassDef(name="UnicodeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "UnicodeWarning", 5985, 0), + "UnicodeWarning", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 5990, 4), + "__init__", + astroid.ClassDef(name="UnicodeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 5993, 4), + "__new__", + astroid.ClassDef(name="UnicodeWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "UserWarning": ClassScope( + GlobalVariable(astroid.ClassDef(name="UserWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), NodeID("BUILTIN", "UserWarning", 5999, 0), "UserWarning"), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 6001, 4), + "__init__", + astroid.ClassDef(name="UserWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 6004, 4), + "__new__", + astroid.ClassDef(name="UserWarning", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), + "ZeroDivisionError": ClassScope( + GlobalVariable( + astroid.ClassDef(name="ZeroDivisionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "ZeroDivisionError", 6010, 0), + "ZeroDivisionError", + ), + [], + None, + { + "__init__": [ + ClassVariable( + astroid.FunctionDef(name="__init__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__init__", 6012, 4), + "__init__", + astroid.ClassDef(name="ZeroDivisionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + "__new__": [ + ClassVariable( + astroid.FunctionDef(name="__new__", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + NodeID("BUILTIN", "__new__", 6015, 4), + "__new__", + astroid.ClassDef(name="ZeroDivisionError", col_offset=0, lineno=0, parent=None, end_col_offset=0, end_lineno=0), + ), + ], + }, + ), +} + +BUILTIN_SPECIALS = { + "get": Pure(), # dict + "update": Pure(), # dict, set + "pop": Pure(), # dict, list, set + "popitem": Pure(), # dict + "clear": Pure(), # dict, # list, set + "copy": Pure(), # dict, # list, set + "fromkeys": Pure(), # dict + "items": Pure(), # dict + "keys": Pure(), # dict + "values": Pure(), # dict + "setdefault": Pure(), # dict + "append": Pure(), # list + "count": Pure(), # list, str + "extend": Pure(), # list + "index": Pure(), # list, str + "insert": Pure(), # list + "remove": Pure(), # list, set + "reverse": Pure(), # list + "sort": Pure(), # list + "add": Pure(), # set + "difference": Pure(), # set + "difference_update": Pure(), # set + "discard": Pure(), # set + "intersection": Pure(), # set + "intersection_update": Pure(), # set + "isdisjoint": Pure(), # set + "issubset": Pure(), # set + "issuperset": Pure(), # set + "symmetric_difference": Pure(), # set + "symmetric_difference_update": Pure(), # set + "union": Pure(), # set + "capitalize": Pure(), # str + "casefold": Pure(), # str + "center": Pure(), # str + "encode": Pure(), # str + "endswith": Pure(), # str + "expandtabs": Pure(), # str + "find": Pure(), # str + "format": Pure(), # str + "format_map": Pure(), # str + "isalnum": Pure(), # str + "isalpha": Pure(), # str + "isascii": Pure(), # str + "isdecimal": Pure(), # str + "isdigit": Pure(), # str + "isidentifier": Pure(), # str + "islower": Pure(), # str + "isnumeric": Pure(), # str + "isprintable": Pure(), # str + "isspace": Pure(), # str + "istitle": Pure(), # str + "isupper": Pure(), # str + "join": Pure(), # str + "ljust": Pure(), # str + "lower": Pure(), # str + "lstrip": Pure(), # str + "maketrans": Pure(), # str + "partition": Pure(), # str + "removeprefix": Pure(), # str + "removesuffix": Pure(), # str + "replace": Pure(), # str + "rfind": Pure(), # str + "rindex": Pure(), # str + "rjust": Pure(), # str + "rpartition": Pure(), # str + "rsplit": Pure(), # str + "rstrip": Pure(), # str + "split": Pure(), # str + "splitlines": Pure(), # str + "startswith": Pure(), # str + "strip": Pure(), # str + "swapcase": Pure(), # str + "title": Pure(), # str + "translate": Pure(), # str + "upper": Pure(), # str + "zfill": Pure(), # str + "GenericAlias": Pure(), + "UnionType": Pure(), + "EllipsisType": Pure(), + "NoneType": Pure(), + "NotImplementedType": Pure(), +} diff --git a/src/safeds_stubgen/api_analyzer/purity_analysis/model/_reference.py b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_reference.py new file mode 100644 index 00000000..d07d3f27 --- /dev/null +++ b/src/safeds_stubgen/api_analyzer/purity_analysis/model/_reference.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +import astroid + +from safeds_stubgen.api_analyzer.purity_analysis.model._module_data import ( + ClassScope, + FunctionScope, + MemberAccessTarget, + MemberAccessValue, + NodeID, + Reference, + Scope, + Symbol, +) + +if TYPE_CHECKING: + + from safeds_stubgen.api_analyzer.purity_analysis.model import ( + CallGraphForest, + NonLocalVariableRead, + NonLocalVariableWrite, + PurityResult, + UnknownProto, + ) + + +@dataclass +class ReferenceNode(ABC): + """Class for reference nodes. + + A reference node represents a reference to a list of its referenced symbols. + + + Attributes + ---------- + node : + The node that references the symbols. + scope : + The scope of the node. + referenced_symbols : + The list of referenced symbols. + These are the symbols of the nodes that node references. + """ + + node: Symbol | Reference + scope: Scope + referenced_symbols: list[Symbol] = field(default_factory=list) + + def __repr__(self) -> str: # pragma: no cover + if isinstance(self.node, astroid.Call) and isinstance(self.node.func, astroid.Name): + return f"{self.node.func.name}.line{self.node.lineno}" + if isinstance(self.node, MemberAccessTarget | MemberAccessValue): + return f"{self.node.name}.line{self.node.node.lineno}" + return f"{self.node.name}.line{self.node.node.lineno}" + + +@dataclass +class TargetReference(ReferenceNode): + """Class for target reference nodes. + + A TargetReference represents a reference from a target (=Symbol) to a list of Symbols. + This is used to represent a Reference from a reassignment to the original assignment + (or another previous assignment) of the same variable. + """ + + node: Symbol + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + +@dataclass +class ValueReference(ReferenceNode): + """Class for value reference nodes. + + A ValueReference represents a reference from a value to a list of Symbols. + This is used to represent a reference from a function call to the function definition. + """ + + node: Reference + + def __hash__(self) -> int: + return hash(str(self)) # pragma: no cover + + +@dataclass +class ModuleAnalysisResult: + """Class for module analysis results. + + After the references of a module have been resolved, all necessary information for the purity analysis is available in this class. + + Attributes + ---------- + resolved_references : + The dictionary of references. + The key is the name of the reference node, the value is the list of ReferenceNodes. + raw_reasons : + The dictionary of function references. + The key is the NodeID of the function, the value is the Reasons for the function. + classes : + All classes and their ClassScope. + call_graph_forest : CallGraphForest + The call graph forest of the module. + module_id : + The NodeID of the module which the analysis result belongs to. + """ + + resolved_references: dict[str, list[ReferenceNode]] = field(default_factory=dict) + raw_reasons: dict[NodeID, Reasons] = field(default_factory=dict) + classes: dict[str, ClassScope] = field(default_factory=dict) + call_graph_forest: CallGraphForest | None = None + module_id: NodeID | None = None + + +@dataclass +class Reasons: + """ + Represents a function and the raw reasons for impurity. + + Raw reasons means that the reasons are just collected and not yet processed. + + Attributes + ---------- + function_scope : + The scope of the function which the reasons belong to. + Is None if the reasons are not for a FunctionDef node. + This is the case when either a builtin or a combined node is created, + or a ClassScope is used to propagate reasons. + writes_to : + A dict of all nodes that are written to. + reads_from : + A dict of all nodes that are read from. + calls : + A set of all nodes that are called. + result : + The result of the purity analysis + This also works as a flag to determine if the purity analysis has already been performed: + If it is None, the purity analysis has not been performed + unknown_calls : + A dict of all unknown calls. + Unknown calls are calls to functions that are not defined in the module or are parameters. + """ + + id: NodeID + function_scope: FunctionScope | None = field(default=None) + writes_to: dict[NodeID, NonLocalVariableWrite] = field(default_factory=dict) + reads_from: dict[NodeID, NonLocalVariableRead] = field(default_factory=dict) + calls: set[Symbol] = field(default_factory=set) # TODO: SORTED SET oder LIST + result: PurityResult | None = field(default=None) + unknown_calls: dict[NodeID, UnknownProto] = field(default_factory=dict) + + def join_reasons_list(self, reasons_list: list[Reasons]) -> Reasons: + """Join a list of Reasons objects. + + Combines a list of Reasons objects into one Reasons object. + + Parameters + ---------- + reasons_list : + The list of Reasons objects. + + + Returns + ------- + Reasons + The combined Reasons object. + + Raises + ------ + ValueError + If the list of Reasons objects is empty. + """ + if not reasons_list: + raise ValueError("List of Reasons is empty.") # pragma: no cover + + result = self + for reason in reasons_list: + result.join_reasons(reason) + return result + + def join_reasons(self, other: Reasons) -> Reasons: + """Join two Reasons objects. + + When a function has multiple reasons for impurity, the Reasons objects are joined. + This means that the writes, reads, calls and unknown_calls are merged. + + Parameters + ---------- + other : + The other Reasons object. + + Returns + ------- + Reasons + The updated Reasons object. + """ + self.writes_to.update(other.writes_to) + self.reads_from.update(other.reads_from) + self.calls.update(other.calls) + self.unknown_calls.update(other.unknown_calls) + + return self + + def remove_unknown_call(self, node_id: NodeID) -> None: + """Remove an unknown call from the reasons. + + Parameters + ---------- + node_id : + The NodeID of the unknown call to remove. + """ + del self.unknown_calls[node_id] diff --git a/src/safeds_stubgen/docstring_parsing/_create_docstring_parser.py b/src/safeds_stubgen/docstring_parsing/_create_docstring_parser.py index f9d034a1..ad451329 100644 --- a/src/safeds_stubgen/docstring_parsing/_create_docstring_parser.py +++ b/src/safeds_stubgen/docstring_parsing/_create_docstring_parser.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from griffe.enumerations import Parser +from griffe import Parser from ._docstring_parser import DocstringParser from ._docstring_style import DocstringStyle @@ -15,6 +15,7 @@ def create_docstring_parser(style: DocstringStyle, package_path: Path) -> AbstractDocstringParser: + # pm: Whats about EpyDoc? if style == DocstringStyle.GOOGLE: return DocstringParser(parser=Parser.google, package_path=package_path) elif style == DocstringStyle.NUMPYDOC: diff --git a/src/safeds_stubgen/docstring_parsing/_docstring.py b/src/safeds_stubgen/docstring_parsing/_docstring.py index b1ad83af..7492e493 100644 --- a/src/safeds_stubgen/docstring_parsing/_docstring.py +++ b/src/safeds_stubgen/docstring_parsing/_docstring.py @@ -3,6 +3,9 @@ import dataclasses from dataclasses import dataclass from typing import TYPE_CHECKING +from safeds_stubgen.api_analyzer._extract_boundary_values import extract_boundary +from safeds_stubgen.api_analyzer._extract_valid_values import extract_valid_literals +from safeds_stubgen.api_analyzer._types import BoundaryType if TYPE_CHECKING: from typing import Any @@ -30,23 +33,65 @@ def to_dict(self) -> dict[str, Any]: return dataclasses.asdict(self) -@dataclass(frozen=True) +@dataclass(unsafe_hash=True) # unsafe hash so that we wont have to call the extractors before init class ParameterDocstring: type: AbstractType | None = None default_value: str = "" + type_string: str = "" description: str = "" - - def to_dict(self) -> dict[str, Any]: - return dataclasses.asdict(self) - - -@dataclass(frozen=True) + boundaries: frozenset[BoundaryType] | None = None + valid_values: frozenset[str] | None = None + + def __init__(self, type: AbstractType | None = None, default_value: str = "", type_string: str = "", description: str = ""): + self.type = type + self.default_value = default_value + self.type_string = type_string + self.description = description + self.boundaries = frozenset(extract_boundary(description, type_string)) + self.valid_values = frozenset(extract_valid_literals(description, type_string)) + + def to_dict(self) -> dict[str, Any]: # custom to dict function, as sets are not JSON serializable + boundaries = list(self.boundaries) if self.boundaries is not None else None + valid_values = sorted(self.valid_values) if self.valid_values is not None else None + type = self.type.to_dict() if self.type is not None else None + return { + "type": type, + "default_value": self.default_value, + "description": self.description, + "boundaries": sorted( + map( + lambda boundary: boundary.to_dict(), + boundaries if boundaries else [] + ), + key=(lambda boundary_dict: str(boundary_dict["min"]) + str(boundary_dict["max"])) + ), + "valid_values": valid_values + } + + +@dataclass(unsafe_hash=True) class AttributeDocstring: type: AbstractType | None = None + type_string: str = "" description: str = "" - def to_dict(self) -> dict[str, Any]: - return dataclasses.asdict(self) + def __init__(self, type: AbstractType | None = None, default_value: str = "", type_string: str = "", description: str = ""): + self.type = type + self.type_string = type_string + self.description = description + self.boundaries = frozenset(extract_boundary(description, type_string)) + self.valid_values = frozenset(extract_valid_literals(description, type_string)) + + def to_dict(self) -> dict[str, Any]: # custom to dict function, as sets are not JSON serializable + boundaries = list(self.boundaries) if self.boundaries is not None else None + valid_values = sorted(self.valid_values) if self.valid_values is not None else None + type = self.type.to_dict() if self.type is not None else None + return { + "type": type, + "description": self.description, + "boundaries": sorted(map(lambda boundary: boundary.to_dict(), boundaries), key=(lambda boundary_dict: str(boundary_dict["min"]) + str(boundary_dict["max"]))), + "valid_values": valid_values + } @dataclass(frozen=True) diff --git a/src/safeds_stubgen/docstring_parsing/_docstring_parser.py b/src/safeds_stubgen/docstring_parsing/_docstring_parser.py index 87a06b8e..827dd68b 100644 --- a/src/safeds_stubgen/docstring_parsing/_docstring_parser.py +++ b/src/safeds_stubgen/docstring_parsing/_docstring_parser.py @@ -3,12 +3,12 @@ import logging from typing import TYPE_CHECKING, Literal +from _griffe import models as griffe_models from griffe import load -from griffe.dataclasses import Docstring -from griffe.docstrings.dataclasses import DocstringAttribute, DocstringParameter -from griffe.docstrings.utils import parse_annotation -from griffe.enumerations import DocstringSectionKind, Parser -from griffe.expressions import Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprList, ExprName, ExprSubscript, ExprTuple +from griffe import DocstringAttribute, DocstringParameter +from griffe import parse_docstring_annotation +from griffe import DocstringSectionKind, Parser +from griffe import Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprList, ExprName, ExprSubscript, ExprTuple # noinspection PyProtectedMember import safeds_stubgen.api_analyzer._types as sds_types @@ -25,34 +25,49 @@ if TYPE_CHECKING: from pathlib import Path - from griffe.dataclasses import Object from mypy import nodes class DocstringParser(AbstractDocstringParser): def __init__(self, parser: Parser, package_path: Path): + self.parser = parser + while True: # If a package has no __init__.py file Griffe can't parse it, therefore we check the parent try: - self.griffe_build = load(package_path, docstring_parser=parser) + griffe_build = load(package_path, docstring_parser=parser) break except KeyError: package_path = package_path.parent - self.parser = parser - self.__cached_node: str | None = None - self.__cached_docstring: Docstring | None = None + self.griffe_index: dict[str, griffe_models.Object] = {} + self._recursive_griffe_indexer(griffe_build) + + def _recursive_griffe_indexer(self, griffe_build: griffe_models.Object | griffe_models.Alias) -> None: + for member in griffe_build.all_members.values(): + if isinstance( + member, + griffe_models.Class | griffe_models.Function | griffe_models.Attribute | griffe_models.Alias, + ): + self.griffe_index[member.path] = member + + if isinstance(member, griffe_models.Module | griffe_models.Class): + self._recursive_griffe_indexer(member) def get_class_documentation(self, class_node: nodes.ClassDef) -> ClassDocstring: - griffe_node = self._get_griffe_node(class_node.fullname) + griffe_node = self.griffe_index.get(class_node.fullname, None) if griffe_node is None: # pragma: no cover - raise TypeError(f"Expected a griffe node for {class_node.fullname}, got None.") + msg = ( + f"Something went wrong while searching for the docstring for {class_node.fullname}. Please make sure" + " that all directories with python files have an __init__.py file." + ) + logging.warning(msg) description = "" docstring = "" examples = [] - if griffe_node.docstring is not None: + if griffe_node is not None and griffe_node.docstring is not None: docstring = griffe_node.docstring.value.strip("\n") try: @@ -76,7 +91,7 @@ def get_function_documentation(self, function_node: nodes.FuncDef) -> FunctionDo docstring = "" description = "" examples = [] - griffe_docstring = self.__get_cached_docstring(function_node.fullname) + griffe_docstring = self._get_griffe_docstring(function_node.fullname) if griffe_docstring is not None: docstring = griffe_docstring.value.strip("\n") @@ -110,9 +125,9 @@ def get_parameter_documentation( # For constructors (__init__ functions) the parameters are described on the class if function_name == "__init__" and parent_class_qname: parent_qname = parent_class_qname.replace("/", ".") - griffe_docstring = self.__get_cached_docstring(parent_qname) + griffe_docstring = self._get_griffe_docstring(parent_qname) else: - griffe_docstring = self.__get_cached_docstring(function_qname) + griffe_docstring = self._get_griffe_docstring(function_qname) # Find matching parameter docstrings matching_parameters = [] @@ -123,7 +138,7 @@ def get_parameter_documentation( # https://github.com/Safe-DS/Library-Analyzer/issues/10) if self.parser == Parser.numpy and len(matching_parameters) == 0 and function_name == "__init__": # Get constructor docstring & find matching parameter docstrings - constructor_docstring = self.__get_cached_docstring(function_qname) + constructor_docstring = self._get_griffe_docstring(function_qname) if constructor_docstring is not None: matching_parameters = self._get_matching_docstrings(constructor_docstring, parameter_name, "param") @@ -136,7 +151,7 @@ def get_parameter_documentation( raise TypeError(f"Expected parameter docstring, got {type(last_parameter)}.") if griffe_docstring is None: # pragma: no cover - griffe_docstring = Docstring("") + griffe_docstring = griffe_models.Docstring("") annotation = last_parameter.annotation if annotation is None: @@ -148,25 +163,27 @@ def get_parameter_documentation( if last_parameter.default: default_value = str(last_parameter.default) + type_string = "" + if isinstance(last_parameter.annotation, Expr): + type_string = last_parameter.annotation.canonical_name + if isinstance(last_parameter.annotation, str): + type_string = last_parameter.annotation return ParameterDocstring( - type=type_, + type=type_, # type from docstring default_value=default_value, + type_string=type_string, description=last_parameter.description.strip("\n") or "", ) - def get_attribute_documentation( - self, - parent_class_qname: str, - attribute_name: str, - ) -> AttributeDocstring: + def get_attribute_documentation(self, parent_class_qname: str, attribute_name: str) -> AttributeDocstring: parent_class_qname = parent_class_qname.replace("/", ".") # Find matching attribute docstrings parent_qname = parent_class_qname - griffe_docstring = self.__get_cached_docstring(parent_qname) + griffe_docstring = self._get_griffe_docstring(parent_qname) if griffe_docstring is None: matching_attributes = [] - griffe_docstring = Docstring("") + griffe_docstring = griffe_models.Docstring("") else: matching_attributes = self._get_matching_docstrings(griffe_docstring, attribute_name, "attr") @@ -174,7 +191,7 @@ def get_attribute_documentation( # (see issue https://github.com/Safe-DS/Library-Analyzer/issues/10) if self.parser == Parser.numpy and len(matching_attributes) == 0: constructor_qname = f"{parent_class_qname}.__init__" - constructor_docstring = self.__get_cached_docstring(constructor_qname) + constructor_docstring = self._get_griffe_docstring(constructor_qname) # Find matching parameter docstrings if constructor_docstring is not None: @@ -190,15 +207,21 @@ def get_attribute_documentation( type_ = None else: type_ = self._griffe_annotation_to_api_type(annotation, griffe_docstring) - + + type_string = "" + if isinstance(last_attribute.annotation, Expr): + type_string = last_attribute.annotation.canonical_name + if isinstance(last_attribute.annotation, str): + type_string = last_attribute.annotation return AttributeDocstring( type=type_, + type_string=type_string, description=last_attribute.description.strip("\n"), ) def get_result_documentation(self, function_qname: str) -> list[ResultDocstring]: # Find matching parameter docstrings - griffe_docstring = self.__get_cached_docstring(function_qname) + griffe_docstring = self._get_griffe_docstring(function_qname) if griffe_docstring is None: return [] @@ -251,7 +274,7 @@ def get_result_documentation(self, function_qname: str) -> list[ResultDocstring] @staticmethod def _get_matching_docstrings( - function_doc: Docstring, + function_doc: griffe_models.Docstring, name: str, type_: Literal["attr", "param"], ) -> list[DocstringAttribute | DocstringParameter]: @@ -278,7 +301,7 @@ def _get_matching_docstrings( def _griffe_annotation_to_api_type( self, annotation: Expr | str, - docstring: Docstring, + docstring: griffe_models.Docstring, ) -> sds_types.AbstractType | None: if isinstance(annotation, ExprName | ExprAttribute): if annotation.canonical_path == "typing.Any": @@ -291,6 +314,8 @@ def _griffe_annotation_to_api_type( return sds_types.NamedType(name="float", qname="builtins.float") elif annotation.canonical_path == "str": return sds_types.NamedType(name="str", qname="builtins.str") + elif annotation.canonical_path == "bytes": + return sds_types.NamedType(name="bytes", qname="builtins.bytes") elif annotation.canonical_path == "list": return sds_types.ListType(types=[]) elif annotation.canonical_path == "tuple": @@ -371,7 +396,7 @@ def _griffe_annotation_to_api_type( return sds_types.TupleType(elements) elif isinstance(annotation, str): new_annotation = self._remove_default_from_griffe_annotation(annotation) - parsed_annotation = parse_annotation(new_annotation, docstring) + parsed_annotation = parse_docstring_annotation(new_annotation, docstring) if parsed_annotation in (new_annotation, annotation): if parsed_annotation == "None": return sds_types.NamedType(name="None", qname="builtins.None") @@ -403,49 +428,15 @@ def _remove_default_from_griffe_annotation(self, annotation: str) -> str: return annotation.split(", default")[0] return annotation - def _get_griffe_node(self, qname: str) -> Object | None: - node_qname_parts = qname.split(".") - griffe_node = self.griffe_build - for part in node_qname_parts: - if part in griffe_node.modules: - griffe_node = griffe_node.modules[part] - elif part in griffe_node.classes: - griffe_node = griffe_node.classes[part] - elif part in griffe_node.functions: - griffe_node = griffe_node.functions[part] - elif part in griffe_node.attributes: - griffe_node = griffe_node.attributes[part] - elif part == "__init__" and griffe_node.is_class: - return None - elif griffe_node.name == part: - continue - else: # pragma: no cover - msg = ( - f"Something went wrong while searching for the docstring for {qname}. Please make sure" - " that all directories with python files have an __init__.py file.", - ) - logging.warning(msg) - - return griffe_node - - def __get_cached_docstring(self, qname: str) -> Docstring | None: - """ - Return the Docstring for the given function node. + def _get_griffe_docstring(self, qname: str) -> griffe_models.Docstring | None: + griffe_node = self.griffe_index.get(qname, None) - It is only recomputed when the function node differs from the previous one that was passed to this function. - This avoids reparsing the docstring for the function itself and all of its parameters. + if griffe_node is not None: + return griffe_node.docstring - On Lars's system this caused a significant performance improvement: Previously, 8.382s were spent inside the - function get_parameter_documentation when parsing sklearn. Afterward, it was only 2.113s. - """ - if self.__cached_node != qname or qname.endswith("__init__"): - self.__cached_node = qname - - griffe_node = self._get_griffe_node(qname) - if griffe_node is not None: - griffe_docstring = griffe_node.docstring - self.__cached_docstring = griffe_docstring - else: - self.__cached_docstring = None - - return self.__cached_docstring + msg = ( + f"Something went wrong while searching for the docstring for {qname}. Please make sure" + " that all directories with python files have an __init__.py file.", + ) + logging.warning(msg) + return None diff --git a/src/safeds_stubgen/stubs_generator/_stub_string_generator.py b/src/safeds_stubgen/stubs_generator/_stub_string_generator.py index 8fd717a6..cc18f776 100644 --- a/src/safeds_stubgen/stubs_generator/_stub_string_generator.py +++ b/src/safeds_stubgen/stubs_generator/_stub_string_generator.py @@ -4,7 +4,7 @@ from collections import defaultdict from pathlib import Path from types import NoneType -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from safeds_stubgen import is_internal from safeds_stubgen.api_analyzer import ( @@ -22,6 +22,8 @@ VarianceKind, result_name_generator, ) +from safeds_stubgen.api_analyzer.purity_analysis.model._module_data import NodeID +from safeds_stubgen.api_analyzer.purity_analysis.model._purity import APIPurity, Impure, PurityResult from safeds_stubgen.docstring_parsing import AttributeDocstring from ._helper import ( @@ -45,25 +47,51 @@ class StubsStringGenerator: method. """ - def __init__(self, api: API, convert_identifiers: bool) -> None: + def __init__(self, api: API, purity_api: APIPurity, convert_identifiers: bool) -> None: self.module_id: str = "" self.class_generics: list = [] self.module_imports: set[str] = set() self.currently_creating_reexport_data: bool = False + self.import_index: dict[str, str] = {} - self.api = api + self.api: API = api + self.purity_api = purity_api self.naming_convention = NamingConvention.SAFE_DS if convert_identifiers else NamingConvention.PYTHON self.classes_outside_package: set[str] = set() self.reexport_modules: dict[str, list[Class | Function]] = defaultdict(list) + self.purity_api = purity_api + def __call__(self, module: Module) -> tuple[str, str]: self._set_module_id(module.id) self.reexport_module_id = "" self.class_generics = [] self.module_imports = set() + # narrow purity dict TODO + purity_dict = self.purity_api.to_dict() + module_path = self.module_id.split("/") + package_name = self.api.package + index_to_split = module_path.index(package_name) + module_path = module_path[index_to_split:] + self.module_path = ".".join(module_path) + self.module_purity_data: dict[str, PurityResult] = purity_dict[self.module_path] + self._current_todo_msgs: set[str] = set() return self._create_module_string(module) + + def _create_function_purity_id(self, function: Function) -> NodeID: + full_id = function.id.split("/") + id = full_id[-1] + nodeId = NodeID(function.module_id_which_contains_def, id, function.line, function.column) + return nodeId + + def _get_purity_result(self, function: Function) -> PurityResult: + function_id = self._create_function_purity_id(function) + module_node_id = NodeID(None, function.module_id_which_contains_def) + purity_dict = self.purity_api.purity_results[module_node_id] + purity_data = purity_dict[function_id] + return purity_data def create_reexport_module_strings(self, out_path: Path) -> list[tuple[Path, str, str, bool]]: module_data = [] @@ -97,7 +125,8 @@ def create_reexport_module_strings(self, out_path: Path) -> list[tuple[Path, str if isinstance(element, Class): module_text = f"\n{self._create_class_string(class_=element, in_reexport_module=True)}\n" elif isinstance(element, Function): - module_text = f"\n{self._create_function_string(function=element, in_reexport_module=True)}\n" + purity_result = self._get_purity_result(element) # TODO for class methods, the module can be different, as a class can inherit methods so they are from a different module, so I cant use the module path here + module_text = f"\n{self._create_function_string(function=element, purity_result=purity_result, in_reexport_module=True)}\n" else: # pragma: no cover msg = f"Could not create a module for {element.id}. Unsupported type {type(element)}." logging.warning(msg) @@ -141,8 +170,10 @@ def _create_module_string(self, module: Module) -> tuple[str, str]: # Create global functions and properties for function in module.global_functions: if function.is_public: + purity_result = self._get_purity_result(function) # TODO for class methods, the module can be different, as a class can inherit methods so they are from a different module, so I cant use the module path here function_string = self._create_function_string( function=function, + purity_result=purity_result, is_method=False, in_reexport_module=in_reexport_module, ) @@ -199,14 +230,15 @@ def _create_class_string(self, class_: Class, class_indentation: str = "", in_re else: constructor = class_.constructor parameter_info = "" + parameter_boundaries = "" if constructor: - parameter_info = self._create_parameter_string( + parameter_info, parameter_boundaries = self._create_parameter_string( constructor.parameters, class_indentation, is_instance_method=True, ) - constructor_info = f"({parameter_info})" + constructor_info = f"({parameter_info}){parameter_boundaries}" # Type parameters constraints_info = "" @@ -254,7 +286,7 @@ def _create_class_string(self, class_: Class, class_indentation: str = "", in_re class_signature_todo = self._create_todo_msg(class_indentation) # Attributes - class_text, added_class_attributes = self._create_class_attribute_string(class_.attributes, inner_indentations) + class_text, added_class_attributes, attribute_boundaries = self._create_class_attribute_string(class_.attributes, inner_indentations) # Inner classes for inner_class in class_.classes: @@ -323,7 +355,7 @@ def _create_class_string(self, class_: Class, class_indentation: str = "", in_re # Close class class_text += f"{class_indentation}}}" - return f"{docstring}{class_signature} {{{class_text}" + return f"{docstring}{class_signature} {{{class_text}{attribute_boundaries}" def _create_class_method_string( self, @@ -351,9 +383,10 @@ def _create_class_method_string( self._create_property_function_string(method, inner_indentations), ) else: + purity_result = self._get_purity_result(method) # TODO for class methods, the module can be different, as a class can inherit methods so they are from a different module, so I cant use the module path here all_method_names.add(method.name) class_methods.append( - self._create_function_string(function=method, indentations=inner_indentations, is_method=True), + self._create_function_string(function=method, purity_result=purity_result, indentations=inner_indentations, is_method=True), ) method_text = "" @@ -371,19 +404,26 @@ def _create_class_attribute_string( self, attributes: list[Attribute], inner_indentations: str, - ) -> tuple[str, set[str]]: + ) -> tuple[str, set[str], str]: class_attributes: list[str] = [] all_attr_names: set[str] = set() + boundary_data: dict[str, list[dict[str, Any]]] = {} for attribute in attributes: if not attribute.is_public: continue - attribute_type = None + attribute_type_data = None + attribute_boundaries: list[dict[str, Any]] = [] + # attribute_valid_values: list[str] = [] if attribute.type: - attribute_type = attribute.type.to_dict() + attribute_type_data = attribute.type.to_dict() + if attribute.docstring.boundaries is not None: + attribute_boundaries = sorted(map(lambda boundary: boundary.to_dict(), attribute.docstring.boundaries), key=(lambda boundary_dict: boundary_dict["min"] + boundary_dict["max"])) + # if attribute.docstring.valid_values is not None: + # attribute_valid_values = sorted(attribute.docstring.valid_values) # Don't create TypeVar attributes - if attribute_type["kind"] == "TypeVarType": + if attribute_type_data["kind"] == "TypeVarType": continue static_string = "static " if attribute.is_static else "" @@ -401,10 +441,10 @@ def _create_class_attribute_string( # Create type information attr_docstring: AttributeDocstring = attribute.docstring - if attribute_type is None and attr_docstring and attr_docstring.type: - attribute_type = attr_docstring.type.to_dict() + if attribute_type_data is None and attr_docstring and attr_docstring.type: + attribute_type_data = attr_docstring.type.to_dict() - attr_type = self._create_type_string(attribute_type) + attr_type = self._create_type_string(attribute_type_data) type_string = f": {attr_type}" if attr_type else "" if not type_string: self._current_todo_msgs.add("attr without type") @@ -412,6 +452,23 @@ def _create_class_attribute_string( # Create docstring text docstring = self._create_sds_docstring(attr_docstring, inner_indentations) + # check for boundaries and enums of attribute + # TODO valid values extractor has some problems and needs to be fixed once it is fixed, uncomment + # if len(attribute_valid_values) != 0 and not (len(attribute_valid_values) == 1 and attribute_valid_values[0] == "None"): + # for i, valid_value in enumerate(attribute_valid_values): + # if valid_value == "None": + # attribute_valid_values[i] = "null" + # elif valid_value in ["True", "False"]: + # attribute_valid_values[i] = valid_value.lower() + # if len(attribute_valid_values) == 1 and attribute_valid_values[0] == "unlistable_str": + # type_string = ": String" + # elif attribute_valid_values == ["false", "true"]: + # type_string = ": Boolean" + # else: + # type_string = ": literal<" + ", ".join(attribute_valid_values) + ">" + if len(attribute_boundaries) != 0: + boundary_data[attribute.name] = attribute_boundaries + # Create attribute string class_attributes.append( f"{self._create_todo_msg(inner_indentations)}" @@ -421,14 +478,17 @@ def _create_class_attribute_string( ) attribute_text = "" + boundaries = "" if class_attributes: + boundaries = self._create_boundary_string(boundary_data, inner_indentations) attribute_infos = "\n".join(class_attributes) attribute_text += f"\n{attribute_infos}\n" - return attribute_text, all_attr_names + return attribute_text, all_attr_names, boundaries def _create_function_string( self, function: Function, + purity_result: PurityResult, indentations: str = "", is_method: bool = False, in_reexport_module: bool = False, @@ -449,7 +509,7 @@ def _create_function_string( self._current_todo_msgs.add("class_method") # Parameters - func_params = self._create_parameter_string( + func_params, boundaries = self._create_parameter_string( parameters=function.parameters, indentations=indentations, is_instance_method=not is_static and is_method, @@ -487,14 +547,23 @@ def _create_function_string( result_string = self._create_result_string(function.results) + + purity_str = "@Pure" + if isinstance(purity_result, Impure): + reasons_str = "" + reasons = purity_result.reasons + reasons_str = ", ".join(list(map(lambda x: str(x), reasons))) + + purity_str = f"@Impure([{reasons_str}])" + # Create string and return return ( f"{self._create_todo_msg(indentations)}" f"{docstring}" - f"{indentations}@Pure\n" + f"{indentations}{purity_str}\n" f"{function_name_annotation}" f"{indentations}{static}fun {camel_case_name}{type_var_info}" - f"({func_params}){result_string}" + f"({func_params}){result_string}{boundaries}" ) def _create_property_function_string(self, function: Function, indentations: str = "") -> str: @@ -559,8 +628,9 @@ def _create_parameter_string( parameters: list[Parameter], indentations: str, is_instance_method: bool = False, - ) -> str: + ) -> tuple[str, str] : # also returns the boundaries as second entry of the returned tuple parameters_data: list[str] = [] + boundary_data: dict[str, list[dict[str, Any]]] = {} first_loop_skipped = False for parameter in parameters: # Skip self parameter for functions @@ -571,12 +641,18 @@ def _create_parameter_string( assigned_by = parameter.assigned_by type_string = "" param_value = "" + parameter_boundaries: list[dict[str, Any]] = [] + # parameter_valid_values: list[str] = [] # Parameter type if parameter.type is not None: param_default_value = parameter.default_value parameter_type_data = parameter.type.to_dict() - + if parameter.docstring.boundaries is not None: + parameter_boundaries = sorted(map(lambda boundary: boundary.to_dict(), parameter.docstring.boundaries), key=(lambda boundary_dict: boundary_dict["min"] + boundary_dict["max"])) + # if parameter.docstring.valid_values is not None: + # parameter_valid_values = sorted(parameter.docstring.valid_values) + # Default value if parameter.is_optional: if isinstance(param_default_value, str): @@ -637,16 +713,63 @@ def _create_parameter_string( # Check if it's a Safe-DS keyword and escape it camel_case_name = _replace_if_safeds_keyword(camel_case_name) + # Check for boundaries and enums + # TODO needs to be improved with union type etc + # TODO valid values extractor has some problems and needs to be fixed once it is fixed, uncomment + # if len(parameter_valid_values) != 0 and not (len(parameter_valid_values) == 1 and parameter_valid_values[0] == "None"): + # for i, valid_value in enumerate(parameter_valid_values): + # if valid_value == "None": + # parameter_valid_values[i] = "null" + # elif valid_value in ["True", "False"]: + # parameter_valid_values[i] = valid_value.lower() + # if len(parameter_valid_values) == 1 and parameter_valid_values[0] == "unlistable_str": + # type_string = ": String" + # else: + # type_string = ": literal<" + ", ".join(parameter_valid_values) + ">" + if len(parameter_boundaries) != 0: + boundary_data[parameter.name] = parameter_boundaries + # Create string and append to the list parameters_data.append( f"{name_annotation}{camel_case_name}{type_string}{param_value}", ) - + inner_indentations = indentations + INDENTATION if parameters_data: + boundaries = self._create_boundary_string(boundary_data, inner_indentations) inner_param_data = f",\n{inner_indentations}".join(parameters_data) - return f"\n{inner_indentations}{inner_param_data}\n{indentations}" - return "" + return (f"\n{inner_indentations}{inner_param_data}\n{indentations}", boundaries) + return ("", "") + + def _create_boundary_string(self, boundary_data: dict[str, list[dict[str, Any]]], inner_indentations: str) -> str: + boundary_str = "" + for parameter_name in boundary_data: + boundaries = boundary_data[parameter_name] + boundary_str += inner_indentations + for i, boundary in enumerate(boundaries): + if str(boundary["min"]) == "NegativeInfinity": # pragma: no cover + boundary_str += parameter_name + " <= " if boundary["max_inclusive"] else " < " + str(boundary["max"]) + continue + elif str(boundary["max"]) == "Infinity": # pragma: no cover + boundary_str += parameter_name + " >= " if boundary["min_inclusive"] else " > " + str(boundary["min"]) + continue + + if boundary["min_inclusive"]: # [] + boundary_str += parameter_name + " >= " + str(boundary["min"]) + " and " + else: # () + boundary_str += parameter_name + " > " + str(boundary["min"]) + " and " + + if boundary["max_inclusive"]: + boundary_str += parameter_name + " <= " + str(boundary["max"]) + else: + boundary_str += parameter_name + " < " + str(boundary["max"]) + if i + 1 < len(boundaries): + boundary_str += " or " + boundary_str += ",\n" + + if len(boundary_str) == 0: + return "" + return f" where {{\n{boundary_str}}}" def _create_enum_string(self, enum_data: Enum) -> str: # Docstring @@ -1012,20 +1135,20 @@ def _has_node_shorter_reexport(self, node: Class | Function) -> bool: return False def _is_path_connected_to_class(self, path: str, class_path: str) -> bool: - if class_path.endswith(path): + if class_path.endswith(f"/{path}") or class_path == path: return True name = path.split("/")[-1] class_name = class_path.split("/")[-1] for reexport in self.api.reexport_map: - if reexport.endswith(name): - for module in self.api.reexport_map[reexport]: + if reexport.endswith(f"/{name}") or reexport == name: + for module in self.api.reexport_map[reexport]: # pragma: no cover # Added "no cover" since I can't recreate this in the tests if ( path.startswith(module.id) and class_path.startswith(module.id) and path.lstrip(module.id).lstrip("/") == name == class_name - ): # pragma: no cover + ): return True return False @@ -1047,28 +1170,49 @@ def _add_to_imports(self, import_qname: str) -> None: module_id = self._get_module_id(get_actual_id=True).replace("/", ".") if module_id not in import_qname: - # We need the full path for an import from the same package, but we sometimes don't get enough information, - # therefore we have to search for the class and get its id - import_qname_path = import_qname.replace(".", "/") - in_package = False qname = "" - for class_id in self.api.classes: - if self._is_path_connected_to_class(import_qname_path, class_id): - qname = class_id.replace("/", ".") - - name = qname.split(".")[-1] - shortest_qname, _ = _get_shortest_public_reexport_and_alias( - reexport_map=self.api.reexport_map, - name=name, - qname=qname, - is_module=False, - ) + module_id_parts = module_id.split(".") + + # First we hope that we already found and indexed the type we are searching + if import_qname in self.import_index: + qname = self.import_index[import_qname] + + # To save performance we next try to build the possible paths the type could originate from + if not qname: + for i in range(1, len(module_id_parts)): + test_id = ".".join(module_id_parts[:-i]) + "." + import_qname + if test_id.replace(".", "/") in self.api.classes: + qname = test_id + break + + # If the tries above did not work we have to use this performance heavy way. + # We need the full path for an import from the same package, but we sometimes don't get enough + # information, therefore we have to search for the class and get its id + if not qname: + import_qname_path = import_qname.replace(".", "/") + import_path_name = import_qname_path.split("/")[-1] + for class_id in self.api.classes: + if (import_path_name == class_id.split("/")[-1] and + self._is_path_connected_to_class(import_qname_path, class_id)): + qname = class_id.replace("/", ".") + break + + in_package = False + if qname: + self.import_index[import_qname] = qname + + name = qname.split(".")[-1] + shortest_qname, _ = _get_shortest_public_reexport_and_alias( + reexport_map=self.api.reexport_map, + name=name, + qname=qname, + is_module=False, + ) - if shortest_qname: - qname = f"{shortest_qname}.{name}" + if shortest_qname: + qname = f"{shortest_qname}.{name}" - in_package = True - break + in_package = True qname = qname or import_qname diff --git a/tests/conftest.py b/tests/conftest.py index 21b7464e..8eb1a51a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ from typing import Any import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.extensions.single_file import SingleFileSnapshotExtension from syrupy.types import SerializedData diff --git a/tests/data/boundary_enum_package_googledoc/__init__.py b/tests/data/boundary_enum_package_googledoc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/boundary_enum_package_googledoc/main_module_googledoc.py b/tests/data/boundary_enum_package_googledoc/main_module_googledoc.py new file mode 100644 index 00000000..ab98b702 --- /dev/null +++ b/tests/data/boundary_enum_package_googledoc/main_module_googledoc.py @@ -0,0 +1,57 @@ +"""GoogleDoc Docstring of the main_module_googledoc.py module.""" + +test = 10 + + +# noinspection PyUnusedLocal +def global_func1_google(param1, param2, param3, param4): + """Lorem ipsum + + Args: + param1 (str): If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + param2 (str or bool): Valid values are [False, None, 'sparse matrix'] + param3 (float): Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + param4 (int or float): If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + """ + return test + +class ClassWithAttributes: + """ + ClassAndConstructorWithParameters + + Dolor sit amet. + + Attributes: + attribute1 (float): + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 (str): + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + """ + def __init__(self) -> None: + """ + Attributes: + attribute1 (float): + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 (str): + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + """ + self.attribute1: float + self.attribute2: str diff --git a/tests/data/boundary_enum_package_numpydoc/__init__.py b/tests/data/boundary_enum_package_numpydoc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/boundary_enum_package_numpydoc/main_module_numpydoc.py b/tests/data/boundary_enum_package_numpydoc/main_module_numpydoc.py new file mode 100644 index 00000000..d6b5e51c --- /dev/null +++ b/tests/data/boundary_enum_package_numpydoc/main_module_numpydoc.py @@ -0,0 +1,66 @@ +"""NumpyDoc Docstring of the main_module_numpydoc.py module.""" + +test = 10 + +# noinspection PyUnusedLocal +def global_func1_numpy(param1, param2, param3, param4): + """Lorem ipsum + + Parameters + -------- + param1 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + + param2 : str or bool + Valid values are [False, None, 'sparse matrix'] + + param3 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + + param4 : int or float + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + """ + return test + +class ClassWithAttributes: + """ + ClassAndConstructorWithParameters + + Dolor sit amet. + + Attributes + ---------- + attribute1 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + """ + def __init__(self) -> None: + """ + Attributes + ---------- + attribute1 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + """ + self.attribute1: float + self.attribute2: str \ No newline at end of file diff --git a/tests/data/boundary_enum_package_restdoc/__init__.py b/tests/data/boundary_enum_package_restdoc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/boundary_enum_package_restdoc/main_module_restdoc.py b/tests/data/boundary_enum_package_restdoc/main_module_restdoc.py new file mode 100644 index 00000000..b4bce28c --- /dev/null +++ b/tests/data/boundary_enum_package_restdoc/main_module_restdoc.py @@ -0,0 +1,66 @@ +"""ReST Docstring of the main_module_restdoc.py module.""" + +test = 10 + +# noinspection PyUnusedLocal +def global_func1_rest(param1, param2, param3, param4): + """Lorem ipsum + + :param param1: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type param1: int + :param param2: Valid values are [False, None, 'sparse matrix'] + :type param2: str or bool + :param param3: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type param3: float + :param param4: + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + :type param4: int or float + """ + return test + +# TODO Currently disabled, since Griffe can't analyze ReST (Sphinx) attributes (see issue #98) +# that is why the stub generator wont generate the class stub correctly +class ClassWithAttributes: + """ + ClassAndConstructorWithParameters + + Dolor sit amet. + + :param attribute1: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type attribute1: float + :param attribute2: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type attribute2: str + """ + def __init__(self) -> None: + """ + :param attribute1: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type attribute1: float + :param attribute2: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type attribute2: str + """ + self.attribute1: float + self.attribute2: str \ No newline at end of file diff --git a/tests/data/docstring_parser_package/googledoc.py b/tests/data/docstring_parser_package/googledoc.py index 70781cc7..e06aba66 100644 --- a/tests/data/docstring_parser_package/googledoc.py +++ b/tests/data/docstring_parser_package/googledoc.py @@ -175,6 +175,7 @@ class ClassWithVariousParameterTypes: bool_type (bool): str_type (str): float_type (float): + byte_type (bytes): multiple_types (int, bool): list_type_1 (list): list_type_2 (list[str]): @@ -195,9 +196,9 @@ class ClassWithVariousParameterTypes: """ def __init__( - self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, multiple_types, list_type_1, - list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, set_type_5, - tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, + self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, byte_type, multiple_types, + list_type_1, list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, + set_type_5, tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, optional_type_2: Optional[int], class_type, imported_type ) -> None: pass diff --git a/tests/data/docstring_parser_package/numpydoc.py b/tests/data/docstring_parser_package/numpydoc.py index b066ff52..9a7a1386 100644 --- a/tests/data/docstring_parser_package/numpydoc.py +++ b/tests/data/docstring_parser_package/numpydoc.py @@ -308,6 +308,7 @@ class ClassWithVariousParameterTypes: bool_type : bool str_type : str float_type : float + byte_type : bytes multiple_types : int, bool list_type_1 : list list_type_2 : list[str] @@ -328,9 +329,9 @@ class ClassWithVariousParameterTypes: """ def __init__( - self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, multiple_types, list_type_1, - list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, set_type_5, - tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, + self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, byte_type, multiple_types, + list_type_1, list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, + set_type_5, tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, optional_type_2: Optional[int], class_type, imported_type ) -> None: pass diff --git a/tests/data/docstring_parser_package/restdoc.py b/tests/data/docstring_parser_package/restdoc.py index 94e34a5b..9ae96883 100644 --- a/tests/data/docstring_parser_package/restdoc.py +++ b/tests/data/docstring_parser_package/restdoc.py @@ -164,6 +164,8 @@ class ClassWithVariousParameterTypes: :type str_type: str :param float_type: :type float_type: float + :param byte_type: + :type byte_type: bytes :param multiple_types: :type multiple_types: int, bool :param list_type_1: @@ -201,9 +203,9 @@ class ClassWithVariousParameterTypes: """ def __init__( - self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, multiple_types, list_type_1, - list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, set_type_5, - tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, + self, no_type, optional_type, none_type, int_type, bool_type, str_type, float_type, byte_type, multiple_types, + list_type_1, list_type_2, list_type_3, list_type_4, list_type_5, set_type_1, set_type_2, set_type_3, set_type_4, + set_type_5, tuple_type_1, tuple_type_2, tuple_type_3, tuple_type_4, any_type: Any, optional_type_2: Optional[int], class_type, imported_type ) -> None: pass diff --git a/tests/data/main_package/main_module.py b/tests/data/main_package/main_module.py index 2ff8649a..9157de9e 100644 --- a/tests/data/main_package/main_module.py +++ b/tests/data/main_package/main_module.py @@ -45,7 +45,10 @@ def __init__(self, init_param_1): def _some_function(self, param_1: ac_alias, param_2: bool = False) -> ac_alias: """Function Docstring. - param_2: bool. + Parameters + --------- + param_2 : bool, optional. + Valid values are True and False. """ class NestedClass(_AcImportAlias): diff --git a/tests/data/purity_package/__init__.py b/tests/data/purity_package/__init__.py new file mode 100644 index 00000000..4b8884eb --- /dev/null +++ b/tests/data/purity_package/__init__.py @@ -0,0 +1,6 @@ +global_var = 0 +def test_init_py_pure(): + return global_var + +def test_init_py_impure(): + return global_var \ No newline at end of file diff --git a/tests/data/purity_package/another_purity_path/__init__.py b/tests/data/purity_package/another_purity_path/__init__.py new file mode 100644 index 00000000..80073ef7 --- /dev/null +++ b/tests/data/purity_package/another_purity_path/__init__.py @@ -0,0 +1,16 @@ +def test_init_py_pure(): + return 10 + +global_var = 0 +def test_init_py_impure(): + return global_var + +class ClassWithPureStaticMethods: + @staticmethod + def test(): + return 10 + +class ClassWithImpureStaticMethods: + @staticmethod + def test(): + return global_var \ No newline at end of file diff --git a/tests/data/purity_package/another_purity_path/another_purity_module.py b/tests/data/purity_package/another_purity_path/another_purity_module.py new file mode 100644 index 00000000..87c965cb --- /dev/null +++ b/tests/data/purity_package/another_purity_path/another_purity_module.py @@ -0,0 +1,215 @@ +from __future__ import annotations +from typing import Callable + +global_var = 10 +global_var2 = 20 +global_var3 = 30 +class SuperClass: + def __init__(self) -> None: + pass + + def same_name(self) -> int: + x = 20 + return x + + def only_in_super_pure(self) -> int: + return 10 + + def only_in_super_impure(self) -> int: + return global_var + + def in_super_and_child_pure(self) -> int: + return 10 + + def in_super_and_child_impure(self) -> int: + return global_var + + def in_super_and_child_of_child_pure(self) -> int: + return 10 + + def in_super_and_child_of_child_impure(self) -> int: + return global_var + +class ClassPure(SuperClass): + """contains pure functions only + """ + def same_name(self) -> int: + return 20 + + def only_in_T(self) -> int: + return 20 + + def in_super_and_child_pure(self) -> int: + return 20 + + def in_super_and_child_impure(self) -> int: + return 20 + + def in_child_and_child_of_child_pure(self) -> int: + return 20 + + def in_child_and_child_of_child_impure(self) -> int: + return 20 + + def __add__(self, other: ClassPure) -> ClassPure: + return other + +class ClassImpure(SuperClass): + """contains impure functions only + """ + def same_name(self) -> int: + return global_var2 + + def only_in_T(self) -> int: + return global_var2 + + def in_super_and_child_pure(self) -> int: + return global_var2 + + def in_super_and_child_impure(self) -> int: + return global_var2 + + def in_child_and_child_of_child_pure(self) -> int: + return global_var2 + + def in_child_and_child_of_child_impure(self) -> int: + return global_var2 + + def __add__(self, other: ClassImpure) -> ClassImpure: + return other + +class ChildClassPure(ClassPure): + """contains pure functions only + """ + def only_in_child(self) -> int: + return 30 + + def in_child_and_child_of_child_pure(self) -> int: + return 30 + + def in_child_and_child_of_child_impure(self) -> int: + return 30 + + def in_super_and_child_of_child_pure(self) -> int: + return 30 + + def in_super_and_child_of_child_impure(self) -> int: + return 30 + + def only_in_child_self(self) -> int: + result = self.same_name() + return result + + def super_same_name(self) -> int: + result = super().same_name() + return result + +class ChildClassImpure(ClassImpure): + """contains impure functions only + """ + def only_in_child(self) -> int: + return global_var3 + + def in_child_and_child_of_child_pure(self) -> int: + return global_var3 + + def in_child_and_child_of_child_impure(self) -> int: + return global_var3 + + def in_super_and_child_of_child_pure(self) -> int: + return global_var3 + + def in_super_and_child_of_child_impure(self) -> int: + return global_var3 + + def only_in_child_self(self) -> int: + result = self.same_name() + return result + + def super_same_name(self) -> int: + result = super().same_name() + return result + + +class SuperWithNestedClassAsMember: + def __init__(self) -> None: + self.super_member_pure: ClassPure = ClassPure() + self.super_member_impure: ClassImpure = ClassImpure() + + def only_in_super_nested_call_pure(self) -> ClassPure: + return ClassPure() + + def only_in_super_nested_call_impure(self) -> ClassImpure: + return ClassImpure() + +class ClassWithNestedClassAsMember(SuperWithNestedClassAsMember): + def __init__(self): + self.memberWithPureMethods: ClassPure = ClassPure() + self.memberWithImpureMethods: ClassImpure = ClassImpure() + self.memberWithPureMethodsWithoutTypeHint = ClassPure() + self.memberWithImpureMethodsWithoutTypeHint = ClassImpure() + self.listMemberWithPureMethods: list[ClassPure] = [ClassPure()] + self.listMemberWithImpureMethods: list[ClassImpure] = [ClassImpure()] + self.dictMemberWithPureMethods: dict[str, ClassPure] = {"key": ClassPure()} + self.dictMemberWithImpureMethods: dict[str, ClassImpure] = {"key": ClassImpure()} + self.recursive: ClassWithNestedClassAsMember = ClassWithNestedClassAsMember() + + def return_class_impure(self) -> ClassImpure: + return ClassImpure() + + def return_class_pure(self) -> ClassPure: + return ClassPure() + + def recursive_function(self) -> ClassWithNestedClassAsMember: # type: ignore + return ClassWithNestedClassAsMember() + + def double_function_pure(self) -> Callable[[], int]: + return lambda: 10 + + def double_function_impure(self) -> Callable[[], int]: + return lambda: global_var + + def super_impure(self) -> int: + result = super().super_member_impure.same_name() + return result + + def super_pure(self) -> int: + result = super().super_member_pure.same_name() + return result + + +class AnotherPureClass: + def __init__(self): + pass + + def same_name(self): + return 21 + +class PureInitClass: + def __init__(self): + result = 10 + +class ImpureInitClass: + def __init__(self): + self.test = global_var + +class PureSuperInit(PureInitClass): + def __init__(self): + super().__init__() + +class ImpureSuperInit(ImpureInitClass): + def __init__(self): + super().__init__() + +class PureSuperInitFromKeyError(KeyError): + def __init__(self): + super().__init__() + +class ContextForWithTest: + def __enter__(self): + print("Entering the context") + return self + + def __exit__(self, exc_type, exc_value, traceback): + print("Exiting the context") + return False \ No newline at end of file diff --git a/tests/data/purity_package/main_purity_module.py b/tests/data/purity_package/main_purity_module.py new file mode 100644 index 00000000..94c47819 --- /dev/null +++ b/tests/data/purity_package/main_purity_module.py @@ -0,0 +1,836 @@ +from .another_purity_path.another_purity_module import ContextForWithTest, SuperClass, ClassPure, ClassImpure, ChildClassPure, ChildClassImpure, ClassWithNestedClassAsMember, AnotherPureClass, PureSuperInit, ImpureSuperInit, PureSuperInitFromKeyError +from . import another_purity_path +from . import test_init_py_pure + + +# purity analysis should categorize functions as pure, when their name suffix is "pure" +# else they should be categorized as impure +# check: tests\data\out\purity_package__api_purity.json after snapshot_update + +def global_func_same_name_pure() -> int: + child_class_instance = ChildClassPure() + result = child_class_instance.same_name() + return result + +def global_func_same_name_impure() -> int: + child_class_instance = ChildClassImpure() + result = child_class_instance.same_name() # call reference to impure function + return result + +def global_func_only_in_T_pure() -> int: + """ + improved purity analysis should reference function only_in_T() of ClassPure + and not only_in_T() of ClassImpure as well + """ + instance = ClassPure() + result = instance.only_in_T() + return result + +def global_func_only_in_T_impure() -> int: + instance = ClassImpure() + result = instance.only_in_T() + return result + +def global_func_only_in_T_from_Child_pure() -> int: + instance = ChildClassPure() + result = instance.only_in_T() + return result + +def global_func_only_in_T_from_Child_impure() -> int: + instance = ChildClassImpure() + result = instance.only_in_T() + return result + +def global_func_only_in_Child_pure() -> int: + instance = ChildClassPure() + result = instance.only_in_child() + return result + +def global_func_only_in_Child_impure() -> int: + instance = ChildClassImpure() + result = instance.only_in_child() + return result + +def global_func_only_in_super_from_T_pure() -> int: + instance = ClassPure() + result = instance.only_in_super_pure() + return result + +def global_func_only_in_super_from_T_impure() -> int: + instance = ClassPure() + result = instance.only_in_super_impure() + return result + +def global_func_only_in_super_from_child_pure() -> int: + instance = ChildClassPure() + result = instance.only_in_super_pure() + return result + +def global_func_only_in_super_from_child_impure() -> int: + instance = ChildClassPure() + result = instance.only_in_super_impure() + return result + +def global_func_in_super_and_child_from_super_impure() -> int: + """ + this call is impure as ClassImpure also has in_super_and_child_pure() which is impure + ClassImpure is a child of SuperClass + """ + instance = SuperClass() + result = instance.in_super_and_child_pure() + return result + +def global_func_in_super_and_child_from_T_pure() -> int: + instance = ClassPure() + result = instance.in_super_and_child_pure() + return result + +def global_func_in_super_and_child_from_T_impure() -> int: + instance = ClassImpure() + result = instance.in_super_and_child_impure() + return result + +def global_func_in_child_and_child_of_child_from_T_pure() -> int: + """ + pure function, as instance is of type ClassPure, which has ChildClassPure as child + there no function is impure and so the purity analysis should only find + referenced functions which are pure + """ + instance = ClassPure() + result = instance.in_child_and_child_of_child_pure() + instance.in_child_and_child_of_child_impure() + return result + +def global_func_in_child_and_child_of_child_from_T_impure() -> int: + instance = ClassImpure() + result = instance.in_child_and_child_of_child_impure() + instance.in_child_and_child_of_child_pure() + instance.only_in_super_impure() + return result + +def global_func_in_child_and_child_of_child_from_child_of_child_pure() -> int: + instance = ChildClassPure() + result = instance.in_child_and_child_of_child_pure() + instance.in_child_and_child_of_child_impure() + return result + +def global_func_in_child_and_child_of_child_from_child_of_child_impure() -> int: + instance = ChildClassImpure() + result = instance.in_child_and_child_of_child_impure() + instance.in_child_and_child_of_child_pure() + return result + +def global_func_in_super_and_child_of_child_from_T_pure() -> int: + instance = ClassPure() + result = instance.in_super_and_child_of_child_pure() + return result + +def global_func_in_super_and_child_of_child_from_T_impure() -> int: + """ + should be impure as ClassPure is a subtype of SuperClass and there in_super_and_child_of_child_impure is + impure + """ + instance = ClassPure() + result = instance.in_super_and_child_of_child_impure() + return result + +def global_func_all_functions_pure() -> int: + instance = ChildClassPure() + if True: + instance.only_in_child() + if False: + pass + else: + instance.in_child_and_child_of_child_pure() + for x in range(1): + instance.only_in_T() + instance.only_in_super_pure() + instance.in_super_and_child_pure() + instance.same_name() + return 10 + +def global_func_find_deeply_nested_function_impure() -> int: + instance = ClassImpure() + if True: + while True: + if False: + pass + elif True: + for x in range(1): + try: + instance.only_in_super_pure() + except NameError: + if False: + pass + else: + instance.only_in_T() + break + return 10 + +def global_func_nested_class_should_be_pure_but_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this + .memberWithPureMethods + """ + instance = ClassWithNestedClassAsMember() + result = instance.memberWithPureMethods.only_in_T() + return result + +def global_func_nested_class_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.memberWithImpureMethods.only_in_T() + return result + +def global_func_from_parameter_same_name_pure(instance: ClassPure) -> int: + result = instance.same_name() + return result + +def global_func_from_parameter_same_name_impure(instance: ClassImpure) -> int: + result = instance.same_name() + return result + +def global_func_from_parameter_same_name_nested_pure_but_impure(instance: ClassWithNestedClassAsMember) -> int: + """ + member access is counted as impure in general and instance can be changed from outside the function + .memberWithPureMethods + """ + result = instance.memberWithPureMethods.same_name() + return result + +def global_func_from_parameter_same_name_nested_impure(instance: ClassWithNestedClassAsMember) -> int: + result = instance.memberWithImpureMethods.same_name() + return result + +def global_func_nested_from_second_call_reference_pure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.return_class_pure().only_in_T() + return result + +def global_func_nested_from_second_call_reference_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.return_class_impure().only_in_T() + return result + +def global_func_nested_with_list_should_be_pure_but_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this, as here instance is created and not changed + .listMemberWithPureMethods + """ + instance = ClassWithNestedClassAsMember() + result = instance.listMemberWithPureMethods[0].only_in_T() + return result + +def global_func_nested_with_list_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.listMemberWithImpureMethods[0].only_in_T() + return result + +def global_func_nested_with_dict_should_be_pure_but_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this, as here instance is created and not changed + .dictMemberWithPureMethods + """ + instance = ClassWithNestedClassAsMember() + result = instance.dictMemberWithPureMethods["key"].only_in_T() + return result + +def global_func_nested_with_dict_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.dictMemberWithImpureMethods["key"].only_in_T() + return result + +def global_func_multiple_nested_member_should_be_pure_but_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this, as here instance is created and not changed + .recursive + """ + instance = ClassWithNestedClassAsMember() + result = instance.recursive.recursive.memberWithPureMethods.only_in_T() + return result + +def global_func_multiple_nested_member_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.recursive.recursive.memberWithImpureMethods.only_in_T() + return result + +def global_func_multiple_nested_methods_pure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.recursive_function().recursive_function().return_class_pure().only_in_T() + return result + +def global_func_multiple_nested_methods_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.recursive_function().recursive_function().return_class_impure().only_in_T() + return result + +def global_func_nested_method_from_super_pure() -> int: + """ + function access is counted as pure, as it is assumed that functions wont be changed during runtime + """ + instance = ClassWithNestedClassAsMember() + result = instance.only_in_super_nested_call_pure().only_in_T() + return result + +def global_func_nested_method_from_super_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.only_in_super_nested_call_impure().only_in_T() + return result + +def global_func_nested_member_from_super_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this, as here instance is created and not changed + """ + instance = ClassWithNestedClassAsMember() + result = instance.super_member_pure.only_in_T() + return result + +def global_func_nested_member_from_super_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.super_member_impure.only_in_T() + return result + +def global_func_multiple_recursion_impure() -> int: + """ + member access is counted as impure in general, dataflow analysis could improve this, as here instance is created and not changed + .recursive. is the problem here + """ + instance = ClassWithNestedClassAsMember() + result = instance.recursive_function().recursive.recursive_function().return_class_pure().only_in_T() + return result + +def global_func_multiple_recursion_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.recursive_function().recursive.recursive_function().return_class_impure().only_in_T() + return result + +def global_func_double_function_impure() -> int: + """ + purity analysis cant analyze ()() and returns UNKNOWNCALL, references are found correctly + """ + instance = ClassWithNestedClassAsMember() + result = instance.double_function_pure()() + return result + +def global_func_double_function_impure() -> int: + """ + purity analysis cant analyze ()() and returns UNKNOWNCALL, references are found correctly + """ + instance = ClassWithNestedClassAsMember() + result = instance.double_function_impure()() + return result + +def global_func_return_pure_class() -> ClassPure: + return ClassPure() + +def global_func_start_with_function_pure() -> int: + result = global_func_return_pure_class().only_in_T() + return result + +def global_func_return_impure_class() -> ClassImpure: + return ClassImpure() + +def global_func_start_with_function_impure() -> int: + result = global_func_return_impure_class().only_in_T() + return result + +def global_func_start_with_list_pure() -> int: + instances = [ClassPure()] + result = instances[0].only_in_T() + return result + +def global_func_start_with_list_impure() -> int: + instances = [ClassImpure()] + result = instances[0].only_in_T() + return result + +def global_func_start_with_tuple_pure() -> int: + instances = (ClassPure(),) + result = instances[0].only_in_T() + return result + +def global_func_start_with_tuple_impure() -> int: + instances = (ClassImpure(),) + result = instances[0].only_in_T() + return result + +def global_func_start_with_dict_pure() -> int: + instances = {"key": ClassPure()} + result = instances["key"].only_in_T() + return result + +def global_func_start_with_dict_impure() -> int: + instances = {"key": ClassImpure()} + result = instances["key"].only_in_T() + return result + +def global_func_start_with_nested_type_pure() -> int: + instances = {"key": [{"key2": ClassPure()}]} + result = instances["key"][0]["key2"].only_in_T() + return result + +def global_func_start_with_nested_type_impure() -> int: + instances = {"key": [{"key2": ClassImpure()}]} + result = instances["key"][0]["key2"].only_in_T() + return result + +def global_helper_func_nested_type_pure_with_list_and_dict() -> dict[str, list[dict[str, ClassPure]]]: + return {"key": [{"key2": ClassPure()}]} + +def global_func_start_with_nested_type_and_function_pure() -> int: + result = global_helper_func_nested_type_pure_with_list_and_dict()["key"][0]["key2"].only_in_T() + return result + +def global_helper_func_nested_type_impure_with_list_and_dict() -> dict[str, list[dict[str, ClassImpure]]]: + return {"key": [{"key2": ClassImpure()}]} + +def global_func_start_with_nested_type_and_function_impure() -> int: + result = global_helper_func_nested_type_impure_with_list_and_dict()["key"][0]["key2"].only_in_T() + return result + +def global_func_from_docstring_same_name_pure(instance) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instance : ClassPure + Lorem ipsum + """ + result = instance.same_name() + return result + +def global_func_from_docstring_same_name_impure(instance) -> int: + """this function should be impure as same_name of ClassImpure is impure + + Parameters + -------- + instance : ClassImpure + Lorem ipsum + """ + result = instance.same_name() + return result + +def global_func_from_docstring_as_list_same_name_pure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : list[ClassPure] + Lorem ipsum + """ + result = instances[0].same_name() + return result + +def global_func_from_docstring_as_list_same_name_impure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : list[ClassImpure] + Lorem ipsum + """ + result = instances[0].same_name() + return result + +def global_func_from_docstring_as_tuple_same_name_pure(instances: tuple[AnotherPureClass]) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[ClassPure] + Lorem ipsum + """ + result = instances[0].same_name() + return result + +def global_func_from_docstring_as_tuple_same_name_impure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[ClassImpure] + Lorem ipsum + """ + result = instances[0].same_name() + return result + +def global_func_from_parameter_as_multiple_tuple_same_name_pure(instances: tuple[str, ClassPure]) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[str, ClassPure] + Lorem ipsum + """ + result = instances[1].same_name() + return result + +def global_func_from_parameter_as_multiple_tuple_same_name_impure(instances: tuple[str, ClassImpure]) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[str, ClassImpure] + Lorem ipsum + """ + result = instances[1].same_name() + return result + +def global_func_from_docstring_as_multiple_tuple_same_name_pure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[str, ClassPure] + Lorem ipsum + """ + result = instances[1].same_name() + return result + +def global_func_from_docstring_as_multiple_tuple_same_name_impure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : tuple[str, ClassImpure] + Lorem ipsum + """ + result = instances[1].same_name() + return result + +def global_func_from_docstring_as_dict_same_name_pure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : dict[str, ClassPure] + Lorem ipsum + """ + result = instances["key"].same_name() + return result + +def global_func_from_docstring_as_dict_same_name_impure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : dict[str, ClassImpure] + Lorem ipsum + """ + result = instances["key"].same_name() + return result + +def global_func_from_docstring_as_dict_with_union_same_name_pure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : dict[str, ClassPure | AnotherPureClass] + Lorem ipsum + """ + result = instances["key"].same_name() + return result + +def global_func_from_docstring_as_dict_with_union_same_name_impure(instances) -> int: + """this function should be pure as same_name of ClassPure is pure + + Parameters + -------- + instances : dict[str, ClassPure | ClassImpure] + Lorem ipsum + """ + result = instances["key"].same_name() + return result + +def global_func_union_type_pure(instance: ClassPure | AnotherPureClass) -> int: + result = instance.same_name() + return result + +def global_func_union_type_impure(instance: ClassImpure | ClassPure) -> int: + result = instance.only_in_T() + return result + +def global_func_union_type_from_docstring_pure(instance) -> int: + """this function should be pure as only_in_T() from ClassPure and ChildClassPure are pure + + Parameters + -------- + instance : ClassPure | AnotherPureClass + Lorem ipsum + """ + result = instance.only_in_T() + return result + +def global_func_union_type_from_docstring_impure(instance) -> int: + """this function should be impure as only_in_T() from ClassImpure is impure + + Parameters + -------- + instance : ClassPure | ClassImpure + Lorem ipsum + """ + result = instance.only_in_T() + return result + +def global_func_call_reference_in_index_pure() -> int: + instance = ClassPure() + dictionary = {} + dictionary[instance.only_in_T()] = 10 + return dictionary[instance.only_in_T()] + +def global_func_call_reference_in_index_impure() -> int: + instance = ClassImpure() + dictionary = {} + dictionary[instance.only_in_T()] = 10 + return dictionary[instance.only_in_T()] + +def global_func_self_pure() -> int: + instance = ChildClassPure() + result = instance.only_in_child_self() + return result + +def global_func_self_impure() -> int: + instance = ChildClassImpure() + result = instance.only_in_child_self() + return result + +def global_func_super_init_pure(): + instance = PureSuperInit().__init__() + return instance + +def global_func_super_init_impure(): + instance = ImpureSuperInit().__init__() + return instance + +def global_func_super_method_pure() -> int: + instance = ChildClassPure() + result = instance.super_same_name() + return result + +def global_func_super_method_impure() -> int: + instance = ChildClassImpure() + result = instance.super_same_name() + return result + +def global_func_super_nested_method_should_be_pure_but_impure() -> int: + """ + is impure as .super_pure() has a member variable read + """ + instance: ClassWithNestedClassAsMember = ClassWithNestedClassAsMember() + result = instance.super_pure() + return result + +def global_func_super_nested_method_impure() -> int: + instance = ClassWithNestedClassAsMember() + result = instance.super_impure() + return result + +def global_func_super_from_builtin_keyError_pure() -> int: + instance = PureSuperInitFromKeyError() + return 10 + +def global_func_list_comprehension_generator_pure() -> list[int]: + instance = ClassPure() + result = [instance.same_name() for _ in range(10)] + return result + +def global_func_list_comprehension_generator_impure() -> list[int]: + instance = ClassImpure() + result = [instance.same_name() for _ in range(10)] + return result + +def global_func_list_comprehension_with_if_generator_pure() -> list[int]: + instance = ClassPure() + result = [10 for _ in range(10) if instance.same_name() < 10] + return result + +def global_func_list_comprehension_with_if_generator_impure() -> list[int]: + instance = ClassImpure() + result = [10 for _ in range(10) if instance.same_name() < 10] + return result + +def global_func_list_comprehension_iterable_is_call_ref_pure() -> list[int]: + instance = ClassPure() + result = [10 for _ in range(instance.same_name())] + return result + +def global_func_list_comprehension_iterable_is_call_ref_impure() -> list[int]: + instance = ClassImpure() + result = [10 for _ in range(instance.same_name())] + return result + +def global_func_dict_comprehension_generator_pure() -> dict[int, int]: + instance = ClassPure() + result = {instance.same_name(): 10 for _ in range(10)} + return result + +def global_func_dict_comprehension_generator_impure() -> dict[int, int]: + instance = ClassImpure() + result = {instance.same_name(): 10 for _ in range(10)} + return result + +def global_func_dict_comprehension_with_if_generator_pure() -> dict[int, int]: + instance = ClassPure() + result = {10: i for i in range(10) if instance.same_name() < 10} + return result + +def global_func_dict_comprehension_with_if_generator_impure() -> dict[int, int]: + instance = ClassImpure() + result = {10: i for i in range(10) if instance.same_name() < 10} + return result + +def global_func_dict_comprehension_iterable_is_call_ref_pure() -> dict[int, int]: + instance = ClassPure() + result = {10: i for i in range(instance.same_name())} + return result + +def global_func_dict_comprehension_iterable_is_call_ref_impure() -> dict[int, int]: + instance = ClassImpure() + result = {10: i for i in range(instance.same_name())} + return result + +def global_func_set_comprehension_generator_pure() -> set[int]: + instance = ClassPure() + result = {instance.same_name() for _ in range(10)} + return result + +def global_func_set_comprehension_generator_impure() -> set[int]: + instance = ClassImpure() + result = {instance.same_name() for _ in range(10)} + return result + +def global_func_set_comprehension_with_if_generator_pure() -> set[int]: + instance = ClassPure() + result = {i for i in range(10) if instance.same_name() < 10} + return result + +def global_func_set_comprehension_with_if_generator_impure() -> set[int]: + instance = ClassImpure() + result = {i for i in range(10) if instance.same_name() < 10} + return result + +def global_func_set_comprehension_iterable_is_call_ref_pure() -> set[int]: + instance = ClassPure() + result = {i for i in range(instance.same_name())} + return result + +def global_func_set_comprehension_iterable_is_call_ref_impure() -> set[int]: + instance = ClassImpure() + result = {i for i in range(instance.same_name())} + return result + +def global_func_operator_expression_pure(): + instance: ClassPure = ClassPure() + result = (instance.same_name() + instance.same_name()) + return result + +def global_func_isinstance_check_should_be_pure_but_impure(instance: object): + """ + isinstance checks dont update the ast correctly + """ + if isinstance(instance, ClassPure): + result = instance.same_name() + return result + +def global_func_isinstance_check_impure(instance: object): + """ + isinstance checks dont update the ast correctly + """ + if isinstance(instance, ClassImpure): + result = instance.same_name() + return result + +def global_func_closure_pure(): + def closure(): + return 0 + closure() + +def global_func_closure_should_be_pure_but_impure(): + """ + impure because the receiver of instance.same_name() has any type + mypy some how cant infer the type here, seems to be a bug? + """ + def closure(): + instance: ClassPure = ClassPure() + instance.same_name() # instance has type any + closure() + +def global_func_closure_impure(): + def closure(): + instance = ClassImpure() + instance.same_name() + closure() + +def global_func_call_another_global_func_pure(): + global_func_same_name_pure() + +def global_func_call_another_global_func_impure(): + global_func_same_name_impure() + +def global_func_call_ref_in_dict_generation_pure(): + instance: ClassPure = ClassPure() + return {"foo": instance.same_name()} + +def global_func_call_ref_in_dict_generation_impure(): + instance = ClassImpure() + return {"foo": instance.same_name()} + +def global_func_init_import_pure(): + return another_purity_path.test_init_py_pure() + +def global_func_init_import_impure(): + return another_purity_path.test_init_py_impure() + +def global_func_import_global_func_impure(): + return test_init_py_pure() + +def global_func_module_class_with_static_method_pure(): + return another_purity_path.ClassWithPureStaticMethods.test() + +def global_func_module_class_with_static_method_impure(): + return another_purity_path.ClassWithImpureStaticMethods.test() + +def global_func_unreachable_code_pure(): + if False: + instance: ClassPure = ClassPure() + return instance.same_name() + +def global_func_unreachable_code_impure(): + if False: + instance: ClassImpure = ClassImpure() + return instance.same_name() + +def global_func_inside_of_lambda_with_map_should_be_pure_but_impure(): + """ + mypy doesnt store the type inside of lambda bodies + """ + instances: list[ClassPure] = [ClassPure()] + test = map(lambda x: x.same_name(), instances) + return test + +def global_func_inside_of_lambda_with_map_impure(): + instances: list[ClassPure] = [ClassImpure()] + test = map(lambda x: x.same_name(), instances) + return test + +def global_func_operator_receiver_with_brackets_should_be_pure_but_impure(): + """ + Solution: look into the type of instance1 and retrieve the return type of __add__ which then is the receiver of .same_name() + but actually mypy should have the type stored in method_type attribute of OpExpr + """ + instance1: ClassPure = ClassPure() + instance2: ClassPure = ClassPure() + return ((instance1 + instance2) + .same_name()) + +def global_func_operator_receiver_with_brackets_impure(): + instance1: ClassImpure = ClassImpure() + instance2: ClassImpure = ClassImpure() + return ((instance1 + instance2) + .same_name()) + +def global_func_with_keyword_pure(): + with ContextForWithTest() as context: + instance: ClassPure = ClassPure() + return instance.same_name() + +def global_func_with_keyword_impure(): + with ContextForWithTest() as context: + instance: ClassImpure = ClassImpure() + return instance.same_name() + \ No newline at end of file diff --git a/tests/data/various_modules_package/enum_module.py b/tests/data/various_modules_package/enum_module.py index c8b8232d..1f46b851 100644 --- a/tests/data/various_modules_package/enum_module.py +++ b/tests/data/various_modules_package/enum_module.py @@ -29,7 +29,7 @@ class EnumTest3(IntEnum): ele_ven = 11 -class EmptyEnum(Enum, IntEnum): +class EmptyEnum(IntEnum): ... diff --git a/tests/safeds_stubgen/__snapshots__/test_main.ambr b/tests/safeds_stubgen/__snapshots__/test_main.ambr index 2c5d2f99..6beb6ef3 100644 --- a/tests/safeds_stubgen/__snapshots__/test_main.ambr +++ b/tests/safeds_stubgen/__snapshots__/test_main.ambr @@ -1,16 +1,36 @@ # serializer version: 1 -# name: test_main +# name: test_main[test_googledoc_with_boundary_enum_extractor] dict({ 'attributes': list([ dict({ 'docstring': dict({ - 'description': '', - 'type': None, + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': False, + 'min': 0.5, + 'min_inclusive': True, + }), + ]), + 'description': ''' + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + 'valid_values': list([ + ]), }), - 'id': 'tests/data/main_package/main_module/ModuleClass/_init_attr_private', - 'is_public': False, + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/attribute1', + 'is_public': True, 'is_static': False, - 'name': '_init_attr_private', + 'name': 'attribute1', 'type': dict({ 'kind': 'NamedType', 'name': 'float', @@ -19,165 +39,71 @@ }), dict({ 'docstring': dict({ - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/attr_1', - 'is_public': True, - 'is_static': True, - 'name': 'attr_1', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'int', - 'qname': 'builtins.int', - }), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'type': None, + 'boundaries': list([ + ]), + 'description': ''' + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + 'valid_values': list([ + '"constant"', + '"mean"', + '"median"', + '"most_frequent"', + ]), }), - 'id': 'tests/data/main_package/main_module/ModuleClass/init_attr', + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/attribute2', 'is_public': True, 'is_static': False, - 'name': 'init_attr', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'bool', - 'qname': 'builtins.bool', - }), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/nested_class_attr', - 'is_public': False, - 'is_static': True, - 'name': 'nested_class_attr', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'int', - 'qname': 'builtins.int', - }), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/public_attr_in_private_class', - 'is_public': False, - 'is_static': True, - 'name': 'public_attr_in_private_class', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'int', - 'qname': 'builtins.int', - }), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/public_init_attr_in_private_class', - 'is_public': False, - 'is_static': False, - 'name': 'public_init_attr_in_private_class', + 'name': 'attribute2', 'type': dict({ 'kind': 'NamedType', - 'name': 'int', - 'qname': 'builtins.int', + 'name': 'str', + 'qname': 'builtins.str', }), }), ]), 'classes': list([ dict({ 'attributes': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/attribute1', + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/attribute2', ]), 'classes': list([ ]), - 'constructor': None, - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/another_path/another_module/AnotherClass', - 'inherits_from_exception': False, - 'is_public': True, - 'methods': list([ - ]), - 'name': 'AnotherClass', - 'reexported_by': list([ - ]), - 'superclasses': list([ - ]), - 'type_parameters': list([ - ]), - }), - dict({ - 'attributes': list([ - ]), - 'classes': list([ - ]), - 'constructor': None, - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass', - 'inherits_from_exception': False, - 'is_public': True, - 'methods': list([ - 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function', - ]), - 'name': 'yetAnotherClass', - 'reexported_by': list([ - ]), - 'superclasses': list([ - ]), - 'type_parameters': list([ - ]), - }), - dict({ - 'attributes': list([ - 'tests/data/main_package/main_module/ModuleClass/attr_1', - 'tests/data/main_package/main_module/ModuleClass/init_attr', - 'tests/data/main_package/main_module/ModuleClass/_init_attr_private', - ]), - 'classes': list([ - 'tests/data/main_package/main_module/ModuleClass/NestedClass', - ]), 'constructor': dict({ 'docstring': dict({ - 'description': ''' - Summary of the init description. - - Full init description. - ''', + 'description': '', 'examples': list([ ]), 'full_docstring': ''' - Summary of the init description. - - Full init description. + Attributes: + attribute1 (float): + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 (str): + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. ''', }), - 'id': 'tests/data/main_package/main_module/ModuleClass/__init__', + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/__init__', 'is_class_method': False, 'is_property': False, 'is_public': True, 'is_static': False, 'name': '__init__', 'parameters': list([ - 'tests/data/main_package/main_module/ModuleClass/__init__/self', - 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/__init__/self', ]), 'reexported_by': list([ ]), @@ -186,155 +112,523 @@ }), 'docstring': dict({ 'description': ''' - Summary of the description. + ClassAndConstructorWithParameters - Full description + Dolor sit amet. ''', 'examples': list([ ]), 'full_docstring': ''' - Summary of the description. + ClassAndConstructorWithParameters - Full description + Dolor sit amet. + + Attributes: + attribute1 (float): + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 (str): + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. ''', }), - 'id': 'tests/data/main_package/main_module/ModuleClass', + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes', 'inherits_from_exception': False, 'is_public': True, 'methods': list([ - 'tests/data/main_package/main_module/ModuleClass/_some_function', ]), - 'name': 'ModuleClass', + 'name': 'ClassWithAttributes', 'reexported_by': list([ ]), 'superclasses': list([ - 'tests.data.main_package.another_path.another_module.AnotherClass', ]), 'type_parameters': list([ ]), }), + ]), + 'distribution': '', + 'enum_instances': list([ + ]), + 'enums': list([ + ]), + 'functions': list([ dict({ - 'attributes': list([ - ]), - 'classes': list([ - ]), - 'constructor': None, 'docstring': dict({ 'description': '', 'examples': list([ ]), - 'full_docstring': '', + 'full_docstring': ''' + Attributes: + attribute1 (float): + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 (str): + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', }), - 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass', - 'inherits_from_exception': False, + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/__init__', + 'is_class_method': False, + 'is_property': False, 'is_public': True, - 'methods': list([ - 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function', + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/__init__/self', ]), - 'name': 'NestedClass', 'reexported_by': list([ ]), - 'superclasses': list([ - 'another_path.another_module.AnotherClass', - ]), - 'type_parameters': list([ + 'results': list([ ]), }), dict({ - 'attributes': list([ - 'tests/data/main_package/main_module/_PrivateClass/public_attr_in_private_class', - 'tests/data/main_package/main_module/_PrivateClass/public_init_attr_in_private_class', - ]), - 'classes': list([ - 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass', - ]), - 'constructor': dict({ - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__', - 'is_class_method': False, - 'is_property': False, - 'is_public': False, - 'is_static': False, - 'name': '__init__', - 'parameters': list([ - 'tests/data/main_package/main_module/_PrivateClass/__init__/self', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - ]), - }), 'docstring': dict({ - 'description': '', + 'description': 'Lorem ipsum', 'examples': list([ ]), - 'full_docstring': '', + 'full_docstring': ''' + Lorem ipsum + + Args: + param1 (str): If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + param2 (str or bool): Valid values are [False, None, 'sparse matrix'] + param3 (float): Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + param4 (int or float): If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + ''', }), - 'id': 'tests/data/main_package/main_module/_PrivateClass', - 'inherits_from_exception': False, - 'is_public': False, - 'methods': list([ - 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class', + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'global_func1_google', + 'parameters': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param1', + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param2', + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param3', + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param4', ]), - 'name': '_PrivateClass', 'reexported_by': list([ ]), - 'superclasses': list([ - ]), - 'type_parameters': list([ + 'results': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/result_1', ]), }), + ]), + 'modules': list([ dict({ - 'attributes': list([ - 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/nested_class_attr', - ]), 'classes': list([ - 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/NestedNestedPrivateClass', ]), - 'constructor': None, - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass', - 'inherits_from_exception': False, - 'is_public': False, - 'methods': list([ - 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/static_nested_private_class_function', + 'docstring': '', + 'enums': list([ ]), - 'name': 'NestedPrivateClass', - 'reexported_by': list([ + 'functions': list([ ]), - 'superclasses': list([ + 'id': 'tests/data/boundary_enum_package_googledoc', + 'name': '__init__', + 'qualified_imports': list([ ]), - 'type_parameters': list([ + 'wildcard_imports': list([ + ]), + }), + dict({ + 'classes': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes', ]), + 'docstring': 'GoogleDoc Docstring of the main_module_googledoc.py module.', + 'enums': list([ + ]), + 'functions': list([ + 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google', + ]), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc', + 'name': 'main_module_googledoc', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + ]), + 'package': 'boundary_enum_package_googledoc', + 'parameters': list([ + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/ClassWithAttributes/__init__/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': ''' + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + 'valid_values': list([ + '"constant"', + '"mean"', + '"median"', + '"most_frequent"', + ]), + }), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param1', + 'is_optional': False, + 'name': 'param1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': "Valid values are [False, None, 'sparse matrix']", + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + ]), + }), + 'valid_values': list([ + '"sparse matrix"', + 'False', + 'None', + 'True', + ]), + }), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param2', + 'is_optional': False, + 'name': 'param2', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + ]), + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': False, + 'min': 0.5, + 'min_inclusive': True, + }), + ]), + 'default_value': '', + 'description': ''' + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param3', + 'is_optional': False, + 'name': 'param3', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': True, + 'min': 0.0, + 'min_inclusive': False, + }), + dict({ + 'base_type': 'int', + 'kind': 'BoundaryType', + 'max': 10, + 'max_inclusive': True, + 'min': 0, + 'min_inclusive': True, + }), + ]), + 'default_value': '', + 'description': ''' + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + ''', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + ]), + }), + 'valid_values': list([ + 'None', + ]), + }), + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/param4', + 'is_optional': False, + 'name': 'param4', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + ]), + }), + }), + ]), + 'results': list([ + dict({ + 'id': 'tests/data/boundary_enum_package_googledoc/main_module_googledoc/global_func1_google/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'test', + 'qname': 'tests.data.boundary_enum_package_googledoc.main_module_googledoc.test', + }), + }), + ]), + 'schemaVersion': 1, + 'version': '', + }) +# --- +# name: test_main[test_numpydoc_with_boundary_enum_extractor] + dict({ + 'attributes': list([ + dict({ + 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': False, + 'min': 0.5, + 'min_inclusive': True, + }), + ]), + 'description': ''' + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/attribute1', + 'is_public': True, + 'is_static': False, + 'name': 'attribute1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': ''' + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + 'valid_values': list([ + '"constant"', + '"mean"', + '"median"', + '"most_frequent"', + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/attribute2', + 'is_public': True, + 'is_static': False, + 'name': 'attribute2', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), }), + ]), + 'classes': list([ dict({ 'attributes': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/attribute1', + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/attribute2', ]), 'classes': list([ ]), - 'constructor': None, + 'constructor': dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': ''' + Attributes + ---------- + attribute1 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/__init__/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), 'docstring': dict({ - 'description': '', + 'description': ''' + ClassAndConstructorWithParameters + + Dolor sit amet. + ''', 'examples': list([ ]), - 'full_docstring': '', + 'full_docstring': ''' + ClassAndConstructorWithParameters + + Dolor sit amet. + + Attributes + ---------- + attribute1 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/NestedNestedPrivateClass', + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes', 'inherits_from_exception': False, - 'is_public': False, + 'is_public': True, 'methods': list([ ]), - 'name': 'NestedNestedPrivateClass', + 'name': 'ClassWithAttributes', 'reexported_by': list([ ]), 'superclasses': list([ @@ -342,283 +636,2482 @@ 'type_parameters': list([ ]), }), - ]), - 'distribution': '', - 'enum_instances': list([ - ]), - 'enums': list([ - ]), - 'functions': list([ + ]), + 'distribution': '', + 'enum_instances': list([ + ]), + 'enums': list([ + ]), + 'functions': list([ + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': ''' + Attributes + ---------- + attribute1 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + attribute2 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/__init__/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': 'Lorem ipsum', + 'examples': list([ + ]), + 'full_docstring': ''' + Lorem ipsum + + Parameters + -------- + param1 : str + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + + param2 : str or bool + Valid values are [False, None, 'sparse matrix'] + + param3 : float + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + + param4 : int or float + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + ''', + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'global_func1_numpy', + 'parameters': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param1', + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param2', + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param3', + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param4', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/result_1', + ]), + }), + ]), + 'modules': list([ + dict({ + 'classes': list([ + ]), + 'docstring': '', + 'enums': list([ + ]), + 'functions': list([ + ]), + 'id': 'tests/data/boundary_enum_package_numpydoc', + 'name': '__init__', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + dict({ + 'classes': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes', + ]), + 'docstring': 'NumpyDoc Docstring of the main_module_numpydoc.py module.', + 'enums': list([ + ]), + 'functions': list([ + 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy', + ]), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc', + 'name': 'main_module_numpydoc', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + ]), + 'package': 'boundary_enum_package_numpydoc', + 'parameters': list([ + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/ClassWithAttributes/__init__/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': ''' + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + 'valid_values': list([ + '"constant"', + '"mean"', + '"median"', + '"most_frequent"', + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param1', + 'is_optional': False, + 'name': 'param1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': "Valid values are [False, None, 'sparse matrix']", + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + ]), + }), + 'valid_values': list([ + '"sparse matrix"', + 'False', + 'None', + 'True', + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param2', + 'is_optional': False, + 'name': 'param2', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + ]), + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': False, + 'min': 0.5, + 'min_inclusive': True, + }), + ]), + 'default_value': '', + 'description': ''' + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + ''', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param3', + 'is_optional': False, + 'name': 'param3', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '', + 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': True, + 'min': 0.0, + 'min_inclusive': False, + }), + dict({ + 'base_type': 'int', + 'kind': 'BoundaryType', + 'max': 10, + 'max_inclusive': True, + 'min': 0, + 'min_inclusive': True, + }), + ]), + 'default_value': '', + 'description': ''' + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + ''', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + ]), + }), + 'valid_values': list([ + 'None', + ]), + }), + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/param4', + 'is_optional': False, + 'name': 'param4', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + ]), + }), + }), + ]), + 'results': list([ + dict({ + 'id': 'tests/data/boundary_enum_package_numpydoc/main_module_numpydoc/global_func1_numpy/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'test', + 'qname': 'tests.data.boundary_enum_package_numpydoc.main_module_numpydoc.test', + }), + }), + ]), + 'schemaVersion': 1, + 'version': '', + }) +# --- +# name: test_main[test_plaintext] + dict({ + 'attributes': list([ + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/_init_attr_private', + 'is_public': False, + 'is_static': False, + 'name': '_init_attr_private', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/attr_1', + 'is_public': True, + 'is_static': True, + 'name': 'attr_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/init_attr', + 'is_public': True, + 'is_static': False, + 'name': 'init_attr', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/nested_class_attr', + 'is_public': False, + 'is_static': True, + 'name': 'nested_class_attr', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/public_attr_in_private_class', + 'is_public': False, + 'is_static': True, + 'name': 'public_attr_in_private_class', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + dict({ + 'docstring': dict({ + 'boundaries': list([ + ]), + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/public_init_attr_in_private_class', + 'is_public': False, + 'is_static': False, + 'name': 'public_init_attr_in_private_class', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + ]), + 'classes': list([ + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/another_path/another_module/AnotherClass', + 'inherits_from_exception': False, + 'is_public': True, + 'methods': list([ + ]), + 'name': 'AnotherClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass', + 'inherits_from_exception': False, + 'is_public': True, + 'methods': list([ + 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function', + ]), + 'name': 'yetAnotherClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + 'tests/data/main_package/main_module/ModuleClass/attr_1', + 'tests/data/main_package/main_module/ModuleClass/init_attr', + 'tests/data/main_package/main_module/ModuleClass/_init_attr_private', + ]), + 'classes': list([ + 'tests/data/main_package/main_module/ModuleClass/NestedClass', + ]), + 'constructor': dict({ + 'docstring': dict({ + 'description': ''' + Summary of the init description. + + Full init description. + ''', + 'examples': list([ + ]), + 'full_docstring': ''' + Summary of the init description. + + Full init description. + ''', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/main_package/main_module/ModuleClass/__init__/self', + 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + 'docstring': dict({ + 'description': ''' + Summary of the description. + + Full description + ''', + 'examples': list([ + ]), + 'full_docstring': ''' + Summary of the description. + + Full description + ''', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass', + 'inherits_from_exception': False, + 'is_public': True, + 'methods': list([ + 'tests/data/main_package/main_module/ModuleClass/_some_function', + ]), + 'name': 'ModuleClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + 'tests.data.main_package.another_path.another_module.AnotherClass', + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass', + 'inherits_from_exception': False, + 'is_public': True, + 'methods': list([ + 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function', + ]), + 'name': 'NestedClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + 'another_path.another_module.AnotherClass', + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + 'tests/data/main_package/main_module/_PrivateClass/public_attr_in_private_class', + 'tests/data/main_package/main_module/_PrivateClass/public_init_attr_in_private_class', + ]), + 'classes': list([ + 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass', + ]), + 'constructor': dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/main_package/main_module/_PrivateClass/__init__/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass', + 'inherits_from_exception': False, + 'is_public': False, + 'methods': list([ + 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class', + ]), + 'name': '_PrivateClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/nested_class_attr', + ]), + 'classes': list([ + 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/NestedNestedPrivateClass', + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass', + 'inherits_from_exception': False, + 'is_public': False, + 'methods': list([ + 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/static_nested_private_class_function', + ]), + 'name': 'NestedPrivateClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/NestedNestedPrivateClass', + 'inherits_from_exception': False, + 'is_public': False, + 'methods': list([ + ]), + 'name': 'NestedNestedPrivateClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), + ]), + 'distribution': '', + 'enum_instances': list([ + ]), + 'enums': list([ + ]), + 'functions': list([ + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'another_function', + 'parameters': list([ + 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/result_1', + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'nested_class_function', + 'parameters': list([ + 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/self', + 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/param_1', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/result_1', + ]), + }), + dict({ + 'docstring': dict({ + 'description': ''' + Summary of the init description. + + Full init description. + ''', + 'examples': list([ + ]), + 'full_docstring': ''' + Summary of the init description. + + Full init description. + ''', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/main_package/main_module/ModuleClass/__init__/self', + 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': ''' + Function Docstring. + + Parameters + --------- + param_2 : bool, optional. + Valid values are True and False. + ''', + 'examples': list([ + ]), + 'full_docstring': ''' + Function Docstring. + + Parameters + --------- + param_2 : bool, optional. + Valid values are True and False. + ''', + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '_some_function', + 'parameters': list([ + 'tests/data/main_package/main_module/ModuleClass/_some_function/self', + 'tests/data/main_package/main_module/ModuleClass/_some_function/param_1', + 'tests/data/main_package/main_module/ModuleClass/_some_function/param_2', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/main_package/main_module/ModuleClass/_some_function/result_1', + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/static_nested_private_class_function', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': True, + 'name': 'static_nested_private_class_function', + 'parameters': list([ + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/main_package/main_module/_PrivateClass/__init__/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': 'public_func_in_private_class', + 'parameters': list([ + 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/main_package/main_module/_private_global_func', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '_private_global_func', + 'parameters': list([ + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/main_package/main_module/_private_global_func/result_1', + ]), + }), + dict({ + 'docstring': dict({ + 'description': ''' + Docstring 1. + + Docstring 2. + ''', + 'examples': list([ + ]), + 'full_docstring': ''' + Docstring 1. + + Docstring 2. + ''', + }), + 'id': 'tests/data/main_package/main_module/global_func', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'global_func', + 'parameters': list([ + 'tests/data/main_package/main_module/global_func/main_test_param_1', + 'tests/data/main_package/main_module/global_func/main_test_param_2', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'tests/data/main_package/main_module/global_func/result_1', + ]), + }), + ]), + 'modules': list([ + dict({ + 'classes': list([ + ]), + 'docstring': '', + 'enums': list([ + ]), + 'functions': list([ + ]), + 'id': 'tests/data/main_package', + 'name': '__init__', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + dict({ + 'classes': list([ + ]), + 'docstring': '', + 'enums': list([ + ]), + 'functions': list([ + ]), + 'id': 'tests/data/main_package/another_path', + 'name': '__init__', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + dict({ + 'classes': list([ + 'tests/data/main_package/another_path/another_module/AnotherClass', + 'tests/data/main_package/another_path/another_module/yetAnotherClass', + ]), + 'docstring': ''' + Another Module Docstring. + + Full Docstring Description + + ''', + 'enums': list([ + ]), + 'functions': list([ + ]), + 'id': 'tests/data/main_package/another_path/another_module', + 'name': 'another_module', + 'qualified_imports': list([ + ]), + 'wildcard_imports': list([ + ]), + }), + dict({ + 'classes': list([ + 'tests/data/main_package/main_module/ModuleClass', + 'tests/data/main_package/main_module/_PrivateClass', + ]), + 'docstring': 'Docstring of the some_class.py module.', + 'enums': list([ + ]), + 'functions': list([ + 'tests/data/main_package/main_module/global_func', + 'tests/data/main_package/main_module/_private_global_func', + ]), + 'id': 'tests/data/main_package/main_module', + 'name': 'main_module', + 'qualified_imports': list([ + dict({ + 'alias': 'mathematics', + 'qualified_name': 'math', + }), + dict({ + 'alias': None, + 'qualified_name': 'mypy', + }), + dict({ + 'alias': None, + 'qualified_name': 'another_path.another_module.AnotherClass', + }), + dict({ + 'alias': '_AcImportAlias', + 'qualified_name': 'another_path.another_module.AnotherClass', + }), + ]), + 'wildcard_imports': list([ + ]), + }), + ]), + 'package': 'main_package', + 'parameters': list([ + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/param_1', + 'is_optional': False, + 'name': 'param_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', + 'is_optional': False, + 'name': 'init_param_1', + 'type': None, + }), + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/__init__/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/param_1', + 'is_optional': False, + 'name': 'param_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': False, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/param_2', + 'is_optional': True, + 'name': 'param_2', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + }), + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': '"first param"', + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/global_func/main_test_param_1', + 'is_optional': True, + 'name': 'main_test_param_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'boundaries': list([ + ]), + 'default_value': '', + 'description': '', + 'type': None, + 'valid_values': list([ + ]), + }), + 'id': 'tests/data/main_package/main_module/global_func/main_test_param_2', + 'is_optional': True, + 'name': 'main_test_param_2', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), + ]), + 'results': list([ + dict({ + 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + }), + dict({ + 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'SetType', + 'types': list([ + dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + ]), + }), + }), + dict({ + 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), + dict({ + 'id': 'tests/data/main_package/main_module/_private_global_func/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + ]), + }), + }), + dict({ + 'id': 'tests/data/main_package/main_module/global_func/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), + ]), + 'schemaVersion': 1, + 'version': '', + }) +# --- +# name: test_main[test_purity_analysis] + dict({ + 'purity_package.__init__': dict({ + }), + 'purity_package.another_purity_path.__init__': dict({ + }), + 'purity_package.another_purity_path.another_purity_module': dict({ + 'purity_package.another_purity_path.another_purity_module.ClassWithNestedClassAsMember.145.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__add__.54.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__add__.78.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__enter__.209.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'FileWrite': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.__exit__.213.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'FileWrite': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.__init__.135.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.146.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.182.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.189.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.193.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.__init__.197.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.201.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.__init__.205.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.__init__.8.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.double_function_impure.169.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.double_function_pure.166.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_impure.116.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_impure.51.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_impure.75.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_impure.90.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_pure.113.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_pure.48.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_pure.72.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_child_and_child_of_child_pure.87.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_impure.24.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_impure.45.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_impure.69.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_impure.122.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_impure.30.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_impure.96.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_pure.119.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_pure.27.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_of_child_pure.93.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_pure.21.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_pure.42.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.in_super_and_child_pure.66.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.only_in_T.39.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.only_in_T.63.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.only_in_child.110.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.only_in_child.84.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.only_in_child_self.125.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.only_in_child_self.99.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.only_in_super_impure.18.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.only_in_super_nested_call_impure.142.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.only_in_super_nested_call_pure.139.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.only_in_super_pure.15.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.recursive_function.163.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.return_class_impure.157.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.return_class_pure.160.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.same_name.11.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.same_name.185.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.same_name.36.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.same_name.60.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.super_impure.172.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'UnknownCall': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.super_pure.176.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'UnknownCall': 1, + }), + }), + 'purity_package.another_purity_path.another_purity_module.super_same_name.103.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.another_purity_path.another_purity_module.super_same_name.129.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + }), + 'purity_package.main_purity_module': dict({ + 'purity_package.main_purity_module.closure.739.4': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.closure.748.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.closure.754.4': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_all_functions_pure.133.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_call_another_global_func_impure.762.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_call_another_global_func_pure.759.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_call_ref_in_dict_generation_impure.769.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_call_ref_in_dict_generation_pure.765.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_call_reference_in_index_impure.576.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_call_reference_in_index_pure.570.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_closure_impure.753.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_closure_pure.738.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_closure_should_be_pure_but_impure.743.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_generator_impure.662.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_generator_pure.657.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_iterable_is_call_ref_impure.682.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_iterable_is_call_ref_pure.677.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_with_if_generator_impure.672.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_dict_comprehension_with_if_generator_pure.667.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_double_function_impure.302.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_double_function_impure.310.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_find_deeply_nested_function_impure.148.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_dict_same_name_impure.507.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_dict_same_name_pure.496.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_dict_with_union_same_name_impure.529.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_dict_with_union_same_name_pure.518.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_list_same_name_impure.419.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_list_same_name_pure.408.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_multiple_tuple_same_name_impure.485.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_multiple_tuple_same_name_pure.474.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_tuple_same_name_impure.441.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_as_tuple_same_name_pure.430.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_docstring_same_name_impure.397.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_docstring_same_name_pure.386.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_parameter_as_multiple_tuple_same_name_impure.463.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_parameter_as_multiple_tuple_same_name_pure.452.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_from_parameter_same_name_impure.184.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_parameter_same_name_nested_impure.196.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 2, + }), + }), + 'purity_package.main_purity_module.global_func_from_parameter_same_name_nested_pure_but_impure.188.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_from_parameter_same_name_pure.180.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_import_global_func_impure.779.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_in_child_and_child_of_child_from_T_impure.103.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 3, + }), + }), + 'purity_package.main_purity_module.global_func_in_child_and_child_of_child_from_T_pure.93.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_in_child_and_child_of_child_from_child_of_child_impure.114.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_in_child_and_child_of_child_from_child_of_child_pure.109.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_in_super_and_child_from_T_impure.88.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_in_super_and_child_from_T_pure.83.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_in_super_and_child_from_super_impure.74.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_in_super_and_child_of_child_from_T_impure.124.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_in_super_and_child_of_child_from_T_pure.119.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_init_import_impure.776.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_init_import_pure.773.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_inside_of_lambda_with_map_impure.806.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_inside_of_lambda_with_map_should_be_pure_but_impure.798.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_isinstance_check_impure.730.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_isinstance_check_should_be_pure_but_impure.722.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_list_comprehension_generator_impure.632.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_list_comprehension_generator_pure.627.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_list_comprehension_iterable_is_call_ref_impure.652.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_list_comprehension_iterable_is_call_ref_pure.647.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_list_comprehension_with_if_generator_impure.642.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_list_comprehension_with_if_generator_pure.637.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_module_class_with_static_method_impure.785.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_module_class_with_static_method_pure.782.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_multiple_nested_member_impure.247.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 3, + }), + }), + 'purity_package.main_purity_module.global_func_multiple_nested_member_should_be_pure_but_impure.238.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 2, + }), + }), + 'purity_package.main_purity_module.global_func_multiple_nested_methods_impure.257.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_multiple_nested_methods_pure.252.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_multiple_recursion_impure.288.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_multiple_recursion_impure.297.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 2, + }), + }), + 'purity_package.main_purity_module.global_func_nested_class_impure.175.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 2, + }), + }), + 'purity_package.main_purity_module.global_func_nested_class_should_be_pure_but_impure.166.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_from_second_call_reference_impure.205.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_from_second_call_reference_pure.200.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_nested_member_from_super_impure.275.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_member_from_super_impure.283.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 2, + }), + }), + 'purity_package.main_purity_module.global_func_nested_method_from_super_impure.270.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_method_from_super_pure.262.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_nested_with_dict_impure.233.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'NonLocalVariableWrite': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_with_dict_should_be_pure_but_impure.224.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableWrite': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_with_list_impure.219.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'NonLocalVariableWrite': 1, + }), + }), + 'purity_package.main_purity_module.global_func_nested_with_list_should_be_pure_but_impure.210.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableWrite': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_Child_impure.49.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_Child_pure.44.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_only_in_T_from_Child_impure.39.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_T_from_Child_pure.34.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_only_in_T_impure.29.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_T_pure.20.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_only_in_super_from_T_impure.59.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_super_from_T_pure.54.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_only_in_super_from_child_impure.69.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_only_in_super_from_child_pure.64.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_operator_expression_pure.717.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_operator_receiver_with_brackets_impure.821.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_operator_receiver_with_brackets_should_be_pure_but_impure.811.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_return_impure_class.325.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_return_pure_class.318.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_same_name_impure.15.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_same_name_pure.10.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_self_impure.587.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_self_pure.582.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_set_comprehension_generator_impure.692.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_set_comprehension_generator_pure.687.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_set_comprehension_iterable_is_call_ref_impure.712.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_set_comprehension_iterable_is_call_ref_pure.707.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_set_comprehension_with_if_generator_impure.702.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_set_comprehension_with_if_generator_pure.697.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_dict_impure.357.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_dict_pure.352.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_function_impure.328.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_function_pure.321.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_list_impure.337.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_list_pure.332.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_nested_type_and_function_impure.382.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_nested_type_and_function_pure.375.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_nested_type_impure.367.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_nested_type_pure.362.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_start_with_tuple_impure.347.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_start_with_tuple_pure.342.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_super_from_builtin_keyError_pure.623.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_super_init_impure.596.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_super_init_pure.592.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_super_method_impure.605.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_super_method_pure.600.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_super_nested_method_impure.618.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_super_nested_method_should_be_pure_but_impure.610.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + 'UnknownCall': 1, + }), + }), + 'purity_package.main_purity_module.global_func_union_type_from_docstring_impure.559.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_union_type_from_docstring_pure.548.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_union_type_impure.544.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_union_type_pure.540.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_unreachable_code_impure.793.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_unreachable_code_pure.788.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_func_with_keyword_impure.832.0': dict({ + 'purity': 'Impure', + 'reasons': dict({ + 'NonLocalVariableRead': 1, + }), + }), + 'purity_package.main_purity_module.global_func_with_keyword_pure.827.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_helper_func_nested_type_impure_with_list_and_dict.379.0': dict({ + 'purity': 'Pure', + }), + 'purity_package.main_purity_module.global_helper_func_nested_type_pure_with_list_and_dict.372.0': dict({ + 'purity': 'Pure', + }), + }), + }) +# --- +# name: test_main[test_restdoc_with_boundary_enum_extractor] + dict({ + 'attributes': list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', - 'examples': list([ + 'type': None, + 'valid_values': list([ ]), - 'full_docstring': '', }), - 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function', - 'is_class_method': False, - 'is_property': False, + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/attribute1', 'is_public': True, 'is_static': False, - 'name': 'another_function', - 'parameters': list([ - 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/self', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/result_1', - ]), + 'name': 'attribute1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', - 'examples': list([ + 'type': None, + 'valid_values': list([ ]), - 'full_docstring': '', }), - 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function', - 'is_class_method': False, - 'is_property': False, + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/attribute2', 'is_public': True, 'is_static': False, - 'name': 'nested_class_function', - 'parameters': list([ - 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/self', - 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/param_1', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/result_1', - ]), + 'name': 'attribute2', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), }), + ]), + 'classes': list([ dict({ - 'docstring': dict({ - 'description': ''' - Summary of the init description. - - Full init description. - ''', - 'examples': list([ - ]), - 'full_docstring': ''' - Summary of the init description. - - Full init description. - ''', - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/__init__', - 'is_class_method': False, - 'is_property': False, - 'is_public': True, - 'is_static': False, - 'name': '__init__', - 'parameters': list([ - 'tests/data/main_package/main_module/ModuleClass/__init__/self', - 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', - ]), - 'reexported_by': list([ + 'attributes': list([ + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/attribute1', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/attribute2', ]), - 'results': list([ + 'classes': list([ ]), - }), - dict({ + 'constructor': dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': ''' + :param attribute1: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type attribute1: float + :param attribute2: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type attribute2: str + ''', + }), + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/__init__', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': '__init__', + 'parameters': list([ + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/__init__/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), 'docstring': dict({ 'description': ''' - Function Docstring. + ClassAndConstructorWithParameters - param_2: bool. + Dolor sit amet. ''', 'examples': list([ ]), 'full_docstring': ''' - Function Docstring. + ClassAndConstructorWithParameters - param_2: bool. + Dolor sit amet. + + :param attribute1: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type attribute1: float + :param attribute2: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type attribute2: str ''', }), - 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function', - 'is_class_method': False, - 'is_property': False, - 'is_public': False, - 'is_static': False, - 'name': '_some_function', - 'parameters': list([ - 'tests/data/main_package/main_module/ModuleClass/_some_function/self', - 'tests/data/main_package/main_module/ModuleClass/_some_function/param_1', - 'tests/data/main_package/main_module/ModuleClass/_some_function/param_2', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes', + 'inherits_from_exception': False, + 'is_public': True, + 'methods': list([ ]), + 'name': 'ClassWithAttributes', 'reexported_by': list([ ]), - 'results': list([ - 'tests/data/main_package/main_module/ModuleClass/_some_function/result_1', - ]), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/NestedPrivateClass/static_nested_private_class_function', - 'is_class_method': False, - 'is_property': False, - 'is_public': False, - 'is_static': True, - 'name': 'static_nested_private_class_function', - 'parameters': list([ - ]), - 'reexported_by': list([ + 'superclasses': list([ ]), - 'results': list([ + 'type_parameters': list([ ]), }), + ]), + 'distribution': '', + 'enum_instances': list([ + ]), + 'enums': list([ + ]), + 'functions': list([ dict({ 'docstring': dict({ 'description': '', 'examples': list([ ]), - 'full_docstring': '', + 'full_docstring': ''' + :param attribute1: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type attribute1: float + :param attribute2: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type attribute2: str + ''', }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/__init__', 'is_class_method': False, 'is_property': False, - 'is_public': False, + 'is_public': True, 'is_static': False, 'name': '__init__', 'parameters': list([ - 'tests/data/main_package/main_module/_PrivateClass/__init__/self', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - ]), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class', - 'is_class_method': False, - 'is_property': False, - 'is_public': False, - 'is_static': False, - 'name': 'public_func_in_private_class', - 'parameters': list([ - 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class/self', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - ]), - }), - dict({ - 'docstring': dict({ - 'description': '', - 'examples': list([ - ]), - 'full_docstring': '', - }), - 'id': 'tests/data/main_package/main_module/_private_global_func', - 'is_class_method': False, - 'is_property': False, - 'is_public': False, - 'is_static': False, - 'name': '_private_global_func', - 'parameters': list([ + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/__init__/self', ]), 'reexported_by': list([ ]), 'results': list([ - 'tests/data/main_package/main_module/_private_global_func/result_1', ]), }), dict({ 'docstring': dict({ - 'description': ''' - Docstring 1. - - Docstring 2. - ''', + 'description': 'Lorem ipsum', 'examples': list([ ]), 'full_docstring': ''' - Docstring 1. + Lorem ipsum - Docstring 2. + :param param1: + If "mean", then replace missing values using the mean along each column. + If "median", then replace missing values using the median along each column. + If "most_frequent", then replace missing using the most frequent value along each column. + If "constant", then replace missing values with fill_value. + :type param1: int + :param param2: Valid values are [False, None, 'sparse matrix'] + :type param2: str or bool + :param param3: + Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative + to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when + updating these values (messages). + :type param3: float + :param param4: + If bootstrap is True, the number of samples to draw from X to train each base estimator. + If None (default), then draw X.shape[0] samples. + If int, then max_samples values in [0, 10]. + If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0]. + :type param4: int or float ''', }), - 'id': 'tests/data/main_package/main_module/global_func', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest', 'is_class_method': False, 'is_property': False, 'is_public': True, 'is_static': False, - 'name': 'global_func', + 'name': 'global_func1_rest', 'parameters': list([ - 'tests/data/main_package/main_module/global_func/main_test_param_1', - 'tests/data/main_package/main_module/global_func/main_test_param_2', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param1', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param2', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param3', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param4', ]), 'reexported_by': list([ ]), 'results': list([ - 'tests/data/main_package/main_module/global_func/result_1', - ]), - }), - ]), - 'modules': list([ - dict({ - 'classes': list([ - ]), - 'docstring': '', - 'enums': list([ - ]), - 'functions': list([ - ]), - 'id': 'tests/data/main_package', - 'name': '__init__', - 'qualified_imports': list([ - ]), - 'wildcard_imports': list([ - ]), - }), - dict({ - 'classes': list([ - ]), - 'docstring': '', - 'enums': list([ - ]), - 'functions': list([ - ]), - 'id': 'tests/data/main_package/another_path', - 'name': '__init__', - 'qualified_imports': list([ - ]), - 'wildcard_imports': list([ + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/result_1', ]), }), - dict({ - 'classes': list([ - 'tests/data/main_package/another_path/another_module/AnotherClass', - 'tests/data/main_package/another_path/another_module/yetAnotherClass', - ]), - 'docstring': ''' - Another Module Docstring. - - Full Docstring Description - - ''', + ]), + 'modules': list([ + dict({ + 'classes': list([ + ]), + 'docstring': '', 'enums': list([ ]), 'functions': list([ ]), - 'id': 'tests/data/main_package/another_path/another_module', - 'name': 'another_module', + 'id': 'tests/data/boundary_enum_package_restdoc', + 'name': '__init__', 'qualified_imports': list([ ]), 'wildcard_imports': list([ @@ -626,303 +3119,223 @@ }), dict({ 'classes': list([ - 'tests/data/main_package/main_module/ModuleClass', - 'tests/data/main_package/main_module/_PrivateClass', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes', ]), - 'docstring': 'Docstring of the some_class.py module.', + 'docstring': 'ReST Docstring of the main_module_restdoc.py module.', 'enums': list([ ]), 'functions': list([ - 'tests/data/main_package/main_module/global_func', - 'tests/data/main_package/main_module/_private_global_func', + 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest', ]), - 'id': 'tests/data/main_package/main_module', - 'name': 'main_module', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc', + 'name': 'main_module_restdoc', 'qualified_imports': list([ - dict({ - 'alias': 'mathematics', - 'qualified_name': 'math', - }), - dict({ - 'alias': None, - 'qualified_name': 'mypy', - }), - dict({ - 'alias': None, - 'qualified_name': 'another_path.another_module.AnotherClass', - }), - dict({ - 'alias': '_AcImportAlias', - 'qualified_name': 'another_path.another_module.AnotherClass', - }), ]), 'wildcard_imports': list([ ]), }), ]), - 'package': 'main_package', + 'package': 'boundary_enum_package_restdoc', 'parameters': list([ dict({ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), - 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/self', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/ClassWithAttributes/__init__/self', 'is_optional': False, 'name': 'self', 'type': None, }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': None, + 'default_value': '', 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', - 'description': '', - 'type': None, + 'description': 'If "mean", then replace missing values using the mean along each column. If "median", then replace missing values using the median along each column. If "most_frequent", then replace missing using the most frequent value along each column. If "constant", then replace missing values with fill_value.', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + 'valid_values': list([ + '"constant"', + '"mean"', + '"median"', + '"most_frequent"', + ]), }), - 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/param_1', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param1', 'is_optional': False, - 'name': 'param_1', + 'name': 'param1', 'type': dict({ 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), }), - dict({ - 'assigned_by': 'IMPLICIT', - 'default_value': None, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/self', - 'is_optional': False, - 'name': 'self', - 'type': None, - }), - dict({ - 'assigned_by': 'POSITION_OR_NAME', - 'default_value': None, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/__init__/init_param_1', - 'is_optional': False, - 'name': 'init_param_1', - 'type': None, - }), - dict({ - 'assigned_by': 'IMPLICIT', - 'default_value': None, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/__init__/self', - 'is_optional': False, - 'name': 'self', - 'type': None, - }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': None, + 'default_value': '', 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', - 'description': '', - 'type': None, + 'description': "Valid values are [False, None, 'sparse matrix']", + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + ]), + }), + 'valid_values': list([ + '"sparse matrix"', + 'False', + 'None', + 'True', + ]), }), - 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/param_1', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param2', 'is_optional': False, - 'name': 'param_1', + 'name': 'param2', 'type': dict({ - 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'bool', + 'qname': 'builtins.bool', + }), + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + ]), }), }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': False, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/param_2', - 'is_optional': True, - 'name': 'param_2', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'bool', - 'qname': 'builtins.bool', - }), - }), - dict({ - 'assigned_by': 'IMPLICIT', - 'default_value': None, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/self', - 'is_optional': False, - 'name': 'self', - 'type': None, - }), - dict({ - 'assigned_by': 'IMPLICIT', - 'default_value': None, - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/__init__/self', - 'is_optional': False, - 'name': 'self', - 'type': None, - }), - dict({ - 'assigned_by': 'IMPLICIT', - 'default_value': None, + 'default_value': '', 'docstring': dict({ + 'boundaries': list([ + dict({ + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': False, + 'min': 0.5, + 'min_inclusive': True, + }), + ]), 'default_value': '', - 'description': '', - 'type': None, + 'description': 'Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when updating these values (messages).', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + 'valid_values': list([ + ]), }), - 'id': 'tests/data/main_package/main_module/_PrivateClass/public_func_in_private_class/self', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param3', 'is_optional': False, - 'name': 'self', - 'type': None, - }), - dict({ - 'assigned_by': 'POSITION_OR_NAME', - 'default_value': '"first param"', - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/global_func/main_test_param_1', - 'is_optional': True, - 'name': 'main_test_param_1', + 'name': 'param3', 'type': dict({ 'kind': 'NamedType', - 'name': 'str', - 'qname': 'builtins.str', + 'name': 'float', + 'qname': 'builtins.float', }), }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': None, + 'default_value': '', 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': None, - }), - 'id': 'tests/data/main_package/main_module/global_func/main_test_param_2', - 'is_optional': True, - 'name': 'main_test_param_2', - 'type': dict({ - 'kind': 'UnionType', - 'types': list([ + 'boundaries': list([ dict({ - 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + 'base_type': 'float', + 'kind': 'BoundaryType', + 'max': 1.0, + 'max_inclusive': True, + 'min': 0.0, + 'min_inclusive': False, }), dict({ - 'kind': 'NamedType', - 'name': 'None', - 'qname': 'builtins.None', + 'base_type': 'int', + 'kind': 'BoundaryType', + 'max': 10, + 'max_inclusive': True, + 'min': 0, + 'min_inclusive': True, }), ]), - }), - }), - ]), - 'results': list([ - dict({ - 'id': 'tests/data/main_package/another_path/another_module/yetAnotherClass/another_function/result_1', - 'name': 'result_1', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'str', - 'qname': 'builtins.str', - }), - }), - dict({ - 'id': 'tests/data/main_package/main_module/ModuleClass/NestedClass/nested_class_function/result_1', - 'name': 'result_1', - 'type': dict({ - 'kind': 'SetType', - 'types': list([ - dict({ - 'kind': 'UnionType', - 'types': list([ - dict({ - 'kind': 'NamedType', - 'name': 'bool', - 'qname': 'builtins.bool', - }), - dict({ - 'kind': 'NamedType', - 'name': 'None', - 'qname': 'builtins.None', - }), - ]), - }), + 'default_value': '', + 'description': 'If bootstrap is True, the number of samples to draw from X to train each base estimator. If None (default), then draw X.shape[0] samples. If int, then max_samples values in [0, 10]. If float, then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, 1.0].', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + ]), + }), + 'valid_values': list([ + 'None', ]), }), - }), - dict({ - 'id': 'tests/data/main_package/main_module/ModuleClass/_some_function/result_1', - 'name': 'result_1', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', - }), - }), - dict({ - 'id': 'tests/data/main_package/main_module/_private_global_func/result_1', - 'name': 'result_1', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/param4', + 'is_optional': False, + 'name': 'param4', 'type': dict({ 'kind': 'UnionType', 'types': list([ dict({ 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', - }), - dict({ - 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + 'name': 'float', + 'qname': 'builtins.float', }), dict({ 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + 'name': 'int', + 'qname': 'builtins.int', }), ]), }), }), + ]), + 'results': list([ dict({ - 'id': 'tests/data/main_package/main_module/global_func/result_1', + 'id': 'tests/data/boundary_enum_package_restdoc/main_module_restdoc/global_func1_rest/result_1', 'name': 'result_1', 'type': dict({ 'kind': 'NamedType', - 'name': 'AnotherClass', - 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + 'name': 'test', + 'qname': 'tests.data.boundary_enum_package_restdoc.main_module_restdoc.test', }), }), ]), diff --git a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr index 9b81e2a7..a391ff5f 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -3,8 +3,12 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/_ignore_me', 'is_public': False, @@ -18,8 +22,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/_multi_attr_2_private', 'is_public': False, @@ -33,8 +41,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/_multi_attr_4_private', 'is_public': False, @@ -53,8 +65,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/_no_type_hint_private', 'is_public': False, @@ -68,8 +84,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/_type_hint_private', 'is_public': False, @@ -83,8 +103,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/attr_default_value_from_outside_package', 'is_public': True, @@ -98,8 +122,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/attr_type_from_outside_package', 'is_public': True, @@ -113,8 +141,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/bool_attr', 'is_public': True, @@ -128,8 +160,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/callexpr_attr_class', 'is_public': True, @@ -139,8 +175,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/callexpr_attr_function', 'is_public': True, @@ -150,8 +190,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/defined_three_times', 'is_public': True, @@ -165,8 +209,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/dict_attr_1', 'is_public': True, @@ -188,8 +236,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/dict_attr_2', 'is_public': True, @@ -211,8 +263,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/dict_attr_3', 'is_public': True, @@ -254,8 +310,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/final', 'is_public': True, @@ -272,8 +332,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/final_int', 'is_public': True, @@ -293,8 +357,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/final_union', 'is_public': True, @@ -321,8 +389,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/finals', 'is_public': True, @@ -349,8 +421,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/flaot_attr', 'is_public': True, @@ -364,8 +440,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/init_attr', 'is_public': True, @@ -379,8 +459,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/int_or_bool_attr', 'is_public': True, @@ -404,8 +488,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/list_attr_1', 'is_public': True, @@ -419,8 +507,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/list_attr_2', 'is_public': True, @@ -449,8 +541,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/list_attr_3', 'is_public': True, @@ -474,8 +570,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/list_attr_4', 'is_public': True, @@ -509,8 +609,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/literal', 'is_public': True, @@ -525,8 +629,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/mixed_literal_union', 'is_public': True, @@ -574,8 +682,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_1', 'is_public': True, @@ -589,8 +701,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_3', 'is_public': True, @@ -609,8 +725,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_5', 'is_public': True, @@ -624,8 +744,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_6', 'is_public': True, @@ -639,8 +763,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_7', 'is_public': True, @@ -654,8 +782,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multi_attr_8', 'is_public': True, @@ -669,8 +801,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/multiple_literals', 'is_public': True, @@ -708,8 +844,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/no_final_type', 'is_public': True, @@ -719,8 +859,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/no_type_hint_public', 'is_public': True, @@ -734,8 +878,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/none_attr', 'is_public': True, @@ -749,8 +897,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/object_attr', 'is_public': True, @@ -764,8 +916,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/optional', 'is_public': True, @@ -789,8 +945,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/set_attr_1', 'is_public': True, @@ -804,8 +964,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/set_attr_2', 'is_public': True, @@ -834,8 +998,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/set_attr_3', 'is_public': True, @@ -859,8 +1027,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/set_attr_4', 'is_public': True, @@ -894,8 +1066,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/str_attr_with_none_value', 'is_public': True, @@ -909,8 +1085,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/tuple_attr_1', 'is_public': True, @@ -929,8 +1109,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/tuple_attr_2', 'is_public': True, @@ -959,8 +1143,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/tuple_attr_3', 'is_public': True, @@ -984,8 +1172,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/type_hint_public', 'is_public': True, @@ -1003,8 +1195,12 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/class_module/ClassModuleClassD/ClassModuleNestedClassE/_nested_attr_2', 'is_public': False, @@ -1018,8 +1214,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/class_module/ClassModuleClassD/ClassModuleNestedClassE/nested_attr_1', 'is_public': True, @@ -1037,11 +1237,17 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': 'Attribute of the calculator. (Google Style)', 'type': dict({ + 'kind': 'NamedType', 'name': 'str', 'qname': 'builtins.str', }), + 'valid_values': list([ + 'unlistable_str', + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/attr_1', 'is_public': True, @@ -1059,11 +1265,17 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': 'Attribute of the calculator. (Numpy)', 'type': dict({ + 'kind': 'NamedType', 'name': 'str', 'qname': 'builtins.str', }), + 'valid_values': list([ + 'unlistable_str', + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/attr_1', 'is_public': True, @@ -1081,8 +1293,12 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/attr_1', 'is_public': True, @@ -1100,8 +1316,12 @@ list([ dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/class_module/_ClassModulePrivateClassG/_attr_1', 'is_public': False, @@ -1115,8 +1335,12 @@ }), dict({ 'docstring': dict({ + 'boundaries': list([ + ]), 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/class_module/_ClassModulePrivateClassG/_attr_2', 'is_public': False, @@ -3027,9 +3251,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/__init__/init_param', 'is_optional': False, @@ -3040,9 +3268,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/__init__/self', 'is_optional': False, @@ -3057,9 +3289,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/_private/a', 'is_optional': False, @@ -3074,9 +3310,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_method_params/param_1', 'is_optional': False, @@ -3091,9 +3331,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': False, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_method_params/param_2', 'is_optional': True, @@ -3108,9 +3352,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': True, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_method_params/param_3', 'is_optional': True, @@ -3125,9 +3373,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_method_params/self', 'is_optional': False, @@ -3142,9 +3394,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_property_method/self', 'is_optional': False, @@ -3159,9 +3415,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/abstract_module/AbstractModuleClass/abstract_static_method_params/param', 'is_optional': False, @@ -3180,9 +3440,13 @@ 'assigned_by': 'POSITIONAL_VARARG', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/arg/args', 'is_optional': False, @@ -3193,9 +3457,13 @@ 'assigned_by': 'NAMED_VARARG', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/arg/kwargs', 'is_optional': False, @@ -3210,9 +3478,13 @@ 'assigned_by': 'POSITIONAL_VARARG', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/args_type/args', 'is_optional': False, @@ -3232,9 +3504,13 @@ 'assigned_by': 'NAMED_VARARG', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/args_type/kwargs', 'is_optional': False, @@ -3261,9 +3537,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/callable_type/param', 'is_optional': False, @@ -3302,9 +3582,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/class_method/cls', 'is_optional': False, @@ -3319,9 +3603,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/class_method_params/cls', 'is_optional': False, @@ -3332,9 +3620,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/class_method_params/param_1', 'is_optional': False, @@ -3353,12 +3645,18 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Parameter of the calculator. (Google Style)', 'type': dict({ + 'kind': 'NamedType', 'name': 'str', 'qname': 'builtins.str', }), + 'valid_values': list([ + 'unlistable_str', + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/__init__/param_1', 'is_optional': False, @@ -3373,9 +3671,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/__init__/self', 'is_optional': False, @@ -3390,9 +3692,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/google_docstring_func/self', 'is_optional': False, @@ -3403,12 +3709,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'First integer value for the calculation. (Google Style)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/google_docstring_func/x', 'is_optional': False, @@ -3423,12 +3734,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Second integer value for the calculation. (Google Style)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/GoogleDocstringClass/google_docstring_func/y', 'is_optional': False, @@ -3447,9 +3763,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': '"String"', 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/illegal_params/_', 'is_optional': True, @@ -3464,9 +3784,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/illegal_params/dct', 'is_optional': False, @@ -3489,9 +3813,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/illegal_params/lst', 'is_optional': False, @@ -3516,9 +3844,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/illegal_params/lst_2', 'is_optional': False, @@ -3548,9 +3880,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/illegal_params/tpl', 'is_optional': False, @@ -3589,9 +3925,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/type_var_module/multiple_type_var/a', 'is_optional': False, @@ -3606,9 +3946,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/type_var_module/multiple_type_var/b', 'is_optional': False, @@ -3627,9 +3971,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/FunctionModuleClassC/nested_class_function/param1', 'is_optional': False, @@ -3644,9 +3992,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/FunctionModuleClassC/nested_class_function/self', 'is_optional': False, @@ -3661,12 +4013,18 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Parameter of the calculator. (Numpy)', 'type': dict({ + 'kind': 'NamedType', 'name': 'str', 'qname': 'builtins.str', }), + 'valid_values': list([ + 'unlistable_str', + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/__init__/param_1', 'is_optional': False, @@ -3681,9 +4039,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/__init__/self', 'is_optional': False, @@ -3698,9 +4060,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/numpy_docstring_func/self', 'is_optional': False, @@ -3711,12 +4077,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'First integer value for the calculation. (Numpy)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/numpy_docstring_func/x', 'is_optional': False, @@ -3731,12 +4102,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Second integer value for the calculation. (Numpy)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/NumpyDocstringClass/numpy_docstring_func/y', 'is_optional': False, @@ -3755,9 +4131,13 @@ 'assigned_by': 'POSITION_ONLY', 'default_value': 1, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/opt_pos_only/optional', 'is_optional': True, @@ -3772,9 +4152,13 @@ 'assigned_by': 'POSITION_ONLY', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/opt_pos_only/required', 'is_optional': False, @@ -3789,9 +4173,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_from_outside_the_package/param_type', 'is_optional': False, @@ -3806,9 +4194,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_from_outside_the_package/param_value', 'is_optional': False, @@ -3823,9 +4215,13 @@ 'assigned_by': 'POSITION_ONLY', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/a', 'is_optional': False, @@ -3836,9 +4232,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/b', 'is_optional': False, @@ -3853,9 +4253,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/c', 'is_optional': False, @@ -3866,9 +4270,13 @@ 'assigned_by': 'NAME_ONLY', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/d', 'is_optional': False, @@ -3879,9 +4287,13 @@ 'assigned_by': 'NAME_ONLY', 'default_value': 1, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/e', 'is_optional': True, @@ -3896,9 +4308,13 @@ 'assigned_by': 'POSITION_ONLY', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/param_position/self', 'is_optional': False, @@ -3913,9 +4329,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/any_', 'is_optional': False, @@ -3930,9 +4350,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/boolean', 'is_optional': False, @@ -3947,9 +4371,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/callable_none', 'is_optional': False, @@ -3989,9 +4417,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/callexpr', 'is_optional': False, @@ -4002,9 +4434,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/dict_none', 'is_optional': False, @@ -4037,9 +4473,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/dictionary', 'is_optional': False, @@ -4072,9 +4512,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/float_', 'is_optional': False, @@ -4089,9 +4533,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/integer', 'is_optional': False, @@ -4106,9 +4554,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/list_', 'is_optional': False, @@ -4128,9 +4580,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/list_class_none', 'is_optional': False, @@ -4160,9 +4616,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/literal', 'is_optional': False, @@ -4178,9 +4638,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/literal_none', 'is_optional': False, @@ -4212,9 +4676,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/literal_none2', 'is_optional': False, @@ -4246,9 +4714,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/named_class_none', 'is_optional': False, @@ -4273,9 +4745,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/none', 'is_optional': False, @@ -4290,9 +4766,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/obj', 'is_optional': False, @@ -4307,9 +4787,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/optional', 'is_optional': False, @@ -4334,9 +4818,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/set_', 'is_optional': False, @@ -4356,9 +4844,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/set_none', 'is_optional': False, @@ -4388,9 +4880,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/string', 'is_optional': False, @@ -4405,9 +4901,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/tuple_', 'is_optional': False, @@ -4437,9 +4937,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/tuple_class_none', 'is_optional': False, @@ -4474,9 +4978,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/union', 'is_optional': False, @@ -4501,9 +5009,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/union_with_none_1', 'is_optional': False, @@ -4528,9 +5040,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/params/union_with_none_2', 'is_optional': False, @@ -4563,9 +5079,13 @@ 'assigned_by': 'NAME_ONLY', 'default_value': 1, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/req_name_only/optional', 'is_optional': True, @@ -4580,9 +5100,13 @@ 'assigned_by': 'NAME_ONLY', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/req_name_only/required', 'is_optional': False, @@ -4597,12 +5121,18 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Parameter of the calculator. (ReST)', 'type': dict({ + 'kind': 'NamedType', 'name': 'str', 'qname': 'builtins.str', }), + 'valid_values': list([ + 'unlistable_str', + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/__init__/param_1', 'is_optional': False, @@ -4617,9 +5147,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/__init__/self', 'is_optional': False, @@ -4634,9 +5168,13 @@ 'assigned_by': 'IMPLICIT', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/rest_docstring_func/self', 'is_optional': False, @@ -4647,12 +5185,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'First integer value for the calculation. (ReST)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/rest_docstring_func/x', 'is_optional': False, @@ -4667,12 +5210,17 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': 'Second integer value for the calculation. (ReST)', 'type': dict({ + 'kind': 'NamedType', 'name': 'int', 'qname': 'builtins.int', }), + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/docstring_module/RestDocstringClass/rest_docstring_func/y', 'is_optional': False, @@ -4691,9 +5239,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/bool_none_str', 'is_optional': False, @@ -4718,9 +5270,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/bool_none_union', 'is_optional': False, @@ -4745,9 +5301,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none', 'is_optional': False, @@ -4762,9 +5322,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_bool_int_union', 'is_optional': False, @@ -4794,9 +5358,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_bool_none_union', 'is_optional': False, @@ -4826,9 +5394,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_bool_union', 'is_optional': False, @@ -4853,9 +5425,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_list_union_none_none', 'is_optional': False, @@ -4900,9 +5476,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_none_bool_none_union', 'is_optional': False, @@ -4937,9 +5517,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/none_union', 'is_optional': False, @@ -4964,9 +5548,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': 'UnknownValue', 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/special_params/not_true', 'is_optional': True, @@ -4985,9 +5573,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/function_module/FunctionModuleClassB/static_method_params/param_1', 'is_optional': False, @@ -5006,9 +5598,13 @@ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, 'docstring': dict({ + 'boundaries': list([ + ]), 'default_value': '', 'description': '', 'type': None, + 'valid_values': list([ + ]), }), 'id': 'tests/data/various_modules_package/type_var_module/type_var_func/a', 'is_optional': False, diff --git a/tests/safeds_stubgen/api_analyzer/purity_analysis/__init__.py b/tests/safeds_stubgen/api_analyzer/purity_analysis/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/purity_analysis/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/safeds_stubgen/api_analyzer/purity_analysis/test_build_call_graph.py b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_build_call_graph.py new file mode 100644 index 00000000..9c99ac06 --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_build_call_graph.py @@ -0,0 +1,884 @@ +from __future__ import annotations + +import pytest +from safeds_stubgen.api_analyzer.purity_analysis import resolve_references + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "function call - in declaration order" + """ +def fun1(): + pass + +def fun2(): + fun1() + """, # language=none + { + ".fun1.2.0": set(), + ".fun2.5.0": {".fun1.2.0"}, + }, + ), + ( # language=Python "function call - against declaration order" + """ +def fun1(): + fun2() + +def fun2(): + pass + """, # language=none + { + ".fun1.2.0": {".fun2.5.0"}, + ".fun2.5.0": set(), + }, + ), + ( # language=Python "function call - against declaration order with multiple calls" + """ +def fun1(): + fun2() + +def fun2(): + fun3() + +def fun3(): + pass + """, # language=none + { + ".fun1.2.0": {".fun2.5.0"}, + ".fun2.5.0": {".fun3.8.0"}, + ".fun3.8.0": set(), + }, + ), + ( # language=Python "function conditional with branching" + """ +def fun1(): + return "Function 1" + +def fun2(): + return "Function 2" + +def call_function(a): + if a == 1: + return fun1() + else: + return fun2() + """, # language=none + { + ".fun1.2.0": set(), + ".fun2.5.0": set(), + ".call_function.8.0": {".fun1.2.0", ".fun2.5.0"}, + }, + ), + ( # language=Python "builtin function call", + """ +def fun1(): + fun2() + +def fun2(): + print("Function 2") + """, # language=none + { + ".fun1.2.0": {".fun2.5.0"}, + ".fun2.5.0": { + "BUILTIN.print", + }, # print is a builtin function and therefore has no function def to reference -> therefor it has no line + }, + ), + ( # language=Python "external function call", + """ +from external import call + +def fun1(): + call() + """, # language=none + { + ".fun1.4.0": set(), # Since this function could not be resolved, there is no node for it to add to the call graph. + # It will be handled as an unknown call when analyzing the purity. + }, + ), + ( # language=Python "lambda", + """ +def fun1(x): + return x + 1 + +def fun2(): + return lambda x: fun1(x) * 2 + """, # language=none + { + ".fun1.2.0": set(), + ".fun2.5.0": {".fun1.2.0"}, + }, + ), + ( # language=Python "lambda with name", + """ +double = lambda x: 2 * x + """, # language=none + { + ".double.2.9": set(), + }, + ), + ], + ids=[ + "function call - in declaration order", + "function call - against declaration flow", + "function call - against declaration flow with multiple calls", + "function conditional with branching", + "builtin function call", + "external function call", + "lambda", + "lambda with name", + ], +) +def test_build_call_graph_basics(code: str, expected: dict[str, set]) -> None: + call_graph_forest = resolve_references(code).call_graph_forest + + if call_graph_forest is None: + assert expected == {} + return + + transformed_call_graph_forest: dict = {} + for tree_id, tree in call_graph_forest.graphs.items(): + transformed_call_graph_forest[f"{tree_id}"] = set() + for child in tree.children: + transformed_call_graph_forest[f"{tree_id}"].add(child.__str__()) + + assert transformed_call_graph_forest == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "function call with cycle - direct entry" + """ +def fun1(count): + if count > 0: + fun2(count - 1) + +def fun2(count): + if count > 0: + fun1(count - 1) + """, # language=none + { + ".fun1.2.0+.fun2.6.0": set(), + }, + ), + ( # language=Python "function call with cycle - one entry point" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + cycle1() + print() + +def entry(): + cycle1() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0": {"BUILTIN.print"}, + ".entry.12.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + }, + ), + ( # language=Python "function call with cycle - many entry points" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + cycle1() + +def entry1(): + cycle1() + +def entry2(): + cycle2() + +def entry3(): + cycle3() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0": set(), + ".entry1.11.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + ".entry2.14.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + ".entry3.17.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + }, + ), + ( # language=Python "function call with cycle - other call in cycle" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + other() + +def cycle3(): + cycle1() + +def entry(): + cycle1() + +def other(): + pass + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.9.0": {".other.15.0"}, + ".entry.12.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.9.0"}, + ".other.15.0": set(), + }, + ), + ( # language=Python "function call with cycle - multiple other calls in cycle" + """ +def cycle1(): + cycle2() + other3() + +def cycle2(): + cycle3() + other1() + +def cycle3(): + cycle1() + +def entry(): + cycle1() + other2() + +def other1(): + pass + +def other2(): + pass + +def other3(): + pass + """, # language=none + { + ".cycle1.2.0+.cycle2.6.0+.cycle3.10.0": {".other1.17.0", ".other3.23.0"}, + ".entry.13.0": {".cycle1.2.0+.cycle2.6.0+.cycle3.10.0", ".other2.20.0"}, + ".other1.17.0": set(), + ".other2.20.0": set(), + ".other3.23.0": set(), + }, + ), + ( # language=Python "function call with cycle - cycle within a cycle" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + cycle1() + cycle2() + +def entry(): + cycle1() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0": set(), + ".entry.12.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + }, + ), + ( # language=Python "function call with cycle - external cycle within a cycle" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + external_inner_cycle1() + cycle1() + +def external_inner_cycle1(): + external_inner_cycle2() + +def external_inner_cycle2(): + external_inner_cycle1() + +def entry(): + cycle1() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0": {".external_inner_cycle1.12.0+.external_inner_cycle2.15.0"}, + ".external_inner_cycle1.12.0+.external_inner_cycle2.15.0": set(), + ".entry.18.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + }, + ), + ( # language=Python "function call with cycle - external recursive cycle within a cycle" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + external_inner_cycle1() + cycle1() + +def external_inner_cycle1(): + external_inner_cycle2() + +def external_inner_cycle2(): + external_inner_cycle2() + +def entry(): + cycle1() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0": {".external_inner_cycle1.12.0"}, + ".external_inner_cycle1.12.0": {".external_inner_cycle2.15.0"}, + ".external_inner_cycle2.15.0": set(), + ".entry.18.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0"}, + }, + ), + ( # language=Python "function call with cycle - inner cycle within a cycle" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + inner_cycle1() + cycle1() + +def inner_cycle1(): + inner_cycle2() + +def inner_cycle2(): + inner_cycle1() + cycle2() + +def entry(): + cycle1() + """, # language=none + { + ".cycle1.2.0+.cycle2.5.0+.cycle3.8.0+.inner_cycle1.12.0+.inner_cycle2.15.0": set(), + ".entry.19.0": {".cycle1.2.0+.cycle2.5.0+.cycle3.8.0+.inner_cycle1.12.0+.inner_cycle2.15.0"}, + }, + ), + ( # language=Python "cycle in class" + """ +class C: + def fun1(self): + self.fun2() + + def fun2(self): + self.fun1() + """, # language=none + { + ".C.2.0": set(), + ".fun1.3.4+.fun2.6.4": set(), + }, + ), + ( # language=Python "cycle with same name in class" + """ +from typing import Any + +class A: + def __init__(self): + pass + +class B(Any): + def __init__(self): + super().__init__() + +class C(Any): + def __init__(self): + Any.__init__(self) + """, # language=none + { + ".A.4.0": {".__init__.5.4"}, + ".B.8.0": {".__init__.9.4"}, + ".C.12.0": {".__init__.13.4"}, + ".__init__.5.4+.__init__.9.4+.__init__.13.4": {"BUILTIN.Super"}, + }, + ), # TODO: fix cycle creation for functions with the same name and remove Any as cgn. + ( # language=Python "recursive function call", + """ +def f(a): + if a > 0: + f(a - 1) + """, # language=none + { + ".f.2.0": set(), + }, + ), + ], + ids=[ + "function call with cycle - direct entry", + "function call with cycle - one entry point", + "function call with cycle - many entry points", + "function call with cycle - other call in cycle", + "function call with cycle - multiple other calls in cycle", + "function call with cycle - inner cycle within a cycle", + "function call with cycle - external cycle within a cycle", + "function call with cycle - external recursive cycle within a cycle", + "function call with cycle - cycle within a cycle", + "cycle in class", + "cycle with same name in class", + "recursive function call", + ], +) +@pytest.mark.xfail( + reason="The current implementation does not handle cycles of functions with the same name correctly.", +) +def test_build_call_graph_cycles(code: str, expected: dict[str, set]) -> None: + call_graph_forest = resolve_references(code).call_graph_forest + + if call_graph_forest is None: + assert expected == {} + return + + transformed_call_graph_forest: dict = {} + for tree_id, tree in call_graph_forest.graphs.items(): + transformed_call_graph_forest[f"{tree_id}"] = set() + for child in tree.children: + transformed_call_graph_forest[f"{tree_id}"].add(child.__str__()) + + assert transformed_call_graph_forest == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Class call - pass", + """ +class A: + pass + +def fun(): + a = A() + + """, # language=none + { + ".A.2.0": set(), + ".fun.5.0": {".A.2.0"}, + }, + ), + ( # language=Python "Class call - init", + """ +class A: + def __init__(self): + pass + +def fun(): + a = A() + + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".__init__.3.4": set(), + ".fun.6.0": {".A.2.0"}, + }, + ), + ( # language=Python "Class call - init with super", + """ +class A: + def __init__(self): + pass + +class B(A): + def __init__(self): + super().__init__() + +def fun(): + a = B() + + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".__init__.3.4": set(), + ".B.6.0": {".__init__.7.4"}, + ".__init__.7.4": {"BUILTIN.super", ".__init__.3.4"}, + ".fun.10.0": {".B.6.0"}, + }, + ), + ( # language=Python "Class call - new, init and post_init", + """ +class A: + def __new__(cls): + return super().__new__(cls) + + def __init__(self): + pass + + def __post_init__(self): + pass + +def fun(): + a = A() + + """, # language=none + { + ".A.2.0": {".__new__.3.4", ".__init__.6.4", ".__post_init__.9.4"}, + ".__new__.3.4": { + "BUILTIN.super", + }, # TODO: [LATER] the analysis should be able to resolve the super call, right noow it is lost when the combined call graph node is created, since it is detected as an recursive call. + ".__init__.6.4": set(), + ".__post_init__.9.4": set(), + ".fun.12.0": {".A.2.0"}, + }, + ), + ( # language=Python "Class call - init propagation", + """ +class A: + def __init__(self): + self.a1_fun() + self.b = B() + + def a1_fun(self): + self.a2_fun() + + def a2_fun(self): + pass + +class B: + pass + +def fun(): + a = A() + + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".__init__.3.4": {".a1_fun.7.4", ".B.13.0"}, + ".a1_fun.7.4": {".a2_fun.10.4"}, + ".a2_fun.10.4": set(), + ".B.13.0": set(), + ".fun.16.0": {".A.2.0"}, + }, + ), + ( # language=Python "member access - class", + """ +class A: + class_attr1 = 20 + +def fun(): + a = A().class_attr1 + + """, # language=none + { + ".A.2.0": set(), + ".fun.5.0": {".A.2.0"}, + }, + ), + ( # language=Python "member access - class without call", + """ +class A: + class_attr1 = 20 + +def fun(): + a = A.class_attr1 + + """, # language=none + { + ".A.2.0": set(), + ".fun.5.0": set(), + }, + ), + ( # language=Python "member access - methode", + """ +class A: + class_attr1 = 20 + + def g(self): + pass + +def fun1(): + a = A() + a.g() + +def fun2(): + a = A().g() + + """, # language=none + { + ".A.2.0": set(), + ".g.5.4": set(), + ".fun1.8.0": {".A.2.0", ".g.5.4"}, + ".fun2.12.0": {".A.2.0", ".g.5.4"}, + }, + ), + ( # language=Python "member access - instance function", + """ +class A: + def __init__(self): + self.a_inst = B() + +class B: + def __init__(self): + pass + + def b_fun(self): + pass + +def fun1(): + a = A() + a.a_inst.b_fun() + +def fun2(): + a = A().a_inst.b_fun() + + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".__init__.3.4": {".B.6.0"}, + ".B.6.0": {".__init__.7.4"}, + ".__init__.7.4": set(), + ".b_fun.10.4": set(), + ".fun1.13.0": {".A.2.0", ".b_fun.10.4"}, + ".fun2.17.0": {".A.2.0", ".b_fun.10.4"}, + }, + ), + ( # language=Python "member access - function call of functions with same name" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b): + return a + 2 * b + +def fun_a(): + x = A() + x.add(1, 2) + +def fun_b(): + x = B() + x.add(1, 2) + """, # language=none + { + ".A.2.0": set(), + ".B.7.0": set(), + ".add.4.4": set(), + ".add.9.4": set(), + ".fun_a.12.0": { + ".A.2.0", + ".add.4.4", + ".add.9.4", + }, + ".fun_b.16.0": {".B.7.0", ".add.4.4", ".add.9.4"}, + }, + ), + ( # language=Python "member access - function call of functions with same name and nested calls", + """ +def fun1(): + pass + +def fun2(): + print("Function 2") + +class A: + @staticmethod + def add(a, b): + fun1() + return a + b + +class B: + @staticmethod + def add(a, b): + fun2() + return a + 2 * b + """, # language=none + { + ".A.8.0": set(), + ".B.14.0": set(), + ".fun1.2.0": set(), + ".fun2.5.0": { + "BUILTIN.print", + }, # print is a builtin function and therefore has no function def to reference -> therefor it has no line + ".add.10.4": {".fun1.2.0"}, + ".add.16.4": {".fun2.5.0"}, + }, + ), + ( # language=Python "member access - function call of functions with same name (no distinction possible)" + """ +class A: + @staticmethod + def fun(): + return "Function A" + +class B: + @staticmethod + def fun(): + return "Function B" + +def fun_out(a): + if a == 1: + x = A() + else: + x = B() + x.fun() + """, # language=none + { + ".A.2.0": set(), + ".B.7.0": set(), + ".fun.4.4": set(), + ".fun.9.4": set(), + ".fun_out.12.0": { + ".A.2.0", + ".B.7.0", + ".fun.4.4", + ".fun.9.4", + }, # here we cannot distinguish between the two fun functions + }, + ), + ( # language=Python "member access - function call of functions with same name (different signatures)" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b, c): + return a + b + c + +def fun(): + a = A() + b = B() + x = a.add(1, 2) + y = b.add(1, 2, 3) + """, # language=none + { + ".A.2.0": set(), + ".B.7.0": set(), + ".add.4.4": set(), + ".add.9.4": set(), + ".fun.12.0": { + ".A.2.0", + ".B.7.0", + ".add.4.4", + ".add.9.4", + }, # TODO: [LATER] maybe we can distinguish between the two add functions because of their signature + }, + ), + ( # language=Python "member access - function call of functions with same name (but different instance variables)" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + def __init__(self): + self.value = C() + +class C: + @staticmethod + def add(a, b): + return a + b + +def fun_a(): + x = A() + x.add(1, 2) + +def fun_b(): + x = B() + x.value.add(1, 2) + """, # language=none + { + ".A.2.0": set(), + ".add.4.4": set(), + ".B.7.0": {".__init__.8.4"}, + ".__init__.8.4": {".C.11.0"}, + ".C.11.0": set(), + ".add.13.4": set(), + ".fun_a.16.0": {".A.2.0", ".add.4.4", ".add.13.4"}, + ".fun_b.20.0": { + ".B.7.0", + ".add.4.4", + ".add.13.4", + }, # TODO: [LATER] maybe we can distinguish between the two add functions because of their instance variables + }, + ), + ( # language=Python "member access - lambda function call" + """ +class A: + def __init__(self): + self.add = lambda x, y: x + y + +def fun_a(): + a = A() + b = a.add(3, 4) + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".__init__.3.4": set(), + ".add.4.19": set(), + ".fun_a.6.0": {".A.2.0", ".add.4.19"}, + }, + ), + ( # language=Python "member access - class init and methode call in lambda function" + """ +class A: + def __init__(self): + self.value = B() + +class B: + @staticmethod + def add(a, b): + return a + b + +lambda_add = lambda x, y: A().value.add(x, y) + """, # language=none + { + ".A.2.0": {".__init__.3.4"}, + ".B.6.0": set(), + ".__init__.3.4": {".B.6.0"}, + ".add.8.4": set(), + ".lambda_add.11.13": {".A.2.0", ".add.8.4"}, + }, + ), + ], + ids=[ + "Class call - pass", + "Class call - init", + "Class call - init with super", + "Class call - new, init and post_init", + "Class call - init propagation", + "member access - class", + "member access - class without call", + "member access - methode", + "member access - instance function", + "member access - function call of functions with same name", + "member access - function call of functions with same name and nested calls", + "member access - function call of functions with same name (no distinction possible)", + "member access - function call of functions with same name (different signatures)", + "member access - function call of functions with same name (but different instance variables)", + "member access - lambda function call", + "member access - class init and methode call in lambda function", + ], +) +def test_build_call_graph_member_access(code: str, expected: dict[str, set]) -> None: + call_graph_forest = resolve_references(code).call_graph_forest + + if call_graph_forest is None: + assert expected == {} + return + + transformed_call_graph_forest: dict = {} + for tree_id, tree in call_graph_forest.graphs.items(): + transformed_call_graph_forest[f"{tree_id}"] = set() + for child in tree.children: + transformed_call_graph_forest[f"{tree_id}"].add(child.__str__()) + + assert transformed_call_graph_forest == expected diff --git a/tests/safeds_stubgen/api_analyzer/purity_analysis/test_get_module_data.py b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_get_module_data.py new file mode 100644 index 00000000..8a6f2fa1 --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_get_module_data.py @@ -0,0 +1,3430 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +import astroid +import pytest +from safeds_stubgen.api_analyzer.purity_analysis import ( + get_module_data, +) +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + ClassScope, + FunctionScope, + MemberAccess, + NodeID, + Scope, + Symbol, +) + + +@dataclass +class SimpleScope: + """Class for simple scopes. + + A simplified class of the Scope class for testing purposes. + + Attributes + ---------- + node_name : str | None + The name of the node. + children : list[SimpleScope] | None + The children of the node. + None if the node has no children. + """ + + node_name: str | None + children: list[SimpleScope] | None + + +@dataclass +class SimpleClassScope(SimpleScope): + """Class for simple class scopes. + + A simplified class of the ClassScope class for testing purposes. + + Attributes + ---------- + node_name : str | None + The name of the node. + children : list[SimpleScope] | None + The children of the node. + None if the node has no children. + class_variables : list[str] + The list of class variables. + instance_variables : list[str] + The list of instance variables. + super_class : list[str] | None + The list of super classes, if the class has any. + """ + + class_variables: list[str] + instance_variables: list[str] + new_function: str | None = None + init_function: str | None = None + post_init_function: str | None = None + super_class: list[str] | None = None + + +@dataclass +class SimpleFunctionScope(SimpleScope): + """Class for simple function scopes. + + A simplified class of the FunctionScope class for testing purposes. + + Attributes + ---------- + node_name : str | None + The name of the node. + children : list[SimpleScope] | None + The children of the node. + None if the node has no children. + targets : list[str] + The list of target nodes used in the function as string. + values : list[str] + The list of value nodes used in the function as string. + calls : list[str] + The list of call nodes used in the function as string. + parameters : list[str] + The list of parameter nodes used in the function as string. + globals : list[str] + The list of global nodes used in the function as string. + """ + + targets: list[str] + values: list[str] + calls: list[str] + parameters: list[str] = field(default_factory=list) + globals: list[str] = field(default_factory=list) + + +def transform_scope_node( + node: Scope | ClassScope | FunctionScope, +) -> SimpleScope | SimpleClassScope | SimpleFunctionScope: + """Transform a Scope, ClassScope or FunctionScope instance. + + Parameters + ---------- + node : Scope | ClassScope | FunctionScope + The node to transform. + + Returns + ------- + SimpleScope | SimpleClassScope | SimpleFunctionScope + The transformed node. + """ + if node.children is not None: + if isinstance(node, ClassScope): + instance_vars_transformed = [] + class_vars_transformed = [] + super_classes_transformed = [] + for child in node.instance_variables.values(): + for c1 in child: + if isinstance(c1.node, MemberAccess): + c_str = to_string_class(c1.node.node) + else: + c_str = to_string_class(c1.node) + if c_str is not None: + instance_vars_transformed.append(c_str) # type: ignore[misc] + # it is not possible that c_str is None + for child in node.class_variables.values(): + for c2 in child: + c_str = to_string_class(c2.node) + if c_str is not None: + class_vars_transformed.append(c_str) # type: ignore[misc] + # it is not possible that c_str is None + if node.super_classes: + for klass in node.super_classes: + c_str = to_string_class(klass) + if c_str is not None: + super_classes_transformed.append(c_str) # type: ignore[misc] + # it is not possible that c_str is None + + return SimpleClassScope( + to_string(node.symbol), + [transform_scope_node(child) for child in node.children], + class_vars_transformed, + instance_vars_transformed, + node.new_function.symbol.name if node.new_function else None, + node.init_function.symbol.name if node.init_function else None, + node.post_init_function.symbol.name if node.post_init_function else None, + super_classes_transformed if super_classes_transformed else None, + ) + if isinstance(node, FunctionScope): + targets_transformed = [] + values_transformed = [] + calls_transformed = [] + parameters_transformed = [] + globals_transformed = [] + + for target in node.target_symbols.values(): + for t in target: + string = to_string_func(t.node) + if string not in targets_transformed: + targets_transformed.append(string) + + for value in node.value_references.values(): + for v in value: + string = to_string_func(v.node) + if string not in values_transformed: + values_transformed.append(string) + for call in node.call_references.values(): + for cl in call: + string = to_string_func(cl.node) + if string not in calls_transformed: + calls_transformed.append(string) + for parameter in node.parameters.values(): + parameters_transformed.append(to_string_func(parameter.node)) + for globs in node.globals_used.values(): + for g in globs: + globals_transformed.append(to_string_func(g.node)) + + return SimpleFunctionScope( + to_string(node.symbol), + [transform_scope_node(child) for child in node.children], + targets_transformed, + values_transformed, + calls_transformed, + parameters_transformed, + globals_transformed, + ) + + return SimpleScope(to_string(node.symbol), [transform_scope_node(child) for child in node.children]) + else: + return SimpleScope(to_string(node.symbol), []) + + +def to_string(symbol: Symbol) -> str: + """Transform a Symbol instance to a string. + + Parameters + ---------- + symbol : Symbol + The Symbol instance to transform. + + Returns + ------- + str + The transformed Symbol instance as string. + """ + if isinstance(symbol.node, astroid.Module): + return f"{symbol.node.__class__.__name__}" + elif isinstance(symbol.node, astroid.ClassDef | astroid.FunctionDef | astroid.AssignName): + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.node.name}" + elif isinstance(symbol.node, astroid.AssignAttr): + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.node.attrname}" + elif isinstance(symbol.node, MemberAccess): + result = transform_member_access(symbol.node) + return f"{symbol.__class__.__name__}.MemberAccess.{result}" + elif isinstance(symbol.node, astroid.Import): + return ( # TODO: handle multiple imports and aliases + f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.node.names[0][0]}" + ) + elif isinstance(symbol.node, astroid.ImportFrom): + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.node.modname}.{symbol.node.names[0][0]}" # TODO: handle multiple imports and aliases + elif isinstance(symbol.node, astroid.Name): + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.node.name}" + elif isinstance( + symbol.node, + astroid.ListComp + | astroid.SetComp + | astroid.DictComp + | astroid.GeneratorExp + | astroid.Try + | astroid.With, + ): + return f"{symbol.node.__class__.__name__}" + elif isinstance(symbol.node, astroid.Lambda): + if symbol.name != "Lambda": + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}.{symbol.name}" + return f"{symbol.__class__.__name__}.{symbol.node.__class__.__name__}" + raise NotImplementedError(f"Unknown node type: {symbol.node.__class__.__name__}") + + +def to_string_class(node: astroid.NodeNG | ClassScope) -> str | None: + """Transform a NodeNG or ClassScope instance to a string. + + Parameters + ---------- + node : astroid.NodeNG | ClassScope + The NodeNG or ClassScope instance to transform. + + Returns + ------- + str | None + The transformed NodeNG or ClassScope instance as string. + None if the node is a Lambda, TryExcept, TryFinally or a Comprehension instance. + """ + if isinstance(node, astroid.AssignAttr): + return f"{node.__class__.__name__}.{node.attrname}" + elif isinstance(node, astroid.AssignName | astroid.FunctionDef | astroid.ClassDef): + return f"{node.__class__.__name__}.{node.name}" + elif isinstance( + node, + astroid.Lambda + | astroid.Try + | astroid.ListComp + | astroid.SetComp + | astroid.DictComp + | astroid.GeneratorExp, + ): + return None + elif isinstance(node, ClassScope): + return f"{node.symbol.node.__class__.__name__}.{node.symbol.node.name}" + raise NotImplementedError(f"Unknown node type: {node.__class__.__name__}") + + +def to_string_func(node: astroid.NodeNG | MemberAccess) -> str: + """Transform a NodeNG or MemberAccess instance to a string. + + Parameters + ---------- + node : astroid.NodeNG | MemberAccess + The NodeNG or MemberAccess instance to transform. + + Returns + ------- + str + The transformed NodeNG or FunctionScope instance as string. + """ + if isinstance(node, astroid.Name | astroid.AssignName): + return f"{node.__class__.__name__}.{node.name}" + elif isinstance(node, MemberAccess): + return f"{node.__class__.__name__}.{transform_member_access(node)}" + elif isinstance(node, astroid.Call): + if isinstance(node.func, astroid.Attribute): + return f"Call.{node.func.attrname}" + return f"Call.{node.func.name}" + return f"{node.as_string()}" + + +def transform_member_access(member_access: MemberAccess) -> str: + """Transform a MemberAccess instance to a string. + + Parameters + ---------- + member_access : MemberAccess + The MemberAccess instance to transform. + + Returns + ------- + str + The transformed MemberAccess instance as string. + """ + attribute_names = [] + + while isinstance(member_access, MemberAccess): + if isinstance(member_access.member, astroid.AssignAttr | astroid.Attribute): + attribute_names.append(member_access.member) + else: + attribute_names.append(member_access.member) + member_access = member_access.receiver # type: ignore[assignment] + if isinstance(member_access, astroid.Name): + attribute_names.append(member_access.name) + + return ".".join(reversed(attribute_names)) + + +@pytest.mark.parametrize( + ("node", "expected"), + [ + ( + astroid.Module("numpy"), + "numpy", + ), + ( + astroid.ClassDef( + "A", + lineno=2, + col_offset=3, + parent=astroid.Module("numpy"), + end_lineno=2, + end_col_offset=10, + ), + "numpy.A.2.3", + ), + ( + astroid.FunctionDef( + "local_func", + lineno=1, + col_offset=0, + parent=astroid.ClassDef("A", lineno=2, col_offset=3, end_lineno=2, end_col_offset=10, parent=None), + end_lineno=1, + end_col_offset=0 + ), + "A.local_func.1.0", + ), + ( + astroid.FunctionDef( + "global_func", + lineno=1, + col_offset=0, + end_lineno=1, + end_col_offset=0, + parent=astroid.ClassDef( + "A", + lineno=2, + col_offset=3, + parent=astroid.Module("numpy"), + end_lineno=2, + end_col_offset=10, + ), + ), + "numpy.global_func.1.0", + ), + ( + astroid.AssignName( + "var1", + lineno=1, + col_offset=5, + parent=astroid.FunctionDef("func1", lineno=1, col_offset=0, parent=None, end_lineno=1, end_col_offset=0), + end_lineno=1, + end_col_offset=5 + ), + "func1.var1.1.5", + ), + ( + astroid.Name("var2", lineno=20, col_offset=0, end_lineno=20, end_col_offset=0, parent=astroid.FunctionDef("func1", lineno=1, col_offset=0, parent=None, end_lineno=1, end_col_offset=0)), + "func1.var2.20.0", + ), + ( + astroid.Name( + "glob", + lineno=20, + col_offset=0, + parent=astroid.FunctionDef( + "func1", + lineno=1, + col_offset=0, + parent=astroid.ClassDef( + "A", + lineno=2, + col_offset=3, + parent=astroid.Module("numpy"), + end_lineno=2, + end_col_offset=10, + ), + end_lineno=1, + end_col_offset=0 + ), + end_lineno=20, + end_col_offset=0 + ), + "numpy.glob.20.0", + ), + ], + ids=[ + "Module", + "ClassDef (parent Module)", + "FunctionDef (parent ClassDef)", + "FunctionDef (parent ClassDef, parent Module)", + "AssignName (parent FunctionDef)", + "Name (parent FunctionDef)", + "Name (parent FunctionDef, parent ClassDef, parent Module)", + ], # TODO: add AssignAttr, Call, Lambda, ListComp, MemberAccess +) +def test_calc_node_id( + node: astroid.Module | astroid.ClassDef | astroid.FunctionDef | astroid.AssignName | astroid.Name, + expected: str, +) -> None: + result = NodeID.calc_node_id(node) + assert result.__str__() == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Assign" + """ +def variable(): + var1 = 20 + """, # language=none + { + "variable": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.variable", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.var1"], + [], + [], + [], + ), + ], + }, + ), + ( # language=Python "Multiple Assign" + """ +def variable(): + a = b = c = 1 + """, # language=none + { + "variable": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.variable", + [ + SimpleScope("LocalVariable.AssignName.a", []), + SimpleScope("LocalVariable.AssignName.b", []), + SimpleScope("LocalVariable.AssignName.c", []), + ], + ["AssignName.a", "AssignName.b", "AssignName.c"], + [], + [], + [], + ), + ], + }, + ), + ( # language=Python "Assign Parameter" + """ +def parameter(a): + var1 = a + """, # language=none + { + "parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.parameter", + [SimpleScope("Parameter.AssignName.a", []), SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.a", "AssignName.var1"], + ["Name.a"], + [], + ["AssignName.a"], + ), + ], + }, + ), + ( # language=Python "Assign Class Attribute" + """ +class A: + class_attr = 10 + +def class_attr(): + var1 = A.class_attr + """, # language=none + { + "class_attr": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.class_attr", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.var1"], + ["MemberAccessValue.A.class_attr", "Name.A"], + [], + [], + ), + ], + }, + ), + ( # language=Python "Assign Instance Attribute" + """ +class B: + def __init__(self): + self.instance_attr = 10 + +def instance_attr(): + b = B() + var1 = b.instance_attr + """, # language=none + { + "__init__": [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.instance_attr", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.instance_attr"], + [], + [], + ["AssignName.self"], + ), + ], + "instance_attr": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.instance_attr", + [ + SimpleScope("LocalVariable.AssignName.b", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.b", "AssignName.var1"], + ["MemberAccessValue.b.instance_attr", "Name.b"], + ["Call.B"], + [], + ), + ], + }, + ), + ( # language=Python "AssignAttr" + """ +class A: + class_attr = 10 + +def assign_attr(): + A.class_attr = 1 + """, # language=none + { + "assign_attr": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.assign_attr", + [SimpleScope("LocalVariable.MemberAccess.A.class_attr", [])], + ["MemberAccessTarget.A.class_attr", "Name.A"], + [], + [], + [], + ), + ], + }, + ), + ( # language=Python "AugAssign" + """ +def aug_assign(var1): + var1 += 1 + """, # language=none + { + "aug_assign": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.aug_assign", + [SimpleScope("Parameter.AssignName.var1", []), SimpleScope("Parameter.AssignName.var1", [])], + ["AssignName.var1"], + [], + [], + ["AssignName.var1"], + ), + ], + }, + ), + ( # language=Python "AnnAssign" + """ +def ann_assign(): + var1: int = 10 + """, # language=none + { + "ann_assign": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.ann_assign", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + [], + [], + ), + ], + }, + ), + ( # language=Python "BinOp" + """ +def bin_op(var2): + var1 = 20 + var2 + """, # language=none + { + "bin_op": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.bin_op", + [ + SimpleScope("Parameter.AssignName.var2", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.var2", "AssignName.var1"], + ["Name.var2"], + [], + ["AssignName.var2"], + ), + ], + }, + ), + ( # language=Python "BoolOp" + """ +def bool_op(var2): + var1 = True and var2 + """, # language=none + { + "bool_op": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.bool_op", + [ + SimpleScope("Parameter.AssignName.var2", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.var2", "AssignName.var1"], + ["Name.var2"], + [], + ["AssignName.var2"], + ), + ], + }, + ), + ( # language=Python "FuncCall" + """ +def func(): + pass + +def func_call(): + var1 = func() + """, # language=none + { + "func": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.func", + [], + [], + [], + [], + ), + ], + "func_call": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.func_call", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + [], + ["Call.func"], + ), + ], + }, + ), + ( # language=Python "FuncCall Parameter" + """ +def func(a): + pass + +def func_call_par(param): + var1 = param + func(param) + """, # language=none + { + "func": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.func", + [SimpleScope("Parameter.AssignName.a", [])], + ["AssignName.a"], + [], + [], + ["AssignName.a"], + ), + ], + "func_call_par": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.func_call_par", + [ + SimpleScope("Parameter.AssignName.param", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.param", "AssignName.var1"], + ["Name.param"], + ["Call.func"], + ["AssignName.param"], + ), + ], + }, + ), + ( # language=Python "Return" + """ +def assign_return(var1): + return var1 + """, # language=none + { + "assign_return": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.assign_return", + [ + SimpleScope("Parameter.AssignName.var1", []), + ], + ["AssignName.var1"], + ["Name.var1"], + [], + ["AssignName.var1"], + ), + ], + }, + ), + ( # language=Python "Assign to dict" + """ +def f(): + d = {} + d["a"] = 1 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.d", []), + ], + ["AssignName.d"], + ["Name.d"], + [], + ), + ], + }, + ), + ( # language=Python "Assign to class dict" + """ +class A: + d = {} + + def f(self): + self.d["a"] = 1 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.self", []), + ], + ["AssignName.self", "MemberAccessTarget.self.d"], + ["Name.self"], # This is not correct. This belongs into the targets list. + # But on the other hand it does not result in any problems if it isn't, + # and it would be hard to detect that correctly. + [], + ["AssignName.self"], + ), + ], + }, + ), + ], + ids=[ + "Assign", + "Multiple Assign", + "Assign Parameter", + "Assign Class Attribute", + "Assign Instance Attribute", + "AssignAttr", + "AugAssign", + "AnnAssign", + "BinOp", + "BoolOp", + "FuncCall", + "FuncCall Parameter", + "Return", + "Assign to dict", + "Assign to class dict", + ], +) +def test_get_module_data_value_and_target_nodes(code: str, expected: str) -> None: + functions = get_module_data(code).functions + transformed_functions = { + fun_name: [transform_scope_node(fun) for fun in fun_list] for fun_name, fun_list in functions.items() + } # The result is simplified to make the comparison easier + assert transformed_functions == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Parameter in function scope" + """ +def local_parameter(pos_arg): + return 2 * pos_arg + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.pos_arg", []), + ], + ["AssignName.pos_arg"], + ["Name.pos_arg"], + [], + ["AssignName.pos_arg"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with keyword only" + """ +def local_parameter(*, key_arg_only): + return 2 * key_arg_only + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.key_arg_only", []), + ], + ["AssignName.key_arg_only"], + ["Name.key_arg_only"], + [], + ["AssignName.key_arg_only"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with positional only" + """ +def local_parameter(pos_arg_only, /): + return 2 * pos_arg_only + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.pos_arg_only", []), + ], + ["AssignName.pos_arg_only"], + ["Name.pos_arg_only"], + [], + ["AssignName.pos_arg_only"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with default value" + """ +def local_parameter(def_arg=10): + return def_arg + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.def_arg", []), + ], + ["AssignName.def_arg"], + ["Name.def_arg"], + [], + ["AssignName.def_arg"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with type annotation" + """ +def local_parameter(def_arg: int): + return def_arg + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.def_arg", []), + ], + ["AssignName.def_arg"], + ["Name.def_arg"], + [], + ["AssignName.def_arg"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with *args" + """ +def local_parameter(*args): + return args + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.args", []), + ], + ["AssignName.args"], + ["Name.args"], + [], + ["AssignName.args"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with **kwargs" + """ +def local_parameter(**kwargs): + return kwargs + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.kwargs", []), + ], + ["AssignName.kwargs"], + ["Name.kwargs"], + [], + ["AssignName.kwargs"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with *args and **kwargs" + """ +def local_parameter(*args, **kwargs): + return args, kwargs + """, # language= None + { + "local_parameter": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter", + [ + SimpleScope("Parameter.AssignName.args", []), + SimpleScope("Parameter.AssignName.kwargs", []), + ], + ["AssignName.args", "AssignName.kwargs"], + ["Name.args", "Name.kwargs"], + [], + ["AssignName.args", "AssignName.kwargs"], + ), + ], + }, + ), + ( # language=Python "Parameter in function scope with all arg types" + """ +def all_parameters(pos_arg, def_arg="default_value", *args, key_arg="default_kwarg", **kwargs): + return pos_arg, def_arg, args, key_arg, kwargs + """, # language= None + { + "all_parameters": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.all_parameters", + [ + SimpleScope("Parameter.AssignName.args", []), + SimpleScope("Parameter.AssignName.kwargs", []), + SimpleScope("Parameter.AssignName.pos_arg", []), + SimpleScope("Parameter.AssignName.def_arg", []), + SimpleScope("Parameter.AssignName.key_arg", []), + ], + [ + "AssignName.args", + "AssignName.kwargs", + "AssignName.pos_arg", + "AssignName.def_arg", + "AssignName.key_arg", + ], + ["Name.pos_arg", "Name.def_arg", "Name.args", "Name.key_arg", "Name.kwargs"], + [], + [ + "AssignName.pos_arg", + "AssignName.def_arg", + "AssignName.key_arg", + "AssignName.args", + "AssignName.kwargs", + ], + ), + ], + }, + ), + ( # language=Python "Two Functions with same parameter name" + """ +def local_parameter1(a): + return a + +def local_parameter2(a): + return a + """, # language= None + { + "local_parameter1": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter1", + [ + SimpleScope("Parameter.AssignName.a", []), + ], + ["AssignName.a"], + ["Name.a"], + [], + ["AssignName.a"], + ), + ], + "local_parameter2": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_parameter2", + [ + SimpleScope("Parameter.AssignName.a", []), + ], + ["AssignName.a"], + ["Name.a"], + [], + ["AssignName.a"], + ), + ], + }, + ), + ], + ids=[ + "Parameter in function scope", + "Parameter in function scope with keyword only", + "Parameter in function scope with positional only", + "Parameter in function scope with default value", + "Parameter in function scope with type annotation", + "Parameter in function scope with *args", + "Parameter in function scope with **kwargs", + "Parameter in function scope with *args and **kwargs", + "Parameter in function scope with all arg types", + "Two functions with same parameter name", + ], +) +def test_get_module_data_parameters(code: str, expected: str) -> None: + functions = get_module_data(code).functions + transformed_functions = { + fun_name: [transform_scope_node(fun) for fun in fun_list] for fun_name, fun_list in functions.items() + } # The result is simplified to make the comparison easier + assert transformed_functions == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Global unused" + """ +glob1 = 10 + +def glob(): + global glob1 + """, # language=none + { + "glob": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.glob", + [], + [], + [], + [], + [], + ["AssignName.glob1"], + ), + ], + }, + ), + ( # language=Python "Global with global keyword" + """ +global glob1 # This is not detected since there is no global assignment to be found. + +def glob(): + global glob1 + var1 = glob1 + """, # language=none + { + "glob": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.glob", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + ["Name.glob1"], + [], + [], + [], + ), + ], + }, + ), + ( # language=Python "Reassignment of global variable" + """ +a = True +if a: + var1 = 10 +else: + var1 = 20 + +def f(): + global var1 + print(var1) + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [], + [], + ["Name.var1"], + ["Call.print"], + [], + ["AssignName.var1", "AssignName.var1"], + ), + ], + }, + ), + ( # language=Python "Different uses of globals" + """ +var1, var2 = 10, 20 + +def f(): + global var1 + +def g(): + global var1, var2 + +def h(): + for i in range(var1): + global var2 + pass + + """, # language=none + { + "f": [SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], [], [], ["AssignName.var1"])], + "g": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.g", + [], + [], + [], + [], + [], + ["AssignName.var1", "AssignName.var2"], + ), + ], + "h": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.h", + [SimpleScope("LocalVariable.AssignName.i", [])], + ["AssignName.i"], + ["Name.var1"], + ["Call.range"], + [], + ["AssignName.var1", "AssignName.var2"], + ), + ], + }, + ), + ( # language=Python "Shadowing of global variable" + """ +var1 = 10 + +def f(): + var1 = 1 # this is not a global variable + for i in range(var1): + pass + + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + SimpleScope("LocalVariable.AssignName.i", []), + ], + ["AssignName.var1", "AssignName.i"], + ["Name.var1"], + ["Call.range"], + [], + [], + ), + ], + }, + ), + ( # language=Python "List Comprehension with global" + """ +nums = ["aaa", "bb", "ase"] + +def f(): + global nums + x = [len(num) for num in nums] + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.x", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ["AssignName.x"], + ["Name.nums"], + ["Call.len"], + [], + ["AssignName.nums"], + ), + ], + }, + ), + ( # language=Python "List Comprehension with global and condition" + """ +nums = ["aaa", "bb", "ase"] +var1 = 10 + +def f(): + global nums, var1 + x = [len(num) for num in nums if var1 > 10] + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.x", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ["AssignName.x"], + ["Name.nums", "Name.var1"], + ["Call.len"], + [], + ["AssignName.nums", "AssignName.var1"], + ), + ], + }, + ), + ( # language=Python "List Comprehension with global without global keyword" + """ +nums = ["aaa", "bb", "ase"] + +def f(): + x = [len(num) for num in nums] + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.x", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ["AssignName.x"], + ["Name.nums"], + ["Call.len"], + [], + ["AssignName.nums"], + ), + ], + }, + ), + ( # language=Python "Lambda with global" + """ +var1 = 1 + +def f(): + global var1 + return (lambda y: var1 + y)(4) + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleFunctionScope( + "LocalVariable.Lambda", + [SimpleScope("Parameter.AssignName.y", [])], + ["AssignName.y"], + ["Name.var1", "Name.y"], + [], + ["AssignName.y"], + ["AssignName.var1"], + ), + ], + [], + ["Name.var1"], + [], + [], + ["AssignName.var1"], + ), + ], + }, + ), + ( # language=Python "Lambda with global without global keyword" + """ +var1 = 1 + +def f(): + return (lambda y: var1 + y)(4) + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleFunctionScope( + "LocalVariable.Lambda", + [SimpleScope("Parameter.AssignName.y", [])], + ["AssignName.y"], + ["Name.var1", "Name.y"], + [], + ["AssignName.y"], + ["AssignName.var1"], + ), + ], + [], + ["Name.var1"], + [], + [], + ["AssignName.var1"], + ), + ], + }, + ), + ], + ids=[ + "Global unused", + "Global with global keyword", + "Reassignment of global variable", + "Different uses of globals", + "Shadowing of global variable", + "List Comprehension with global", + "List Comprehension with global and condition", + "List Comprehension with global without global keyword", + "Lambda with global", + "Lambda with global without global keyword", + ], +) +def test_get_module_data_globals(code: str, expected: str) -> None: + functions = get_module_data(code).functions + transformed_functions = { + fun_name: [transform_scope_node(fun) for fun in fun_list] for fun_name, fun_list in functions.items() + } # The result is simplified to make the comparison easier + assert transformed_functions == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Trivial function" + """ +def f(): + pass + """, # language=none + {"f": [SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], [])]}, + ), + ( # language=Python "Function with child" + """ +def f(): + var1 = 1 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + [], + [], + ), + ], + }, + ), + ( # language=Python "Function with parameter" + """ +def f(name): + var1 = name + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.name", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.name", "AssignName.var1"], + ["Name.name"], + [], + ["AssignName.name"], + ), + ], + }, + ), + ( # language=Python "Function with values" + """ +def f(): + name = "name" + var1 = name + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("LocalVariable.AssignName.name", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.name", "AssignName.var1"], + ["Name.name"], + [], + ), + ], + }, + ), + ( # language=Python "Function with return" + """ +def f(): + var1 = 1 + return var1 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + ["Name.var1"], + [], + ), + ], + }, + ), + ( # language=Python "Function with nested return" + """ +def f(a, b): + var1 = 1 + return a + b + var1 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.a", []), + SimpleScope("Parameter.AssignName.b", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.a", "AssignName.b", "AssignName.var1"], + ["Name.a", "Name.b", "Name.var1"], + [], + ["AssignName.a", "AssignName.b"], + ), + ], + }, + ), + ( # language=Python "Function with nested names" + """ +def f(a, b): + var1 = 1 + var2 = a + b + var1 + return var2 + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.a", []), + SimpleScope("Parameter.AssignName.b", []), + SimpleScope("LocalVariable.AssignName.var1", []), + SimpleScope("LocalVariable.AssignName.var2", []), + ], + ["AssignName.a", "AssignName.b", "AssignName.var1", "AssignName.var2"], + ["Name.a", "Name.b", "Name.var1", "Name.var2"], + [], + ["AssignName.a", "AssignName.b"], + ), + ], + }, + ), + ( # language=Python "Function with value in call" + """ +def f(a): + print(a) + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.a", []), + ], + ["AssignName.a"], + ["Name.a"], + ["Call.print"], + ["AssignName.a"], + ), + ], + }, + ), + ( # language=Python "Function with value in loop" + """ +def f(a): + for i in range(10): + pass + + while a: + pass + """, # language=none + { + "f": [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [SimpleScope("Parameter.AssignName.a", []), SimpleScope("LocalVariable.AssignName.i", [])], + ["AssignName.a", "AssignName.i"], + ["Name.a"], + ["Call.range"], + ["AssignName.a"], + ), + ], + }, + ), + ( # language=Python "Function with call" + """ +def f(): + f() + """, # language=none + {"f": [SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], ["Call.f"])]}, + ), + ( # language=Python "Function with same name" + """ +def f(): + f() + +def f(): + pass + """, # language=none + { + "f": [ + SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], ["Call.f"]), + SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], []), + ], + }, + ), + ( # language=Python "Function with @overload" + """ +from typing import overload + +@overload # We do not want these in the results, since they are not actual functions +def f(): + ... + +def f(): + pass + """, # language=none + { + "f": [ + SimpleFunctionScope("GlobalVariable.FunctionDef.f", [], [], [], []), + ], + }, + ), + ], + ids=[ + "Trivial function", + "Function with child", + "Function with parameter", + "Function with values", + "Function with return", + "Function with nested return", + "Function with nested names", + "Function with value in call", + "Function with value in loop", + "Function with call", + "Function with same name", + "Function with @overload", + ], +) +def test_get_module_data_functions(code: str, expected: dict[str, list[str]]) -> None: + functions = get_module_data(code).functions + transformed_functions = { + fun_name: [transform_scope_node(fun) for fun in fun_list] for fun_name, fun_list in functions.items() + } # The result is simplified to make the comparison easier + + assert transformed_functions == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "ClassDef" + """ +class A: + pass + """, # language=none + {"A": SimpleClassScope("GlobalVariable.ClassDef.A", [], [], [])}, + ), + ( # language=Python "ClassDef with class attribute" + """ +class A: + var1 = 1 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [SimpleScope("ClassVariable.AssignName.var1", [])], + ["AssignName.var1"], + [], + ), + }, + ), + ( # language=Python "ClassDef with multiple class attribute" + """ +class A: + var1 = 1 + var2 = 2 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.var1", []), + SimpleScope("ClassVariable.AssignName.var2", []), + ], + ["AssignName.var1", "AssignName.var2"], + [], + ), + }, + ), + ( # language=Python "ClassDef with multiple class attribute (same name)" + """ +class A: + if True: + var1 = 1 + else: + var1 = 2 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.var1", []), + SimpleScope("ClassVariable.AssignName.var1", []), + ], + ["AssignName.var1", "AssignName.var1"], + [], + ), + }, + ), + ( # language=Python "ClassDef with instance attribute" + """ +class A: + def __init__(self): + self.var1 = 1 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.var1", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.var1"], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__"], + ["AssignAttr.var1"], + None, + "__init__", + ), + }, + ), + ( # language=Python "ClassDef with multiple instance attributes (and type annotations)" + """ +class A: + def __init__(self): + self.var1: int = 1 + self.name: str = "name" + self.state: bool = True + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.var1", []), + SimpleScope("InstanceVariable.MemberAccess.self.name", []), + SimpleScope("InstanceVariable.MemberAccess.self.state", []), + ], + [ + "AssignName.self", + "Name.self", + "MemberAccessTarget.self.var1", + "MemberAccessTarget.self.name", + "MemberAccessTarget.self.state", + ], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__"], + ["AssignAttr.var1", "AssignAttr.name", "AssignAttr.state"], + None, + "__init__", + ), + }, + ), + ( # language=Python "ClassDef with conditional instance attributes (instance attributes with the same name)" + """ +class A: + def __init__(self): + if True: + self.var1 = 1 + else: + self.var1 = 0 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.var1", []), + SimpleScope("InstanceVariable.MemberAccess.self.var1", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.var1"], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__"], + ["AssignAttr.var1", "AssignAttr.var1"], + None, + "__init__", + ), + }, + ), + ( # language=Python "ClassDef with class and instance attribute" + """ +class A: + var1 = 1 + + def __init__(self): + self.var1 = 1 + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.var1", []), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.var1", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.var1"], + [], + [], + ["AssignName.self"], + ), + ], + ["AssignName.var1", "FunctionDef.__init__"], + ["AssignAttr.var1"], + None, + "__init__", + ), + }, + ), + ( # language=Python "ClassDef with nested class" + """ +class A: + class B: + pass + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [SimpleClassScope("ClassVariable.ClassDef.B", [], [], [])], + ["ClassDef.B"], + [], + ), + "B": SimpleClassScope("ClassVariable.ClassDef.B", [], [], []), + }, + ), + ( # language=Python "Multiple ClassDef" + """ +class A: + pass + +class B: + pass + """, # language=none + { + "A": SimpleClassScope("GlobalVariable.ClassDef.A", [], [], []), + "B": SimpleClassScope("GlobalVariable.ClassDef.B", [], [], []), + }, + ), + ( # language=Python "ClassDef with superclass" + """ +class A: + pass + +class B(A): + pass + """, # language=none + { + "A": SimpleClassScope("GlobalVariable.ClassDef.A", [], [], []), + "B": SimpleClassScope("GlobalVariable.ClassDef.B", [], [], [], None, None, None, ["ClassDef.A"]), + }, + ), + ( # language=Python "ClassDef with __new__, __init__ and __post_init__" + """ +class A: + def __new__(cls): + return super().__new__(cls) + + def __init__(self): + pass + + def __post_init__(self): + pass + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__new__", + [ + SimpleScope("Parameter.AssignName.cls", []), + ], + ["AssignName.cls"], + ["MemberAccessValue.super.__new__", "Name.cls"], + ["Call.__new__", "Call.super"], + ["AssignName.cls"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + [], + [], + ["AssignName.self"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__post_init__", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__new__", "FunctionDef.__init__", "FunctionDef.__post_init__"], + [], + "__new__", + "__init__", + "__post_init__", + ), + }, + ), + ( # language=Python "Assign Instance Attribute via property" + """ +class A: + def __init__(self, value): + self._value = value + + def f(self): + return self.value + + @property + def value(self): + return self._value + """, # language=none + { + "A": SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.value", []), + SimpleScope("InstanceVariable.MemberAccess.self._value", []), + ], + ["AssignName.self", "Name.self", "AssignName.value", "MemberAccessTarget.self._value"], + ["Name.value"], + [], + ["AssignName.self", "AssignName.value"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.f", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + ["MemberAccessValue.self.value", "Name.self"], + [], + ["AssignName.self"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.value", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + ["MemberAccessValue.self._value", "Name.self"], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__", "FunctionDef.f", "FunctionDef.value"], + ["AssignAttr._value", "FunctionDef.value"], + None, + "__init__", + None, + ), + }, + ), + ], + ids=[ + "ClassDef", + "ClassDef with class attribute", + "ClassDef with multiple class attribute", + "ClassDef with conditional class attribute (same name)", + "ClassDef with instance attribute", + "ClassDef with multiple instance attributes (and type annotations)", + "ClassDef with conditional instance attributes (instance attributes with same name)", + "ClassDef with class and instance attribute", + "ClassDef with nested class", + "Multiple ClassDef", + "ClassDef with super class", + "ClassDef with __new__, __init__ and __post_init__", + "Assign Instance Attribute via property", + ], +) +def test_get_module_data_classes(code: str, expected: dict[str, SimpleClassScope]) -> None: + classes = get_module_data(code).classes + + transformed_classes = { + klassname: transform_scope_node(klass) for klassname, klass in classes.items() + } # The result is simplified to make the comparison easier + assert transformed_classes == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Seminar Example" + """ +glob = 1 +class A: + def __init__(self): + self.value = 10 + self.test = 20 + def f(self): + var1 = 1 +def g(): + var2 = 2 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.glob", []), + SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.value", []), + SimpleScope("InstanceVariable.MemberAccess.self.test", []), + ], + [ + "AssignName.self", + "Name.self", + "MemberAccessTarget.self.value", + "MemberAccessTarget.self.test", + ], + [], + [], + ["AssignName.self"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.self", "AssignName.var1"], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__", "FunctionDef.f"], + ["AssignAttr.value", "AssignAttr.test"], + None, + "__init__", + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.g", + [SimpleScope("LocalVariable.AssignName.var2", [])], + ["AssignName.var2"], + [], + [], + ), + ], + ), + ], + ), + ( # language=Python "Function Scope" + """ +def function_scope(): + res = 23 + return res + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [SimpleScope("LocalVariable.AssignName.res", [])], + ["AssignName.res"], + ["Name.res"], + [], + ), + ], + ), + ], + ), + ( # language=Python "Function Scope with variable" + """ +var1 = 10 +def function_scope(): + res = var1 + return res + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.var1", []), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [SimpleScope("LocalVariable.AssignName.res", [])], + ["AssignName.res"], + ["Name.var1", "Name.res"], + [], + [], + ["AssignName.var1"], + ), + ], + ), + ], + ), + ( # language=Python "Function Scope with global variables" + """ +var1 = 10 +var2 = 20 +def function_scope(): + global var1, var2 + res = var1 + var2 + return res + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.var1", []), + SimpleScope("GlobalVariable.AssignName.var2", []), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [SimpleScope("LocalVariable.AssignName.res", [])], + ["AssignName.res"], + ["Name.var1", "Name.var2", "Name.res"], + [], + [], + ["AssignName.var1", "AssignName.var2"], + ), + ], + ), + ], + ), + ( # language=Python "Function Scope with Parameter" + """ +def function_scope(parameter): + res = parameter + return res + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [ + SimpleScope("Parameter.AssignName.parameter", []), + SimpleScope("LocalVariable.AssignName.res", []), + ], + ["AssignName.parameter", "AssignName.res"], + ["Name.parameter", "Name.res"], + [], + ["AssignName.parameter"], + ), + ], + ), + ], + ), + ( # language=Python "While loop" + """ +def do_something(): + pass + +def while_loop(var1): + while var1 > 0: + do_something() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.while_loop", + [ + SimpleScope("Parameter.AssignName.var1", []), + ], + ["AssignName.var1"], + ["Name.var1"], + ["Call.do_something"], + ["AssignName.var1"], + ), + ], + ), + ], + ), + ( # language=Python "For loop" + """ +def do_something(): + pass + +def for_loop(var1): + for var1 in range(10): + do_something() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.for_loop", + [ + SimpleScope("Parameter.AssignName.var1", []), + SimpleScope("Parameter.AssignName.var1", []), + ], + ["AssignName.var1"], + [], + ["Call.range", "Call.do_something"], + ["AssignName.var1"], + ), + ], + ), + ], + ), + ( # language=Python "If statement" + """ +def do_something(): + pass + +def if_state(var1): + if var1 > 0: + do_something() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.if_state", + [ + SimpleScope("Parameter.AssignName.var1", []), + ], + ["AssignName.var1"], + ["Name.var1"], + ["Call.do_something"], + ["AssignName.var1"], + ), + ], + ), + ], + ), + ( # language=Python "If Else statement" + """ +def do_something(): + pass + +def do_something_else(): + pass + +def if_else_state(var1): + if var1 > 0: + do_something() + else: + do_something_else() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something_else", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.if_else_state", + [ + SimpleScope("Parameter.AssignName.var1", []), + ], + ["AssignName.var1"], + ["Name.var1"], + ["Call.do_something", "Call.do_something_else"], + ["AssignName.var1"], + ), + ], + ), + ], + ), + ( # language=Python "If Elif statement" + """ +def do_something(): + pass + +def do_something_else(): + pass + +def if_elif_state(var1, var2): + if var1 & True: + do_something() + elif var1 | var2: + do_something_else() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.do_something_else", + [], + [], + [], + [], + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.if_elif_state", + [ + SimpleScope("Parameter.AssignName.var1", []), + SimpleScope("Parameter.AssignName.var2", []), + ], + ["AssignName.var1", "AssignName.var2"], + ["Name.var1", "Name.var2"], + ["Call.do_something", "Call.do_something_else"], + ["AssignName.var1", "AssignName.var2"], + ), + ], + ), + ], + ), + ( # language=Python "Class Scope with class attribute and class function" + """ +class A: + class_attr1 = 20 + + def local_class_attr(self): + var1 = A.class_attr1 + return var1 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.class_attr1", []), + SimpleFunctionScope( + "ClassVariable.FunctionDef.local_class_attr", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.self", "AssignName.var1"], + ["MemberAccessValue.A.class_attr1", "Name.A", "Name.var1"], + [], + ["AssignName.self"], + ), + ], + ["AssignName.class_attr1", "FunctionDef.local_class_attr"], + [], + ), + ], + ), + ], + ), + ( # language=Python "Class Scope with instance attribute and class function" + """ +class B: + local_class_attr1 = 20 + local_class_attr2 = 30 + + def __init__(self): + self.instance_attr1 = 10 + + def local_instance_attr(self): + var1 = self.instance_attr1 + return var1 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.B", + [ + SimpleScope("ClassVariable.AssignName.local_class_attr1", []), + SimpleScope("ClassVariable.AssignName.local_class_attr2", []), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.instance_attr1", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.instance_attr1"], + [], + [], + ["AssignName.self"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.local_instance_attr", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("LocalVariable.AssignName.var1", []), + ], + ["AssignName.self", "AssignName.var1"], + ["MemberAccessValue.self.instance_attr1", "Name.self", "Name.var1"], + [], + ["AssignName.self"], + ), + ], + [ + "AssignName.local_class_attr1", + "AssignName.local_class_attr2", + "FunctionDef.__init__", + "FunctionDef.local_instance_attr", + ], + ["AssignAttr.instance_attr1"], + None, + "__init__", + ), + ], + ), + ], + ), + ( # language=Python "Class Scope with instance attribute and module function" + """ +class B: + def __init__(self): + self.instance_attr1 = 10 + +def local_instance_attr(): + var1 = B().instance_attr1 + return var1 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.B", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("InstanceVariable.MemberAccess.self.instance_attr1", []), + ], + ["AssignName.self", "Name.self", "MemberAccessTarget.self.instance_attr1"], + [], + [], + ["AssignName.self"], + ), + ], + ["FunctionDef.__init__"], + ["AssignAttr.instance_attr1"], + None, + "__init__", + ), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.local_instance_attr", + [SimpleScope("LocalVariable.AssignName.var1", [])], + ["AssignName.var1"], + ["MemberAccessValue.B.instance_attr1", "Name.var1"], + ["Call.B"], + ), + ], + ), + ], + ), + ( # language=Python "Class Scope within Class Scope" + """ +class A: + var1 = 10 + + class B: + var2 = 20 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.var1", []), + SimpleClassScope( + "ClassVariable.ClassDef.B", + [SimpleScope("ClassVariable.AssignName.var2", [])], + ["AssignName.var2"], + [], + ), + ], + ["AssignName.var1", "ClassDef.B"], + [], + ), + ], + ), + ], + ), + ( # language=Python "Class Scope with subclass" + """ +class A: + var1 = 10 + +class X: + var3 = 30 + +class B(A, X): + var2 = 20 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.A", + [SimpleScope("ClassVariable.AssignName.var1", [])], + ["AssignName.var1"], + [], + ), + SimpleClassScope( + "GlobalVariable.ClassDef.X", + [SimpleScope("ClassVariable.AssignName.var3", [])], + ["AssignName.var3"], + [], + ), + SimpleClassScope( + "GlobalVariable.ClassDef.B", + [SimpleScope("ClassVariable.AssignName.var2", [])], + ["AssignName.var2"], + [], + None, + None, + None, + ["ClassDef.A", "ClassDef.X"], + ), + ], + ), + ], + ), + ( # language=Python "Class Scope within Function Scope" + """ +def function_scope(): + var1 = 10 + + class B: + var2 = 20 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + SimpleClassScope( + "LocalVariable.ClassDef.B", + [SimpleScope("ClassVariable.AssignName.var2", [])], + ["AssignName.var2"], + [], + ), + ], + ["AssignName.var1"], + [], + [], + ), + ], + ), + ], + ), + ( # language=Python "Function Scope within Function Scope" + """ +def function_scope(): + var1 = 10 + + def local_function_scope(): + var2 = 20 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + SimpleFunctionScope( + "LocalVariable.FunctionDef.local_function_scope", + [SimpleScope("LocalVariable.AssignName.var2", [])], + ["AssignName.var2"], + [], + [], + ), + ], + ["AssignName.var1"], + [], + [], + ), + ], + ), + ], + ), + ( # language=Python "Complex Scope" + """ +def function_scope(): + var1 = 10 + + def local_function_scope(): + var2 = 20 + + class LocalClassScope: + var3 = 30 + + def local_class_function_scope(self): + var4 = 40 + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.function_scope", + [ + SimpleScope("LocalVariable.AssignName.var1", []), + SimpleFunctionScope( + "LocalVariable.FunctionDef.local_function_scope", + [ + SimpleScope("LocalVariable.AssignName.var2", []), + SimpleClassScope( + "LocalVariable.ClassDef.LocalClassScope", + [ + SimpleScope("ClassVariable.AssignName.var3", []), + SimpleFunctionScope( + "ClassVariable.FunctionDef.local_class_function_scope", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope( + "LocalVariable.AssignName.var4", + [], + ), + ], + ["AssignName.self", "AssignName.var4"], + [], + [], + ["AssignName.self"], + ), + ], + ["AssignName.var3", "FunctionDef.local_class_function_scope"], + [], + ), + ], + ["AssignName.var2"], + [], + [], + [], + ), + ], + ["AssignName.var1"], + [], + [], + ), + ], + ), + ], + ), + ( # language=Python "List Comprehension in Module" + """ +nums = ["aaa", "bb", "ase"] +[len(num) for num in nums] + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.nums", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ), + ], + ), + ( # language=Python "List Comprehension in Class" + """ +class A: + nums = ["aaa", "bb", "ase"] + x = [len(num) for num in nums] + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.A", + [ + SimpleScope("ClassVariable.AssignName.nums", []), + SimpleScope("ClassVariable.AssignName.x", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ["AssignName.nums", "AssignName.x"], + [], + ), + ], + ), + ], + ), + ( # language=Python "List Comprehension in Function" + """ +def fun(): + nums = ["aaa", "bb", "ase"] + x = [len(num) for num in nums] + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.fun", + [ + SimpleScope("LocalVariable.AssignName.nums", []), + SimpleScope("LocalVariable.AssignName.x", []), + SimpleScope("ListComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ["AssignName.nums", "AssignName.x"], + [], + ["Call.len"], + ), + ], + ), + ], + ), + ( # language=Python "Dict Comprehension in Module" + """ +nums = [1, 2, 3, 4] +{num: num*num for num in nums} + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.nums", []), + SimpleScope("DictComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ), + ], + ), + ( # language=Python "Set Comprehension in Module" + """ +nums = [1, 2, 3, 4] +{num*num for num in nums if num % 2 == 0} + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.nums", []), + SimpleScope("SetComp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ), + ], + ), + ( # language=Python "Generator Expression in Module" + """ +(num*num for num in range(10)) + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GeneratorExp", [SimpleScope("LocalVariable.AssignName.num", [])]), + ], + ), + ], + ), + ( # language=Python "With Statement" + """ +file = "file.txt" +with file: + a = 1 + """, # language=none + [ + SimpleScope( + "Module", + [SimpleScope("GlobalVariable.AssignName.file", []), SimpleScope("GlobalVariable.AssignName.a", [])], + ), + ], + ), + ( # language=Python "With Statement File" + """ +file = "file.txt" +with open(file, "r") as f: + a = 1 + f.read() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.file", []), + SimpleScope("GlobalVariable.AssignName.f", []), + SimpleScope("GlobalVariable.AssignName.a", []), + ], + ), + ], + ), + ( # language=Python "With Statement Function" + """ +def fun(): + with open("text.txt") as f: + text = f.read() + print(text) + f.close() + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.fun", + [ + SimpleScope("LocalVariable.AssignName.f", []), + SimpleScope("LocalVariable.AssignName.text", []), + ], + ["AssignName.f", "AssignName.text"], + ["MemberAccessValue.f.read", "Name.f", "Name.text", "MemberAccessValue.f.close"], + ["Call.open", "Call.read", "Call.print", "Call.close"], + ), + ], + ), + ], + ), + ( # language=Python "With Statement Class" + """ +class MyContext: + def __enter__(self): + print("Entering the context") + return self + + def __exit__(self): + print("Exiting the context") + +with MyContext() as context: + print("Inside the context") + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleClassScope( + "GlobalVariable.ClassDef.MyContext", + [ + SimpleFunctionScope( + "ClassVariable.FunctionDef.__enter__", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + ["Name.self"], + ["Call.print"], + ["AssignName.self"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__exit__", + [SimpleScope("Parameter.AssignName.self", [])], + ["AssignName.self"], + [], + ["Call.print"], + ["AssignName.self"], + ), + ], + ["FunctionDef.__enter__", "FunctionDef.__exit__"], + [], + ), + SimpleScope("GlobalVariable.AssignName.context", []), + ], + ), + ], + ), + ( # language=Python "Match statement" + """ +var1, var2 = 10, 20 +def f(a): + b = var1 + match var1: + case 1: return var1 + case 2: return var2 + b + case (a, b): return var1, a, b + case _: + result = b + + x = result + y = b + return y + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("GlobalVariable.AssignName.var1", []), + SimpleScope("GlobalVariable.AssignName.var2", []), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [ + SimpleScope("Parameter.AssignName.a", []), + SimpleScope("LocalVariable.AssignName.b", []), + SimpleScope("LocalVariable.AssignName.result", []), + SimpleScope("LocalVariable.AssignName.x", []), + SimpleScope("LocalVariable.AssignName.y", []), + ], + [ + "AssignName.a", + "AssignName.b", + "AssignName.result", + "AssignName.x", + "AssignName.y", + ], + ["Name.var1", "Name.var2", "Name.b", "Name.a", "Name.result", "Name.y"], + [], + ["AssignName.a"], + ["AssignName.var1", "AssignName.var2"], + ), + ], + ), + ], + ), + ( # language=Python "Try Except" + """ +def try_except(num1, num2): + try: + result = num1 / num2 + except ZeroDivisionError as error: + print(error) + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.try_except", + [ + SimpleScope("Parameter.AssignName.num1", []), + SimpleScope("Parameter.AssignName.num2", []), + SimpleScope( + "Try", + [ + SimpleScope("LocalVariable.AssignName.result", []), + SimpleScope("LocalVariable.AssignName.error", []), + ], + ), + ], + [ + "AssignName.num1", + "AssignName.num2", + "AssignName.result", + ], + ["Name.num1", "Name.num2", "Name.ZeroDivisionError", "Name.error"], + ["Call.print"], + ["AssignName.num1", "AssignName.num2"], + ), + ], + ), + ], + ), + ( # language=Python "Try Except Finally" + """ +def try_except_finally(num1, num2, num3): + try: + result = num1 / num2 + except ZeroDivisionError as error: + print(error) + finally: + final = num3 + + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.try_except_finally", + [ + SimpleScope("Parameter.AssignName.num1", []), + SimpleScope("Parameter.AssignName.num2", []), + SimpleScope("Parameter.AssignName.num3", []), + SimpleScope( + "Try", + [ + SimpleScope("LocalVariable.AssignName.result", []), + SimpleScope("LocalVariable.AssignName.error", []), + SimpleScope("LocalVariable.AssignName.final", []), + ], + ), + ], + [ + "AssignName.num1", + "AssignName.num2", + "AssignName.num3", + "AssignName.result", + "AssignName.final", + ], + ["Name.num1", "Name.num2", "Name.ZeroDivisionError", "Name.error", "Name.num3"], + ["Call.print"], + ["AssignName.num1", "AssignName.num2", "AssignName.num3"], + ), + ], + ), + ], + ), + ( # language=Python "Try Except Else Finally" + """ +def try_except_else_finally(num1, num2, num3): + try: + result = num1 / num2 + except ZeroDivisionError as error: + print(error) + except Exception as error: + print(error) + else: + result2 = num1 + finally: + final = num3 + + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.FunctionDef.try_except_else_finally", + [ + SimpleScope("Parameter.AssignName.num1", []), + SimpleScope("Parameter.AssignName.num2", []), + SimpleScope("Parameter.AssignName.num3", []), + SimpleScope( + "Try", + [ + SimpleScope("LocalVariable.AssignName.result", []), + SimpleScope("LocalVariable.AssignName.error", []), + SimpleScope("LocalVariable.AssignName.error", []), + SimpleScope("LocalVariable.AssignName.result2", []), + SimpleScope("LocalVariable.AssignName.final", []), + ], + ), + ], + [ + "AssignName.num1", + "AssignName.num2", + "AssignName.num3", + "AssignName.result", + "AssignName.result2", + "AssignName.final", + ], + [ + "Name.num1", + "Name.num2", + "Name.ZeroDivisionError", + "Name.error", + "Name.Exception", + "Name.num3", + ], + ["Call.print"], + ["AssignName.num1", "AssignName.num2", "AssignName.num3"], + ), + ], + ), + ], + ), + ( # language=Python "Lambda" + """ +lambda x, y: x + y + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.Lambda", + [SimpleScope("Parameter.AssignName.x", []), SimpleScope("Parameter.AssignName.y", [])], + ["AssignName.x", "AssignName.y"], + ["Name.x", "Name.y"], + [], + ["AssignName.x", "AssignName.y"], + ), + ], + ), + ], + ), + ( # language=Python "Lambda" + """ +(lambda x, y: x + y)(10, 20) + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.Lambda", + [SimpleScope("Parameter.AssignName.x", []), SimpleScope("Parameter.AssignName.y", [])], + ["AssignName.x", "AssignName.y"], + ["Name.x", "Name.y"], + [], + ["AssignName.x", "AssignName.y"], + ), + ], + ), + ], + ), + ( # language=Python "Lambda with name" + """ +double = lambda x: 2 * x + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleFunctionScope( + "GlobalVariable.Lambda.double", + [SimpleScope("Parameter.AssignName.x", [])], + ["AssignName.x"], + ["Name.x"], + [], + ["AssignName.x"], + ), + ], + ), + ], + ), + ( # language=Python "Annotations" + """ +from typing import Union + +def f(a: int | str, b: Union[int, str]) -> tuple[float, str]: + return float(a), str(b) + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("Import.ImportFrom.typing.Union", []), + SimpleFunctionScope( + "GlobalVariable.FunctionDef.f", + [SimpleScope("Parameter.AssignName.a", []), SimpleScope("Parameter.AssignName.b", [])], + ["AssignName.a", "AssignName.b"], + ["Name.a", "Name.b"], + ["Call.float", "Call.str"], + ["AssignName.a", "AssignName.b"], + ), + ], + ), + ], + ), + ( # language=Python "ASTWalker" + """ +from collections.abc import Callable +from typing import Any + +import astroid + +_EnterAndLeaveFunctions = tuple[ + Callable[[astroid.NodeNG], None] | None, + Callable[[astroid.NodeNG], None] | None, +] + + +class ASTWalker: + additional_locals = [] + + def __init__(self, handler: Any) -> None: + self._handler = handler + self._cache: dict[type, _EnterAndLeaveFunctions] = {} + + def walk(self, node: astroid.NodeNG) -> None: + self.__walk(node, set()) + + def __walk(self, node: astroid.NodeNG, visited_nodes: set[astroid.NodeNG]) -> None: + if node in visited_nodes: + raise AssertionError("Node visited twice") + visited_nodes.add(node) + + self.__enter(node) + for child_node in node.get_children(): + self.__walk(child_node, visited_nodes) + self.__leave(node) + + def __enter(self, node: astroid.NodeNG) -> None: + method = self.__get_callbacks(node)[0] + if method is not None: + method(node) + + def __leave(self, node: astroid.NodeNG) -> None: + method = self.__get_callbacks(node)[1] + if method is not None: + method(node) + + def __get_callbacks(self, node: astroid.NodeNG) -> _EnterAndLeaveFunctions: + klass = node.__class__ + methods = self._cache.get(klass) + + if methods is None: + handler = self._handler + class_name = klass.__name__.lower() + enter_method = getattr(handler, f"enter_{class_name}", getattr(handler, "enter_default", None)) + leave_method = getattr(handler, f"leave_{class_name}", getattr(handler, "leave_default", None)) + self._cache[klass] = (enter_method, leave_method) + else: + enter_method, leave_method = methods + + return enter_method, leave_method + + """, # language=none + [ + SimpleScope( + "Module", + [ + SimpleScope("Import.ImportFrom.collections.abc.Callable", []), + SimpleScope("Import.ImportFrom.typing.Any", []), + SimpleScope("Import.Import.astroid", []), + SimpleScope("GlobalVariable.AssignName._EnterAndLeaveFunctions", []), + SimpleClassScope( + "GlobalVariable.ClassDef.ASTWalker", + [ + SimpleScope("ClassVariable.AssignName.additional_locals", []), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__init__", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.handler", []), + SimpleScope("InstanceVariable.MemberAccess.self._handler", []), + SimpleScope("InstanceVariable.MemberAccess.self._cache", []), + ], + [ + "AssignName.self", + "Name.self", + "AssignName.handler", + "MemberAccessTarget.self._handler", + "MemberAccessTarget.self._cache", + ], + ["Name.handler"], + [], + ["AssignName.self", "AssignName.handler"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.walk", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.node", []), + ], + ["AssignName.self", "AssignName.node"], + ["MemberAccessValue.self.__walk", "Name.self", "Name.node"], + ["Call.__walk", "Call.set"], + ["AssignName.self", "AssignName.node"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__walk", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.node", []), + SimpleScope("Parameter.AssignName.visited_nodes", []), + SimpleScope("LocalVariable.AssignName.child_node", []), + ], + [ + "AssignName.self", + "AssignName.node", + "AssignName.visited_nodes", + "AssignName.child_node", + ], + [ + "Name.node", + "Name.visited_nodes", + "MemberAccessValue.visited_nodes.add", + "MemberAccessValue.self.__enter", + "Name.self", + "MemberAccessValue.node.get_children", + "MemberAccessValue.self.__walk", + "Name.child_node", + "MemberAccessValue.self.__leave", + ], + [ + "Call.AssertionError", + "Call.add", + "Call.__enter", + "Call.get_children", + "Call.__walk", + "Call.__leave", + ], + ["AssignName.self", "AssignName.node", "AssignName.visited_nodes"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__enter", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.node", []), + SimpleScope("LocalVariable.AssignName.method", []), + ], + ["AssignName.self", "AssignName.node", "AssignName.method"], + ["MemberAccessValue.self.__get_callbacks", "Name.self", "Name.node", "Name.method"], + ["Call.__get_callbacks", "Call.method"], + ["AssignName.self", "AssignName.node"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__leave", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.node", []), + SimpleScope("LocalVariable.AssignName.method", []), + ], + ["AssignName.self", "AssignName.node", "AssignName.method"], + ["MemberAccessValue.self.__get_callbacks", "Name.self", "Name.node", "Name.method"], + ["Call.__get_callbacks", "Call.method"], + ["AssignName.self", "AssignName.node"], + ), + SimpleFunctionScope( + "ClassVariable.FunctionDef.__get_callbacks", + [ + SimpleScope("Parameter.AssignName.self", []), + SimpleScope("Parameter.AssignName.node", []), + SimpleScope("LocalVariable.AssignName.klass", []), + SimpleScope("LocalVariable.AssignName.methods", []), + SimpleScope("LocalVariable.AssignName.handler", []), + SimpleScope("LocalVariable.AssignName.class_name", []), + SimpleScope("LocalVariable.AssignName.enter_method", []), + SimpleScope("LocalVariable.AssignName.leave_method", []), + SimpleScope("LocalVariable.AssignName.enter_method", []), + SimpleScope("LocalVariable.AssignName.leave_method", []), + ], + [ + "AssignName.self", + "AssignName.node", + "AssignName.klass", + "AssignName.methods", + "AssignName.handler", + "AssignName.class_name", + "AssignName.enter_method", + "AssignName.leave_method", + "MemberAccessTarget.self._cache", + ], + [ + "MemberAccessValue.node.__class__", + "Name.node", + "MemberAccessValue.self._cache.get", + "MemberAccessValue.self._cache", + "Name.self", + "Name.klass", + "Name.methods", + "MemberAccessValue.self._handler", + "MemberAccessValue.klass.__name__.lower", + "MemberAccessValue.klass.__name__", + "Name.handler", + "Name.class_name", + "Name.enter_method", + "Name.leave_method", + ], + ["Call.get", "Call.lower", "Call.getattr"], + ["AssignName.self", "AssignName.node"], + [], + ), + ], + [ + "AssignName.additional_locals", + "FunctionDef.__init__", + "FunctionDef.walk", + "FunctionDef.__walk", + "FunctionDef.__enter", + "FunctionDef.__leave", + "FunctionDef.__get_callbacks", + ], + ["AssignAttr._handler", "AssignAttr._cache"], + None, + "__init__", + ), + ], + ), + ], + ), + ], + ids=[ + "Seminar Example", + "Function Scope", + "Function Scope with variable", + "Function Scope with global variables", + "Function Scope with Parameter", + "While loop", + "For loop", + "If statement", + "If Else statement", + "If Elif statement", + "Class Scope with class attribute and Class function", + "Class Scope with instance attribute and Class function", + "Class Scope with instance attribute and Modul function", + "Class Scope within Class Scope", + "Class Scope with subclass", + "Class Scope within Function Scope", + "Function Scope within Function Scope", + "Complex Scope", + "List Comprehension in Module", + "List Comprehension in Class", + "List Comprehension in Function", + "Dict Comprehension in Module", + "Set Comprehension in Module", + "Generator Expression in Module", + "With Statement", + "With Statement File", + "With Statement Function", + "With Statement Class", + "Match statement", + "Try Except", + "Try Except Finally", + "Try Except Else Finally", + "Lambda", + "Lambda call", + "Lambda with name", + "Annotations", + "ASTWalker", + ], +) +def test_get_module_data_scope(code: str, expected: list[SimpleScope | SimpleClassScope]) -> None: + scope = get_module_data(code).scope + # assert result == expected + transformed_result = [ + transform_scope_node(node) for node in scope + ] # The result is simplified to make the comparison easier + assert transformed_result == expected diff --git a/tests/safeds_stubgen/api_analyzer/purity_analysis/test_infer_purity.py b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_infer_purity.py new file mode 100644 index 00000000..d54d1ecc --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_infer_purity.py @@ -0,0 +1,1947 @@ +from dataclasses import dataclass + +import pytest +from safeds_stubgen.api_analyzer.purity_analysis import ( + infer_purity, +) +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + CallOfParameter, + ClassVariable, + FileRead, + FileWrite, + Import, + Impure, + ImpurityReason, + InstanceVariable, + NativeCall, + NodeID, + NonLocalVariableRead, + NonLocalVariableWrite, + ParameterAccess, + Pure, + PurityResult, + StringLiteral, + UnknownCall, + UnknownClassInit, + UnknownFunctionCall, +) + + +@dataclass +class SimpleImpure: + """Class for simple impure results. + + A simplified class of the Impure class for testing purposes. + + Attributes + ---------- + reasons : set[str] + The set of reasons for impurity as strings. + """ + + reasons: set[str] + + +def to_string_function_id(node_id: NodeID | str) -> str: + """Convert a function to a string representation. + + Parameters + ---------- + node_id : NodeID | str + The NodeID to convert. + + Returns + ------- + str + The string representation of the NodeID. + """ + if isinstance(node_id, str): + return f"{node_id}" + return f"{node_id.name}.line{node_id.line}" + + +def to_simple_result(purity_result: PurityResult) -> Pure | SimpleImpure: # type: ignore[return] # all cases are handled + """Convert a purity result to a simple result. + + Parameters + ---------- + purity_result : PurityResult + The purity result to convert, either Pure or Impure. + + Returns + ------- + Pure | SimpleImpure + The converted purity result. + """ + if isinstance(purity_result, Pure): + return Pure() + elif isinstance(purity_result, Impure): + return SimpleImpure({to_string_reason(reason) for reason in purity_result.reasons}) + + +def to_string_reason(reason: ImpurityReason) -> str: # type: ignore[return] # all cases are handled + """Convert an impurity reason to a string. + + Parameters + ---------- + reason : ImpurityReason + The impurity reason to convert. + + Returns + ------- + str + The converted impurity reason. + """ + if reason is None: + raise ValueError("Reason must not be None") + if isinstance(reason, NonLocalVariableRead): + if isinstance(reason.symbol, ClassVariable | InstanceVariable) and reason.symbol.klass is not None: + return f"NonLocalVariableRead.{reason.symbol.__class__.__name__}.{reason.symbol.klass.name}.{reason.symbol.name}" + if isinstance(reason.symbol, Import): + if reason.symbol.name: + return f"NonLocalVariableRead.{reason.symbol.__class__.__name__}.{reason.symbol.module}.{reason.symbol.name}" + return f"NonLocalVariableRead.{reason.symbol.__class__.__name__}.{reason.symbol.module}" + return f"NonLocalVariableRead.{reason.symbol.__class__.__name__}.{reason.symbol.name}" + elif isinstance(reason, NonLocalVariableWrite): + if isinstance(reason.symbol, ClassVariable | InstanceVariable) and reason.symbol.klass is not None: + return f"NonLocalVariableWrite.{reason.symbol.__class__.__name__}.{reason.symbol.klass.name}.{reason.symbol.name}" + if isinstance(reason.symbol, Import): + if reason.symbol.name: + return f"NonLocalVariableWrite.{reason.symbol.__class__.__name__}.{reason.symbol.module}.{reason.symbol.name}" + return f"NonLocalVariableWrite.{reason.symbol.__class__.__name__}.{reason.symbol.module}" + return f"NonLocalVariableWrite.{reason.symbol.__class__.__name__}.{reason.symbol.name}" + elif isinstance(reason, FileRead): + if isinstance(reason.source, ParameterAccess): + return f"FileRead.{reason.source.__class__.__name__}.{reason.source.parameter}" + if isinstance(reason.source, StringLiteral): + return f"FileRead.{reason.source.__class__.__name__}.{reason.source.value}" + elif isinstance(reason, FileWrite): + if isinstance(reason.source, ParameterAccess): + return f"FileWrite.{reason.source.__class__.__name__}.{reason.source.parameter}" + if isinstance(reason.source, StringLiteral): + return f"FileWrite.{reason.source.__class__.__name__}.{reason.source.value}" + elif isinstance(reason, UnknownCall | NativeCall | CallOfParameter): + if isinstance(reason.expression, StringLiteral): + return f"{reason.__class__.__name__}.{reason.expression.__class__.__name__}.{reason.expression.value}" + elif isinstance(reason.expression, ParameterAccess): + return ( + f"{reason.__class__.__name__}.{reason.expression.__class__.__name__}.{reason.expression.parameter.name}" + ) + elif isinstance(reason.expression, UnknownFunctionCall | UnknownClassInit): + return f"{reason.__class__.__name__}.{reason.expression.__class__.__name__}.{reason.expression.name}" + else: + raise NotImplementedError(f"Unknown reason: {reason}") + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Trivial function" + """ +def fun(): + pass # Pure + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "Trivial function with parameter and return" + """ +def fun(x): + return 2 * x # Pure: VariableRead from LocalVariable + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "VariableWrite to LocalVariable" + """ +def fun(): + var1 = 2 # Pure: VariableWrite to LocalVariable + return var1 + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "VariableWrite to LocalVariable with parameter" + """ +def fun(x): + var1 = x # Pure: VariableWrite to LocalVariable + return var1 + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "VariableRead from LocalVariable" + """ +def fun(): + var1 = 1 # Pure: VariableRead from LocalVariable + return var1 + """, # language= None + {"fun.line2": Pure()}, + ), # TODO: [LATER] For this case it would be good to check if the instance that is accessed is local or not. Since this case is actually pure. + ( # language=Python "VariableWrite to InstanceVariable - but actually a LocalVariable" + """ +class A: + def __init__(self): + self.instance_attr1 = 10 + +def fun(): + a = A() + a.instance_attr1 = 20 # Pure: VariableWrite to InstanceVariable - but actually a LocalVariable + """, # language= None + { + "__init__.line3": Pure(), + "fun.line6": SimpleImpure({"NonLocalVariableWrite.InstanceVariable.A.instance_attr1"}), + }, + ), # TODO: [LATER] For this case it would be good to check if the instance that is accessed is local or not. Since this case is actually pure. + ( # language=Python "VariableRead from InstanceVariable - but actually a LocalVariable" + """ +class A: + def __init__(self): + self.instance_attr1 = 10 + +def fun(): + a = A() + res = a.instance_attr1 # Pure: VariableRead from InstanceVariable - but actually a LocalVariable + return res + """, # language= None + { + "__init__.line3": Pure(), + "fun.line6": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A.instance_attr1"}), + }, + ), # TODO: [LATER] For this case it would be good to check if the instance that is accessed is local or not. Since this case is actually pure. + ( # language=Python "VariableRead and VariableWrite in chained class attribute and instance attribute" + """ +class A: + def __init__(self): + self.name = 10 + +class B: + upper_class: A = A() + +def f(): + b = B() + x = b.upper_class.name + +def g(): + b = B() + b.upper_class.name = 20 + """, # language=none + { + "__init__.line3": Pure(), + "f.line9": SimpleImpure( + { + "NonLocalVariableRead.ClassVariable.B.upper_class", + "NonLocalVariableRead.InstanceVariable.A.name", + }, + ), + "g.line13": SimpleImpure( + { + "NonLocalVariableWrite.ClassVariable.B.upper_class", + "NonLocalVariableWrite.InstanceVariable.A.name", + }, + ), + }, + ), + ( # language=Python "Pure Class initialization" + """ +class A: + pass + +class B: + def __init__(self): + pass + +class C: + def __init__(self): + self.name = "test" + +def fun1(): + a = A() + +def fun2(): + b = B() + + """, # language= None + {"__init__.line6": Pure(), "__init__.line10": Pure(), "fun1.line13": Pure(), "fun2.line16": Pure()}, + ), + ( # language=Python "Pure Class initialization with propagated purity in init" + """ +class A: + def __init__(self): + self.name = self.fun1() + self.value = self.fun2() + self.test = None + self.fun3() + + @staticmethod + def fun1(): + return "test" + + def fun2(self): + return self.name # Impure: VariableRead from InstanceVariable + + def fun3(self): + self.test = 10 # Impure: VariableWrite to InstanceVariable + """, # language= None + { # TODO: [LATER] For init we need to filter out all reasons which are related to instance variables of the class (from the init function itself or propagated from called functions) + "__init__.line3": SimpleImpure( + {"NonLocalVariableRead.InstanceVariable.A.name", "NonLocalVariableWrite.InstanceVariable.A.test"}, + ), + "fun1.line10": Pure(), + "fun2.line13": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A.name"}), + "fun3.line16": SimpleImpure({"NonLocalVariableWrite.InstanceVariable.A.test"}), + }, + ), + ( # language=Python "Call of Pure Function" + """ +def fun1(): + res = fun2() # Pure: Call of Pure Function + return res + +def fun2(): + return 1 # Pure + """, # language= None + {"fun1.line2": Pure(), "fun2.line6": Pure()}, + ), + ( # language=Python "Call of Pure Chain of Functions" + """ +def fun1(): + res = fun2() # Pure: Call of Pure Function + return res + +def fun2(): + return fun3() # Pure: Call of Pure Function + +def fun3(): + return 1 # Pure + """, # language= None + {"fun1.line2": Pure(), "fun2.line6": Pure(), "fun3.line9": Pure()}, + ), + ( # language=Python "Call of Pure Chain of Functions with cycle - one entry point" + """ +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + cycle1() + +def entry(): + cycle1() + """, # language= None + { + "cycle1.line2": Pure(), + "cycle2.line5": Pure(), + "cycle3.line8": Pure(), + "entry.line11": Pure(), + }, # for cycles, we want to propagate the purity of the cycle to all functions in the cycle + # but only return the results for the real functions + ), + ( # language=Python "Call of Pure Chain of Functions with cycle - direct entry" + """ +def fun1(count): + if count > 0: + fun2(count - 1) + +def fun2(count): + if count > 0: + fun1(count - 1) + """, # language= None + {"fun1.line2": Pure(), "fun2.line6": Pure()}, + ), + ( # language=Python "Call of Pure Builtin Function" + """ +def fun(): + res = range(2) # Pure: Call of Pure Builtin Function and write to LocalVariable + return res + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "Lambda function" + """ +def fun(): + res = (lambda x, y: x + y)(10, 20) # Pure: Call of a Lambda Function and write to LocalVariable + return res + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "Assigned Lambda function" + """ +double = lambda x: 2 * x + """, # language= None + {"double.line2": Pure()}, + ), + ( # language=Python "Lambda as key" + """ +def fun(): + numbers = [1, 2, 3, 4] + squared_numbers = map(lambda x: x**2, numbers) + return squared_numbers + """, # language= None + {"fun.line2": Pure()}, + ), + ( # language=Python "Async Function" + """ +async def fun1(): + res = await fun2() # Pure: Call of Pure Function + +async def fun2(): + pass + """, # language= None + {"fun1.line2": Pure(), "fun2.line5": Pure()}, + ), + ( # language=Python "Multiple Calls of the same Pure function (Caching)" + """ +def fun1(): + return 1 + +a = fun1() +b = fun1() +c = fun1() + """, # language= None + {"fun1.line2": Pure()}, + ), # here the purity for fun1 can be cached for the other calls + ( # language=Python "Builtins for dict" + """ +def f(): + dictionary = {"a": 1, "b": 2, "c": 3} + + dictionary["a"] = 10 + dictionary.get("a") + dictionary.update({"d": 4}) + dictionary.pop("a") + dictionary.popitem() + dictionary.clear() + dictionary.copy() + dictionary.fromkeys("a") + dictionary.items() + dictionary.keys() + dictionary.values() + dictionary.setdefault("a", 10) + """, # language=none + { + "f.line2": Pure(), + }, + ), + ( # language=Python "Builtins for list" + """ +def f(): + list1 = [1, 2, 3] + list2 = [4, 5, 6] + + list1.append(4) + list1.clear() + list1.copy() + list1.count(1) + list1.extend(list2) + list1.index(1) + list1.insert(1, 10) + list1.pop() + list1.remove(1) + list1.reverse() + list1.sort() + """, # language=none + { + "f.line2": Pure(), + }, + ), + ( # language=Python "Builtins for set" + """ +def f(): + set1 = {1, 2, 3} + set2 = {4, 5, 6} + + set1.add(4) + set1.clear() + set1.copy() + set1.difference(set2) + set1.difference_update(set2) + set1.discard(1) + set1.intersection(set2) + set1.intersection_update(set2) + set1.isdisjoint(set2) + set1.issubset(set2) + set1.issuperset(set2) + set1.pop() + set1.remove(1) + set1.symmetric_difference(set2) + set1.symmetric_difference_update(set2) + set1.union(set2) + set1.update(set2) + """, # language=none + { + "f.line2": Pure(), + }, + ), + ( # language=Python "Assign Instance Attribute via property" + """ +class A: + def __init__(self, value): + self._value = value + + def f(self): + return self.value + + @property + def value(self): + return self._value + """, # language=none + { + "__init__.line3": Pure(), + "f.line6": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A.value"}), + "value.line10": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A._value"}), + }, + ), + ( # language=Python "Assign Instance Attribute via property with propagation" + """ +from abc import ABC + +class A(ABC): + def __init__(self, value): + self._value = value + if impure(): + pass + + @property + def value(self): + return self._value + +class B(A): + def __init__(self, value): + super().__init__(value) + +def impure(): + print("test") + """, # language=none + { + "__init__.line5": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "value.line11": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A._value"}), + "__init__.line15": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "impure.line18": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ], + ids=[ + "Trivial function", + "Trivial function with parameter and return", + "VariableWrite to LocalVariable", + "VariableWrite to LocalVariable with parameter", + "VariableRead from LocalVariable", + "VariableWrite to InstanceVariable - but actually a LocalVariable", + "VariableRead from InstanceVariable - but actually a LocalVariable", + "VariableRead and VariableWrite in chained class attribute and instance attribute", + "Pure Class initialization", + "Pure Class initialization with propagated purity in init", + "Call of Pure Function", + "Call of Pure Chain of Functions", + "Call of Pure Chain of Functions with cycle - one entry point", + "Call of Pure Chain of Functions with cycle - direct entry", + "Call of Pure Builtin Function", + "Lambda function", + "Assigned Lambda function", + "Lambda as key", + "Async Function", + "Multiple Calls of same Pure function (Caching)", + "Builtins for dict", + "Builtins for list", + "Builtins for set", + "Assign Instance Attribute via property", + "Assign Instance Attribute via property with propagation", + ], # TODO: class inits in cycles +) +def test_infer_purity_pure(code: str, expected: list[ImpurityReason]) -> None: + purity_results = next(iter(infer_purity(code).values())) + transformed_purity_results = { + to_string_function_id(function_id): to_simple_result(purity_result) + for function_id, purity_result in purity_results.items() + if not purity_result.is_class + } + + assert transformed_purity_results == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Print with str" + """ +def fun(): + print("text.txt") # Impure: FileWrite + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.StringLiteral.stdout"})}, + ), + ( # language=Python "Print with parameter" + """ +def fun(pos_arg): + print(pos_arg) # Impure: FileWrite + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.StringLiteral.stdout"})}, + ), + ( # language=Python "VariableWrite to shadowed GlobalVariable" + """ +var1 = 1 +def fun(): + var1 = 2 # Pure: VariableWrite to LocalVariable because the global variable is shadowed + return var1 + """, # language= None + { + "fun.line3": Pure(), + }, + ), + ( # language=Python "VariableWrite to GlobalVariable + """ +var1 = 1 +def fun(x): + global var1 + var1 = x # Impure: VariableWrite to GlobalVariable + return var1 # Impure: VariableRead from GlobalVariable + """, # language= None + { + "fun.line3": SimpleImpure( + { + "NonLocalVariableWrite.GlobalVariable.var1", + "NonLocalVariableRead.GlobalVariable.var1", + }, + ), + }, + ), + ( # language=Python "VariableRead from GlobalVariable" + """ +var1 = 1 +def fun(): + res = var1 # Impure: VariableRead from GlobalVariable + return res + """, # language= None + {"fun.line3": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"})}, + ), + ( # language=Python "Impure Class initialization" + """ +class A: + def __init__(self): + print("test") # Impure: FileWrite + +def fun(): + a = A() + """, # language= None + { + "__init__.line3": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun.line6": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Impure Class initialization via super" + """ +class A: + def __init__(self): + print("Test") # Impure: FileWrite + +class B(A): + def __init__(self): + super().__init__() + """, # language= None + { + "__init__.line3": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "__init__.line7": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Impure Class initialization via super multiple classes" + """ +class A: + def __init__(self): + print("Test") # Impure: FileWrite + +var1 = 1 +class C: + def __init__(self): + global var1 + var1 = 2 # Impure: VariableWrite to GlobalVariable + +class D: + def __init__(self): + input() # Impure: FileRead + +class B(A): + def __init__(self): + super().__init__() + """, # language= None + { + "__init__.line3": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "__init__.line8": SimpleImpure({"NonLocalVariableWrite.GlobalVariable.var1"}), + "__init__.line13": SimpleImpure({"FileRead.StringLiteral.stdin"}), + "__init__.line17": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Impure Class initialization via super with Builtin" + """ +class A(ValueError): + def __init__(self): + super().__init__() + """, # language= None + { + "__init__.line3": Pure(), + }, + ), + ( # language=Python "Class methode call" + """ +class A: + state: str = "A" + +class C: + state: int = 0 + + @classmethod + def set_state(cls, state): + cls.state = state + +def fun1(): + c = C() + c.set_state(1) + +def fun2(): + C().set_state(1) + """, # language= None + { + "set_state.line9": SimpleImpure({"NonLocalVariableWrite.ClassVariable.C.state"}), + "fun1.line12": SimpleImpure({"NonLocalVariableWrite.ClassVariable.C.state"}), + "fun2.line16": SimpleImpure({"NonLocalVariableWrite.ClassVariable.C.state"}), + }, + # The analysis checks the class of the classmethod and only returns it (C in this case) + ), + ( # language=Python "Class methode call of superclass" + """ +class A: + state: str = "A" + + @classmethod + def set_state(cls, state): + cls.state = state + +class C(A): + state: int = 0 + +def fun1(): + c = C() + c.set_state(1) + +def fun2(): + C().set_state(1) + """, # language= None + { # Since the analysis only checks the name of the attributes, both class attributes are referenced here. + "set_state.line6": SimpleImpure( + {"NonLocalVariableWrite.ClassVariable.A.state", "NonLocalVariableWrite.ClassVariable.C.state"}, + ), # this mistake is acceptable due to the restrictions we made + "fun1.line12": SimpleImpure( + {"NonLocalVariableWrite.ClassVariable.A.state", "NonLocalVariableWrite.ClassVariable.C.state"}, + ), + "fun2.line16": SimpleImpure( + {"NonLocalVariableWrite.ClassVariable.A.state", "NonLocalVariableWrite.ClassVariable.C.state"}, + ), + }, + ), + ( # language=Python "Class methode call of superclass (overwritten method)" + """ +class A: + state: str = "A" + + @classmethod + def set_state(cls, state): + cls.state = state + +class C(A): + state: int = 0 + + @classmethod + def set_state(cls, state): + cls.state = state + print("test") # Impure: FileWrite + +def fun1(): # Pure + a = A() + a.set_state(1) + +def fun2(): # Impure + C().set_state(1) + """, # language= None + { + "set_state.line6": SimpleImpure({"NonLocalVariableWrite.ClassVariable.A.state"}), + "set_state.line13": SimpleImpure( + {"NonLocalVariableWrite.ClassVariable.C.state", "FileWrite.StringLiteral.stdout"}, + ), + "fun1.line17": SimpleImpure( + { + "NonLocalVariableWrite.ClassVariable.A.state", + "NonLocalVariableWrite.ClassVariable.C.state", # this mistake is acceptable due to the restrictions we made + "FileWrite.StringLiteral.stdout", + }, + ), # this mistake is acceptable due to the restrictions we made + "fun2.line21": SimpleImpure( + { + "NonLocalVariableWrite.ClassVariable.A.state", # this mistake is acceptable due to the restrictions we made + "NonLocalVariableWrite.ClassVariable.C.state", + "FileWrite.StringLiteral.stdout", + }, + ), + }, + ), + ( # language=Python "Instance methode call" + """ +class A: + def __init__(self): + self.a_inst = B() + +class B: + def __init__(self): + pass + + def b_fun(self): + print("test") # Impure: FileWrite + +def fun1(): + a = A() + b = a.a_inst + +def fun2(): + a = A() + a.a_inst.b_fun() + +def fun3(): + a = A().a_inst.b_fun() + """, # language= None + { + "__init__.line3": Pure(), + "__init__.line7": Pure(), + "b_fun.line10": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun1.line13": SimpleImpure( + {"NonLocalVariableRead.InstanceVariable.A.a_inst"}, + ), # this mistake is acceptable due to the restrictions we made + "fun2.line17": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.InstanceVariable.A.a_inst"}, + ), + "fun3.line21": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.InstanceVariable.A.a_inst"}, + ), + }, + ), + ( # language=Python "VariableWrite to ClassVariable" + """ +class A: + class_attr1 = 20 + +def fun1(): + A.class_attr1 = 30 # Impure: VariableWrite to ClassVariable + +def fun2(): + A().class_attr1 = 30 # Impure: VariableWrite to ClassVariable + """, # language= None + { + "fun1.line5": SimpleImpure({"NonLocalVariableWrite.ClassVariable.A.class_attr1"}), + "fun2.line8": SimpleImpure({"NonLocalVariableWrite.ClassVariable.A.class_attr1"}), + }, + ), + ( # language=Python "VariableRead from ClassVariable" + """ +class A: + class_attr1 = 20 + +def fun1(): + res = A.class_attr1 # Impure: VariableRead from ClassVariable + return res + +def fun2(): + res = A().class_attr1 # Impure: VariableRead from ClassVariable + return res + """, # language= None + { + "fun1.line5": SimpleImpure({"NonLocalVariableRead.ClassVariable.A.class_attr1"}), + "fun2.line9": SimpleImpure({"NonLocalVariableRead.ClassVariable.A.class_attr1"}), + }, + ), + ( # language=Python "VariableWrite to InstanceVariable" + """ +class B: + def __init__(self): + self.instance_attr1 = 10 + +def fun(c): + c.instance_attr1 = 20 # Impure: VariableWrite to InstanceVariable + +b = B() +fun(b) + """, # language= None + { + "__init__.line3": Pure(), + "fun.line6": SimpleImpure({"NonLocalVariableWrite.InstanceVariable.B.instance_attr1"}), + }, + ), + ( # language=Python "VariableRead from InstanceVariable" + """ +class B: + def __init__(self): + self.instance_attr1 = 10 + +def fun(c): + res = c.instance_attr1 # Impure: VariableRead from InstanceVariable + return res + +b = B() +a = fun(b) + """, # language= None + { + "__init__.line3": Pure(), + "fun.line6": SimpleImpure({"NonLocalVariableRead.InstanceVariable.B.instance_attr1"}), + }, + ), + ( # language=Python "VariableRead and VariableWrite in chained class attribute and instance attribute" + """ +class A: + def __init__(self): + self.name = 10 + + def a_fun(self): + print(self.name) # Impure: FileWrite + +class B: + upper_class: A = A() + + def b_fun(self): + inp = input() # Impure: FileRead + return self.upper_class + +def f(): + b = B() + b.upper_class.a_fun() + +def g(): + b = B() + x = b.b_fun().name + """, # language=none + { + "__init__.line3": Pure(), + "a_fun.line6": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.InstanceVariable.A.name"}, + ), # this mistake is acceptable due to the restrictions we made + "b_fun.line12": SimpleImpure( + {"FileRead.StringLiteral.stdin", "NonLocalVariableRead.ClassVariable.B.upper_class"}, + ), # this mistake is acceptable due to the restrictions we made + "f.line16": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + "NonLocalVariableRead.ClassVariable.B.upper_class", # this mistake is acceptable due to the restrictions we made + "NonLocalVariableRead.InstanceVariable.A.name", + }, + ), # this mistake is acceptable due to the restrictions we made + "g.line20": SimpleImpure( + { + "FileRead.StringLiteral.stdin", + "NonLocalVariableRead.ClassVariable.B.upper_class", # this mistake is acceptable due to the restrictions we made + "NonLocalVariableRead.InstanceVariable.A.name", + }, + ), # this mistake is acceptable due to the restrictions we made + }, + ), + ( # language=Python "Function call of functions with same name and different purity" + """ +class A: + @staticmethod + def add(a, b): + print("test") # Impure: FileWrite + return a + b + +class B: + @staticmethod + def add(a, b): + return a + 2 * b + +def fun1(): + A.add(1, 2) + B.add(1, 2) + +def fun2(): + B.add(1, 2) + """, # language=none + { + "add.line4": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "add.line10": Pure(), + "fun1.line13": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun2.line17": SimpleImpure( + {"FileWrite.StringLiteral.stdout"}, + ), # here we need to be conservative and assume that the call is impure + }, + ), + ( # language=Python "Function call of functions with same name (different signatures)" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b, c): + print(c) # Impure: FileWrite + return a + b + c + +def fun1(): + A.add(1, 2) + +def fun2(): + B.add(1, 2, 3) + """, # language=none + { + "add.line4": Pure(), + "add.line9": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun1.line13": Pure(), + "fun2.line16": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Call of Impure Function" + """ +var1 = 1 +def fun1(): + res = fun2() # Impure: Call of Impure Function + return res + +def fun2(): + global var1 + return var1 # Impure: VariableRead from GlobalVariable + """, # language= None + { + "fun1.line3": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + "fun2.line7": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + }, + ), # here the reason of impurity for fun2 is propagated to fun1, therefore, fun1 is impure + ( # language=Python "Call of Impure Chain of Functions" + """ +var1 = 1 +def fun1(): + res = fun2() # Impure: Call of Impure Function + return res + +def fun2(): + return fun3() # Impure: Call of Impure Function + +def fun3(): + res = var1 + return res # Impure: VariableRead from GlobalVariable + """, # language= None + { + "fun1.line3": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + "fun2.line7": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + "fun3.line10": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + }, + ), + ( # language=Python "Call of Impure Chain of Functions with cycle - one entry point" + """ +var1 = 1 + +def cycle1(): + cycle2() + +def cycle2(): + global var1 + res = var1 # Impure: VariableRead from GlobalVariable + cycle3() + +def cycle3(): + print("test") # Impure: FileWrite + cycle1() + +def entry(): + cycle1() + """, # language= None + { + "cycle1.line4": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.GlobalVariable.var1"}, + ), + "cycle2.line7": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.GlobalVariable.var1"}, + ), + "cycle3.line12": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.GlobalVariable.var1"}, + ), + "entry.line16": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.GlobalVariable.var1"}, + ), + }, # for cycles, we want to propagate the impurity of the cycle to all functions in the cycle + # but only return the results for the real functions + ), + ( # language=Python "Call of Impure Chain of Functions with cycle - other calls in cycle" + """ +var1 = 1 + +def cycle1(): + other() + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + print("test") # Impure: FileWrite + cycle1() + +def other(): + global var1 + var1 = 2 # Impure: VariableWrite to GlobalVariable + +def entry(): + cycle1() + """, # language= None + { + "cycle1.line4": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "cycle2.line8": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "cycle3.line11": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "other.line15": SimpleImpure({"NonLocalVariableWrite.GlobalVariable.var1"}), + "entry.line19": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + }, # for cycles, we want to propagate the impurity of the cycle to all functions in the cycle + # but only return the results for the real functions + ), + ( # language=Python "Call of Impure Chain of Functions with cycle - cycle in cycle" + """ +var1 = 1 + +def cycle1(): + cycle2() + +def cycle2(): + cycle3() + +def cycle3(): + inner_cycle1() + print("enter inner cycle") # Impure: FileWrite + cycle1() + +def inner_cycle1(): + inner_cycle2() + +def inner_cycle2(): + inner_cycle1() + global var1 + var1 = 2 # Impure: VariableWrite to GlobalVariable + +def entry(): + cycle1() + """, # language= None + { + "cycle1.line4": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "cycle2.line7": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "cycle3.line10": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + "inner_cycle1.line15": SimpleImpure({"NonLocalVariableWrite.GlobalVariable.var1"}), + "inner_cycle2.line18": SimpleImpure({"NonLocalVariableWrite.GlobalVariable.var1"}), + "entry.line23": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableWrite.GlobalVariable.var1"}, + ), + }, # for cycles, we want to propagate the impurity of the cycle to all functions in the cycle + # but only return the results for the real functions + ), + ( # language=Python "Call of Impure Chain of Functions with cycle - direct entry" + """ +def fun1(count): + if count > 0: + fun2(count - 1) + else: + print("end") # Impure: FileWrite + +def fun2(count): + if count > 0: + fun1(count - 1) + else: + print("end") # Impure: FileWrite + """, # language= None + { + "fun1.line2": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun2.line8": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Call of Impure Builtin Function" + """ +def fun(): + res = input() # Impure: Call of Impure Builtin Function - User input is requested + return res + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.stdin"})}, + ), + ( # language=Python "Call of Impure Builtin type class methode" + """ +class A: + pass + +def fun(): + a = A() + res = a.__class__.__name__ # TODO: this is class methode call + return res + """, # language= None + {"fun.line5": SimpleImpure({""})}, # TODO: correct result + ), + ( # language=Python "Lambda function" + """ +var1 = 1 + +def fun(): + global var1 + res = (lambda x: x + var1)(10) # Impure: Call of Lambda Function which has VariableRead from GlobalVariable + return res + """, # language= None + {"fun.line4": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"})}, + ), + ( # language=Python "Lambda function with Impure Call" + """ +var1 = 1 + +def fun1(): + res = (lambda x: x + fun2())(10) # Impure: Call of Lambda Function which calls an Impure function + return res + +def fun2(): + global var1 + return var1 # Impure: VariableRead from GlobalVariable + """, # language= None + { + "fun1.line4": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + "fun2.line8": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"}), + }, + ), + ( # language=Python "Assigned Lambda function" + """ +var1 = 1 +double = lambda x: var1 * x # Impure: VariableRead from GlobalVariable + """, # language= None + {"double.line3": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"})}, + ), + ( # language=Python "Lambda as key" + """ +var1 = "x" + +def fun(): + global var1 + names = ["a", "abc", "ab", "abcd"] + sort = sorted(names, key=lambda x: x + var1) # Impure: Call of Lambda Function which has VariableRead from GlobalVariable + return sort + """, # language= None + { + "fun.line4": SimpleImpure( + {"NonLocalVariableRead.GlobalVariable.var1"}, + ), + }, + ), + ( # language=Python "Multiple Calls of the same Impure function (Caching)" + """ +var1 = 1 +def fun1(): + global var1 + res = var1 # Impure: VariableRead from GlobalVariable + return res + +a = fun1() +b = fun1() +c = fun1() + """, # language= None + {"fun1.line3": SimpleImpure({"NonLocalVariableRead.GlobalVariable.var1"})}, + ), # here the reason of impurity for fun1 can be cached for the other calls + ( # language=Python "Different Reasons for Impurity", + """ +var1 = 1 + +def fun1(): + global var1 + if var1 > 0: + var1 = fun2() # Impure: Call of Impure Function / VariableWrite to GlobalVariable + var1 = 2 # Impure: VariableWrite to GlobalVariable + print("test") # Impure: FileWrite + return var1 # Impure: VariableRead from GlobalVariable + +def fun2(): + res = input() + return res # Impure: Call of Impure Builtin Function - User input is requested + """, # language=none + { + "fun1.line4": SimpleImpure( + { + "NonLocalVariableRead.GlobalVariable.var1", + "NonLocalVariableWrite.GlobalVariable.var1", + "FileWrite.StringLiteral.stdout", + "FileRead.StringLiteral.stdin", + }, + ), # this is propagated from fun2 + "fun2.line12": SimpleImpure({"FileRead.StringLiteral.stdin"}), + }, + ), + ( # language=Python "Impure Write to Local and Global", + """ +var1 = 1 +var2 = 2 +var3 = 3 + +def fun1(a): + global var1, var2, var3 + inp = input() # Impure: Call of Impure Builtin Function - User input is requested + var1 = a = inp # Impure: VariableWrite to GlobalVariable + a = var2 = inp # Impure: VariableWrite to GlobalVariable + inp = a = var3 # Impure: VariableRead from GlobalVariable + + """, # language=none + { + "fun1.line6": SimpleImpure( + { + "FileRead.StringLiteral.stdin", + "NonLocalVariableWrite.GlobalVariable.var1", + "NonLocalVariableWrite.GlobalVariable.var2", + "NonLocalVariableRead.GlobalVariable.var3", + }, + ), + }, + ), + ( # language=Python "Call of Function with function as return", + """ +def fun1(a): + print(a) # Impure: FileWrite + return fun1 + +def fun2(): + x = fun1(1) + """, # language=none + { + "fun1.line2": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + "fun2.line6": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + }, + ), + ( # language=Python "Call within a call", + """ +def fun1(a): + print(a) # Impure: FileWrite + return a + +def fun2(a): + return a * 2 + +def fun3(): + x = fun2(fun1(2)) + """, # language=none + { + "fun1.line2": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + "fun2.line6": Pure(), + "fun3.line9": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + }, + ), + ( # language=Python "Async Function" + """ +async def fun1(): + res = await fun2() # Pure: Call of Pure Function + +async def fun2(): + print("test") # Impure: FileWrite + """, # language= None + { + "fun1.line2": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + "fun2.line5": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Try Except" + """ +glob1 = 10 + +def try_except(num1): + global glob1 + try: + result = num1 / glob1 + except ZeroDivisionError as error: + print(glob1, error) + + print(result) + """, # language=none + { + "try_except.line4": SimpleImpure( + {"FileWrite.StringLiteral.stdout", "NonLocalVariableRead.GlobalVariable.glob1"}, + ), + }, + ), + ( # language=Python "Match statement" + """ +var1, var2 = 10, 20 +def f(a): + b = var1 + match var1: + case 1: return var1 + case 2: return var2 + b + case (a, b): return var1, a, b + case _: + result = b + print(result) + + x = result + y = b + return y + """, # language=none + { + "f.line3": SimpleImpure( + { + "NonLocalVariableRead.GlobalVariable.var1", + "NonLocalVariableRead.GlobalVariable.var2", + "FileWrite.StringLiteral.stdout", + }, + ), + }, + ), + ], + ids=[ + "Print with str", + "Print with parameter", + "VariableWrite to shadowed GlobalVariable", + "VariableWrite to GlobalVariable", + "VariableRead from GlobalVariable", + "Impure Class initialization", + "Impure Class initialization via super", + "Impure Class initialization via super multiple classes", + "Impure Class initialization via super with Builtin", + "Class methode call", + "Class methode call of superclass", + "Class methode call of superclass (overwritten method)", + "Instance methode call", + "VariableWrite to ClassVariable", + "VariableRead from ClassVariable", + "VariableWrite to InstanceVariable", + "VariableRead from InstanceVariable", + "VariableRead and VariableWrite in chained class attribute and instance attribute", + "Function call of functions with same name and different purity", + "Function call of functions with same name (different signatures)", + "Call of Impure Function", + "Call of Impure Chain of Functions", + "Call of Impure Chain of Functions with cycle - one entry point", + "Call of Impure Chain of Functions with cycle - other calls in cycle", + "Call of Impure Chain of Functions with cycle - cycle in cycle", + "Call of Impure Chain of Functions with cycle - direct entry", + "Call of Impure BuiltIn Function", + "Call of Impure Builtin type class methode", + "Lambda function", + "Lambda function with Impure Call", + "Assigned Lambda function", + "Lambda as key", + "Multiple Calls of same Impure function (Caching)", + "Different Reasons for Impurity", + "Impure Write to Local and Global", + "Call of Function with function as return", + "Call within a call", + "Async Function", + "Try Except", + "Match statement", + ], +) +@pytest.mark.xfail(reason="Some cases disabled for merging") +def test_infer_purity_impure(code: str, expected: dict[str, SimpleImpure]) -> None: + purity_results = next(iter(infer_purity(code).values())) + + transformed_purity_results = { + to_string_function_id(function_id): to_simple_result(purity_result) + for function_id, purity_result in purity_results.items() + if not purity_result.is_class + } + + assert transformed_purity_results == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Unknown Call", + """ +def fun1(): + call() + """, # language=none + { + "fun1.line2": SimpleImpure({"UnknownCall.UnknownFunctionCall.call"}), + }, + ), + ( # language=Python "Three Unknown Call", + """ +def fun1(): + call1() + call2() + call3() + """, # language=none + { + "fun1.line2": SimpleImpure( + { + "UnknownCall.UnknownFunctionCall.call1", + "UnknownCall.UnknownFunctionCall.call2", + "UnknownCall.UnknownFunctionCall.call3", + }, + ), + }, + ), + ( # language=Python "Unknown Call of Parameter", + """ +def fun1(a): + a() + """, # language=none + { + "fun1.line2": SimpleImpure({"CallOfParameter.ParameterAccess.a"}), + }, + ), + ( # language=Python "Unknown Call of Parameter with many Parameters", + """ +def fun1(function, a, b , c, **kwargs): + res = function(a, b, c, **kwargs) + """, # language=none + { + "fun1.line2": SimpleImpure({"CallOfParameter.ParameterAccess.function"}), + }, + ), + ( # language=Python "Unknown Callable", + """ +from typing import Callable + +def fun1(): + fun = import_fun("functions.py", "fun1") + fun() + +def import_fun(file: str, f_name: str) -> Callable: + print("test") + return lambda x: x + """, # language=none + { + "fun1.line4": SimpleImpure({"FileWrite.StringLiteral.stdout", "UnknownCall.UnknownFunctionCall.fun"}), + "import_fun.line8": SimpleImpure({"FileWrite.StringLiteral.stdout"}), + }, + ), + ( # language=Python "Unknown Call of Function with function as return", + """ +def fun1(a): + print("Fun1") # Impure: FileWrite + if a < 2: + return fun1 + else: + return fun2 + +def fun2(a): + print("Fun2") # Impure: FileWrite + +def fun3(): + x = fun1(1)(2)(3) # Here the call is unknown - since fun1 returns a function + """, # language=none + { + "fun1.line2": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + "fun2.line9": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + }, + ), + "fun3.line12": SimpleImpure( + { + "FileWrite.StringLiteral.stdout", + "UnknownCall.UnknownFunctionCall.UNKNOWN", # This is the worst case where the call node is unknown. + }, + ), + }, + ), + ], + ids=[ + "Unknown Call", + "Three Unknown Call", + "Unknown Call of Parameter", + "Unknown Call of Parameter with many Parameters", + "Unknown Callable", + "Unknown Call of Function with function as return", + ], +) +def test_infer_purity_unknown(code: str, expected: dict[str, SimpleImpure]) -> None: + purity_results = next(iter(infer_purity(code).values())) + + transformed_purity_results = { + to_string_function_id(function_id): to_simple_result(purity_result) + for function_id, purity_result in purity_results.items() + if not purity_result.is_class + } + + assert transformed_purity_results == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Import module - constant" + """ +import math + +def fun1(): + a = math.pi + """, # language=none + {"fun1.line4": SimpleImpure({"NonLocalVariableRead.Import.math.pi"})}, + ), + ( # language=Python "Import module with alias - constant" + """ +import math as m + +def fun1(): + a = m.pi + """, # language=none + {"fun1.line4": SimpleImpure({"NonLocalVariableRead.Import.math.pi"})}, + ), + ( # language=Python "Import module - function" + """ +import math + +def fun1(a): + math.sqrt(a) + """, # language=none + {"fun1.line4": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "Import module with alias - function" + """ +import math as m + +def fun1(a): + m.sqrt(a) + """, # language=none + {"fun1.line4": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "Import module with alias - function and constant" + """ +import math as m + +def fun1(a): + a = m.pi + m.sqrt(a) + """, # language=none + { + "fun1.line4": SimpleImpure( + {"NonLocalVariableRead.Import.math.pi", "NativeCall.UnknownFunctionCall.math.sqrt"}, + ), + }, + ), + ( # language=Python "FromImport - constant" + """ +from math import pi + +def fun1(): + a = pi + """, # language=none + {"fun1.line4": SimpleImpure({"NonLocalVariableRead.Import.math.pi"})}, + ), + ( # language=Python "FromImport with alias - constant" + """ +from math import pi as p + +def fun1(): + a = p + """, # language=none + {"fun1.line4": SimpleImpure({"NonLocalVariableRead.Import.math.pi"})}, + ), + ( # language=Python "FromImport - function" + """ +from math import sqrt + +def fun1(a): + sqrt(a) + """, # language=none + {"fun1.line4": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "FromImport with alias - function" + """ +from math import sqrt as s + +def fun1(a): + s(a) + """, # language=none + {"fun1.line4": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "FromImport with alias - function and constant" + """ +from math import sqrt as s, pi as p + +def fun1(a): + a = p + s(a) + """, # language=none + { + "fun1.line4": SimpleImpure( + {"NonLocalVariableRead.Import.math.pi", "NativeCall.UnknownFunctionCall.math.sqrt"}, + ), + }, + ), + ( # language=Python "FromImport MemberAccess with alias - class" + """ +from collections.abc import Callable + +def fun1(a): + a = Callable() + """, # language=none + {"fun1.line4": Pure()}, + ), + ( # language=Python "Local Import - function" + """ +def fun1(a): + import math + a = math.sqrt(a) + """, # language=none + {"fun1.line2": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "Local FromImport - constant" + """ +def fun1(a): + from math import pi + a = pi + """, # language=none + {"fun1.line2": SimpleImpure({"NonLocalVariableRead.Import.math.pi"})}, + ), + ( # language=Python "Local FromImport - function" + """ +def fun1(a): + from math import sqrt + sqrt(a) + """, # language=none + {"fun1.line2": SimpleImpure({"NativeCall.UnknownFunctionCall.math.sqrt"})}, + ), + ( # language=Python "Write to Import" + """ +import math as m + +def fun1(): + m.pi = 1 + """, # language=none + {"fun1.line4": SimpleImpure({"NonLocalVariableWrite.Import.math.pi"})}, + ), + ], + ids=[ + "Import module - constant", + "Import module with alias - constant", + "Import module - function", + "Import module with alias - function", + "Import module with alias - function and constant", + "FromImport - constant", + "FromImport with alias - constant", + "FromImport - function", + "FromImport with alias - function", + "FromImport with alias - function and constant", + "FromImport MemberAccess with alias - class", + "Local Import - function", + "Local FromImport - constant", + "Local FromImport - function", + "Write to Import", + ], +) +@pytest.mark.xfail( + reason="This is required because the import handling does not work when running the tests for the PR.", +) +def test_infer_purity_import(code: str, expected: dict[str, SimpleImpure]) -> None: + purity_results = next(iter(infer_purity(code).values())) + + transformed_purity_results = { + to_string_function_id(function_id): to_simple_result(purity_result) + for function_id, purity_result in purity_results.items() + if not purity_result.is_class + } + + assert transformed_purity_results == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Open with str default" + """ +def fun(): + open("text.txt") # Impure: FileRead + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "Open with str read" + """ +def fun(): + open("text.txt", "r") # Impure: FileRead + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "Open with str write" + """ +def fun(): + open("text.txt", "wb") # Impure: FileWrite + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.StringLiteral.text.txt"})}, + ), + ( # language=Python "Open with str read and write" + """ +def fun(): + open("text.txt", "a+") # Impure: FileRead and FileWrite + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt", "FileWrite.StringLiteral.text.txt"})}, + ), + ( # language=Python "Open with parameter default" + """ +def fun(pos_arg): + open(pos_arg) # Impure: FileRead + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.ParameterAccess.pos_arg"})}, + ), + ( # language=Python "Open with parameter write" + """ +def fun(pos_arg): + open(pos_arg, "a") # Impure: FileWrite + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.ParameterAccess.pos_arg"})}, + ), + ( # language=Python "Read" + """ +def fun(): + f = open("text.txt") # Impure: FileRead + f.read() # TODO: [Later] For now open is enough + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "Readline/ Readlines" + """ +def fun(): + f = open("text.txt") # Impure: FileRead + f.readline() # TODO: [Later] For now open is enough + f.readlines() # TODO: [Later] For now open is enough + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "Write" + """ +def fun(): + f = open("text.txt", "w") # Impure: FileWrite + f.write("test") # TODO: [Later] For now open is enough + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.StringLiteral.text.txt"})}, + ), + ( # language=Python "Writelines" + """ +def fun(): + f = open("text.txt", "w") # Impure: FileWrite + f.writelines(["test1", "test2"]) # TODO: [Later] For now open is enough + """, # language= None + {"fun.line2": SimpleImpure({"FileWrite.StringLiteral.text.txt"})}, + ), + ( # language=Python "With open str default" + """ +def fun(): + with open("text.txt") as f: # Impure: FileRead + f.read() + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "With open parameter default" + """ +def fun(pos_arg): + with open(pos_arg) as f: # Impure: FileRead + f.read() + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.ParameterAccess.pos_arg"})}, + ), + ( # language=Python "With open parameter read and write" + """ +def fun(pos_arg): + with open(pos_arg, "wb+") as f: # Impure: FileRead and FileWrite + f.read() + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.ParameterAccess.pos_arg", "FileWrite.ParameterAccess.pos_arg"})}, + ), + ( # language=Python "With open parameter and variable mode" + """ +def fun(pos_arg, mode): + with open(pos_arg, mode) as f: # Impure: FileRead and FileWrite + f.read() + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.ParameterAccess.pos_arg", "FileWrite.ParameterAccess.pos_arg"})}, + ), # TODO: do we want to expect the worst case here? + ( # language=Python "With open close" + """ +def fun(): + with open("text.txt") as f: # Impure: FileRead + f.read() + f.close() # TODO: [Later] For now open is enough + """, # language= None + {"fun.line2": SimpleImpure({"FileRead.StringLiteral.text.txt"})}, + ), + ( # language=Python "Open MemberAccess with str default" + """ +import builtins + +def fun(): + builtins.open("text.txt") # Impure: FileRead + """, # language= None + {"fun.line4": SimpleImpure({"FileRead.StringLiteral.text.txt", "NativeCall.UnknownFunctionCall._io.open"})}, + ), + ( # language=Python "Open MemberAccess with var default" + """ +import builtins + +def fun(file): + builtins.open(file) # Impure: FileRead + """, # language= None + {"fun.line4": SimpleImpure({"FileRead.ParameterAccess.file", "NativeCall.UnknownFunctionCall._io.open"})}, + ), + # ( # language=Python "With open MemberAccess str default" + # """ + # from pathlib import Path + # + # def fun(): + # x: Path = Path("text.txt") + # with x.open() as f: # Impure: FileRead + # f.read() + # """, # language= None + # {"fun.line4": SimpleImpure({"FileRead.StringLiteral.UNKNOWN", "FileWrite.StringLiteral.UNKNOWN"})}, + # ), + ], + ids=[ + "Open with str default", + "Open with str read", + "Open with str write", + "Open with str read and write", + "Open with parameter default", + "Open with parameter write", + "Read", + "Readline/ Readlines", + "Write", + "Writelines", + "With open str default", + "With open parameter default", + "With open parameter read and write", + "With open parameter and variable mode", + "With open close", + "Open MemberAccess with str default", + "Open MemberAccess with var default", + # "With open MemberAccess str default", # TODO: Disabled because of the import of Path and the resulting UnknownCall + ], +) +def test_infer_purity_open(code: str, expected: dict[str, SimpleImpure]) -> None: + purity_results = next(iter(infer_purity(code).values())) + + transformed_purity_results = { + to_string_function_id(function_id): to_simple_result(purity_result) + for function_id, purity_result in purity_results.items() + if not purity_result.is_class + } + + assert transformed_purity_results == expected diff --git a/tests/safeds_stubgen/api_analyzer/purity_analysis/test_resolve_references.py b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_resolve_references.py new file mode 100644 index 00000000..09cde22d --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/purity_analysis/test_resolve_references.py @@ -0,0 +1,3215 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +import astroid +import pytest +from safeds_stubgen.api_analyzer.purity_analysis import ( + resolve_references, +) +from safeds_stubgen.api_analyzer.purity_analysis.model import ( + ClassVariable, + InstanceVariable, + MemberAccess, + MemberAccessTarget, + MemberAccessValue, + NodeID, + Reasons, + ReferenceNode, +) + + +@dataclass +class ReferenceTestNode: + """Class for reference test nodes. + + A simplified class of the ReferenceNode class for testing purposes. + + Attributes + ---------- + name : str + The name of the node. + scope : str + The scope of the node as string. + referenced_symbols : list[str] + The list of referenced symbols as strings. + """ + + name: str + scope: str + referenced_symbols: list[str] + + def __hash__(self) -> int: + return hash(str(self)) + + def __str__(self) -> str: + return f"{self.name}.{self.scope}" + + +@dataclass +class SimpleReasons: + """Class for simple reasons. + + A simplified class of the Reasons class for testing purposes. + + Attributes + ---------- + function_name : str + The name of the function. + writes : set[str] + The set of the functions writes. + reads : set[str] + The set of the function reads. + """ + + function_name: str + writes: set[str] = field(default_factory=set) + reads: set[str] = field(default_factory=set) + + def __hash__(self) -> int: + return hash(self.function_name) + + +def get_base_expression(node: MemberAccess) -> astroid.NodeNG: + """Get the base expression of a MemberAccess node. + + Get the base expression of a MemberAccess node by recursively calling this function on the receiver of the MemberAccess node. + + Parameters + ---------- + node : MemberAccess + The MemberAccess node whose base expression is to be found. + + Returns + ------- + astroid.NodeNG + The base expression of the given MemberAccess node. + """ + if isinstance(node.receiver, MemberAccess): + return get_base_expression(node.receiver) + else: + assert node.receiver is not None + return node.receiver + + +def transform_reference_nodes(nodes: list[ReferenceNode]) -> list[ReferenceTestNode]: + """Transform a list of ReferenceNodes to a list of ReferenceTestNodes. + + Parameters + ---------- + nodes : list[ReferenceNode] + The list of ReferenceNodes to transform. + + Returns + ------- + list[ReferenceTestNode] + The transformed list of ReferenceTestNodes. + """ + transformed_nodes: list[ReferenceTestNode] = [] + + for node in nodes: + transformed_nodes.append(transform_reference_node(node)) + + return transformed_nodes + + +def transform_reference_node(ref_node: ReferenceNode) -> ReferenceTestNode: + """Transform a ReferenceNode to a ReferenceTestNode. + + Transforms a ReferenceNode to a ReferenceTestNode, so that they are no longer complex objects and easier to compare. + + Parameters + ---------- + ref_node : ReferenceNode + The ReferenceNode to transform. + + Returns + ------- + ReferenceTestNode + The transformed ReferenceTestNode. + """ + if isinstance(ref_node.node.node, MemberAccess | MemberAccessValue | MemberAccessTarget): + expression = get_base_expression(ref_node.node.node) + if ( + ref_node.scope.symbol.name == "__init__" + and isinstance(ref_node.scope.symbol, ClassVariable | InstanceVariable) + and ref_node.scope.symbol.klass is not None + ): + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{expression.lineno}", + scope=( + f"{ref_node.scope.symbol.node.__class__.__name__}." + f"{ref_node.scope.symbol.klass.name}." + f"{ref_node.scope.symbol.node.name}" + ), + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{expression.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.node.name}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if isinstance(ref_node.scope.symbol.node, astroid.Lambda) and not isinstance( + ref_node.scope.symbol.node, + astroid.FunctionDef, + ): + if isinstance(ref_node.node.node, astroid.Call): + return ReferenceTestNode( + name=f"{ref_node.node.node.func.name}.line{ref_node.node.node.func.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if isinstance(ref_node.node.node, astroid.Call): + if ( + isinstance(ref_node.scope.symbol.node, astroid.FunctionDef) + and ref_node.scope.symbol.name == "__init__" + and isinstance(ref_node.scope.symbol, ClassVariable | InstanceVariable) + and ref_node.scope.symbol.klass is not None + ): + return ReferenceTestNode( + name=f"{ref_node.node.node.func.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.klass.name}.{ref_node.scope.symbol.node.name}", + # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if isinstance(ref_node.scope.symbol.node, astroid.ListComp): + return ReferenceTestNode( + name=f"{ref_node.node.node.func.name}.line{ref_node.node.node.func.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + return ReferenceTestNode( + name=( + f"{ref_node.node.node.func.attrname}.line{ref_node.node.node.func.lineno}" + if isinstance(ref_node.node.node.func, astroid.Attribute) + else f"{ref_node.node.node.func.name}.line{ref_node.node.node.func.lineno}" + ), + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.node.name}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if isinstance(ref_node.scope.symbol.node, astroid.ListComp): + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if ( + isinstance(ref_node.node.node, astroid.Name) + and ref_node.scope.symbol.name == "__init__" + and isinstance(ref_node.scope.symbol, ClassVariable | InstanceVariable) + and ref_node.scope.symbol.klass is not None + ): + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.klass.name}.{ref_node.scope.symbol.node.name}", + # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + if isinstance(ref_node.node.node, astroid.Name): + return ReferenceTestNode( + name=f"{ref_node.node.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.node.name}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + return ReferenceTestNode( + name=f"{ref_node.node.node.name}.line{ref_node.node.node.lineno}", + scope=f"{ref_node.scope.symbol.node.__class__.__name__}.{ref_node.scope.symbol.node.name}", + referenced_symbols=sorted([str(ref) for ref in ref_node.referenced_symbols]), + ) + + +def transform_reasons(reasons: dict[NodeID, Reasons]) -> dict[str, SimpleReasons]: + """Transform the function references. + + The function references are transformed to a dictionary with the name of the function as key + and the transformed Reasons instance as value. + + Parameters + ---------- + reasons : dict[str, Reasons] + The function references to transform. + + Returns + ------- + dict[str, SimpleReasons] + The transformed function references. + """ + transformed_function_references = {} + for function_id, function_references in reasons.items(): + transformed_function_references.update( + { + function_id.__str__(): SimpleReasons( + function_references.function_scope.symbol.name, # type: ignore[union-attr] # function_scope is not None + { + ( + f"{target_reference.symbol.__class__.__name__}.{target_reference.symbol.klass.name}.{target_reference.symbol.node.name}.line{target_reference.symbol.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + if isinstance(target_reference.symbol, ClassVariable) + and target_reference.symbol.klass is not None + else ( + f"{target_reference.symbol.__class__.__name__}.{target_reference.symbol.klass.name}.{target_reference.symbol.node.member}.line{target_reference.symbol.node.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + if isinstance(target_reference.symbol, InstanceVariable) + else f"{target_reference.symbol.__class__.__name__}.{target_reference.symbol.node.name}.line{target_reference.symbol.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + ) + ) + for target_reference in function_references.writes_to.values() + }, + { + ( + f"{value_reference.symbol.__class__.__name__}.{value_reference.symbol.klass.name}.{value_reference.symbol.node.name}.line{value_reference.symbol.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + if isinstance(value_reference.symbol, ClassVariable) and value_reference.symbol is not None + else ( + f"{value_reference.symbol.__class__.__name__}.{value_reference.symbol.klass.name}.{value_reference.symbol.node.member}.line{value_reference.symbol.node.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + if isinstance(value_reference.symbol, InstanceVariable) + else f"{value_reference.symbol.__class__.__name__}.{value_reference.symbol.node.name}.line{value_reference.symbol.node.fromlineno}" # type: ignore[union-attr] # "None" has no attribute "name" but since we check for the type before, this is fine + ) + ) + for value_reference in function_references.reads_from.values() + }, + ), + }, + ) + + return transformed_function_references + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Local variable in function scope" + """ +def local_var(): + var1 = 1 + return var1 + """, # language= None + [ReferenceTestNode("var1.line4", "FunctionDef.local_var", ["LocalVariable.var1.line3"])], + ), + ( # language=Python "Global variable in module scope" + """ +glob1 = 10 +glob1 + """, # language= None + [], + ), + ( # language=Python "Global variable in class scope" + """ +glob1 = 10 +class A: + global glob1 + glob1 + """, # language= None + [], + ), + ( # language=Python "Global variable in function scope" + """ +glob1 = 10 +def local_global(): + global glob1 + + return glob1 + """, # language= None + [ReferenceTestNode("glob1.line6", "FunctionDef.local_global", ["GlobalVariable.glob1.line2"])], + ), + ( # language=Python "Global variable in function scope but after definition" + """ +def local_global(): + global glob1 + + return glob1 + +glob1 = 10 + """, # language= None + [ReferenceTestNode("glob1.line5", "FunctionDef.local_global", ["GlobalVariable.glob1.line7"])], + ), + ( # language=Python "Global variable in class scope and function scope" + """ +glob1 = 10 +class A: + global glob1 + glob1 + +def local_global(): + global glob1 + + return glob1 + """, # language= None + [ + # ReferenceTestNode("glob1.line5", "ClassDef.A", ["GlobalVariable.glob1.line2"]), + ReferenceTestNode("glob1.line10", "FunctionDef.local_global", ["GlobalVariable.glob1.line2"]), + ], + ), + ( # language=Python "Access of global variable without global keyword" + """ +glob1 = 10 +def local_global_access(): + return glob1 + """, # language= None + [ReferenceTestNode("glob1.line4", "FunctionDef.local_global_access", ["GlobalVariable.glob1.line2"])], + ), + ( # language=Python "Local variable in function scope shadowing global variable without global keyword" + """ +glob1 = 10 +def local_global_shadow(): + glob1 = 20 + + return glob1 + """, # language= None + [ + ReferenceTestNode( + "glob1.line6", + "FunctionDef.local_global_shadow", + ["LocalVariable.glob1.line4"], + ), + ], + ), + ( # language=Python "Two globals in class scope" + """ +glob1 = 10 +glob2 = 20 +class A: + global glob1, glob2 + glob1, glob2 + """, # language= None + [ + # ReferenceTestNode("glob1.line6", "ClassDef.A", ["GlobalVariable.glob1.line2"]), + # ReferenceTestNode("glob2.line6", "ClassDef.A", ["GlobalVariable.glob2.line3"]), + ], + ), + ( # language=Python "New global variable in class scope" + """ +class A: + global glob1 + glob1 = 10 + glob1 + """, # language= None + # [ReferenceTestNode("glob1.line5", "ClassDef.A", ["ClassVariable.A.glob1.line4"])], + [], + # glob1 is not detected as a global variable since it is defined in the class scope - this is intended + ), + ( # language=Python "New global variable in function scope" + """ +def local_global(): + global glob1 + + return glob1 + """, # language= None + [], + # glob1 is not detected as a global variable since it is defined in the function scope - this is intended + ), + ( # language=Python "New global variable in class scope with outer scope usage" + """ +class A: + global glob1 + value = glob1 + +def f(): + a = A().value + glob1 = 10 + b = A().value + a, b + """, # language= None + [ + ReferenceTestNode("A.line7", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.line9", "FunctionDef.f", ["GlobalVariable.A.line2"]), + # ReferenceTestNode("glob1.line4", "ClassDef.A", ["GlobalVariable.glob1.line7"]), + ReferenceTestNode("A.value.line7", "FunctionDef.f", ["ClassVariable.A.value.line4"]), + ReferenceTestNode("A.value.line9", "FunctionDef.f", ["ClassVariable.A.value.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.f", ["LocalVariable.a.line7"]), + ReferenceTestNode("b.line10", "FunctionDef.f", ["LocalVariable.b.line9"]), + ], + ), + ( # language=Python "New global variable in function scope with outer scope usage" + """ +def local_global(): + global glob1 + return glob1 + +def f(): + lg = local_global() + glob1 = 10 + """, # language= None + [ + ReferenceTestNode("local_global.line7", "FunctionDef.f", ["GlobalVariable.local_global.line2"]), + # ReferenceTestNode("glob1.line4", "FunctionDef.local_global", ["GlobalVariable.glob1.line7"]), + ], + ), # Problem: we cannot check weather a function is called before the global variable is declared since + # this would need a context-sensitive approach + # For now we just check if the global variable is declared in the module scope at the cost of loosing precision. + ], + ids=[ + "Local variable in function scope", + "Global variable in module scope", + "Global variable in class scope", + "Global variable in function scope", + "Global variable in function scope but after definition", + "Global variable in class scope and function scope", + "Access of global variable without global keyword", + "Local variable in function scope shadowing global variable without global keyword", + "Two globals in class scope", + "New global variable in class scope", + "New global variable in function scope", + "New global variable in class scope with outer scope usage", + "New global variable in function scope with outer scope usage", + ], +) +def test_resolve_references_local_global(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert transformed_references == expected + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Parameter in function scope" + """ +def local_parameter(pos_arg): + return 2 * pos_arg + """, # language= None + [ReferenceTestNode("pos_arg.line3", "FunctionDef.local_parameter", ["Parameter.pos_arg.line2"])], + ), + ( # language=Python "Parameter in function scope with keyword only" + """ +def local_parameter(*, key_arg_only): + return 2 * key_arg_only + """, # language= None + [ReferenceTestNode("key_arg_only.line3", "FunctionDef.local_parameter", ["Parameter.key_arg_only.line2"])], + ), + ( # language=Python "Parameter in function scope with positional only" + """ +def local_parameter(pos_arg_only, /): + return 2 * pos_arg_only + """, # language= None + [ReferenceTestNode("pos_arg_only.line3", "FunctionDef.local_parameter", ["Parameter.pos_arg_only.line2"])], + ), + ( # language=Python "Parameter in function scope with default value" + """ +def local_parameter(def_arg=10): + return def_arg + """, # language= None + [ReferenceTestNode("def_arg.line3", "FunctionDef.local_parameter", ["Parameter.def_arg.line2"])], + ), + ( # language=Python "Parameter in function scope with type annotation" + """ +def local_parameter(def_arg: int): + return def_arg + """, # language= None + [ReferenceTestNode("def_arg.line3", "FunctionDef.local_parameter", ["Parameter.def_arg.line2"])], + ), + ( # language=Python "Parameter in function scope with *args" + """ +def local_parameter(*args): + return args + """, # language= None + [ReferenceTestNode("args.line3", "FunctionDef.local_parameter", ["Parameter.args.line2"])], + ), + ( # language=Python "Parameter in function scope with **kwargs" + """ +def local_parameter(**kwargs): + return kwargs + """, # language= None + [ReferenceTestNode("kwargs.line3", "FunctionDef.local_parameter", ["Parameter.kwargs.line2"])], + ), + ( # language=Python "Parameter in function scope with *args and **kwargs" + """ +def local_parameter(*args, **kwargs): + return args, kwargs + """, # language= None + [ + ReferenceTestNode("args.line3", "FunctionDef.local_parameter", ["Parameter.args.line2"]), + ReferenceTestNode("kwargs.line3", "FunctionDef.local_parameter", ["Parameter.kwargs.line2"]), + ], + ), + ( # language=Python "Two parameters in function scope" + """ +def local_double_parameter(a, b): + return a, b + """, # language= None + [ + ReferenceTestNode("a.line3", "FunctionDef.local_double_parameter", ["Parameter.a.line2"]), + ReferenceTestNode("b.line3", "FunctionDef.local_double_parameter", ["Parameter.b.line2"]), + ], + ), + ( # language=Python "Self" + """ +class A: + def __init__(self): + self + + def f(self): + x = self + """, # language= None + [ + ReferenceTestNode("self.line4", "FunctionDef.A.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("self.line7", "FunctionDef.f", ["Parameter.self.line6"]), + ], + ), + ], + ids=[ + "Parameter in function scope", + "Parameter in function scope with keyword only", + "Parameter in function scope with positional only", + "Parameter in function scope with default value", + "Parameter in function scope with type annotation", + "Parameter in function scope with *args", + "Parameter in function scope with **kwargs", + "Parameter in function scope with *args and **kwargs", + "Two parameters in function scope", + "Self", + ], +) +def test_resolve_references_parameters(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Class attribute value" + """ +class A: + class_attr1 = 20 + +def f(): + A.class_attr1 + A + """, # language=none + [ + ReferenceTestNode("A.class_attr1.line6", "FunctionDef.f", ["ClassVariable.A.class_attr1.line3"]), + ReferenceTestNode("A.line6", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.line7", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Class attribute target" + """ +class A: + class_attr1 = 20 + +def f(): + A.class_attr1 = 30 + A.class_attr1 + """, # language=none + [ + ReferenceTestNode( + "A.class_attr1.line7", + "FunctionDef.f", + ["ClassVariable.A.class_attr1.line3", "ClassVariable.A.class_attr1.line6"], + ), + ReferenceTestNode("A.line7", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.class_attr1.line6", "FunctionDef.f", ["ClassVariable.A.class_attr1.line3"]), + ReferenceTestNode("A.line6", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Class attribute multiple usage" + """ +class A: + class_attr1 = 20 + +def f(): + a = A().class_attr1 + b = A().class_attr1 + c = A().class_attr1 + """, # language=none + [ + ReferenceTestNode("A.class_attr1.line6", "FunctionDef.f", ["ClassVariable.A.class_attr1.line3"]), + ReferenceTestNode("A.class_attr1.line7", "FunctionDef.f", ["ClassVariable.A.class_attr1.line3"]), + ReferenceTestNode("A.class_attr1.line8", "FunctionDef.f", ["ClassVariable.A.class_attr1.line3"]), + ReferenceTestNode("A.line6", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.line7", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.line8", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Chained class attribute" + """ +class A: + class_attr1 = 20 + +class B: + upper_class: A = A + +def f(): + b = B() + x = b.upper_class.class_attr1 + """, # language=none + [ + ReferenceTestNode( + "UNKNOWN.class_attr1.line10", + "FunctionDef.f", + # we do not analyze the receiver of the member access, hence the name does not matter. + ["ClassVariable.A.class_attr1.line3"], + ), + ReferenceTestNode("b.upper_class.line10", "FunctionDef.f", ["ClassVariable.B.upper_class.line6"]), + ReferenceTestNode("b.line10", "FunctionDef.f", ["LocalVariable.b.line9"]), + ReferenceTestNode("B.line9", "FunctionDef.f", ["GlobalVariable.B.line5"]), + ], + ), + ( # language=Python "Instance attribute value" + """ +class B: + def __init__(self): + self.instance_attr1 : int = 10 + +def f(): + b = B() + var1 = b.instance_attr1 + """, # language=none + [ + ReferenceTestNode( + "b.instance_attr1.line8", + "FunctionDef.f", + ["InstanceVariable.B.instance_attr1.line4"], + ), + ReferenceTestNode("b.line8", "FunctionDef.f", ["LocalVariable.b.line7"]), + ReferenceTestNode("self.line4", "FunctionDef.B.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("B.line7", "FunctionDef.f", ["GlobalVariable.B.line2"]), + ], + ), + ( # language=Python "Instance attribute target" + """ +class B: + def __init__(self): + self.instance_attr1 = 10 + +def f(): + b = B() + b.instance_attr1 = 1 + b.instance_attr1 + """, # language=none + [ + ReferenceTestNode( + "b.instance_attr1.line9", + "FunctionDef.f", + ["InstanceVariable.B.instance_attr1.line4", "InstanceVariable.B.instance_attr1.line8"], + ), + ReferenceTestNode("b.line9", "FunctionDef.f", ["LocalVariable.b.line7"]), + ReferenceTestNode("self.line4", "FunctionDef.B.__init__", ["Parameter.self.line3"]), + ReferenceTestNode( + "b.instance_attr1.line8", + "FunctionDef.f", + ["InstanceVariable.B.instance_attr1.line4"], + ), + ReferenceTestNode("b.line8", "FunctionDef.f", ["LocalVariable.b.line7"]), + ReferenceTestNode("B.line7", "FunctionDef.f", ["GlobalVariable.B.line2"]), + ], + ), + ( # language=Python "Instance attribute with parameter" + """ +class B: + def __init__(self, name: str): + self.name = name + +def f(): + b = B("test") + b.name + """, # language=none + [ + ReferenceTestNode("name.line4", "FunctionDef.B.__init__", ["Parameter.name.line3"]), + ReferenceTestNode("b.name.line8", "FunctionDef.f", ["InstanceVariable.B.name.line4"]), + ReferenceTestNode("b.line8", "FunctionDef.f", ["LocalVariable.b.line7"]), + ReferenceTestNode("self.line4", "FunctionDef.B.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("B.line7", "FunctionDef.f", ["GlobalVariable.B.line2"]), + ], + ), + ( # language=Python "Instance attribute with parameter and class attribute" + """ +class X: + class_attr = 10 + + def __init__(self, name: str): + self.name = name + +def f(): + x = X("test") + x.name + x.class_attr + """, # language=none + [ + ReferenceTestNode("name.line6", "FunctionDef.X.__init__", ["Parameter.name.line5"]), + ReferenceTestNode("x.name.line10", "FunctionDef.f", ["InstanceVariable.X.name.line6"]), + ReferenceTestNode("x.line10", "FunctionDef.f", ["LocalVariable.x.line9"]), + ReferenceTestNode("x.class_attr.line11", "FunctionDef.f", ["ClassVariable.X.class_attr.line3"]), + ReferenceTestNode("x.line11", "FunctionDef.f", ["LocalVariable.x.line9"]), + ReferenceTestNode("self.line6", "FunctionDef.X.__init__", ["Parameter.self.line5"]), + ReferenceTestNode("X.line9", "FunctionDef.f", ["GlobalVariable.X.line2"]), + ], + ), + ( # language=Python "Class attribute initialized with instance attribute" + """ +class B: + instance_attr1: int + + def __init__(self): + self.instance_attr1 = 10 + +def f(): + b = B() + var1 = b.instance_attr1 + """, # language=none + [ + ReferenceTestNode( + "b.instance_attr1.line10", + "FunctionDef.f", + ["ClassVariable.B.instance_attr1.line3", "InstanceVariable.B.instance_attr1.line6"], + ), + ReferenceTestNode("b.line10", "FunctionDef.f", ["LocalVariable.b.line9"]), + ReferenceTestNode( + "self.instance_attr1.line6", + "FunctionDef.B.__init__", + ["ClassVariable.B.instance_attr1.line3"], + ), + ReferenceTestNode("self.line6", "FunctionDef.B.__init__", ["Parameter.self.line5"]), + ReferenceTestNode("B.line9", "FunctionDef.f", ["GlobalVariable.B.line2"]), + ], + ), + ( # language=Python "Chained class attribute and instance attribute" + """ +class A: + def __init__(self): + self.name = 10 + +class B: + upper_class: A = A() + +def f(): + b = B() + x = b.upper_class.name + """, # language=none + [ + ReferenceTestNode("UNKNOWN.name.line11", "FunctionDef.f", ["InstanceVariable.A.name.line4"]), + # we do not analyze the receiver of the member access, hence the name does not matter. + ReferenceTestNode("b.upper_class.line11", "FunctionDef.f", ["ClassVariable.B.upper_class.line7"]), + ReferenceTestNode("b.line11", "FunctionDef.f", ["LocalVariable.b.line10"]), + ReferenceTestNode("self.line4", "FunctionDef.A.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("B.line10", "FunctionDef.f", ["GlobalVariable.B.line6"]), + ], + ), + ( # language=Python "Chained instance attributes value" + """ +class A: + def __init__(self): + self.b = B() + +class B: + def __init__(self): + self.c = C() + +class C: + def __init__(self): + self.name = "name" + +def f(): + a = A() + a.b.c.name + """, # language=none + [ + ReferenceTestNode("UNKNOWN.name.line16", "FunctionDef.f", ["InstanceVariable.C.name.line12"]), + # we do not analyze the receiver of the member access, hence the name does not matter. + ReferenceTestNode("UNKNOWN.c.line16", "FunctionDef.f", ["InstanceVariable.B.c.line8"]), + # we do not analyze the receiver of the member access, hence the name does not matter. + ReferenceTestNode("a.b.line16", "FunctionDef.f", ["InstanceVariable.A.b.line4"]), + ReferenceTestNode("a.line16", "FunctionDef.f", ["LocalVariable.a.line15"]), + ReferenceTestNode("self.line4", "FunctionDef.A.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("self.line8", "FunctionDef.B.__init__", ["Parameter.self.line7"]), + ReferenceTestNode("self.line12", "FunctionDef.C.__init__", ["Parameter.self.line11"]), + ReferenceTestNode("B.line4", "FunctionDef.A.__init__", ["GlobalVariable.B.line6"]), + ReferenceTestNode("C.line8", "FunctionDef.B.__init__", ["GlobalVariable.C.line10"]), + ReferenceTestNode("A.line15", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Chained instance attributes target" + """ +class A: + def __init__(self): + self.b = B() + +class B: + def __init__(self): + self.c = C() + +class C: + def __init__(self): + self.name = "name" + +def f(): + a = A() + a.b.c.name = "test" + """, # language=none + [ + ReferenceTestNode("self.line4", "FunctionDef.A.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("self.line8", "FunctionDef.B.__init__", ["Parameter.self.line7"]), + ReferenceTestNode("self.line12", "FunctionDef.C.__init__", ["Parameter.self.line11"]), + ReferenceTestNode("UNKNOWN.name.line16", "FunctionDef.f", ["InstanceVariable.C.name.line12"]), + ReferenceTestNode("UNKNOWN.c.line16", "FunctionDef.f", ["InstanceVariable.B.c.line8"]), + # we do not analyze the receiver of the member access, hence the name does not matter. + ReferenceTestNode("a.line16", "FunctionDef.f", ["LocalVariable.a.line15"]), + ReferenceTestNode("a.b.line16", "FunctionDef.f", ["InstanceVariable.A.b.line4"]), + ReferenceTestNode("B.line4", "FunctionDef.A.__init__", ["GlobalVariable.B.line6"]), + ReferenceTestNode("C.line8", "FunctionDef.B.__init__", ["GlobalVariable.C.line10"]), + ReferenceTestNode("A.line15", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Two classes with the same signature" + """ +class A: + name: str = "" + + def __init__(self, name: str): + self.name = name + +class B: + name: str = "" + + def __init__(self, name: str): + self.name = name + +def f(): + a = A("value") + b = B("test") + a.name + b.name + """, # language=none + [ + ReferenceTestNode("name.line6", "FunctionDef.A.__init__", ["Parameter.name.line5"]), + ReferenceTestNode("name.line12", "FunctionDef.B.__init__", ["Parameter.name.line11"]), + ReferenceTestNode( + "a.name.line17", + "FunctionDef.f", + [ + "ClassVariable.A.name.line3", # class A + "ClassVariable.B.name.line9", # class B + "InstanceVariable.A.name.line6", # class A + "InstanceVariable.B.name.line12", # class B + ], + ), + ReferenceTestNode("a.line17", "FunctionDef.f", ["LocalVariable.a.line15"]), + ReferenceTestNode( + "b.name.line18", + "FunctionDef.f", + [ + "ClassVariable.A.name.line3", # class A + "ClassVariable.B.name.line9", # class B + "InstanceVariable.A.name.line6", # class A + "InstanceVariable.B.name.line12", # class B + ], + ), + ReferenceTestNode("b.line18", "FunctionDef.f", ["LocalVariable.b.line16"]), + ReferenceTestNode("self.name.line6", "FunctionDef.A.__init__", ["ClassVariable.A.name.line3"]), + ReferenceTestNode("self.line6", "FunctionDef.A.__init__", ["Parameter.self.line5"]), + ReferenceTestNode("self.name.line12", "FunctionDef.B.__init__", ["ClassVariable.B.name.line9"]), + ReferenceTestNode("self.line12", "FunctionDef.B.__init__", ["Parameter.self.line11"]), + ReferenceTestNode("A.line15", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode("B.line16", "FunctionDef.f", ["GlobalVariable.B.line8"]), + ], + ), + ( # language=Python "Getter function with self" + """ +class C: + state: int = 0 + + def get_state(self): + return self.state + """, # language= None + [ + ReferenceTestNode("self.state.line6", "FunctionDef.get_state", ["ClassVariable.C.state.line3"]), + ReferenceTestNode("self.line6", "FunctionDef.get_state", ["Parameter.self.line5"]), + ], + ), + ( # language=Python "Getter function with classname" + """ +class C: + state: int = 0 + + @staticmethod + def get_state(): + return C.state + """, # language= None + [ + ReferenceTestNode("C.state.line7", "FunctionDef.get_state", ["ClassVariable.C.state.line3"]), + ReferenceTestNode("C.line7", "FunctionDef.get_state", ["GlobalVariable.C.line2"]), + ], + ), + ( # language=Python "Setter function with self" + """ +class C: + state: int = 0 + + def set_state(self, state): + self.state = state + """, # language= None + [ + ReferenceTestNode("state.line6", "FunctionDef.set_state", ["Parameter.state.line5"]), + ReferenceTestNode("self.state.line6", "FunctionDef.set_state", ["ClassVariable.C.state.line3"]), + ReferenceTestNode("self.line6", "FunctionDef.set_state", ["Parameter.self.line5"]), + ], + ), + ( # language=Python "Setter function with self different name" + """ +class A: + stateX: str = "A" + +class C: + stateX: int = 0 + + def set_state(self, state): + self.stateX = state + """, # language= None + [ + ReferenceTestNode("state.line9", "FunctionDef.set_state", ["Parameter.state.line8"]), + ReferenceTestNode( + "self.stateX.line9", + "FunctionDef.set_state", + ["ClassVariable.C.stateX.line6"], + ), # here self indicates that we are in class C -> therefore only C.stateX is detected + ReferenceTestNode("self.line9", "FunctionDef.set_state", ["Parameter.self.line8"]), + ], + ), + ( # language=Python "Setter function with classname different name" + """ +class C: + stateX: int = 0 + + @staticmethod + def set_state(state): + C.stateX = state + """, # language= None + [ + ReferenceTestNode("state.line7", "FunctionDef.set_state", ["Parameter.state.line6"]), + ReferenceTestNode("C.stateX.line7", "FunctionDef.set_state", ["ClassVariable.C.stateX.line3"]), + ReferenceTestNode("C.line7", "FunctionDef.set_state", ["GlobalVariable.C.line2"]), + ], + ), + ( # language=Python "Setter function as @staticmethod" + """ +class A: + state: str = "A" + +class C: + state: int = 0 + + @staticmethod + def set_state(node, state): + node.state = state + """, # language= None + [ + ReferenceTestNode("state.line10", "FunctionDef.set_state", ["Parameter.state.line9"]), + ReferenceTestNode( + "node.state.line10", + "FunctionDef.set_state", + ["ClassVariable.C.state.line6"], + ), + ReferenceTestNode("node.line10", "FunctionDef.set_state", ["Parameter.node.line9"]), + ], + ), + ( # language=Python "Setter function as @classmethod" + """ +class A: + state: str = "A" + +class C: + state: int = 0 + + @classmethod + def set_state(cls, state): + cls.state = state + """, # language= None + [ + ReferenceTestNode("state.line10", "FunctionDef.set_state", ["Parameter.state.line9"]), + ReferenceTestNode( + "cls.state.line10", + "FunctionDef.set_state", + ["ClassVariable.C.state.line6"], + ), + ReferenceTestNode("cls.line10", "FunctionDef.set_state", ["Parameter.cls.line9"]), + ], + ), + ( # language=Python "Class call - init", + """ +class A: + pass + +def fun(): + a = A() + + """, # language=none + [ + ReferenceTestNode("A.line6", "FunctionDef.fun", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Member access - class", + """ +class A: + class_attr1 = 20 + +def fun(): + a = A().class_attr1 + + """, # language=none + [ + ReferenceTestNode("A.line6", "FunctionDef.fun", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.class_attr1.line6", "FunctionDef.fun", ["ClassVariable.A.class_attr1.line3"]), + ], + ), + ( # language=Python "Member access - class without init", + """ +class A: + class_attr1 = 20 + +def fun(): + a = A.class_attr1 + + """, # language=none + [ + ReferenceTestNode("A.line6", "FunctionDef.fun", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.class_attr1.line6", "FunctionDef.fun", ["ClassVariable.A.class_attr1.line3"]), + ], + ), + ( # language=Python "Member access - methode", + """ +class A: + class_attr1 = 20 + + def g(self): + pass + +def fun1(): + a = A() + a.g() + +def fun2(): + a = A().g() + """, # language=none + [ + ReferenceTestNode("A.line9", "FunctionDef.fun1", ["GlobalVariable.A.line2"]), + ReferenceTestNode("a.line10", "FunctionDef.fun1", ["LocalVariable.a.line9"]), + ReferenceTestNode("g.line10", "FunctionDef.fun1", ["ClassVariable.A.g.line5"]), + ReferenceTestNode("A.line13", "FunctionDef.fun2", ["GlobalVariable.A.line2"]), + ReferenceTestNode("g.line13", "FunctionDef.fun2", ["ClassVariable.A.g.line5"]), + ], + ), + ( # language=Python "Member access - init", + """ +class A: + def __init__(self): + pass + +def fun(): + a = A() + + """, # language=none + [ + ReferenceTestNode("A.line7", "FunctionDef.fun", ["GlobalVariable.A.line2"]), + ], + ), + ( # language=Python "Member access - instance function", + """ +class A: + def __init__(self): + self.a_inst = B() + +class B: + def __init__(self): + pass + + def b_fun(self): + pass + +def fun1(): + a = A() + a.a_inst.b_fun() + +def fun2(): + a = A().a_inst.b_fun() + """, # language=none + [ + ReferenceTestNode("self.line4", "FunctionDef.A.__init__", ["Parameter.self.line3"]), + ReferenceTestNode("B.line4", "FunctionDef.A.__init__", ["GlobalVariable.B.line6"]), + ReferenceTestNode("A.line14", "FunctionDef.fun1", ["GlobalVariable.A.line2"]), + ReferenceTestNode("a.line15", "FunctionDef.fun1", ["LocalVariable.a.line14"]), + ReferenceTestNode("a.a_inst.line15", "FunctionDef.fun1", ["InstanceVariable.A.a_inst.line4"]), + ReferenceTestNode("b_fun.line15", "FunctionDef.fun1", ["ClassVariable.B.b_fun.line10"]), + ReferenceTestNode("A.line18", "FunctionDef.fun2", ["GlobalVariable.A.line2"]), + ReferenceTestNode("A.a_inst.line18", "FunctionDef.fun2", ["InstanceVariable.A.a_inst.line4"]), + ReferenceTestNode("b_fun.line18", "FunctionDef.fun2", ["ClassVariable.B.b_fun.line10"]), + ], + ), + ( # language=Python "Member access - function call of functions with same name" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b): + return a + 2 * b + +def fun_a(): + x = A() + x.add(1, 2) + +def fun_b(): + x = B() + x.add(1, 2) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.fun_a", ["GlobalVariable.A.line2"]), + ReferenceTestNode("x.line14", "FunctionDef.fun_a", ["LocalVariable.x.line13"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.fun_a", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ReferenceTestNode("B.line17", "FunctionDef.fun_b", ["GlobalVariable.B.line7"]), + ReferenceTestNode("x.line18", "FunctionDef.fun_b", ["LocalVariable.x.line17"]), + ReferenceTestNode( + "add.line18", + "FunctionDef.fun_b", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ], + ), + ( # language=Python "Member access - function call of functions with same name and nested calls", + """ +def fun1(): + pass + +def fun2(): + print("Function 2") + +class A: + @staticmethod + def add(a, b): + fun1() + return a + b + +class B: + @staticmethod + def add(a, b): + fun2() + return a + 2 * b + """, # language=none + [ + ReferenceTestNode("print.line6", "FunctionDef.fun2", ["Builtin.print"]), + ReferenceTestNode("a.line12", "FunctionDef.add", ["Parameter.a.line10"]), + ReferenceTestNode("b.line12", "FunctionDef.add", ["Parameter.b.line10"]), + ReferenceTestNode("fun1.line11", "FunctionDef.add", ["GlobalVariable.fun1.line2"]), + ReferenceTestNode("a.line18", "FunctionDef.add", ["Parameter.a.line16"]), + ReferenceTestNode("b.line18", "FunctionDef.add", ["Parameter.b.line16"]), + ReferenceTestNode("fun2.line17", "FunctionDef.add", ["GlobalVariable.fun2.line5"]), + ], + ), + ( # language=Python "Member access - function call of functions with same name (no distinction possible)" + """ +class A: + @staticmethod + def fun(): + return "Function A" + +class B: + @staticmethod + def fun(): + return "Function B" + +def fun_out(a): + if a == 1: + x = A() + else: + x = B() + x.fun() + """, # language=none + [ + ReferenceTestNode("a.line13", "FunctionDef.fun_out", ["Parameter.a.line12"]), + ReferenceTestNode("A.line14", "FunctionDef.fun_out", ["GlobalVariable.A.line2"]), + ReferenceTestNode("B.line16", "FunctionDef.fun_out", ["GlobalVariable.B.line7"]), + ReferenceTestNode("x.line16", "FunctionDef.fun_out", ["LocalVariable.x.line14"]), + # this is an assumption we need to make since we cannot differentiate between branches before runtime + ReferenceTestNode( + "x.line17", + "FunctionDef.fun_out", + ["LocalVariable.x.line14", "LocalVariable.x.line16"], + ), + ReferenceTestNode( + "fun.line17", + "FunctionDef.fun_out", + ["ClassVariable.A.fun.line4", "ClassVariable.B.fun.line9"], + ), + # here we can't distinguish between the two functions + ], + ), + ( # language=Python "Member access - function call of functions with same name (different signatures)" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b, c): + return a + b + c + +def fun(): + A.add(1, 2) + B.add(1, 2, 3) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("c.line10", "FunctionDef.add", ["Parameter.c.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.fun", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line13", + "FunctionDef.fun", + ["ClassVariable.A.add.line4"], + ), + ReferenceTestNode("B.line14", "FunctionDef.fun", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.fun", + ["ClassVariable.B.add.line9"], + ), + ], + ), + ], + ids=[ + "Class attribute value", + "Class attribute target", + "Class attribute multiple usage", + "Chained class attribute", + "Instance attribute value", + "Instance attribute target", + "Instance attribute with parameter", + "Instance attribute with parameter and class attribute", + "Class attribute initialized with instance attribute", + "Chained class attribute and instance attribute", + "Chained instance attributes value", + "Chained instance attributes target", + "Two classes with the same signature", + "Getter function with self", + "Getter function with classname", + "Setter function with self", + "Setter function with self different name", + "Setter function with classname different name", + "Setter function as @staticmethod", + "Setter function as @classmethod", + "Class call - init", + "Member access - class", + "Member access - class without init", + "Member access - methode", + "Member access - init", + "Member access - instance function", + "Member access - function call of functions with the same name", + "Member access - function call of functions with the same name and nested calls", + "Member access - function call of functions with the same name (no distinction possible)", + "Member access - function call of functions with the same name (different signatures)", + ], +) +def test_resolve_references_member_access(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "If statement" + """ +def f(): + var1 = 10 + if var1 > 0: + var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "If in statement" + """ +def f(): + var1 = [1, 2, 3] + if 1 in var1: + var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "If else statement" + """ +def f(): + var1 = 10 + if var1 > 0: + var1 + else: + 2 * var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line7", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "If elif else statement" + """ +def f(): + var1 = 10 + if var1 > 0: + var1 + elif var1 < 0: + -var1 + else: + var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line6", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line7", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line9", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "Ternary operator" + """ +def f(): + var1 = 10 + result = "even" if var1 % 2 == 0 else "odd" + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "Match statement" + """ +var1, var2 = 10, 20 +def f(a): + b = var1 + match var1: + case 1: return var1 + case 2: return var2 + b + case (a, b): return var1, a, b + case _: + result = b + + x = result + y = b + return y + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ReferenceTestNode("var1.line6", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ReferenceTestNode("var2.line7", "FunctionDef.f", ["GlobalVariable.var2.line2"]), + ReferenceTestNode("b.line7", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("a.line8", "FunctionDef.f", ["Parameter.a.line3"]), + ReferenceTestNode("b.line8", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("var1.line8", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + # ReferenceTestNode("a.line8", "FunctionDef.f", ["Parameter.a.line3", "LocalVariable.a.line8"]), # This is irrelevant for the purity result since they are of local scope to the case block. + # ReferenceTestNode("b.line8", "FunctionDef.f", ["LocalVariable.b.line4", "LocalVariable.b.line8"]), # This is irrelevant for the purity result since they are of local scope to the case block. + ReferenceTestNode("b.line10", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("result.line12", "FunctionDef.f", ["LocalVariable.result.line10"]), + ReferenceTestNode("b.line13", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("y.line14", "FunctionDef.f", ["LocalVariable.y.line13"]), + ], + ), + ( # language=Python "Try Except" + """ +def try_except(num1, num2): + try: + result = num1 / num2 + except ZeroDivisionError as error: # This is of local scope to the except block. + print(error) # This reference is not relevant, and it would be a lot of work to implement its detection correctly. + + print(result) + """, # language=none + [ + ReferenceTestNode("num1.line4", "FunctionDef.try_except", ["Parameter.num1.line2"]), + ReferenceTestNode("num2.line4", "FunctionDef.try_except", ["Parameter.num2.line2"]), + ReferenceTestNode("print.line6", "FunctionDef.try_except", ["Builtin.print"]), + ReferenceTestNode("print.line8", "FunctionDef.try_except", ["Builtin.print"]), + ReferenceTestNode("result.line8", "FunctionDef.try_except", ["LocalVariable.result.line4"]), + ], + ), + ( # language=Python "Try Except Else Finally" + """ +def try_except_else_finally(num1, num2, num3): + try: + result = num1 / num2 + except ZeroDivisionError as error: # This is of local scope to the except block. + print(error) # This reference is not relevant, and it would be a lot of work to implement its detection correctly. + except Exception as error: + print(error) # This reference is not relevant, and it would be a lot of work to implement its detection correctly. + else: + result2 = num1 + finally: + final = num3 + + print(result, result2, final) + + """, # language=none + [ + ReferenceTestNode("num1.line4", "FunctionDef.try_except_else_finally", ["Parameter.num1.line2"]), + ReferenceTestNode("num2.line4", "FunctionDef.try_except_else_finally", ["Parameter.num2.line2"]), + ReferenceTestNode("print.line6", "FunctionDef.try_except_else_finally", ["Builtin.print"]), + ReferenceTestNode("print.line8", "FunctionDef.try_except_else_finally", ["Builtin.print"]), + ReferenceTestNode("num1.line10", "FunctionDef.try_except_else_finally", ["Parameter.num1.line2"]), + ReferenceTestNode("num3.line12", "FunctionDef.try_except_else_finally", ["Parameter.num3.line2"]), + ReferenceTestNode("print.line14", "FunctionDef.try_except_else_finally", ["Builtin.print"]), + ReferenceTestNode( + "result.line14", + "FunctionDef.try_except_else_finally", + ["LocalVariable.result.line4"], + ), + ReferenceTestNode( + "result2.line14", + "FunctionDef.try_except_else_finally", + ["LocalVariable.result2.line10"], + ), + ReferenceTestNode( + "final.line14", + "FunctionDef.try_except_else_finally", + ["LocalVariable.final.line12"], + ), + ], + ), + ], + ids=[ + "If statement", + "If in statement", + "If else statement global scope", + "If elif else statement global scope", + "Ternary operator", + "Match statement", + "Try Except", + "Try Except Else Finally", + ], +) +def test_resolve_references_conditional_statements(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "For loop with global runtime variable" + """ +var1 = 10 +def f(): + for i in range(var1): + i + """, # language=none + [ + ReferenceTestNode("range.line4", "FunctionDef.f", ["Builtin.range"]), + ReferenceTestNode("var1.line4", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ReferenceTestNode("i.line5", "FunctionDef.f", ["LocalVariable.i.line4"]), + ], + ), + ( # language=Python "For loop wih local runtime variable" + """ +def f(): + var1 = 10 + for i in range(var1): + i + """, # language=none + [ + ReferenceTestNode("range.line4", "FunctionDef.f", ["Builtin.range"]), + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("i.line5", "FunctionDef.f", ["LocalVariable.i.line4"]), + ], + ), + ( # language=Python "For loop in list comprehension" + """ +nums = ["one", "two", "three"] +def f(): + lengths = [len(num) for num in nums] + lengths + """, # language=none + [ + ReferenceTestNode("len.line4", "FunctionDef.f", ["Builtin.len"]), + # ReferenceTestNode("num.line4", "ListComp.", ["LocalVariable.num.line4"]), + ReferenceTestNode("nums.line4", "FunctionDef.f", ["GlobalVariable.nums.line2"]), + ReferenceTestNode("lengths.line5", "FunctionDef.f", ["LocalVariable.lengths.line4"]), + ], + ), + ( # language=Python "While loop" + """ +def f(): + var1 = 10 + while var1 > 0: + var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ( # language=Python "While else loop" + """ +def f(): + var1 = 10 + while var1 > 0: + var1 + else: + 2 * var1 + """, # language=none + [ + ReferenceTestNode("var1.line4", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line5", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var1.line7", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ], + ), + ], + ids=[ + "For loop with global runtime variable", + "For loop wih local runtime variable", + "For loop in list comprehension", + "While loop", + "While else loop", + ], +) +def test_resolve_references_loops(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Array and indexed array" + """ +def f(): + arr = [1, 2, 3] + val = arr + res = arr[0] + arr[0] = 10 + """, # language=none + [ + ReferenceTestNode("arr.line4", "FunctionDef.f", ["LocalVariable.arr.line3"]), + ReferenceTestNode("arr.line5", "FunctionDef.f", ["LocalVariable.arr.line3"]), + ReferenceTestNode("arr.line6", "FunctionDef.f", ["LocalVariable.arr.line3"]), + ], + ), + ( # language=Python "Dictionary" + """ +def f(): + dictionary = {"key1": 1, "key2": 2} + dictionary["key1"] = 0 + """, # language=none + [ReferenceTestNode("dictionary.line4", "FunctionDef.f", ["LocalVariable.dictionary.line3"])], + ), + ( # language=Python "Map function" + """ +numbers = [1, 2, 3, 4, 5] + +def square(x): + return x ** 2 + +def f(): + squares = list(map(square, numbers)) + squares + """, # language=none + [ + ReferenceTestNode("list.line8", "FunctionDef.f", ["Builtin.list"]), + ReferenceTestNode("map.line8", "FunctionDef.f", ["Builtin.map"]), + ReferenceTestNode("x.line5", "FunctionDef.square", ["Parameter.x.line4"]), + ReferenceTestNode("square.line8", "FunctionDef.f", ["GlobalVariable.square.line4"]), + ReferenceTestNode("numbers.line8", "FunctionDef.f", ["GlobalVariable.numbers.line2"]), + ReferenceTestNode("squares.line9", "FunctionDef.f", ["LocalVariable.squares.line8"]), + ], + ), + ( # language=Python "Two variables" + """ +def f(): + x = 10 + y = 20 + x + y + """, # language=none + [ + ReferenceTestNode("x.line5", "FunctionDef.f", ["LocalVariable.x.line3"]), + ReferenceTestNode("y.line5", "FunctionDef.f", ["LocalVariable.y.line4"]), + ], + ), + ( # language=Python "Double return" + """ +def double_return(a, b): + return a, b + +def f(): + x, y = double_return(10, 20) + x, y + """, # language=none + [ + ReferenceTestNode("double_return.line6", "FunctionDef.f", ["GlobalVariable.double_return.line2"]), + ReferenceTestNode("a.line3", "FunctionDef.double_return", ["Parameter.a.line2"]), + ReferenceTestNode("b.line3", "FunctionDef.double_return", ["Parameter.b.line2"]), + ReferenceTestNode("x.line7", "FunctionDef.f", ["LocalVariable.x.line6"]), + ReferenceTestNode("y.line7", "FunctionDef.f", ["LocalVariable.y.line6"]), + ], + ), + ( # language=Python "Reassignment" + """ +def f(): + x = 10 + x = 20 + x + """, # language=none + [ + ReferenceTestNode("x.line5", "FunctionDef.f", ["LocalVariable.x.line3", "LocalVariable.x.line4"]), + ReferenceTestNode("x.line4", "FunctionDef.f", ["LocalVariable.x.line3"]), + ], + ), + ( # language=Python "Vars with comma" + """ +def f(): + x = 10 + y = 20 + x, y + """, # language=none + [ + ReferenceTestNode("x.line5", "FunctionDef.f", ["LocalVariable.x.line3"]), + ReferenceTestNode("y.line5", "FunctionDef.f", ["LocalVariable.y.line4"]), + ], + ), + ( # language=Python "Vars with extended iterable unpacking" + """ +def f(): + a, *b, c = [1, 2, 3, 4, 5] + a, b, c + """, # language=none + [ + ReferenceTestNode("a.line4", "FunctionDef.f", ["LocalVariable.a.line3"]), + ReferenceTestNode("b.line4", "FunctionDef.f", ["LocalVariable.b.line3"]), + ReferenceTestNode("c.line4", "FunctionDef.f", ["LocalVariable.c.line3"]), + ], + ), + ( # language=Python "String (f-string)" + """ +def f(): + x = 10 + y = 20 + f"{x} + {y} = {x + y}" + """, # language=none + [ + ReferenceTestNode("x.line5", "FunctionDef.f", ["LocalVariable.x.line3"]), + ReferenceTestNode("y.line5", "FunctionDef.f", ["LocalVariable.y.line4"]), + ReferenceTestNode("x.line5", "FunctionDef.f", ["LocalVariable.x.line3"]), + ReferenceTestNode("y.line5", "FunctionDef.f", ["LocalVariable.y.line4"]), + ], + ), + ( # language=Python "Multiple references in one line" + """ +def f(): + var1 = 10 + var2 = 20 + + res = var1 + var2 - (var1 * var2) + """, # language=none + [ + ReferenceTestNode("var1.line6", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var2.line6", "FunctionDef.f", ["LocalVariable.var2.line4"]), + ReferenceTestNode("var1.line6", "FunctionDef.f", ["LocalVariable.var1.line3"]), + ReferenceTestNode("var2.line6", "FunctionDef.f", ["LocalVariable.var2.line4"]), + ], + ), + ( # language=Python "Walrus operator" + """ +def f(): + y = (x := 3) + 10 + x, y + """, # language=none + [ + ReferenceTestNode("x.line4", "FunctionDef.f", ["LocalVariable.x.line3"]), + ReferenceTestNode("y.line4", "FunctionDef.f", ["LocalVariable.y.line3"]), + ], + ), + ( # language=Python "Variable swap" + """ +def f(): + a = 1 + b = 2 + a, b = b, a + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["LocalVariable.a.line3", "LocalVariable.a.line5"]), + ReferenceTestNode("b.line5", "FunctionDef.f", ["LocalVariable.b.line4", "LocalVariable.b.line5"]), + ReferenceTestNode("b.line5", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("a.line5", "FunctionDef.f", ["LocalVariable.a.line3"]), + ], + ), + ( # language=Python "Aliases" + """ +def f(): + a = 10 + b = a + c = b + c + """, # language=none + [ + ReferenceTestNode("a.line4", "FunctionDef.f", ["LocalVariable.a.line3"]), + ReferenceTestNode("b.line5", "FunctionDef.f", ["LocalVariable.b.line4"]), + ReferenceTestNode("c.line6", "FunctionDef.f", ["LocalVariable.c.line5"]), + ], + ), + ( # language=Python "Various assignments" + """ +def f(): + a = 10 + a = 20 + a = a + 10 + a = a * 2 + a + """, # language=none + [ + ReferenceTestNode( + "a.line5", + "FunctionDef.f", + [ + "LocalVariable.a.line3", + "LocalVariable.a.line4", + "LocalVariable.a.line5", + ], + ), + ReferenceTestNode( + "a.line6", + "FunctionDef.f", + [ + "LocalVariable.a.line3", + "LocalVariable.a.line4", + "LocalVariable.a.line5", + "LocalVariable.a.line6", + ], + ), + ReferenceTestNode( + "a.line7", + "FunctionDef.f", + [ + "LocalVariable.a.line3", + "LocalVariable.a.line4", + "LocalVariable.a.line5", + "LocalVariable.a.line6", + ], + ), + ReferenceTestNode( + "a.line6", + "FunctionDef.f", + ["LocalVariable.a.line3", "LocalVariable.a.line4", "LocalVariable.a.line5"], + ), + ReferenceTestNode("a.line5", "FunctionDef.f", ["LocalVariable.a.line3", "LocalVariable.a.line4"]), + ReferenceTestNode("a.line4", "FunctionDef.f", ["LocalVariable.a.line3"]), + ], + ), + ( # language=Python "Chained assignment" + """ +var1 = 1 +var2 = 2 +var3 = 3 + +def f(): + inp = input() + + var1 = a = inp # var1 is now a local variable + a = var2 = inp # var2 is now a local variable + var1 = a = var3 + """, # language=none + [ + ReferenceTestNode("input.line7", "FunctionDef.f", ["Builtin.input"]), + ReferenceTestNode("inp.line9", "FunctionDef.f", ["LocalVariable.inp.line7"]), + ReferenceTestNode("a.line10", "FunctionDef.f", ["LocalVariable.a.line9"]), + ReferenceTestNode("inp.line10", "FunctionDef.f", ["LocalVariable.inp.line7"]), + ReferenceTestNode("var1.line11", "FunctionDef.f", ["LocalVariable.var1.line9"]), + ReferenceTestNode("a.line11", "FunctionDef.f", ["LocalVariable.a.line10", "LocalVariable.a.line9"]), + ReferenceTestNode("var3.line11", "FunctionDef.f", ["GlobalVariable.var3.line4"]), + ], + ), + ( # language=Python "Chained assignment global keyword" + """ +var1 = 1 +var2 = 2 +var3 = 3 + +def f(a): + global var1, var2, var3 + inp = input() + + var1 = a = inp + a = var2 = inp + var1 = a = var3 + """, # language=none + [ + ReferenceTestNode("input.line8", "FunctionDef.f", ["Builtin.input"]), + ReferenceTestNode("a.line10", "FunctionDef.f", ["Parameter.a.line6"]), + ReferenceTestNode("inp.line10", "FunctionDef.f", ["LocalVariable.inp.line8"]), + ReferenceTestNode("var1.line10", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ReferenceTestNode("a.line11", "FunctionDef.f", ["Parameter.a.line10", "Parameter.a.line6"]), + ReferenceTestNode("var2.line11", "FunctionDef.f", ["GlobalVariable.var2.line3"]), + ReferenceTestNode("inp.line11", "FunctionDef.f", ["LocalVariable.inp.line8"]), + ReferenceTestNode( + "var1.line12", + "FunctionDef.f", + ["GlobalVariable.var1.line10", "GlobalVariable.var1.line2"], + ), + ReferenceTestNode( + "a.line12", + "FunctionDef.f", + ["Parameter.a.line10", "Parameter.a.line11", "Parameter.a.line6"], + ), + ReferenceTestNode("var3.line12", "FunctionDef.f", ["GlobalVariable.var3.line4"]), + ], + ), + ( # language=Python "With Statement Function" + """ +def fun(): + with open("text.txt") as f: + text = f.read() + print(text) + f.close() + """, # language=none + [ + ReferenceTestNode("open.line3", "FunctionDef.fun", ["BuiltinOpen.open"]), + ReferenceTestNode("read.line4", "FunctionDef.fun", ["BuiltinOpen.read"]), + ReferenceTestNode("f.line4", "FunctionDef.fun", ["LocalVariable.f.line3"]), + ReferenceTestNode("print.line5", "FunctionDef.fun", ["Builtin.print"]), + ReferenceTestNode("text.line5", "FunctionDef.fun", ["LocalVariable.text.line4"]), + ReferenceTestNode("close.line6", "FunctionDef.fun", ["BuiltinOpen.close"]), + ReferenceTestNode("f.line6", "FunctionDef.fun", ["LocalVariable.f.line3"]), + ], + ), + ], + ids=[ + "Array and indexed array", + "Dictionary", + "Map function", + "Two variables", + "Double return", + "Reassignment", + "Vars with comma", + "Vars with extended iterable unpacking", + "String (f-string)", + "Multiple references in one line", + "Walrus operator", + "Variable swap", + "Aliases", + "Various assignments", + "Chained assignment", + "Chained assignment global keyword", + "With open", + ], +) +def test_resolve_references_miscellaneous(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Builtin function call" + """ +def f(): + print("Hello, World!") + """, # language=none + [ReferenceTestNode("print.line3", "FunctionDef.f", ["Builtin.print"])], + ), + ( # language=Python "Function call shadowing builtin function" + """ +def print(s): + pass + +def f(): + print("Hello, World!") + """, # language=none + [ + ReferenceTestNode("print.line6", "FunctionDef.f", ["Builtin.print", "GlobalVariable.print.line2"]), + ], + ), + ( # language=Python "Function call" + """ +def f(): + pass + +def g(): + f() + """, # language=none + [ReferenceTestNode("f.line6", "FunctionDef.g", ["GlobalVariable.f.line2"])], + ), + ( # language=Python "Function call with parameter" + """ +def f(a): + return a + +def g(): + x = 10 + f(x) + """, # language=none + [ + ReferenceTestNode("f.line7", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("a.line3", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("x.line7", "FunctionDef.g", ["LocalVariable.x.line6"]), + ], + ), + ( # language=Python "Function call with keyword parameter" + """ +def f(value): + return value + +def g(): + x = 10 + f(value=x) + """, # language=none + [ + ReferenceTestNode("f.line7", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("value.line3", "FunctionDef.f", ["Parameter.value.line2"]), + ReferenceTestNode("x.line7", "FunctionDef.g", ["LocalVariable.x.line6"]), + ], + ), + ( # language=Python "Function call as value" + """ +def f(a): + return a + +def g(): + x = f(10) + """, # language=none + [ + ReferenceTestNode("f.line6", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("a.line3", "FunctionDef.f", ["Parameter.a.line2"]), + ], + ), + ( # language=Python "Nested function call" + """ +def f(a): + return a * 2 + +def g(): + f(f(f(10))) + """, # language=none + [ + ReferenceTestNode("f.line6", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("f.line6", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("f.line6", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("a.line3", "FunctionDef.f", ["Parameter.a.line2"]), + ], + ), + ( # language=Python "Two functions" + """ +def fun1(): + return "Function 1" + +def fun2(): + return "Function 2" + +def g(): + fun1() + fun2() + """, # language=none + [ + ReferenceTestNode("fun1.line9", "FunctionDef.g", ["GlobalVariable.fun1.line2"]), + ReferenceTestNode("fun2.line10", "FunctionDef.g", ["GlobalVariable.fun2.line5"]), + ], + ), + ( # language=Python "Functon with function as parameter" + """ +def fun1(): + return "Function 1" + +def fun2(): + return "Function 2" + +def call_function(f): + return f() + +def g(): + call_function(fun1) + call_function(fun2) + """, # language=none + [ + ReferenceTestNode("f.line9", "FunctionDef.call_function", ["Parameter.f.line8"]), + # f should be detected as a call but is treated as a parameter, since the passed function is not known before runtime + # this is later handled as an unknown call + ReferenceTestNode("call_function.line12", "FunctionDef.g", ["GlobalVariable.call_function.line8"]), + ReferenceTestNode("call_function.line13", "FunctionDef.g", ["GlobalVariable.call_function.line8"]), + ReferenceTestNode("fun1.line12", "FunctionDef.g", ["GlobalVariable.fun1.line2"]), + ReferenceTestNode("fun2.line13", "FunctionDef.g", ["GlobalVariable.fun2.line5"]), + ], + ), + ( # language=Python "Functon conditional with branching" + """ +def fun1(): + return "Function 1" + +def fun2(): + return "Function 2" + +def call_function(a): + if a == 1: + return fun1() + else: + return fun2() + +def g(): + call_function(1) + """, # language=none + [ + ReferenceTestNode("fun1.line10", "FunctionDef.call_function", ["GlobalVariable.fun1.line2"]), + ReferenceTestNode("fun2.line12", "FunctionDef.call_function", ["GlobalVariable.fun2.line5"]), + ReferenceTestNode("call_function.line15", "FunctionDef.g", ["GlobalVariable.call_function.line8"]), + ReferenceTestNode("a.line9", "FunctionDef.call_function", ["Parameter.a.line8"]), + ], + ), + ( # language=Python "Recursive function call", + """ +def f(a): + print(a) + if a > 0: + f(a - 1) + +def g(): + x = 10 + f(x) + """, # language=none + [ + ReferenceTestNode("print.line3", "FunctionDef.f", ["Builtin.print"]), + ReferenceTestNode("f.line5", "FunctionDef.f", ["GlobalVariable.f.line2"]), + ReferenceTestNode("f.line9", "FunctionDef.g", ["GlobalVariable.f.line2"]), + ReferenceTestNode("a.line3", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("a.line4", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("x.line9", "FunctionDef.g", ["LocalVariable.x.line8"]), + ], + ), + ( # language=Python "Class instantiation" + """ +class F: + pass + +def g(): + F() + """, # language=none + [ReferenceTestNode("F.line6", "FunctionDef.g", ["GlobalVariable.F.line2"])], + ), + ( # language=Python "Lambda function" + """ +var1 = 1 + +def f(): + global var1 + lambda x, y: x + y + var1 + """, # language=none + [ + ReferenceTestNode("var1.line6", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ], + ), + ( # language=Python "Lambda function call" + """ +var1 = 1 + +def f(): + (lambda x, y: x + y + var1)(10, 20) + """, # language=none + [ + ReferenceTestNode("var1.line5", "FunctionDef.f", ["GlobalVariable.var1.line2"]), + ], + ), + ( # language=Python "Lambda function used as normal function" + """ +double = lambda x: 2 * x + +def f(): + double(10) + """, # language=none + [ + ReferenceTestNode("x.line2", "Lambda", ["Parameter.x.line2"]), + ReferenceTestNode("double.line5", "FunctionDef.f", ["GlobalVariable.double.line2"]), + ], + ), + ( # language=Python "Two lambda function used as normal function with the same name" + """ +class A: + double = lambda x: 2 * x + +class B: + double = lambda x: 2 * x + +def f(): + A.double(10) + B.double(10) + """, # language=none + [ + ReferenceTestNode("x.line3", "Lambda", ["Parameter.x.line3"]), + ReferenceTestNode("A.line9", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "double.line9", + "FunctionDef.f", + ["ClassVariable.A.double.line3", "ClassVariable.B.double.line6"], + ), + ReferenceTestNode("x.line6", "Lambda", ["Parameter.x.line6"]), + ReferenceTestNode("B.line10", "FunctionDef.f", ["GlobalVariable.B.line5"]), + ReferenceTestNode( + "double.line10", + "FunctionDef.f", + ["ClassVariable.A.double.line3", "ClassVariable.B.double.line6"], + ), + ], + ), # since we only return a list of all possible references, we can't distinguish between the two functions + ( # language=Python "Lambda function used as normal function and normal function with the same name" + """ +class A: + double = lambda x: 2 * x + +class B: + @staticmethod + def double(x): + return 2 * x + +def f(): + A.double(10) + B.double(10) + """, # language=none + [ + ReferenceTestNode("x.line3", "Lambda", ["Parameter.x.line3"]), + ReferenceTestNode("A.line11", "FunctionDef.f", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "double.line11", + "FunctionDef.f", + ["ClassVariable.A.double.line3", "ClassVariable.B.double.line7"], + ), + ReferenceTestNode("x.line8", "FunctionDef.double", ["Parameter.x.line7"]), + ReferenceTestNode("B.line12", "FunctionDef.f", ["GlobalVariable.B.line5"]), + ReferenceTestNode( + "double.line12", + "FunctionDef.f", + ["ClassVariable.A.double.line3", "ClassVariable.B.double.line7"], + ), + ], + ), # since we only return a list of all possible references, we can't distinguish between the two functions + ( # language=Python "Lambda function as key" + """ +def f(): + names = ["a", "abc", "ab", "abcd"] + + sort = sorted(names, key=lambda x: len(x)) + sort + """, # language=none + [ + ReferenceTestNode("sorted.line5", "FunctionDef.f", ["Builtin.sorted"]), + ReferenceTestNode("len.line5", "FunctionDef.f", ["Builtin.len"]), + ReferenceTestNode("names.line5", "FunctionDef.f", ["LocalVariable.names.line3"]), + # ReferenceTestNode("x.line5", "Lambda", ["Parameter.x.line5"]), + ReferenceTestNode("sort.line6", "FunctionDef.f", ["LocalVariable.sort.line5"]), + ], + ), + ( # language=Python "Generator function" + """ +def square_generator(limit): + for i in range(limit): + yield i**2 + +def g(): + gen = square_generator(5) + for value in gen: + value + """, # language=none + [ + ReferenceTestNode("range.line3", "FunctionDef.square_generator", ["Builtin.range"]), + ReferenceTestNode("square_generator.line7", "FunctionDef.g", ["GlobalVariable.square_generator.line2"]), + ReferenceTestNode("limit.line3", "FunctionDef.square_generator", ["Parameter.limit.line2"]), + ReferenceTestNode("i.line4", "FunctionDef.square_generator", ["LocalVariable.i.line3"]), + ReferenceTestNode("gen.line8", "FunctionDef.g", ["LocalVariable.gen.line7"]), + ReferenceTestNode("value.line9", "FunctionDef.g", ["LocalVariable.value.line8"]), + ], + ), + ( # language=Python "Functions with the same name but different classes" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b): + return a + 2 * b + +def g(): + A.add(1, 2) + B.add(1, 2) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line13", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ReferenceTestNode("B.line14", "FunctionDef.g", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ], + ), # since we only return a list of all possible references, we can't distinguish between the two functions + ( # language=Python "Functions with the same name but different arity" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b, c): + return a + b + c + +def g(): + A.add(1, 2) + B.add(1, 2, 3) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("c.line10", "FunctionDef.add", ["Parameter.c.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line13", + "FunctionDef.g", + ["ClassVariable.A.add.line4"], + ), + ReferenceTestNode("B.line14", "FunctionDef.g", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.g", + ["ClassVariable.B.add.line9"], + ), + ], + ), + ( # language=Python "Functions with the same name but different arity with star arguments" + """ +class A: + @staticmethod + def add(a, b, c): + return a + b + c + +class B: + @staticmethod + def add(a, b, *args): + return a + b + args + +class C: + @staticmethod + def add(a, b, *c): + return a + b + c + +class D: + @staticmethod + def add(pos_arg, def_arg="default_value", *args, key_arg="default_kwarg", **kwargs): + pass + +def g(): + A.add(1, 2, 3) + B.add(1, 2, 3, 4, [5, 6]) + D.add(1, "2", 3, 4, [5, 6], key_arg="seven", name="eight") + D.add(1, def_arg="2", args=(3, 4, [5, 6]), key_arg="seven", name="eight") + D.add(pos_arg=1, def_arg="2", key_arg="seven") + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("c.line5", "FunctionDef.add", ["Parameter.c.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("args.line10", "FunctionDef.add", ["Parameter.args.line9"]), + ReferenceTestNode("a.line15", "FunctionDef.add", ["Parameter.a.line14"]), + ReferenceTestNode("b.line15", "FunctionDef.add", ["Parameter.b.line14"]), + ReferenceTestNode("c.line15", "FunctionDef.add", ["Parameter.c.line14"]), + ReferenceTestNode("A.line23", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line23", + "FunctionDef.g", + [ + "ClassVariable.A.add.line4", + "ClassVariable.B.add.line9", + "ClassVariable.C.add.line14", + "ClassVariable.D.add.line19", + ], + ), + ReferenceTestNode("B.line24", "FunctionDef.g", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line24", + "FunctionDef.g", + ["ClassVariable.B.add.line9", "ClassVariable.C.add.line14", "ClassVariable.D.add.line19"], + ), + ReferenceTestNode("D.line25", "FunctionDef.g", ["GlobalVariable.D.line17"]), + ReferenceTestNode( + "add.line25", + "FunctionDef.g", + ["ClassVariable.D.add.line19"], + ), + ReferenceTestNode("D.line26", "FunctionDef.g", ["GlobalVariable.D.line17"]), + ReferenceTestNode( + "add.line26", + "FunctionDef.g", + ["ClassVariable.D.add.line19"], + ), + ReferenceTestNode("D.line27", "FunctionDef.g", ["GlobalVariable.D.line17"]), + ReferenceTestNode( + "add.line27", + "FunctionDef.g", + ["ClassVariable.D.add.line19"], + ), + ], + ), + ( # language=Python "Functions with the same name but different signature positionals" + """ +class A: + @staticmethod + def add(a:int, b:int): + return a + b + +class B: + @staticmethod + def add(a:int, b:float): + return a + b + +def g(): + A.add(a=1, b=2) + B.add(a=1, b=2) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line13", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ReferenceTestNode("B.line14", "FunctionDef.g", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ], + ), + ( # language=Python "Functions with the same name but different signature keywords" + """ +class A: + @staticmethod + def add(a:int=1, b:int=2): + return a + b + +class B: + @staticmethod + def add(a:int=1, b:float=2.0): + return a + b + +def g(): + A.add(a=1, b=2) + B.add(a=1, b=2) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.add", ["Parameter.a.line4"]), + ReferenceTestNode("b.line5", "FunctionDef.add", ["Parameter.b.line4"]), + ReferenceTestNode("a.line10", "FunctionDef.add", ["Parameter.a.line9"]), + ReferenceTestNode("b.line10", "FunctionDef.add", ["Parameter.b.line9"]), + ReferenceTestNode("A.line13", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode( + "add.line13", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ReferenceTestNode("B.line14", "FunctionDef.g", ["GlobalVariable.B.line7"]), + ReferenceTestNode( + "add.line14", + "FunctionDef.g", + ["ClassVariable.A.add.line4", "ClassVariable.B.add.line9"], + ), + ], + ), + ( # language=Python "Class function call" + """ +class A: + def fun_a(self): + return + +def g(): + a = A() + a.fun_a() + """, # language=none + [ + ReferenceTestNode("A.line7", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode("fun_a.line8", "FunctionDef.g", ["ClassVariable.A.fun_a.line3"]), + ReferenceTestNode("a.line8", "FunctionDef.g", ["LocalVariable.a.line7"]), + ], + ), + ( # language=Python "Class function call, direct call" + """ +class A: + def fun_a(self): + return + +def g(): + A().fun_a() + """, # language=none + [ + ReferenceTestNode("A.line7", "FunctionDef.g", ["GlobalVariable.A.line2"]), + ReferenceTestNode("fun_a.line7", "FunctionDef.g", ["ClassVariable.A.fun_a.line3"]), + ], + ), + # ( # language=Python "class function and class variable with same name" + # """ + # class A: + # fun = 1 + # + # def fun(self): + # return + # + # def g(): + # A().fun() + # """, # language=none + # [ReferenceTestNode("fun.line9", "FunctionDef.g", ["ClassVariable.A.fun.line3", + # "ClassVariable.A.fun.line5"]), + # ReferenceTestNode("A.line9", "FunctionDef.g", ["GlobalVariable.A.line2"])] + # ), + ], + ids=[ + "Builtin function call", + "Function call shadowing builtin function", + "Function call", + "Function call with parameter", + "Function call with keyword parameter", + "Function call as value", + "Nested function call", + "Two functions", + "Function with function as parameter", + "Function with conditional branching", + "Recursive function call", + "Class instantiation", + "Lambda function", + "Lambda function call", + "Lambda function used as normal function", + "Two lambda functions used as normal function with the same name", + "Lambda function used as normal function and normal function with the same name", + "Lambda function as key", + "Generator function", + "Functions with the same name but different classes", + "Functions with the same name but different arity", + "Functions with the same name but different arity with star arguments", + "Functions with the same name but different signature positionals", # TODO: [LATER] detect the difference between the two functions + "Functions with the same name but different signature keywords", # TODO: [LATER] detect the difference between the two functions + "Class function call", + "Class function call, direct call", + # "Class function and class variable with the same name" # This is bad practice and therfore is not covered- only the function def will be found in this case + ], +) +def test_resolve_references_calls(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + # assert references == expected + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Import module - module" + """ +import math + +def f(): + a = math + """, # language=none + [ReferenceTestNode("math.line5", "FunctionDef.f", ["Import.math.line2"])], + ), + ( # language=Python "Import module - constant" + """ +import math + +def f(): + a = math.pi + """, # language=none + [ + ReferenceTestNode("math.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("math.pi.line5", "FunctionDef.f", ["Import.math.pi.line2"]), + ], + ), + ( # language=Python "Import module with alias - constant" + """ +import math as m + +def f(): + a = m.pi + """, # language=none + [ + ReferenceTestNode("m.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("m.pi.line5", "FunctionDef.f", ["Import.math.pi.line2"]), + ], + ), + ( # language=Python "Import module - function" + """ +import math + +def f(a): + math.sqrt(a) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("math.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("math.sqrt.line5", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ], + ), + ( # language=Python "Import module with alias - function" + """ +import math as m + +def f(a): + m.sqrt(a) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("m.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("m.sqrt.line5", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ], + ), + ( # language=Python "Import module with alias - function and constant" + """ +import math as m + +def f(a): + a = m.pi + m.sqrt(a) + m.pi = 1 # this is very uncommon but possible - make sure we detect targets as well since this is very impure + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("m.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("m.pi.line5", "FunctionDef.f", ["Import.math.pi.line2"]), + ReferenceTestNode("a.line6", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("m.line6", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("m.sqrt.line6", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ReferenceTestNode("m.pi.line7", "FunctionDef.f", ["Import.math.pi.line2"]), + ], + ), + ( # language=Python "Import two modules with alias - function and module" + """ +import math as m, sys as s + +def f(a): + m.sqrt(a) + x = s + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("m.line5", "FunctionDef.f", ["Import.math.line2"]), + ReferenceTestNode("m.sqrt.line5", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ReferenceTestNode("s.line6", "FunctionDef.f", ["Import.sys.line2"]), + ], + ), + ( # language=Python "FromImport - constant" + """ +from math import pi + +def f(): + a = pi + """, # language=none + [ReferenceTestNode("pi.line5", "FunctionDef.f", ["Import.math.pi.line2"])], + ), + ( # language=Python "FromImport with alias - constant" + """ +from math import pi as p + +def f(): + a = p + """, # language=none + [ReferenceTestNode("p.line5", "FunctionDef.f", ["Import.math.pi.line2"])], + ), + ( # language=Python "FromImport - function" + """ +from math import sqrt + +def f(a): + sqrt(a) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("sqrt.line5", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ], + ), + ( # language=Python "FromImport with alias - function" + """ +from math import sqrt as s + +def f(a): + s(a) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("s.line5", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ], + ), + ( # language=Python "FromImport with alias - function and constant" + """ +from math import sqrt as s, pi as p + +def f(a): + a = p + s(a) + """, # language=none + [ + ReferenceTestNode("a.line5", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("p.line5", "FunctionDef.f", ["Import.math.pi.line2"]), + ReferenceTestNode("a.line6", "FunctionDef.f", ["Parameter.a.line4"]), + ReferenceTestNode("s.line6", "FunctionDef.f", ["Import.math.sqrt.line2"]), + ], + ), + ( # language=Python "Local FromImport - constant" + """ +def f(a): + from math import pi + a = pi + """, # language=none + [ + ReferenceTestNode("a.line4", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("pi.line4", "FunctionDef.f", ["Import.math.pi.line3"]), + ], + ), + ( # language=Python "Local FromImport - function" + """ +def f(a): + from math import sqrt as s + s(a) + """, # language=none + [ + ReferenceTestNode("a.line4", "FunctionDef.f", ["Parameter.a.line2"]), + ReferenceTestNode("s.line4", "FunctionDef.f", ["Import.math.sqrt.line3"]), + ], + ), + ], + ids=[ + "Import module - module", + "Import module - constant", + "Import module with alias - constant", + "Import module - function", + "Import module with alias - function", + "Import module with alias - function and constant", + "Import two modules with alias - function and module", + "FromImport - constant", + "FromImport with alias - constant", + "FromImport - function", + "FromImport with alias - function", + "FromImport with alias - function and constant", + "Local FromImport - constant", + "Local FromImport - function", + ], +) +def test_resolve_references_imports(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Dataclass" + """ +from dataclasses import dataclass + +@dataclass +class State: + pass + +def f(): + State() + """, # language=none + [ReferenceTestNode("State.line9", "FunctionDef.f", ["GlobalVariable.State.line5"])], + ), + ( # language=Python "Dataclass with default attribute" + """ +from dataclasses import dataclass + +@dataclass +class State: + state: int = 0 + +def f(): + State().state + """, # language=none + [ + ReferenceTestNode("State.line9", "FunctionDef.f", ["GlobalVariable.State.line5"]), + ReferenceTestNode("State.state.line9", "FunctionDef.f", ["ClassVariable.State.state.line6"]), + ], + ), + ( # language=Python "Dataclass with attribute" + """ +from dataclasses import dataclass + +@dataclass +class State: + state: int + +def f(): + State(0).state + """, # language=none + [ + ReferenceTestNode("State.line9", "FunctionDef.f", ["GlobalVariable.State.line5"]), + ReferenceTestNode("State.state.line9", "FunctionDef.f", ["ClassVariable.State.state.line6"]), + ], + ), + ( # language=Python "Dataclass with @property and @setter" + """ +from dataclasses import dataclass + +@dataclass +class State: + _state: int + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + self._state = value + +def f(): + a = State(1) + a.state = 2 + """, # language=none + [ + ReferenceTestNode("self.line10", "FunctionDef.state", ["Parameter.self.line9"]), + ReferenceTestNode("self._state.line10", "FunctionDef.state", ["ClassVariable.State._state.line6"]), + ReferenceTestNode("self.line14", "FunctionDef.state", ["Parameter.self.line13"]), + ReferenceTestNode("self._state.line14", "FunctionDef.state", ["ClassVariable.State._state.line6"]), + ReferenceTestNode("value.line14", "FunctionDef.state", ["Parameter.value.line13"]), + ReferenceTestNode("State.line17", "FunctionDef.f", ["GlobalVariable.State.line5"]), + ReferenceTestNode( + "a.state.line18", + "FunctionDef.f", + [ + "ClassVariable.State.state.line13", + "ClassVariable.State.state.line9", + "InstanceVariable.State.state.line9", + ], + ), + ReferenceTestNode("a.line18", "FunctionDef.f", ["LocalVariable.a.line17"]), + ], + ), + ], + ids=[ + "Dataclass", + "Dataclass with default attribute", + "Dataclass with attribute", + "Dataclass with @property and @setter", + ], +) +def test_resolve_references_dataclasses(code: str, expected: list[ReferenceTestNode]) -> None: + references = resolve_references(code).resolved_references + transformed_references: list[ReferenceTestNode] = [] + + for node in references.values(): + transformed_references.extend(transform_reference_nodes(node)) + + # assert references == expected + assert set(transformed_references) == set(expected) + + +@pytest.mark.parametrize( + ("code", "expected"), + [ + ( # language=Python "Basics" + """ +b = 1 +c = 2 +d = 3 +def g(): + pass + +def f(): + global b + a = 1 # LocaleWrite + b = 2 # NonLocalVariableWrite + a # LocaleRead + c # NonLocalVariableRead + b = d # NonLocalVariableWrite, NonLocalVariableRead + g() # Call + x = open("text.txt") # LocalWrite, Call + """, # language=none + { + ".f.8.0": SimpleReasons( + "f", + {"GlobalVariable.b.line2", "GlobalVariable.b.line11"}, + { + "GlobalVariable.c.line3", + "GlobalVariable.d.line4", + }, + ), + ".g.5.0": SimpleReasons("g", set(), set()), + }, + ), + ( # language=Python "Control flow statements" + """ +b = 1 +c = 0 + +def f(): + global b, c + if b > 1: # we ignore all control flow statements + a = 1 # LocaleWrite + else: + c = 2 # NonLocalVariableWrite + + while a < 10: # we ignore all control flow statements + b += 1 # NonLocalVariableWrite + """, # language=none + { + ".f.5.0": SimpleReasons( + "f", + { + "GlobalVariable.c.line3", + "GlobalVariable.b.line2", + }, + { + "GlobalVariable.b.line2", + }, + ), + }, + ), + ( # language=Python "Class attribute" + """ +class A: + class_attr1 = 20 + +def f(): + a = A() + a.class_attr1 = 10 # NonLocalVariableWrite + +def g(): + a = A() + c = a.class_attr1 # NonLocalVariableRead + """, # language=none + { + ".f.5.0": SimpleReasons( + "f", + { + "ClassVariable.A.class_attr1.line3", + }, + set(), + ), + ".g.9.0": SimpleReasons("g", set(), {"ClassVariable.A.class_attr1.line3"}), + }, + ), + ( # language=Python "Instance attribute" + """ +class A: + def __init__(self): + self.instance_attr1 = 20 + +def f1(): + a = A() + a.instance_attr1 = 10 # NonLocalVariableWrite # TODO [Later] we should detect that this is a local variable + +b = A() +def f2(x): + x.instance_attr1 = 10 # NonLocalVariableWrite + +def f3(): + global b + b.instance_attr1 = 10 # NonLocalVariableWrite + +def g1(): + a = A() + c = a.instance_attr1 # NonLocalVariableRead # TODO [Later] we should detect that this is a local variable + +def g2(x): + c = x.instance_attr1 # NonLocalVariableRead + +def g3(): + global b + c = b.instance_attr1 # NonLocalVariableRead + """, # language=none + { + ".__init__.3.4": SimpleReasons("__init__"), + ".f1.6.0": SimpleReasons( + "f1", + { + "InstanceVariable.A.instance_attr1.line4", + }, + set(), + ), + ".f2.11.0": SimpleReasons( + "f2", + { + "InstanceVariable.A.instance_attr1.line4", + }, + ), + ".f3.14.0": SimpleReasons( + "f3", + {"GlobalVariable.b.line10", "InstanceVariable.A.instance_attr1.line4"}, + ), + ".g1.18.0": SimpleReasons( + "g1", + set(), + { + "InstanceVariable.A.instance_attr1.line4", + }, + ), + ".g2.22.0": SimpleReasons( + "g2", + set(), + { + "InstanceVariable.A.instance_attr1.line4", + }, + ), + ".g3.25.0": SimpleReasons( + "g3", + set(), + { + "GlobalVariable.b.line10", + "InstanceVariable.A.instance_attr1.line4", + }, + ), + }, + ), + ( # language=Python "Chained attributes" + """ +class A: + def __init__(self): + self.name = 10 + + def set_name(self, name): + self.name = name + +class B: + upper_class: A = A() + +def f(): + b = B() + x = b.upper_class.name + b.upper_class.set_name("test") + """, # language=none + { + ".__init__.3.4": SimpleReasons("__init__"), + ".set_name.6.4": SimpleReasons("set_name", {"InstanceVariable.A.name.line4"}), + ".f.12.0": SimpleReasons( + "f", + set(), + { + "InstanceVariable.A.name.line4", + "ClassVariable.B.upper_class.line10", + }, + ), + }, + ), + ( # language=Python "Chained class function call" + """ +class B: + def __init__(self): + self.b = 20 + + def f(self): + pass + +class A: + class_attr1 = B() + +def g(): + A().class_attr1.f() + """, # language=none + { + ".__init__.3.4": SimpleReasons("__init__"), + ".f.6.4": SimpleReasons("f", set(), set()), + ".g.12.0": SimpleReasons( + "g", + set(), + { + "ClassVariable.A.class_attr1.line10", + }, + ), + }, + ), + ( # language=Python "Two classes with same attribute name" + """ +class A: + name: str = "" + + def __init__(self, name: str): + self.name = name + +class B: + name: str = "" + + def __init__(self, name: str): + self.name = name + +def f(): + a = A("value") + b = B("test") + a.name + b.name + """, # language=none + { + ".__init__.5.4": SimpleReasons("__init__", {"ClassVariable.A.name.line3"}), + ".__init__.11.4": SimpleReasons("__init__", {"ClassVariable.B.name.line9"}), + ".f.14.0": SimpleReasons( + "f", + set(), + { # Here we find both: ClassVariables and InstanceVariables because we can't distinguish between them + "ClassVariable.A.name.line3", + "ClassVariable.B.name.line9", + "InstanceVariable.A.name.line6", + "InstanceVariable.B.name.line12", + }, + ), + }, + ), + ( # language=Python "Multiple classes with same function name - same signature" + """ +z = 2 + +class A: + @staticmethod + def add(a, b): + global z + return a + b + z + +class B: + @staticmethod + def add(a, b): + return a + 2 * b + +def f(): + x = A.add(1, 2) # This is not a global read of A. Since we define classes and functions as immutable. + y = B.add(1, 2) + if x == y: + pass + """, # language=none + { + ".add.6.4": SimpleReasons("add", set(), {"GlobalVariable.z.line2"}), + ".add.12.4": SimpleReasons( + "add", + ), + ".f.15.0": SimpleReasons( + "f", + set(), + set(), + ), + }, + ), # since we only return a list of all possible references, we can't distinguish between the two functions + ( # language=Python "Multiple classes with same function name - different signature" + """ +class A: + @staticmethod + def add(a, b): + return a + b + +class B: + @staticmethod + def add(a, b, c): + return a + b + c + +def f(): + A.add(1, 2) + B.add(1, 2, 3) + """, # language=none + { + ".add.4.4": SimpleReasons( + "add", + ), + ".add.9.4": SimpleReasons( + "add", + ), + ".f.12.0": SimpleReasons( + "f", + set(), + set(), + ), + }, + ), + ], + ids=[ + "Basics", + "Control flow statements", + "Class attribute", + "Instance attribute", + "Chained attributes", + "Chained class function call", + "Two classes with same attribute name", + "Multiple classes with same function name - same signature", + "Multiple classes with same function name - different signature", + ], +) +def test_resolve_references_reasons(code: str, expected: dict[str, SimpleReasons]) -> None: + function_references = resolve_references(code).raw_reasons + + transformed_function_references = transform_reasons(function_references) + # assert function_references == expected + + assert transformed_function_references == expected diff --git a/tests/safeds_stubgen/api_analyzer/test__get_api.py b/tests/safeds_stubgen/api_analyzer/test__get_api.py index 7964b480..f718bc2b 100644 --- a/tests/safeds_stubgen/api_analyzer/test__get_api.py +++ b/tests/safeds_stubgen/api_analyzer/test__get_api.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from collections.abc import Generator - from syrupy import SnapshotAssertion + from syrupy.assertion import SnapshotAssertion # Setup: API data _test_dir = Path(__file__).parent.parent.parent diff --git a/tests/safeds_stubgen/api_analyzer/test_extract_boundary_values.py b/tests/safeds_stubgen/api_analyzer/test_extract_boundary_values.py new file mode 100644 index 00000000..674f0a6a --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/test_extract_boundary_values.py @@ -0,0 +1,265 @@ +from typing import TypeAlias + +import pytest +from safeds_stubgen.api_analyzer._extract_boundary_values import extract_boundary +from safeds_stubgen.api_analyzer._types import BoundaryType + +_Numeric: TypeAlias = int | float +BoundaryValueType = tuple[str, tuple[_Numeric | str, bool], tuple[_Numeric | str, bool]] + + +# @pytest.mark.skip(reason="Currently not testting this") +@pytest.mark.parametrize( + ("type_string", "description", "expected_boundary"), + [ + ( + "float", + ( + "Damping factor in the range [0.5, 1.0) is the extent to which the current value is maintained relative" + " to incoming values (weighted 1 - damping). This in order to avoid numerical oscillations when" + " updating these values (messages)." + ), + [("float", (0.5, True), (1.0, False))], + ), + ( + "float", + ( + "An upper bound on the fraction of training errors and a lower bound of the fraction of support" + " vectors. Should be in the interval (0, 1]. By default 0.5 will be taken." + ), + [("float", (0.0, False), (1.0, True))], + ), + ( + "non-negative float", + ( + "Complexity parameter used for Minimal Cost-Complexity Pruning. The subtree with the largest cost " + "complexity that is smaller than ccp_alpha will be chosen. By default, no pruning is performed. See " + ":ref:minimal_cost_complexity_pruning for details." + ), + [("float", (0.0, True), ("Infinity", False))], + ), + ( + "{'scale', 'auto'} or float", + ( + "Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.\n\nif gamma='scale' (default) is passed then it" + " uses 1 / (n_features * X.var()) as value of gamma,\nif 'auto', uses 1 / n_features\nif float, must be" + " non-negative.\n\n.. versionchanged: 0.22 The default value of gamma changed from 'auto' to 'scale'." + ), + [("float", (0.0, True), ("Infinity", False))], + ), + ( + "int", + "Degree of the polynomial kernel function ('poly'). Must be non-negative. Ignored by all other kernels.", + [("int", (0, True), ("Infinity", False))], + ), + ( + "int", + "The verbosity level. The default, zero, means silent mode. Range of values is [0, inf].", + [("int", (0, True), ("Infinity", False))], + ), + ( + "int", + "The verbosity level. The default, zero, means silent mode. Range of values is at least 3.", + [("int", (3, True), ("Infinity", False))], + ), + ( + "float", + "Momentum for gradient descent update. Should be between 0 and 1. Only used when solver='sgd'.", + [("float", (0.0, True), (1.0, True))], + ), + ( + "float between 0 and 1", + ( + "Determines the minimum steepness on the reachability plot that constitutes a cluster boundary. For " + "example, an upwards point in the reachability plot is defined by the ratio from one point to its " + "successor being at most 1-xi. Used only when cluster_method='xi'." + ), + [("float", (0.0, True), (1.0, True))], + ), + ( + "float", + "Momentum for gradient descent update. Should be non-positive. Only used when solver='sgd'.", + [("float", ("NegativeInfinity", False), (0.0, True))], + ), + ( + "float", + ( + "Regularization parameter. The strength of the regularization is inversely proportional to C. Must be " + "strictly positive." + ), + [("float", (0.0, False), ("Infinity", False))], + ), + ( + "int or float", + ( + "If bootstrap is True, the number of samples to draw from X to train each base estimator.\n\nIf None (" + "default), then draw X.shape[0] samples.\nIf int, then draw max_samples samples.\n If float, " + "then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, " + "1.0].\n\n.. versionadded: 0.22" + ), + [("float", (0.0, False), (1.0, True))], + ), + ( + "int or float", + ( + "If bootstrap is True, the number of samples to draw from X to train each base estimator.\n\nIf None (" + "default), then draw X.shape[0] samples.\nIf int, then max_samples values in [0, 10].\n If float, " + "then draw max_samples * X.shape[0] samples. Thus, max_samples should be in the interval (0.0, " + "1.0].\n\n.. versionadded: 0.22" + ), + [("int", (0, True), (10, True)), ("float", (0.0, False), (1.0, True))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with 0 <= l1_ratio <= 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, True), (1.0, True))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with 0 < l1_ratio <= 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, False), (1.0, True))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with 0 <= l1_ratio < 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, True), (1.0, False))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with 0 < l1_ratio < 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, False), (1.0, False))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with 1 > l1_ratio > 0. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, False), (1.0, False))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with l1_ratio > 0 and < 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, False), (1.0, False))], + ), + ( + "float", + ( + "The Elastic Net mixing parameter, with l1_ratio >= 0 and < 1. l1_ratio=0 corresponds to L2 penalty, " + "l1_ratio=1 to L1. Only used if penalty is 'elasticnet'." + ), + [("float", (0.0, True), (1.0, False))], + ), + ( + "int > 1 or float between 0 and 1", + ( + "Minimum number of samples in an OPTICS cluster, expressed as an absolute number or a fraction of the " + "number of samples (rounded to be at least 2). If None, the value of min_samples is used instead. Used " + "only when cluster_method='xi'." + ), + [("int", (1, False), ("Infinity", False)), ("float", (0.0, True), (1.0, True))], + ), + ( + "float < 0.5", + ( + "Minimum number of samples in an OPTICS cluster, expressed as an absolute number or a fraction of the " + "number of samples (rounded to be at least 2). If None, the value of min_samples is used instead. Used " + "only when cluster_method='xi'." + ), + [("float", ("NegativeInfinity", False), (0.5, False))], + ), + ( + "int >= 1", + ( + "Minimum number of samples in an OPTICS cluster, expressed as an absolute number or a fraction of the " + "number of samples (rounded to be at least 2). If None, the value of min_samples is used instead. Used " + "only when cluster_method='xi'." + ), + [("int", (1, True), ("Infinity", False))], + ), + ( + "int <= 10", + ( + "Minimum number of samples in an OPTICS cluster, expressed as an absolute number or a fraction of the " + "number of samples (rounded to be at least 2). If None, the value of min_samples is used instead. Used " + "only when cluster_method='xi'." + ), + [("int", ("NegativeInfinity", False), (10, True))], + ), + ("float ([0, 1])", "abc", [("float", (0.0, True), (1.0, True))]), + ( + "float", + ( + "The quantile that the model tries to predict. It must be strictly\nbetween 0 and 1." + " If 0.5 (default), the model predicts the 50%\nquantile, i.e. the median." + ), + [("float", (0.0, False), (1.0, False))], + ), + ( + "float", + "The quantile that the model tries to predict. It must be in [1e-2, 0.0].", + [("float", (0.0, True), (0.01, True))], + ), + ( + 'dict, list of dicts, "balanced", or None', + ( + "Weights associated with classes in the form {class_label: weight}. If not given, all classes are" + " supposed to have weight one. For multi-output problems, a list of dicts can be provided in the same" + " order as the columns of y.\n\nNote that for multioutput (including multilabel) weights should be" + " defined for each class of every column in its own dict. For example, for four-class multilabel" + " classification weights should be [{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}] instead of" + ' [{1:1}, {2:5}, {3:1}, {4:1}].\n\nThe "balanced" mode uses the values of y to automatically adjust' + " weights inversely proportional to class frequencies in the input data: n_samples / (n_classes *" + " np.bincount(y)).\n\nFor multi-output, the weights of each column of y will be multiplied." + ), + [], + ), + ( + "int, RandomState instance or None", + ( + "Controls the randomness of the estimator. The features are always randomly permuted at each split," + ' even if splitter is set to "best". When max_features < n_features, the algorithm will select' + " max_features at random at each split before finding the best split among them. But the best found" + " split may vary across different runs, even if max_features=n_features. That is the case, if the" + " improvement of the criterion is identical for several splits and one split has to be selected at" + " random. To obtain a deterministic behaviour during fitting, random_state has to be fixed to an" + " integer. See :term:Glossary for details." + ), + [], + ), + ( + "{'ovo', 'ovr'}", + ( + "Whether to return a one-vs-rest ('ovr') decision function of shape (n_samples, n_classes) as all other" + " classifiers, or the original one-vs-one ('ovo') decision function of libsvm which has shape" + " (n_samples, n_classes * (n_classes - 1) / 2). However, note that internally, one-vs-one ('ovo') is" + " always used as a multi-class strategy to train models; an ovr matrix is only constructed from the ovo" + " matrix. The parameter is ignored for binary classification.\n\n.. versionchanged: 0.19" + " decision_function_shape is 'ovr' by default.\n\n.. versionadded: 0.17 decision_function_shape='ovr'" + " is recommended.\n\n.. versionchanged: 0.17 Deprecated decision_function_shape='ovo' and None." + ), + [], + ), + ], +) +def test_extract_boundaries(type_string: str, description: str, expected_boundary: list[BoundaryValueType]) -> None: + expected = [ + BoundaryType(base_type=type_, min=min_[0], max=max_[0], min_inclusive=min_[1], max_inclusive=max_[1]) + for type_, min_, max_ in expected_boundary + ] + assert extract_boundary(description, type_string) == set(expected) diff --git a/tests/safeds_stubgen/api_analyzer/test_extract_valid_literals.py b/tests/safeds_stubgen/api_analyzer/test_extract_valid_literals.py new file mode 100644 index 00000000..fa527786 --- /dev/null +++ b/tests/safeds_stubgen/api_analyzer/test_extract_valid_literals.py @@ -0,0 +1,133 @@ +import pytest +from safeds_stubgen.api_analyzer._extract_valid_values import extract_valid_literals + +@pytest.mark.parametrize( + ("type_", "description", "expected_literals"), + [ + ( + "str", + ( + 'If "mean", then replace missing values using the mean along each column. ' + 'If "median", then replace missing values using the median along each column. ' + 'If "most_frequent", then replace missing using the most frequent value along each column. ' + 'If "constant", then replace missing values with fill_value.' + ), + ['"mean"', '"median"', '"most_frequent"', '"constant"'], + ), + ("str or bool", "Valid values are [False, None, 'allow-nan']", ["True", "False", "None", '"allow-nan"']), + ( + "str or bool", + "Valid values are [False, None, 'sparse matrix']", + ["True", "False", "None", '"sparse matrix"'], + ), + ( + "str or bool", + "Valid values are 'allow-nan', 'lbfgs', None and 'False'.", + ["None", "True", "False", '"lbfgs"', '"allow-nan"'], + ), + ( + "str or bool", + "If `'None'`, this parameter will use the default value.", + ["unlistable_str", "None", "True", "False"], + ), + ( + "str or bool", + "If `'alpha'` is a callable, the estimator will use it to calculate the value.", + ["unlistable_str", "True", "False"], + ), + ( + "str or bool", + "If False, the estimator will only use the default calculation.", + ["unlistable_str", "True", "False"], + ), + ("str, float", "If `float`, the value must be between 0 and 1.", ["unlistable_str"]), + ("str, float", "If `False`, the value must be between 0 and 1.", ["unlistable_str"]), + ("str, float", "If '123*mean', the value must be between 0 and 1.", ["unlistable_str"]), + ("'auto' or float", "If float, the value must be between 0 and 1.", ['"auto"']), + ("str or bool", "Valid values are 'auto', 'allow-nan', False.", ['"auto"', '"allow-nan"', "False", "True"]), + ( + "str, callable", + ( + "String inputs, 'absolute_error' and 'squared_error' are supported which\nfind the absolute error and" + " squared error per sample respectively.\n\nIf ``loss`` is a callable, then it should be a function" + " that takes\ntwo arrays as inputs, the true and predicted value and returns a 1-D\narray with the i-th" + " value of the array corresponding to the loss\non ``X[i]``." + ), + ['"absolute_error"', '"squared_error"'], + ), + ( + "str", + ( + "If 'mean', then replace missing values using the mean along each column." + "If 'median', then replace " + "missing values using the median along each column. If 'most_frequent', then replace missing using the " + "most frequent value along each column. If 'constant', then replace missing values with fill_value." + ), + ['"median"', '"most_frequent"', '"constant"', '"mean"'], + ), + ( + "str, list or tuple of str", + ( + 'Attribute name(s) given as string or a list/tuple of strings Eg.: ["coef_", "estimator_", ...],' + ' "coef_" If None, estimator is considered fitted if there exist an attribute that ends with a' + " underscore and does not start with double underscore." + ), + ["None", "unlistable_str"], + ), + ( + "bool or 'allow-nan'", + ( + "Whether to raise an error on np.inf, np.nan, pd.NA in X. This parameter does not influence whether y" + " can have np.inf, np.nan, pd.NA values. The possibilities are: \n\n\tTrue: Force all values of X to be" + " finite. \n\tFalse: accepts np.inf, np.nan, pd.NA in X. \n\t'allow-nan': accepts only np.nan or pd.NA" + " values in X. Values cannot be infinite. \n\n.. versionadded: 0.20 force_all_finite accepts the string" + " 'allow-nan'. \n\n.. versionchanged: 0.23 Accepts pd.NA and converts it into np.nan" + ), + ['"allow-nan"', "False", "True"], + ), + ( + '{"random", "best"}', + ( + 'The strategy used to choose the split at each node. Supported strategies are "best" to choose the best' + ' split and "random" to choose the best random split.' + ), + ['"best"', '"random"'], + ), + ( + "bool or str", + ( + "When set to True, change the display of 'values' and/or 'samples' to be proportions and percentages " + "respectively." + ), + ["False", "True", "unlistable_str"], + ), + ( + "int, RandomState instance or None", + ( + "Controls the randomness of the estimator. The features are always randomly permuted at each split," + ' even if splitter is set to "best". When max_features < n_features, the algorithm will select' + " max_features at random at each split before finding the best split among them. But the best found" + " split may vary across different runs, even if max_features=n_features. That is the case, if the" + " improvement of the criterion is identical for several splits and one split has to be selected at" + " random. To obtain a deterministic behaviour during fitting, random_state has to be fixed to an" + " integer. See :term:Glossary for details." + ), + ["None"], + ), + ("float", "Independent term in kernel function. It is only significant in 'poly' and 'sigmoid'.", []), + ( + "float", + ( + "When self.fit_intercept is True, instance vector x becomes [x, self.intercept_scaling], i.e. a" + ' "synthetic" feature with constant value equals to intercept_scaling is appended to the instance' + " vector. The intercept becomes intercept_scaling * synthetic feature weight Note! the synthetic" + " feature weight is subject to l1/l2 regularization as all other features. To lessen the effect of" + " regularization on synthetic feature weight (and therefore on the intercept) intercept_scaling has to" + " be increased." + ), + [], + ), + ], +) +def test_extract_values(type_: str, description: str, expected_literals: list) -> None: + assert extract_valid_literals(description, type_) == set(expected_literals) diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index 230a3b61..e69c6e3e 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -30,7 +30,7 @@ def test_correct_hash() -> None: is_optional=True, default_value="test_str", assigned_by=ParameterAssignment.POSITION_OR_NAME, - docstring=ParameterDocstring(None, "r", "r"), + docstring=ParameterDocstring(None, "r", "r", "r"), type=NamedType(name="str", qname=""), ) assert hash(parameter) == hash(deepcopy(parameter)) diff --git a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py index afcb18db..80c193af 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from griffe.enumerations import Parser +from griffe import Parser from mypy import nodes from safeds_stubgen.api_analyzer import ( @@ -126,6 +126,7 @@ def test_get_function_documentation( "p", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo. Defaults to 1.", ), @@ -136,6 +137,7 @@ def test_get_function_documentation( "missing", ParameterDocstring( type=None, + type_string="", default_value="", description="", ), @@ -146,6 +148,7 @@ def test_get_function_documentation( "no_type_no_default", ParameterDocstring( type=None, + type_string="", default_value="", description="no type and no default.", ), @@ -156,6 +159,7 @@ def test_get_function_documentation( "optional_type", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="optional type.", ), @@ -166,6 +170,7 @@ def test_get_function_documentation( "type_no_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="type but no default.", ), @@ -176,6 +181,7 @@ def test_get_function_documentation( "with_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo. Defaults to 2.", ), @@ -186,6 +192,7 @@ def test_get_function_documentation( "*args", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="()", description="foo: *args", ), @@ -199,6 +206,7 @@ def test_get_function_documentation( key_type=NamedType(name="str", qname="builtins.str"), value_type=NamedType(name="int", qname="builtins.int"), ), + type_string="dict", default_value="{}", description="foo: **kwargs", ), @@ -207,7 +215,7 @@ def test_get_function_documentation( "function_with_parameters", False, "missing", - ParameterDocstring(type=None, default_value="", description=""), + ParameterDocstring(type=None, type_string="", default_value="", description=""), ), ( "function_with_attributes_and_parameters", @@ -215,6 +223,7 @@ def test_get_function_documentation( "q", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo. Defaults to 2.", ), @@ -225,6 +234,7 @@ def test_get_function_documentation( "p", ParameterDocstring( type=None, + type_string="", default_value="", description="", ), @@ -239,37 +249,37 @@ def test_get_function_documentation( "ClassWithVariousParameterTypes", True, "optional_type", - ParameterDocstring(type=NamedType(name="int", qname="builtins.int")), + ParameterDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousParameterTypes", True, "none_type", - ParameterDocstring(type=NamedType(name="None", qname="builtins.None")), + ParameterDocstring(type=NamedType(name="None", qname="builtins.None"), type_string="None"), ), ( "ClassWithVariousParameterTypes", True, "int_type", - ParameterDocstring(type=NamedType(name="int", qname="builtins.int")), + ParameterDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousParameterTypes", True, "bool_type", - ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool")), + ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool"), type_string="bool"), ), ( "ClassWithVariousParameterTypes", True, "str_type", - ParameterDocstring(type=NamedType(name="str", qname="builtins.str")), + ParameterDocstring(type=NamedType(name="str", qname="builtins.str"), type_string="str"), ), ( "ClassWithVariousParameterTypes", True, "float_type", - ParameterDocstring(type=NamedType(name="float", qname="builtins.float")), + ParameterDocstring(type=NamedType(name="float", qname="builtins.float"), type_string="float"), ), ( "ClassWithVariousParameterTypes", @@ -279,19 +289,20 @@ def test_get_function_documentation( type=TupleType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="bool", qname="builtins.bool")], ), + type_string="(int, bool)", ), ), ( "ClassWithVariousParameterTypes", True, "list_type_1", - ParameterDocstring(type=ListType(types=[])), + ParameterDocstring(type=ListType(types=[]), type_string="list"), ), ( "ClassWithVariousParameterTypes", True, "list_type_2", - ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")]), type_string="list"), ), ( "ClassWithVariousParameterTypes", @@ -304,25 +315,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="list", ), ), ( "ClassWithVariousParameterTypes", True, "list_type_4", - ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="list"), ), ( "ClassWithVariousParameterTypes", True, "set_type_1", - ParameterDocstring(type=SetType(types=[])), + ParameterDocstring(type=SetType(types=[]), type_string="set"), ), ( "ClassWithVariousParameterTypes", True, "set_type_2", - ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")]), type_string="set"), ), ( "ClassWithVariousParameterTypes", @@ -335,25 +347,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="set" ), ), ( "ClassWithVariousParameterTypes", True, "set_type_4", - ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="set"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_1", - ParameterDocstring(type=TupleType(types=[])), + ParameterDocstring(type=TupleType(types=[]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_2", - ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", @@ -366,19 +379,20 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="tuple" ), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_4", - ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", True, "any_type", - ParameterDocstring(type=NamedType(name="Any", qname="typing.Any")), + ParameterDocstring(type=NamedType(name="Any", qname="typing.Any"), type_string="Any"), ), ( "ClassWithVariousParameterTypes", @@ -391,6 +405,7 @@ def test_get_function_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="Optional" ), ), ( @@ -402,6 +417,7 @@ def test_get_function_documentation( name="ClassWithAttributes", qname="tests.data.docstring_parser_package.googledoc.ClassWithAttributes", ), + type_string="ClassWithAttributes" ), ), ( @@ -413,6 +429,7 @@ def test_get_function_documentation( name="AnotherClass", qname="tests.data.various_modules_package.another_path.another_module.AnotherClass", ), + type_string="AnotherClass" ), ), ], @@ -496,6 +513,7 @@ def test_get_parameter_documentation( AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), description="foo. Defaults to 1.", + type_string="int" ), ), ( @@ -512,6 +530,7 @@ def test_get_parameter_documentation( AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), description="foo.", + type_string="int" ), ), ( @@ -522,32 +541,32 @@ def test_get_parameter_documentation( ( "ClassWithVariousAttributeTypes", "optional_type", - AttributeDocstring(type=NamedType(name="int", qname="builtins.int")), + AttributeDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousAttributeTypes", "none_type", - AttributeDocstring(type=NamedType(name="None", qname="builtins.None")), + AttributeDocstring(type=NamedType(name="None", qname="builtins.None"), type_string="None"), ), ( "ClassWithVariousAttributeTypes", "int_type", - AttributeDocstring(type=NamedType(name="int", qname="builtins.int")), + AttributeDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousAttributeTypes", "bool_type", - AttributeDocstring(type=NamedType(name="bool", qname="builtins.bool")), + AttributeDocstring(type=NamedType(name="bool", qname="builtins.bool"), type_string="bool"), ), ( "ClassWithVariousAttributeTypes", "str_type", - AttributeDocstring(type=NamedType(name="str", qname="builtins.str")), + AttributeDocstring(type=NamedType(name="str", qname="builtins.str"), type_string="str"), ), ( "ClassWithVariousAttributeTypes", "float_type", - AttributeDocstring(type=NamedType(name="float", qname="builtins.float")), + AttributeDocstring(type=NamedType(name="float", qname="builtins.float"), type_string="float"), ), ( "ClassWithVariousAttributeTypes", @@ -556,17 +575,18 @@ def test_get_parameter_documentation( type=TupleType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="bool", qname="builtins.bool")], ), + type_string="(int, bool)" ), ), ( "ClassWithVariousAttributeTypes", "list_type_1", - AttributeDocstring(type=ListType(types=[])), + AttributeDocstring(type=ListType(types=[]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", "list_type_2", - AttributeDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", @@ -578,22 +598,23 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="list" ), ), ( "ClassWithVariousAttributeTypes", "list_type_4", - AttributeDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", "set_type_1", - AttributeDocstring(type=SetType(types=[])), + AttributeDocstring(type=SetType(types=[]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", "set_type_2", - AttributeDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", @@ -605,22 +626,23 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="set" ), ), ( "ClassWithVariousAttributeTypes", "set_type_4", - AttributeDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", "tuple_type_1", - AttributeDocstring(type=TupleType(types=[])), + AttributeDocstring(type=TupleType(types=[]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", "tuple_type_2", - AttributeDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", @@ -632,17 +654,18 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="tuple" ), ), ( "ClassWithVariousAttributeTypes", "tuple_type_4", - AttributeDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", "any_type", - AttributeDocstring(type=NamedType(name="Any", qname="typing.Any")), + AttributeDocstring(type=NamedType(name="Any", qname="typing.Any"), type_string="Any"), ), ( "ClassWithVariousAttributeTypes", @@ -654,6 +677,7 @@ def test_get_parameter_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="Optional" ), ), ( @@ -664,6 +688,7 @@ def test_get_parameter_documentation( name="ClassWithAttributes", qname="tests.data.docstring_parser_package.googledoc.ClassWithAttributes", ), + type_string="ClassWithAttributes" ), ), ( @@ -674,6 +699,7 @@ def test_get_parameter_documentation( name="AnotherClass", qname="tests.data.various_modules_package.another_path.another_module.AnotherClass", ), + type_string="AnotherClass" ), ), ], diff --git a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py index 0f8b34ca..2b764284 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from griffe.enumerations import Parser +from griffe import Parser from mypy import nodes from safeds_stubgen.api_analyzer import ( @@ -127,6 +127,7 @@ def test_get_function_documentation( "p", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="1", description="foo", ), @@ -137,6 +138,7 @@ def test_get_function_documentation( "grouped_parameter_1", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="4", description="foo: grouped_parameter_1 and grouped_parameter_2", ), @@ -147,6 +149,7 @@ def test_get_function_documentation( "grouped_parameter_2", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="4", description="foo: grouped_parameter_1 and grouped_parameter_2", ), @@ -177,6 +180,7 @@ def test_get_function_documentation( "type_no_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo: type_no_default", ), @@ -187,6 +191,7 @@ def test_get_function_documentation( "optional_unknown_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo: optional_unknown_default", ), @@ -197,6 +202,7 @@ def test_get_function_documentation( "with_default_syntax_1", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="1", description="foo: with_default_syntax_1", ), @@ -207,6 +213,7 @@ def test_get_function_documentation( "with_default_syntax_2", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="2", description="foo: with_default_syntax_2", ), @@ -217,6 +224,7 @@ def test_get_function_documentation( "with_default_syntax_3", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="3", description="foo: with_default_syntax_3", ), @@ -227,6 +235,7 @@ def test_get_function_documentation( "grouped_parameter_1", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="4", description="foo: grouped_parameter_1 and grouped_parameter_2", ), @@ -237,6 +246,7 @@ def test_get_function_documentation( "grouped_parameter_2", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="4", description="foo: grouped_parameter_1 and grouped_parameter_2", ), @@ -247,6 +257,7 @@ def test_get_function_documentation( "*args", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="()", description="foo: *args", ), @@ -260,6 +271,7 @@ def test_get_function_documentation( key_type=NamedType(name="str", qname="builtins.str"), value_type=NamedType(name="int", qname="builtins.int"), ), + type_string="dict", default_value="{}", description="foo: **kwargs", ), @@ -276,6 +288,7 @@ def test_get_function_documentation( "x", ParameterDocstring( type=NamedType(name="str", qname="builtins.str"), + type_string="str", default_value="", description="Lorem ipsum 1.", ), @@ -286,6 +299,7 @@ def test_get_function_documentation( "y", ParameterDocstring( type=NamedType(name="str", qname="builtins.str"), + type_string="str", default_value="", description="Lorem ipsum 2.", ), @@ -296,6 +310,7 @@ def test_get_function_documentation( "z", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="5", description="Lorem ipsum 3.", ), @@ -306,6 +321,7 @@ def test_get_function_documentation( "x", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="1", description="foo", ), @@ -330,37 +346,37 @@ def test_get_function_documentation( "ClassWithVariousParameterTypes", True, "optional_type", - ParameterDocstring(type=NamedType(name="int", qname="builtins.int")), + ParameterDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousParameterTypes", True, "none_type", - ParameterDocstring(type=NamedType(name="None", qname="builtins.None")), + ParameterDocstring(type=NamedType(name="None", qname="builtins.None"), type_string="None"), ), ( "ClassWithVariousParameterTypes", True, "int_type", - ParameterDocstring(type=NamedType(name="int", qname="builtins.int")), + ParameterDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousParameterTypes", True, "bool_type", - ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool")), + ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool"), type_string="bool"), ), ( "ClassWithVariousParameterTypes", True, "str_type", - ParameterDocstring(type=NamedType(name="str", qname="builtins.str")), + ParameterDocstring(type=NamedType(name="str", qname="builtins.str"), type_string="str"), ), ( "ClassWithVariousParameterTypes", True, "float_type", - ParameterDocstring(type=NamedType(name="float", qname="builtins.float")), + ParameterDocstring(type=NamedType(name="float", qname="builtins.float"), type_string="float"), ), ( "ClassWithVariousParameterTypes", @@ -370,19 +386,20 @@ def test_get_function_documentation( type=TupleType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="bool", qname="builtins.bool")], ), + type_string="(int, bool)", ), ), ( "ClassWithVariousParameterTypes", True, "list_type_1", - ParameterDocstring(type=ListType(types=[])), + ParameterDocstring(type=ListType(types=[]), type_string="list"), ), ( "ClassWithVariousParameterTypes", True, "list_type_2", - ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")]), type_string="list"), ), ( "ClassWithVariousParameterTypes", @@ -395,25 +412,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="list" ), ), ( "ClassWithVariousParameterTypes", True, "list_type_4", - ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="list"), ), ( "ClassWithVariousParameterTypes", True, "set_type_1", - ParameterDocstring(type=SetType(types=[])), + ParameterDocstring(type=SetType(types=[]), type_string="set"), ), ( "ClassWithVariousParameterTypes", True, "set_type_2", - ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")]), type_string="set"), ), ( "ClassWithVariousParameterTypes", @@ -426,25 +444,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="set", ), ), ( "ClassWithVariousParameterTypes", True, "set_type_4", - ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="set"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_1", - ParameterDocstring(type=TupleType(types=[])), + ParameterDocstring(type=TupleType(types=[]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_2", - ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", @@ -457,19 +476,20 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="tuple" ), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_4", - ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", True, "any_type", - ParameterDocstring(type=NamedType(name="Any", qname="typing.Any")), + ParameterDocstring(type=NamedType(name="Any", qname="typing.Any"), type_string="Any"), ), ( "ClassWithVariousParameterTypes", @@ -482,6 +502,7 @@ def test_get_function_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="Optional" ), ), ( @@ -493,6 +514,7 @@ def test_get_function_documentation( name="ClassWithAttributes", qname="tests.data.docstring_parser_package.numpydoc.ClassWithAttributes", ), + type_string="ClassWithAttributes" ), ), ( @@ -504,6 +526,7 @@ def test_get_function_documentation( name="AnotherClass", qname="tests.data.various_modules_package.another_path.another_module.AnotherClass", ), + type_string="AnotherClass" ), ), ], @@ -603,6 +626,7 @@ def test_get_parameter_documentation( "type_no_default", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", description="foo: type_no_default", ), ), @@ -613,6 +637,7 @@ def test_get_parameter_documentation( type=UnionType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="None", qname="builtins.None")], ), + type_string="(int, optional)", description="foo: optional_unknown_default", ), ), @@ -621,6 +646,7 @@ def test_get_parameter_documentation( "with_default_syntax_1", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int, default 1", description="foo: with_default_syntax_1", ), ), @@ -629,6 +655,7 @@ def test_get_parameter_documentation( "with_default_syntax_2", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int, default: 2", description="foo: with_default_syntax_2", ), ), @@ -637,6 +664,7 @@ def test_get_parameter_documentation( "with_default_syntax_3", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int, default=3", description="foo: with_default_syntax_3", ), ), @@ -674,6 +702,7 @@ def test_get_parameter_documentation( "q", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int, default=1", description="foo", ), ), @@ -692,32 +721,33 @@ def test_get_parameter_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="(int, optional)" ), ), ( "ClassWithVariousAttributeTypes", "none_type", - AttributeDocstring(type=NamedType(name="None", qname="builtins.None")), + AttributeDocstring(type=NamedType(name="None", qname="builtins.None"), type_string="None"), ), ( "ClassWithVariousAttributeTypes", "int_type", - AttributeDocstring(type=NamedType(name="int", qname="builtins.int")), + AttributeDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousAttributeTypes", "bool_type", - AttributeDocstring(type=NamedType(name="bool", qname="builtins.bool")), + AttributeDocstring(type=NamedType(name="bool", qname="builtins.bool"), type_string="bool"), ), ( "ClassWithVariousAttributeTypes", "str_type", - AttributeDocstring(type=NamedType(name="str", qname="builtins.str")), + AttributeDocstring(type=NamedType(name="str", qname="builtins.str"), type_string="str"), ), ( "ClassWithVariousAttributeTypes", "float_type", - AttributeDocstring(type=NamedType(name="float", qname="builtins.float")), + AttributeDocstring(type=NamedType(name="float", qname="builtins.float"), type_string="float"), ), ( "ClassWithVariousAttributeTypes", @@ -726,17 +756,18 @@ def test_get_parameter_documentation( type=TupleType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="bool", qname="builtins.bool")], ), + type_string="(int, bool)" ), ), ( "ClassWithVariousAttributeTypes", "list_type_1", - AttributeDocstring(type=ListType(types=[])), + AttributeDocstring(type=ListType(types=[]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", "list_type_2", - AttributeDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", @@ -748,22 +779,23 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="list" ), ), ( "ClassWithVariousAttributeTypes", "list_type_4", - AttributeDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="list"), ), ( "ClassWithVariousAttributeTypes", "set_type_1", - AttributeDocstring(type=SetType(types=[])), + AttributeDocstring(type=SetType(types=[]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", "set_type_2", - AttributeDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", @@ -775,22 +807,23 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="set" ), ), ( "ClassWithVariousAttributeTypes", "set_type_4", - AttributeDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="set"), ), ( "ClassWithVariousAttributeTypes", "tuple_type_1", - AttributeDocstring(type=TupleType(types=[])), + AttributeDocstring(type=TupleType(types=[]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", "tuple_type_2", - AttributeDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")])), + AttributeDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", @@ -802,17 +835,18 @@ def test_get_parameter_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="tuple" ), ), ( "ClassWithVariousAttributeTypes", "tuple_type_4", - AttributeDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + AttributeDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="tuple"), ), ( "ClassWithVariousAttributeTypes", "any_type", - AttributeDocstring(type=NamedType(name="Any", qname="typing.Any")), + AttributeDocstring(type=NamedType(name="Any", qname="typing.Any"), type_string="Any"), ), ( "ClassWithVariousAttributeTypes", @@ -824,6 +858,7 @@ def test_get_parameter_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="Optional", ), ), ( @@ -834,6 +869,7 @@ def test_get_parameter_documentation( name="ClassWithAttributes", qname="tests.data.docstring_parser_package.numpydoc.ClassWithAttributes", ), + type_string="ClassWithAttributes", ), ), ( @@ -844,6 +880,7 @@ def test_get_parameter_documentation( name="AnotherClass", qname="tests.data.various_modules_package.another_path.another_module.AnotherClass", ), + type_string="AnotherClass", ), ), ( @@ -851,6 +888,7 @@ def test_get_parameter_documentation( "x", AttributeDocstring( type=NamedType(name="str", qname="builtins.str"), + type_string="str", description="Lorem ipsum 1.", ), ), @@ -859,6 +897,7 @@ def test_get_parameter_documentation( "y", AttributeDocstring( type=NamedType(name="str", qname="builtins.str"), + type_string="str", description="Lorem ipsum 2.", ), ), @@ -867,6 +906,7 @@ def test_get_parameter_documentation( "z", AttributeDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int, default=5", description="Lorem ipsum 3.", ), ), diff --git a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py index 021316bd..7377b11e 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from griffe.enumerations import Parser +from griffe import Parser from mypy import nodes from safeds_stubgen.api_analyzer import ( @@ -123,6 +123,7 @@ def test_get_function_documentation( "p", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo defaults to 1", ), @@ -155,6 +156,7 @@ def test_get_function_documentation( type=UnionType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="None", qname="builtins.None")], ), + type_string="int, optional", default_value="", description="optional type", ), @@ -165,6 +167,7 @@ def test_get_function_documentation( "type_no_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="type but no default", ), @@ -175,6 +178,7 @@ def test_get_function_documentation( "with_default", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="", description="foo that defaults to 2", ), @@ -185,6 +189,7 @@ def test_get_function_documentation( "*args", ParameterDocstring( type=NamedType(name="int", qname="builtins.int"), + type_string="int", default_value="()", description="foo: *args", ), @@ -198,6 +203,7 @@ def test_get_function_documentation( key_type=NamedType(name="str", qname="builtins.str"), value_type=NamedType(name="int", qname="builtins.int"), ), + type_string="dict[str, int]", default_value="{}", description="foo: **kwargs", ), @@ -225,37 +231,38 @@ def test_get_function_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="int, optional" ), ), ( "ClassWithVariousParameterTypes", True, "none_type", - ParameterDocstring(type=NamedType(name="None", qname="builtins.None")), + ParameterDocstring(type=NamedType(name="None", qname="builtins.None"), type_string="None"), ), ( "ClassWithVariousParameterTypes", True, "int_type", - ParameterDocstring(type=NamedType(name="int", qname="builtins.int")), + ParameterDocstring(type=NamedType(name="int", qname="builtins.int"), type_string="int"), ), ( "ClassWithVariousParameterTypes", True, "bool_type", - ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool")), + ParameterDocstring(type=NamedType(name="bool", qname="builtins.bool"), type_string="bool"), ), ( "ClassWithVariousParameterTypes", True, "str_type", - ParameterDocstring(type=NamedType(name="str", qname="builtins.str")), + ParameterDocstring(type=NamedType(name="str", qname="builtins.str"), type_string="str"), ), ( "ClassWithVariousParameterTypes", True, "float_type", - ParameterDocstring(type=NamedType(name="float", qname="builtins.float")), + ParameterDocstring(type=NamedType(name="float", qname="builtins.float"), type_string="float"), ), ( "ClassWithVariousParameterTypes", @@ -265,19 +272,20 @@ def test_get_function_documentation( type=TupleType( types=[NamedType(name="int", qname="builtins.int"), NamedType(name="bool", qname="builtins.bool")], ), + type_string="int, bool", ), ), ( "ClassWithVariousParameterTypes", True, "list_type_1", - ParameterDocstring(type=ListType(types=[])), + ParameterDocstring(type=ListType(types=[]), type_string="list"), ), ( "ClassWithVariousParameterTypes", True, "list_type_2", - ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=ListType(types=[NamedType(name="str", qname="builtins.str")]), type_string="list[str]"), ), ( "ClassWithVariousParameterTypes", @@ -290,25 +298,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="list[int, bool]" ), ), ( "ClassWithVariousParameterTypes", True, "list_type_4", - ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=ListType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="list[list[int]]"), ), ( "ClassWithVariousParameterTypes", True, "set_type_1", - ParameterDocstring(type=SetType(types=[])), + ParameterDocstring(type=SetType(types=[]), type_string="set"), ), ( "ClassWithVariousParameterTypes", True, "set_type_2", - ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=SetType(types=[NamedType(name="str", qname="builtins.str")]), type_string="set[str]"), ), ( "ClassWithVariousParameterTypes", @@ -321,25 +330,26 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="set[int, bool]", ), ), ( "ClassWithVariousParameterTypes", True, "set_type_4", - ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=SetType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="set[list[int]]"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_1", - ParameterDocstring(type=TupleType(types=[])), + ParameterDocstring(type=TupleType(types=[]), type_string="tuple"), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_2", - ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")])), + ParameterDocstring(type=TupleType(types=[NamedType(name="str", qname="builtins.str")]), type_string="tuple[str]"), ), ( "ClassWithVariousParameterTypes", @@ -352,19 +362,20 @@ def test_get_function_documentation( NamedType(name="bool", qname="builtins.bool"), ], ), + type_string="tuple[int, bool]" ), ), ( "ClassWithVariousParameterTypes", True, "tuple_type_4", - ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])])), + ParameterDocstring(type=TupleType(types=[ListType(types=[NamedType(name="int", qname="builtins.int")])]), type_string="tuple[list[int]]"), ), ( "ClassWithVariousParameterTypes", True, "any_type", - ParameterDocstring(type=NamedType(name="Any", qname="typing.Any")), + ParameterDocstring(type=NamedType(name="Any", qname="typing.Any"), type_string="Any"), ), ( "ClassWithVariousParameterTypes", @@ -377,6 +388,7 @@ def test_get_function_documentation( NamedType(name="None", qname="builtins.None"), ], ), + type_string="Optional" ), ), ( @@ -388,6 +400,7 @@ def test_get_function_documentation( name="ClassWithMethod", qname="tests.data.docstring_parser_package.restdoc.ClassWithMethod", ), + type_string="ClassWithMethod" ), ), ( @@ -399,6 +412,7 @@ def test_get_function_documentation( name="AnotherClass", qname="tests.data.various_modules_package.another_path.another_module.AnotherClass", ), + type_string="AnotherClass" ), ), ], diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub index 319c2b00..4b30c20e 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub @@ -1,7 +1,6 @@ @PythonModule("tests.data.various_modules_package.function_module") package tests.data.variousModulesPackage.functionModule -from numpy import ndarray from tests.data.mainPackage.anotherPath.anotherModule import AnotherClass // TODO Result type information missing. @@ -346,13 +345,13 @@ fun typeAliasParam( @Pure @PythonName("alias_subclass_result_type") -fun aliasSubclassResultType() -> result1: union +fun aliasSubclassResultType() -> result1: union> // TODO Result type information missing. @Pure @PythonName("alias_subclass_param_type") fun aliasSubclassParamType( - x: union + x: union> ) // TODO Some parameter have no type information. diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub index e3a67c14..e255041f 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub @@ -46,7 +46,7 @@ class InferMyTypes( @PythonName("tuple_") tuple ) - @Pure + @Impure([FileRead: StringLiteral.no path]) @PythonName("infer_function") static fun inferFunction( @PythonName("infer_param") inferParam: Int = 1, diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[googledoc-GOOGLE].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[googledoc-GOOGLE].sdsstub index 3846e8fe..b1d7a11d 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[googledoc-GOOGLE].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[googledoc-GOOGLE].sdsstub @@ -250,6 +250,7 @@ class ClassWithVariousParameterTypes( @PythonName("bool_type") boolType: Boolean, @PythonName("str_type") strType: String, @PythonName("float_type") floatType: Float, + @PythonName("byte_type") byteType: bytes, @PythonName("multiple_types") multipleTypes: Tuple, @PythonName("list_type_1") listType1: List, @PythonName("list_type_2") listType2: List, diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[numpydoc-NUMPYDOC].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[numpydoc-NUMPYDOC].sdsstub index 94d6886a..69da1e9a 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[numpydoc-NUMPYDOC].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[numpydoc-NUMPYDOC].sdsstub @@ -377,6 +377,7 @@ class ClassWithVariousParameterTypes( @PythonName("bool_type") boolType: Boolean, @PythonName("str_type") strType: String, @PythonName("float_type") floatType: Float, + @PythonName("byte_type") byteType: bytes, @PythonName("multiple_types") multipleTypes: Tuple, @PythonName("list_type_1") listType1: List, @PythonName("list_type_2") listType2: List, diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[restdoc-REST].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[restdoc-REST].sdsstub index aa8939ca..748762da 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[restdoc-REST].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_stub_docstring_creation[restdoc-REST].sdsstub @@ -191,6 +191,7 @@ class ClassWithVariousParameterTypes( @PythonName("bool_type") boolType: Boolean, @PythonName("str_type") strType: String, @PythonName("float_type") floatType: Float, + @PythonName("byte_type") byteType: bytes, @PythonName("multiple_types") multipleTypes: Tuple, @PythonName("list_type_1") listType1: List, @PythonName("list_type_2") listType2: List, diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index c13a6e37..45591931 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING import pytest - +from safeds_stubgen.api_analyzer.purity_analysis._infer_purity import get_purity_results from safeds_stubgen.api_analyzer import TypeSourcePreference, TypeSourceWarning, get_api from safeds_stubgen.docstring_parsing import DocstringStyle from safeds_stubgen.stubs_generator import NamingConvention, StubsStringGenerator, create_stub_files, generate_stub_data @@ -15,7 +15,7 @@ if TYPE_CHECKING: from collections.abc import Generator - from syrupy import SnapshotAssertion + from syrupy.assertion import SnapshotAssertion # Setup - Run API to create stub files @@ -29,7 +29,8 @@ _docstring_package_dir = Path(_lib_dir / "data" / _docstring_package_name) api = get_api(_test_package_dir, is_test_run=True) -stubs_generator = StubsStringGenerator(api=api, convert_identifiers=True) +purity_api = get_purity_results(_test_package_dir, api_data=api, test_run=True) +stubs_generator = StubsStringGenerator(api=api, purity_api=purity_api, convert_identifiers=True) stubs_data = generate_stub_data(stubs_generator=stubs_generator, out_path=_out_dir) @@ -168,7 +169,8 @@ def test_stub_docstring_creation( type_source_preference=type_source_preference, type_source_warning=type_source_warning, ) - docstring_stubs_generator = StubsStringGenerator(api=docstring_api, convert_identifiers=True) + purity_api = get_purity_results(_docstring_package_dir, api_data=docstring_api, test_run=True) + docstring_stubs_generator = StubsStringGenerator(api=docstring_api, purity_api=purity_api, convert_identifiers=True) docstring_stubs_data = generate_stub_data(stubs_generator=docstring_stubs_generator, out_path=_out_dir) for stub_text in docstring_stubs_data: diff --git a/tests/safeds_stubgen/test_main.py b/tests/safeds_stubgen/test_main.py index 756e20a1..55aecd3f 100644 --- a/tests/safeds_stubgen/test_main.py +++ b/tests/safeds_stubgen/test_main.py @@ -3,41 +3,85 @@ from pathlib import Path import pytest -from syrupy import SnapshotAssertion - from safeds_stubgen.main import main +from syrupy.assertion import SnapshotAssertion _lib_dir = Path(__file__).parent.parent.parent _test_package_name = "main_package" +_test_package_name_boundaries_valid_values_numpydoc = "boundary_enum_package_numpydoc" +_test_package_name_boundaries_valid_values_googledoc = "boundary_enum_package_googledoc" +_test_package_name_boundaries_valid_values_restdoc = "boundary_enum_package_restdoc" +_test_package_purity = "purity_package" _main_dir = Path(_lib_dir / "src" / "main.py") _test_package_dir = Path(_lib_dir / "tests" / "data" / _test_package_name) _out_dir = Path(_lib_dir / "tests" / "data" / "out") -_out_file_dir = Path(_out_dir / f"{_test_package_name}__api.json") - -def test_main(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + ("test_package_name", "out_file_dir", "docstyle"), + [ + ( + _test_package_name, + Path(_out_dir / f"{_test_package_name}__api.json"), + "plaintext" + ), + ( + _test_package_name_boundaries_valid_values_numpydoc, + Path(_out_dir / f"{_test_package_name_boundaries_valid_values_numpydoc}__api.json"), + "numpydoc" + ), + ( + _test_package_name_boundaries_valid_values_googledoc, + Path(_out_dir / f"{_test_package_name_boundaries_valid_values_googledoc}__api.json"), + "google" + ), + ( + _test_package_name_boundaries_valid_values_restdoc, + Path(_out_dir / f"{_test_package_name_boundaries_valid_values_restdoc}__api.json"), + "rest" + ), + ( + _test_package_purity, + Path(_out_dir / f"{_test_package_purity}__api_purity.json"), + "numpydoc" + ), + ], + ids=[ + "test_plaintext", + "test_numpydoc_with_boundary_enum_extractor", + "test_googledoc_with_boundary_enum_extractor", + "test_restdoc_with_boundary_enum_extractor", + "test_purity_analysis" + ], +) +def test_main( + test_package_name: str, + out_file_dir: Path, + docstyle: str, + snapshot: SnapshotAssertion +) -> None: # Overwrite system arguments + sys.argv = [ str(_main_dir), "-v", "-s", - str(_test_package_dir), + str(Path(_lib_dir / "tests" / "data" / test_package_name)), "-o", str(_out_dir), "-tr", "--docstyle", - "plaintext", + docstyle, "-nc", + # "-old" # uncomment to run stelligkeitsbasierte purity analysis ] main() - with Path.open(_out_file_dir, encoding="utf-8") as f: + with Path.open(out_file_dir, encoding="utf-8") as f: json_data = json.load(f) assert json_data == snapshot - def test_main_empty() -> None: # Overwrite system arguments sys.argv = [