feat: modernize to uv + pyproject.toml + semantic-release - #381
feat: modernize to uv + pyproject.toml + semantic-release#381irfanuddinahmad wants to merge 9 commits into
Conversation
Replace setup.py/setup.cfg with PEP 621 static metadata in pyproject.toml, following the org-wide modernization effort tracked in openedx/public-engineering#506. - Static [project] metadata (name, description, classifiers, static dependencies) with dynamic version resolved via setuptools-scm from git tags instead of the hardcoded __init__.py string - SPDX license expression (license = "AGPL-3.0") + license-files, replacing the old license="AGPL 3.0" string - __version__ in openedx_filters/__init__.py now reads from importlib.metadata instead of being hand-maintained (and permanently stale relative to git tags) - Move .coveragerc and setup.cfg's [isort] section into pyproject.toml ([tool.coverage.*], [tool.isort]) - Exclude test subpackages from the built wheel via [tool.setuptools.packages.find] / [tool.setuptools.exclude-package-data] - Drop changelog.d/scriv.ini's now-stale version literal (was pointed at the removed __init__.py __version__ string)
Replace pip-compile/requirements/*.txt with uv + PEP 735 dependency groups, per openedx/public-engineering#506. - Add [dependency-groups] (test-base, test, django42, quality, doc, ci, dev) mirroring the old base/test/quality/doc/ci/dev requirements layering; commit the resulting uv.lock - [tool.edx_lint].uv_constraints / [tool.uv].constraint-dependencies wired up via `edx_lint write_uv_constraints` - tox.ini: tox-uv>=1 with the uv-venv-lock-runner; dependency_groups replace deps/commands_pre (the old py311-only "make upgrade" + requirements/test.txt install pre-step is gone, no longer needed since uv.lock drives all envs identically) - Makefile: `requirements`/`upgrade`/quality targets now use `uv sync`/`uv lock`/`uv run tox` instead of pip-tools and `python setup.py bdist_wheel` - .readthedocs.yaml: install docs deps via RTD's native uv support (method: uv, group: doc) instead of requirements/doc.txt - ci.yml: astral-sh/setup-uv instead of actions/setup-python + pip install; fetch-depth: 0 so setuptools-scm can see tags; per-toxenv job names; contents: read permissions - Delete the requirements/ directory Explicitly pin the `uv` package version (a transitive dependency of tox-uv/tox-uv-bare) via [tool.edx_lint].uv_constraints, and match it in ci.yml's setup-uv step, and explicitly list all django42-conflicting dependency groups in [tool.uv].conflicts instead of relying on uv to derive them. Both were needed for a reproducible uv.lock: different uv releases serialize auto-derived [tool.uv].conflicts entries in different (and, within a single uv release, not always stable) order, which otherwise makes tox-uv-bare's `uv sync --locked` step fail nondeterministically depending on which uv version last touched the lockfile.
Replace the manual workflow_dispatch release process (bump-my-version + scriv github-release) with python-semantic-release, per openedx/public-engineering#506. - [tool.semantic_release]: build via `python -m build` with SETUPTOOLS_SCM_PRETEND_VERSION so setuptools-scm reports the version semantic-release just computed; major_on_zero = false, allow_zero_version = true - .github/workflows/release.yml: on push to main, run CI, then python-semantic-release (tag + GitHub release, no changelog file management since CHANGELOG.rst stays scriv/manually curated), then publish to PyPI via OIDC (id-token: write, no stored credentials) - ci.yml: add workflow_call trigger so release.yml can reuse it as its test gate; drop the push-to-main trigger now that release.yml owns that path, avoiding a duplicate test run on every push to main - Delete pypi-publish.yml (the old release: published-triggered, token-based publish workflow) so it can't race the new OIDC-based publish_to_pypi job once semantic-release starts pushing tags - commitlint.yml already existed (reusable openedx/.github workflow) and needed no changes Pre-flight check: the latest git tag (v3.8.0, via `git tag --sort=-v:refname`) matches the version currently live on PyPI (3.8.0), so the first semantic-release run has an accurate base to compute the next version from. Pinned pypa/gh-action-pypi-publish to a commit SHA (not the floating @release/v1 ref some other org repos still use) and python-semantic-release/python-semantic-release + python-semantic-release/publish-action + actions/upload-artifact + actions/download-artifact to commit SHAs too, matching this repo's existing house style of SHA-pinning every action. Note: PyPI's trusted publisher (OIDC) configuration for this project still needs to be confirmed/added by a maintainer with PyPI project admin access before the first automated release can publish -- this PR cannot configure that from here. See PR description.
|
Thanks for the pull request, @irfanuddinahmad! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
…ttern The pre-migration .coveragerc's `omit = tests` never actually matched anything -- coverage.py omit patterns need a wildcard (e.g. `*/tests/*`), so test modules were inadvertently included in the coverage measurement and inflated the reported project total (test files execute almost all of their own lines, so including them nudges the % up). Verified locally: re-running the exact same test suite with the old config's omit pattern measures 99.7809%, vs 99.5338% with the new (correct) [tool.coverage.run] omit patterns from `pyproject.toml` -- a ~0.25 point drop entirely explained by that fix, not a real regression in production code coverage. Add a documented 1% threshold to codecov.yml's project status so this one-time methodology correction doesn't fail codecov/project, while still catching genuine coverage regressions going forward.
Per the org-wide decision on openedx/public-engineering#506 (src/ layout in scope for this modernization cycle, precedent set in xblocks-core/xblocks-extra), move openedx_filters/ to src/openedx_filters/. Fixes editable-install resolution under tools like mypy, which don't reliably resolve flat-layout editable installs (see https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). - pyproject.toml: [tool.setuptools] package-dir = {"" = "src"}; [tool.setuptools.packages.find] where = ["src"] - MANIFEST.in: recursive-include path updated to src/openedx_filters - mypy.ini: files = src/openedx_filters - Makefile: pylint/pycodestyle/ruff/isort quality-check targets updated to point at src/openedx_filters (these take filesystem paths, not importable module names, so they needed the explicit path fix; mypy.ini's `files` setting is likewise path-based) - docs/conf.py: sys.path.insert now targets ../src; linkcode_resolve's REPO_URL relpath calculation now walks up two directories (out of src/) instead of one, so generated GitHub source links point at src/openedx_filters/... instead of the now-stale openedx_filters/... - Fixed two docs files with hardcoded (now-broken) GitHub blob links to the pre-move flat path (docs/how-tos/create-a-pipeline-step.rst, docs/reference/glossary.rst) [tool.coverage.run].source and tox.ini's `pytest --cov openedx_filters` are left as the module name (not a path) -- both coverage.py and pytest-cov resolve module names via the installed package regardless of physical layout, verified locally to still report accurate per-file coverage against the new src/ paths. Verified locally: editable install (`uv sync`) resolves `openedx_filters.__file__` to src/openedx_filters/__init__.py; `uv run mypy` succeeds against the new layout; full tox matrix (py311-django42, py311-django52, django42, django52, quality, docs) passes; `uv build` produces a wheel with an unchanged install layout (openedx_filters/... at the wheel root, no src/ prefix leaks into the installed package) -- so this does not change the package's import path or public API for consumers like openedx-platform, only this repo's internal file layout.
Verified every uses:@sha in release.yml against the GitHub API (repos/<owner>/<repo>/commits/<sha>). python-semantic-release/publish-action was pinned to a SHA that doesn't exist in that repo at all; python-semantic-release/python-semantic-release and pypa/gh-action-pypi-publish were pinned to their tag *objects'* SHAs rather than the underlying commit SHAs (a real but different git object, not resolvable the same way a `uses:` checkout needs). Both invisible to PR CI since release.yml only runs on push to main. Reverted python-semantic-release, publish-action, upload-artifact, and download-artifact to plain version tags matching openedx/XBlock's actual, already-releasing release.yml. Corrected pypa/gh-action-pypi-publish to the real commit SHA.
changelog: "false" was blindly copied from the sample-plugin reference
template with no ticket ever requiring it to be disabled. Wire up PSR
to update the existing CHANGELOG.rst in place instead:
- Add the insertion marker as the first line of CHANGELOG.rst so PSR's
update mode has an anchor to insert new version sections above.
- Remove changelog: "false" from release.yml's PSR step.
- Configure [tool.semantic_release.changelog] (mode="update", RST
output) to target CHANGELOG.rst.
- Set tag_format = "v{version}" explicitly to match this repo's actual
tag convention (confirmed via git tag, e.g. v3.8.0).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The PR template still told contributors to add a changelog entry "using scriv" — a manual process this migration effort already replaced with python-semantic-release, which now generates CHANGELOG.rst automatically from conventional commit messages. Drop the obsolete checklist item so the template doesn't ask for a step that no longer applies. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
python-semantic-release's release.yml now creates the tag, GitHub release, and publishes to PyPI automatically on merge -- these were no longer real manual steps for a contributor to perform.
Summary
Modernize
openedx-filtersto uv + pyproject.toml (PEP 621/735) + python-semantic-release.Part of openedx/public-engineering#506.
openedx_filters/tosrc/openedx_filters/(org decision on #506: src/ layout is in scope this cycle, precedent inxblocks-extra) — fixes editable-install resolution under tools like mypy; does not change the installed package's import path (verified: wheel contents are unchanged, stillopenedx_filters/...at the wheel root, nosrc/leaks in)setup.py/setup.cfgwithpyproject.toml(PEP 621 static metadata);.coveragercandsetup.cfg's[isort]section also folded intopyproject.tomluvwith PEP 735 dependency groups; commituv.locktox.inito usetox-uvwithuv-venv-lock-runnerastral-sh/setup-uv; SHA-pin all actionspython-semantic-release+release.yml; replace the old manualworkflow_dispatchrelease process (bump-my-version + scriv github-release) and the old token-basedpypi-publish.ymlcommitlint.ymlalready existed in this repo and needed no changesNot included
changelog.d(scriv) is left in place as the manually-curated changelog;python-semantic-releaseis configured withchangelog: "false"so it only owns version tagging/PyPI publish, not changelog content.changelog.d/scriv.ini'sversion = literal: openedx_filters/__init__.py: __version__setting was removed since that__version__string no longer exists (see Versioning below) — runningmake changelog(scriv collect) will now just omit a version number from the entry heading, which a maintainer can pass manually if desired..github/workflows/upgrade-python-requirements.ymlhas a latent gap this PR can't fix. It callsopenedx/.github's shared reusable workflow, which hardcodesadd-paths: requirementswhen opening its automated dependency-bump PR (no input is exposed to override it). Now thatrequirements/is deleted andmake upgrade/uv lock --upgradeinstead touchesuv.lockat the repo root, the scheduled Monday cron run will find no changes under the allow-listedrequirementspath and won't open a PR (or will open an empty one). Fixing this requires a change to the sharedopenedx/.githubreusable workflow itself, not this repo — flagging it here so it isn't mistaken for something this PR forgot.Versioning
Dynamic, via
setuptools-scmfrom git tags (previously a hardcoded__version__string inopenedx_filters/__init__.py, kept in sync by hand viabump-my-version).__version__is now read at import time viaimportlib.metadata.Pre-flight check: the latest git tag (
v3.8.0, viagit tag --sort=-v:refname) matches the version currently live on PyPI (3.8.0), so the first semantic-release run has an accurate base to compute the next version from.Testing Notes
Verified locally:
uv lockresolves cleanly (129 packages)uv sync --group devsucceeds; editable install resolvesopenedx_filters.__file__tosrc/openedx_filters/__init__.pyuv run mypysucceeds against the new src/ layout (17 source files, no issues) — the specific resolution problem src/ layout is meant to fixpy311-django42,py311-django52,django42,django52(93 tests each),quality(pylint/mypy/pycodestyle/ruff/isort/build+twine check),docs(Sphinx build + doc8, including confirminglinkcode_resolvenow emits correct.../blob/main/src/openedx_filters/...URLs)uv buildproduces a wheel/sdist with correct metadata (version 3.8.0 from the git tag,AGPL-3.0SPDX license, test subpackages excluded from the wheel) and unchanged wheel contents (openedx_filters/...at the wheel root, byte-identical file listing to before the src/ move)Could not be verified locally (needs CI / maintainer action):
release.yml's reusable-workflow call, OIDC token exchange) — validated the YAML parses and the job/permission structure locally, but GitHub Actions-specific behavior (e.g.secrets: inheritthrough a reusable workflow call) can only be confirmed once CI runs on the PR.Code reviewer notes
release.yml'spublish_to_pypijob assumes a PyPI trusted publisher is set up foropenedx-filters(Settings → Publishing on the PyPI project page, pointing atopenedx/openedx-filters, workflowrelease.yml, environment optional). This can't be configured from a PR — a maintainer with PyPI project admin access needs to confirm/add it before the first automated release can actually publish. Until then,release/tagging/GitHub-release-creation will still work; only the finalpublish_to_pypistep would fail.uv/tox-uvversion pinning: while verifying this locally, differentuvreleases turned out to serialize auto-derived[tool.uv].conflictsentries (for thedjango42dependency-group conflict) in a not-always-stable order, which madetox-uv-bare's internaluv sync --lockedcheck fail nondeterministically depending on whichuvversion last touched the lockfile. Fixed by (a) listing all of the transitively-conflicting groups (test,doc,quality,dev) explicitly in[tool.uv].conflictsinstead of relying on derivation, and (b) pinning theuvpackage version itself via[tool.edx_lint].uv_constraintsto match theversion:pin on theastral-sh/setup-uvstep inci.yml. If a futuremake upgradebumps the lockeduvversion, bump thesetup-uvstep'sversion:input to match (see comments inpyproject.toml/ci.yml).commands_prestep intox.ini(make upgrade+ manualrequirements/test.txtinstall) is gone; it's no longer needed sinceuv.lockdrives every env's dependencies identically regardless of Python version.This PR was created with Claude Code.