Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!-- Generated by tasks/upgrade_wheels.py; do not edit by hand. -->

# Third-party notices

virtualenv is distributed under the MIT License, see `LICENSE`. The wheels under `src/virtualenv/seed/wheels/embed/` are
redistributed unmodified and remain under their own licenses, reproduced below. Each wheel also carries its own license
text inside its `.dist-info/licenses/` directory, including the licenses of anything it in turn vendors.

## pip

Bundled as: `pip-26.0.1-py3-none-any.whl`, `pip-26.2.1-py3-none-any.whl`

Upstream: https://pypi.org/project/pip/

```
Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

## setuptools

Bundled as: `setuptools-82.0.1-py3-none-any.whl`, `setuptools-84.0.0-py3-none-any.whl`

Upstream: https://pypi.org/project/setuptools/

```
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
```
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ keywords = [
"virtual",
]
license = "MIT"
license-files = [
"LICENSE",
"THIRD-PARTY-NOTICES.md",
]
maintainers = [
{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" },
{ name = "Rahul Devikar", email = "rahuldevikar5512@gmail.com" },
Expand Down
63 changes: 63 additions & 0 deletions tasks/upgrade_wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import shutil
import subprocess
import sys
import zipfile
from collections import OrderedDict, defaultdict
from pathlib import Path
from tempfile import TemporaryDirectory
Expand All @@ -20,11 +21,13 @@
BUNDLED = ["pip", "setuptools"]
SUPPORT = [(3, i) for i in range(9, 17)]
DEST = Path(__file__).resolve().parents[1] / "src" / "virtualenv" / "seed" / "wheels" / "embed"
NOTICES_DEST = Path(__file__).resolve().parents[1] / "THIRD-PARTY-NOTICES.md"


def run() -> NoReturn:
if "--regen" in sys.argv[1:]:
render_init()
render_notices()
raise SystemExit(0)
old_batch = {i.name for i in DEST.iterdir() if i.suffix == ".whl"}
with TemporaryDirectory() as temp:
Expand All @@ -39,6 +42,7 @@ def run() -> NoReturn:
print(f"Outcome {outcome} added {added} removed {removed}") # ruff:ignore[print]
_write_changelog(added, removed)
render_init(folders=folders)
render_notices()
raise SystemExit(outcome)


Expand Down Expand Up @@ -218,6 +222,65 @@ def _hash_bundled_wheel(path: Path) -> str:
subprocess.run([sys.executable, "-m", "ruff", "format", str(dest_target), "--preview"], check=False)


def render_notices() -> None:
"""Write ``THIRD-PARTY-NOTICES.md`` from the wheels currently bundled in DEST.

Reads the ``BUNDLE_SHA256`` table ``render_init`` just wrote, so the notices always match what is actually bundled.
Only the license of the wheel virtualenv itself redistributes is reproduced here - whatever that wheel in turn
vendors (pip's own ``_vendor`` tree, for example) already carries its license inside the wheel and stays there
rather than being duplicated at the top level.

"""
tree = ast.parse((DEST / "__init__.py").read_text(encoding="utf-8"))
wheel_names: list[str] = []
for node in tree.body:
if isinstance(node, ast.Assign) and any(
isinstance(target, ast.Name) and target.id == "BUNDLE_SHA256" for target in node.targets
):
wheel_names = sorted(ast.literal_eval(node.value))
break
groups: dict[tuple[str, str], list[str]] = defaultdict(list)
for wheel_name in wheel_names:
distribution = wheel_name.split("-")[0]
groups[distribution, _wheel_license_text(DEST / wheel_name)].append(wheel_name)
sections = []
for (distribution, text), names in sorted(groups.items()):
filenames = ", ".join(f"``{name}``" for name in sorted(names))
sections.append(
f"## {distribution}\n\nBundled as: {filenames}\n\n"
f"Upstream: https://pypi.org/project/{distribution}/\n\n"
f"```\n{text.strip()}\n```\n"
)
header = dedent("""\
<!-- Generated by tasks/upgrade_wheels.py; do not edit by hand. -->
# Third-party notices

virtualenv is distributed under the MIT License, see `LICENSE`. The wheels under
`src/virtualenv/seed/wheels/embed/` are redistributed unmodified and remain under their own licenses,
reproduced below. Each wheel also carries its own license text inside its `.dist-info/licenses/`
directory, including the licenses of anything it in turn vendors.

""")
NOTICES_DEST.write_text(header + "\n".join(sections), encoding="utf-8")


def _wheel_license_text(path: Path) -> str:
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
dist_info = next(name.split("/", 1)[0] for name in names if name.endswith(".dist-info/METADATA"))
prefix = f"{dist_info}/licenses/"
# top-level files only: a nested match would be a license belonging to something the wheel itself vendors
candidates = sorted(
name
for name in names
if name.startswith(prefix) and "/" not in name[len(prefix) :] and "LICENSE" in Path(name).name.upper()
)
if not candidates:
msg = f"no top-level license file found under {prefix} in {path.name}"
raise RuntimeError(msg)
return archive.read(candidates[0]).decode("utf-8")


def _support_table_from_existing_init() -> OrderedDict[str, OrderedDict[str, str]]:
source = (DEST / "__init__.py").read_text(encoding="utf-8")
tree = ast.parse(source)
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/seed/wheels/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def test_every_wheel_on_disk_has_sha256() -> None:
assert on_disk == BUNDLE_SHA256.keys()


def test_notices_mention_every_bundled_distribution() -> None:
# rooted at this test file rather than BUNDLE_FOLDER: a non-editable install puts the installed package
# under site-packages with no relationship to the checkout, but tests/ always runs from the repo itself
repo_root = Path(__file__).resolve().parents[4]
notices = (repo_root / "THIRD-PARTY-NOTICES.md").read_text(encoding="utf-8")
distributions = {wheel_name.split("-")[0] for wheel_name in BUNDLE_SHA256}
missing = {name for name in distributions if f"## {name}" not in notices}
assert not missing, f"THIRD-PARTY-NOTICES.md is missing a section for: {sorted(missing)}"


def test_get_embed_wheel_verifies_pip(for_py_version: str) -> None:
wheel = get_embed_wheel("pip", for_py_version)
assert wheel is not None
Expand Down