diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 968642d4..99f4beb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: mypy args: ["--config-file", "pyproject.toml"] additional_dependencies: - [pytest, click, importlib_resources, types-requests] + [pytest, click, importlib_resources, types-requests, fastapi] stages: [manual] - repo: https://github.com/sirosen/check-jsonschema diff --git a/jupyter_releaser/mock_github.py b/jupyter_releaser/mock_github.py new file mode 100644 index 00000000..0860e2a2 --- /dev/null +++ b/jupyter_releaser/mock_github.py @@ -0,0 +1,197 @@ +import atexit +import datetime +import os +import tempfile +import uuid +from typing import Dict, List + +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from jupyter_releaser.util import MOCK_GITHUB_URL + +app = FastAPI() + +static_dir = tempfile.TemporaryDirectory() +atexit.register(static_dir.cleanup) +app.mount("/static", StaticFiles(directory=static_dir.name), name="static") + +releases: Dict[int, "Release"] = {} +pulls: Dict[int, "PullRequest"] = {} +release_ids_for_asset: Dict[int, int] = {} +tag_refs: Dict[str, "Tag"] = {} + + +class Asset(BaseModel): + id: int + name: str + content_type: str + size: int + state: str = "uploaded" + url: str + node_id: str = "" + download_count: int = 0 + label: str = "" + uploader: None = None + browser_download_url: str = "" + created_at: str = "" + updated_at: str = "" + + +class Release(BaseModel): + assets_url: str = "" + upload_url: str + tarball_url: str = "" + zipball_url: str = "" + created_at: str + published_at: str = "" + draft: bool + body: str = "" + id: int + node_id: str = "" + author: str = "" + html_url: str + name: str = "" + prerelease: bool + tag_name: str + target_commitish: str + assets: List[Asset] + url: str + + +class User(BaseModel): + login: str = "bar" + html_url: str = "http://bar.com" + + +class PullRequest(BaseModel): + number: int = 0 + html_url: str = "http://foo.com" + title: str = "foo" + user: User = User() + + +class TagObject(BaseModel): + sha: str + + +class Tag(BaseModel): + ref: str + object: TagObject + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/repos/{owner}/{repo}/releases") +def list_releases(owner: str, repo: str) -> List[Release]: + """https://docs.github.com/en/rest/releases/releases#list-releases""" + return list(releases.values()) + + +@app.post("/repos/{owner}/{repo}/releases") +async def create_a_release(owner: str, repo: str, request: Request) -> Release: + """https://docs.github.com/en/rest/releases/releases#create-a-release""" + release_id = uuid.uuid4().int + data = await request.json() + url = f"https://github.com/repos/{owner}/{repo}/releases/{release_id}" + html_url = f"https://github.com/{owner}/{repo}/releases/tag/{data['tag_name']}" + upload_url = f"{MOCK_GITHUB_URL}/repos/{owner}/{repo}/releases/{release_id}/assets" + fmt_str = r"%Y-%m-%dT%H:%M:%SZ" + created_at = datetime.datetime.utcnow().strftime(fmt_str) + model = Release( + id=release_id, + url=url, + html_url=html_url, + assets=[], + upload_url=upload_url, + created_at=created_at, + **data, + ) + releases[model.id] = model + return model + + +@app.patch("/repos/{owner}/{repo}/releases/{release_id}") +async def update_a_release(owner: str, repo: str, release_id: int, request: Request) -> Release: + """https://docs.github.com/en/rest/releases/releases#update-a-release""" + data = await request.json() + model = releases[release_id] + for name, value in data.items(): + setattr(model, name, value) + return model + + +@app.post("/repos/{owner}/{repo}/releases/{release_id}/assets") +async def upload_a_release_asset(owner: str, repo: str, release_id: int, request: Request) -> None: + """https://docs.github.com/en/rest/releases/assets#upload-a-release-asset""" + model = releases[release_id] + asset_id = uuid.uuid4().int + name = request.query_params["name"] + with open(f"{static_dir.name}/{asset_id}", "wb") as fid: + async for chunk in request.stream(): + fid.write(chunk) + headers = request.headers + url = f"{MOCK_GITHUB_URL}/static/{asset_id}" + asset = Asset( + id=asset_id, + name=name, + size=headers["content-length"], + url=url, + content_type=headers["content-type"], + ) + release_ids_for_asset[asset_id] = release_id + model.assets.append(asset) + + +@app.delete("/repos/{owner}/{repo}/releases/assets/{asset_id}") +async def delete_a_release_asset(owner: str, repo: str, asset_id: int) -> None: + """https://docs.github.com/en/rest/releases/assets#delete-a-release-asset""" + release = releases[release_ids_for_asset[asset_id]] + os.remove(f"{static_dir.name}/{asset_id}") + release.assets = [a for a in release.assets if a.id != asset_id] + + +@app.delete("/repos/{owner}/{repo}/releases/{release_id}") +def delete_a_release(owner: str, repo: str, release_id: int) -> None: + """https://docs.github.com/en/rest/releases/releases#delete-a-release""" + del releases[release_id] + + +@app.get("/repos/{owner}/{repo}/pulls/{pull_number}") +def get_a_pull_request(owner: str, repo: str, pull_number: int) -> PullRequest: + """https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request""" + if pull_number not in pulls: + pulls[pull_number] = PullRequest() + return pulls[pull_number] + + +@app.post("/repos/{owner}/{repo}/pulls") +def create_a_pull_request(owner: str, repo: str) -> PullRequest: + """https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request""" + pull = PullRequest() + pulls[pull.number] = pull + return pull + + +@app.post("/repos/{owner}/{repo}/issues/{issue_number}/labels") +def add_labels_to_an_issue(owner: str, repo: str, issue_number: int) -> BaseModel: + """https://docs.github.com/en/rest/issues/labels#add-labels-to-an-issue""" + return BaseModel() + + +@app.post("/create_tag_ref/{tag_ref}/{sha}") +def create_tag_ref(tag_ref: str, sha: str) -> None: + """Create a remote tag ref object for testing""" + tag = Tag(ref=f"refs/tags/{tag_ref}", object=TagObject(sha=sha)) + tag_refs[tag_ref] = tag + + +@app.get("/repos/{owner}/{repo}/git/matching-refs/tags/{tag_ref}") +def list_matching_references(owner: str, repo: str, tag_ref: str) -> List[Tag]: + """https://docs.github.com/en/rest/git/refs#list-matching-references""" + # raise ValueError("we should have an api to set a sha for a tag ref for tests") + return [tag_refs[tag_ref]] diff --git a/jupyter_releaser/tests/conftest.py b/jupyter_releaser/tests/conftest.py index 698e3749..0615356b 100644 --- a/jupyter_releaser/tests/conftest.py +++ b/jupyter_releaser/tests/conftest.py @@ -4,14 +4,14 @@ import os import os.path as osp from pathlib import Path -from urllib.request import OpenerDirector from click.testing import CliRunner +from ghapi import core from pytest import fixture from jupyter_releaser import cli, util from jupyter_releaser.tests import util as testutil -from jupyter_releaser.util import run +from jupyter_releaser.util import MOCK_GITHUB_URL, run, start_mock_github @fixture(autouse=True) @@ -26,6 +26,7 @@ def mock_env(mocker): del env[key] mocker.patch.dict(os.environ, env, clear=True) + core.GH_HOST = MOCK_GITHUB_URL try: run("git config --global user.name") @@ -169,13 +170,6 @@ def git_prep(runner, git_repo): runner(["prep-git", "--git-url", git_repo]) -@fixture -def open_mock(mocker): - open_mock = mocker.patch.object(OpenerDirector, "open", autospec=True) - open_mock.return_value = testutil.MockHTTPResponse() - yield open_mock - - @fixture def build_mock(mocker): orig_run = util.run @@ -193,3 +187,12 @@ def wrapped(cmd, **kwargs): return orig_run(cmd, **kwargs) mock_run = mocker.patch("jupyter_releaser.util.run", wraps=wrapped) + + +@fixture +def mock_github(): + proc = start_mock_github() + yield proc + + proc.kill() + proc.wait() diff --git a/jupyter_releaser/tests/test_cli.py b/jupyter_releaser/tests/test_cli.py index c5757705..221329d8 100644 --- a/jupyter_releaser/tests/test_cli.py +++ b/jupyter_releaser/tests/test_cli.py @@ -15,12 +15,10 @@ from jupyter_releaser import changelog, util from jupyter_releaser.tests.util import ( CHANGELOG_ENTRY, - HTML_URL, PR_ENTRY, - REPO_DATA, VERSION_SPEC, - MockHTTPResponse, - MockRequestResponse, + create_draft_release, + create_tag_ref, get_log, mock_changelog_entry, ) @@ -239,13 +237,10 @@ def test_build_changelog_existing(py_package, mocker, runner): run("pre-commit run -a", cwd=util.CHECKOUT_NAME) -def test_build_changelog_backport(py_package, mocker, runner, open_mock): +def test_build_changelog_backport(py_package, mocker, runner, mock_github): changelog_file = "CHANGELOG.md" changelog_path = Path(util.CHECKOUT_NAME) / changelog_file - data = dict(title="foo", html_url="bar", user=dict(login="snuffy", html_url="baz")) - open_mock.return_value = MockHTTPResponse(data) - runner(["prep-git", "--git-url", py_package]) runner(["bump-version", "--version-spec", VERSION_SPEC]) @@ -260,7 +255,7 @@ def test_build_changelog_backport(py_package, mocker, runner, open_mock): assert changelog.START_MARKER in text assert changelog.END_MARKER in text - assert "- foo [#50](bar) ([@snuffy](baz))" in text, text + assert "- foo [#50](http://foo.com) ([@bar](http://bar.com))" in text, text assert len(re.findall(changelog.START_MARKER, text)) == 1 assert len(re.findall(changelog.END_MARKER, text)) == 1 @@ -268,7 +263,7 @@ def test_build_changelog_backport(py_package, mocker, runner, open_mock): run("pre-commit run -a") -def test_build_changelog_slashes(py_package, mocker, runner, open_mock): +def test_build_changelog_slashes(py_package, mocker, runner): branch = "a/b/c" util.run(f"git checkout -b {branch} foo") env = dict(RH_REF=f"refs/heads/{branch}", RH_BRANCH=branch) @@ -299,7 +294,7 @@ def test_build_changelog_slashes(py_package, mocker, runner, open_mock): run("pre-commit run -a") -def test_draft_changelog_full(py_package, mocker, runner, open_mock, git_prep): +def test_draft_changelog_full(py_package, mocker, runner, git_prep, mock_github): mock_changelog_entry(py_package, runner, mocker) runner(["draft-changelog", "--version-spec", VERSION_SPEC]) @@ -307,10 +302,8 @@ def test_draft_changelog_full(py_package, mocker, runner, open_mock, git_prep): assert "before-draft-changelog" in log assert "after-draft-changelog" in log - assert len(open_mock.call_args) == 2 - -def test_draft_changelog_skip_config(py_package, mocker, runner, open_mock, git_prep): +def test_draft_changelog_skip_config(py_package, mocker, runner, git_prep): mock_changelog_entry(py_package, runner, mocker) config_path = Path(util.CHECKOUT_NAME) / util.JUPYTER_RELEASER_CONFIG @@ -319,10 +312,9 @@ def test_draft_changelog_skip_config(py_package, mocker, runner, open_mock, git_ config_path.write_text(util.toml.dumps(config), encoding="utf-8") runner(["draft-changelog", "--version-spec", VERSION_SPEC, "--since", "foo"]) - open_mock.assert_not_called() -def test_draft_changelog_skip_environ(py_package, mocker, runner, open_mock, git_prep): +def test_draft_changelog_skip_environ(py_package, mocker, runner, git_prep): mock_changelog_entry(py_package, runner, mocker) config_path = Path(util.CHECKOUT_NAME) / util.JUPYTER_RELEASER_CONFIG @@ -331,7 +323,6 @@ def test_draft_changelog_skip_environ(py_package, mocker, runner, open_mock, git config_path.write_text(util.toml.dumps(config), encoding="utf-8") runner(["draft-changelog", "--version-spec", VERSION_SPEC, "--since", "foo"]) - open_mock.assert_not_called() del os.environ["RH_STEPS_TO_SKIP"] @@ -350,10 +341,9 @@ def test_draft_changelog_dry_run(npm_package, mocker, runner, git_prep): del os.environ["RH_SINCE_LAST_STABLE"] -def test_draft_changelog_lerna(workspace_package, mocker, runner, open_mock, git_prep): +def test_draft_changelog_lerna(workspace_package, mocker, runner, mock_github, git_prep): mock_changelog_entry(workspace_package, runner, mocker) runner(["draft-changelog", "--version-spec", VERSION_SPEC]) - assert len(open_mock.call_args) == 2 def test_check_links(py_package, runner): @@ -524,7 +514,7 @@ def test_tag_release(py_package, runner, build_mock, git_prep): assert "after-tag-release" in log -def test_draft_release_dry_run(py_dist, mocker, runner, open_mock, git_prep): +def test_draft_release_dry_run(py_dist, mocker, runner, git_prep): # Publish the release - dry run runner( [ @@ -536,46 +526,24 @@ def test_draft_release_dry_run(py_dist, mocker, runner, open_mock, git_prep): "haha", ] ) - open_mock.assert_not_called() log = get_log() assert "before-draft-release" in log assert "after-draft-release" in log -def test_draft_release_final(npm_dist, runner, mocker, open_mock, git_prep): - open_mock.side_effect = [ - MockHTTPResponse([REPO_DATA]), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - ] - +def test_draft_release_final(npm_dist, runner, mock_github, git_prep): # Publish the release os.environ["GITHUB_ACTIONS"] = "true" runner(["draft-release"]) - assert len(open_mock.call_args) == 2 -def test_delete_release(npm_dist, runner, mocker, open_mock, git_prep): +def test_delete_release(npm_dist, runner, mock_github, git_prep): # Publish the release # Mimic being on GitHub actions so we get the magic output os.environ["GITHUB_ACTIONS"] = "true" - open_mock.side_effect = [ - MockHTTPResponse([REPO_DATA]), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - MockHTTPResponse(), - ] result = runner(["draft-release"]) - assert len(open_mock.call_args) == 2 - url = "" for line in result.output.splitlines(): match = re.match(r"::set-output name=release_url::(.*)", line) @@ -583,14 +551,7 @@ def test_delete_release(npm_dist, runner, mocker, open_mock, git_prep): url = match.groups()[0] # Delete the release - data = dict(assets=[dict(id="bar")]) - open_mock.side_effect = [ - MockHTTPResponse([data]), - MockHTTPResponse(), - MockHTTPResponse(), - ] runner(["delete-release", url]) - assert len(open_mock.call_args) == 2 log = get_log() assert "before-delete-release" in log @@ -601,7 +562,7 @@ def test_delete_release(npm_dist, runner, mocker, open_mock, git_prep): os.name == "nt" and sys.version_info.major == 3 and sys.version_info.minor < 8, reason="See https://bugs.python.org/issue26660", ) -def test_extract_dist_py(py_package, runner, mocker, open_mock, tmp_path, git_prep): +def test_extract_dist_py(py_package, runner, mocker, mock_github, tmp_path, git_prep): changelog_entry = mock_changelog_entry(py_package, runner, mocker) # Create the dist files @@ -610,37 +571,15 @@ def test_extract_dist_py(py_package, runner, mocker, open_mock, tmp_path, git_pr # Finalize the release runner(["tag-release"]) - os.makedirs("staging") - shutil.move(f"{util.CHECKOUT_NAME}/dist", "staging") - - def helper(path, **kwargs): - return MockRequestResponse(f"{py_package}/staging/dist/{path}") + # Create a tag ref + ref = create_tag_ref() - get_mock = mocker.patch("requests.get", side_effect=helper) + # Create the release. + dist_dir = os.path.join(util.CHECKOUT_NAME, "dist") + release = create_draft_release(ref, glob(f"{dist_dir}/*.*")) + shutil.rmtree(f"{util.CHECKOUT_NAME}/dist") - tag_name = f"v{VERSION_SPEC}" - - dist_names = [osp.basename(f) for f in glob("staging/dist/*.*")] - releases = [ - dict( - tag_name=tag_name, - target_commitish=util.get_branch(), - assets=[dict(name=dist_name, url=dist_name) for dist_name in dist_names], - ) - ] - sha = run("git rev-parse HEAD", cwd=util.CHECKOUT_NAME) - - tags = [dict(ref=f"refs/tags/{tag_name}", object=dict(sha=sha))] - url = normalize_path(osp.join(os.getcwd(), util.CHECKOUT_NAME)) - open_mock.side_effect = [ - MockHTTPResponse(releases), - MockHTTPResponse(tags), - MockHTTPResponse(dict(html_url=url)), - ] - - runner(["extract-release", HTML_URL]) - assert len(open_mock.mock_calls) == 2 - assert len(get_mock.mock_calls) == len(dist_names) == 2 + runner(["extract-release", release.html_url]) log = get_log() assert "before-extract-release" not in log @@ -651,52 +590,32 @@ def helper(path, **kwargs): os.name == "nt" and sys.version_info.major == 3 and sys.version_info.minor < 8, reason="See https://bugs.python.org/issue26660", ) -def test_extract_dist_multipy(py_multipackage, runner, mocker, open_mock, tmp_path, git_prep): +def test_extract_dist_multipy(py_multipackage, runner, mocker, mock_github, tmp_path, git_prep): git_repo = py_multipackage[0]["abs_path"] changelog_entry = mock_changelog_entry(git_repo, runner, mocker) # Create the dist files + files = [] dist_dir = normalize_path(Path(util.CHECKOUT_NAME).resolve() / "dist") for package in py_multipackage: run( f"python -m build . -o {dist_dir}", cwd=Path(util.CHECKOUT_NAME) / package["rel_path"], ) + files.extend(glob(dist_dir + "/*.*")) # Finalize the release runner(["tag-release"]) - os.makedirs("staging") - shutil.move(f"{util.CHECKOUT_NAME}/dist", "staging") - - def helper(path, **kwargs): - return MockRequestResponse(f"{git_repo}/staging/dist/{path}") - - get_mock = mocker.patch("requests.get", side_effect=helper) + # Create a tag ref + ref = create_tag_ref() - tag_name = f"v{VERSION_SPEC}" + # Create the release. + dist_dir = os.path.join(util.CHECKOUT_NAME, "dist") + release = create_draft_release(ref, files) + shutil.rmtree(f"{util.CHECKOUT_NAME}/dist") - dist_names = [osp.basename(f) for f in glob("staging/dist/*.*")] - releases = [ - dict( - tag_name=tag_name, - target_commitish=util.get_branch(), - assets=[dict(name=dist_name, url=dist_name) for dist_name in dist_names], - ) - ] - sha = run("git rev-parse HEAD", cwd=util.CHECKOUT_NAME) - - tags = [dict(ref=f"refs/tags/{tag_name}", object=dict(sha=sha))] - url = normalize_path(osp.join(os.getcwd(), util.CHECKOUT_NAME)) - open_mock.side_effect = [ - MockHTTPResponse(releases), - MockHTTPResponse(tags), - MockHTTPResponse(dict(html_url=url)), - ] - - runner(["extract-release", HTML_URL]) - assert len(open_mock.mock_calls) == 2 - assert len(get_mock.mock_calls) == len(dist_names) == 2 * len(py_multipackage) + runner(["extract-release", release.html_url]) log = get_log() assert "before-extract-release" not in log @@ -707,37 +626,17 @@ def helper(path, **kwargs): os.name == "nt" and sys.version_info.major == 3 and sys.version_info.minor < 8, reason="See https://bugs.python.org/issue26660", ) -def test_extract_dist_npm(npm_dist, runner, mocker, open_mock, tmp_path): +def test_extract_dist_npm(npm_dist, runner, mocker, mock_github, tmp_path): - os.makedirs("staging") - shutil.move(f"{util.CHECKOUT_NAME}/dist", "staging") + # Create a tag ref + ref = create_tag_ref() - def helper(path, **kwargs): - return MockRequestResponse(f"{npm_dist}/staging/dist/{path}") + # Create the release. + dist_dir = os.path.join(util.CHECKOUT_NAME, "dist") + release = create_draft_release(ref, glob(f"{dist_dir}/*.*")) + shutil.rmtree(f"{util.CHECKOUT_NAME}/dist") - get_mock = mocker.patch("requests.get", side_effect=helper) - - dist_names = [osp.basename(f) for f in glob("staging/dist/*.tgz")] - url = normalize_path(osp.join(os.getcwd(), util.CHECKOUT_NAME)) - tag_name = f"v{VERSION_SPEC}" - releases = [ - dict( - tag_name=tag_name, - target_commitish="foo", - assets=[dict(name=dist_name, url=dist_name) for dist_name in dist_names], - ) - ] - sha = run("git rev-parse HEAD", cwd=util.CHECKOUT_NAME) - tags = [dict(ref=f"refs/tags/{tag_name}", object=dict(sha=sha))] - open_mock.side_effect = [ - MockHTTPResponse(releases), - MockHTTPResponse(tags), - MockHTTPResponse(dict(html_url=url)), - ] - - runner(["extract-release", HTML_URL]) - assert len(open_mock.mock_calls) == 2 - assert len(get_mock.mock_calls) == len(dist_names) == 3 + runner(["extract-release", release.html_url]) log = get_log() assert "before-extract-release" not in log @@ -745,7 +644,7 @@ def helper(path, **kwargs): @pytest.mark.skipif(os.name == "nt", reason="pypiserver does not start properly on Windows") -def test_publish_assets_py(py_package, runner, mocker, git_prep): +def test_publish_assets_py(py_package, runner, mocker, git_prep, mock_github): # Create the dist files changelog_entry = mock_changelog_entry(py_package, runner, mocker) run("python -m build .", cwd=util.CHECKOUT_NAME) @@ -765,7 +664,8 @@ def wrapped(cmd, **kwargs): mock_run = mocker.patch("jupyter_releaser.util.run", wraps=wrapped) dist_dir = py_package / util.CHECKOUT_NAME / "dist" - runner(["publish-assets", "--dist-dir", dist_dir, "--dry-run", HTML_URL]) + release = create_draft_release() + runner(["publish-assets", "--dist-dir", dist_dir, "--dry-run", release.html_url]) assert called == 2, called log = get_log() @@ -791,7 +691,7 @@ def wrapped(cmd, **kwargs): assert called == 3, called -def test_publish_assets_npm_exists(npm_dist, runner, mocker): +def test_publish_assets_npm_exists(npm_dist, runner, mocker, mock_github): dist_dir = npm_dist / util.CHECKOUT_NAME / "dist" called = 0 @@ -805,7 +705,7 @@ def wrapped(cmd, **kwargs): raise err mock_run = mocker.patch("jupyter_releaser.util.run", wraps=wrapped) - + release = create_draft_release() runner( [ "publish-assets", @@ -815,14 +715,14 @@ def wrapped(cmd, **kwargs): "npm publish --dry-run", "--dist-dir", dist_dir, - HTML_URL, + release.html_url, ] ) assert called == 3, called -def test_publish_assets_npm_all_exists(npm_dist, runner, mocker): +def test_publish_assets_npm_all_exists(npm_dist, runner, mocker, mock_github): dist_dir = npm_dist / util.CHECKOUT_NAME / "dist" called = 0 @@ -835,7 +735,7 @@ def wrapped(cmd, **kwargs): raise err mocker.patch("jupyter_releaser.util.run", wraps=wrapped) - + release = create_draft_release() runner( [ "publish-assets", @@ -845,24 +745,21 @@ def wrapped(cmd, **kwargs): "npm publish --dry-run", "--dist-dir", dist_dir, - HTML_URL, + release.html_url, ] ) assert called == 3, called -def test_publish_release(npm_dist, runner, mocker, open_mock): - open_mock.side_effect = [MockHTTPResponse([REPO_DATA]), MockHTTPResponse()] - dist_dir = npm_dist / util.CHECKOUT_NAME / "dist" - runner(["publish-release", HTML_URL]) +def test_publish_release(npm_dist, runner, mocker, mock_github): + release = create_draft_release("bar") + runner(["publish-release", release.html_url]) log = get_log() assert "before-publish-release" in log assert "after-publish-release" in log - assert len(open_mock.call_args) == 2 - def test_config_file(py_package, runner, mocker, git_prep): @@ -935,9 +832,8 @@ def wrapped(cmd, **kwargs): assert "after-build-python" in log -def test_forwardport_changelog_no_new(npm_package, runner, mocker, open_mock, git_prep): - - open_mock.side_effect = [MockHTTPResponse([REPO_DATA]), MockHTTPResponse()] +def test_forwardport_changelog_no_new(npm_package, runner, mocker, mock_github, git_prep): + release = create_draft_release("bar") # Create a branch with a changelog entry util.run("git checkout -b backport_branch", cwd=util.CHECKOUT_NAME) @@ -947,19 +843,17 @@ def test_forwardport_changelog_no_new(npm_package, runner, mocker, open_mock, gi util.run(f"git tag v{VERSION_SPEC}", cwd=util.CHECKOUT_NAME) # Run the forwardport workflow against default branch - runner(["forwardport-changelog", HTML_URL]) - - assert len(open_mock.mock_calls) == 3 + runner(["forwardport-changelog", release.html_url]) log = get_log() assert "before-forwardport-changelog" in log assert "after-forwardport-changelog" in log -def test_forwardport_changelog_has_new(npm_package, runner, mocker, open_mock, git_prep): +def test_forwardport_changelog_has_new(npm_package, runner, mocker, mock_github, git_prep): + release = create_draft_release("bar") - open_mock.side_effect = [MockHTTPResponse([REPO_DATA]), MockHTTPResponse()] - current = util.run("git branch --show-current") + current = util.run("git branch --show-current", cwd=util.CHECKOUT_NAME) # Create a branch with a changelog entry util.run("git checkout -b backport_branch", cwd=util.CHECKOUT_NAME) @@ -983,9 +877,8 @@ def test_forwardport_changelog_has_new(npm_package, runner, mocker, open_mock, g # Run the forwardport workflow against default branch url = osp.abspath(npm_package) os.chdir(npm_package) - runner(["forwardport-changelog", HTML_URL, "--branch", current]) + runner(["forwardport-changelog", release.html_url, "--branch", current]) - assert len(open_mock.call_args) == 2 util.run(f"git checkout {current}", cwd=npm_package) expected = """ diff --git a/jupyter_releaser/tests/test_functions.py b/jupyter_releaser/tests/test_functions.py index 03d4f521..a15dfc6d 100644 --- a/jupyter_releaser/tests/test_functions.py +++ b/jupyter_releaser/tests/test_functions.py @@ -72,12 +72,10 @@ def test_get_version_npm(npm_package): assert util.get_version() == "1.0.1" -def test_format_pr_entry(mocker, open_mock): - data = dict(title="foo", user=dict(login="bar", html_url=testutil.HTML_URL)) - open_mock.return_value = testutil.MockHTTPResponse(data) - resp = changelog.format_pr_entry("snuffy/foo", 121, auth="baz") - open_mock.assert_called_once() - +def test_format_pr_entry(mock_github): + gh = GhApi(owner="snuffy", repo="foo") + info = gh.pulls.create("title", "head", "base", "body", True, False, None) + resp = changelog.format_pr_entry("snuffy/foo", info["number"], auth="baz") assert resp.startswith("- ") @@ -331,9 +329,16 @@ def test_get_config_file(git_repo): assert "before-build-python" in config["hooks"]["before-build-python"] -def test_get_latest_draft_release(mocker, open_mock): - open_mock.side_effect = [testutil.MockHTTPResponse([testutil.REPO_DATA, testutil.REPO_DATA_2])] - gh = GhApi() +def test_get_latest_draft_release(mock_github): + gh = GhApi(owner="foo", repo="bar") + gh.create_release( + "v1.0.0", + "main", + "v1.0.0", + "body", + True, + True, + files=[], + ) latest = util.lastest_draft_release(gh) - assert latest.name == testutil.REPO_DATA_2["name"] - assert len(open_mock.call_args) == 2 + assert latest.name == "v1.0.0" diff --git a/jupyter_releaser/tests/test_mock_github.py b/jupyter_releaser/tests/test_mock_github.py new file mode 100644 index 00000000..393dc286 --- /dev/null +++ b/jupyter_releaser/tests/test_mock_github.py @@ -0,0 +1,53 @@ +import os + +import requests +from ghapi.core import GhApi + + +def test_mock_github(mock_github): + owner = "foo" + repo_name = "bar" + auth = "hi" + + gh = GhApi(owner=owner, repo=repo_name, token=auth) + print(list(gh.repos.list_releases())) + + here = os.path.dirname(os.path.abspath(__file__)) + files = [os.path.join(here, f) for f in os.listdir(here)] + files = [f for f in files if not os.path.isdir(f)] + + release = gh.create_release( + "v1.0.0", + "main", + "v1.0.0", + "body", + True, + True, + files=files, + ) + + print(release.html_url) + + release = gh.repos.update_release( + release["id"], + release["tag_name"], + release["target_commitish"], + release["name"], + "body", + False, + release["prerelease"], + ) + assert release.draft is False + + for asset in release.assets: + headers = dict(Authorization=f"token {auth}", Accept="application/octet-stream") + print(asset.name) + with requests.get(asset.url, headers=headers, stream=True) as r: + r.raise_for_status() + for _ in r.iter_content(chunk_size=8192): + pass + + gh.repos.delete_release(release.id) + + pull = gh.pulls.create("title", "head", "base", "body", True, False, None) + gh.issues.add_labels(pull.number, ["documentation"]) diff --git a/jupyter_releaser/tests/util.py b/jupyter_releaser/tests/util.py index a8144882..0df3f8e0 100644 --- a/jupyter_releaser/tests/util.py +++ b/jupyter_releaser/tests/util.py @@ -1,12 +1,14 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import json +import os import shutil -import typing as t from pathlib import Path +import requests +from ghapi.core import GhApi + from jupyter_releaser import changelog, cli, util -from jupyter_releaser.util import run +from jupyter_releaser.util import MOCK_GITHUB_URL, get_latest_tag, run VERSION_SPEC = "1.0.1" @@ -158,33 +160,16 @@ def tbump_py_template(package_name="foo"): {changelog.START_MARKER} -## 0.0.1 +## 0.0.2 -Initial commit +Second commit {changelog.END_MARKER} -""" -HTML_URL = "https://github.com/snuffy/test/releases/tag/bar" -URL = "https://api.gihub.com/repos/snuffy/test/releases/tags/bar" -REPO_DATA = dict( - body="bar", - tag_name=f"v{VERSION_SPEC}", - target_commitish="bar", - name="foo", - prerelease=False, - draft=True, - created_at="2013-02-27T19:35:32Z", -) -REPO_DATA_2 = dict( - body="bar", - tag_name=f"v{VERSION_SPEC}", - target_commitish="bar", - name="foo2", - prerelease=False, - draft=True, - created_at="2013-02-27T20:35:32Z", -) +## 0.0.1 + +Initial commit +""" def mock_changelog_entry(package_path, runner, mocker, version_spec=VERSION_SPEC): @@ -301,52 +286,25 @@ def write_files(git_repo, sub_packages=None, package_name="foo", module_name=Non return git_repo -class MockHTTPResponse: - header: t.Dict[str, t.Any] = {} - code = 200 - - def __init__(self, data=None): - self.url = "" - data = data or {} - defaults = dict(id="foo", html_url=HTML_URL, url=URL, upload_url=URL, number=100) - if isinstance(data, list): - for datum in data: - for key in defaults: - datum.setdefault(key, defaults[key]) - else: - for key in defaults: - data.setdefault(key, defaults[key]) - self.data = json.dumps(data).encode("utf-8") - self.headers = {} - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - pass - - def read(self, amt=None): - return self.data - - @property - def status(self): - return self.code - - -class MockRequestResponse: - def __init__(self, filename, status_code=200): - self.filename = filename - self.status_code = status_code - - def raise_for_status(self): - pass - - def __enter__(self): - return self +def create_draft_release(ref="bar", files=None): + gh = GhApi("snuffy", "test") + return gh.create_release( + ref, + "bar", + ref, + "body", + True, + True, + files=files or [], + ) - def __exit__(self, *args): - pass - def iter_content(self, *args, **kwargs): - with open(self.filename, "rb") as fid: - return [fid.read()] +def create_tag_ref(): + curr_dir = os.getcwd() + os.chdir(util.CHECKOUT_NAME) + ref = get_latest_tag(None) + sha = run("git rev-parse HEAD") + url = f"{MOCK_GITHUB_URL}/create_tag_ref/{ref}/{sha}" + requests.post(url) + os.chdir(curr_dir) + return ref diff --git a/jupyter_releaser/util.py b/jupyter_releaser/util.py index f0981f48..0d34f8d8 100644 --- a/jupyter_releaser/util.py +++ b/jupyter_releaser/util.py @@ -9,6 +9,7 @@ import re import shlex import shutil +import subprocess import sys import tempfile import time @@ -18,6 +19,7 @@ from pathlib import Path from subprocess import PIPE, CalledProcessError, check_output +import requests import toml from importlib_resources import files from jsonschema import Draft4Validator as Validator @@ -53,6 +55,8 @@ GIT_FETCH_CMD = "git fetch origin --filter=blob:none --quiet" +MOCK_GITHUB_URL = "http://127.0.0.1:8000" + def run(cmd, **kwargs): """Run a command as a subprocess and get the output as a string""" @@ -416,3 +420,22 @@ def read_config(): validator = Validator(SCHEMA) validator.validate(config) return config + + +def start_mock_github(): + proc = subprocess.Popen([sys.executable, "-m", "uvicorn", "jupyter_releaser.mock_github:app"]) + + try: + ret = proc.wait(1) + if ret > 0: + raise ValueError(f"mock_github failed with {proc.returncode}") + except subprocess.TimeoutExpired: + pass + + while 1: + try: + requests.get(MOCK_GITHUB_URL) + break + except requests.ConnectionError: + pass + return proc diff --git a/pyproject.toml b/pyproject.toml index 4a1660bf..2b0720c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,9 +49,11 @@ content-type = "text/markdown" [project.optional-dependencies] test = [ "coverage", + "fastapi", "pytest>=7.0", "pytest-cov", "pytest-mock", + "uvicorn" ] [project.scripts]