From 7dd1325bef323daf1817d527ccf30a8a3f530c2e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 17 Jul 2024 05:07:30 -0400 Subject: [PATCH 01/29] [pypi.Uploader] No upload code in place, but it tells you everything it *would* be doing. --- Pipfile | 12 +++++++++ Pipfile.lock | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ bork/api.py | 5 +++- bork/builder.py | 8 +++++- bork/pypi.py | 48 +++++++++++++++++++++++++++--------- bork/version.py | 2 +- pyproject.toml | 10 ++------ 7 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..8d506b5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +bork = {editable = true, path = "."} + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..746c024 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,65 @@ +{ + "_meta": { + "hash": { + "sha256": "82ec35691fb595e89ef60f258b8334ba11f3f49074fd0dcf1f0c0845810e97e7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "build": { + "hashes": [ + "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d", + "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4" + ], + "markers": "python_version >= '3.8'", + "version": "==1.2.1" + }, + "coloredlogs": { + "hashes": [ + "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", + "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==15.0.1" + }, + "humanfriendly": { + "hashes": [ + "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", + "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==10.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pip": { + "hashes": [ + "sha256:7cd207eed4c60b0f411b444cd1464198fe186671c323b6cd6d433ed80fc9d247", + "sha256:e5458a0b89f2755e0ee8c0c77613fe5273e05f337907874d64f13171a898a7ff" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1.2" + }, + "pyproject-hooks": { + "editable": true, + "path": "." + } + }, + "develop": {} +} diff --git a/bork/api.py b/bork/api.py index 796ff6b..bb51f37 100644 --- a/bork/api.py +++ b/bork/api.py @@ -128,7 +128,10 @@ def release(repository_name, dry_run): bork_config = pyproject.get('tool', {}).get('bork', {}) release_config = bork_config.get('release', {}) github_token = os.environ.get('BORK_GITHUB_TOKEN', None) - version = builder.version_from_bdist_file() + try: + version = builder.version_from_bdist_file() + except builder.NeedsBuildError: + raise RuntimeError("No wheel files found. Please run 'bork build' first.") project_name = pyproject.get('project', {}).get('name', None) diff --git a/bork/builder.py b/bork/builder.py index 7a6e034..249916e 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -10,6 +10,9 @@ from .filesystem import load_pyproject, try_delete # from .log import logger +class NeedsBuildError(Exception): + pass + # The "proper" way to handle the default would be to check python_requires # in setup.cfg. But, since Bork needs Python 3, there's no point. @@ -62,7 +65,10 @@ def _python_interpreter(config): def _bdist_file(): - return max(Path.cwd().glob('dist/*.whl')) + files = list(Path.cwd().glob('dist/*.whl')) + if not files: + raise NeedsBuildError + return max(files) def _prepare_zipapp(dest, bdist_file): diff --git a/bork/pypi.py b/bork/pypi.py index dd05986..0127706 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -1,8 +1,6 @@ import configparser from pathlib import Path -from twine.cli import dispatch as twine_dispatch # type: ignore - from .asset_manager import download_assets from .filesystem import find_files from .log import logger @@ -50,15 +48,41 @@ def download(repository_name, package, release, file_pattern, directory): directory) +class Uploader: + PYPI_ENDPOINT = "https://upload.pypi.org/legacy/" + TESTPYPI_ENDPOINT = "https://test.pypi.org/legacy/" + + def __init__(self, files, repository=None): + if repository == "pypi" or repository is None: + repository = self.PYPI_ENDPOINT + elif repository == "testpypi": + repository = self.TESTPYPI_ENDPOINT + elif repository.startswith("http://") or repository.startswith("https://"): + pass # Everything is fine. + else: + logger().error("Only the 'pypi' and 'testpypi' repository shorthands are supported.") + logger().error("Please provide a full URL for custom endpoints, such as .") + logger().error("Please open an issue at https://github.com/duckinator/bork if you need help.") + exit(1) + + if not files: + logger().error("No files to upload?") + exit(1) + + self.files = files + self.repository = repository + + def upload(self, dry_run=True): + msg_prefix = "Uploading" + if dry_run: + logger().warn("Skipping PyPi release since this is a dry run.") + msg_prefix = "Pretending to upload" + + logger().info("%s %i files to PyPi repository '%s':", msg_prefix, len(self.files), self.repository) + for file in self.files: + logger().info("- %s %s", file, "(skipping for dry run)" if dry_run else "") + + def upload(repository_name, *globs, dry_run=False): files = find_files(globs) - logger().info( - "Uploading files to PyPI instance '%s': %s", - repository_name, - ', '.join((f"'{file}'" for file in files)), - ) - - if dry_run: - logger().warning('Skipping PyPI upload step since this is a dry run.') - else: - twine_dispatch(['upload', '--repository', repository_name, *files]) + Uploader(files, repository_name).upload(dry_run) diff --git a/bork/version.py b/bork/version.py index caa10e5..4f50d11 100644 --- a/bork/version.py +++ b/bork/version.py @@ -1,4 +1,4 @@ # This file should only ever be modified to change the version. # This will automatically prepare and create a release. -__version__ = '8.0.0' +__version__ = '9.0.0' diff --git a/pyproject.toml b/pyproject.toml index d3fb35e..cd06e70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,21 +22,15 @@ classifiers = [ "Operating System :: OS Independent", ] -# The packaging library is used by bork.github_api, and also pulled in via -# twine -> readme_renderer -> bleach -> packaging. dependencies = [ - "build~=1.0.3", + "build~=1.2.1", + # The packaging library is used by bork.github_api. "packaging~=23.2", "toml~=0.10.2; python_version < '3.11'", - "twine==4.0.1", "coloredlogs~=15.0.1", # pip is used by bork.builder._prepare_zipapp(), # meaning it is in fact a proper dependency. "pip", - # readme-renderer is a dependency of twine. - # With v42.0, it switched from the `bleach` library to the `nh3` library. - # However, nh3 uses a Rust library, so it requires having Rust installed. - "readme-renderer>=35.0,<42.0", ] requires-python = ">= 3.8" From a9f85109ba62b0eb65b70c9e90ec0792844572b8 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 30 Jul 2024 22:41:05 -0400 Subject: [PATCH 02/29] Use backend-generated metadata instead of trying to get it ourselves. --- bork/builder.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/bork/builder.py b/bork/builder.py index 249916e..991affe 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -1,7 +1,10 @@ import configparser +import contextlib +import importlib from pathlib import Path import subprocess import sys +import tempfile # Slight kludge so we can have a function named zipapp(). import zipapp as Zipapp # noqa: N812 @@ -18,6 +21,18 @@ class NeedsBuildError(Exception): # in setup.cfg. But, since Bork needs Python 3, there's no point. DEFAULT_PYTHON_INTERPRETER = '/usr/bin/env python3' +@contextlib.contextmanager +def prepared_environment(srcdir, outdir): + """Usage: + with prepared_environment(".", "./dist") as (env, builder): + # ... + """ + with build.env.DefaultIsolatedEnv() as env: + builder = build.ProjectBuilder.from_isolated_env(env, srcdir) + # Install deps from `project.build_system_requires` + env.install(builder.build_system_requires) + # Yield env and builder. + yield (env, builder) def dist(backend_settings=None): """Build the sdist and wheel distributions. @@ -44,17 +59,14 @@ def dist(backend_settings=None): return results -def _setup_cfg_package_name(): - setup_cfg = configparser.ConfigParser() - setup_cfg.read('setup.cfg') - - if 'metadata' not in setup_cfg or 'name' not in setup_cfg['metadata']: - raise RuntimeError( - "You need to set project.name in pyproject.toml OR metadata.name in setup.cfg" - ) - - return setup_cfg['metadata']['name'] +def metadata(): + srcdir = "." + outdir = "./dist" + with prepared_environment(srcdir, outdir) as (env, builder): + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(builder.metadata_path(tmpdir)) + return importlib.metadata.Distribution.at(path).metadata def _python_interpreter(config): # To override the default interpreter, add this to your project's setup.cfg: @@ -99,11 +111,7 @@ def zipapp(): if not want_zipapp: return - # If the project name is specified in pyproject.toml, use it. - # Otherwise, try getting it from setup.cfg. - name = pyproject.get('project', {}).get('name', None) - if name is None: - name = _setup_cfg_package_name() + name = metadata()['name'] dest = str(Path('build', 'zipapp')) version = version_from_bdist_file() main = zipapp_cfg['main'] From 9374515d686f5c5f1b2555a3b7161ae4fd96adc3 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 30 Jul 2024 22:44:27 -0400 Subject: [PATCH 03/29] Add bork.httpform: a crude attempt at implementing RFC 7578. --- bork/httpform.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 bork/httpform.py diff --git a/bork/httpform.py b/bork/httpform.py new file mode 100644 index 0000000..74ccbf6 --- /dev/null +++ b/bork/httpform.py @@ -0,0 +1,70 @@ +""" +Crude implementation of RFC 7578: "Returning Values from Forms: multipart/form-data" + +https://datatracker.ietf.org/doc/html/rfc7578 +""" + +import http.client +import mimetypes +from pathlib import Path +import secrets + +class Form: + def __init__(self, parts): + self.parts = parts + + def __bytes__(self, boundary=None): + if boundary is None: + boundary = secrets.token_hex(32) + + lines = [] + + for part in self.parts: + lines.append(f"--{boundary}".encode()) + lines += part._bytes_lines() + lines.append(f"--{boundary}--".encode()) + lines.append("".encode()) + + return b"\r\n".join(lines) + +class FormField: + def __init__(self, field, value): + self.field = field + self.value = value + self.filename = None + + def _bytes_lines(self): + lines = [] + + disposition = f'form-data; name="{self.field}"' + if self.filename: + disposition += f'; filename="{self.filename}"' + lines.append(f"Content-Disposition: {disposition}".encode()) + + if self.filename: + lines.append(f"Content-Type: {self._guess_content_type(self.filename)}".encode()) + else: + lines.append(f"Content-Type: text/plain;charset=UTF-8".encode()) + + lines.append("".encode()) + + value = self.value + if isinstance(value, str): + value = value.encode() + lines.append(value) + return lines + + def __bytes__(self): + return b"\r\n".join(self._bytes_lines()) + + def _guess_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + +class FormFile(FormField): + def __init__(self, key, filename, value=None): + if value is None: + value = Path(filename).read_bytes() + + super().__init__(key, value) + self.filename = filename From 1e20cdd04430dab6fa517643abd1efbb7277990f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 30 Jul 2024 23:43:33 -0400 Subject: [PATCH 04/29] Add bork.filesystem.wheel_file_info(). --- bork/filesystem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bork/filesystem.py b/bork/filesystem.py index 5e142f9..898cc22 100644 --- a/bork/filesystem.py +++ b/bork/filesystem.py @@ -1,5 +1,6 @@ from pathlib import Path import shutil +import re try: import tomllib @@ -46,3 +47,11 @@ def try_delete(path): shutil.rmtree(path) elif Path(path).exists(): raise RuntimeError(f"'{path}' is not a directory") + + +_WHEEL_FILENAME_REGEX = re.compile( + r'^(?P.+)-(?P.+)(-(?P.+))?-(?P.+)-(?P.+)-(?P.+).whl$', + re.VERBOSE, +) +def wheel_file_info(path): + return re.match(_WHEEL_FILENAME_REGEX, Path(path).name).groupdict() From 0cd59830cf306f07670b288c2a37467c6b7a1939 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:19:46 -0400 Subject: [PATCH 05/29] Remove unneeded httpform.py --- bork/httpform.py | 70 ------------------------------------------------ 1 file changed, 70 deletions(-) delete mode 100644 bork/httpform.py diff --git a/bork/httpform.py b/bork/httpform.py deleted file mode 100644 index 74ccbf6..0000000 --- a/bork/httpform.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Crude implementation of RFC 7578: "Returning Values from Forms: multipart/form-data" - -https://datatracker.ietf.org/doc/html/rfc7578 -""" - -import http.client -import mimetypes -from pathlib import Path -import secrets - -class Form: - def __init__(self, parts): - self.parts = parts - - def __bytes__(self, boundary=None): - if boundary is None: - boundary = secrets.token_hex(32) - - lines = [] - - for part in self.parts: - lines.append(f"--{boundary}".encode()) - lines += part._bytes_lines() - lines.append(f"--{boundary}--".encode()) - lines.append("".encode()) - - return b"\r\n".join(lines) - -class FormField: - def __init__(self, field, value): - self.field = field - self.value = value - self.filename = None - - def _bytes_lines(self): - lines = [] - - disposition = f'form-data; name="{self.field}"' - if self.filename: - disposition += f'; filename="{self.filename}"' - lines.append(f"Content-Disposition: {disposition}".encode()) - - if self.filename: - lines.append(f"Content-Type: {self._guess_content_type(self.filename)}".encode()) - else: - lines.append(f"Content-Type: text/plain;charset=UTF-8".encode()) - - lines.append("".encode()) - - value = self.value - if isinstance(value, str): - value = value.encode() - lines.append(value) - return lines - - def __bytes__(self): - return b"\r\n".join(self._bytes_lines()) - - def _guess_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - -class FormFile(FormField): - def __init__(self, key, filename, value=None): - if value is None: - value = Path(filename).read_bytes() - - super().__init__(key, value) - self.filename = filename From bf6384e9eaed73809b359e51976baec2d3c5fe6c Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:20:23 -0400 Subject: [PATCH 06/29] Prepare .cirrus.yml for Bork self-release. --- .cirrus.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index b08b90b..25fa8c4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -170,8 +170,8 @@ Release_task: only_if: "changesInclude('bork/version.py') && $BRANCH == 'main' && $CIRRUS_CRON == ''" depends_on: [CI success] env: - TWINE_USERNAME: "__token__" - TWINE_PASSWORD: ENCRYPTED[00007524e18bea7b59efea288653efa57b1dbd235ed8af00cc325febfc9076631a2bf58ed330d8fa7ca057adb81579b0] + BORK_PYPI_USERNAME: "__token__" + BORK_PYPI_PASSWORD: ENCRYPTED[00007524e18bea7b59efea288653efa57b1dbd235ed8af00cc325febfc9076631a2bf58ed330d8fa7ca057adb81579b0] BORK_GITHUB_TOKEN: ENCRYPTED[29eac4d276e1e86020bbc415c04ce91136508e5bdeacd756310c32ebdd3fb7f910c6ed3159f08765915d9e656964e8f5] container: image: python:3.11-slim From 715f7f8d80d90eb8dcfae96b2c90ab9bdaa7e56e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:21:25 -0400 Subject: [PATCH 07/29] Add --[no-]github and --[no-]pypi flags to override pyproject.toml configuration. --- bork/api.py | 16 +++++++++++++++- bork/cli.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/bork/api.py b/bork/api.py index bb51f37..2262a28 100644 --- a/bork/api.py +++ b/bork/api.py @@ -113,7 +113,7 @@ def download(package, release_tag, file_pattern, directory): downloader.download(package, release_tag, file_pattern, directory) # type:ignore -def release(repository_name, dry_run): +def release(repository_name, dry_run, github_release_override=None, pypi_release_override=None): """Uploads build artifacts to a PyPi instance or GitHub, as configured in pyproject.toml. @@ -123,6 +123,14 @@ def release(repository_name, dry_run): dry_run: If True, don't actually release, just show what a release would do. + + github_release_override: + If True, enable GitHub releases; if False, disable GitHub releases; + if None, respect the configuration in pyproject.toml. + + py_release_override: + If True, enable PyPi releases; if False, disable PyPi releases; + if None, respect the configuration in pyproject.toml. """ pyproject = load_pyproject() bork_config = pyproject.get('tool', {}).get('bork', {}) @@ -141,6 +149,12 @@ def release(repository_name, dry_run): release_to_github = release_config.get('github', False) release_to_pypi = release_config.get('pypi', True) + if github_release_override is not None: + release_to_github = github_release_override + + if pypi_release_override is not None: + release_to_pypi = pypi_release_override + if not release_to_github and not release_to_pypi: print('Configured to release to neither PyPi nor GitHub?') diff --git a/bork/cli.py b/bork/cli.py index 7f36dd7..62f0888 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -109,7 +109,7 @@ def release(args): pypi_repository = args.pypi_repository if args.test_pypi: pypi_repository = 'testpypi' - api.release(pypi_repository, args.dry_run) + api.release(pypi_repository, args.dry_run, args.github, args.pypi) def run(args): @@ -169,6 +169,19 @@ def _arg_parser(): "Equivalent to '--pypi-repository testpypi'.") releasep.add_argument("--dry-run", action="store_true", help="Don't actually release, just show what a release would do.") + + releasep.add_argument("--github", action="store_true", + help="Release to GitHub, ignoring pyproject.toml.") + releasep.add_argument("--no-github", dest="github", action="store_false", + help="Don't release to GitHub, ignoring pyproject.toml.") + releasep.set_defaults(github=None) + + releasep.add_argument("--pypi", action="store_true", + help="Release to PyPi, ignoring pyproject.toml.") + releasep.add_argument("--no-pypi", dest="pypi", action="store_false", + help="Don't release to GitHub, ignoring pyproject.toml.") + releasep.set_defaults(pypi=None) + releasep.set_defaults(func=release) runp = subparsers.add_parser("run", help="Run the specified alias.") From 6ddae2e31e85b661cbea0fe6a582648d21d86d74 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:22:04 -0400 Subject: [PATCH 08/29] Have default logging level be INFO, and make --verbose and --debug be equivalent to the old --debug. --- bork/cli.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bork/cli.py b/bork/cli.py index 62f0888..8b8d401 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -214,15 +214,11 @@ def main(cmd_args=None): import coloredlogs # type: ignore # pylint: enable=import-outside-toplevel - # Default to only printing WARNING and higher severity messages. - log_level = logging.WARNING + # Default to only printing INFO and higher severity messages. + log_level = logging.INFO - # If we got '--verbose', print INFO and higher severity messages. - if args.verbose: - log_level = logging.INFO - - # If we got '--debug', print DEBUG and higher severity messages. - if args.debug: + # If we got '--verbose' or '--debug', print DEBUG and higher severity messages. + if args.verbose or args.debug: log_level = logging.DEBUG coloredlogs.install( From 22730d0c574be53ed304604e5fa3f47a6992567f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:25:40 -0400 Subject: [PATCH 09/29] Consolidate implementations of --verbose and --debug. --- bork/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bork/cli.py b/bork/cli.py index 8b8d401..e613d22 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -127,10 +127,8 @@ def _arg_parser(): description="A build and release tool for Python projects, with ZipApp support.") parser.add_argument("--version", action="store_true", help="Print version information and exit.") - parser.add_argument("--verbose", action="store_true", + parser.add_argument("--verbose", "--debug", action="store_true", help="Enable verbose logging.") - parser.add_argument("--debug", action="store_true", - help="Enable VERY verbose logging. (Sometimes too noisy to be helpful.)") subparsers = parser.add_subparsers(title="Commands") aliasesp = subparsers.add_parser("aliases", @@ -218,7 +216,7 @@ def main(cmd_args=None): log_level = logging.INFO # If we got '--verbose' or '--debug', print DEBUG and higher severity messages. - if args.verbose or args.debug: + if args.verbose: log_level = logging.DEBUG coloredlogs.install( From 118d9f5564ec8cc4ca291d48ed60ff9008a5c856 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:26:25 -0400 Subject: [PATCH 10/29] Use urllib3 for PyPi uploads. --- bork/http.py | 40 ++++++++++++++++++++++++++++ bork/pypi.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 bork/http.py diff --git a/bork/http.py b/bork/http.py new file mode 100644 index 0000000..e21d73b --- /dev/null +++ b/bork/http.py @@ -0,0 +1,40 @@ +#import urllib.request +import urllib3 + +from . import version +from .log import logger + +MAX_RETRIES = False + +def _request(url, fields, headers, method, auth): + log = logger() + + user_agent = f"bork/{version.__version__} (+https://github.com/duckinator/bork)" + + http = urllib3.PoolManager() + headers = urllib3.util.make_headers(user_agent=user_agent, basic_auth=":".join(auth)) + response = http.request(method, url, fields=fields, headers=headers, retries=MAX_RETRIES) + + if 399 < response.status < 500: + raise RuntimeError(response.data.decode()) + + log.debug(response.getheaders()) + + log.debug("%s %s returned %i", method, response.geturl(), response.status) + + if response.status == 200: + log.info("Upload successful!") + else: + log.info(response.data.decode().strip()) + + return response + +def get(url, headers={}, auth=None): + return _request(url, None, headers, "GET", auth) + +def post(url, fields=None, headers=None, auth=None): + if headers is None: + headers = {} + + headers["Content-Type"] = "application/x-www-form-urlencoded" + return _request(url, fields, headers, "POST", auth) diff --git a/bork/pypi.py b/bork/pypi.py index 0127706..885c1d9 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -1,10 +1,14 @@ import configparser +import hashlib +import os from pathlib import Path +from . import builder from .asset_manager import download_assets -from .filesystem import find_files +from .filesystem import find_files, wheel_file_info from .log import logger from .pypi_api import get_download_info +from .http import post class Downloader: # pylint: disable=too-few-public-methods @@ -57,7 +61,10 @@ def __init__(self, files, repository=None): repository = self.PYPI_ENDPOINT elif repository == "testpypi": repository = self.TESTPYPI_ENDPOINT - elif repository.startswith("http://") or repository.startswith("https://"): + elif repository.startswith("http://"): + logger().error("Configured to use insecure repository: %s", repository) + exit(1) + elif repository.startswith("https://"): pass # Everything is fine. else: logger().error("Only the 'pypi' and 'testpypi' repository shorthands are supported.") @@ -72,15 +79,76 @@ def __init__(self, files, repository=None): self.files = files self.repository = repository + self.username = os.environ.get("BORK_PYPI_USERNAME", None) + self.password = os.environ.get("BORK_PYPI_PASSWORD", None) + + def _upload_file(self, url, file, metadata): + file_contents = Path(file).read_bytes() + file_digest = hashlib.sha256(file_contents).hexdigest() + + if file.endswith(".whl"): + file_type = "bdist_wheel" + pyversion = wheel_file_info(file)["pyversion"] + else: + file_type = "sdist" + pyversion = "source" + + md = metadata + + wanted_fields = [ + "summary", + "description", "description_content_type", + "keywords", "home_page", "download_url", + "author", "author_email", "maintainer", "maintainer_email", + "license", "classifier", + "requires_dist", "requires_python", "requires_external", + "project_url", + ] + + other_fields = [] + for key in md.keys(): + if key not in wanted_fields: + continue + + values = md[key] + if not isinstance(values, list): + values = [values] + + for value in values: + other_fields.append((key, value)) + + form = [ + (":action", "file_upload"), + ("protocol_version", "1"), + ("content", (Path(file).name, file_contents)), + ("sha256_digest", file_digest), + ("filetype", file_type), + ("pyversion", pyversion), + + # Required "core metadata" fields. + # These are set here to trigger a hard error if they're missing. + ("metadata_version", md["metadata_version"]), + ("name", md["name"]), + ("version", md["version"]), + + # Remaining "core metadata" fields. + *other_fields + ] + result = post(url, form, auth=(self.username, self.password)) + def upload(self, dry_run=True): msg_prefix = "Uploading" if dry_run: logger().warn("Skipping PyPi release since this is a dry run.") msg_prefix = "Pretending to upload" + metadata = builder.metadata().json + logger().info("%s %i files to PyPi repository '%s':", msg_prefix, len(self.files), self.repository) for file in self.files: logger().info("- %s %s", file, "(skipping for dry run)" if dry_run else "") + if not dry_run: + self._upload_file(self.repository, file, metadata) def upload(repository_name, *globs, dry_run=False): diff --git a/pyproject.toml b/pyproject.toml index cd06e70..7e68dd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "packaging~=23.2", "toml~=0.10.2; python_version < '3.11'", "coloredlogs~=15.0.1", + "urllib3~=2.2.2", # pip is used by bork.builder._prepare_zipapp(), # meaning it is in fact a proper dependency. "pip", From 4a70e0295818ec005d23ead1974226f3c75dab9b Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:35:30 -0400 Subject: [PATCH 11/29] clean up CLI output a bit --- bork/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bork/cli.py b/bork/cli.py index e613d22..47476ca 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -221,7 +221,8 @@ def main(cmd_args=None): coloredlogs.install( level=log_level, - fmt='%(name)s %(levelname)s %(message)s', + # %(name)15s means the name field is padded to at least 15 characters. + fmt='%(name)15s %(levelname)s %(message)s', ) except ModuleNotFoundError: From 0b9589ffd924396c84ad41acc8adfde478600b77 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:38:34 -0400 Subject: [PATCH 12/29] Bail immediately when not 'bork release' is not configured to publish the release anywhere. --- bork/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bork/api.py b/bork/api.py index 2262a28..d7930fa 100644 --- a/bork/api.py +++ b/bork/api.py @@ -156,7 +156,7 @@ def release(repository_name, dry_run, github_release_override=None, pypi_release release_to_pypi = pypi_release_override if not release_to_github and not release_to_pypi: - print('Configured to release to neither PyPi nor GitHub?') + raise RuntimeError('Configured to release to neither PyPi nor GitHub?') if release_to_github: github_repository = release_config.get('github_repository', None) From 1abbb1d85add8acb9b8244197c5c0388df38cc3f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:46:52 -0400 Subject: [PATCH 13/29] Cleaned up output from bork.pypi.upload(). --- bork/http.py | 5 ----- bork/pypi.py | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bork/http.py b/bork/http.py index e21d73b..b1b709b 100644 --- a/bork/http.py +++ b/bork/http.py @@ -22,11 +22,6 @@ def _request(url, fields, headers, method, auth): log.debug("%s %s returned %i", method, response.geturl(), response.status) - if response.status == 200: - log.info("Upload successful!") - else: - log.info(response.data.decode().strip()) - return response def get(url, headers={}, auth=None): diff --git a/bork/pypi.py b/bork/pypi.py index 885c1d9..f69629e 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -134,21 +134,31 @@ def _upload_file(self, url, file, metadata): # Remaining "core metadata" fields. *other_fields ] - result = post(url, form, auth=(self.username, self.password)) + response = post(url, form, auth=(self.username, self.password)) + return response def upload(self, dry_run=True): + log = logger() + msg_prefix = "Uploading" if dry_run: - logger().warn("Skipping PyPi release since this is a dry run.") msg_prefix = "Pretending to upload" metadata = builder.metadata().json - logger().info("%s %i files to PyPi repository '%s':", msg_prefix, len(self.files), self.repository) + log.info("%s %i files to PyPi repository '%s'.", msg_prefix, len(self.files), self.repository) for file in self.files: - logger().info("- %s %s", file, "(skipping for dry run)" if dry_run else "") - if not dry_run: - self._upload_file(self.repository, file, metadata) + filename = Path(file).name + if dry_run: + log.info("SUCCESS - Pretended to upload %s!", file) + continue + + response = self._upload_file(self.repository, file, metadata) + if response.status == 200: + log.info("SUCCESS - %s uploaded to %s", filename, self.repository) + else: + log.info("FAILED - %s couldn't be uploaded to %s", filename, self.repository) + log.info(response.data.decode().strip()) def upload(repository_name, *globs, dry_run=False): From 21494354914a0a7a3d78c593a7bb42ca401a4f24 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 22:47:53 -0400 Subject: [PATCH 14/29] Revert "clean up CLI output a bit" This reverts commit 4a70e0295818ec005d23ead1974226f3c75dab9b. --- bork/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bork/cli.py b/bork/cli.py index 47476ca..e613d22 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -221,8 +221,7 @@ def main(cmd_args=None): coloredlogs.install( level=log_level, - # %(name)15s means the name field is padded to at least 15 characters. - fmt='%(name)15s %(levelname)s %(message)s', + fmt='%(name)s %(levelname)s %(message)s', ) except ModuleNotFoundError: From 320090513da2b678aeba669f91d7efcc665535dc Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:01:07 -0400 Subject: [PATCH 15/29] Throw error if username + password are not specified. --- bork/pypi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bork/pypi.py b/bork/pypi.py index f69629e..e6cff15 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -82,6 +82,10 @@ def __init__(self, files, repository=None): self.username = os.environ.get("BORK_PYPI_USERNAME", None) self.password = os.environ.get("BORK_PYPI_PASSWORD", None) + if self.username is None and self.password is None: + raise RuntimeError("BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables aren't defined.\n\n" + "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and TWINE_PASSWORD. You can use the same values.") + def _upload_file(self, url, file, metadata): file_contents = Path(file).read_bytes() file_digest = hashlib.sha256(file_contents).hexdigest() From 50f74ec546b43bf066d48901b68f32226da47b1c Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:12:49 -0400 Subject: [PATCH 16/29] Update comments to stop referencing setup.cfg. --- bork/builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bork/builder.py b/bork/builder.py index 991affe..de274f6 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -18,7 +18,7 @@ class NeedsBuildError(Exception): # The "proper" way to handle the default would be to check python_requires -# in setup.cfg. But, since Bork needs Python 3, there's no point. +# in pyproject.toml. But, since Bork needs Python 3, there's no point. DEFAULT_PYTHON_INTERPRETER = '/usr/bin/env python3' @contextlib.contextmanager @@ -69,7 +69,7 @@ def metadata(): return importlib.metadata.Distribution.at(path).metadata def _python_interpreter(config): - # To override the default interpreter, add this to your project's setup.cfg: + # To override the default interpreter, add this to your project's pyproject.toml: # # [bork] # python_interpreter = /path/to/python From bf931145d33a4a332cf4b4846a9ff71c6ac950cf Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:15:46 -0400 Subject: [PATCH 17/29] Remove now-unnecessary try/catch bork.api.build(). --- bork/api.py | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/bork/api.py b/bork/api.py index d7930fa..421648e 100644 --- a/bork/api.py +++ b/bork/api.py @@ -28,33 +28,8 @@ def aliases(): def build(): """Build the project.""" - try: - builder.dist() - builder.zipapp() - - except FileNotFoundError as e: - if e.filename != 'pyproject.toml': - raise e - - def setup(ext): - return Path.cwd() / f"setup.{ext}" - - if setup("cfg").exists() or setup("py").exists(): - msg = """If you use setuptools, the following should be sufficient: - - [build-system] - requires = ["setuptools > 42", "wheel"] - build-backend = "setuptools.build_meta" """ - - else: - msg = "Please refer to your build system's documentation." - - logger().error( - "You need a 'pyproject.toml' file describing which buildsystem to " - "use, per PEP 517. %s", msg - ) - - raise e + builder.dist() + builder.zipapp() def clean(): From 228cf29b3d317f4d4d608f75cc2a121d9a6fad3d Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:21:24 -0400 Subject: [PATCH 18/29] Refactor bork.http. --- bork/http.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bork/http.py b/bork/http.py index b1b709b..0310aa5 100644 --- a/bork/http.py +++ b/bork/http.py @@ -6,7 +6,7 @@ MAX_RETRIES = False -def _request(url, fields, headers, method, auth): +def request(method, url, fields, auth): log = logger() user_agent = f"bork/{version.__version__} (+https://github.com/duckinator/bork)" @@ -24,12 +24,8 @@ def _request(url, fields, headers, method, auth): return response -def get(url, headers={}, auth=None): - return _request(url, None, headers, "GET", auth) +def get(url, auth=None): + return request("GET", url, None, auth) -def post(url, fields=None, headers=None, auth=None): - if headers is None: - headers = {} - - headers["Content-Type"] = "application/x-www-form-urlencoded" - return _request(url, fields, headers, "POST", auth) +def post(url, fields=None, auth=None): + return request("POST", url, fields, auth) From 8d9907cf5fc4b19279985f5ab7cc21ad58e2dd8c Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:25:12 -0400 Subject: [PATCH 19/29] Move error checking, to avoid affecting --dry-run. --- bork/pypi.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bork/pypi.py b/bork/pypi.py index e6cff15..e02590b 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -82,10 +82,6 @@ def __init__(self, files, repository=None): self.username = os.environ.get("BORK_PYPI_USERNAME", None) self.password = os.environ.get("BORK_PYPI_PASSWORD", None) - if self.username is None and self.password is None: - raise RuntimeError("BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables aren't defined.\n\n" - "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and TWINE_PASSWORD. You can use the same values.") - def _upload_file(self, url, file, metadata): file_contents = Path(file).read_bytes() file_digest = hashlib.sha256(file_contents).hexdigest() @@ -138,6 +134,11 @@ def _upload_file(self, url, file, metadata): # Remaining "core metadata" fields. *other_fields ] + + if self.username is None and self.password is None: + raise RuntimeError("BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables aren't defined.\n\n" + "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and TWINE_PASSWORD. You can use the same values.") + response = post(url, form, auth=(self.username, self.password)) return response From 44dcb6f290947a4873fb18c951db307b0e6f571e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:33:18 -0400 Subject: [PATCH 20/29] Use importlib_metadata for Python versions before 3.10. --- bork/builder.py | 9 +++++++-- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bork/builder.py b/bork/builder.py index de274f6..2b2a00d 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -1,6 +1,5 @@ import configparser import contextlib -import importlib from pathlib import Path import subprocess import sys @@ -8,6 +7,12 @@ # Slight kludge so we can have a function named zipapp(). import zipapp as Zipapp # noqa: N812 +# When Python 3.8 and 3.9 support is dropped, drop the importlib_metadata backport. +if sys.version_info[:2] >= (3, 10): + from importlib import metadata as importlib_metadata +else: + import importlib_metadata + import build from .filesystem import load_pyproject, try_delete @@ -66,7 +71,7 @@ def metadata(): with prepared_environment(srcdir, outdir) as (env, builder): with tempfile.TemporaryDirectory() as tmpdir: path = Path(builder.metadata_path(tmpdir)) - return importlib.metadata.Distribution.at(path).metadata + return importlib_metadata.Distribution.at(path).metadata def _python_interpreter(config): # To override the default interpreter, add this to your project's pyproject.toml: diff --git a/pyproject.toml b/pyproject.toml index 7e68dd9..ebc444e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "toml~=0.10.2; python_version < '3.11'", "coloredlogs~=15.0.1", "urllib3~=2.2.2", + "importlib_metadata; python_version < '3.10'", # pip is used by bork.builder._prepare_zipapp(), # meaning it is in fact a proper dependency. "pip", From 8d2d412e9e5a2b2e810216301e90760ef1e4dc92 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:38:11 -0400 Subject: [PATCH 21/29] Fix linting errors. --- bork/pypi.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bork/pypi.py b/bork/pypi.py index e02590b..e2f7af6 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -57,6 +57,8 @@ class Uploader: TESTPYPI_ENDPOINT = "https://test.pypi.org/legacy/" def __init__(self, files, repository=None): + log = logger() + if repository == "pypi" or repository is None: repository = self.PYPI_ENDPOINT elif repository == "testpypi": @@ -67,13 +69,13 @@ def __init__(self, files, repository=None): elif repository.startswith("https://"): pass # Everything is fine. else: - logger().error("Only the 'pypi' and 'testpypi' repository shorthands are supported.") - logger().error("Please provide a full URL for custom endpoints, such as .") - logger().error("Please open an issue at https://github.com/duckinator/bork if you need help.") + log.error("Only the 'pypi' and 'testpypi' repository shorthands are supported.") + log.error("For custom endpoints, provide a full URL.") + log.error("Open an issue at https://github.com/duckinator/bork if you need help.") exit(1) if not files: - logger().error("No files to upload?") + log.error("No files to upload?") exit(1) self.files = files @@ -136,8 +138,10 @@ def _upload_file(self, url, file, metadata): ] if self.username is None and self.password is None: - raise RuntimeError("BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables aren't defined.\n\n" - "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and TWINE_PASSWORD. You can use the same values.") + raise RuntimeError( + "BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables are undefined.\n\n" + "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and " + "TWINE_PASSWORD. You can use the same values.") response = post(url, form, auth=(self.username, self.password)) return response @@ -151,7 +155,8 @@ def upload(self, dry_run=True): metadata = builder.metadata().json - log.info("%s %i files to PyPi repository '%s'.", msg_prefix, len(self.files), self.repository) + log.info("%s %i files to PyPi repository '%s'.", msg_prefix, len(self.files), + self.repository) for file in self.files: filename = Path(file).name if dry_run: From e30cef5313043318c2f912e4d2f66950842feecb Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:39:04 -0400 Subject: [PATCH 22/29] Remove unused import. --- bork/builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bork/builder.py b/bork/builder.py index 2b2a00d..1412e43 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -1,4 +1,3 @@ -import configparser import contextlib from pathlib import Path import subprocess From ffd371384f77f39563d36dcddf195983e2299bd9 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:39:40 -0400 Subject: [PATCH 23/29] Clean up bork.builder. --- bork/builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bork/builder.py b/bork/builder.py index 1412e43..d5ca39a 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -1,3 +1,5 @@ +from .filesystem import load_pyproject, try_delete +import build import contextlib from pathlib import Path import subprocess @@ -12,11 +14,6 @@ else: import importlib_metadata -import build - -from .filesystem import load_pyproject, try_delete -# from .log import logger - class NeedsBuildError(Exception): pass From c30667d4cdf99ff37d89173accb789e963c128e2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:43:05 -0400 Subject: [PATCH 24/29] Switch from pylint to ruff; re-enable mypy. --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ebc444e..3e9cffa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ documentation = "https://bork.readthedocs.io/en/latest/" [project.optional-dependencies] lint = [ - "pylint==3.0.1", + "ruff", "mypy==1.5.1", ] @@ -81,9 +81,8 @@ strip_zipapp_version = true [tool.bork.aliases] lint = [ - "pylint bork tests", -# TODO: Re-enable mypy after we move away from Click. -# "mypy bork", + "ruff check", + "mypy bork", ] # Runs all tests. test = "pytest --junitxml=bork-junit.xml --verbose" From 3ac998b5fc994dd46eaf0805b90bdbf3e8059ec2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:44:59 -0400 Subject: [PATCH 25/29] Remove leftovers from pylint -> ruff migration. --- .cirrus.yml | 2 +- .pylintrc | 33 --------------------------------- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 .pylintrc diff --git a/.cirrus.yml b/.cirrus.yml index 25fa8c4..e5fc97a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,7 +6,7 @@ Lint_task: - pip install . - pip install .[lint] .[test] script: - - pylint --version + - ruff --version - mypy --version - bork run lint diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index bc696b1..0000000 --- a/.pylintrc +++ /dev/null @@ -1,33 +0,0 @@ -[MASTER] -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins=pylint.extensions.check_elif, - pylint.extensions.overlapping_exceptions, - pylint.extensions.redefined_variable_type, - -[MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -enable=all - -# Disable the message, report, category or checker with the given id(s). -disable=missing-docstring, - compare-to-zero - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=colorized - -[TYPECHECK] -# Workaround for https://github.com/PyCQA/pylint/issues/2804 -ignored-modules=signal From 1d56c9c30a3a851046139466b726ea3b95918bed Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 31 Jul 2024 23:50:25 -0400 Subject: [PATCH 26/29] Remove unnececssary zipapp_main(). --- bork/cli.py | 11 ----------- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/bork/cli.py b/bork/cli.py index e613d22..015efc0 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -239,14 +239,3 @@ def main(cmd_args=None): (log.exception if args.verbose else log.error)(str(err)) sys.exit(1) - - -def zipapp_main(): - # If Bork is put in a zipapp, this allows scripts executed as subprocesses - # to access pep517.compat. - # - # The problem area is the `import compat` line in pep517's _in_process.py. - # https://github.com/pypa/pep517/blob/master/pep517/_in_process.py - os.environ['PYTHONPATH'] = ':'.join([*sys.path, sys.argv[0] + '/pep517']) - - main() diff --git a/pyproject.toml b/pyproject.toml index 3e9cffa..ac1e6b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ version = {attr = "bork.__version__"} [tool.bork.zipapp] enabled = true -main = "bork.cli:zipapp_main" +main = "bork.cli:main" [tool.bork.release] pypi = true From 2216abeb3cd36dfa4559119a79e7a4b10a925f01 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 1 Aug 2024 00:02:39 -0400 Subject: [PATCH 27/29] Add --zipapp/--no-zipapp. --- bork/api.py | 5 +++-- bork/builder.py | 10 ++++++---- bork/cli.py | 19 +++++++++++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/bork/api.py b/bork/api.py index 421648e..c1a175a 100644 --- a/bork/api.py +++ b/bork/api.py @@ -26,10 +26,11 @@ def aliases(): return pyproject.get('tool', {}).get('bork', {}).get('aliases', {}) -def build(): +def build(build_zipapp=False): """Build the project.""" builder.dist() - builder.zipapp() + if build_zipapp: + builder.zipapp() def clean(): diff --git a/bork/builder.py b/bork/builder.py index d5ca39a..f8dc613 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -1,4 +1,5 @@ from .filesystem import load_pyproject, try_delete +from .log import logger import build import contextlib from pathlib import Path @@ -104,13 +105,13 @@ def zipapp(): dist() should be called before zipapp(). """ + log = logger() + + log.info("Building ZipApp.") + pyproject = load_pyproject() config = pyproject.get('tool', {}).get('bork', {}) zipapp_cfg = config.get('zipapp', {}) - want_zipapp = zipapp_cfg.get('enabled', False) - - if not want_zipapp: - return name = metadata()['name'] dest = str(Path('build', 'zipapp')) @@ -126,3 +127,4 @@ def zipapp(): compressed=True) if not Path(target).exists(): raise RuntimeError(f"Failed to build zipapp: {target}") + log.info("Finished building ZipApp.") diff --git a/bork/cli.py b/bork/cli.py index 015efc0..7f03346 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -22,6 +22,7 @@ from . import __version__ from . import api +from .filesystem import load_pyproject from .log import logger @@ -43,13 +44,22 @@ def aliases(_args): print(key) -def build(_args): +def build(args): """ ### `bork build` Build the project. """ - api.build() + + pyproject = load_pyproject() + config = pyproject.get('tool', {}).get('bork', {}) + zipapp_cfg = config.get('zipapp', {}) + want_zipapp = zipapp_cfg.get('enabled', False) + + if args.zipapp is not None: + want_zipapp = args.zipapp + + api.build(want_zipapp) def clean(_args): @@ -137,6 +147,11 @@ def _arg_parser(): buildp = subparsers.add_parser("build", help="Build the project.") buildp.set_defaults(func=build) + buildp.add_argument("--zipapp", action="store_true", + help="Always build a zipapp.") + buildp.add_argument("--no-zipapp", dest="zipapp", action="store_false", + help="Never build to zipapp.") + buildp.set_defaults(zipapp=None) cleanp = subparsers.add_parser("clean", help="Remove files generated by `bork build`.") cleanp.set_defaults(func=clean) From 52df9afc7624ccd0e2d0f73b27bd9c2fca4fad49 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 1 Aug 2024 00:26:43 -0400 Subject: [PATCH 28/29] Add --zipapp-main. --- bork/api.py | 8 +++++--- bork/builder.py | 4 ++-- bork/cli.py | 11 +++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bork/api.py b/bork/api.py index c1a175a..0b5c9a3 100644 --- a/bork/api.py +++ b/bork/api.py @@ -26,11 +26,13 @@ def aliases(): return pyproject.get('tool', {}).get('bork', {}).get('aliases', {}) -def build(build_zipapp=False): +def build(): """Build the project.""" builder.dist() - if build_zipapp: - builder.zipapp() + +def build_zipapp(zipapp_main=None): + """Build the project as a ZipApp.""" + builder.zipapp(zipapp_main) def clean(): diff --git a/bork/builder.py b/bork/builder.py index f8dc613..23f4d75 100644 --- a/bork/builder.py +++ b/bork/builder.py @@ -98,7 +98,7 @@ def version_from_bdist_file(): return _bdist_file().name.replace('.tar.gz', '').split('-')[1] -def zipapp(): +def zipapp(zipapp_main): """ Build a zipapp for the project. @@ -116,7 +116,7 @@ def zipapp(): name = metadata()['name'] dest = str(Path('build', 'zipapp')) version = version_from_bdist_file() - main = zipapp_cfg['main'] + main = zipapp_main or zipapp_cfg['main'] # Output file is dist/-.pyz. target = f"dist/{name}-{version}.pyz" diff --git a/bork/cli.py b/bork/cli.py index 7f03346..1a6d902 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -54,12 +54,12 @@ def build(args): pyproject = load_pyproject() config = pyproject.get('tool', {}).get('bork', {}) zipapp_cfg = config.get('zipapp', {}) - want_zipapp = zipapp_cfg.get('enabled', False) + zipapp_enabled = zipapp_cfg.get('enabled', False) - if args.zipapp is not None: - want_zipapp = args.zipapp + api.build() - api.build(want_zipapp) + if args.zipapp or zipapp_enabled: + api.build_zipapp(args.zipapp_main) def clean(_args): @@ -152,6 +152,9 @@ def _arg_parser(): buildp.add_argument("--no-zipapp", dest="zipapp", action="store_false", help="Never build to zipapp.") buildp.set_defaults(zipapp=None) + buildp.add_argument("--zipapp-main", action="store", + help="Entrypoint for the ZipApp. Format is: module.submodule:function") + buildp.set_defaults(zipapp_main=None) cleanp = subparsers.add_parser("clean", help="Remove files generated by `bork build`.") cleanp.set_defaults(func=clean) From 393a0cb490777e423c6148b478a911abf178501b Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 1 Aug 2024 00:28:21 -0400 Subject: [PATCH 29/29] Remove unused import. --- bork/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bork/cli.py b/bork/cli.py index 1a6d902..ea5a1d4 100644 --- a/bork/cli.py +++ b/bork/cli.py @@ -17,7 +17,6 @@ import argparse import inspect import logging -import os import sys from . import __version__