Skip to content

Commit c16943d

Browse files
author
Steven Silvester
authored
Merge pull request #176 from davidbrochart/py_multi_package
Handle multiple Python packages
2 parents b76199b + 1a50bae commit c16943d

File tree

11 files changed

+230
-61
lines changed

11 files changed

+230
-61
lines changed

docs/source/get_started/making_first_release.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ already uses Jupyter Releaser.
2323
owner2/repo2,token2
2424
```
2525
26+
If you have multiple Python packages in one repository, you can point to them as follows:
27+
28+
```text
29+
owner1/repo1/path/to/package1,token1
30+
owner1/repo1/path/to/package2,token1
31+
```
32+
2633
- If the repo generates npm release(s), add access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens), saved as `NPM_TOKEN` in "Secrets".
2734
2835
## Draft Changelog

docs/source/how_to_guides/convert_repo.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ A. Prep the `jupyter_releaser` fork:
2323
_Note_ For security reasons, it is recommended that you scope the access
2424
to a single repository, and use a variable called `PYPI_TOKEN_MAP` that is formatted as follows:
2525

26-
```
26+
```text
2727
owner1/repo1,token1
2828
owner2/repo2,token2
2929
```
3030

31+
If you have multiple Python packages in one repository, you can point to them as follows:
32+
33+
```text
34+
owner1/repo1/path/to/package1,token1
35+
owner1/repo1/path/to/package2,token1
36+
```
37+
3138
- [ ] If needed, add access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens), saved as `NPM_TOKEN`.
3239

3340
B. Prep target repository:

jupyter_releaser/cli.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ def main(force):
171171
)
172172
]
173173

174+
python_packages_options = [
175+
click.option(
176+
"--python-packages",
177+
envvar="RH_PYTHON_PACKAGES",
178+
default=["."],
179+
multiple=True,
180+
help="The list of paths to Python packages",
181+
)
182+
]
183+
174184
dry_run_options = [
175185
click.option(
176186
"--dry-run", is_flag=True, envvar="RH_DRY_RUN", help="Run as a dry run"
@@ -273,10 +283,15 @@ def prep_git(ref, branch, repo, auth, username, git_url):
273283
@main.command()
274284
@add_options(version_spec_options)
275285
@add_options(version_cmd_options)
286+
@add_options(python_packages_options)
276287
@use_checkout_dir()
277-
def bump_version(version_spec, version_cmd):
288+
def bump_version(version_spec, version_cmd, python_packages):
278289
"""Prep git and env variables and bump version"""
279-
lib.bump_version(version_spec, version_cmd)
290+
prev_dir = os.getcwd()
291+
for python_package in python_packages:
292+
os.chdir(python_package)
293+
lib.bump_version(version_spec, version_cmd)
294+
os.chdir(prev_dir)
280295

281296

282297
@main.command()
@@ -363,13 +378,24 @@ def check_changelog(
363378

364379
@main.command()
365380
@add_options(dist_dir_options)
381+
@add_options(python_packages_options)
366382
@use_checkout_dir()
367-
def build_python(dist_dir):
383+
def build_python(dist_dir, python_packages):
368384
"""Build Python dist files"""
369-
if not util.PYPROJECT.exists() and not util.SETUP_PY.exists():
370-
util.log("Skipping build-python since there are no python package files")
371-
return
372-
python.build_dist(dist_dir)
385+
prev_dir = os.getcwd()
386+
clean = True
387+
for python_package in python_packages:
388+
os.chdir(python_package)
389+
if not util.PYPROJECT.exists() and not util.SETUP_PY.exists():
390+
util.log(
391+
f"Skipping build-python in {python_package} since there are no python package files"
392+
)
393+
else:
394+
python.build_dist(
395+
Path(os.path.relpath(".", python_package)) / dist_dir, clean=clean
396+
)
397+
clean = False
398+
os.chdir(prev_dir)
373399

374400

375401
@main.command()
@@ -580,6 +606,7 @@ def extract_release(auth, dist_dir, dry_run, release_url, npm_install_options):
580606
default="https://pypi.org/simple/",
581607
)
582608
@add_options(dry_run_options)
609+
@add_options(python_packages_options)
583610
@click.argument("release-url", nargs=1, required=False)
584611
@use_checkout_dir()
585612
def publish_assets(
@@ -591,18 +618,21 @@ def publish_assets(
591618
twine_registry,
592619
dry_run,
593620
release_url,
621+
python_packages,
594622
):
595623
"""Publish release asset(s)"""
596-
lib.publish_assets(
597-
dist_dir,
598-
npm_token,
599-
npm_cmd,
600-
twine_cmd,
601-
npm_registry,
602-
twine_registry,
603-
dry_run,
604-
release_url,
605-
)
624+
for python_package in python_packages:
625+
lib.publish_assets(
626+
dist_dir,
627+
npm_token,
628+
npm_cmd,
629+
twine_cmd,
630+
npm_registry,
631+
twine_registry,
632+
dry_run,
633+
release_url,
634+
python_package,
635+
)
606636

607637

608638
@main.command()

jupyter_releaser/lib.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ def publish_assets(
381381
twine_registry,
382382
dry_run,
383383
release_url,
384+
python_package,
384385
):
385386
"""Publish release asset(s)"""
386387
os.environ["NPM_REGISTRY"] = npm_registry
@@ -393,7 +394,7 @@ def publish_assets(
393394
util.run("npm whoami")
394395

395396
if len(glob(f"{dist_dir}/*.whl")):
396-
twine_token = python.get_pypi_token(release_url)
397+
twine_token = python.get_pypi_token(release_url, python_package)
397398

398399
if dry_run:
399400
# Start local pypi server with no auth, allowing overwrites,

jupyter_releaser/python.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717
SETUP_PY = util.SETUP_PY
1818

1919

20-
def build_dist(dist_dir):
20+
def build_dist(dist_dir, clean=True):
2121
"""Build the python dist files into a dist folder"""
2222
# Clean the dist folder of existing npm tarballs
2323
os.makedirs(dist_dir, exist_ok=True)
2424
dest = Path(dist_dir)
25-
for pkg in glob(f"{dest}/*.gz") + glob(f"{dest}/*.whl"):
26-
os.remove(pkg)
25+
if clean:
26+
for pkg in glob(f"{dest}/*.gz") + glob(f"{dest}/*.whl"):
27+
os.remove(pkg)
2728

2829
if PYPROJECT.exists():
29-
util.run(f"python -m build --outdir {dest} .", quiet=True)
30+
util.run(f"python -m build --outdir {dest} .", quiet=True, show_cwd=True)
3031
elif SETUP_PY.exists():
3132
util.run(f"python setup.py sdist --dist-dir {dest}", quiet=True)
3233
util.run(f"python setup.py bdist_wheel --dist-dir {dest}", quiet=True)
@@ -60,7 +61,7 @@ def check_dist(dist_file, test_cmd=""):
6061
util.run(f"{bin_path}/{test_cmd}")
6162

6263

63-
def get_pypi_token(release_url):
64+
def get_pypi_token(release_url, python_package):
6465
"""Get the PyPI token
6566
6667
Note: Do not print the token in CI since it will not be sanitized
@@ -70,6 +71,8 @@ def get_pypi_token(release_url):
7071
if pypi_token_map and release_url:
7172
parts = release_url.replace("https://github.com/", "").split("/")
7273
repo_name = f"{parts[0]}/{parts[1]}"
74+
if python_package != ".":
75+
repo_name += f"/{python_package}"
7376
util.log(f"Looking for PYPI token for {repo_name} in token map")
7477
for line in pypi_token_map.splitlines():
7578
name, _, token = line.partition(",")

jupyter_releaser/tee.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ def tee_func(line: bytes, sink: List[str], pipe: Optional[Any]) -> None:
140140

141141

142142
def run(args: Union[str, List[str]], **kwargs: Any) -> CompletedProcess:
143-
"""Drop-in replacement for subprocerss.run that behaves like tee.
143+
"""Drop-in replacement for subprocess.run that behaves like tee.
144144
Extra arguments added by our version:
145145
echo: False - Prints command before executing it.
146146
quiet: False - Avoid printing output
147+
show_cwd: False - Prints the current working directory.
147148
"""
148149
if isinstance(args, str):
149150
cmd = args
@@ -158,7 +159,11 @@ def run(args: Union[str, List[str]], **kwargs: Any) -> CompletedProcess:
158159
if kwargs.get("echo", False):
159160
# This is modified from the default implementation since
160161
# we want all output to be interleved on the same stream
161-
print(f"COMMAND: {cmd}", file=sys.stderr)
162+
prefix = "COMMAND"
163+
if kwargs.pop("show_cwd", False):
164+
prefix += f" (in '{os.getcwd()}')"
165+
prefix += ":"
166+
print(f"{prefix} {cmd}", file=sys.stderr)
162167

163168
loop = asyncio.get_event_loop()
164169
result = loop.run_until_complete(_stream_subprocess(cmd, **kwargs))

jupyter_releaser/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def py_package(git_repo):
7575
return testutil.create_python_package(git_repo)
7676

7777

78+
@fixture
79+
def py_multipackage(git_repo):
80+
return testutil.create_python_package(git_repo, multi=True)
81+
82+
7883
@fixture
7984
def npm_package(git_repo):
8085
return testutil.create_npm_package(git_repo)

jupyter_releaser/tests/test_cli.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def test_list_envvars(runner):
162162
output: RH_CHANGELOG_OUTPUT
163163
post-version-message: RH_POST_VERSION_MESSAGE
164164
post-version-spec: RH_POST_VERSION_SPEC
165+
python-packages: RH_PYTHON_PACKAGES
165166
ref: RH_REF
166167
release-message: RH_RELEASE_MESSAGE
167168
repo: RH_REPOSITORY
@@ -596,6 +597,64 @@ def helper(path, **kwargs):
596597
assert "after-extract-release" in log
597598

598599

600+
@pytest.mark.skipif(
601+
os.name == "nt" and sys.version_info.major == 3 and sys.version_info.minor < 8,
602+
reason="See https://bugs.python.org/issue26660",
603+
)
604+
def test_extract_dist_multipy(
605+
py_multipackage, runner, mocker, open_mock, tmp_path, git_prep
606+
):
607+
git_repo = py_multipackage[0]["abs_path"]
608+
changelog_entry = mock_changelog_entry(git_repo, runner, mocker)
609+
610+
# Create the dist files
611+
dist_dir = normalize_path(Path(util.CHECKOUT_NAME).resolve() / "dist")
612+
for package in py_multipackage:
613+
run(
614+
f"python -m build . -o {dist_dir}",
615+
cwd=Path(util.CHECKOUT_NAME) / package["rel_path"],
616+
)
617+
618+
# Finalize the release
619+
runner(["tag-release"])
620+
621+
os.makedirs("staging")
622+
shutil.move(f"{util.CHECKOUT_NAME}/dist", "staging")
623+
624+
def helper(path, **kwargs):
625+
return MockRequestResponse(f"{git_repo}/staging/dist/{path}")
626+
627+
get_mock = mocker.patch("requests.get", side_effect=helper)
628+
629+
tag_name = f"v{VERSION_SPEC}"
630+
631+
dist_names = [osp.basename(f) for f in glob("staging/dist/*.*")]
632+
releases = [
633+
dict(
634+
tag_name=tag_name,
635+
target_commitish=util.get_branch(),
636+
assets=[dict(name=dist_name, url=dist_name) for dist_name in dist_names],
637+
)
638+
]
639+
sha = run("git rev-parse HEAD", cwd=util.CHECKOUT_NAME)
640+
641+
tags = [dict(ref=f"refs/tags/{tag_name}", object=dict(sha=sha))]
642+
url = normalize_path(osp.join(os.getcwd(), util.CHECKOUT_NAME))
643+
open_mock.side_effect = [
644+
MockHTTPResponse(releases),
645+
MockHTTPResponse(tags),
646+
MockHTTPResponse(dict(html_url=url)),
647+
]
648+
649+
runner(["extract-release", HTML_URL])
650+
assert len(open_mock.mock_calls) == 2
651+
assert len(get_mock.mock_calls) == len(dist_names) == 2 * len(py_multipackage)
652+
653+
log = get_log()
654+
assert "before-extract-release" not in log
655+
assert "after-extract-release" in log
656+
657+
599658
@pytest.mark.skipif(
600659
os.name == "nt" and sys.version_info.major == 3 and sys.version_info.minor < 8,
601660
reason="See https://bugs.python.org/issue26660",

jupyter_releaser/tests/test_functions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Jupyter Development Team.
22
# Distributed under the terms of the Modified BSD License.
33
import json
4+
import os
45
import shutil
56
from pathlib import Path
67

@@ -30,6 +31,16 @@ def test_get_version_python(py_package):
3031
assert util.get_version() == "0.0.2a0"
3132

3233

34+
def test_get_version_multipython(py_multipackage):
35+
prev_dir = os.getcwd()
36+
for package in py_multipackage:
37+
os.chdir(package["rel_path"])
38+
assert util.get_version() == "0.0.1"
39+
util.bump_version("0.0.2a0")
40+
assert util.get_version() == "0.0.2a0"
41+
os.chdir(prev_dir)
42+
43+
3344
def test_get_version_npm(npm_package):
3445
assert util.get_version() == "1.0.0"
3546
npm = util.normalize_path(shutil.which("npm"))

0 commit comments

Comments
 (0)