diff --git a/news/2709.refactor.md b/news/2709.refactor.md new file mode 100644 index 0000000000..f7f186ff74 --- /dev/null +++ b/news/2709.refactor.md @@ -0,0 +1 @@ +Switch to `httpx.Client` for HTTP requests, drop `requests` dependency. diff --git a/pdm.lock b/pdm.lock index dd1a4ca981..5e0cf21200 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,23 @@ groups = ["default", "all", "doc", "pytest", "test", "tox", "workflow"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:4a83de59b2dba724dc70f19f822ddcc9e230f4f450652a91e6593bad219522c1" +content_hash = "sha256:4913ba83e2aba4315d326a33dbfd713c3a506ee6a4a5e14af25b345946e6133b" + +[[package]] +name = "anyio" +version = "3.7.1" +requires_python = ">=3.7" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["all", "default", "test"] +dependencies = [ + "exceptiongroup; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", +] +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] [[package]] name = "arpeggio" @@ -66,37 +82,6 @@ files = [ {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, ] -[[package]] -name = "cachecontrol" -version = "0.13.1" -requires_python = ">=3.7" -summary = "httplib2 caching for requests" -groups = ["all", "default", "test"] -dependencies = [ - "msgpack>=0.5.2", - "requests>=2.16.0", -] -files = [ - {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, - {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, -] - -[[package]] -name = "cachecontrol" -version = "0.13.1" -extras = ["filecache"] -requires_python = ">=3.7" -summary = "httplib2 caching for requests" -groups = ["all", "default", "test"] -dependencies = [ - "cachecontrol==0.13.1", - "filelock>=3.8.0", -] -files = [ - {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, - {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, -] - [[package]] name = "cachetools" version = "5.3.1" @@ -211,7 +196,7 @@ name = "charset-normalizer" version = "3.1.0" requires_python = ">=3.7.0" summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["all", "default", "doc", "test"] +groups = ["all", "doc"] files = [ {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, @@ -603,7 +588,7 @@ name = "exceptiongroup" version = "1.1.1" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" -groups = ["pytest", "test"] +groups = ["all", "default", "pytest", "test"] marker = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, @@ -683,6 +668,65 @@ files = [ {file = "griffe-0.29.0.tar.gz", hash = "sha256:6fc892aaa251b3761e3a8d2f5893758e1850ec5d81d4605c4557be0666202a0b"}, ] +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["all", "default", "test"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "hishel" +version = "0.0.24" +requires_python = ">=3.8" +summary = "Persistent cache implementation for httpx and httpcore" +groups = ["all", "default", "test"] +dependencies = [ + "httpx>=0.22.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "hishel-0.0.24-py3-none-any.whl", hash = "sha256:8b6e43481485e1938d78bd35c0bcb38646fe8f2e090fedb64b4dc1d6015ffe49"}, + {file = "hishel-0.0.24.tar.gz", hash = "sha256:4ac494c6bfedc431e480ab85d3435d4710230b2ad6092766b6ccf82b1d7e4152"}, +] + +[[package]] +name = "httpcore" +version = "1.0.4" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["all", "default", "test"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, +] + +[[package]] +name = "httpx" +version = "0.27.0" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["all", "default", "test"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + [[package]] name = "idna" version = "3.4" @@ -1146,73 +1190,67 @@ files = [ [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.8" +requires_python = ">=3.8" summary = "MessagePack serializer" groups = ["all", "default", "test"] files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] @@ -1803,7 +1841,7 @@ name = "requests" version = "2.31.0" requires_python = ">=3.7" summary = "Python HTTP for Humans." -groups = ["all", "default", "doc", "test"] +groups = ["all", "doc"] dependencies = [ "certifi>=2017.4.17", "charset-normalizer<4,>=2", @@ -1815,20 +1853,6 @@ files = [ {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "A utility belt for advanced users of python-requests" -groups = ["all", "default", "test"] -dependencies = [ - "requests<3.0.0,>=2.0.1", -] -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - [[package]] name = "resolvelib" version = "1.0.1" @@ -1904,6 +1928,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["all", "default", "test"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "text-unidecode" version = "1.3" @@ -2008,28 +2043,28 @@ files = [ [[package]] name = "typing-extensions" -version = "4.7.1" -requires_python = ">=3.7" -summary = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.10.0" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" groups = ["all", "default", "doc", "test", "workflow"] files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "unearth" -version = "0.14.0" +version = "0.15.0" requires_python = ">=3.8" summary = "A utility to fetch and download python packages" groups = ["all", "default", "test"] dependencies = [ + "httpx<1,>=0.27.0", "packaging>=20", - "requests>=2.25", ] files = [ - {file = "unearth-0.14.0-py3-none-any.whl", hash = "sha256:a2b937ca22198043f5360192bce38708f11ddc5d4cdea973ee38583219b97d5d"}, - {file = "unearth-0.14.0.tar.gz", hash = "sha256:f3cddfb94ac0f865fbcf964231556ef7183010379c00b01205517a50c78a186d"}, + {file = "unearth-0.15.0-py3-none-any.whl", hash = "sha256:1ca3e5108196059d9a28f010918c849f4ab403ba2b07d8869fbe3deb3027e83c"}, + {file = "unearth-0.15.0.tar.gz", hash = "sha256:95f4764dab61f6bb637427934fa543b3610a38908760c06014fb861dcca29458"}, ] [[package]] @@ -2037,7 +2072,7 @@ name = "urllib3" version = "2.0.3" requires_python = ">=3.7" summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["all", "default", "doc", "test"] +groups = ["all", "doc"] files = [ {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, diff --git a/pyproject.toml b/pyproject.toml index 6120d17d05..7b05d2af70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,7 @@ dependencies = [ "rich>=12.3.0", "virtualenv>=20", "pyproject-hooks", - "requests-toolbelt", - "unearth>=0.14.0", + "unearth>=0.15.0", "dep-logic>=0.2.0,<1.0", "findpython>=0.4.0,<1.0.0a0", "tomlkit>=0.11.1,<1", @@ -30,11 +29,12 @@ dependencies = [ "python-dotenv>=0.15", "resolvelib>=1.0.1", "installer<0.8,>=0.7", - "cachecontrol[filecache]>=0.13.0", "truststore; python_version >= \"3.10\"", "tomli>=1.1.0; python_version < \"3.11\"", "importlib-resources>=5; python_version < \"3.9\"", "importlib-metadata>=3.6; python_version < \"3.10\"", + "hishel>=0.0.24,<0.1.0", + "msgpack>=1.0", ] readme = "README.md" keywords = ["packaging", "dependency", "workflow"] diff --git a/src/pdm/cli/commands/publish/__init__.py b/src/pdm/cli/commands/publish/__init__.py index 551e4a8ae1..40c2fa8394 100644 --- a/src/pdm/cli/commands/publish/__init__.py +++ b/src/pdm/cli/commands/publish/__init__.py @@ -14,7 +14,7 @@ from pdm.termui import logger if TYPE_CHECKING: - from requests import Response + from httpx import Response from pdm.project import Project @@ -90,7 +90,7 @@ def _make_package(filename: str, signatures: dict[str, str], options: argparse.N @staticmethod def _skip_upload(response: Response) -> bool: status = response.status_code - reason = response.reason.lower() + reason = response.reason_phrase.lower() text = response.text.lower() # Borrowed from https://github.com/pypa/twine/blob/main/twine/commands/upload.py#L149 @@ -109,21 +109,21 @@ def _skip_upload(response: Response) -> bool: @staticmethod def _check_response(response: Response) -> None: - import requests + import httpx message = "" - if response.status_code == 410 and "pypi.python.org" in response.url: + if response.status_code == 410 and "pypi.python.org" in str(response.url): message = ( "Uploading to these sites is deprecated. " "Try using https://upload.pypi.org/legacy/ " "(or https://test.pypi.org/legacy/) instead." ) - elif response.status_code == 405 and "pypi.org" in response.url: + elif response.status_code == 405 and "pypi.org" in str(response.url): message = "It appears you're trying to upload to pypi.org but have an invalid URL." else: try: response.raise_for_status() - except requests.HTTPError as err: + except httpx.HTTPStatusError as err: message = str(err) if response.text: logger.debug(response.text) @@ -149,7 +149,7 @@ def get_repository(project: Project, options: argparse.Namespace) -> Repository: config.ca_certs = ca_certs if options.verify_ssl is False: config.verify_ssl = options.verify_ssl - return Repository(project, config.url, config.username, config.password, config.ca_certs, config.verify_ssl) + return Repository(project, config) def handle(self, project: Project, options: argparse.Namespace) -> None: hooks = HookManager(project, options.skip) @@ -172,7 +172,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: ) for package in packages: resp = repository.upload(package) - logger.debug("Response from %s:\n%s %s", resp.url, resp.status_code, resp.reason) + logger.debug("Response from %s:\n%s %s", resp.url, resp.status_code, resp.reason_phrase) if options.skip_existing and self._skip_upload(resp): project.core.ui.warn(f"Skipping {package.base_filename} because it appears to already exist") diff --git a/src/pdm/cli/commands/publish/repository.py b/src/pdm/cli/commands/publish/repository.py index 6d05fca20d..3f24b51d79 100644 --- a/src/pdm/cli/commands/publish/repository.py +++ b/src/pdm/cli/commands/publish/repository.py @@ -1,10 +1,10 @@ from __future__ import annotations import os -import pathlib -from typing import TYPE_CHECKING, Any, Iterable +from typing import TYPE_CHECKING, Any, Iterable, cast from urllib.parse import urlparse, urlunparse +import httpx from rich.progress import BarColumn, DownloadColumn, TimeRemainingColumn, TransferSpeedColumn from pdm import termui @@ -12,30 +12,39 @@ from pdm.exceptions import PdmUsageError from pdm.project import Project from pdm.project.config import DEFAULT_REPOSITORIES +from pdm.utils import get_trusted_hosts if TYPE_CHECKING: - from requests import Response + from typing import Callable, Self + + from httpx import Response + from httpx._multipart import MultipartStream + + from pdm._types import RepositoryConfig + + +class CallbackWrapperStream(httpx.SyncByteStream): + def __init__(self, stream: httpx.SyncByteStream, callback: Callable[[Self], Any]) -> None: + self._stream = stream + self._callback = callback + self.bytes_read = 0 + + def __iter__(self) -> Iterable[bytes]: + for chunk in self._stream: + self.bytes_read += len(chunk) + self._callback(self) + yield chunk class Repository: - def __init__( - self, - project: Project, - url: str, - username: str | None, - password: str | None, - ca_certs: str | None, - verify_ssl: bool | None = True, - ) -> None: - self.url = url - self.session = project.environment.session - if verify_ssl is False: - self.session.verify = verify_ssl - elif ca_certs is not None: - self.session.set_ca_certificates(pathlib.Path(ca_certs)) + def __init__(self, project: Project, config: RepositoryConfig) -> None: + self.url = cast(str, config.url) + trusted_hosts = get_trusted_hosts([config]) + self.session = project.environment._build_session(trusted_hosts, verify=config.ca_certs) + self._credentials_to_save: tuple[str, str, str] | None = None self.ui = project.core.ui - username, password = self._ensure_credentials(username, password) + username, password = self._ensure_credentials(config.username, config.password) self.session.auth = (username, password) def _ensure_credentials(self, username: str | None, password: str | None) -> tuple[str, str]: @@ -69,7 +78,7 @@ def _get_pypi_token_via_oidc(self) -> str | None: if not ACTIONS_ID_TOKEN_REQUEST_TOKEN or not ACTIONS_ID_TOKEN_REQUEST_URL: return None self.ui.echo("Getting PyPI token via GitHub Actions OIDC...") - import requests + import httpx try: parsed_url = urlparse(self.url) @@ -89,7 +98,7 @@ def _get_pypi_token_via_oidc(self) -> str | None: resp = self.session.post(mint_token_url, json={"token": oidc_token}) resp.raise_for_status() token = resp.json()["token"] - except requests.RequestException: + except httpx.HTTPError: self.ui.echo("Failed to get PyPI token via GitHub Actions OIDC", err=True) return None else: @@ -137,16 +146,13 @@ def get_release_urls(self, packages: list[PackageFile]) -> Iterable[str]: return {f"{base}project/{package.metadata['name']}/{package.metadata['version']}/" for package in packages} def upload(self, package: PackageFile) -> Response: - import requests_toolbelt - - payload = package.metadata_dict - payload.update( + data_fields = package.metadata_dict + data_fields.update( { ":action": "file_upload", "protocol_version": "1", } ) - field_parts = self._convert_to_list_of_tuples(payload) with self.ui.make_progress( " [progress.percentage]{task.percentage:>3.0f}%", BarColumn(), @@ -162,20 +168,18 @@ def upload(self, package: PackageFile) -> Response: progress.console.print(f"Uploading [success]{package.base_filename}") with open(package.filename, "rb") as fp: - field_parts.append(("content", (package.base_filename, fp, "application/octet-stream"))) + file_fields = [("content", (package.base_filename, fp, "application/octet-stream"))] - def on_upload(monitor: requests_toolbelt.MultipartEncoderMonitor) -> None: + def on_upload(monitor: CallbackWrapperStream) -> None: progress.update(job, completed=monitor.bytes_read) - monitor = requests_toolbelt.MultipartEncoderMonitor.from_fields(field_parts, callback=on_upload) - job = progress.add_task("", total=monitor.len) - resp = self.session.post( - self.url, - data=monitor, - headers={"Content-Type": monitor.content_type}, - allow_redirects=False, - ) - if resp.status_code < 400 and self._credentials_to_save is not None: + request = self.session.build_request("POST", self.url, data=data_fields, files=file_fields) + stream = cast("MultipartStream", request.stream) + request.stream = CallbackWrapperStream(stream, on_upload) + + job = progress.add_task("", total=stream.get_content_length()) + resp = self.session.send(request, follow_redirects=False) + if not resp.is_error and self._credentials_to_save is not None: self._save_credentials(*self._credentials_to_save) self._credentials_to_save = None return resp diff --git a/src/pdm/environments/base.py b/src/pdm/environments/base.py index 42caeb341a..e51db01714 100644 --- a/src/pdm/environments/base.py +++ b/src/pdm/environments/base.py @@ -12,8 +12,7 @@ from contextlib import contextmanager from functools import cached_property, partial from pathlib import Path -from threading import local -from typing import TYPE_CHECKING, Generator, cast, no_type_check +from typing import TYPE_CHECKING, Generator, Mapping, no_type_check from pdm.exceptions import BuildError, PdmUsageError from pdm.models.in_process import get_pep508_environment, get_python_abis, get_uname, sysconfig_get_platform @@ -23,9 +22,11 @@ if TYPE_CHECKING: import unearth + from httpx import BaseTransport + from httpx._types import CertTypes, VerifyTypes from pdm._types import RepositoryConfig - from pdm.models.session import PDMSession + from pdm.models.session import PDMPyPIClient from pdm.project import Project @@ -72,8 +73,6 @@ def __init__(self, project: Project, *, python: str | None = None) -> None: else: self._interpreter = PythonInfo.from_path(python) - self._local_cache = local() - @property def is_global(self) -> bool: """For backward compatibility, it is opposite to ``is_local``.""" @@ -112,34 +111,48 @@ def target_python(self) -> unearth.TargetPython: tp.supported_tags() return tp - def _build_session(self) -> PDMSession: - from pdm.models.session import PDMSession + def _build_session( + self, + trusted_hosts: list[str] | None = None, + verify: VerifyTypes | None = None, + cert: CertTypes | None = None, + mounts: Mapping[str, BaseTransport | None] | None = None, + ) -> PDMPyPIClient: + from pdm.models.session import PDMPyPIClient - ca_certs = self.project.config.get("pypi.ca_certs") - trusted_hosts = get_trusted_hosts(self.project.sources) - session = PDMSession( - cache_dir=self.project.cache("http"), - trusted_hosts=trusted_hosts, - ca_certificates=Path(ca_certs) if ca_certs is not None else None, - timeout=self.project.config["request_timeout"], - ) - certfn = self.project.config.get("pypi.client_cert") - if certfn: - keyfn = self.project.config.get("pypi.client_key") - session.cert = (Path(certfn), Path(keyfn) if keyfn else None) + if trusted_hosts is None: + trusted_hosts = get_trusted_hosts(self.project.sources) - session.auth = self.auth + if verify is None: + verify = self.project.config.get("pypi.ca_certs") + + if cert is None: + certfn = self.project.config.get("pypi.client_cert") + keyfn = self.project.config.get("pypi.client_key") + if certfn: + cert = (certfn, keyfn) + + session_args = { + "cache_dir": self.project.cache("http"), + "trusted_hosts": trusted_hosts, + "timeout": self.project.config["request_timeout"], + "auth": self.auth, + } + if verify is not None: + session_args["verify"] = verify + if cert is not None: + session_args["cert"] = cert + if mounts: + session_args["mounts"] = mounts + + session = PDMPyPIClient(**session_args) self.project.core.exit_stack.callback(session.close) return session - @property - def session(self) -> PDMSession: - """Build the session and cache it in the thread local storage.""" - sess = getattr(self._local_cache, "session", None) - if sess is None: - sess = self._build_session() - self._local_cache.session = sess - return cast("PDMSession", sess) + @cached_property + def session(self) -> PDMPyPIClient: + """Build the session and cache it.""" + return self._build_session() @contextmanager def _patch_target_python(self) -> Generator[None, None, None]: diff --git a/src/pdm/models/caches.py b/src/pdm/models/caches.py index 14640c508c..a177f95eee 100644 --- a/src/pdm/models/caches.py +++ b/src/pdm/models/caches.py @@ -8,10 +8,9 @@ import stat from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Generic, Iterable, TypeVar, cast +from typing import TYPE_CHECKING, Generic, Iterable, TypeVar -from cachecontrol.cache import SeparateBodyBaseCache -from cachecontrol.caches import FileCache +import httpx from packaging.utils import canonicalize_name, parse_wheel_filename from pdm._types import CandidateInfo @@ -23,7 +22,6 @@ if TYPE_CHECKING: from packaging.tags import Tag - from requests import Session from unearth import Link, TargetPython from pdm.environments import BaseEnvironment @@ -124,21 +122,21 @@ class HashCache: def __init__(self, directory: Path | str) -> None: self.directory = Path(directory) - def _read_from_link(self, link: Link, session: Session) -> Iterable[bytes]: + def _read_from_link(self, link: Link, session: httpx.Client) -> Iterable[bytes]: if link.is_file: with open(link.file_path, "rb") as f: yield from f else: - import requests + import httpx - with session.get(link.normalized, stream=True) as resp: + with session.stream("GET", link.normalized) as resp: try: resp.raise_for_status() - except requests.HTTPError as e: + except httpx.HTTPStatusError as e: raise PdmException(f"Failed to read from {link.redacted}: {e}") from e - yield from resp.iter_content(chunk_size=8192) + yield from resp.iter_bytes(chunk_size=8192) - def _get_file_hash(self, link: Link, session: Session) -> str: + def _get_file_hash(self, link: Link, session: httpx.Client) -> str: h = hashlib.new(self.FAVORITE_HASH) logger.debug("Downloading link %s for calculating hash", link.redacted) for chunk in self._read_from_link(link, session): @@ -150,7 +148,7 @@ def _should_cache(self, link: Link) -> bool: # We may add more when we know better about it. return not link.is_file - def get_hash(self, link: Link, session: Session) -> str: + def get_hash(self, link: Link, session: httpx.Client) -> str: # If there is no link hash (i.e., md5, sha256, etc.), we don't want # to store it. hash_value = self.get(link.url_without_fragment) @@ -269,60 +267,6 @@ def _get_from_path(self, path: Path, canonical_name: str, tags_priorities: dict[ return min(candidates, key=lambda x: x[0])[1] -class SafeFileCache(SeparateBodyBaseCache): - """ - A file based cache which is safe to use even when the target directory may - not be accessible or writable. - """ - - def __init__(self, directory: str) -> None: - super().__init__() - self.directory = directory - - def _get_cache_path(self, name: str) -> str: - # From cachecontrol.caches.file_cache.FileCache._fn, brought into our - # class for backwards-compatibility and to avoid using a non-public - # method. - hashed = FileCache.encode(name) - parts = [*list(hashed[:5]), hashed] - return os.path.join(self.directory, *parts) - - def get(self, key: str) -> bytes | None: - path = self._get_cache_path(key) - with contextlib.suppress(OSError): - with open(path, "rb") as f: - return f.read() - - return None - - def get_body(self, key: str) -> BinaryIO | None: - path = self._get_cache_path(key) - with contextlib.suppress(OSError): - return cast(BinaryIO, open(f"{path}.body", "rb")) - - return None - - def set(self, key: str, value: bytes, expires: int | None = None) -> None: - path = self._get_cache_path(key) - with contextlib.suppress(OSError): - with atomic_open_for_write(path, mode="wb") as f: - cast(BinaryIO, f).write(value) - - def set_body(self, key: str, body: bytes) -> None: - if body is None: - return - - path = self._get_cache_path(key) - with contextlib.suppress(OSError): - with atomic_open_for_write(f"{path}.body", mode="wb") as f: - cast(BinaryIO, f).write(body) - - def delete(self, key: str) -> None: - path = self._get_cache_path(key) - with contextlib.suppress(OSError): - os.remove(path) - - @lru_cache(maxsize=None) def get_wheel_cache(directory: Path | str) -> WheelCache: return WheelCache(directory) diff --git a/src/pdm/models/candidates.py b/src/pdm/models/candidates.py index a4c88b978f..a16fcc84e0 100644 --- a/src/pdm/models/candidates.py +++ b/src/pdm/models/candidates.py @@ -386,15 +386,16 @@ def direct_url(self) -> dict[str, Any] | None: "subdirectory": req.subdirectory, } ) - with self.environment.get_finder() as finder: - hash_cache = self.environment.project.make_hash_cache() - return _filter_none( - { - "url": self.link.url_without_fragment, - "archive_info": {"hash": hash_cache.get_hash(self.link, finder.session).replace(":", "=")}, - "subdirectory": req.subdirectory, - } - ) + hash_cache = self.environment.project.make_hash_cache() + return _filter_none( + { + "url": self.link.url_without_fragment, + "archive_info": { + "hash": hash_cache.get_hash(self.link, self.environment.session).replace(":", "=") + }, + "subdirectory": req.subdirectory, + } + ) else: return None @@ -524,14 +525,13 @@ def prepare_metadata(self, force_build: bool = False) -> im.Distribution: def _get_metadata_from_metadata_link( self, link: Link, medata_hash: bool | dict[str, str] | None ) -> im.Distribution | None: - with self.environment.get_finder() as finder: - resp = finder.session.get(link.normalized, headers={"Cache-Control": "max-age=0"}) - if isinstance(medata_hash, dict): - hash_name, hash_value = next(iter(medata_hash.items())) - if hashlib.new(hash_name, resp.content).hexdigest() != hash_value: - termui.logger.warning("Metadata hash mismatch for %s, ignoring the metadata", link) - return None - return MetadataDistribution(resp.text) + resp = self.environment.session.get(link.normalized) + if isinstance(medata_hash, dict): + hash_name, hash_value = next(iter(medata_hash.items())) + if hashlib.new(hash_name, resp.content).hexdigest() != hash_value: + termui.logger.warning("Metadata hash mismatch for %s, ignoring the metadata", link) + return None + return MetadataDistribution(resp.text) def _get_metadata_from_cached(self, cached: CachedPackage) -> im.Distribution: # Get metadata from METADATA inside the wheel diff --git a/src/pdm/models/finder.py b/src/pdm/models/finder.py index 328982d6c3..6bb544b336 100644 --- a/src/pdm/models/finder.py +++ b/src/pdm/models/finder.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import unearth from packaging.version import Version from unearth.evaluator import Package -from unearth.session import PyPISession + +if TYPE_CHECKING: + from pdm.models.session import PDMPyPIClient class ReverseVersion(Version): @@ -27,7 +29,7 @@ def __ge__(self, other: Any) -> bool: class PDMPackageFinder(unearth.PackageFinder): def __init__( self, - session: PyPISession | None = None, + session: PDMPyPIClient | None = None, *, minimal_version: bool = False, **kwargs: Any, diff --git a/src/pdm/models/repositories.py b/src/pdm/models/repositories.py index 40411f7708..b42706a173 100644 --- a/src/pdm/models/repositories.py +++ b/src/pdm/models/repositories.py @@ -345,28 +345,28 @@ def get_hashes(self, candidate: Candidate) -> list[FileHash]: if req.is_named and respect_source_order and comes_from: sources = [s for s in sources if comes_from.startswith(s.url)] - with self.environment.get_finder(sources, self.ignore_compatibility) as finder: - if req.is_file_or_url: - this_link = cast("Link", candidate.prepare(self.environment).link) - links: list[Link] = [this_link] - else: # the req must be a named requirement + if req.is_file_or_url: + this_link = cast("Link", candidate.prepare(self.environment).link) + links: list[Link] = [this_link] + else: # the req must be a named requirement + with self.environment.get_finder(sources, self.ignore_compatibility) as finder: links = [package.link for package in finder.find_matches(req.as_line())] - if self.ignore_compatibility: - links = [link for link in links if self._is_python_match(link)] - for link in links: - if not link or link.is_vcs or link.is_file and link.file_path.is_dir(): - # The links found can still be a local directory or vcs, skippping it. - continue - if not logged: - termui.logger.info("Fetching hashes for %s", candidate) - logged = True - result.append( - { - "url": link.url_without_fragment, - "file": link.filename, - "hash": self._hash_cache.get_hash(link, finder.session), - } - ) + if self.ignore_compatibility: + links = [link for link in links if self._is_python_match(link)] + for link in links: + if not link or link.is_vcs or link.is_file and link.file_path.is_dir(): + # The links found can still be a local directory or vcs, skippping it. + continue + if not logged: + termui.logger.info("Fetching hashes for %s", candidate) + logged = True + result.append( + { + "url": link.url_without_fragment, + "file": link.filename, + "hash": self._hash_cache.get_hash(link, self.environment.session), + } + ) return result def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateInfo]]: @@ -400,24 +400,23 @@ def _get_dependencies_from_json(self, candidate: Candidate) -> CandidateInfo: for proc_url in (raw_url.rstrip("/") for raw_url in (source.url for source in sources) if raw_url) if proc_url.endswith("/simple") ] - with self.environment.get_finder(sources) as finder: - session = finder.session - for prefix in url_prefixes: - json_url = f"{prefix}/pypi/{candidate.name}/{candidate.version}/json" - resp = session.get(json_url) - if not resp.ok: - continue + session = self.environment.session + for prefix in url_prefixes: + json_url = f"{prefix}/pypi/{candidate.name}/{candidate.version}/json" + resp = session.get(json_url) + if resp.is_error: + continue - info = resp.json()["info"] + info = resp.json()["info"] - requires_python = info["requires_python"] or "" - summary = info["summary"] or "" - try: - requirement_lines = info["requires_dist"] or [] - except KeyError: - requirement_lines = info["requires"] or [] - requirements = filter_requirements_with_extras(requirement_lines, candidate.req.extras or ()) - return requirements, requires_python, summary + requires_python = info["requires_python"] or "" + summary = info["summary"] or "" + try: + requirement_lines = info["requires_dist"] or [] + except KeyError: + requirement_lines = info["requires"] or [] + requirements = filter_requirements_with_extras(requirement_lines, candidate.req.extras or ()) + return requirements, requires_python, summary raise CandidateInfoNotFound(candidate) def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateInfo]]: @@ -451,20 +450,19 @@ def search(self, query: str) -> SearchResult: else: search_url = pypi_simple + "/search" - with self.environment.get_finder() as finder: - session = finder.session - resp = session.get(search_url, params={"q": query}) - if resp.status_code == 404: - self.environment.project.core.ui.warn( - f"{pypi_simple!r} doesn't support '/search' endpoint, fallback " - f"to {self.DEFAULT_INDEX_URL!r} now.\n" - "This may take longer depending on your network condition.", - ) - resp = session.get(f"{self.DEFAULT_INDEX_URL}/search", params={"q": query}) - parser = SearchResultParser() - resp.raise_for_status() - parser.feed(resp.text) - return parser.results + session = self.environment.session + resp = session.get(search_url, params={"q": query}) + if resp.status_code == 404: + self.environment.project.core.ui.warn( + f"{pypi_simple!r} doesn't support '/search' endpoint, fallback " + f"to {self.DEFAULT_INDEX_URL!r} now.\n" + "This may take longer depending on your network condition.", + ) + resp = session.get(f"{self.DEFAULT_INDEX_URL}/search", params={"q": query}) + parser = SearchResultParser() + resp.raise_for_status() + parser.feed(resp.text) + return parser.results class LockedRepository(BaseRepository): diff --git a/src/pdm/models/session.py b/src/pdm/models/session.py index 784a30695c..80641ef862 100644 --- a/src/pdm/models/session.py +++ b/src/pdm/models/session.py @@ -1,15 +1,14 @@ from __future__ import annotations -import functools import sys from pathlib import Path -from typing import IO, TYPE_CHECKING, Any, Mapping +from typing import TYPE_CHECKING, Any, cast -from cachecontrol import CacheControlAdapter as BaseCCAdapter -from cachecontrol.serialize import Serializer -from requests import Request -from requests_toolbelt.utils import user_agent -from unearth.session import InsecureMixin, PyPISession +import hishel +import msgpack +from hishel._serializers import Metadata +from httpcore import Request, Response +from unearth.fetchers import PyPIClient from pdm.__version__ import __version__ from pdm.termui import logger @@ -17,7 +16,7 @@ if TYPE_CHECKING: from ssl import SSLContext - from urllib3 import HTTPResponse + from httpx import Response as HTTPXResponse def _create_truststore_ssl_context() -> SSLContext | None: @@ -38,54 +37,114 @@ def _create_truststore_ssl_context() -> SSLContext | None: return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) -class InsecureCacheControlAdapter(InsecureMixin, BaseCCAdapter): - pass - - -class CacheControlAdapter(BaseCCAdapter): - def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): # type: ignore[no-untyped-def] - context = _create_truststore_ssl_context() - pool_kwargs.setdefault("ssl_context", context) - return super().init_poolmanager(connections, maxsize, block, **pool_kwargs) - - -class CompatibleSerializer(Serializer): - """We've switched the cache to SeparateBodyCache since 2.7.1, we use this serializer to - read the old cache. However, reading the new cache with older PDM versions will still - result in a broken cache. - """ - - def prepare_response( - self, request: Request, cached: Mapping[str, Any], body_file: IO[bytes] | None = None - ) -> HTTPResponse | None: - body_raw = cached["response"].get("body") - if not body_raw and body_file is None: - # When we update the old cache using SeparateBodyCache, body_raw is set to empty - # but the body_file hasn't been created yet. The cache is broken at this point. - # return None to ignore this entry. - return None - return super().prepare_response(request, cached, body_file) - - -class PDMSession(PyPISession): +CACHES_TTL = 7 * 24 * 60 * 60 # 7 days + + +class MsgPackSerializer(hishel.BaseSerializer): + KNOWN_REQUEST_EXTENSIONS = ("timeout", "sni_hostname") + KNOWN_RESPONSE_EXTENSIONS = ("http_version", "reason_phrase") + DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + + def dumps(self, response: Response, request: Request, metadata: Metadata) -> bytes: + response_dict = { + "status": response.status, + "headers": response.headers, + "content": response.content, + "extensions": { + key: value for key, value in response.extensions.items() if key in self.KNOWN_RESPONSE_EXTENSIONS + }, + } + + request_dict = { + "method": request.method.decode("ascii"), + "url": str(request.url), + "headers": request.headers, + "extensions": { + key: value for key, value in request.extensions.items() if key in self.KNOWN_REQUEST_EXTENSIONS + }, + } + + metadata_dict = { + "cache_key": metadata["cache_key"], + "number_of_uses": metadata["number_of_uses"], + "created_at": metadata["created_at"].strftime(self.DATETIME_FORMAT), + } + + full_dict = { + "response": response_dict, + "request": request_dict, + "metadata": metadata_dict, + } + return cast(bytes, msgpack.packb(full_dict, use_bin_type=True)) + + def loads(self, data: bytes) -> tuple[Response, Request, Metadata]: + from datetime import datetime + + full_dict = cast("dict[str, Any]", msgpack.loads(data, raw=False)) + + response_dict = full_dict["response"] + request_dict = full_dict["request"] + metadata_dict = full_dict["metadata"] + metadata_dict["created_at"] = datetime.strptime(metadata_dict["created_at"], self.DATETIME_FORMAT) + + response = Response( + status=response_dict["status"], + headers=response_dict["headers"], + content=response_dict["content"], + extensions=response_dict["extensions"], + ) + + request = Request( + method=request_dict["method"], + url=request_dict["url"], + headers=request_dict["headers"], + extensions=request_dict["extensions"], + ) + + metadata = Metadata( + cache_key=metadata_dict["cache_key"], + created_at=metadata_dict["created_at"], + number_of_uses=metadata_dict["number_of_uses"], + ) + + return response, request, metadata + + @property + def is_binary(self) -> bool: + return True + + +class PDMPyPIClient(PyPIClient): def __init__(self, *, cache_dir: Path, **kwargs: Any) -> None: - from pdm.models.caches import SafeFileCache + storage = hishel.FileStorage(serializer=MsgPackSerializer(), base_path=cache_dir, ttl=CACHES_TTL) + controller = hishel.Controller() + kwargs.setdefault("verify", _create_truststore_ssl_context() or True) + kwargs.setdefault("follow_redirects", True) - cache = SafeFileCache(str(cache_dir)) - serializer = CompatibleSerializer() - self.secure_adapter_cls = functools.partial(CacheControlAdapter, cache=cache, serializer=serializer) - self.insecure_adapter_cls = functools.partial(InsecureCacheControlAdapter, cache=cache, serializer=serializer) super().__init__(**kwargs) self.headers["User-Agent"] = self._make_user_agent() + self.event_hooks["response"].append(self.on_response) + + self._transport = hishel.CacheTransport(self._transport, storage, controller) # type: ignore[has-type] + for name, transport in self._mounts.items(): + if name.scheme == "file" or transport is None: + # don't cache file:// transport + continue + self._mounts[name] = hishel.CacheTransport(transport, storage, controller) def _make_user_agent(self) -> str: - return user_agent.UserAgentBuilder("pdm", __version__).include_implementation().build() + import platform + + return "pdm/{} {}/{} {}/{}".format( + __version__, + platform.python_implementation(), + platform.python_version(), + platform.system(), + platform.release(), + ) - # HACK: make the sessions identical to functools.lru_cache - # so that the same index page won't be fetched twice. - # See unearth/collector.py:fetch_page - def __hash__(self) -> int: - return hash(self.headers["User-Agent"]) + def on_response(self, response: HTTPXResponse) -> None: + from unearth.utils import ARCHIVE_EXTENSIONS - def __eq__(self, __o: Any) -> bool: - return isinstance(__o, PDMSession) and self.headers["User-Agent"] == __o.headers["User-Agent"] + if response.extensions.get("from_cache") and response.url.path.endswith(ARCHIVE_EXTENSIONS): + logger.info("Using cached response for %s", response.url) diff --git a/src/pdm/pytest.py b/src/pdm/pytest.py index cee88eef30..4dc5fa7614 100644 --- a/src/pdm/pytest.py +++ b/src/pdm/pytest.py @@ -27,12 +27,11 @@ import shutil import sys from dataclasses import dataclass -from io import BufferedReader, BytesIO, StringIO +from io import StringIO from pathlib import Path from typing import ( TYPE_CHECKING, Any, - BinaryIO, Callable, Dict, Iterable, @@ -42,10 +41,9 @@ Union, cast, ) -from urllib.parse import urlparse +import httpx import pytest -import requests from packaging.version import parse as parse_version from pytest_mock import MockerFixture from unearth import Link @@ -62,7 +60,7 @@ filter_requirements_with_extras, parse_requirement, ) -from pdm.models.session import PDMSession +from pdm.models.session import PDMPyPIClient from pdm.project.config import Config from pdm.project.core import Project from pdm.utils import find_python_in_path, normalize_name, path_to_url @@ -75,9 +73,9 @@ from pdm._types import CandidateInfo, FileHash, RepositoryConfig -class LocalFileAdapter(requests.adapters.BaseAdapter): +class LocalIndexTransport(httpx.BaseTransport): """ - A local file adapter for request. + A local file transport for HTTPX. Allows to mock some HTTP requests with some local files """ @@ -85,14 +83,13 @@ class LocalFileAdapter(requests.adapters.BaseAdapter): def __init__( self, aliases: dict[str, Path], - overrides: dict | None = None, + overrides: IndexOverrides | None = None, strip_suffix: bool = False, ): super().__init__() self.aliases = sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True) self.overrides = overrides if overrides is not None else {} self.strip_suffix = strip_suffix - self._opened_files: list[BytesIO | BufferedReader | BinaryIO] = [] def get_file_path(self, path: str) -> Path | None: for prefix, base_path in self.aliases: @@ -106,42 +103,26 @@ def get_file_path(self, path: str) -> Path | None: ) return None - def send( - self, - request: requests.PreparedRequest, - stream: bool = False, - timeout: float | tuple[float, float] | tuple[float, None] | None = None, - verify: bool | str = True, - cert: str | bytes | tuple[bytes | str, str | bytes] | None = None, - proxies: Mapping[str, str] | None = None, - ) -> requests.models.Response: - request_path = str(urlparse(request.url).path) + def handle_request(self, request: httpx.Request) -> httpx.Response: + from httpx._content import IteratorByteStream + + request_path = request.url.path file_path = self.get_file_path(request_path) - response = requests.models.Response() - response.url = request.url or "" - response.request = request + headers: dict[str, str] = {} + stream: httpx.SyncByteStream | None = None + content: bytes | None = None if request_path in self.overrides: - response.status_code = 200 - response.reason = "OK" - response.raw = BytesIO(self.overrides[request_path]) - response.headers["Content-Type"] = "text/html" + status_code = 200 + content = self.overrides[request_path] + headers["Content-Type"] = "text/html" elif file_path is None or not file_path.exists(): - response.status_code = 404 - response.reason = "Not Found" - response.raw = BytesIO(b"Not Found") + status_code = 404 else: - response.status_code = 200 - response.reason = "OK" - response.raw = file_path.open("rb") + status_code = 200 + stream = IteratorByteStream(file_path.open("rb")) if file_path.suffix == ".html": - response.headers["Content-Type"] = "text/html" - self._opened_files.append(response.raw) - return response - - def close(self) -> None: - for fp in self._opened_files: - fp.close() - self._opened_files.clear() + headers["Content-Type"] = "text/html" + return httpx.Response(status_code, headers=headers, content=content, stream=stream) class _FakeLink: @@ -294,7 +275,7 @@ def __delitem__(self, key: str) -> None: IndexMap = Dict[str, Path] """Path some root-relative http paths to some local paths""" -IndexOverrides = Dict[str, str] +IndexOverrides = Dict[str, bytes] """PyPI indexes overrides fixture format""" IndexesDefinition = Dict[str, Union[Tuple[IndexMap, IndexOverrides, bool], IndexMap]] """Mock PyPI indexes format""" @@ -347,13 +328,14 @@ def pypi_indexes() -> IndexesDefinition: @pytest.fixture -def pdm_session(pypi_indexes: IndexesDefinition) -> Callable[[Any], PDMSession]: - def get_pypi_session(*args: Any, **kwargs: Any) -> PDMSession: - session = _build_session(*args, **kwargs) +def build_test_session(pypi_indexes: IndexesDefinition) -> Callable[..., PDMPyPIClient]: + def get_pypi_session(*args: Any, **kwargs: Any) -> PDMPyPIClient: + mounts: dict[str, httpx.BaseTransport] = {} for root, specs in pypi_indexes.items(): index, overrides, strip = specs if isinstance(specs, tuple) else (specs, None, False) - session.mount(root, LocalFileAdapter(index, overrides=overrides, strip_suffix=strip)) - return session + mounts[root] = LocalIndexTransport(index, overrides=overrides, strip_suffix=strip) + kwargs["mounts"] = mounts + return _build_session(*args, **kwargs) return get_pypi_session @@ -382,7 +364,7 @@ def project_no_init( tmp_path: Path, mocker: MockerFixture, core: Core, - pdm_session: type[PDMSession], + build_test_session: Callable[..., PDMPyPIClient], monkeypatch: pytest.MonkeyPatch, build_env: Path, ) -> Project: @@ -399,7 +381,7 @@ def project_no_init( ) p = core.create_project(tmp_path, global_config=test_home.joinpath("config.toml").as_posix()) p.global_config["venv.location"] = str(tmp_path / "venvs") - mocker.patch.object(BaseEnvironment, "_build_session", pdm_session) + mocker.patch.object(BaseEnvironment, "_build_session", build_test_session) mocker.patch("pdm.builders.base.EnvBuilder.get_shared_env", return_value=str(build_env)) tmp_path.joinpath("caches").mkdir(parents=True) p.global_config["cache_dir"] = tmp_path.joinpath("caches").as_posix() diff --git a/tasks/max_versions.py b/tasks/max_versions.py index f16baa7e9e..dc972f06e2 100644 --- a/tasks/max_versions.py +++ b/tasks/max_versions.py @@ -4,7 +4,7 @@ from html.parser import HTMLParser from pathlib import Path -import requests +import httpx PROJECT_DIR = Path(__file__).parent.parent @@ -37,7 +37,7 @@ def handle_data(self, data: str) -> None: def dump_python_version_module(dest_file) -> None: - resp = requests.get("https://python.org/downloads") + resp = httpx.get("https://python.org/downloads") resp_text = resp.text parser = PythonVersionParser() parser.feed(resp_text) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 877f170a24..80ed147dac 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -7,8 +7,8 @@ from typing import Any from unittest.mock import MagicMock +import httpx import pytest -import requests from pytest_mock import MockerFixture from pdm.cli.commands.publish.package import PackageFile @@ -41,16 +41,12 @@ def prepare_packages(tmp_path: Path): @pytest.fixture def mock_pypi(mocker: MockerFixture): - def post(url, *, data, **kwargs): + def send(request, **kwargs): # consume the data body to make the progress complete - data.read() - resp = requests.Response() - resp.status_code = 200 - resp.reason = "OK" - resp.url = url - return resp + request.read() + return httpx.Response(status_code=200, request=request) - return mocker.patch("pdm.models.session.PDMSession.post", side_effect=post) + return mocker.patch("pdm.models.session.PDMPyPIClient.send", side_effect=send) @pytest.fixture @@ -59,11 +55,7 @@ def uploaded(mocker: MockerFixture): def fake_upload(package): packages.append(package) - resp = requests.Response() - resp.status_code = 200 - resp.reason = "OK" - resp.url = "https://upload.pypi.org/legacy/" - return resp + return httpx.Response(status_code=200, request=httpx.Request("POST", "https://upload.pypi.org/legacy/")) mocker.patch.object(Repository, "upload", side_effect=fake_upload) return packages diff --git a/tests/cli/test_cache.py b/tests/cli/test_cache.py index 09ee74b93d..c0b893646a 100644 --- a/tests/cli/test_cache.py +++ b/tests/cli/test_cache.py @@ -156,9 +156,8 @@ def test_cache_info(project, pdm): ], ) def test_hash_cache(project, url, hash): - with project.environment.get_finder() as finder: - hash_cache = project.make_hash_cache() - assert hash_cache.get_hash(Link(url), finder.session) == hash + hash_cache = project.make_hash_cache() + assert hash_cache.get_hash(Link(url), project.environment.session) == hash def test_clear_package_cache(project, pdm): diff --git a/tests/cli/test_publish.py b/tests/cli/test_publish.py index 795bc70328..a080655dff 100644 --- a/tests/cli/test_publish.py +++ b/tests/cli/test_publish.py @@ -3,6 +3,7 @@ import pytest +from pdm._types import RepositoryConfig from pdm.cli.commands.publish import Command as PublishCommand from pdm.cli.commands.publish.package import PackageFile from pdm.cli.commands.publish.repository import Repository @@ -69,10 +70,13 @@ def test_repository_get_release_urls(project): "demo-0.0.1.zip", ] ] - repository = Repository(project, "https://upload.pypi.org/legacy/", "abc", "123", None) + config = RepositoryConfig( + config_prefix="repository", name="test", url="https://upload.pypi.org/legacy/", username="abc", password="123" + ) + repository = Repository(project, config) assert repository.get_release_urls(package_files) == {"https://pypi.org/project/demo/0.0.1/"} - repository = Repository(project, "https://example.pypi.org/legacy/", "abc", "123", None) + repository.url = "https://example.pypi.org/legacy/" assert not repository.get_release_urls(package_files) @@ -121,14 +125,24 @@ def test_publish_and_build_in_one_run(fixture_project, pdm, mock_pypi): assert "https://pypi.org/project/demo-module/0.1.0/" in result -def test_publish_cli_args_and_env_var_precedence(project, monkeypatch): - repo = PublishCommand.get_repository( +def test_publish_cli_args_and_env_var_precedence(project, monkeypatch, mocker): + repository = mocker.patch.object(Repository, "__init__", return_value=None) + PublishCommand.get_repository( project, Namespace(repository=None, username="foo", password="bar", ca_certs="custom.pem", verify_ssl=True), ) - assert repo.url == "https://upload.pypi.org/legacy/" - assert repo.session.auth == ("foo", "bar") - assert repo.session.verify == "custom.pem" + repository.assert_called_with( + project, + RepositoryConfig( + config_prefix="repository", + name="pypi", + url="https://upload.pypi.org/legacy/", + username="foo", + password="bar", + ca_certs="custom.pem", + verify_ssl=None, + ), + ) with monkeypatch.context() as m: m.setenv("PDM_PUBLISH_USERNAME", "bar") @@ -136,24 +150,44 @@ def test_publish_cli_args_and_env_var_precedence(project, monkeypatch): m.setenv("PDM_PUBLISH_REPO", "testpypi") m.setenv("PDM_PUBLISH_CA_CERTS", "override.pem") - repo = PublishCommand.get_repository( + PublishCommand.get_repository( project, Namespace(repository=None, username=None, password=None, ca_certs=None, verify_ssl=True), ) - assert repo.url == "https://test.pypi.org/legacy/" - assert repo.session.auth == ("bar", "secret") - assert repo.session.verify == "override.pem" + repository.assert_called_with( + project, + RepositoryConfig( + config_prefix="repository", + name="testpypi", + url="https://test.pypi.org/legacy/", + username="bar", + password="secret", + ca_certs="override.pem", + verify_ssl=None, + ), + ) - repo = PublishCommand.get_repository( + PublishCommand.get_repository( project, Namespace(repository="pypi", username="foo", password=None, ca_certs="custom.pem", verify_ssl=True), ) - assert repo.url == "https://upload.pypi.org/legacy/" - assert repo.session.auth == ("foo", "secret") - assert repo.session.verify == "custom.pem" + repository.assert_called_with( + project, + RepositoryConfig( + config_prefix="repository", + name="pypi", + url="https://upload.pypi.org/legacy/", + username="foo", + password="secret", + ca_certs="custom.pem", + verify_ssl=None, + ), + ) -def test_repository_get_credentials_from_keyring(project, keyring): +def test_repository_get_credentials_from_keyring(project, keyring, mocker): keyring.save_auth_info("https://test.org/upload", "foo", "barbaz") - repository = Repository(project, "https://test.org/upload", None, None, None) - assert repository.session.auth == ("foo", "barbaz") + config = RepositoryConfig(config_prefix="repository", name="test", url="https://test.org/upload") + basic_auth = mocker.patch("httpx.BasicAuth.__init__", return_value=None) + Repository(project, config) + basic_auth.assert_called_with(username="foo", password="barbaz") diff --git a/tests/conftest.py b/tests/conftest.py index 8df33646aa..203d3ddc1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ @pytest.fixture -def index() -> dict[str, str]: +def index() -> dict[str, bytes]: return {} diff --git a/tests/test_project.py b/tests/test_project.py index 7b568885cc..5a62ae1613 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -292,10 +292,9 @@ def test_access_index_with_auth(project, httpserver: HTTPServer): "pypi.extra.password": "bar", } ) - with project.environment.get_finder() as finder: - session = finder.session - resp = session.get(httpserver.url_for("/simple/my-package")) - assert resp.ok + session = project.environment.session + resp = session.get(httpserver.url_for("/simple/my-package")) + assert resp.is_success def test_configured_source_overwriting(project):