From d55b6b1ee638b3c00d7797c5884f85e7bcc8b510 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Wed, 22 Jan 2025 22:54:57 +0000 Subject: [PATCH] ci: switch building & release to uv, update ruff and mypy configs --- .ci/release-uv | 60 ++++++++++++++++++++++++++++++++++++++ .ci/run | 15 ++-------- .github/workflows/main.yml | 20 ++++++++----- mypy.ini | 9 ++++-- pyproject.toml | 7 +++++ ruff.toml | 1 - src/orger/common.py | 4 +-- src/orger/org_view.py | 6 ++-- src/orger/state.py | 4 +-- tox.ini | 20 +++++++------ 10 files changed, 107 insertions(+), 39 deletions(-) create mode 100755 .ci/release-uv diff --git a/.ci/release-uv b/.ci/release-uv new file mode 100755 index 0000000..c56697c --- /dev/null +++ b/.ci/release-uv @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +''' +Deploys Python package onto [[https://pypi.org][PyPi]] or [[https://test.pypi.org][test PyPi]]. + +- running manually + + You'll need =UV_PUBLISH_TOKEN= env variable + +- running on Github Actions + + Instead of env variable, relies on configuring github as Trusted publisher (https://docs.pypi.org/trusted-publishers/) -- both for test and regular pypi + + It's running as =pypi= job in [[file:.github/workflows/main.yml][Github Actions config]]. + Packages are deployed on: + - every master commit, onto test pypi + - every new tag, onto production pypi +''' + +UV_PUBLISH_TOKEN = 'UV_PUBLISH_TOKEN' + +import argparse +import os +import shutil +from pathlib import Path +from subprocess import check_call + +is_ci = os.environ.get('CI') is not None + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument('--use-test-pypi', action='store_true') + args = p.parse_args() + + publish_url = ['--publish-url', 'https://test.pypi.org/legacy/'] if args.use_test_pypi else [] + + root = Path(__file__).absolute().parent.parent + os.chdir(root) # just in case + + if is_ci: + # see https://github.com/actions/checkout/issues/217 + check_call('git fetch --prune --unshallow'.split()) + + # TODO ok, for now uv won't remove dist dir if it already exists + # https://github.com/astral-sh/uv/issues/10293 + dist = root / 'dist' + if dist.exists(): + shutil.rmtree(dist) + + # todo what is --force-pep517? + check_call(['uv', 'build']) + + if not is_ci: + # CI relies on trusted publishers so doesn't need env variable + assert UV_PUBLISH_TOKEN in os.environ, f'no {UV_PUBLISH_TOKEN} passed' + + check_call(['uv', 'publish', *publish_url]) + + +if __name__ == '__main__': + main() diff --git a/.ci/run b/.ci/run index 7fc809f..dc9c3e4 100755 --- a/.ci/run +++ b/.ci/run @@ -36,16 +36,5 @@ if [ -n "${CI-}" ]; then esac fi - -PY_BIN="python3" -# some systems might have python pointing to python3 -if ! command -v python3 &> /dev/null; then - PY_BIN="python" -fi - - -# TODO hmm for some reason installing uv with pip and then running -# "$PY_BIN" -m uv tool fails with missing setuptools error?? -# just uvx directly works, but it's not present in PATH... -"$PY_BIN" -m pip install --user pipx -"$PY_BIN" -m pipx run uv tool run --with=tox-uv tox $tox_cmd "$@" +# NOTE: expects uv installed +uv tool run --with tox-uv tox $tox_cmd "$@" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d8b716..b03b408 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,6 +41,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: false # we don't have lock files, so can't use them as cache key - uses: actions/checkout@v4 with: @@ -70,7 +74,9 @@ jobs: pypi: runs-on: ubuntu-latest needs: [build] # add all other jobs here - + permissions: + # necessary for Trusted Publishing + id-token: write steps: # ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation - run: echo "$HOME/.local/bin" >> $GITHUB_PATH @@ -79,6 +85,10 @@ jobs: with: python-version: '3.10' + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: false # we don't have lock files, so can't use them as cache key + - uses: actions/checkout@v4 with: submodules: recursive @@ -86,14 +96,10 @@ jobs: - name: 'release to test pypi' # always deploy merged master to test pypi if: github.event_name != 'pull_request' && github.event.ref == 'refs/heads/master' - env: - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD_TEST }} - run: pip3 install --user --upgrade build twine && .ci/release --test + run: .ci/release-uv --use-test-pypi - name: 'release to pypi' # always deploy tags to release pypi # NOTE: release tags are guarded by on: push: tags on the top if: github.event_name != 'pull_request' && startsWith(github.event.ref, 'refs/tags') - env: - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - run: pip3 install --user --upgrade build twine && .ci/release + run: .ci/release-uv diff --git a/mypy.ini b/mypy.ini index 9ab1c91..25d4dec 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,11 +3,14 @@ pretty = True show_error_context = True show_column_numbers = True show_error_end = True -warn_redundant_casts = True -warn_unused_ignores = True + check_untyped_defs = True + +# see https://mypy.readthedocs.io/en/stable/error_code_list2.html +warn_redundant_casts = True strict_equality = True -enable_error_code = possibly-undefined +warn_unused_ignores = True +enable_error_code = deprecated,redundant-expr,possibly-undefined,truthy-bool,truthy-iterable,ignore-without-code,unused-awaitable # an example of suppressing # [mypy-my.config.repos.pdfannots.pdfannots] diff --git a/pyproject.toml b/pyproject.toml index c9e62ea..b471aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ Homepage = "https://github.com/karlicoss/orger" optional = [ "colorlog", ] + +[dependency-groups] testing = [ "pytest", "ruff", @@ -42,3 +44,8 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] version_scheme = "python-simplified-semver" local_scheme = "dirty-tag" + +# workaround for error during uv publishing +# see https://github.com/astral-sh/uv/issues/9513#issuecomment-2519527822 +[tool.setuptools] +license-files = [] diff --git a/ruff.toml b/ruff.toml index 632f356..3d3c79a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -109,7 +109,6 @@ lint.ignore = [ "PLW0603", # global variable update.. we usually know why we are doing this "PLW2901", # for loop variable overwritten, usually this is intentional - "PT004", # deprecated rule, will be removed later "PT011", # pytest raises should is too broad "PT012", # pytest raises should contain a single statement diff --git a/src/orger/common.py b/src/orger/common.py index 872b201..58ba0a8 100644 --- a/src/orger/common.py +++ b/src/orger/common.py @@ -2,7 +2,7 @@ import traceback import warnings -from datetime import datetime +from datetime import datetime, tzinfo from pathlib import Path from typing import TYPE_CHECKING @@ -15,7 +15,7 @@ class settings: USE_PANDOC: bool = True -_timezones = set() # type: ignore +_timezones: set[tzinfo | None] = set() def dt_heading(dt: datetime | None, heading: str) -> str: diff --git a/src/orger/org_view.py b/src/orger/org_view.py index 0f15798..6aca729 100644 --- a/src/orger/org_view.py +++ b/src/orger/org_view.py @@ -280,7 +280,8 @@ def main(cls, setup_parser=None) -> None: def test_org_view_overwrite(tmp_path: Path): class TestView(Mirror): def __init__(self, items: list[OrgWithKey], *args, **kwargs) -> None: - super().__init__(*args, file_header='# autogenerated!\n#+TITLE: sometitle\nalso text\n', **kwargs) # type: ignore + kwargs['file_header'] = '# autogenerated!\n#+TITLE: sometitle\nalso text\n' + super().__init__(*args, **kwargs) self.items = items def get_items(self): @@ -313,7 +314,8 @@ def get_items(self): def test_org_view_append(tmp_path: Path) -> None: class TestView(Queue): def __init__(self, items: list[OrgWithKey], *args, **kwargs) -> None: - super().__init__(*args, file_header='# autogenerated!', **kwargs) # type: ignore + kwargs['file_header'] = '# autogenerated!' + super().__init__(*args, **kwargs) self.items = items def get_items(self): diff --git a/src/orger/state.py b/src/orger/state.py index 357befb..5c7f6df 100644 --- a/src/orger/state.py +++ b/src/orger/state.py @@ -111,10 +111,10 @@ def action() -> None: assert mtime() == m2 # shouldn't trigger because item is already present - state.feed('a', 'err', lambda: None.whatever) # type: ignore + state.feed('a', 'err', lambda: None.whatever) # type: ignore[attr-defined] with pytest.raises(AttributeError): - state.feed('hiii', 'error 2 ', lambda: None.whatever) # type: ignore + state.feed('hiii', 'error 2 ', lambda: None.whatever) # type: ignore[attr-defined] assert mtime() == m2 # shouldn't corrupt or modify the file diff --git a/tox.ini b/tox.ini index 7d4ce0f..cd388bb 100644 --- a/tox.ini +++ b/tox.ini @@ -18,21 +18,21 @@ passenv = MYPY_CACHE_DIR RUFF_CACHE_DIR usedevelop = true # for some reason tox seems to ignore "-e ." in deps section?? +uv_seed = true # seems necessary so uv creates separate venvs per tox env? setenv = HPI_MODULE_INSTALL_USE_UV=true -uv_seed = true # seems necessary so uv creates separate venvs per tox env? [testenv:ruff] -deps = - -e .[testing] +dependency_groups = testing commands = {envpython} -m ruff check src/ +# todo not sure if there's much difference between deps and extras= like here? +# https://github.com/tox-dev/tox-uv?tab=readme-ov-file#uvlock-support [testenv:tests] -deps = - -e .[testing] +dependency_groups = testing commands = {envpython} -m pytest \ --pyargs {[testenv]package_name} \ @@ -41,10 +41,11 @@ commands = [testenv:mypy-core] +dependency_groups = testing deps = - -e .[testing,optional] + -e .[optional] commands = - {envpython} -m mypy --install-types --non-interactive \ + {envpython} -m mypy --no-install-types \ # note: modules are tested separately, below -p {[testenv]package_name} \ --exclude 'orger.modules' \ @@ -55,8 +56,9 @@ commands = [testenv:mypy-misc] +dependency_groups = testing deps = - -e .[testing,optional] + -e .[optional] HPI uv # for hpi module install commands = @@ -67,7 +69,7 @@ commands = my.pinboard \ my.kobo - {envpython} -m mypy --install-types --non-interactive \ + {envpython} -m mypy --no-install-types \ -p {[testenv]package_name}.modules \ # txt report is a bit more convenient to view on CI --txt-report .coverage.mypy-misc \