Repository: pypa/pip

Issue:
Add support for pre-releases to our release automation
Currently, our release automation does not permit / handle pre-release version numbers being passed as part of the CLI (you can't do `nox -s prepare-release -- 20.1.b1` for example).

This would likely be important for the work being undertaken as part of https://wiki.python.org/psf/Pip2020DonorFundedRoadmap, to roll out a fix for #988.


Discussion:
It would be really nice if we had this -- I don't think we need to implement it IN April prior to #7951 but @pradyunsg should please tell me if I am wrong.
Yea. This isn't needed for 20.1, and does not affect / get affected by the release cycle.

Resolving patch:
diff --git a/noxfile.py b/noxfile.py
--- a/noxfile.py
+++ b/noxfile.py
@@ -155,9 +155,9 @@ def lint(session):
 # -----------------------------------------------------------------------------
 @nox.session(name="prepare-release")
 def prepare_release(session):
-    version = release.get_version_from_arguments(session.posargs)
+    version = release.get_version_from_arguments(session)
     if not version:
-        session.error("Usage: nox -s prepare-release -- YY.N[.P]")
+        session.error("Usage: nox -s prepare-release -- <version>")
 
     session.log("# Ensure nothing is staged")
     if release.modified_files_in_git("--staged"):
@@ -190,7 +190,7 @@ def prepare_release(session):
 
 @nox.session(name="build-release")
 def build_release(session):
-    version = release.get_version_from_arguments(session.posargs)
+    version = release.get_version_from_arguments(session)
     if not version:
         session.error("Usage: nox -s build-release -- YY.N[.P]")
 
@@ -249,7 +249,7 @@ def build_dists(session):
 
 @nox.session(name="upload-release")
 def upload_release(session):
-    version = release.get_version_from_arguments(session.posargs)
+    version = release.get_version_from_arguments(session)
     if not version:
         session.error("Usage: nox -s upload-release -- YY.N[.P]")
 
diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py
--- a/tools/automation/release/__init__.py
+++ b/tools/automation/release/__init__.py
@@ -14,24 +14,25 @@
 from nox.sessions import Session
 
 
-def get_version_from_arguments(arguments: List[str]) -> Optional[str]:
+def get_version_from_arguments(session: Session) -> Optional[str]:
     """Checks the arguments passed to `nox -s release`.
 
     If there is only 1 argument that looks like a pip version, returns that.
     Otherwise, returns None.
     """
-    if len(arguments) != 1:
+    if len(session.posargs) != 1:
         return None
-
-    version = arguments[0]
-
-    parts = version.split('.')
-    if not 2 <= len(parts) <= 3:
-        # Not of the form: YY.N or YY.N.P
-        return None
-
-    if not all(part.isdigit() for part in parts):
-        # Not all segments are integers.
+    version = session.posargs[0]
+
+    # We delegate to a script here, so that it can depend on packaging.
+    session.install("packaging")
+    cmd = [
+        os.path.join(session.bin, "python"),
+        "tools/automation/release/check_version.py",
+        version
+    ]
+    not_ok = subprocess.run(cmd).returncode
+    if not_ok:
         return None
 
     # All is good.
diff --git a/tools/automation/release/check_version.py b/tools/automation/release/check_version.py
new file mode 100644
--- /dev/null
+++ b/tools/automation/release/check_version.py
@@ -0,0 +1,43 @@
+"""Checks if the version is acceptable, as per this project's release process.
+"""
+
+import sys
+from datetime import datetime
+from typing import Optional
+
+from packaging.version import InvalidVersion, Version
+
+
+def is_this_a_good_version_number(string: str) -> Optional[str]:
+    try:
+        v = Version(string)
+    except InvalidVersion as e:
+        return str(e)
+
+    if v.local:
+        return "Nope. PyPI refuses local release versions."
+
+    if v.dev:
+        return "No development releases on PyPI. What are you even thinking?"
+
+    if v.is_prerelease and v.pre[0] != "b":
+        return "Only beta releases are allowed. No alphas."
+
+    release = v.release
+    expected_major = datetime.now().year % 100
+
+    if len(release) not in [2, 3]:
+        return "Not of the form: {0}.N or {0}.N.P".format(expected_major)
+
+    return None
+
+
+def main() -> None:
+    problem = is_this_a_good_version_number(sys.argv[1])
+    if problem is not None:
+        print("ERROR:", problem)
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()


Test patch: