chore: import upstream snapshot with attribution
fuzz / fuzz (3.11) (push) Failing after 1s
fuzz / fuzz (3.12) (push) Failing after 1s
build and publish / sdist + pure wheel (push) Failing after 0s
diff-shades / analysis / base / ${{ matrix.mode }} (push) Has been skipped
docs / docs (ubuntu-latest) (push) Failing after 2s
fuzz / fuzz (3.10) (push) Failing after 1s
fuzz / fuzz (3.14) (push) Failing after 0s
lint and format / lint (push) Failing after 1s
diff-shades / configure (push) Failing after 1s
docker / build (linux/amd64) (push) Has been skipped
diff-shades / analysis / target / ${{ matrix.mode }} (push) Has been skipped
fuzz / fuzz (3.13) (push) Failing after 0s
build and publish / generate wheels matrix (push) Failing after 0s
test release tool / test-release-tool (ubuntu-latest, 3.13) (push) Failing after 1s
build and publish / mypyc wheels ${{ matrix.only }} (push) Has been skipped
test / test (ubuntu-latest, 3.10) (push) Failing after 0s
test / test (ubuntu-latest, 3.11) (push) Failing after 1s
test / test (ubuntu-latest, 3.13) (push) Failing after 0s
test / test (ubuntu-latest, pypy3.11-v7.3.22) (push) Failing after 1s
test / test (ubuntu-latest, 3.15) (push) Failing after 1s
test release tool / test-release-tool (ubuntu-latest, 3.15) (push) Failing after 0s
zizmor / zizmor (push) Failing after 0s
test release tool / test-release-tool (ubuntu-latest, 3.12) (push) Failing after 4s
test release tool / test-release-tool (ubuntu-latest, 3.14) (push) Failing after 3s
test / uvloop (ubuntu-latest) (push) Failing after 1s
test / test (ubuntu-latest, 3.14) (push) Failing after 1s
test / test (ubuntu-latest, 3.12.10) (push) Failing after 5s
docker / build (linux/arm64) (push) Has been cancelled
docker / push (push) Has been cancelled
docs / docs (windows-latest) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.14) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.15) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.12) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.13) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.14) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.15) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.12) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.11) (push) Has been cancelled
test / test (macOS-latest, 3.12.10) (push) Has been cancelled
test / test (macOS-latest, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.14) (push) Has been cancelled
test / test (macOS-latest, 3.15) (push) Has been cancelled
test / test (macOS-latest, pypy3.11-v7.3.22) (push) Has been cancelled
test / test (windows-11-arm, 3.11) (push) Has been cancelled
test / test (windows-11-arm, 3.12.10) (push) Has been cancelled
test / test (windows-11-arm, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.10) (push) Has been cancelled
test / coveralls-finish (push) Has been cancelled
test / uvloop (macOS-latest) (push) Has been cancelled
test / uvloop (windows-11-arm) (push) Has been cancelled
test / uvloop (windows-latest) (push) Has been cancelled
test / test (windows-11-arm, 3.14) (push) Has been cancelled
test / test (windows-11-arm, 3.15) (push) Has been cancelled
test / test (windows-latest, 3.10) (push) Has been cancelled
test / test (windows-latest, 3.11) (push) Has been cancelled
test / test (windows-latest, 3.12.10) (push) Has been cancelled
test / test (windows-latest, 3.13) (push) Has been cancelled
test / test (windows-latest, 3.14) (push) Has been cancelled
test / test (windows-latest, 3.15) (push) Has been cancelled
test / test (windows-latest, pypy3.11-v7.3.22) (push) Has been cancelled
diff-shades / compare / ${{ matrix.mode }} (push) Has been cancelled
fuzz / create-issue (push) Has been cancelled
build and publish / publish-mypyc (push) Has been cancelled
build and publish / publish-hatch (push) Has been cancelled
fuzz / fuzz (3.11) (push) Failing after 1s
fuzz / fuzz (3.12) (push) Failing after 1s
build and publish / sdist + pure wheel (push) Failing after 0s
diff-shades / analysis / base / ${{ matrix.mode }} (push) Has been skipped
docs / docs (ubuntu-latest) (push) Failing after 2s
fuzz / fuzz (3.10) (push) Failing after 1s
fuzz / fuzz (3.14) (push) Failing after 0s
lint and format / lint (push) Failing after 1s
diff-shades / configure (push) Failing after 1s
docker / build (linux/amd64) (push) Has been skipped
diff-shades / analysis / target / ${{ matrix.mode }} (push) Has been skipped
fuzz / fuzz (3.13) (push) Failing after 0s
build and publish / generate wheels matrix (push) Failing after 0s
test release tool / test-release-tool (ubuntu-latest, 3.13) (push) Failing after 1s
build and publish / mypyc wheels ${{ matrix.only }} (push) Has been skipped
test / test (ubuntu-latest, 3.10) (push) Failing after 0s
test / test (ubuntu-latest, 3.11) (push) Failing after 1s
test / test (ubuntu-latest, 3.13) (push) Failing after 0s
test / test (ubuntu-latest, pypy3.11-v7.3.22) (push) Failing after 1s
test / test (ubuntu-latest, 3.15) (push) Failing after 1s
test release tool / test-release-tool (ubuntu-latest, 3.15) (push) Failing after 0s
zizmor / zizmor (push) Failing after 0s
test release tool / test-release-tool (ubuntu-latest, 3.12) (push) Failing after 4s
test release tool / test-release-tool (ubuntu-latest, 3.14) (push) Failing after 3s
test / uvloop (ubuntu-latest) (push) Failing after 1s
test / test (ubuntu-latest, 3.14) (push) Failing after 1s
test / test (ubuntu-latest, 3.12.10) (push) Failing after 5s
docker / build (linux/arm64) (push) Has been cancelled
docker / push (push) Has been cancelled
docs / docs (windows-latest) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.14) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.15) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.12) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.13) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.14) (push) Has been cancelled
test release tool / test-release-tool (macOS-latest, 3.15) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.12) (push) Has been cancelled
test release tool / test-release-tool (windows-latest, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.11) (push) Has been cancelled
test / test (macOS-latest, 3.12.10) (push) Has been cancelled
test / test (macOS-latest, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.14) (push) Has been cancelled
test / test (macOS-latest, 3.15) (push) Has been cancelled
test / test (macOS-latest, pypy3.11-v7.3.22) (push) Has been cancelled
test / test (windows-11-arm, 3.11) (push) Has been cancelled
test / test (windows-11-arm, 3.12.10) (push) Has been cancelled
test / test (windows-11-arm, 3.13) (push) Has been cancelled
test / test (macOS-latest, 3.10) (push) Has been cancelled
test / coveralls-finish (push) Has been cancelled
test / uvloop (macOS-latest) (push) Has been cancelled
test / uvloop (windows-11-arm) (push) Has been cancelled
test / uvloop (windows-latest) (push) Has been cancelled
test / test (windows-11-arm, 3.14) (push) Has been cancelled
test / test (windows-11-arm, 3.15) (push) Has been cancelled
test / test (windows-latest, 3.10) (push) Has been cancelled
test / test (windows-latest, 3.11) (push) Has been cancelled
test / test (windows-latest, 3.12.10) (push) Has been cancelled
test / test (windows-latest, 3.13) (push) Has been cancelled
test / test (windows-latest, 3.14) (push) Has been cancelled
test / test (windows-latest, 3.15) (push) Has been cancelled
test / test (windows-latest, pypy3.11-v7.3.22) (push) Has been cancelled
diff-shades / compare / ${{ matrix.mode }} (push) Has been cancelled
fuzz / create-issue (push) Has been cancelled
build and publish / publish-mypyc (push) Has been cancelled
build and publish / publish-hatch (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Check that the rev value in the example pre-commit configuration matches
|
||||
the latest version of Black. This saves us from forgetting to update that
|
||||
during the release process.
|
||||
|
||||
Why can't we just use `rev: stable` and call it a day? Well pre-commit
|
||||
won't auto update the hook as you may expect (and for good reasons, some
|
||||
technical and some pragmatic). Encouraging bad practice is also just
|
||||
not ideal. xref: https://github.com/psf/black/issues/420
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import commonmark
|
||||
import yaml
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def main(changes: str, content: str, filename: str) -> None:
|
||||
changes_html = commonmark.commonmark(changes)
|
||||
changes_soup = BeautifulSoup(changes_html, "html.parser")
|
||||
headers = changes_soup.find_all("h2")
|
||||
latest_tag, *_ = (
|
||||
(header.string or "").removeprefix("Version ")
|
||||
for header in headers
|
||||
if header.string != "Unreleased"
|
||||
)
|
||||
|
||||
source_version_control_html = commonmark.commonmark(content)
|
||||
source_version_control_soup = BeautifulSoup(
|
||||
source_version_control_html, "html.parser"
|
||||
)
|
||||
codeblocks = source_version_control_soup.find_all(class_="language-yaml")
|
||||
|
||||
for codeblock in codeblocks:
|
||||
parsed = yaml.safe_load(codeblock.string) # type: ignore[arg-type]
|
||||
if not isinstance(parsed, dict):
|
||||
return
|
||||
pre_commit_rev = parsed["repos"][0]["rev"]
|
||||
if not pre_commit_rev == latest_tag:
|
||||
print(
|
||||
f"Please set the rev in ``{filename}`` to be the latest one.\n"
|
||||
f"Expected {latest_tag}, got {pre_commit_rev}.\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("CHANGES.md", encoding="utf-8") as fd:
|
||||
changes = fd.read()
|
||||
with open(
|
||||
os.path.join("docs", "integrations", "source_version_control.md"),
|
||||
encoding="utf-8",
|
||||
) as fd:
|
||||
content = fd.read()
|
||||
main(changes, content, "source_version_control.md")
|
||||
with open(
|
||||
os.path.join("docs", "guides", "using_black_with_jupyter_notebooks.md"),
|
||||
encoding="utf-8",
|
||||
) as fd:
|
||||
content = fd.read()
|
||||
main(changes, content, "using_black_with_jupyter_notebooks.md")
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Check that the rev value in the example from ``the_basics.md`` matches
|
||||
the latest version of Black. This saves us from forgetting to update that
|
||||
during the release process.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import commonmark
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def main(changes: str, the_basics: str) -> None:
|
||||
changes_html = commonmark.commonmark(changes)
|
||||
changes_soup = BeautifulSoup(changes_html, "html.parser")
|
||||
headers = changes_soup.find_all("h2")
|
||||
tags = []
|
||||
for header in headers:
|
||||
text = header.string or ""
|
||||
if text == "Unreleased":
|
||||
continue
|
||||
# Strip "Version " prefix if present
|
||||
tags.append(text.removeprefix("Version "))
|
||||
latest_tag = tags[0]
|
||||
|
||||
the_basics_html = commonmark.commonmark(the_basics)
|
||||
the_basics_soup = BeautifulSoup(the_basics_html, "html.parser")
|
||||
version_examples = [
|
||||
code_block.string
|
||||
for code_block in the_basics_soup.find_all(class_="language-console")
|
||||
if "$ black --version" in code_block.string # type: ignore[operator]
|
||||
]
|
||||
|
||||
for tag in tags:
|
||||
for version_example in version_examples:
|
||||
if tag in version_example and tag != latest_tag: # type: ignore[operator]
|
||||
print(
|
||||
"Please set the version in the ``black --version`` "
|
||||
"examples from ``the_basics.md`` to be the latest one.\n"
|
||||
f"Expected {latest_tag}, got {tag}.\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("CHANGES.md", encoding="utf-8") as fd:
|
||||
changes = fd.read()
|
||||
with open(
|
||||
os.path.join("docs", "usage_and_configuration", "the_basics.md"),
|
||||
encoding="utf-8",
|
||||
) as fd:
|
||||
the_basics = fd.read()
|
||||
main(changes, the_basics)
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Helper script for psf/black's diff-shades Github Actions integration.
|
||||
|
||||
diff-shades is a tool for analyzing what happens when you run Black on
|
||||
OSS code capturing it for comparisons or other usage. It's used here to
|
||||
help measure the impact of a change *before* landing it (in particular
|
||||
posting a comment on completion for PRs).
|
||||
|
||||
This script exists as a more maintainable alternative to using inline
|
||||
Javascript in the workflow YAML files. The revision configuration and
|
||||
resolving, caching, and PR comment logic is contained here.
|
||||
|
||||
For more information, please see the developer docs:
|
||||
|
||||
https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import pprint
|
||||
import subprocess
|
||||
import sys
|
||||
from base64 import b64encode
|
||||
from os.path import dirname, join
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import click
|
||||
import urllib3
|
||||
from packaging.version import Version
|
||||
|
||||
COMMENT_FILE: Final = ".pr-comment.md"
|
||||
DIFF_STEP_NAME: Final = "Generate HTML diff report"
|
||||
DOCS_URL: Final = (
|
||||
"https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades"
|
||||
)
|
||||
SHA_LENGTH: Final = 10
|
||||
GH_API_TOKEN: Final = os.getenv("GITHUB_TOKEN")
|
||||
REPO: Final = os.getenv("GITHUB_REPOSITORY", default="psf/black")
|
||||
USER_AGENT: Final = f"{REPO} diff-shades workflow via urllib3/{urllib3.__version__}"
|
||||
http = urllib3.PoolManager()
|
||||
|
||||
|
||||
def set_output(name: str, value: str) -> None:
|
||||
if len(value) < 200:
|
||||
print(f"[INFO]: setting '{name}' to '{value}'")
|
||||
else:
|
||||
print(f"[INFO]: setting '{name}' to [{len(value)} chars]")
|
||||
|
||||
if "GITHUB_OUTPUT" in os.environ:
|
||||
if "\n" in value:
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
delimiter = b64encode(os.urandom(16)).decode()
|
||||
value = f"{delimiter}\n{value}\n{delimiter}"
|
||||
command = f"{name}<<{value}"
|
||||
else:
|
||||
command = f"{name}={value}"
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||
print(command, file=f)
|
||||
|
||||
|
||||
def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any:
|
||||
headers = kwargs.get("headers") or {}
|
||||
headers["User-Agent"] = USER_AGENT
|
||||
if "github" in url:
|
||||
if GH_API_TOKEN:
|
||||
headers["Authorization"] = f"token {GH_API_TOKEN}"
|
||||
headers["Accept"] = "application/vnd.github.v3+json"
|
||||
kwargs["headers"] = headers
|
||||
|
||||
r = http.request("GET", url, **kwargs)
|
||||
if is_json:
|
||||
data = json.loads(r.data.decode("utf-8"))
|
||||
else:
|
||||
data = r.data
|
||||
print(f"[INFO]: issued GET request for {r.geturl()}")
|
||||
if not (200 <= r.status < 300):
|
||||
pprint.pprint(dict(r.info()))
|
||||
pprint.pprint(data)
|
||||
raise RuntimeError(f"unexpected status code: {r.status}")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_latest_revision(ref: str) -> str:
|
||||
data = http_get(
|
||||
f"https://api.github.com/repos/{REPO}/commits",
|
||||
fields={"per_page": "1", "sha": ref},
|
||||
)
|
||||
assert isinstance(data[0]["sha"], str)
|
||||
return data[0]["sha"]
|
||||
|
||||
|
||||
def get_pr_branches(pr: int | None = None) -> tuple[Any, Any, int]:
|
||||
if not pr:
|
||||
pr_ref = os.getenv("GITHUB_REF")
|
||||
assert pr_ref is not None
|
||||
pr = int(pr_ref[10:-6])
|
||||
|
||||
data = http_get(f"https://api.github.com/repos/{REPO}/pulls/{pr}")
|
||||
assert isinstance(data["base"]["sha"], str)
|
||||
assert isinstance(data["head"]["sha"], str)
|
||||
return data["base"], data["head"], pr
|
||||
|
||||
|
||||
def get_pypi_version() -> Version:
|
||||
data = http_get("https://pypi.org/pypi/black/json")
|
||||
versions = [Version(v) for v in data["releases"]]
|
||||
sorted_versions = sorted(versions, reverse=True)
|
||||
return sorted_versions[0]
|
||||
|
||||
|
||||
@click.group()
|
||||
def main() -> None:
|
||||
pass
|
||||
|
||||
|
||||
@main.command("config", help="Acquire run configuration and metadata.")
|
||||
def config() -> None:
|
||||
import diff_shades # type: ignore[import-not-found]
|
||||
|
||||
jobs = [{"mode": "preview-new-changes", "style": "preview"}]
|
||||
|
||||
event = os.getenv("GITHUB_EVENT_NAME")
|
||||
if event == "push":
|
||||
# Push on main, let's use PyPI Black as the baseline.
|
||||
baseline_name = str(get_pypi_version())
|
||||
baseline_cmd = f"git checkout {baseline_name}"
|
||||
|
||||
target_rev = os.getenv("GITHUB_SHA")
|
||||
assert target_rev is not None
|
||||
target_name = "main-" + target_rev[:SHA_LENGTH]
|
||||
target_cmd = f"git checkout {target_rev}"
|
||||
|
||||
elif event == "pull_request":
|
||||
jobs.insert(0, {"mode": "assert-no-changes", "style": "stable"})
|
||||
# PR, let's use the PR base as the baseline.
|
||||
base, head, pr_num = get_pr_branches()
|
||||
|
||||
baseline_rev = get_latest_revision(base["ref"])
|
||||
baseline_name = f"{base['ref']}-{baseline_rev[:SHA_LENGTH]}"
|
||||
baseline_cmd = f"git checkout {baseline_rev}"
|
||||
|
||||
target_name = f"pr-{pr_num}-{head['sha'][:SHA_LENGTH]}"
|
||||
target_cmd = f"gh pr checkout {pr_num}\ngit merge origin/{base['ref']}"
|
||||
else:
|
||||
raise ValueError(f"Unknown event {event}")
|
||||
|
||||
env = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}"
|
||||
for entry in jobs:
|
||||
entry["baseline-analysis"] = f"{entry['style']}-{baseline_name}.json"
|
||||
entry["baseline-setup-cmd"] = baseline_cmd
|
||||
entry["baseline-cache-key"] = f"{env}-{baseline_name}-{entry['style']}"
|
||||
|
||||
entry["target-analysis"] = f"{entry['style']}-{target_name}.json"
|
||||
entry["target-setup-cmd"] = target_cmd
|
||||
|
||||
set_output("matrix", json.dumps(jobs, indent=None))
|
||||
pprint.pprint(jobs)
|
||||
|
||||
|
||||
@main.command("comment-body", help="Generate the body for a summary PR comment.")
|
||||
@click.argument("baseline", type=click.Path(exists=True, path_type=Path))
|
||||
@click.argument("target", type=click.Path(exists=True, path_type=Path))
|
||||
@click.argument("style")
|
||||
@click.argument("mode")
|
||||
def comment_body(baseline: Path, target: Path, style: str, mode: str) -> None:
|
||||
cmd = (
|
||||
f"{sys.executable} -m diff_shades --no-color "
|
||||
f"compare {baseline} {target} --quiet --check"
|
||||
).split(" ")
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, encoding="utf-8")
|
||||
if proc.returncode:
|
||||
run_id = os.getenv("GITHUB_RUN_ID")
|
||||
jobs = http_get(
|
||||
f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}/jobs",
|
||||
)["jobs"]
|
||||
job = next(j for j in jobs if j["name"] == f"compare / {mode}")
|
||||
diff_step = next(s for s in job["steps"] if s["name"] == DIFF_STEP_NAME)
|
||||
diff_url = f"{job['html_url']}#step:{diff_step['number']}:1"
|
||||
|
||||
body = (
|
||||
"<details>"
|
||||
f"<summary><b><code>--{style}</code> style</b> "
|
||||
f'(<a href="{diff_url}">View full diff</a>):</summary>'
|
||||
f"<pre>{proc.stdout.strip()}</pre>"
|
||||
"</details>"
|
||||
)
|
||||
else:
|
||||
body = f"<b><code>--{style}</code> style</b>: no changes"
|
||||
|
||||
filename = f".{style}{COMMENT_FILE}"
|
||||
print(f"[INFO]: writing comment details to {filename}")
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(body)
|
||||
|
||||
|
||||
@main.command("comment-details", help="Get PR comment resources from a workflow run.")
|
||||
@click.argument("pr")
|
||||
@click.argument("run-id")
|
||||
@click.argument("styles", nargs=-1)
|
||||
def comment_details(pr: int, run_id: str, styles: tuple[str, ...]) -> None:
|
||||
base, head, _ = get_pr_branches(pr)
|
||||
|
||||
lines = [
|
||||
f"**diff-shades** results comparing this PR ({head['sha']}) to {base['ref']}"
|
||||
f" ({base['sha']}):"
|
||||
]
|
||||
for style_file in styles:
|
||||
with open(
|
||||
join(dirname(__file__), "..", style_file),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
content = f.read()
|
||||
lines.append(content)
|
||||
|
||||
lines.append("---")
|
||||
|
||||
lines.append(
|
||||
f"[**What is this?**]({DOCS_URL}) | "
|
||||
f"[Workflow run](https://github.com/psf/black/actions/runs/{run_id}) | "
|
||||
"[diff-shades documentation](https://github.com/ichard26/diff-shades#readme)"
|
||||
)
|
||||
|
||||
body = "\n\n".join(lines)
|
||||
set_output("comment-body", body)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Property-based tests for Black.
|
||||
|
||||
By Zac Hatfield-Dodds, based on my Hypothesmith tool for source code
|
||||
generation. You can run this file with `python`, `pytest`, or (soon)
|
||||
a coverage-guided fuzzer I'm working on.
|
||||
"""
|
||||
|
||||
import hypothesmith
|
||||
from hypothesis import HealthCheck, given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
import black
|
||||
|
||||
|
||||
# This test uses the Hypothesis and Hypothesmith libraries to generate random
|
||||
# syntactically-valid Python source code and run Black in odd modes.
|
||||
@settings(
|
||||
max_examples=1000, # roughly 1k tests/minute, or half that under coverage
|
||||
derandomize=True, # deterministic mode to avoid CI flakiness
|
||||
deadline=None, # ignore Hypothesis' health checks; we already know that
|
||||
suppress_health_check=list(HealthCheck), # this is slow and filter-heavy.
|
||||
)
|
||||
@given(
|
||||
# Note that while Hypothesmith might generate code unlike that written by
|
||||
# humans, it's a general test that should pass for any *valid* source code.
|
||||
# (so e.g. running it against code scraped of the internet might also help)
|
||||
src_contents=hypothesmith.from_grammar() | hypothesmith.from_node(),
|
||||
# Using randomly-varied modes helps us to exercise less common code paths.
|
||||
mode=st.builds(
|
||||
black.FileMode,
|
||||
line_length=st.just(88) | st.integers(0, 200),
|
||||
string_normalization=st.booleans(),
|
||||
preview=st.booleans(),
|
||||
is_pyi=st.booleans(),
|
||||
magic_trailing_comma=st.booleans(),
|
||||
),
|
||||
)
|
||||
def test_idempotent_any_syntactically_valid_python(
|
||||
src_contents: str, mode: black.FileMode
|
||||
) -> None:
|
||||
# Before starting, let's confirm that the input string is valid Python:
|
||||
compile(src_contents, "<string>", "exec") # else the bug is in hypothesmith
|
||||
|
||||
# Then format the code...
|
||||
dst_contents = black.format_str(src_contents, mode=mode)
|
||||
|
||||
# And check that we got equivalent and stable output.
|
||||
black.assert_equivalent(src_contents, dst_contents)
|
||||
black.assert_stable(src_contents, dst_contents, mode=mode)
|
||||
|
||||
# Future test: check that pure-python and mypyc versions of black
|
||||
# give identical output for identical input?
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests, including shrinking and reporting any known failures.
|
||||
test_idempotent_any_syntactically_valid_python()
|
||||
|
||||
# If Atheris is available, run coverage-guided fuzzing.
|
||||
# (if you want only bounded fuzzing, just use `pytest fuzz.py`)
|
||||
try:
|
||||
import sys
|
||||
|
||||
import atheris
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
test = test_idempotent_any_syntactically_valid_python
|
||||
atheris.Setup(
|
||||
sys.argv,
|
||||
test.hypothesis.fuzz_one_input, # type: ignore[attr-defined]
|
||||
)
|
||||
atheris.Fuzz()
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
import json
|
||||
from typing import IO, Any
|
||||
|
||||
import click
|
||||
|
||||
import black
|
||||
|
||||
|
||||
def generate_schema_from_click(
|
||||
cmd: click.Command,
|
||||
) -> dict[str, Any]:
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for param in cmd.params:
|
||||
if not isinstance(param, click.Option) or param.is_eager:
|
||||
continue
|
||||
|
||||
assert param.name
|
||||
name = param.name.replace("_", "-")
|
||||
|
||||
result[name] = {}
|
||||
|
||||
match param.type:
|
||||
case click.types.IntParamType():
|
||||
result[name]["type"] = "integer"
|
||||
case click.types.StringParamType() | click.types.Path():
|
||||
result[name]["type"] = "string"
|
||||
case click.types.Choice(choices=choices):
|
||||
result[name]["enum"] = choices
|
||||
case click.types.BoolParamType():
|
||||
result[name]["type"] = "boolean"
|
||||
case _:
|
||||
msg = f"{param.type!r} not a known type for {param}"
|
||||
raise TypeError(msg)
|
||||
|
||||
if param.multiple:
|
||||
result[name] = {"type": "array", "items": result[name]}
|
||||
|
||||
result[name]["description"] = param.help
|
||||
|
||||
default = param.to_info_dict()["default"]
|
||||
if default is not None and not param.multiple:
|
||||
result[name]["default"] = default
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
||||
@click.option("--schemastore", is_flag=True, help="SchemaStore format")
|
||||
@click.option("--outfile", type=click.File(mode="w"), help="Write to file")
|
||||
def main(schemastore: bool, outfile: IO[str]) -> None:
|
||||
properties = generate_schema_from_click(black.main)
|
||||
del properties["line-ranges"]
|
||||
|
||||
schema: dict[str, Any] = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": (
|
||||
"https://github.com/psf/black/blob/main/src/black/resources/black.schema.json"
|
||||
),
|
||||
"$comment": "tool.black table in pyproject.toml",
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": properties,
|
||||
}
|
||||
|
||||
if schemastore:
|
||||
schema["$id"] = "https://json.schemastore.org/partial-black.json"
|
||||
# The precise list of unstable features may change frequently, so don't
|
||||
# bother putting it in SchemaStore
|
||||
schema["properties"]["enable-unstable-feature"]["items"] = {"type": "string"}
|
||||
|
||||
print(json.dumps(schema, indent=2), file=outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Generates a width table for Unicode characters.
|
||||
|
||||
This script generates a width table for Unicode characters that are not
|
||||
narrow (width 1). The table is written to src/black/_width_table.py (note
|
||||
that although this file is generated, it is checked into Git) and is used
|
||||
by the char_width() function in src/black/strings.py.
|
||||
|
||||
You should run this script when you upgrade wcwidth, which is expected to
|
||||
happen when a new Unicode version is released. The generated table contains
|
||||
the version of wcwidth and Unicode that it was generated for.
|
||||
|
||||
In order to run this script, you need to install the latest version of wcwidth.
|
||||
You can do this by running:
|
||||
|
||||
pip install --group width-table
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from os.path import basename, dirname, join
|
||||
|
||||
import wcwidth # type: ignore[import-not-found]
|
||||
|
||||
|
||||
def make_width_table() -> Iterable[tuple[int, int, int]]:
|
||||
start_codepoint = -1
|
||||
end_codepoint = -1
|
||||
range_width = -2
|
||||
for codepoint in range(0, sys.maxunicode + 1):
|
||||
width = wcwidth.wcwidth(chr(codepoint))
|
||||
if width <= 1:
|
||||
# Ignore narrow characters along with zero-width characters so that
|
||||
# they are treated as single-width. Note that treating zero-width
|
||||
# characters as single-width is consistent with the heuristics built
|
||||
# on top of str.isascii() in the str_width() function in strings.py.
|
||||
continue
|
||||
if start_codepoint < 0:
|
||||
start_codepoint = codepoint
|
||||
range_width = width
|
||||
elif width != range_width or codepoint != end_codepoint + 1:
|
||||
yield (start_codepoint, end_codepoint, range_width)
|
||||
start_codepoint = codepoint
|
||||
range_width = width
|
||||
end_codepoint = codepoint
|
||||
if start_codepoint >= 0:
|
||||
yield (start_codepoint, end_codepoint, range_width)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
table_path = join(dirname(__file__), "..", "src", "black", "_width_table.py")
|
||||
with open(table_path, "w") as f:
|
||||
f.write(f"""# Generated by {basename(__file__)}
|
||||
# wcwidth {wcwidth.__version__}
|
||||
# Unicode {wcwidth.list_versions()[-1]}
|
||||
from typing import Final
|
||||
|
||||
WIDTH_TABLE: Final[list[tuple[int, int, int]]] = [
|
||||
""")
|
||||
for triple in make_width_table():
|
||||
f.write(f" {triple!r},\n")
|
||||
f.write("]\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# check out every commit added by the current branch, blackify them,
|
||||
# and generate diffs to reconstruct the original commits, but then
|
||||
# blackified
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from subprocess import PIPE, Popen, check_output, run
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return check_output(["git", *args]).decode("utf8").strip()
|
||||
|
||||
|
||||
def blackify(base_branch: str, black_command: str, logger: logging.Logger) -> int:
|
||||
current_branch = git("branch", "--show-current")
|
||||
|
||||
if not current_branch or base_branch == current_branch:
|
||||
logger.error("You need to check out a feature branch to work on")
|
||||
return 1
|
||||
|
||||
if not os.path.exists(".git"):
|
||||
logger.error("Run me in the root of your repo")
|
||||
return 1
|
||||
|
||||
merge_base = git("merge-base", "HEAD", base_branch)
|
||||
if not merge_base:
|
||||
logger.error(
|
||||
f"Could not find a common commit for current head and {base_branch}"
|
||||
)
|
||||
return 1
|
||||
|
||||
commits = git(
|
||||
"log", "--reverse", "--pretty=format:%H", f"{merge_base}~1..HEAD"
|
||||
).split()
|
||||
for commit in commits:
|
||||
git("checkout", commit, f"-b{commit}-black")
|
||||
check_output(black_command, shell=True)
|
||||
git("commit", "-aqm", "blackify")
|
||||
|
||||
git("checkout", base_branch, f"-b{current_branch}-black")
|
||||
|
||||
for last_commit, commit in zip(commits, commits[1:], strict=False):
|
||||
allow_empty = (
|
||||
b"--allow-empty" in run(["git", "apply", "-h"], stdout=PIPE).stdout
|
||||
)
|
||||
quiet = b"--quiet" in run(["git", "apply", "-h"], stdout=PIPE).stdout
|
||||
git_diff = Popen(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--binary",
|
||||
"--find-copies",
|
||||
f"{last_commit}-black..{commit}-black",
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
git_apply = Popen(
|
||||
[
|
||||
"git",
|
||||
"apply",
|
||||
]
|
||||
+ (["--quiet"] if quiet else [])
|
||||
+ [
|
||||
"-3",
|
||||
"--intent-to-add",
|
||||
]
|
||||
+ (["--allow-empty"] if allow_empty else [])
|
||||
+ [
|
||||
"-",
|
||||
],
|
||||
stdin=git_diff.stdout,
|
||||
)
|
||||
if git_diff.stdout is not None:
|
||||
git_diff.stdout.close()
|
||||
git_apply.communicate()
|
||||
git("commit", "--allow-empty", "-aqC", commit)
|
||||
|
||||
for commit in commits:
|
||||
git("branch", "-qD", f"{commit}-black")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("base_branch")
|
||||
parser.add_argument("--black_command", default="black -q .")
|
||||
parser.add_argument("--logfile", type=argparse.FileType("w"), default=sys.stdout)
|
||||
args = parser.parse_args()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.StreamHandler(args.logfile))
|
||||
logger.setLevel(logging.INFO)
|
||||
sys.exit(blackify(args.base_branch, args.black_command, logger))
|
||||
Executable
+248
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Tool to help automate changes needed in commits during and after releases
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
NEW_VERSION_CHANGELOG_TEMPLATE = """\
|
||||
## Unreleased
|
||||
|
||||
<!-- PR authors:
|
||||
Please include the PR number in the changelog entry, not the issue number -->
|
||||
|
||||
### Highlights
|
||||
|
||||
<!-- Include any especially major or disruptive changes here -->
|
||||
|
||||
### Stable style
|
||||
|
||||
<!-- Changes that affect Black's stable style -->
|
||||
|
||||
### Preview style
|
||||
|
||||
<!-- Changes that affect Black's preview style -->
|
||||
|
||||
### Configuration
|
||||
|
||||
<!-- Changes to how Black can be configured -->
|
||||
|
||||
### Packaging
|
||||
|
||||
<!-- Changes to how Black is packaged, such as dependency requirements -->
|
||||
|
||||
### Parser
|
||||
|
||||
<!-- Changes to the parser or to version autodetection -->
|
||||
|
||||
### Performance
|
||||
|
||||
<!-- Changes that improve Black's performance. -->
|
||||
|
||||
### Output
|
||||
|
||||
<!-- Changes to Black's terminal output and error messages -->
|
||||
|
||||
### _Blackd_
|
||||
|
||||
<!-- Changes to blackd -->
|
||||
|
||||
### Integrations
|
||||
|
||||
<!-- For example, Docker, GitHub Actions, pre-commit, editors -->
|
||||
|
||||
### Documentation
|
||||
|
||||
<!-- Major changes to documentation and policies. Small docs changes
|
||||
don't need a changelog entry. -->
|
||||
"""
|
||||
|
||||
|
||||
class NoGitTagsError(Exception): ...
|
||||
|
||||
|
||||
# TODO: Do better with alpha + beta releases
|
||||
# Maybe we vendor packaging library
|
||||
def get_git_tags(versions_only: bool = True) -> list[str]:
|
||||
"""Pull out all tags or calvers only"""
|
||||
cp = run(["git", "tag"], capture_output=True, check=True, encoding="utf8")
|
||||
if not cp.stdout:
|
||||
LOG.error(f"Returned no git tags stdout: {cp.stderr}")
|
||||
raise NoGitTagsError
|
||||
git_tags = cp.stdout.splitlines()
|
||||
if versions_only:
|
||||
return [t for t in git_tags if t[0].isdigit()]
|
||||
return git_tags
|
||||
|
||||
|
||||
# TODO: Support sorting alpha/beta releases correctly
|
||||
def tuple_calver(calver: str) -> tuple[int, ...]: # mypy can't notice maxsplit below
|
||||
"""Convert a calver string into a tuple of ints for sorting"""
|
||||
try:
|
||||
return tuple(map(int, calver.split(".", maxsplit=2)))
|
||||
except ValueError:
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
class SourceFiles:
|
||||
def __init__(self, black_repo_dir: Path):
|
||||
# File path fun all pathlib to be platform agnostic
|
||||
self.black_repo_path = black_repo_dir
|
||||
self.changes_path = self.black_repo_path / "CHANGES.md"
|
||||
self.docs_path = self.black_repo_path / "docs"
|
||||
self.version_doc_paths = (
|
||||
self.docs_path / "integrations" / "source_version_control.md",
|
||||
self.docs_path / "usage_and_configuration" / "the_basics.md",
|
||||
self.docs_path / "guides" / "using_black_with_jupyter_notebooks.md",
|
||||
)
|
||||
self.current_version = self.get_current_version()
|
||||
self.next_version = self.get_next_version()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""\
|
||||
> SourceFiles ENV:
|
||||
Repo path: {self.black_repo_path}
|
||||
CHANGES.md path: {self.changes_path}
|
||||
docs path: {self.docs_path}
|
||||
Current version: {self.current_version}
|
||||
Next version: {self.next_version}
|
||||
"""
|
||||
|
||||
def add_template_to_changes(self) -> int:
|
||||
"""Add the template to CHANGES.md if it does not exist"""
|
||||
LOG.info(f"Adding template to {self.changes_path}")
|
||||
|
||||
with self.changes_path.open("r", encoding="utf-8") as cfp:
|
||||
changes_string = cfp.read()
|
||||
|
||||
if "## Unreleased" in changes_string:
|
||||
LOG.error(f"{self.changes_path} already has unreleased template")
|
||||
return 1
|
||||
|
||||
templated_changes_string = changes_string.replace(
|
||||
"# Change Log\n",
|
||||
f"# Change Log\n\n{NEW_VERSION_CHANGELOG_TEMPLATE}",
|
||||
)
|
||||
|
||||
with self.changes_path.open("w", encoding="utf-8") as cfp:
|
||||
cfp.write(templated_changes_string)
|
||||
|
||||
LOG.info(f"Added template to {self.changes_path}")
|
||||
return 0
|
||||
|
||||
def cleanup_changes_template_for_release(self) -> None:
|
||||
LOG.info(f"Cleaning up {self.changes_path}")
|
||||
|
||||
with self.changes_path.open("r", encoding="utf-8") as cfp:
|
||||
changes_string = cfp.read()
|
||||
|
||||
# Change Unreleased to next version
|
||||
changes_string = changes_string.replace(
|
||||
"## Unreleased", f"## Version {self.next_version}"
|
||||
)
|
||||
|
||||
# Remove all comments
|
||||
changes_string = re.sub(r"(?m)^<!--(?>(?:.|\n)*?-->)\n+", "", changes_string)
|
||||
|
||||
# Remove empty subheadings
|
||||
changes_string = re.sub(r"(?m)^###.+\n+(?=#)", "", changes_string)
|
||||
|
||||
with self.changes_path.open("w", encoding="utf-8") as cfp:
|
||||
cfp.write(changes_string)
|
||||
|
||||
LOG.debug(f"Finished Cleaning up {self.changes_path}")
|
||||
|
||||
def get_current_version(self) -> str:
|
||||
"""Get the latest git (version) tag as latest version"""
|
||||
return sorted(get_git_tags(), key=lambda k: tuple_calver(k))[-1]
|
||||
|
||||
def get_next_version(self) -> str:
|
||||
"""Workout the year and month + version number we need to move to"""
|
||||
base_calver = datetime.today().strftime("%y.%m")
|
||||
calver_parts = base_calver.split(".")
|
||||
base_calver = f"{calver_parts[0]}.{int(calver_parts[1])}" # Remove leading 0
|
||||
git_tags = get_git_tags()
|
||||
same_month_releases = [
|
||||
t for t in git_tags if t.startswith(base_calver) and "a" not in t
|
||||
]
|
||||
if len(same_month_releases) < 1:
|
||||
return f"{base_calver}.0"
|
||||
same_month_version = same_month_releases[-1].split(".", 2)[-1]
|
||||
return f"{base_calver}.{int(same_month_version) + 1}"
|
||||
|
||||
def update_repo_for_release(self) -> int:
|
||||
"""Update CHANGES.md + doc files ready for release"""
|
||||
self.cleanup_changes_template_for_release()
|
||||
self.update_version_in_docs()
|
||||
return 0 # return 0 if no exceptions hit
|
||||
|
||||
def update_version_in_docs(self) -> None:
|
||||
for doc_path in self.version_doc_paths:
|
||||
LOG.info(f"Updating black version to {self.next_version} in {doc_path}")
|
||||
|
||||
with doc_path.open("r", encoding="utf-8") as dfp:
|
||||
doc_string = dfp.read()
|
||||
|
||||
next_version_doc = doc_string.replace(
|
||||
self.current_version, self.next_version
|
||||
)
|
||||
|
||||
with doc_path.open("w", encoding="utf-8") as dfp:
|
||||
dfp.write(next_version_doc)
|
||||
|
||||
LOG.debug(
|
||||
f"Finished updating black version to {self.next_version} in {doc_path}"
|
||||
)
|
||||
|
||||
|
||||
def _handle_debug(debug: bool) -> None:
|
||||
"""Turn on debugging if asked otherwise INFO default"""
|
||||
log_level = logging.DEBUG if debug else logging.INFO
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s] %(levelname)s: %(message)s (%(filename)s:%(lineno)d)",
|
||||
level=log_level,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--add-changes-template",
|
||||
action="store_true",
|
||||
help="Add the Unreleased template to CHANGES.md",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--debug", action="store_true", help="Verbose debug output"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
_handle_debug(args.debug)
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
# Need parent.parent cause script is in scripts/ directory
|
||||
sf = SourceFiles(Path(__file__).parent.parent)
|
||||
|
||||
if args.add_changes_template:
|
||||
return sf.add_template_to_changes()
|
||||
|
||||
LOG.info(f"Current version detected to be {sf.current_version}")
|
||||
LOG.info(f"Next version will be {sf.next_version}")
|
||||
return sf.update_repo_for_release()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from release import SourceFiles, tuple_calver # type: ignore
|
||||
|
||||
|
||||
class FakeDateTime:
|
||||
"""Used to mock the date to test generating next calver function"""
|
||||
|
||||
def today(*args: Any, **kwargs: Any) -> "FakeDateTime": # noqa: B902
|
||||
return FakeDateTime()
|
||||
|
||||
# Add leading 0 on purpose to ensure we remove it
|
||||
def strftime(*args: Any, **kwargs: Any) -> str: # noqa: B902
|
||||
return "69.01"
|
||||
|
||||
|
||||
class TestRelease(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
# We only test on >= 3.12
|
||||
self.tempdir = TemporaryDirectory(delete=False) # type: ignore
|
||||
self.tempdir_path = Path(self.tempdir.name)
|
||||
self.sf = SourceFiles(self.tempdir_path)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
rmtree(self.tempdir.name)
|
||||
return super().tearDown()
|
||||
|
||||
@patch("release.get_git_tags")
|
||||
def test_get_current_version(self, mocked_git_tags: Mock) -> None:
|
||||
mocked_git_tags.return_value = ["1.1.0", "69.1.0", "69.1.1", "2.2.0"]
|
||||
self.assertEqual("69.1.1", self.sf.get_current_version())
|
||||
|
||||
@patch("release.get_git_tags")
|
||||
@patch("release.datetime", FakeDateTime)
|
||||
def test_get_next_version(self, mocked_git_tags: Mock) -> None:
|
||||
# test we handle no args
|
||||
mocked_git_tags.return_value = []
|
||||
self.assertEqual(
|
||||
"69.1.0",
|
||||
self.sf.get_next_version(),
|
||||
"Unable to get correct next version with no git tags",
|
||||
)
|
||||
|
||||
# test we handle
|
||||
mocked_git_tags.return_value = ["1.1.0", "69.1.0", "69.1.1", "2.2.0"]
|
||||
self.assertEqual(
|
||||
"69.1.2",
|
||||
self.sf.get_next_version(),
|
||||
"Unable to get correct version with 2 previous versions released this"
|
||||
" month",
|
||||
)
|
||||
|
||||
def test_tuple_calver(self) -> None:
|
||||
first_month_release = tuple_calver("69.1.0")
|
||||
second_month_release = tuple_calver("69.1.1")
|
||||
self.assertEqual((69, 1, 0), first_month_release)
|
||||
self.assertEqual((0, 0, 0), tuple_calver("69.1.1a0")) # Hack for alphas/betas
|
||||
self.assertTrue(first_month_release < second_month_release)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user