From d2ccac0addbe3591a887d19aa21ab69245296241 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 20 Jan 2025 15:50:57 +0100 Subject: [PATCH 1/9] Add support for Python 3.12 and 3.13 to AWS Lambda integration. (#3965) Its time to add support for newer versions of Python to our AWS Lambda integration. Fixes #3946 --- .craft.yml | 2 ++ tests/integrations/aws_lambda/test_aws.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.craft.yml b/.craft.yml index 70875d5404..665f06834a 100644 --- a/.craft.yml +++ b/.craft.yml @@ -25,6 +25,8 @@ targets: - python3.9 - python3.10 - python3.11 + - python3.12 + - python3.13 license: MIT - name: sentry-pypi internalPypiRepo: getsentry/pypi diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index e229812336..f60bedc846 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -38,10 +38,9 @@ RUNTIMES_TO_TEST = [ "python3.8", - "python3.9", "python3.10", - "python3.11", "python3.12", + "python3.13", ] LAMBDA_PRELUDE = """ From 6b9417f466de80442fcd0b0656f6b38e403af64d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 20 Jan 2025 16:54:09 +0100 Subject: [PATCH 2/9] capitalization --- scripts/populate_tox/README.md | 2 +- scripts/populate_tox/populate_tox.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index f279dd939e..5e990dfaab 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -13,7 +13,7 @@ There is a template in this directory called `tox.jinja` which contains a combination of hardcoded and generated entries. The `populate_tox.py` script fills out the auto-generated part of that template. -It does this by querying PYPI for each framework's package and its metadata and +It does this by querying PyPI for each framework's package and its metadata and then determining which versions make sense to test to get good coverage. The lowest supported and latest version of a framework are always tested, with diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 7337a4c9f8..cc259a2928 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -112,7 +112,7 @@ @functools.cache def fetch_package(package: str) -> dict: - """Fetch package metadata from PYPI.""" + """Fetch package metadata from PyPI.""" url = PYPI_PROJECT_URL.format(project=package) pypi_data = requests.get(url) From c97198174fb5b4b117721af9e77c7c6d8f4c570d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 20 Jan 2025 17:07:18 +0100 Subject: [PATCH 3/9] comment --- scripts/populate_tox/populate_tox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index cc259a2928..6953829c81 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -252,7 +252,7 @@ def pick_releases_to_test(releases: list[Version]) -> list[Version]: indexes = [ 0, # oldest version supported len(releases) // 3, - len(releases) // 3 * 2, + len(releases) // 3 * 2, # two releases in between, roughly evenly spaced -1, # latest ] From c2d59aea4117141f6365d539acc7dcf699c3deef Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 20 Jan 2025 17:12:03 +0100 Subject: [PATCH 4/9] explicit none check --- scripts/populate_tox/populate_tox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 6953829c81..e26d6a580f 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -136,7 +136,7 @@ def fetch_release(package: str, version: Version) -> dict: def _prefilter_releases(integration: str, releases: dict[str, dict]) -> list[Version]: """Drop versions that are unsupported without making additional API calls.""" min_supported = _MIN_VERSIONS.get(integration) - if min_supported: + if min_supported is not None: min_supported = Version(".".join(map(str, min_supported))) else: print( From 48d63683e675800edc079435bd4a63bed66e1e60 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 21 Jan 2025 09:55:09 +0100 Subject: [PATCH 5/9] Split gevent tests off (#3964) Same as https://github.com/getsentry/sentry-python/pull/3962, but for master --- .../workflows/test-integrations-gevent.yml | 91 +++++++++++++++++++ .../workflows/test-integrations-network.yml | 8 -- .../split_tox_gh_actions.py | 4 +- 3 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/test-integrations-gevent.yml diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml new file mode 100644 index 0000000000..088f952ea3 --- /dev/null +++ b/.github/workflows/test-integrations-gevent.yml @@ -0,0 +1,91 @@ +# Do not edit this YAML file. This file is generated automatically by executing +# python scripts/split_tox_gh_actions/split_tox_gh_actions.py +# The template responsible for it is in +# scripts/split_tox_gh_actions/templates/base.jinja +name: Test Gevent +on: + push: + branches: + - master + - release/** + - potel-base + pull_request: +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +permissions: + contents: read +env: + BUILD_CACHE_KEY: ${{ github.sha }} + CACHED_BUILD_PATHS: | + ${{ github.workspace }}/dist-serverless +jobs: + test-gevent-pinned: + name: Gevent (pinned) + timeout-minutes: 30 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.6","3.8","3.10","3.11","3.12"] + # python3.6 reached EOL and is no longer being supported on + # new versions of hosted runners on Github Actions + # ubuntu-20.04 is the last version that supported python3.6 + # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 + os: [ubuntu-20.04] + steps: + - uses: actions/checkout@v4.2.2 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Setup Test Env + run: | + pip install "coverage[toml]" tox + - name: Erase coverage + run: | + coverage erase + - name: Test gevent pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-gevent" + - name: Generate coverage XML (Python 3.6) + if: ${{ !cancelled() && matrix.python-version == '3.6' }} + run: | + export COVERAGE_RCFILE=.coveragerc36 + coverage combine .coverage-sentry-* + coverage xml --ignore-errors + - name: Generate coverage XML + if: ${{ !cancelled() && matrix.python-version != '3.6' }} + run: | + coverage combine .coverage-sentry-* + coverage xml + - name: Upload coverage to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v5.1.2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + # make sure no plugins alter our coverage reports + plugin: noop + verbose: true + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: .junitxml + verbose: true + check_required_tests: + name: All pinned Gevent tests passed + needs: test-gevent-pinned + # Always run this, even if a dependent job failed + if: always() + runs-on: ubuntu-20.04 + steps: + - name: Check for failures + if: contains(needs.test-gevent-pinned.result, 'failure') || contains(needs.test-gevent-pinned.result, 'skipped') + run: | + echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index ab1c5b0658..b5593a58fd 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -47,10 +47,6 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test gevent latest - run: | - set -x # print commands that are executed - ./scripts/runtox.sh "py${{ matrix.python-version }}-gevent-latest" - name: Test grpc latest run: | set -x # print commands that are executed @@ -115,10 +111,6 @@ jobs: - name: Erase coverage run: | coverage erase - - name: Test gevent pinned - run: | - set -x # print commands that are executed - ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-gevent" - name: Test grpc pinned run: | set -x # print commands that are executed diff --git a/scripts/split_tox_gh_actions/split_tox_gh_actions.py b/scripts/split_tox_gh_actions/split_tox_gh_actions.py index 1537ad8389..43307c3093 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -89,6 +89,9 @@ "openfeature", "unleash", ], + "Gevent": [ + "gevent", + ], "GraphQL": [ "ariadne", "gql", @@ -96,7 +99,6 @@ "strawberry", ], "Network": [ - "gevent", "grpc", "httpx", "requests", From efaa55fd4d8d164eee177d9c65b4088c73885869 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 22 Jan 2025 09:55:35 +0100 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- scripts/populate_tox/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index 5e990dfaab..64bb8d747c 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -102,11 +102,10 @@ you can say: 1. Add the minimum supported version of the framework/library to `_MIN_VERSIONS` in `integrations/__init__.py`. This should be the lowest version of the framework that we can guarantee works with the SDK. If you've just added the - integration, it's fine to set this to the latest version of the framework + integration, you should generally set this to the latest version of the framework at the time. 2. Add the integration and any constraints to `TEST_SUITE_CONFIG`. See the - "Defining constraints" section for the format (or copy-paste one - of the existing entries). + "Defining constraints" section for the format. 3. Add the integration to one of the groups in the `GROUPS` dictionary in `scripts/split_tox_gh_actions/split_tox_gh_actions.py`. 4. Add the `TESTPATH` for the test suite in `tox.jinja`'s `setenv` section. From 474c67965a72f5a076e2838c2b04f86030f6cfba Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 22 Jan 2025 10:15:13 +0100 Subject: [PATCH 7/9] better readme structure --- scripts/populate_tox/README.md | 62 +++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/scripts/populate_tox/README.md b/scripts/populate_tox/README.md index 64bb8d747c..1c119bcfa4 100644 --- a/scripts/populate_tox/README.md +++ b/scripts/populate_tox/README.md @@ -48,7 +48,20 @@ integration_name: { } ``` -The following can be set as a rule: +### `package` + +The name of the third party package as it's listed on PyPI. The script will +be picking different versions of this package to test. + +This key is mandatory. + +### `deps` + +The test dependencies of the test suite. They're defined as a dictionary of +`rule: [package1, package2, ...]` key-value pairs. All packages +in the package list of a rule will be installed as long as the rule applies. + +`rule`s are predefined. Each `rule` must be one of the following: - `*`: packages will be always installed - a version specifier on the main package (e.g. `<=0.32`): packages will only be installed if the main package falls into the version bounds specified @@ -60,41 +73,50 @@ package's dependencies, for example. If e.g. Flask tests generally need Werkzeug and don't care about its version, but Flask older than 3.0 needs a specific Werkzeug version to work, you can say: -``` +```python "flask": { "deps": { "*": ["Werkzeug"], "<3.0": ["Werkzeug<2.1.0"], - } -} -``` - -Sometimes, things depend on the Python version installed. If the integration -test should only run on specific Python version, e.g. if you want AIOHTTP -tests to only run on Python 3.7+, you can say: - -``` -"aiohttp": { + }, ... - "python": ">=3.7", } ``` -If, on the other hand, you need to install a specific version of a secondary -dependency on specific Python versions (so the test suite should still run on -said Python versions, just with different dependency-of-a-dependency bounds), -you can say: +If you need to install a specific version of a secondary dependency on specific +Python versions, you can say: -``` +```python "celery": { - ... "deps": { "*": ["newrelic", "redis"], "py3.7": ["importlib-metadata<5.0"], }, -}, + ... +} +``` + +### `python` + +Sometimes, the whole test suite should only run on specific Python versions. +This can be achieved via the `python` key, which expects a version specifier. + +For example, if you want AIOHTTP tests to only run on Python 3.7+, you can say: + +```python +"aiohttp": { + "python": ">=3.7", + ... +} ``` +Specifying `python` is discouraged as the script itself finds out which +Python versions are supported by the package. However, if a package has broken +metadata or the SDK is explicitly not supporting some packages on specific +Python versions (because of, for example, broken context vars), the `python` +key can be used. + + ## How-Tos ### Add a new test suite From 8904a1bd53fb3c3dccebd82b99501ccdefc9d763 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 22 Jan 2025 10:21:45 +0100 Subject: [PATCH 8/9] remove test suite config --- scripts/populate_tox/config.py | 379 +-------------------------------- 1 file changed, 1 insertion(+), 378 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index 788c7eedac..9e1366c25b 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -5,381 +5,4 @@ # # See scripts/populate_tox/README.md for more info on the format and examples. -TEST_SUITE_CONFIG = { - "aiohttp": { - "package": "aiohttp", - "deps": {"*": ["pytest-aiohttp", "pytest-asyncio"]}, - "python": ">=3.7", - }, - "anthropic": { - "package": "anthropic", - "deps": { - "*": ["pytest-asyncio"], - "<=0.32": ["httpx<0.28.0"], - }, - "python": ">=3.7", - }, - "ariadne": { - "package": "ariadne", - "deps": { - "*": ["fastapi", "flask", "httpx"], - }, - "python": ">=3.8", - }, - "arq": { - "package": "arq", - "deps": { - "*": ["fakeredis>=2.2.0,<2.8", "pytest-asyncio", "async-timeout"], - "<=0.25": ["pydantic<2"], - }, - "python": ">=3.7", - }, - "asyncpg": { - "package": "asyncpg", - "deps": { - "*": ["pytest-asyncio"], - }, - "python": ">=3.7", - }, - "beam": { - "package": "apache-beam", - "deps": { - "*": [], - }, - "python": ">=3.7", - }, - "boto3": { - "package": "boto3", - "deps": { - "*": [], - }, - }, - "bottle": { - "package": "bottle", - "deps": { - "*": ["werkzeug<2.1.0"], - }, - }, - "celery": { - "package": "celery", - "deps": { - "*": ["newrelic", "redis"], - "py3.7": ["importlib-metadata<5.0"], - }, - }, - "chalice": { - "package": "chalice", - "deps": { - "*": ["pytest-chalice==0.0.5"], - }, - }, - "clickhouse_driver": { - "package": "clickhouse-driver", - "deps": { - "*": [], - }, - }, - "cohere": { - "package": "cohere", - "deps": { - "*": ["httpx"], - }, - }, - "django": { - "package": "django", - "deps": { - "*": [ - "psycopg2-binary", - "werkzeug", - ], - ">=2.0,<3.0": ["six"], - "<=3.2": [ - "werkzeug<2.1.0", - "djangorestframework>=3.0.0,<4.0.0", - "pytest-django", - ], - ">=2.0": ["channels[daphne]"], - "<=3.0": ["pytest-django<4.0"], - ">=4.0": ["djangorestframework", "pytest-asyncio"], - }, - }, - "dramatiq": { - "package": "dramatiq", - "deps": {}, - }, - "falcon": { - "package": "falcon", - "deps": {}, - "python": "<3.13", - }, - "fastapi": { - "package": "fastapi", - "deps": { - "*": [ - "httpx", - "anyio<4.0.0", - "python-multipart", - "pytest-asyncio", - "requests", - ] - }, - "python": ">=3.7", - }, - "flask": { - "package": "flask", - "deps": { - "*": ["flask-login", "werkzeug"], - "<2.0": ["werkzeug<2.1.0", "markupsafe<2.1.0"], - }, - }, - "gql": { - "package": "gql[all]", - "deps": {}, - }, - "graphene": { - "package": "graphene", - "deps": { - "*": ["blinker", "fastapi", "flask", "httpx"], - "py3.6": ["aiocontextvars"], - }, - }, - "grpc": { - "package": "grpcio", - "deps": { - "*": ["protobuf", "mypy-protobuf", "types-protobuf", "pytest-asyncio"], - }, - "python": ">=3.7", - }, - "httpx": { - "package": "httpx", - "deps": { - "*": ["anyio<4.0.0", "pytest-httpx"], - "==0.16": ["pytest-httpx==0.10.0"], - "==0.18": ["pytest-httpx==0.12.0"], - "==0.20": ["pytest-httpx==0.14.0"], - "==0.22": ["pytest-httpx==0.19.0"], - "==0.23": ["pytest-httpx==0.21.0"], - "==0.24": ["pytest-httpx==0.22.0"], - "==0.25": ["pytest-httpx==0.25.0"], - }, - }, - "huey": { - "package": "huey", - "deps": { - "*": [], - }, - }, - "huggingface_hub": { - "package": "huggingface_hub", - "deps": {"*": []}, - }, - "langchain": { - "package": "langchain", - "deps": { - "*": ["openai", "tiktoken", "httpx"], - ">=0.3": ["langchain-community"], - }, - }, - "langchain_notiktoken": { - "package": "langchain", - "deps": { - "*": ["openai", "httpx"], - ">=0.3": ["langchain-community"], - }, - }, - "litestar": { - "package": "litestar", - "deps": { - "*": ["pytest-asyncio", "python-multipart", "requests", "cryptography"], - "<=2.6": ["httpx<0.28"], - }, - }, - "loguru": { - "package": "loguru", - "deps": { - "*": [], - }, - }, - # XXX - # openai-latest: tiktoken~=0.6.0 - "openai": { - "package": "openai", - "deps": { - "*": ["pytest-asyncio", "tiktoken", "httpx"], - "<=1.22": ["httpx<0.28.0"], - }, - }, - "openai_notiktoken": { - "package": "openai", - "deps": { - "*": ["pytest-asyncio", "httpx"], - "<=1.22": ["httpx<0.28.0"], - }, - }, - "openfeature": { - "package": "openfeature-sdk", - "deps": { - "*": [], - }, - }, - "launchdarkly": { - "package": "launchdarkly-server-sdk", - "deps": { - "*": [], - }, - }, - "opentelemetry": { - "package": "opentelemetry-distro", - "deps": { - "*": [], - }, - }, - "pure_eval": { - "package": "pure_eval", - "deps": { - "*": [], - }, - }, - "pymongo": { - "package": "pymongo", - "deps": { - "*": ["mockupdb"], - }, - }, - "pyramid": { - "package": "pyramid", - "deps": { - "*": ["werkzeug<2.1.0"], - }, - }, - "quart": { - "package": "quart", - "deps": { - "*": [ - "quart-auth", - "pytest-asyncio", - "werkzeug", - ], - "<=0.19": [ - "blinker<1.6", - "jinja2<3.1.0", - "Werkzeug<2.1.0", - "hypercorn<0.15.0", - ], - "py3.8": ["taskgroup==0.0.0a4"], - }, - }, - "ray": { - "package": "ray", - "deps": {}, - }, - "redis": { - "package": "redis", - "deps": { - "*": ["fakeredis!=1.7.4", "pytest<8.0.0", "pytest-asyncio"], - "py3.6,py3.7": [ - "fakeredis!=2.26.0" - ], # https://github.com/cunla/fakeredis-py/issues/341 - }, - }, - "redis_py_cluster_legacy": { - "package": "redis-py-cluster", - "deps": {}, - }, - "requests": { - "package": "requests", - "deps": {}, - }, - "rq": { - "package": "rq", - "deps": { - "*": ["fakeredis"], - "<0.13": [ - "fakeredis<1.0", - "redis<3.2.2", - ], # https://github.com/jamesls/fakeredis/issues/245 - ">=0.13,<=1.10": ["fakeredis>=1.0,<1.7.4"], - "py3.6,py3.7": [ - "fakeredis!=2.26.0" - ], # https://github.com/cunla/fakeredis-py/issues/341 - }, - }, - "sanic": { - "package": "sanic", - "deps": { - "*": ["websockets<11.0", "aiohttp", "sanic_testing"], - ">=22.0": ["sanic_testing"], - "py3.6": ["aiocontextvars==0.2.1"], - }, - }, - "spark": { - "package": "pyspark", - "deps": {}, - "python": ">=3.8", - }, - "starlette": { - "package": "starlette", - "deps": { - "*": [ - "pytest-asyncio", - "python-multipart", - "requests", - "anyio<4.0.0", - "jinja2", - "httpx", - ], - "<=0.36": ["httpx<0.28.0"], - "<0.15": ["jinja2<3.1"], - "py3.6": ["aiocontextvars"], - }, - }, - "starlite": { - "package": "starlite", - "deps": { - "*": [ - "pytest-asyncio", - "python-multipart", - "requests", - "cryptography", - "pydantic<2.0.0", - "httpx<0.28", - ], - }, - "python": "<=3.11", - }, - "sqlalchemy": { - "package": "sqlalchemy", - "deps": {}, - }, - "strawberry": { - "package": "strawberry-graphql[fastapi,flask]", - "deps": { - "*": ["fastapi", "flask", "httpx"], - }, - }, - "tornado": { - "package": "tornado", - "deps": { - "*": ["pytest"], - "<=6.4.1": [ - "pytest<8.2" - ], # https://github.com/tornadoweb/tornado/pull/3382 - "py3.6": ["aiocontextvars"], - }, - }, - "trytond": { - "package": "trytond", - "deps": { - "*": ["werkzeug"], - "<=5.0": ["werkzeug<1.0"], - }, - }, - "typer": { - "package": "typer", - "deps": {}, - }, - "unleash": { - "package": "UnleashClient", - "deps": {}, - }, -} +TEST_SUITE_CONFIG = {} From 85724894bf02493caad70c10a3f56d1561a32836 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 22 Jan 2025 10:50:06 +0100 Subject: [PATCH 9/9] dont require deps --- scripts/populate_tox/populate_tox.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index e26d6a580f..53ad9f01b4 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -347,6 +347,10 @@ def _render_python_versions(python_versions: list[Version]) -> str: def _render_dependencies(integration: str, releases: list[Version]) -> list[str]: rendered = [] + + if TEST_SUITE_CONFIG[integration].get("deps") is None: + return rendered + for constraint, deps in TEST_SUITE_CONFIG[integration]["deps"].items(): if constraint == "*": for dep in deps: