diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 00000000..27745a45
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,49 @@
+name: Check Scripts
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ correctly-generated:
+ name: "are correctly generated"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ - run: pip install nox
+
+ - run: nox -s generate
+ - name: Check regenerated scripts vs what is generated by automation.
+ run: git diff --exit-code
+
+ work-as-advertised:
+ name: "work as advertised"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ - run: pip install nox
+
+ # Install supported Python versions
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 2.7
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.5
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.6
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+
+ # Check that the scripts work.
+ - run: nox -s check
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 80ac12af..00000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Tests
-
-on:
- push:
- pull_request:
-
-jobs:
- tests:
- name: ${{ matrix.os }} / ${{ matrix.python }}
- runs-on: ${{ matrix.os }}-latest
- strategy:
- matrix:
- os: [Ubuntu, Windows]
- python: ['2.7', '3.7']
- steps:
- - uses: actions/checkout@master
-
- - uses: actions/setup-python@v2
- - run: pip install nox
-
- - uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python }}
- - name: nox + Windows + Python 2.7 workaround
- # This is in PATH, so nox resolves to it - but then subsequent steps fail
- run: rm C:/ProgramData/Chocolatey/bin/python2.7.exe
- if: matrix.os == 'Windows' && matrix.python == '2.7'
-
- - run: nox -s tests-${{ matrix.python }}
diff --git a/.gitignore b/.gitignore
index 23276e9b..5ef39967 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/.nox/
/venv/
__pycache__/
+.web_cache/
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..58a87c0a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+# get-pip.py
+
+`get-pip.py` is a bootstrapping script that enables users to install pip,
+setuptools, and wheel in Python environments that don't already have them. You
+should not directly reference the files located in this repository and instead
+use the versions located at .
+
+## Usage
+
+```console
+$ curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py
+$ python get-pip.py
+```
+
+Upon execution, `get-pip.py` will install `pip`, `setuptools` and `wheel` in
+the current Python environment.
+
+It is possible to provide additional arguments to the underlying script. These
+are passed through to the underlying `pip install` command, and can thus be
+used to constraint the versions of the packages, or to pass other pip options
+such as `--no-index`.
+
+```console
+$ python get-pip.py "pip < 21.0" "setuptools < 50.0" "wheel < 1.0"
+$ python get-pip.py --no-index --find-links=/local/copies
+```
+
+### get-pip.py options
+
+This script also has it's own options, which control which packages it will
+install.
+
+- `--no-setuptools`: do not attempt to install `setuptools`.
+- `--no-wheel`: do not attempt to install `wheel`.
+
+## Development
+
+You need to have a [`nox`](https://nox.readthedocs.io/) available on the CLI.
+
+### How it works
+
+`get-pip.py` bundles a copy of pip with a tiny amount of glue code. This glue
+code comes from the `templates/` directory.
+
+### Updating after a pip release
+
+If you just made a pip release, run `nox -s update-for-release -- `.
+This session will handle all the script updates (by running `generate`), commit
+the changes and tag the commit.
+
+IMPORTANT: Check that the correct files got modified before pushing. The session
+will pause to let you do this.
+
+### Generating the scripts
+
+Run `nox -s generate`.
+
+## Discussion
+
+If you run into bugs, you can file them in our [issue tracker].
+
+You can also join `#pypa` or `#pypa-dev` on Freenode to ask questions or
+get involved.
+
+[issue tracker]: https://github.com/pypa/get-pip/issues
+
+## Code of Conduct
+
+Everyone interacting in the get-pip project's codebases, issue trackers, chat
+rooms, and mailing lists is expected to follow the [PSF Code of Conduct].
+
+[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
diff --git a/README.rst b/README.rst
deleted file mode 100644
index d58210bc..00000000
--- a/README.rst
+++ /dev/null
@@ -1,86 +0,0 @@
-get-pip.py
-==========
-
-``get-pip.py`` is a bootstrapping script that enables users to install pip,
-setuptools, and wheel in Python environments that don't already have them. You
-should not directly reference the files located in this repository and instead
-use the versions located at https://bootstrap.pypa.io/.
-
-
-Usage
------
-
-.. code-block:: console
-
- $ curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py
- $ python get-pip.py
-
-Upon execution, ``get-pip.py`` will install ``pip``, ``setuptools`` and
-``wheel`` in the current Python environment.
-
-It is possible to provide additional arguments to the underlying script. These
-are passed through to the underlying `pip install` command, and can thus be
-used to constraint the versions of the packages, or to pass other pip options
-such as ``--no-index``.
-
-.. code-block:: console
-
- $ python get-pip.py "pip < 21.0" "setuptools < 50.0" "wheel < 1.0"
- $ python get-pip.py --no-index --find-links=/local/copies
-
-get-pip.py options
-^^^^^^^^^^^^^^^^^^
-
-This script also has it's own options, which control which packages it will
-install.
-
-``--no-setuptools``
- If set, do not attempt to install ``setuptools``.
-
-``--no-wheel``
- If set, do not attempt to install ``wheel``.
-
-
-Development
------------
-
-Most of the work of ``get-pip.py`` comes from the copy of pip that is bundled
-inside of it. However, there is a tiny bit of glue code located inside of
-``template.py`` to enable ``get-pip.py`` to actually work.
-
-Install the dependencies from ``requirements.txt`` to get setup with the repo.
-You may want to perform something like:
-
-.. code-block:: console
-
- $ python3 -m venv venv
- $ source venv/bin/activate
- $ pip install -r requirements.txt
-
-Any pull request should include regenerated files, which can be generated by
-running:
-
-.. code-block:: console
-
- $ invoke generate
-
-
-Discussion
-----------
-
-If you run into bugs, you can file them in our `issue tracker`_.
-
-You can also join ``#pypa`` or ``#pypa-dev`` on Freenode to ask questions or
-get involved.
-
-
-.. _`issue tracker`: https://github.com/pypa/get-pip/issues
-
-
-Code of Conduct
----------------
-
-Everyone interacting in the get-pip project's codebases, issue trackers, chat
-rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
-
-.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
diff --git a/noxfile.py b/noxfile.py
index b2081f45..a542f74d 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,9 +1,94 @@
import os
+import textwrap
+from pathlib import Path
import nox
+nox.options.sessions = ["check", "generate"]
-@nox.session(python=['2.7', '3.7'])
-def tests(session):
- session.install('pytest')
- session.run('pytest')
+
+# Keep versions in sync with .github/workflows/check.yml
+@nox.session(
+ python=["2.6", "2.7", "3.2", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9"]
+)
+def check(session):
+ """Ensure that get-pip.py for various Python versions, works on that version."""
+
+ # Find the appropriate get-pip.py file
+ public = Path(".")
+ locations = [
+ public / session.python / "get-pip.py",
+ public / "get-pip.py",
+ ]
+ for location in locations:
+ if location.exists():
+ break
+ else: # AKA nobreak
+ raise RuntimeError("There is no public get-pip.py")
+
+ # Get rid of provided-by-nox pip
+ session.run("python", "-m", "pip", "uninstall", "pip", "--yes")
+ # Run the get-pip.py file
+ session.run("python", str(location))
+ # Ensure that pip is installed
+ session.run("python", "-m", "pip", "--version")
+ session.run("pip", "--version")
+
+
+@nox.session
+def generate(session):
+ """Update the scripts, to the latest versions."""
+ session.install("packaging", "requests", "cachecontrol[filecache]")
+
+ session.run("python", "scripts/generate.py")
+
+
+@nox.session(name="update-for-release")
+def update_for_release(session):
+ """Automation to run after a pip release."""
+ allowed_upstreams = [
+ "git@github.com:pypa/get-pip.git",
+ "https://github.com/pypa/get-pip.git",
+ ]
+
+ if len(session.posargs) != 1:
+ session.error("Usage: nox -s update-for-release -- ")
+
+ release_version, = session.posargs
+
+ session.install("release-helper")
+ session.run("release-helper", "version-check-validity", release_version)
+ session.run("release-helper", "git-check-tag", release_version, "--does-not-exist")
+ session.run("release-helper", "git-check-remote", "upstream", *allowed_upstreams)
+ session.run("release-helper", "git-check-branch", "master")
+ session.run("release-helper", "git-check-clean")
+
+ # Generate the scripts.
+ generate(session)
+
+ # Make the commit and present it to the user.
+ session.run("git", "add", ".", external=True)
+ session.run(
+ "git", "commit", "-m", f"Update to {release_version}", external=True
+ )
+ session.run("git", "show", "HEAD", "--stat")
+
+ input(textwrap.dedent(
+ """\
+ **********************************************
+ * IMPORTANT: Check which files got modified. *
+ **********************************************
+
+ Press enter to continue. This script will generate a signed tag for this
+ commit and push it -- which will publish these changes.
+ """
+ ))
+
+ session.run(
+ # fmt: off
+ "git", "tag", release_version, "-m", f"Release {release_version}",
+ "--annotate", "--sign",
+ external=True,
+ # fmt: on
+ )
+ session.run("git", "push", "upstream", "master", release_version, external=True)
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 0c13a44b..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-invoke
-packaging
diff --git a/scripts/generate.py b/scripts/generate.py
new file mode 100644
index 00000000..e7e142e1
--- /dev/null
+++ b/scripts/generate.py
@@ -0,0 +1,197 @@
+"""Update all the get-pip.py scripts."""
+
+import hashlib
+import operator
+import re
+import subprocess
+import sys
+from base64 import b85encode
+from functools import lru_cache
+from io import BytesIO
+from pathlib import Path, PosixPath
+from typing import Dict, Iterable, List, Tuple
+from zipfile import ZipFile
+
+import requests
+from cachecontrol import CacheControl
+from cachecontrol.caches.file_cache import FileCache
+from packaging.specifiers import SpecifierSet
+from packaging.version import Version
+
+SCRIPT_CONSTRAINTS = {
+ "default": {
+ "pip": "",
+ "setuptools": "",
+ "wheel": "",
+ },
+ "2.6": {
+ "pip": "<10",
+ "setuptools": "<37",
+ "wheel": "<0.30",
+ },
+ "2.7": {
+ "pip": "<21.0",
+ "setuptools": "<45",
+ "wheel": "",
+ },
+ "3.2": {
+ "pip": "<8",
+ "setuptools": "<30",
+ "wheel": "<0.30",
+ },
+ "3.3": {
+ "pip": "<18",
+ "setuptools": "",
+ "wheel": "<0.30",
+ },
+ "3.4": {
+ "pip": "<19.2",
+ "setuptools": "",
+ "wheel": "",
+ },
+ "3.5": {
+ "pip": "<21.0",
+ "setuptools": "",
+ "wheel": "",
+ },
+}
+
+
+def get_all_pip_versions() -> Dict[Version, Tuple[str, str]]:
+ data = requests.get("https://pypi.python.org/pypi/pip/json").json()
+
+ versions = sorted(Version(s) for s in data["releases"].keys())
+
+ retval = {}
+ for version in versions:
+ wheels = [
+ (file["url"], file["md5_digest"])
+ for file in data["releases"][str(version)]
+ if file["url"].endswith(".whl")
+ ]
+ if not wheels:
+ continue
+ assert len(wheels) == 1, (version, wheels)
+ retval[version] = wheels[0]
+ return retval
+
+
+def determine_latest(versions: Iterable[Version], *, constraint: str):
+ assert sorted(versions) == list(versions)
+ return list(SpecifierSet(constraint).filter(versions))[-1]
+
+
+@lru_cache
+def get_ordered_templates() -> List[Tuple[Version, Path]]:
+ all_templates = list(Path("./templates").iterdir())
+
+ fallback = None
+ ordered_templates = []
+ for template in all_templates:
+ if template.name == "default.py":
+ fallback = template
+ continue
+ assert template.name.startswith("pre-")
+
+ version_str = template.name[4:-3] # "pre-{version}.py"
+ version = Version(version_str)
+ ordered_templates.append((version, template))
+
+ # Use the epoch mechanism, to force the fallback to the end.
+ assert fallback is not None
+ ordered_templates.append((Version("1!0"), fallback))
+
+ # Order the (version, template) tuples, by increasing version numbers.
+ return sorted(ordered_templates, key=operator.itemgetter(0))
+
+
+def determine_template(version: Version):
+ ordered_templates = get_ordered_templates()
+ for template_version, template in ordered_templates:
+ if version < template_version:
+ return template
+ else:
+ assert template.name == "default.py"
+ return template
+
+
+def download_wheel(url: str, expected_md5: str) -> bytes:
+ session = requests.session()
+ cached_session = CacheControl(session, cache=FileCache(".web_cache"))
+
+ response = cached_session.get(url)
+ return response.content
+
+
+def repack_wheel(data: bytes):
+ """Remove the .dist-info, so that this is no longer a valid wheel."""
+ new_data = BytesIO()
+ with ZipFile(BytesIO(data)) as existing_zip:
+ with ZipFile(new_data, mode="w") as new_zip:
+ for zipinfo in existing_zip.infolist():
+ if re.search(r"pip-.+\.dist-info/", zipinfo.filename):
+ continue
+ new_zip.writestr(zipinfo, existing_zip.read(zipinfo))
+
+ return new_data.getvalue()
+
+
+def encode_wheel_contents(data: bytes) -> str:
+ zipdata = b85encode(data).decode("utf8")
+
+ chunked = []
+ for i in range(0, len(zipdata), 79):
+ chunked.append(zipdata[i : i + 79])
+
+ return "\n".join(chunked)
+
+
+def determine_destination(version: str) -> Path:
+ public = Path(".")
+ if not public.exists():
+ public.mkdir()
+
+ if version == "default":
+ return public / "get-pip.py"
+
+ retval = public / version / "get-pip.py"
+ if not retval.parent.exists():
+ retval.parent.mkdir()
+
+ return retval
+
+
+def main() -> None:
+ print("Fetch available pip versions...")
+ pip_versions = get_all_pip_versions()
+
+ for version, mapping in SCRIPT_CONSTRAINTS.items():
+ print(f"Working on {version}")
+ destination = determine_destination(version)
+ pip_version = determine_latest(
+ pip_versions.keys(),
+ constraint=mapping["pip"],
+ )
+ template = determine_template(pip_version)
+
+ wheel_url, wheel_hash = pip_versions[pip_version]
+ print(f" Downloading {PosixPath(wheel_url).name}")
+ original_wheel = download_wheel(wheel_url, wheel_hash)
+ repacked_wheel = repack_wheel(original_wheel)
+ encoded_wheel = encode_wheel_contents(repacked_wheel)
+
+ print(f" Generating with {template}")
+ rendered_template = template.read_text().format(
+ zipfile=encoded_wheel,
+ installed_version=pip_version,
+ pip_version=mapping["pip"],
+ setuptools_version=mapping["setuptools"],
+ wheel_version=mapping["wheel"],
+ )
+
+ print(f" Writing to {destination}")
+ destination.write_text(rendered_template)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tasks/__init__.py b/tasks/__init__.py
deleted file mode 100644
index 250fe9c4..00000000
--- a/tasks/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import invoke
-
-from . import generate
-
-ns = invoke.Collection(generate)
diff --git a/tasks/generate.py b/tasks/generate.py
deleted file mode 100644
index 0933821c..00000000
--- a/tasks/generate.py
+++ /dev/null
@@ -1,150 +0,0 @@
-import base64
-import hashlib
-import io
-import json
-import os
-import os.path
-import re
-import zipfile
-
-import urllib.request
-
-import invoke
-import packaging.specifiers
-import packaging.version
-
-
-PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
-
-
-def _path(pyversion=None):
- parts = [PROJECT_ROOT, pyversion, "get-pip.py"]
- return os.path.join(*filter(None, parts))
-
-
-def _template(name="default.py"):
- return os.path.join(PROJECT_ROOT, "templates", name)
-
-
-@invoke.task
-def installer(ctx,
- pip_version=None, wheel_version=None, setuptools_version=None,
- installer_path=_path(), template_path=_template()):
-
- print("[generate.installer] Generating installer {} (using {})".format(
- os.path.relpath(installer_path, PROJECT_ROOT),
- "pip" + pip_version if pip_version is not None else "latest"
- ))
-
- # Load our wrapper template
- with open(template_path, "r", encoding="utf8") as fp:
- WRAPPER_TEMPLATE = fp.read()
-
- # Get all of the versions on PyPI
- resp = urllib.request.urlopen("https://pypi.python.org/pypi/pip/json")
- data = json.loads(resp.read().decode("utf8"))
- versions = sorted(data["releases"].keys(), key=packaging.version.parse)
-
- # Filter our list of versions based on the given specifier
- s = packaging.specifiers.SpecifierSet(
- "" if pip_version is None else pip_version)
- versions = list(s.filter(versions))
-
- # Select the latest version that matches our specifier is
- latest = versions[-1]
-
- # Select the wheel file (we assume there will be only one per release)
- file_urls = [
- (x["url"], x["md5_digest"])
- for x in data["releases"][latest]
- if x["url"].endswith(".whl")
- ]
- assert len(file_urls) == 1
- url, expected_hash = file_urls[0]
-
- # Fetch the file itself.
- data = urllib.request.urlopen(url).read()
- assert hashlib.md5(data).hexdigest() == expected_hash
-
- # We need to repack the downloaded wheel file to remove the .dist-info,
- # after this it will no longer be a valid wheel, but it will still work
- # perfectly fine for our use cases.
- new_data = io.BytesIO()
- with zipfile.ZipFile(io.BytesIO(data)) as existing_zip:
- with zipfile.ZipFile(new_data, mode="w") as new_zip:
- for zinfo in existing_zip.infolist():
- if re.search(r"pip-.+\.dist-info/", zinfo.filename):
- continue
- new_zip.writestr(zinfo, existing_zip.read(zinfo))
- data = new_data.getvalue()
-
- # Write out the wrapper script that will take the place of the zip script
- # The reason we need to do this instead of just directly executing the
- # zip script is that while Python will happily execute a zip script if
- # passed it on the file system, it will not however allow this to work if
- # passed it via stdin. This means that this wrapper script is required to
- # make ``curl https://...../get-pip.py | python`` continue to work.
- print(
- "[generate.installer] Write the wrapper script with the bundled zip "
- "file"
- )
-
- zipdata = base64.b85encode(data).decode("utf8")
- chunked = []
-
- for i in range(0, len(zipdata), 79):
- chunked.append(zipdata[i:i + 79])
-
- os.makedirs(os.path.dirname(installer_path), exist_ok=True)
- with open(installer_path, "w") as fp:
- fp.write(
- WRAPPER_TEMPLATE.format(
- pip_version="" if pip_version is None else pip_version,
- wheel_version="" if wheel_version is None else wheel_version,
- setuptools_version=(
- "" if setuptools_version is None else setuptools_version),
- installed_version=latest,
- zipfile="\n".join(chunked),
- ),
- )
-
- # Ensure the permissions on the newly created file
- oldmode = os.stat(installer_path).st_mode & 0o7777
- newmode = (oldmode | 0o555) & 0o7777
- os.chmod(installer_path, newmode)
-
- print("[generate.installer] Generated installer")
-
-
-@invoke.task(
- default=True,
- pre=[
- invoke.call(installer),
- invoke.call(installer, installer_path=_path("2.6"),
- template_path=_template("pre-10.py"),
- pip_version="<10",
- wheel_version="<0.30",
- setuptools_version="<37"),
- invoke.call(installer, installer_path=_path("2.7"),
- template_path=_template("pre-21.0.py"),
- pip_version="<21.0",
- setuptools_version="<45"),
- invoke.call(installer, installer_path=_path("3.2"),
- template_path=_template("pre-10.py"),
- pip_version="<8",
- wheel_version="<0.30",
- setuptools_version="<30"),
- invoke.call(installer, installer_path=_path("3.3"),
- template_path=_template("pre-18.1.py"),
- pip_version="<18",
- wheel_version="<0.30"),
- invoke.call(installer, installer_path=_path("3.4"),
- template_path=_template("pre-19.3.py"),
- pip_version="<19.2"),
- invoke.call(installer, installer_path=_path("3.5"),
- template_path=_template("pre-21.0.py"),
- pip_version="<21.0"),
- ],
-)
-def all(ctx):
- pass
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/test_basics.py b/tests/test_basics.py
deleted file mode 100644
index 7f2df179..00000000
--- a/tests/test_basics.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def test_basics():
- pass