Skip to content

Commit

Permalink
Merge pull request #374 from duckinator/remolisher/config
Browse files Browse the repository at this point in the history
Use a common configuration model
  • Loading branch information
duckinator authored Aug 20, 2024
2 parents 184e2c6 + 44dbfdc commit 9cb64f2
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 128 deletions.
32 changes: 1 addition & 31 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ Lint_task:
- mypy --version
- bork run lint

Zipapp_bootstrap_task:
alias: Zipapp bootstraps
Linux_task:
persistent_worker: &linuxes
isolation:
container:
Expand All @@ -27,34 +26,6 @@ Zipapp_bootstrap_task:
- image: python:3.11-slim
- image: python:3.12-slim
- image: python:3.13-rc-slim
setup_script:
- pip install -U --upgrade-strategy eager pip 'setuptools>61'
- cp -r . /tmp/bork-pristine
- cp -r /tmp/bork-pristine /tmp/pass1
- cp -r /tmp/bork-pristine /tmp/pass2
- cp -r /tmp/bork-pristine /tmp/pass3
# Make sure Bork can build itself.
pass_1_script:
- cd /tmp/pass1
- python3 --version
- pip install . .[test]
- bork build
# Make sure the Bork zipapp from Pass 1 can build Bork.
pass_2_script:
- cd /tmp/pass2
- cp /tmp/pass1/dist/bork-*.pyz /tmp/bork-pass1.pyz
- python3 /tmp/bork-pass1.pyz build
# Make sure the Bork zipapp built from Pass 2 can build Bork.
# ime with other self-building software, this is prone to blowing up.
pass_3_script:
- cd /tmp/pass3
- cp /tmp/pass2/dist/bork-*.pyz /tmp/bork-pass2.pyz
- python3 /tmp/bork-pass2.pyz build
- '[ -e ./dist/bork-*.pyz ]'


Linux_task:
persistent_worker: *linuxes
install_script:
- apt-get update
- apt-get install -y git
Expand Down Expand Up @@ -167,7 +138,6 @@ success_task:
- Linux quick
- Linux slow
- macOS tests
- Zipapp bootstraps
- Lint
- Windows

Expand Down
69 changes: 25 additions & 44 deletions bork/api.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from functools import partial
import os
from pathlib import Path
from signal import Signals
import subprocess
import sys
from warnings import warn

from . import builder
from .filesystem import try_delete, load_pyproject
from .config import Config
from .creds import Credentials
from .filesystem import try_delete
from .log import logger


def aliases():
"""Returns a list of the aliases defined in pyproject.toml."""
pyproject = load_pyproject()
return pyproject.get('tool', {}).get('bork', {}).get('aliases', {})
"""Returns the aliases defined in pyproject.toml."""
return Config.from_project(Path.cwd()).bork.aliases


def build():
Expand Down Expand Up @@ -121,36 +121,21 @@ def release(repository_name, dry_run, github_release_override=None, pypi_release
if None, respect the configuration in pyproject.toml.
"""
from . import github, pypi
pyproject = load_pyproject()
bork_config = pyproject.get('tool', {}).get('bork', {})
release_config = bork_config.get('release', {})
github_token = os.environ.get('BORK_GITHUB_TOKEN', None)
config = Config.from_project(Path.cwd())
credentials = Credentials.from_env()

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)

strip_zipapp_version = release_config.get('strip_zipapp_version', False)
globs = release_config.get('github_release_globs', ['./dist/*.pyz'])

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

release_to_github = github_release_override if github_release_override is not None else config.bork.release.github
release_to_pypi = pypi_release_override if pypi_release_override is not None else config.bork.release.pypi
if not release_to_github and not release_to_pypi:
raise RuntimeError('Configured to release to neither PyPi nor GitHub?')

if release_to_github:
github_repository = release_config.get('github_repository', None)

if github_token is None:
if credentials.github is None:
logger().error('No GitHub token specified. Use the BORK_GITHUB_TOKEN '
'environment variable to set it.')

Expand All @@ -159,11 +144,18 @@ def release(repository_name, dry_run, github_release_override=None, pypi_release
else:
sys.exit(1)

config = github.GithubConfig(github_token, github_repository, project_name)
github_config = github.GithubConfig(
credentials.github,
config.bork.release.github_repository,
config.project_name
)
github_release = github.GithubRelease(
config, tag=f'v{version}', commitish=None, body=None,
globs=globs,
dry_run=dry_run, strip_zipapp_version=strip_zipapp_version)
github_config,
tag = f'v{version}', commitish = None, body = None,
globs = config.bork.release.github_release_globs,
dry_run = dry_run,
strip_zipapp_version = config.bork.release.strip_zipapp_version,
)
github_release.prepare()

if release_to_pypi:
Expand All @@ -176,22 +168,11 @@ def release(repository_name, dry_run, github_release_override=None, pypi_release

def run(alias):
"""Run the alias specified by `alias`, as defined in pyproject.toml."""
pyproject = load_pyproject()

try:
commands = pyproject['tool']['bork']['aliases'][alias]
except KeyError as error:
raise RuntimeError(f"No such alias: '{alias}'") from error
commands = aliases().get(alias)
if commands is None:
raise RuntimeError(f"No such alias: '{alias}'")

logger().info("Running '%s'", commands)

if isinstance(commands, str):
commands = [commands]
elif isinstance(commands, list):
pass
else:
raise TypeError(f"commands must be str or list, was {type(commands)}")

try:
for command in commands:
print(command)
Expand Down
9 changes: 4 additions & 5 deletions bork/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
.. _wheel: https://packaging.python.org/en/latest/glossary/#term-Wheel
"""

from .filesystem import load_pyproject
from .config import Config
from .log import logger

import build
Expand Down Expand Up @@ -125,8 +125,7 @@ def zipapp(self, main):
log.info("Building zipapp")

log.debug("Loading configuration")
config = load_pyproject().get("tool", {}).get("bork", {})
zipapp_cfg = config.get("zipapp")
config = Config.from_project(self.src)

log.debug("Loading metadata")
meta = self.metadata()
Expand All @@ -147,8 +146,8 @@ def zipapp(self, main):
zipapp.create_archive(
source = tmp,
target = dst,
interpreter = config.get('python_interpreter', DEFAULT_PYTHON_INTERPRETER),
main = main or zipapp_cfg['main'], # TODO: give a more user-friendly error if neither is set
interpreter = config.bork.python_interpreter,
main = main or config.bork.zipapp.main, # TODO error if main is None and there's no __main__.py
compressed = True,
)

Expand Down
17 changes: 5 additions & 12 deletions bork/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
"""

import argparse
import inspect
import logging
import sys
from pathlib import Path
import argparse, inspect, logging, sys

from . import __version__
from . import api
from .filesystem import load_pyproject
from .config import Config
from .log import logger


Expand All @@ -46,15 +44,10 @@ def build(args):
Build the project.
"""

pyproject = load_pyproject()
config = pyproject.get('tool', {}).get('bork', {})
zipapp_cfg = config.get('zipapp', {})
zipapp_enabled = zipapp_cfg.get('enabled', False)

api.build()

if args.zipapp or zipapp_enabled:
config = Config.from_project(Path.cwd())
if args.zipapp or config.bork.zipapp.enabled:
api.build_zipapp(args.zipapp_main)


Expand Down
79 changes: 79 additions & 0 deletions bork/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from collections.abc import Mapping, Set, Sequence
from functools import partial, reduce
from pathlib import Path
from typing import Annotated, Optional # after Py3.10, replace string annotations with Self

from pydantic import dataclasses, BeforeValidator, TypeAdapter

try:
import tomllib
except ImportError:
# Py3.10 compatibility shim
import toml as tomllib # type: ignore


# TODO(nicoo): tie model definitions into CLI parsing

# Ensure we don't accidentally make non-frozen or non-kw-only dataclasses
dataclass = partial(dataclasses.dataclass, frozen = True, kw_only = True)

@dataclass
class ReleaseConfig:
# Related CLI flags: dry_run, pypi_repository
github: bool = False
github_release_globs: Set[str] = frozenset(("./dist/*.pyz", ))
github_repository: Optional[str] = None # TODO(nicoo) refine type

pypi: bool = True
strip_zipapp_version: bool = False

@dataclass
class ZipappConfig:
enabled: bool = False # args.zipapp
main: Optional[str] = None # args.zipapp_main
# TODO(nicoo): specify entrypoint format w/ regex annotation


Commands = Annotated[
Sequence[str],
BeforeValidator(lambda x, _: (x, ) if isinstance(x, str) else x),
]

@dataclass
class ToolConfig:
aliases: Mapping[str, Commands] = dataclasses.Field(default_factory = dict)
release: ReleaseConfig = ReleaseConfig()

python_interpreter: str = "/usr/bin/env python3" # TODO: move to ZipappConfig
zipapp: ZipappConfig = ZipappConfig()

ToolConfigAdapter = TypeAdapter(ToolConfig)


@dataclass
class Config:
bork: ToolConfig
project_name: Optional[str] = None

@classmethod
def from_project(cls, root: Path) -> 'Config':
try:
# Inefficient but necessary for compatibility with toml shim
# To be improved once Py3.10 support is removed
pyproject = tomllib.loads((root / "pyproject.toml").read_text())
except FileNotFoundError:
if any((root / fn).exists() for fn in ("setup.py", "setup.cfg")):
# Legacy setuptools project without Bork-specific config
pyproject = {}
else:
raise

def get(*ks):
return reduce(lambda d, k: d.get(k, {}), ks, pyproject)

# TODO(nicoo) figure out why mypy doesn't accept this
# according to the documentation it should
return Config( # type: ignore
bork = ToolConfigAdapter.validate_python(get("tool", "bork")),
project_name = get("project", "name") or None, # get may return {}
)
43 changes: 43 additions & 0 deletions bork/creds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from os import getenv
from typing import Optional

from pydantic.dataclasses import dataclass

@dataclass(frozen = True, kw_only = True)
class Credentials:
github: Optional[str] = None
pypi: Optional['Credentials.PyPI'] = None

@classmethod
def from_env(cls) -> 'Credentials':
# TODO: support specifying another env than os.environ?
return cls(
github = getenv("BORK_GITHUB_TOKEN"),
pypi = cls.PyPI.from_env(),
)

@dataclass(frozen = True)
class PyPI:
username: str
password: str

@classmethod
def from_env(cls) -> Optional['Credentials.PyPI']:
match getenv("BORK_PYPI_USERNAME"), getenv("BORK_PYPI_PASSWORD"), getenv("BORK_GITHUB_TOKEN"):
case username, password, None if username and password:
return cls(username, password)
case None, None, token if token:
return cls("__token__", token)
case None, None, None:
return None

# Error cases
case _, password, token if password and token:
raise RuntimeError("Cannot specify both BORK_PYPI_{PASSWORD, TOKEN}")
case (x, None, _) | (None, x, _) if x:
raise RuntimeError(
"Either both or none of BORK_PYPI_{USERNAME, PASSWORD} must be specified"
)

# mypy cannot check for completeness of pattern matching (yet?)
raise AssertionError("Accidentally-incomplete pattern matching")
Loading

0 comments on commit 9cb64f2

Please sign in to comment.