From 0efb1ba2833c5e332a95598a1b7fab6a354729ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 23 Dec 2024 16:19:22 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delin?= =?UTF-8?q?t).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 14 ++--- __main__.py | 4 +- backend.py | 52 ++++++++-------- bootstrap.py | 4 +- discovery.py | 144 ++++++++++++++++++++++---------------------- metadata.py | 50 +++++++-------- tests/test_hooks.py | 6 +- 7 files changed, 138 insertions(+), 136 deletions(-) diff --git a/__init__.py b/__init__.py index 9ecce8a..8af6c99 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ __requires__ = [ - 'build', - 'git-fame', + "build", + "git-fame", 'importlib_resources; python_version < "3.12"', ] @@ -18,9 +18,9 @@ __all__ = [ - 'build_sdist', - 'prepare_metadata_for_build_wheel', - 'prepare_metadata_for_build_editable', - 'build_wheel', - 'build_editable', + "build_sdist", + "prepare_metadata_for_build_wheel", + "prepare_metadata_for_build_editable", + "build_wheel", + "build_editable", ] diff --git a/__main__.py b/__main__.py index 2b2a790..6567c2a 100644 --- a/__main__.py +++ b/__main__.py @@ -7,7 +7,7 @@ def run(): logging.basicConfig() with bootstrap.write_pyproject(): - runpy.run_module('build', run_name='__main__') + runpy.run_module("build", run_name="__main__") -__name__ == '__main__' and run() +__name__ == "__main__" and run() diff --git a/backend.py b/backend.py index 17ef5ae..3c31ae5 100644 --- a/backend.py +++ b/backend.py @@ -40,13 +40,13 @@ def __call__(self, info): be omitted. Otherwise, mutate the object to include self.name as a prefix. """ - if info.name == '.': + if info.name == ".": info.name = self.name return info - ignore_pattern = '|'.join(self.ignored) - if re.match(ignore_pattern, r_fix(info.name).removeprefix('./')): + ignore_pattern = "|".join(self.ignored) + if re.match(ignore_pattern, r_fix(info.name).removeprefix("./")): return - info.name = self.name + '/' + r_fix(info.name).removeprefix('./') + info.name = self.name + "/" + r_fix(info.name).removeprefix("./") return info @@ -68,17 +68,17 @@ class SDist(Filter): namespace(name='foo/bar/dist') """ - ignored = ['dist', r'(.*[/])?__pycache__$', r'(.*[/])?[.]'] + ignored = ["dist", r"(.*[/])?__pycache__$", r"(.*[/])?[.]"] class Wheel(Filter): ignored = [ - 'docs', - 'tests', - r'README.*', - 'PKG-INFO', - re.escape('(meta)'), - re.escape('pyproject.toml'), + "docs", + "tests", + r"README.*", + "PKG-INFO", + re.escape("(meta)"), + re.escape("pyproject.toml"), ] @@ -96,7 +96,7 @@ def wheel_walk(filter_: Wheel) -> Iterator[ZipInfo]: """ Walk the current directory, applying and honoring the filter for traversal. """ - for root, dirs, files in os.walk('.'): + for root, dirs, files in os.walk("."): zi = ZipInfo(path=root) if not filter_(zi): dirs[:] = [] @@ -107,8 +107,8 @@ def wheel_walk(filter_: Wheel) -> Iterator[ZipInfo]: def make_sdist_metadata(metadata: Message) -> tarfile.TarInfo: - info = tarfile.TarInfo(f'{metadata.id}/PKG-INFO') - file = io.BytesIO(metadata.render().encode('utf-8')) + info = tarfile.TarInfo(f"{metadata.id}/PKG-INFO") + file = io.BytesIO(metadata.render().encode("utf-8")) info.size = len(file.getbuffer()) info.mtime = time.time() return info, file @@ -117,7 +117,7 @@ def make_sdist_metadata(metadata: Message) -> tarfile.TarInfo: def prepare_metadata(metadata_directory, config_settings=None): metadata = Message.load() or Message.discover() - md_root = pathlib.Path(metadata_directory, f'{metadata.id}.dist-info') + md_root = pathlib.Path(metadata_directory, f"{metadata.id}.dist-info") md_root.mkdir() for name, contents in metadata.render_wheel(): md_root.joinpath(name).write_text(contents) @@ -130,20 +130,20 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): or Message.load() or Message.discover() ) - root = metadata['Name'].replace('.', '/') - filename = pathlib.Path(wheel_directory) / f'{metadata.id}-py3-none-any.whl' - with WheelFile(filename, 'w') as zf: + root = metadata["Name"].replace(".", "/") + filename = pathlib.Path(wheel_directory) / f"{metadata.id}-py3-none-any.whl" + with WheelFile(filename, "w") as zf: for info in wheel_walk(Wheel(root)): zf.write(info.path, arcname=info.name) for name, contents in metadata.render_wheel(): - zf.writestr(f'{metadata.id}.dist-info/{name}', contents) + zf.writestr(f"{metadata.id}.dist-info/{name}", contents) return str(filename) def build_sdist(sdist_directory, config_settings=None): metadata = Message.discover() - filename = pathlib.Path(sdist_directory) / f'{metadata.id}.tar.gz' - with tarfile.open(filename, 'w:gz') as tf: + filename = pathlib.Path(sdist_directory) / f"{metadata.id}.tar.gz" + with tarfile.open(filename, "w:gz") as tf: tf.add(pathlib.Path(), filter=SDist(metadata.id)) tf.addfile(*make_sdist_metadata(metadata)) return str(filename) @@ -155,12 +155,12 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non or Message.load() or Message.discover() ) - root = metadata['Name'].replace('.', '/') - filename = pathlib.Path(wheel_directory) / f'{metadata.id}-py3-none-any.whl' - with WheelFile(filename, 'w') as zf: - zf.writestr(f'{root}/__init__.py', proxy()) + root = metadata["Name"].replace(".", "/") + filename = pathlib.Path(wheel_directory) / f"{metadata.id}-py3-none-any.whl" + with WheelFile(filename, "w") as zf: + zf.writestr(f"{root}/__init__.py", proxy()) for name, contents in metadata.render_wheel(): - zf.writestr(f'{metadata.id}.dist-info/{name}', contents) + zf.writestr(f"{metadata.id}.dist-info/{name}", contents) return str(filename) diff --git a/bootstrap.py b/bootstrap.py index 88ae6ab..60ceef1 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -7,8 +7,8 @@ def write_pyproject(target: pathlib.Path = pathlib.Path()) -> ContextManager[None]: return assured( - target / 'pyproject.toml', - importlib.resources.files().joinpath('system.toml').read_text, + target / "pyproject.toml", + importlib.resources.files().joinpath("system.toml").read_text, ) diff --git a/discovery.py b/discovery.py index 09d71b6..fbfafa7 100644 --- a/discovery.py +++ b/discovery.py @@ -31,17 +31,17 @@ log = logging.getLogger(__name__) -mimetypes.add_type('text/plain', '', strict=True) -mimetypes.add_type('text/markdown', '.md', strict=True) -mimetypes.add_type('text/x-rst', '.rst', strict=True) +mimetypes.add_type("text/plain", "", strict=True) +mimetypes.add_type("text/markdown", ".md", strict=True) +mimetypes.add_type("text/x-rst", ".rst", strict=True) @suppress(subprocess.CalledProcessError) def origin() -> str: return subprocess.check_output( - ['git', 'remote', 'get-url', 'origin'], + ["git", "remote", "get-url", "origin"], text=True, - encoding='utf-8', + encoding="utf-8", ).strip() @@ -63,14 +63,14 @@ def owner_from_vcs() -> str | None: @jaraco.functools.pass_none def name_from_origin(origin: str | None) -> str: - _, _, tail = origin.rpartition('/') - return r_fix(tail).removesuffix('.git') + _, _, tail = origin.rpartition("/") + return r_fix(tail).removesuffix(".git") @jaraco.functools.pass_none def owner_from_origin(origin: str | None) -> str: - head, _, _ = origin.rpartition('/') - _, _, owner = head.rpartition('/') + head, _, _ = origin.rpartition("/") + _, _, owner = head.rpartition("/") return owner @@ -79,7 +79,7 @@ def name_from_path(): >>> name_from_vcs() 'coherent.build' """ - return pathlib.Path('.').absolute().name + return pathlib.Path(".").absolute().name def best_name(): @@ -108,9 +108,9 @@ def repo_info() -> Mapping: def github_repo_info() -> Mapping: data = json.loads( subprocess.check_output( - ['gh', 'repo', 'view', '--json', 'description,url'], + ["gh", "repo", "view", "--json", "description,url"], text=True, - encoding='utf-8', + encoding="utf-8", ) ) return {k: v for k, v in data.items() if v} @@ -118,10 +118,10 @@ def github_repo_info() -> Mapping: @suppress(gitlab.exceptions.GitlabError) def gitlab_repo_info() -> Mapping: - api = gitlab.Gitlab('https://gitlab.com') + api = gitlab.Gitlab("https://gitlab.com") name = name_from_vcs() owner = owner_from_vcs() - project = api.projects.get(f'{owner}/{name}') + project = api.projects.get(f"{owner}/{name}") result = dict(url=project.web_url) if project.description: result.update(description=project.description) @@ -135,7 +135,7 @@ def summary(): >>> summary() 'A zero-config Python project build backend' """ - return repo_info().get('description') + return repo_info().get("description") def source_url(): @@ -145,7 +145,7 @@ def source_url(): >>> source_url() 'https://github.com/coherent-oss/coherent.build' """ - return repo_info().get('url') + return repo_info().get("url") def python_requires_supported(): @@ -153,9 +153,9 @@ def python_requires_supported(): >>> python_requires_supported() '>= 3...' """ - owner = 'python' - repo = 'cpython' - url = f'https://api.github.com/repos/{owner}/{repo}/branches' + owner = "python" + repo = "cpython" + url = f"https://api.github.com/repos/{owner}/{repo}/branches" branches = requests.get(url).json() # cheat and grab the first branch, which is the oldest supported Python version try: @@ -163,14 +163,14 @@ def python_requires_supported(): except KeyError: log.warning(f"Unexpected {branches=}") min_ver = "3.8" - return f'>= {min_ver}' + return f">= {min_ver}" def declared_deps(): """ Read deps from ``__init__.py``. """ - return scripts.DepsReader.search(['__init__.py']) + return scripts.DepsReader.search(["__init__.py"]) def source_files(): @@ -182,12 +182,12 @@ def source_files(): """ return ( pathlib.Path(path) - for path in subprocess.check_output(['git', 'ls-files'], text=True).splitlines() + for path in subprocess.check_output(["git", "ls-files"], text=True).splitlines() ) def is_python(path: pathlib.Path) -> bool: - return path.suffix == '.py' + return path.suffix == ".py" def base(module): @@ -197,7 +197,7 @@ def base(module): >>> base(pathlib.Path('foo.py')) 'coherent.build' """ - return '.'.join((best_name(),) + module.parent.parts) + return ".".join((best_name(),) + module.parent.parts) def is_local(import_): @@ -221,7 +221,7 @@ def inferred_deps(): def combined_deps(): def normalize(name): - return re.sub(r'[.-_]', '-', name).lower() + return re.sub(r"[.-_]", "-", name).lower() def package_name(dep): return normalize(packaging.requirements.Requirement(dep).name) @@ -245,11 +245,11 @@ def extra_for(module: pathlib.Path) -> str: >>> extra_for(pathlib.Path('docs/conf.py')) '; extra=="doc"' """ - mapping = dict(tests='test', docs='doc') + mapping = dict(tests="test", docs="doc") try: return f'; extra=="{mapping[str(list(module.parents)[-2])]}"' except (KeyError, IndexError): - return '' + return "" def extras_from_dep(dep): @@ -260,7 +260,7 @@ def extras_from_dep(dep): return set( marker[2].value for marker in markers - if isinstance(marker, tuple) and marker[0].value == 'extra' + if isinstance(marker, tuple) and marker[0].value == "extra" ) @@ -286,13 +286,13 @@ def full_extras(deps): Ref coherent-oss/coherent.test#5. """ - deps.add('test') - deps.add('doc') + deps.add("test") + deps.add("doc") return deps def _to_mapping(fame): - return (dict(zip(fame['columns'], row)) for row in fame['data']) + return (dict(zip(fame["columns"], row)) for row in fame["data"]) class Contributor(types.SimpleNamespace): @@ -304,25 +304,25 @@ def combined_detail(self): @suppress(Exception) def author_from_vcs(): # run git-fame twice to get both name and email - cmd = ['git-fame', '--format', 'json'] + cmd = ["git-fame", "--format", "json"] names_data = json.loads( subprocess.check_output( cmd, text=True, - encoding='utf-8', + encoding="utf-8", stderr=subprocess.DEVNULL, ) ) emails_data = json.loads( subprocess.check_output( - cmd + ['--show-email'], + cmd + ["--show-email"], text=True, - encoding='utf-8', + encoding="utf-8", stderr=subprocess.DEVNULL, ) ) - names_data['columns'][0] = 'name' - emails_data['columns'][0] = 'email' + names_data["columns"][0] = "name" + emails_data["columns"][0] = "email" emails_contribs = _to_mapping(emails_data) names_contribs = _to_mapping(names_data) @@ -350,17 +350,17 @@ def guess_content_type(path: pathlib.Path): def join(*strings): - return ''.join(strings) + return "".join(strings) def inject_badges(readme, type): """ Put badges at the top of the readme. """ - return '\n\n'.join(itertools.chain(render_badges(type), [readme])) + return "\n\n".join(itertools.chain(render_badges(type), [readme])) -def render_badge(type, *, image, target=None, alt_text=''): +def render_badge(type, *, image, target=None, alt_text=""): """ >>> print(render_badge('markdown', image='file://foo.img', alt_text='foo')) ![foo](file://foo.img) @@ -369,59 +369,59 @@ def render_badge(type, *, image, target=None, alt_text=''): :alt: foo """ markdown = join( - '[' * bool(target), - '![{alt_text}]({image})', - ']({target})' * bool(target), + "[" * bool(target), + "![{alt_text}]({image})", + "]({target})" * bool(target), ) rst = join( - '.. image:: {image}', - '\n :target: {target}' * bool(target), - '\n :alt: {alt_text}' * bool(alt_text), + ".. image:: {image}", + "\n :target: {target}" * bool(target), + "\n :alt: {alt_text}" * bool(alt_text), ) return locals().get(type, markdown).format_map(locals()) def render_badges(type): - _, _, subtype = type.partition('/') - rb = functools.partial(render_badge, subtype.replace('x-', '')) + _, _, subtype = type.partition("/") + rb = functools.partial(render_badge, subtype.replace("x-", "")) PROJECT = best_name() URL = source_url() yield rb( - image=f'https://img.shields.io/pypi/v/{PROJECT}', - target=f'https://pypi.org/project/{PROJECT}', + image=f"https://img.shields.io/pypi/v/{PROJECT}", + target=f"https://pypi.org/project/{PROJECT}", ) - yield rb(image=f'https://img.shields.io/pypi/pyversions/{PROJECT}') + yield rb(image=f"https://img.shields.io/pypi/pyversions/{PROJECT}") yield rb( - image=f'{URL}/actions/workflows/main.yml/badge.svg', - target=f'{URL}/actions?query=workflow%3A%22tests%22', + image=f"{URL}/actions/workflows/main.yml/badge.svg", + target=f"{URL}/actions?query=workflow%3A%22tests%22", ) yield rb( image=( - 'https://img.shields.io/endpoint?' - 'url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json' + "https://img.shields.io/endpoint?" + "url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" ), - target='https://github.com/astral-sh/ruff', - alt_text='Ruff', + target="https://github.com/astral-sh/ruff", + alt_text="Ruff", ) - system = urllib.parse.quote('coherent system') + system = urllib.parse.quote("coherent system") yield rb( - image=f'https://img.shields.io/badge/{system}-informational', - target='https://github.com/coherent-oss/system', - alt_text='Coherent Software Development System', + image=f"https://img.shields.io/badge/{system}-informational", + target="https://github.com/coherent-oss/system", + alt_text="Coherent Software Development System", ) def description_from_readme(): with contextlib.suppress(ValueError, AssertionError): - (readme,) = pathlib.Path().glob('README*') + (readme,) = pathlib.Path().glob("README*") ct = guess_content_type(readme) assert ct - yield 'Description-Content-Type', ct - yield 'Description', inject_badges(readme.read_text(encoding='utf-8'), ct) + yield "Description-Content-Type", ct + yield "Description", inject_badges(readme.read_text(encoding="utf-8"), ct) def age_of_repo(): @@ -431,13 +431,13 @@ def age_of_repo(): def generate_classifiers(): yield ( - 'Development Status :: 4 - Beta' - if Version(version_from_vcs()) < Version('1.0') - else 'Development Status :: 5 - Production/Stable' + "Development Status :: 4 - Beta" + if Version(version_from_vcs()) < Version("1.0") + else "Development Status :: 5 - Production/Stable" if age_of_repo() < datetime.timedelta(days=365) - else 'Development Status :: 6 - Mature' + else "Development Status :: 6 - Mature" ) - yield 'Intended Audience :: Developers' - yield 'License :: OSI Approved :: MIT License' - yield 'Programming Language :: Python :: 3' - yield 'Programming Language :: Python :: 3 :: Only' + yield "Intended Audience :: Developers" + yield "License :: OSI Approved :: MIT License" + yield "Programming Language :: Python :: 3" + yield "Programming Language :: Python :: 3 :: Only" diff --git a/metadata.py b/metadata.py index d574c3a..befb0ba 100644 --- a/metadata.py +++ b/metadata.py @@ -19,7 +19,7 @@ def _normalize(name): - return packaging.utils.canonicalize_name(name).replace('-', '_') + return packaging.utils.canonicalize_name(name).replace("-", "_") @functools.singledispatch @@ -64,9 +64,9 @@ def __init__(self, values): self.add_header(*item) def _description_in_payload(self): - if 'Description' in self: - self.set_payload(self['Description']) - del self['Description'] + if "Description" in self: + self.set_payload(self["Description"]) + del self["Description"] @property def id(self): @@ -85,21 +85,21 @@ def discover(cls): @staticmethod def _discover_fields(): - yield 'Metadata-Version', '2.3' - yield 'Name', discovery.best_name() - yield 'Version', discovery.version_from_vcs() - yield 'Author-Email', discovery.author_from_vcs() - yield 'Summary', discovery.summary() - yield 'Requires-Python', discovery.python_requires_supported() + yield "Metadata-Version", "2.3" + yield "Name", discovery.best_name() + yield "Version", discovery.version_from_vcs() + yield "Author-Email", discovery.author_from_vcs() + yield "Summary", discovery.summary() + yield "Requires-Python", discovery.python_requires_supported() deps = list(discovery.combined_deps()) for dep in deps: - yield 'Requires-Dist', dep + yield "Requires-Dist", dep for extra in discovery.full_extras(discovery.extras_from_deps(deps)): - yield 'Provides-Extra', extra - yield 'Project-URL', f'Source, {discovery.source_url()}' + yield "Provides-Extra", extra + yield "Project-URL", f"Source, {discovery.source_url()}" yield from discovery.description_from_readme() for classifier in discovery.generate_classifiers(): - yield 'Classifier', classifier + yield "Classifier", classifier @classmethod def load(cls, info: str | pathlib.Path = pathlib.Path()): @@ -114,16 +114,18 @@ def render_wheel(self): """ Yield (name, contents) pairs for all metadata files. """ - yield 'METADATA', self.render() - wheel_md = Message({ - 'Wheel-Version': '1.0', - 'Generator': 'coherent.build', - 'Root-Is-Purelib': 'true', - 'Tag': 'py3-none-any', - }) - yield 'WHEEL', wheel_md.render() + yield "METADATA", self.render() + wheel_md = Message( + { + "Wheel-Version": "1.0", + "Generator": "coherent.build", + "Root-Is-Purelib": "true", + "Tag": "py3-none-any", + } + ) + yield "WHEEL", wheel_md.render() with contextlib.suppress(FileNotFoundError): yield ( - 'entry_points.txt', - pathlib.Path('(meta)/entry_points.txt').read_text(), + "entry_points.txt", + pathlib.Path("(meta)/entry_points.txt").read_text(), ) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 41ffe36..acd916d 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -5,12 +5,12 @@ def test_prepared_metadata(tmp_path, monkeypatch): """ Ensure that prepared metadata can be used to build a wheel. """ - md_root = tmp_path / 'metadata-build' + md_root = tmp_path / "metadata-build" md_root.mkdir() md_name = coherent.build.prepare_metadata_for_build_wheel(md_root) md_dir = md_root / md_name - wheel_root = tmp_path / 'wheel-build' + wheel_root = tmp_path / "wheel-build" wheel_root.mkdir() # ensure Message.discover is not called - monkeypatch.delattr(coherent.build.metadata.Message, 'discover') + monkeypatch.delattr(coherent.build.metadata.Message, "discover") coherent.build.build_wheel(wheel_root, metadata_directory=md_dir)