diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94a3040..5bc2f27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,6 @@ jobs: fail-fast: false matrix: python-version: - - '3.8' - '3.9' - '3.10' - '3.11' @@ -25,7 +24,7 @@ jobs: - '3.13t' - '3.14-dev' - '3.14t-dev' - - 'pypy-3.8' + - 'pypy-3.9' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index 79438f0..83cd038 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,6 @@ coverage.xml .pytest_cache/ cover/ *,cover -.hypothesis/ .pytest_cache # Translations diff --git a/README.md b/README.md index 84b003f..206417d 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ those fixtures are shared between threads. ## Features -- Five global CLI flags: +- Six global CLI flags: - `--parallel-threads` to run a test suite in parallel - `--iterations` to run multiple times in each thread - `--skip-thread-unsafe` to skip running tests marked as or @@ -72,6 +72,14 @@ those fixtures are shared between threads. adding support for Python 3.14 to a library that already runs tests under pytest-run-parallel on Python 3.13 or older. + - `--mark-hypothesis-as-unsafe`, to always skip runing tests that + use [hypothesis](https://github.com/hypothesisworks/hypothesis). + While newer version of Hypothesis are thread-safe, and versions + which are not are automatically skipped by `pytest-run-parallel`, + this flag is an escape hatch in case you run into thread-safety + problems caused by Hypothesis, or in tests that happen to use + hypothesis and were skipped in older versions of pytest-run-parallel. + - Three corresponding markers: - `pytest.mark.parallel_threads(n)` to mark a single test to run diff --git a/pyproject.toml b/pyproject.toml index 7b00799..f7f35be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ name = "pytest-run-parallel" description = "A simple pytest plugin to run tests concurrently" version = "0.5.1-dev" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "pytest>=6.2.0", ] @@ -28,7 +28,6 @@ classifiers = [ "Topic :: Software Development :: Testing", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -66,7 +65,7 @@ exclude = ["docs/conf.py"] select = ["E4", "E7", "E9", "F", "I"] [tool.tox] -env_list = ["py38", "py39", "py310", "py311", "py312", "py313", "py313t", "psutil", "pypy3", "ruff"] +env_list = ["py39", "py310", "py311", "py312", "py313", "py313t", "psutil", "pypy3", "ruff"] [tool.tox.env_run_base] deps = [ @@ -74,7 +73,7 @@ deps = [ "pytest-cov", "pytest-order", "check-manifest", - "hypothesis", + "hypothesis>=6.135.33", ] commands = [ [ @@ -104,11 +103,10 @@ extras = ["psutil"] [tool.tox.gh.python] -"3.8" = ["py38"] "3.9" = ["py39"] "3.10" = ["py310"] "3.11" = ["py311"] "3.12" = ["py312"] "3.13" = ["py313", "psutil"] "3.13t" = ["py313t"] -"pypy-3.8" = ["pypy3"] +"pypy-3.9" = ["pypy3"] diff --git a/src/pytest_run_parallel/plugin.py b/src/pytest_run_parallel/plugin.py index 3c83b3a..00197f2 100644 --- a/src/pytest_run_parallel/plugin.py +++ b/src/pytest_run_parallel/plugin.py @@ -94,6 +94,7 @@ def __init__(self, config): self.skip_thread_unsafe = config.option.skip_thread_unsafe self.mark_warnings_as_unsafe = config.option.mark_warnings_as_unsafe self.mark_ctypes_as_unsafe = config.option.mark_ctypes_as_unsafe + self.mark_hypothesis_as_unsafe = config.option.mark_hypothesis_as_unsafe skipped_functions = [ x.split(".") for x in config.getini("thread_unsafe_functions") @@ -140,6 +141,7 @@ def _is_thread_unsafe(self, item): self.skipped_functions, self.mark_warnings_as_unsafe, self.mark_ctypes_as_unsafe, + self.mark_hypothesis_as_unsafe, ) @pytest.hookimpl(trylast=True) @@ -332,6 +334,12 @@ def pytest_addoption(parser): dest="mark_ctypes_as_unsafe", default=False, ) + parser.addoption( + "--mark-hypothesis-as-unsafe", + action="store_true", + dest="mark_hypothesis_as_unsafe", + default=False, + ) parser.addini( "thread_unsafe_fixtures", "list of thread-unsafe fixture names that cause a test to " diff --git a/src/pytest_run_parallel/thread_unsafe_detection.py b/src/pytest_run_parallel/thread_unsafe_detection.py index 73e0e2f..90ae753 100644 --- a/src/pytest_run_parallel/thread_unsafe_detection.py +++ b/src/pytest_run_parallel/thread_unsafe_detection.py @@ -16,6 +16,13 @@ def is_hypothesis_test(fn): return False +try: + from hypothesis import __version_info__ as hypothesis_version +except ImportError: + hypothesis_version = (0, 0, 0) + +HYPOTHESIS_THREADSAFE_VERSION = (6, 136, 3) + WARNINGS_IS_THREADSAFE = bool( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0) @@ -47,7 +54,9 @@ def construct_base_blocklist(unsafe_warnings, unsafe_ctypes): class ThreadUnsafeNodeVisitor(ast.NodeVisitor): - def __init__(self, fn, skip_set, unsafe_warnings, unsafe_ctypes, level=0): + def __init__( + self, fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=0 + ): self.thread_unsafe = False self.thread_unsafe_reason = None blocklist = construct_base_blocklist(unsafe_warnings, unsafe_ctypes) @@ -64,6 +73,7 @@ def __init__(self, fn, skip_set, unsafe_warnings, unsafe_ctypes, level=0): self.skip_set = skip_set self.unsafe_warnings = unsafe_warnings self.unsafe_ctypes = unsafe_ctypes + self.unsafe_hypothesis = unsafe_hypothesis self.level = level self.modules_aliases = {} self.func_aliases = {} @@ -141,6 +151,7 @@ def _get_child_fn(mod, node): self.skip_set, self.unsafe_warnings, self.unsafe_ctypes, + self.unsafe_hypothesis, self.level + 1, ) ) @@ -192,6 +203,7 @@ def _recursive_analyze_name(self, node): self.skip_set, self.unsafe_warnings, self.unsafe_ctypes, + self.unsafe_hypothesis, self.level + 1, ) ) @@ -236,10 +248,22 @@ def visit(self, node): def _identify_thread_unsafe_nodes( - fn, skip_set, unsafe_warnings, unsafe_ctypes, level=0 + fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=0 ): if is_hypothesis_test(fn): - return True, "uses hypothesis" + if hypothesis_version < HYPOTHESIS_THREADSAFE_VERSION: + return ( + True, + f"uses hypothesis v{'.'.join(map(str, hypothesis_version))}, which " + "is before the first thread-safe version " + f"(v{'.'.join(map(str, HYPOTHESIS_THREADSAFE_VERSION))})", + ) + if unsafe_hypothesis: + return ( + True, + "uses Hypothesis, and pytest-run-parallel was run with " + "--mark-hypothesis-as-unsafe", + ) try: src = inspect.getsource(fn) @@ -252,7 +276,7 @@ def _identify_thread_unsafe_nodes( return False, None visitor = ThreadUnsafeNodeVisitor( - fn, skip_set, unsafe_warnings, unsafe_ctypes, level=level + fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=level ) visitor.visit(tree) return visitor.thread_unsafe, visitor.thread_unsafe_reason @@ -261,15 +285,11 @@ def _identify_thread_unsafe_nodes( cached_thread_unsafe_identify = functools.lru_cache(_identify_thread_unsafe_nodes) -def identify_thread_unsafe_nodes(fn, skip_set, unsafe_warnings, unsafe_ctypes, level=0): +def identify_thread_unsafe_nodes(*args, **kwargs): try: - return cached_thread_unsafe_identify( - fn, skip_set, unsafe_warnings, unsafe_ctypes, level=level - ) + return cached_thread_unsafe_identify(*args, **kwargs) except TypeError: - return _identify_thread_unsafe_nodes( - fn, skip_set, unsafe_warnings, unsafe_ctypes, level=level - ) + return _identify_thread_unsafe_nodes(*args, **kwargs) def construct_thread_unsafe_fixtures(config): diff --git a/tests/test_run_parallel.py b/tests/test_run_parallel.py index 6b6ee65..970a814 100644 --- a/tests/test_run_parallel.py +++ b/tests/test_run_parallel.py @@ -1,5 +1,12 @@ import os +import pytest + +try: + import hypothesis +except ImportError: + hypothesis = None + def test_default_threads(pytester): """Make sure that pytest accepts our fixture.""" @@ -617,3 +624,21 @@ def test_parallel(num_parallel_threads): "*::test_doctests_marked_thread_unsafe.txt PASSED*", ] ) + + +@pytest.mark.skipif(hypothesis is None, reason="hypothesis needs to be installed") +def test_runs_hypothesis_in_parallel(pytester): + pytester.makepyfile(""" + from hypothesis import given, strategies as st, settings, HealthCheck + + @given(a=st.none()) + @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) + def test_uses_hypothesis(a, num_parallel_threads): + assert num_parallel_threads == 10 + """) + result = pytester.runpytest("--parallel-threads=10", "-v") + result.stdout.fnmatch_lines( + [ + "*::test_uses_hypothesis PARALLEL PASSED*", + ] + ) diff --git a/tests/test_thread_unsafe_detection.py b/tests/test_thread_unsafe_detection.py index b46f8c1..eb8819b 100644 --- a/tests/test_thread_unsafe_detection.py +++ b/tests/test_thread_unsafe_detection.py @@ -367,24 +367,6 @@ def test_should_be_marked_3(num_parallel_threads): ) -@pytest.mark.skipif(hypothesis is None, reason="hypothesis needs to be installed") -def test_detect_hypothesis(pytester): - pytester.makepyfile(""" - from hypothesis import given, strategies as st, settings, HealthCheck - - @given(a=st.none()) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) - def test_uses_hypothesis(a, num_parallel_threads): - assert num_parallel_threads == 1 - """) - result = pytester.runpytest("--parallel-threads=10", "-v") - result.stdout.fnmatch_lines( - [ - "*::test_uses_hypothesis PASSED*", - ] - ) - - def test_detect_unittest_mock(pytester): pytester.makepyfile(""" import sys @@ -688,3 +670,25 @@ def test_thread_unsafe_pytest_warns_multiline_string2(self, num_parallel_threads f"*::test_thread_unsafe_pytest_warns_multiline_string2 {WARNINGS_PASS}PASSED*", ] ) + + +@pytest.mark.skipif(hypothesis is None, reason="hypothesis needs to be installed") +def test_thread_unsafe_hypothesis_config_option(pytester): + pytester.makepyfile(""" + from hypothesis import given, strategies as st, settings, HealthCheck + + @given(st.integers()) + @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) + def test_thread_unsafe_hypothesis(num_parallel_threads, n): + assert num_parallel_threads == 1 + assert isinstance(n, int) + """) + + result = pytester.runpytest( + "--parallel-threads=10", "-v", "--mark-hypothesis-as-unsafe" + ) + result.stdout.fnmatch_lines( + [ + "*::test_thread_unsafe_hypothesis PASSED*", + ] + ) diff --git a/uv.lock b/uv.lock index d41b383..e888316 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,9 @@ version = 1 revision = 2 -requires-python = ">=3.8" +requires-python = ">=3.9" resolution-markers = [ - "python_full_version < '3.10'", "python_full_version >= '3.10'", + "python_full_version < '3.10'", ] [[package]] @@ -98,16 +98,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, - { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, - { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, - { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, - { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, @@ -373,13 +363,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218, upload-time = "2024-08-06T20:33:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067, upload-time = "2024-08-06T20:33:07.879Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812, upload-time = "2024-08-06T20:33:12.542Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531, upload-time = "2024-08-06T20:33:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820, upload-time = "2024-08-06T20:33:16.586Z" }, - { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514, upload-time = "2024-08-06T20:33:22.414Z" }, - { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702, upload-time = "2024-08-06T20:33:23.813Z" }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },