From 7970f785c8b1ce92afbff18c9c89e9fe82b312b0 Mon Sep 17 00:00:00 2001 From: funkpower Date: Fri, 18 Sep 2026 13:40:37 -0400 Subject: [PATCH] fix what a 1.0 would have welded into PyPI metadata An adversarial review of the release, and four of its findings were things PyPI never lets you replace once uploaded. - rust-version said 1.74. clap has needed 1.85 since 4.6, and cargo hard-errors rather than warning, so the sdist advertised a floor it could not build on. docs/releasing.md sends macOS and Windows users to that sdist. - The wheel was classified Development Status :: 4 - Beta on a release the changelog calls stable, with none of the Python version classifiers PyPI indexes on. - Six README links were relative. PyPI resolves them against the project page, so every one 404'd there, as they do on the live 0.2.0 page now. - in that order packs the wheel into the sdist. The tarball on disk was 431KB with an x86-64 binary inside it. Also: twine upload gains --skip-existing, so a retry after a partial upload finishes rather than dying on a version that cannot be reused; twine check gains the renderer that makes it parse the description at all, without which it returns None and reports PASSED; and the README no longer claims a twenty-year sweep the test did not run, because 366 * 21 / 4 is five years. The test now runs the twenty-one it was named for. The crates.io name belongs to an unrelated project, which the README now says rather than leaving a reader to find out from cargo. --- .forgejo/workflows/release.yml | 12 +++++++++--- .gitignore | 2 ++ CHANGELOG.md | 10 ++++++++++ Cargo.toml | 2 +- README.md | 34 +++++++++++++++++++--------------- docs/releasing.md | 3 ++- pyproject.toml | 16 ++++++++++++++-- python/typikon/__init__.py | 10 ++++++---- tests/typikon.rs | 2 +- 9 files changed, 64 insertions(+), 27 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 2888edb..bb01ea1 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -39,7 +39,10 @@ jobs: run: | apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3 -m venv /tmp/venv - /tmp/venv/bin/pip install --quiet 'maturin[zig]' twine + # readme-renderer[md] is what makes `twine check` actually parse the + # long description. Without it the render returns None and the + # check reports PASSED regardless, even with --strict. + /tmp/venv/bin/pip install --quiet 'maturin[zig]' twine 'readme-renderer[md]' # --zig cross-compiles against an older glibc than this image ships, so # the wheel is not silently pinned to bookworm's. for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do @@ -60,5 +63,8 @@ jobs: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | - /tmp/venv/bin/twine check dist/* - /tmp/venv/bin/twine upload --non-interactive dist/* + /tmp/venv/bin/twine check --strict dist/* + # --skip-existing so a retry after a partial upload finishes the job + # instead of dying on the first file. A version cannot be reused, + # so a half-published release would otherwise burn it. + /tmp/venv/bin/twine upload --non-interactive --skip-existing dist/* diff --git a/.gitignore b/.gitignore index da5ea15..1e973c0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .venv/ *.egg-info/ /dist +__pycache__/ +*.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index 47308ea..c53aa73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,16 @@ Everything below changes what a day reports against pre-1.0 versions. - `--set-default` remembers the tradition and the discipline alongside the jurisdiction. +### Packaging + +- Requires Rust 1.85, which is what the dependency tree has needed since clap + 4.6. `rust-version` had said 1.74, so the source distribution claimed to + build on toolchains where cargo refuses outright. +- Classified as production/stable, with the Python version and platform + classifiers PyPI indexes on, and Changelog, Documentation and Issues links. +- README links are absolute, because PyPI does not resolve relative ones and + every one of them 404'd on the project page. + ### Changed - Rewritten in Rust. It ships as one static binary with the menologion compiled diff --git a/Cargo.toml b/Cargo.toml index ffa17e9..d6c6fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "typikon" version = "1.0.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.85" description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched" license = "GPL-2.0-only" repository = "https://git.pumpin.online/funkpower/typikon" diff --git a/README.md b/README.md index fe9fc84..d5f1e07 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ One static binary, no runtime to install first. $ cargo install --git https://git.pumpin.online/funkpower/typikon ``` +Rust 1.85 or newer. The name `typikon` on crates.io belongs to an unrelated +project, so `cargo install typikon` and a `typikon = "1"` dependency will not +get you this; install and depend on it by git URL. + Or build it from a checkout: ```console @@ -63,7 +67,9 @@ $ pip install typikon ``` The menologion is compiled into that too, so it carries no data files and needs -no network either. +no network either. Wheels are built for Linux on x86-64 and aarch64; everywhere +else pip falls back to the source distribution, which needs a Rust toolchain of +1.85 or newer on the installing machine. ## Which calendar? @@ -176,7 +182,7 @@ will want. Ten of the fourteen Byzantine-rite Eastern Catholic churches have no entry of their own. Each keeps one of the three reckonings, and -[docs/calendars.md](docs/calendars.md) sets out which questions decide it. +[docs/calendars.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md) sets out which questions decide it. ## The fast @@ -425,22 +431,18 @@ It lands in `~/.local/share/typikon/` and is overlaid automatically. ## Further reading -- [docs/calendars.md](docs/calendars.md) sets out which churches keep which +- [docs/calendars.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md) sets out which churches keep which reckoning, how the split came about, what is modelled here and what is not. -- [docs/versioning.md](docs/versioning.md) says what counts as the public +- [docs/versioning.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/versioning.md) says what counts as the public surface, and how a correction to the calendar itself is announced. -- [CHANGELOG.md](CHANGELOG.md) carries those announcements. +- [CHANGELOG.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/CHANGELOG.md) carries those announcements. ## Accuracy -`typikon` is pinned against published calendars: the paschalion for six years -by both reckonings and its invariants across two centuries, 146,000 calendar -round-trips from 1800 to 2200, the fast on every day that is not open to -interpretation, and every jurisdiction over twenty years. - -It was ported from a Python implementation, and the port was checked by -running both over 199,446 day-records, 26 jurisdictions across 21 years, and -requiring every field of every record to match. +`typikon` is pinned against published calendars: the paschalion for eleven +named years and its invariants across the two centuries from 1900, 146,000 +calendar round-trips from 1800 to 2200, the fast on every day that is not open +to interpretation, and every jurisdiction over the twenty-one years from 2020. It is a calendar, not an authority. If it disagrees with the one your parish prints, your parish is right. Please open an issue. @@ -455,10 +457,12 @@ widgets for Hyprland (Quickshell), waybar and eww, all reading the JSON above. ## Releasing `main` is always releasable; a `vX.Y.Z` tag on it builds the wheels and publishes -to PyPI. [docs/releasing.md](docs/releasing.md) has the workflow, and what has to +to PyPI. [docs/releasing.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/releasing.md) has the workflow, and what has to be set up before the first one. ## Licence -GPL-2.0-only. See [LICENSE](LICENSE). The menologion is under it too; +GPL-2.0-only. The source for any published release is the source distribution +beside it on PyPI, and `Cargo.lock` pins every dependency to an exact version. +See [LICENSE](https://git.pumpin.online/funkpower/typikon/src/branch/main/LICENSE). The menologion is under it too; `data/LICENSE` says so and notes what the fetched sets are not. diff --git a/docs/releasing.md b/docs/releasing.md index b78233c..afbcb8e 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -110,7 +110,8 @@ developers, a wall for everyone else. ## Checking a release without publishing ```console -$ maturin build --release --out dist && maturin sdist --out dist +$ rm -rf dist # a wheel left in dist/ gets packed into the sdist +$ maturin sdist --out dist && maturin build --release --out dist $ twine check dist/* $ python -m venv /tmp/v && /tmp/v/bin/pip install --no-binary :all: dist/*.tar.gz $ /tmp/v/bin/python -c 'import typikon, datetime; print(typikon.today())' diff --git a/pyproject.toml b/pyproject.toml index eab46d5..56067ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,19 @@ license-files = ["LICENSE"] requires-python = ">=3.9" keywords = ["orthodox", "liturgical", "calendar", "byzantine", "pascha", "fasting"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", "Intended Audience :: Developers", - "Programming Language :: Rust", + "Intended Audience :: End Users/Desktop", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Rust", "Topic :: Religion", "Topic :: Software Development :: Libraries", "Typing :: Typed", @@ -25,6 +34,9 @@ dynamic = ["version"] [project.urls] Homepage = "https://git.pumpin.online/funkpower/typikon" Repository = "https://git.pumpin.online/funkpower/typikon" +Changelog = "https://git.pumpin.online/funkpower/typikon/src/branch/main/CHANGELOG.md" +Documentation = "https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md" +Issues = "https://git.pumpin.online/funkpower/typikon/issues" [tool.maturin] bindings = "pyo3" diff --git a/python/typikon/__init__.py b/python/typikon/__init__.py index f92fd5b..58571f3 100644 --- a/python/typikon/__init__.py +++ b/python/typikon/__init__.py @@ -4,15 +4,17 @@ Everything derives from the paschalion and the fixed calendar, so this needs no network, cannot go stale, and answers for any year you ask about. >>> import typikon, datetime - >>> day = typikon.describe(datetime.date(2026, 4, 12)) + >>> day = typikon.describe(datetime.date(2026, 4, 12), jurisdiction='new') >>> day.feast 'The Radiant and Life-giving Resurrection of our Lord — Holy Pascha' >>> day.fast, day.days_to_pascha ('none', 0) -Called without a jurisdiction, every function uses the one `typikon ---set-default` wrote, so the library and the command line agree about the same -date. Pass ``jurisdiction=`` to override it; ``jurisdictions()`` lists the keys. +The jurisdiction is given here because 12 April 2026 is Pascha on the Julian +paschalion and an ordinary Sunday on the Gregorian one. Called without one, +every function uses whatever `typikon --set-default` wrote, so the library and +the command line agree about the same date. Pass ``jurisdiction=`` to override +it; ``jurisdictions()`` lists the keys. """ from ._typikon import ( diff --git a/tests/typikon.rs b/tests/typikon.rs index fbe35ff..18e7126 100644 --- a/tests/typikon.rs +++ b/tests/typikon.rs @@ -371,7 +371,7 @@ fn ics_is_well_formed() { #[test] fn every_jurisdiction_holds_together_for_twenty_years() { for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) { - for day in week(d("2020-01-01"), 366 * 21 / 4, jur) { + for day in week(d("2020-01-01"), 366 * 21, jur) { assert!(!day.commemorations.is_empty(), "{} {}", jur.key, day.date); assert!(!day.title().is_empty(), "{} {}", jur.key, day.date); // `binds` may never be stricter than the level it sits under.