def prepare_release(session: nox.Session) -> None: version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s prepare-release -- <version>") session.log("# Ensure nothing is staged") if release.modified_files_in_git("--staged"): session.error("There are files staged in git") session.log(f"# Updating {AUTHORS_FILE}") release.generate_authors(AUTHORS_FILE) if release.modified_files_in_git(): release.commit_file(session, AUTHORS_FILE, message=f"Update {AUTHORS_FILE}") else: session.log(f"# No changes to {AUTHORS_FILE}") session.log("# Generating NEWS") release.generate_news(session, version) session.log(f"# Bumping for release {version}") release.update_version_file(version, VERSION_FILE) release.commit_file(session, VERSION_FILE, message="Bump for release") session.log("# Tagging release") release.create_git_tag(session, version, message=f"Release {version}") session.log("# Bumping for development") next_dev_version = release.get_next_development_version(version) release.update_version_file(next_dev_version, VERSION_FILE) release.commit_file(session, VERSION_FILE, message="Bump for development")
def do_release(session: nox.Session) -> None: """Do a release to PyPI.""" # TODO: maybe add version validation if modified_files_in_git() or modified_files_in_git("--staged"): session.error( "Repository not clean, please remove, unstage, or commit your changes" ) if not len(session.posargs): session.error("Usage: nox -s publish -- <version> [publish-args]") else: version = session.posargs[0] update_version(session, version, MAIN_MODULE) update_changelog(session, version, CHANGELOG) commit_files(session, [MAIN_MODULE, CHANGELOG], message=f"Prepare for release {version}") create_git_tag(session, version, message=f"Release {version}") with isolated_temporary_checkout(session) as workdir: session.chdir(workdir) session.install("flit") session.run("flit", "publish", *session.posargs[1:]) session.chdir(THIS_DIR) update_version(session, next_development_version(version), MAIN_MODULE) update_changelog(session, "<unreleased>", CHANGELOG) commit_files(session, [MAIN_MODULE, CHANGELOG], message="Let's get back to development") session.log( "Alright, just do a push to GitHub and everything should be done") session.log("If you're paranoid, go ahead, verify that things are fine") session.log("If not, sit back and relax, you just did a release 🎉")
def build_release(session: nox.Session) -> None: version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s build-release -- YY.N[.P]") session.log("# Ensure no files in dist/") if release.have_files_in_folder("dist"): session.error( "There are files in dist/. Remove them and try again. " "You can use `git clean -fxdi -- dist` command to do this") session.log("# Install dependencies") session.install("setuptools", "wheel", "twine") with release.isolated_temporary_checkout(session, version) as build_dir: session.log( "# Start the build in an isolated, " f"temporary Git checkout at {build_dir!s}", ) with release.workdir(session, build_dir): tmp_dists = build_dists(session) tmp_dist_paths = (build_dir / p for p in tmp_dists) session.log(f"# Copying dists from {build_dir}") os.makedirs("dist", exist_ok=True) for dist, final in zip(tmp_dist_paths, tmp_dists): session.log(f"# Copying {dist} to {final}") shutil.copy(dist, final)
def update_changelog(session: nox.Session, version: str, filepath: Path) -> None: """Update the changelog for both a new release and development.""" with open(filepath, encoding="utf-8") as f: lines = f.read().splitlines() if version != "<unreleased>": for index, line in enumerate(lines): if line == "## Unreleased": # Let's replace that unreleased text with the actual version # and fix up that date of release. lines[index] = f"## {version}" lines[index + 2] = f"Date of release: {get_today_date()}" break else: session.error("Couldn't find changelog section for next release") else: # Grab the title and newline right after it updated_lines = [*lines[0:2]] # ... then add some boilerplace updated_lines.append("## Unreleased\n") updated_lines.append("Date of release: *n/a*\n") updated_lines.append("**Bugfixes & enhancements**:\n") updated_lines.append("- *so far it's looking like a desert*\n") # and finally add back the rest of the changelog. updated_lines.extend(lines[2:]) lines = updated_lines with open(filepath, "w", encoding="utf-8") as f: f.write("\n".join(lines)) f.write("\n")
def dev_test_sim( session: nox.Session, sim: Optional[str], toplevel_lang: Optional[str], gpi_interface: Optional[str], ) -> None: """Test a development version of cocotb against a simulator.""" session.env["CFLAGS"] = "-Werror -Wno-deprecated-declarations -g --coverage" session.env["COCOTB_LIBRARY_COVERAGE"] = "1" session.env["CXXFLAGS"] = "-Werror" session.env["LDFLAGS"] = "--coverage" session.install(*test_deps, *coverage_deps) session.install("-e", ".") env = env_vars_for_test(sim, toplevel_lang, gpi_interface) config_str = stringify_dict(env) # Remove a potentially existing coverage file from a previous run for the # same test configuration. coverage_file = Path(f".coverage.test.sim-{sim}-{toplevel_lang}-{gpi_interface}") with suppress(FileNotFoundError): coverage_file.unlink() session.log(f"Running 'make test' against a simulator {config_str}") session.run("make", "test", external=True, env=env) session.log(f"Running simulator-specific tests against a simulator {config_str}") session.run( "pytest", "-v", "--cov=cocotb", "--cov-branch", # Don't display coverage report here "--cov-report=", "-k", "simulator_required", ) Path(".coverage").rename(".coverage.pytest") session.log(f"All tests passed with configuration {config_str}!") # Combine coverage produced during the test runs, and place it in a file # with a name specific to this invocation of dev_test_sim(). coverage_files = glob.glob("**/.coverage.cocotb", recursive=True) if not coverage_files: session.error( "No coverage files found. Something went wrong during the test execution." ) coverage_files.append(".coverage.pytest") session.run("coverage", "combine", "--append", *coverage_files) Path(".coverage").rename(coverage_file) session.log(f"Stored Python coverage for this test run in {coverage_file}.") # Combine coverage from all nox sessions as last step after all sessions # have completed. session.notify("dev_coverage_combine")
def vendoring(session: nox.Session) -> None: session.install("vendoring>=0.3.0") if "--upgrade" not in session.posargs: session.run("vendoring", "sync", ".", "-v") return def pinned_requirements(path): # type: (Path) -> Iterator[Tuple[str, str]] for line in path.read_text().splitlines(keepends=False): one, sep, two = line.partition("==") if not sep: continue name = one.strip() version = two.split("#", 1)[0].strip() if name and version: yield name, version vendor_txt = Path("src/pip/_vendor/vendor.txt") for name, old_version in pinned_requirements(vendor_txt): if name == "setuptools": continue # update requirements.txt session.run("vendoring", "update", ".", name) # get the updated version new_version = old_version for inner_name, inner_version in pinned_requirements(vendor_txt): if inner_name == name: # this is a dedicated assignment, to make flake8 happy new_version = inner_version break else: session.error(f"Could not find {name} in {vendor_txt}") # check if the version changed. if new_version == old_version: continue # no change, nothing more to do here. # synchronize the contents session.run("vendoring", "sync", ".") # Determine the correct message message = f"Upgrade {name} to {new_version}" # Write our news fragment news_file = Path("news") / (name + ".vendor.rst") news_file.write_text(message + "\n") # "\n" appeases end-of-line-fixer # Commit the changes release.commit_file(session, ".", message=message)
def docs(session: nox.Session) -> None: """ Build the docs. """ session.install("-e", ".[docs]") if session.posargs: if "serve" in session.posargs: session.run("mkdocs", "serve") else: session.error("Unrecognized args, use 'serve'") else: session.run("mkdocs", "build")
def update_version(session: nox.Session, version: str, filepath: Path) -> None: with open(filepath, encoding="utf-8") as f: lines = f.read().splitlines() for index, line in enumerate(lines): if line.startswith("__version__"): lines[index] = f'__version__ = "{version}"' break else: session.error("Couldn't find __version__ in {filepath}") with open(filepath, "w", encoding="utf-8") as f: f.write("\n".join(lines)) f.write("\n")
def enforce_branch_no_changes(session: nox.Session) -> None: """ Errors out the current session if we're not on default branch or if there are uncommitted changes. """ if has_changes(): session.error("All changes must be committed or removed before release") branch = get_branch() if branch != DEFAULT_BRANCH: session.error( f"Must be on {DEFAULT_BRANCH!r} branch. Currently on {branch!r} branch" )
def dev(session: nox.Session) -> None: """ Sets up a python dev environment for the project if one doesn't already exist. This session will: - Create a python virtualenv for the session - Install the `virtualenv` cli tool into this environment - Use `virtualenv` to create a global project virtual environment - Invoke the python interpreter from the global project environment to install the project and all it's development dependencies. """ # Check if dev has been run before # this prevents manual running nox -s dev more than once # thus potentially corrupting an environment if VENV_DIR.exists(): session.error( "There is already a virtual environment deactivate and remove it " "before running 'dev' again" ) # Create the project virtual environment using virtualenv # installed into this sessions virtual environment # confusing but it works! session.install("virtualenv") session.run("virtualenv", os.fsdecode(VENV_DIR), silent=True) # Use the venv's interpreter to install the project along with # all it's dev dependencies, this ensure it's installed # in the right way session.run( PYTHON, "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel", silent=True, external=True, ) session.run(PYTHON, "-m", "pip", "install", "-e", ".[dev]", external=True) if bool(shutil.which("code")) or bool(shutil.which("code-insiders")): # Only do this is user has VSCode installed set_up_vscode(session)
def release(session: nox.Session) -> None: """ Kicks off the automated release process by creating and pushing a new tag. Invokes bump2version with the posarg setting the version. Usage: $ nox -s release -- [major|minor|patch] """ enforce_branch_no_changes(session) parser = argparse.ArgumentParser( description="Release a new semantic version.") parser.add_argument( "version", type=str, nargs=1, help="The type of semver release to make.", choices={"major", "minor", "patch"}, ) args: argparse.Namespace = parser.parse_args(args=session.posargs) version: str = args.version.pop() # If we get here, we should be good to go # Let's do a final check for safety confirm = input( f"You are about to bump the {version!r} version. Are you sure? [y/n]: " ) # Abort on anything other than 'y' if confirm.lower().strip() != "y": session.error( f"You said no when prompted to bump the {version!r} version.") update_seeds(session) session.install("bump2version") session.log(f"Bumping the {version!r} version") session.run("bump2version", version) session.log("Pushing the new tag") session.run("git", "push", external=True) session.run("git", "push", "--tags", external=True)
def docs(session: nox.Session) -> None: """ Build the docs. Pass "serve" to serve. """ session.chdir("docs") session.install("-r", "requirements.txt") session.run("sphinx-build", "-M", "html", ".", "_build") if session.posargs: if "serve" in session.posargs: session.log( "Launching docs at http://localhost:8000/ - use Ctrl-C to quit" ) session.run("python", "-m", "http.server", "8000", "-d", "_build/html") else: session.error("Unsupported argument to docs")
def upload_release(session: nox.Session) -> None: version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s upload-release -- YY.N[.P]") session.log("# Install dependencies") session.install("twine") distribution_files = glob.glob("dist/*") session.log(f"# Distribution files: {distribution_files}") # Sanity check: Make sure there's 2 distribution files. count = len(distribution_files) if count != 2: session.error( f"Expected 2 distribution files for upload, got {count}. " f"Remove dist/ and run 'nox -s build-release -- {version}'") # Sanity check: Make sure the files are correctly named. distfile_names = (os.path.basename(fn) for fn in distribution_files) expected_distribution_files = [ f"pip-{version}-py3-none-any.whl", f"pip-{version}.tar.gz", ] if sorted(distfile_names) != sorted(expected_distribution_files): session.error( f"Distribution files do not seem to be for {version} release.") session.log("# Upload distributions") session.run("twine", "upload", *distribution_files)
def build_dists(session: nox.Session) -> List[str]: """Return dists with valid metadata.""" session.log( "# Check if there's any Git-untracked files before building the wheel", ) has_forbidden_git_untracked_files = any( # Don't report the environment this session is running in not untracked_file.startswith(".nox/build-release/") for untracked_file in release.get_git_untracked_files()) if has_forbidden_git_untracked_files: session.error( "There are untracked files in the working directory. " "Remove them and try again", ) session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) produced_dists = glob.glob("dist/*") session.log(f"# Verify distributions: {', '.join(produced_dists)}") session.run("twine", "check", *produced_dists, silent=True) return produced_dists
def release(session: nox.Session) -> None: """ Kicks off the automated release process by creating and pushing a new tag. Invokes bump2version with the posarg setting the version. Usage: $ nox -s release -- [major|minor|patch] """ # Little known Nox fact: Passing silent=True captures the output status = session.run( "git", "status", "--porcelain", silent=True, external=True ).strip() if len(status) > 1: session.error("All changes must be committed or removed before release") branch = session.run( "git", "rev-parse", "--abbrev-ref", "HEAD", silent=True, external=True ).strip() if branch != DEFAULT_BRANCH: session.error( f"Must be on {DEFAULT_BRANCH!r} branch. Currently on {branch!r} branch" ) parser = argparse.ArgumentParser(description="Release a new semantic version.") parser.add_argument( "version", type=str, nargs=1, help="The type of semver release to make.", choices={"major", "minor", "patch"}, ) args: argparse.Namespace = parser.parse_args(args=session.posargs) version: str = args.version.pop() # If we get here, we should be good to go # Let's do a final check for safety confirm = input( f"You are about to bump the {version!r} version. Are you sure? [y/n]: " ) # Abort on anything other than 'y' if confirm.lower().strip() != "y": session.error(f"You said no when prompted to bump the {version!r} version.") session.install("--upgrade", "pip", "setuptools", "wheel") session.install("bump2version") session.log(f"Bumping the {version!r} version") session.run("bump2version", version) session.log("Pushing the new tag") session.run("git", "push", external=True) session.run("git", "push", "--tags", external=True)
def release(session: nox.Session) -> None: """ Kicks off the automated release process by creating and pushing a new tag. Invokes bump2version with the posarg setting the version. Usage: $ nox -s release -- [major|minor|patch] """ enforce_branch_no_changes(session) allowed_args: Set[str] = {"major", "minor", "patch"} n_args: int = len(session.posargs) if n_args != 1: session.error( f"Only 1 session arg allowed, got {n_args}. Pass one of: {allowed_args}" ) # If we get here, we know there's only 1 posarg version = session.posargs.pop() if version not in allowed_args: session.error( f"Invalid argument: got {version!r}, expected one of: {allowed_args}" ) # If we get here, we should be good to go # Let's do a final check for safety confirm = input( f"You are about to bump the {version!r} version. Are you sure? [y/n]: " ) # Abort on anything other than 'y' if confirm.lower().strip() != "y": session.error( f"You said no when prompted to bump the {version!r} version.") update_seeds(session) session.install("bump2version") session.log(f"Bumping the {version!r} version") session.run("bump2version", version) session.log("Pushing the new tag") session.run("git", "push", external=True) session.run("git", "push", "--tags", external=True)
def find_session_runner(session: nox.Session, name: str): """Helper function to find parameterized action by name""" for s, _ in session._runner.manifest.list_all_sessions(): if name in s.signatures: return s session.error(f"Could not find a nox session by the name {name!r}")