)]}'
{
  "commit": "b38786c556d7e777db40ebcca31c68f76b967c40",
  "tree": "f98b1c37368b75f0e29b77fe18e9a0e029e31eb4",
  "parents": [
    "36e59d54f374da3e3f6c75cbd372b215fde76e7d"
  ],
  "author": {
    "name": "Kevin Park",
    "email": "kevin.park1217@gmail.com",
    "time": "Thu Feb 12 07:36:55 2026 -0500"
  },
  "committer": {
    "name": "Richard Levasseur",
    "email": "rlevasseur@google.com",
    "time": "Sun Feb 22 16:53:26 2026 -0800"
  },
  "message": "fix(pypi): normalize extras in requirement strings per PEP 685 (#3588)\n\n## Summary\n\nExtras parsed from requirement strings (e.g., from `requirements.txt`)\nwere not being normalized, causing mismatches when evaluating PEP 508\nmarker expressions. For example, `sqlalchemy[postgresql-psycopg2binary]`\nwould fail to resolve `psycopg2-binary` as a transitive dependency\nbecause the wheel METADATA marker expression `extra \u003d\u003d\n\"postgresql_psycopg2binary\"` uses the underscore-normalized form (per\nPEP 685), while the extras set retained the original hyphenated form\nfrom the requirement string.\n\n## Before\n\n```\n# requirements.txt\nsqlalchemy[postgresql-psycopg2binary]\u003d\u003d2.0.36\n\n# Parsed extras: [\"postgresql-psycopg2binary\"]\n# Marker evaluation: \"postgresql-psycopg2binary\" !\u003d \"postgresql_psycopg2binary\" -\u003e MISS\n# Result: psycopg2-binary NOT included as a dependency\n```\n\n## After\n\n```\n# requirements.txt\nsqlalchemy[postgresql-psycopg2binary]\u003d\u003d2.0.36\n\n# Parsed extras: [\"postgresql_psycopg2binary\"]  (normalized)\n# Marker evaluation: \"postgresql_psycopg2binary\" \u003d\u003d \"postgresql_psycopg2binary\" -\u003e MATCH\n# Result: psycopg2-binary correctly included as a dependency\n```\n\n## Changes\n\n- **`python/private/pypi/pep508_requirement.bzl`**: Apply\n`normalize_name()` to each extra during requirement parsing, consistent\nwith how the package name is already normalized.\n- **`tests/pypi/pep508/requirement_tests.bzl`**: Updated existing test\nexpectation for case normalization and added test case for hyphenated\nextras\n(`sqlalchemy[asyncio,postgresql-psycopg2binary,postgresql-asyncpg]`).\n- **`tests/pypi/pep508/deps_tests.bzl`**: Added\n`test_extras_with_hyphens_are_normalized` integration test confirming\nthat dependencies gated behind hyphenated extras are correctly resolved.\n- **`CHANGELOG.md`**: Added entry under Unreleased \u003e Fixed.\n\nFixes #3587\n\n---------\n\nCo-authored-by: Ignas Anikevicius \u003c240938+aignas@users.noreply.github.com\u003e\n(cherry picked from commit 9fe42b1f0badfc258b159dbc0a05a8392c0234e5)\n",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "81ca1099bcc643803fbde308497f0eefce5bf834",
      "old_mode": 33188,
      "old_path": "CHANGELOG.md",
      "new_id": "a8c298e8abfcbe19aa59a447db4628d0a7153b33",
      "new_mode": 33188,
      "new_path": "CHANGELOG.md"
    },
    {
      "type": "modify",
      "old_id": "8194bb520d860b0a4a68e1de58293ed68c3075b8",
      "old_mode": 33188,
      "old_path": "python/private/pypi/BUILD.bazel",
      "new_id": "e51148a56c91fcae35f1ececab371e05bd623454",
      "new_mode": 33188,
      "new_path": "python/private/pypi/BUILD.bazel"
    },
    {
      "type": "modify",
      "old_id": "ad6589cfac53fdf550ea6132e298a61a64164ddb",
      "old_mode": 33188,
      "old_path": "python/private/pypi/pep508_deps.bzl",
      "new_id": "c004334d962321259536355718de31ef5eacaf95",
      "new_mode": 33188,
      "new_path": "python/private/pypi/pep508_deps.bzl"
    },
    {
      "type": "modify",
      "old_id": "5031ebae12ff7719a667cf693e9604b88057e092",
      "old_mode": 33188,
      "old_path": "python/private/pypi/pep508_env.bzl",
      "new_id": "9fe9dcaada10f82605049741d37c63bcccfd2d6c",
      "new_mode": 33188,
      "new_path": "python/private/pypi/pep508_env.bzl"
    },
    {
      "type": "modify",
      "old_id": "fe2cac965a294b57f1d9b56c5caca7755877876e",
      "old_mode": 33188,
      "old_path": "python/private/pypi/pep508_evaluate.bzl",
      "new_id": "61e461a54f04e4dc86191f9d9f20bec1bf846d28",
      "new_mode": 33188,
      "new_path": "python/private/pypi/pep508_evaluate.bzl"
    },
    {
      "type": "modify",
      "old_id": "b5be17f890f3bc7775240fc7f459e3f367883943",
      "old_mode": 33188,
      "old_path": "python/private/pypi/pep508_requirement.bzl",
      "new_id": "7552642572c94a65a8d9b5b6d14c106dd0df4d75",
      "new_mode": 33188,
      "new_path": "python/private/pypi/pep508_requirement.bzl"
    },
    {
      "type": "modify",
      "old_id": "1404ad6fc1ebee8f0dfe6f64120ab40d24896075",
      "old_mode": 33188,
      "old_path": "tests/pypi/pep508/deps_tests.bzl",
      "new_id": "e88acb8c568397203053077ed171c74a0a81ce27",
      "new_mode": 33188,
      "new_path": "tests/pypi/pep508/deps_tests.bzl"
    },
    {
      "type": "modify",
      "old_id": "9afb43a4374e60351bdd7c03e1b0967d7181a85a",
      "old_mode": 33188,
      "old_path": "tests/pypi/pep508/requirement_tests.bzl",
      "new_id": "2ce45922daa63cf38f2d2483d1aff9bf7d343f4a",
      "new_mode": 33188,
      "new_path": "tests/pypi/pep508/requirement_tests.bzl"
    }
  ]
}
