tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

noxfile.py (9993B)


      1 # mypy: disallow-untyped-defs=False, disallow-untyped-calls=False
      2 
      3 import contextlib
      4 import datetime
      5 import difflib
      6 import glob
      7 import os
      8 import re
      9 import shutil
     10 import subprocess
     11 import sys
     12 import tempfile
     13 import textwrap
     14 import time
     15 import webbrowser
     16 from pathlib import Path
     17 
     18 import nox
     19 
     20 nox.options.sessions = ["lint"]
     21 nox.options.reuse_existing_virtualenvs = True
     22 
     23 
     24 @nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"])
     25 def tests(session):
     26    def coverage(*args):
     27        session.run("python", "-m", "coverage", *args)
     28 
     29    # Once coverage 5 is used then `.coverage` can move into `pyproject.toml`.
     30    session.install("coverage<5.0.0", "pretend", "pytest>=6.2.0", "pip>=9.0.2")
     31    session.install(".")
     32 
     33    if "pypy" not in session.python:
     34        coverage(
     35            "run",
     36            "--source",
     37            "packaging/",
     38            "-m",
     39            "pytest",
     40            "--strict-markers",
     41            *session.posargs,
     42        )
     43        coverage("report", "-m", "--fail-under", "100")
     44    else:
     45        # Don't do coverage tracking for PyPy, since it's SLOW.
     46        session.run(
     47            "python",
     48            "-m",
     49            "pytest",
     50            "--capture=no",
     51            "--strict-markers",
     52            *session.posargs,
     53        )
     54 
     55 
     56 @nox.session(python="3.9")
     57 def lint(session):
     58    # Run the linters (via pre-commit)
     59    session.install("pre-commit")
     60    session.run("pre-commit", "run", "--all-files")
     61 
     62    # Check the distribution
     63    session.install("build", "twine")
     64    session.run("pyproject-build")
     65    session.run("twine", "check", *glob.glob("dist/*"))
     66 
     67 
     68 @nox.session(python="3.9")
     69 def docs(session):
     70    shutil.rmtree("docs/_build", ignore_errors=True)
     71    session.install("furo")
     72    session.install("-e", ".")
     73 
     74    variants = [
     75        # (builder, dest)
     76        ("html", "html"),
     77        ("latex", "latex"),
     78        ("doctest", "html"),
     79    ]
     80 
     81    for builder, dest in variants:
     82        session.run(
     83            "sphinx-build",
     84            "-W",
     85            "-b",
     86            builder,
     87            "-d",
     88            "docs/_build/doctrees/" + dest,
     89            "docs",  # source directory
     90            "docs/_build/" + dest,  # output directory
     91        )
     92 
     93 
     94 @nox.session
     95 def release(session):
     96    package_name = "packaging"
     97    version_file = Path(f"{package_name}/__about__.py")
     98    changelog_file = Path("CHANGELOG.rst")
     99 
    100    try:
    101        release_version = _get_version_from_arguments(session.posargs)
    102    except ValueError as e:
    103        session.error(f"Invalid arguments: {e}")
    104        return
    105 
    106    # Check state of working directory and git.
    107    _check_working_directory_state(session)
    108    _check_git_state(session, release_version)
    109 
    110    # Prepare for release.
    111    _changelog_update_unreleased_title(release_version, file=changelog_file)
    112    session.run("git", "add", str(changelog_file), external=True)
    113    _bump(session, version=release_version, file=version_file, kind="release")
    114 
    115    # Tag the release commit.
    116    # fmt: off
    117    session.run(
    118        "git", "tag",
    119        "-s", release_version,
    120        "-m", f"Release {release_version}",
    121        external=True,
    122    )
    123    # fmt: on
    124 
    125    # Prepare for development.
    126    _changelog_add_unreleased_title(file=changelog_file)
    127    session.run("git", "add", str(changelog_file), external=True)
    128 
    129    major, minor = map(int, release_version.split("."))
    130    next_version = f"{major}.{minor + 1}.dev0"
    131    _bump(session, version=next_version, file=version_file, kind="development")
    132 
    133    # Checkout the git tag.
    134    session.run("git", "checkout", "-q", release_version, external=True)
    135 
    136    session.install("build", "twine")
    137 
    138    # Build the distribution.
    139    session.run("python", "-m", "build")
    140 
    141    # Check what files are in dist/ for upload.
    142    files = sorted(glob.glob("dist/*"))
    143    expected = [
    144        f"dist/{package_name}-{release_version}-py3-none-any.whl",
    145        f"dist/{package_name}-{release_version}.tar.gz",
    146    ]
    147    if files != expected:
    148        diff_generator = difflib.context_diff(
    149            expected, files, fromfile="expected", tofile="got", lineterm=""
    150        )
    151        diff = "\n".join(diff_generator)
    152        session.error(f"Got the wrong files:\n{diff}")
    153 
    154    # Get back out into main.
    155    session.run("git", "checkout", "-q", "main", external=True)
    156 
    157    # Check and upload distribution files.
    158    session.run("twine", "check", *files)
    159 
    160    # Push the commits and tag.
    161    # NOTE: The following fails if pushing to the branch is not allowed. This can
    162    #       happen on GitHub, if the main branch is protected, there are required
    163    #       CI checks and "Include administrators" is enabled on the protection.
    164    session.run("git", "push", "upstream", "main", release_version, external=True)
    165 
    166    # Upload the distribution.
    167    session.run("twine", "upload", *files)
    168 
    169    # Open up the GitHub release page.
    170    webbrowser.open("https://github.com/pypa/packaging/releases")
    171 
    172 
    173 # -----------------------------------------------------------------------------
    174 # Helpers
    175 # -----------------------------------------------------------------------------
    176 def _get_version_from_arguments(arguments):
    177    """Checks the arguments passed to `nox -s release`.
    178 
    179    Only 1 argument that looks like a version? Return the argument.
    180    Otherwise, raise a ValueError describing what's wrong.
    181    """
    182    if len(arguments) != 1:
    183        raise ValueError("Expected exactly 1 argument")
    184 
    185    version = arguments[0]
    186    parts = version.split(".")
    187 
    188    if len(parts) != 2:
    189        # Not of the form: YY.N
    190        raise ValueError("not of the form: YY.N")
    191 
    192    if not all(part.isdigit() for part in parts):
    193        # Not all segments are integers.
    194        raise ValueError("non-integer segments")
    195 
    196    # All is good.
    197    return version
    198 
    199 
    200 def _check_working_directory_state(session):
    201    """Check state of the working directory, prior to making the release."""
    202    should_not_exist = ["build/", "dist/"]
    203 
    204    bad_existing_paths = list(filter(os.path.exists, should_not_exist))
    205    if bad_existing_paths:
    206        session.error(f"Remove {', '.join(bad_existing_paths)} and try again")
    207 
    208 
    209 def _check_git_state(session, version_tag):
    210    """Check state of the git repository, prior to making the release."""
    211    # Ensure the upstream remote pushes to the correct URL.
    212    allowed_upstreams = [
    213        "git@github.com:pypa/packaging.git",
    214        "https://github.com/pypa/packaging.git",
    215    ]
    216    result = subprocess.run(
    217        ["git", "remote", "get-url", "--push", "upstream"],
    218        capture_output=True,
    219        encoding="utf-8",
    220    )
    221    if result.stdout.rstrip() not in allowed_upstreams:
    222        session.error(f"git remote `upstream` is not one of {allowed_upstreams}")
    223    # Ensure we're on main branch for cutting a release.
    224    result = subprocess.run(
    225        ["git", "rev-parse", "--abbrev-ref", "HEAD"],
    226        capture_output=True,
    227        encoding="utf-8",
    228    )
    229    if result.stdout != "main\n":
    230        session.error(f"Not on main branch: {result.stdout!r}")
    231 
    232    # Ensure there are no uncommitted changes.
    233    result = subprocess.run(
    234        ["git", "status", "--porcelain"], capture_output=True, encoding="utf-8"
    235    )
    236    if result.stdout:
    237        print(result.stdout, end="", file=sys.stderr)
    238        session.error("The working tree has uncommitted changes")
    239 
    240    # Ensure this tag doesn't exist already.
    241    result = subprocess.run(
    242        ["git", "rev-parse", version_tag], capture_output=True, encoding="utf-8"
    243    )
    244    if not result.returncode:
    245        session.error(f"Tag already exists! {version_tag} -- {result.stdout!r}")
    246 
    247    # Back up the current git reference, in a tag that's easy to clean up.
    248    _release_backup_tag = "auto/release-start-" + str(int(time.time()))
    249    session.run("git", "tag", _release_backup_tag, external=True)
    250 
    251 
    252 def _bump(session, *, version, file, kind):
    253    session.log(f"Bump version to {version!r}")
    254    contents = file.read_text()
    255    new_contents = re.sub(
    256        '__version__ = "(.+)"', f'__version__ = "{version}"', contents
    257    )
    258    file.write_text(new_contents)
    259 
    260    session.log("git commit")
    261    subprocess.run(["git", "add", str(file)])
    262    subprocess.run(["git", "commit", "-m", f"Bump for {kind}"])
    263 
    264 
    265 @contextlib.contextmanager
    266 def _replace_file(original_path):
    267    # Create a temporary file.
    268    fh, replacement_path = tempfile.mkstemp()
    269 
    270    try:
    271        with os.fdopen(fh, "w") as replacement:
    272            with open(original_path) as original:
    273                yield original, replacement
    274    except Exception:
    275        raise
    276    else:
    277        shutil.copymode(original_path, replacement_path)
    278        os.remove(original_path)
    279        shutil.move(replacement_path, original_path)
    280 
    281 
    282 def _changelog_update_unreleased_title(version, *, file):
    283    """Update an "*unreleased*" heading to "{version} - {date}" """
    284    yyyy_mm_dd = datetime.datetime.today().strftime("%Y-%m-%d")
    285    title = f"{version} - {yyyy_mm_dd}"
    286 
    287    with _replace_file(file) as (original, replacement):
    288        for line in original:
    289            if line == "*unreleased*\n":
    290                replacement.write(f"{title}\n")
    291                replacement.write(len(title) * "~" + "\n")
    292                # Skip processing the next line (the heading underline for *unreleased*)
    293                # since we already wrote the heading underline.
    294                next(original)
    295            else:
    296                replacement.write(line)
    297 
    298 
    299 def _changelog_add_unreleased_title(*, file):
    300    with _replace_file(file) as (original, replacement):
    301        # Duplicate first 3 lines from the original file.
    302        for _ in range(3):
    303            line = next(original)
    304            replacement.write(line)
    305 
    306        # Write the heading.
    307        replacement.write(
    308            textwrap.dedent(
    309                """\
    310                *unreleased*
    311                ~~~~~~~~~~~~
    312 
    313                No unreleased changes.
    314 
    315                """
    316            )
    317        )
    318 
    319        # Duplicate all the remaining lines.
    320        for line in original:
    321            replacement.write(line)