Skip to content

feat: modernize to uv + pyproject.toml + semantic-release - #381

Open
irfanuddinahmad wants to merge 9 commits into
openedx:mainfrom
irfanuddinahmad:irfanuddinahmad/modernize-python-tooling
Open

feat: modernize to uv + pyproject.toml + semantic-release#381
irfanuddinahmad wants to merge 9 commits into
openedx:mainfrom
irfanuddinahmad:irfanuddinahmad/modernize-python-tooling

Conversation

@irfanuddinahmad

@irfanuddinahmad irfanuddinahmad commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Modernize openedx-filters to uv + pyproject.toml (PEP 621/735) + python-semantic-release.

Part of openedx/public-engineering#506.

  • Move openedx_filters/ to src/openedx_filters/ (org decision on #506: src/ layout is in scope this cycle, precedent in xblocks-extra) — fixes editable-install resolution under tools like mypy; does not change the installed package's import path (verified: wheel contents are unchanged, still openedx_filters/... at the wheel root, no src/ leaks in)
  • Replace setup.py/setup.cfg with pyproject.toml (PEP 621 static metadata); .coveragerc and setup.cfg's [isort] section also folded into pyproject.toml
  • Switch from pip-compile to uv with PEP 735 dependency groups; commit uv.lock
  • Update tox.ini to use tox-uv with uv-venv-lock-runner
  • Update CI to use astral-sh/setup-uv; SHA-pin all actions
  • Add python-semantic-release + release.yml; replace the old manual workflow_dispatch release process (bump-my-version + scriv github-release) and the old token-based pypi-publish.yml
  • commitlint.yml already existed in this repo and needed no changes

Not included

  • Lint tooling (pylint/isort/pycodestyle/mypy/ruff) left exactly as configured before — out of scope per the parent ticket, and this repo already had all of them wired up.
  • CHANGELOG.rst / changelog.d (scriv) is left in place as the manually-curated changelog; python-semantic-release is configured with changelog: "false" so it only owns version tagging/PyPI publish, not changelog content. changelog.d/scriv.ini's version = literal: openedx_filters/__init__.py: __version__ setting was removed since that __version__ string no longer exists (see Versioning below) — running make 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.yml has a latent gap this PR can't fix. It calls openedx/.github's shared reusable workflow, which hardcodes add-paths: requirements when opening its automated dependency-bump PR (no input is exposed to override it). Now that requirements/ is deleted and make upgrade/uv lock --upgrade instead touches uv.lock at the repo root, the scheduled Monday cron run will find no changes under the allow-listed requirements path and won't open a PR (or will open an empty one). Fixing this requires a change to the shared openedx/.github reusable workflow itself, not this repo — flagging it here so it isn't mistaken for something this PR forgot.

Versioning

Dynamic, via setuptools-scm from git tags (previously a hardcoded __version__ string in openedx_filters/__init__.py, kept in sync by hand via bump-my-version). __version__ is now read at import time via importlib.metadata.

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.

Testing Notes

Verified locally:

  • uv lock resolves cleanly (129 packages)
  • uv sync --group dev succeeds; editable install resolves openedx_filters.__file__ to src/openedx_filters/__init__.py
  • uv run mypy succeeds against the new src/ layout (17 source files, no issues) — the specific resolution problem src/ layout is meant to fix
  • Full tox matrix passes: py311-django42, py311-django52, django42, django52 (93 tests each), quality (pylint/mypy/pycodestyle/ruff/isort/build+twine check), docs (Sphinx build + doc8, including confirming linkcode_resolve now emits correct .../blob/main/src/openedx_filters/... URLs)
  • uv build produces a wheel/sdist with correct metadata (version 3.8.0 from the git tag, AGPL-3.0 SPDX 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):

  • The actual GitHub Actions run (matrix jobs, 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: inherit through a reusable workflow call) can only be confirmed once CI runs on the PR.
  • PyPI OIDC publish itself (see below).

Code reviewer notes

  • PyPI trusted publisher (OIDC) is not yet confirmed configured for this project. release.yml's publish_to_pypi job assumes a PyPI trusted publisher is set up for openedx-filters (Settings → Publishing on the PyPI project page, pointing at openedx/openedx-filters, workflow release.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 final publish_to_pypi step would fail.
  • uv/tox-uv version pinning: while verifying this locally, different uv releases turned out to serialize auto-derived [tool.uv].conflicts entries (for the django42 dependency-group conflict) in a not-always-stable order, which made tox-uv-bare's internal uv sync --locked check fail nondeterministically depending on which uv version last touched the lockfile. Fixed by (a) listing all of the transitively-conflicting groups (test, doc, quality, dev) explicitly in [tool.uv].conflicts instead of relying on derivation, and (b) pinning the uv package version itself via [tool.edx_lint].uv_constraints to match the version: pin on the astral-sh/setup-uv step in ci.yml. If a future make upgrade bumps the locked uv version, bump the setup-uv step's version: input to match (see comments in pyproject.toml/ci.yml).
  • The old py311-only commands_pre step in tox.ini (make upgrade + manual requirements/test.txt install) is gone; it's no longer needed since uv.lock drives every env's dependencies identically regardless of Python version.

This PR was created with Claude Code.

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.
@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Jul 27, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @irfanuddinahmad!

This repository is currently maintained by @openedx/committers-openedx-filters.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 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.
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Ready for Review in Contributions Jul 27, 2026
@mphilbrick211
mphilbrick211 requested a review from a team July 27, 2026 21:42
irfanuddinahmad and others added 4 commits July 28, 2026 02:45
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Ready for Review

Development

Successfully merging this pull request may close these issues.

3 participants