feat: pip-compile -> uv migration - #38915
Conversation
|
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. |
|
Re: the Good catch, and worth noting: the org already ran a dedicated SHA-pinning sweep for this exact reason (openedx/.github#165, prompted by the tj-actions/changed-files supply-chain incident), but openedx-platform wasn't part of that ~121-repo effort. That said, I don't think pinning just |
ecd4ab7 to
362c94b
Compare
| "libsass", | ||
| "nodeenv", | ||
| ] | ||
| development = [ |
There was a problem hiding this comment.
Minor naming consistency issue: the group names aren't uniform. You have doc (abbreviated) alongside development (full word)
There was a problem hiding this comment.
Fair point — renamed doc to docs to match development's un-abbreviated naming (and the docs/ folder). Updated its one internal reference plus .readthedocs.yaml, which also referenced it by name. Fixed in 6d4d18a.
There was a problem hiding this comment.
We still have both development and dev groups. I think having both is a bit confusing. Could we use more consistent naming?
There was a problem hiding this comment.
Good point -- renamed the dev group to default (added explicit tool.uv.default-groups = ["default"] to preserve bare uv sync's current behavior, since that was relying on uv's built-in "dev" default-group name). Now only development remains, wrapped by default = development + ci. Fixed in a1d3ea1.
There was a problem hiding this comment.
Fixed -- consolidated to just development, no more separate dev group.
There was a problem hiding this comment.
doc.in has been moved to docs altering the name not in unparity with master
Headup on it if it's under consideration
There was a problem hiding this comment.
Already decided, not overlooked -- see salman2013's thread above (3656743218): renamed doc->docs on 2026-07-27 to match development's un-abbreviated style.
|
@irfanuddinahmad This commit changes are not related to PR so either they raised in conflicts resolution or AI tried to fix something by own. |
| - name: Install Python dependencies | ||
| run: | | ||
| pip install -r requirements/edx/coverage.txt | ||
| pip install coverage diff-cover |
There was a problem hiding this comment.
why we are using pip here?
There was a problem hiding this comment.
Fixed -- this now uses uv sync --no-default-groups --only-group coverage --no-install-project --frozen, picking up the coverage dependency-group already defined in pyproject.toml for exactly this purpose. Restores the pinning that got lost when this was a bare pip install. See 1c592ea.
|
Requested a few nits. The PR looks to be in good shape now. 👏👏👏 💡 Major thing to discuss is what strategy do we follow for operators to migrate to uv from old requirements.
|
|
Re: the fast-track DEPR suggestion -- openedx/public-engineering#552 already covers this (filed 2026-07-22). But the transition can't actually start without this PR. |
|
Recent testing on this branch, for the record:
|
|
Looks like this needs to be rebased but then I think it's good to merge. |
e780a7a to
5a94fdb
Compare
|
Thanks. Opened #39023 to drop it. |
Summary
Migrates edx-platform from pip-compile/pip-tools to
pyproject.toml+uv, tracked in openedx/public-engineering#543.Environments / install targets
make base-requirements->pip-sync base.txt(=kernel.in+bundled.in, which pulls ingithub.in)make base-requirements->uv sync --no-default-groups --group bundled --frozen([project.dependencies]mergeskernel+github, and installs on everyuv syncregardless of--group, so--group bundledalone reproduces the oldbase.txt)make dev-requirements->pip-sync development.txt(=pip-tools.txt+testing.txt+doc.txt+assets.txt+ dev-own deps)make dev-requirements->uv sync --group development --group ci --frozen(developmentitself includestesting+docs+assets).pip-toolsitself is dropped, not needed under uv.make test-requirements->pip-sync testing.txt(=base.txt+coverage.txt+ testing-own deps, includingtox)make test-requirements->uv sync --no-default-groups --group testing --frozen(testingincludescoverage+bundled). Note:toxmoved out oftestinginto the newcigroup, sotest-requirementsno longer pulls intoxthe waytesting.txtused to -- intentional (this target runs pytest, not tox itself), but a real composition change, not just a rename.Old file -> new equivalent
requirements/edx/kernel.in[project.dependencies]requirements/edx/github.in[project.dependencies](merged withkernel.in)requirements/edx/bundled.in[dependency-groups].bundledrequirements/edx/testing.in[dependency-groups].testing--{bundled}+{coverage}+ own deps, minustox(see above)requirements/edx/doc.in[dependency-groups].docs--{bundled}+ own deps (doc.indid-r base.txt, i.e. kernel + bundled, anddocs/conf.pyneeds bundled XBlocks likeopenedx-forum/ora2importable to build at all)requirements/edx/assets.in[dependency-groups].assetsrequirements/edx/development.in[dependency-groups].development--{testing}+{docs}+{assets}+ own deps (pip-toolsdropped)requirements/edx/coverage.in[dependency-groups].coveragerequirements/edx/semgrep.inscripts/semgrep/{pyproject.toml,uv.lock}-- not a root dependency-group, see belowrequirements/edx/openstack.txt[project.optional-dependencies].openstack(a PEP 621 extra, not a[dependency-groups]entry -- installable viapip install openedx-platform[openstack])requirements/constraints.txt+requirements/common_constraints.txt[tool.edx_lint].uv_constraints(hand-maintained) +[tool.uv].constraint-dependencies(machine-generated from it)requirements/pip-tools.{in,txt}requirements/edx/private.in/private.txtuv sync --inexact-- seerequirements/README.rstfor the exact workflowrequirements/edx/testing.in'stoxentry[dependency-groups].ci, alongside newtox-uv(needed fortox.ini'suv-venv-lock-runner) --make dev-requirementspasses--group ciexplicitly alongside--group developmentsotoxstays available for local dev[dependency-groups].django42/django52: a real, independently-resolved Django-version test matrix, not a runtime patch on an already-resolved venvrequirements/edx/{base,assets,development}.txtand the 5 sub-projects' compat.txtfiles stay as machine-generateduv exportoutputs at their historical paths (for external tooling, e.g. Tutor's Dockerfile, that still installs from them directly) -- see the file-by-file breakdown below for exactly what each one exports.File-by-file summary
Root packaging & build config
[project.dependencies]list (previously just["setuptools"]), a new[dependency-groups]tree (coverage,bundled,testing,docs,assets,development,django42,django52,ci),[tool.uv].default-groups/constraint-dependencies(machine-managed byedx_lint write_uv_constraints),[tool.edx_lint].uv_constraints(the hand-maintained version pins, each with a dated comment and issue link, carried over from the oldconstraints.txt), and a new[tool.coverage.*]tree.django42/django52: a real, independently-resolved Django-version test matrix, not a runtime patch on an already-resolved venv --[tool.uv].conflictsdeclares the two mutually exclusive souv.lockcarries a genuinely separate resolution for each (confirmed:django42locks Django 4.2.30,django52/testinglocks 5.2.x, with real transitive-dependency divergence between the two, not just a Django-version override).[tool.uv].default-groups: an earlier version of this PR set this to["development", "ci"]so a bareuv syncwould install them, and briefly introduced adefaultgroup that just wrappeddevelopment+cifordefault-groupsto point at (removed as pure indirection --default-groupsaccepts real group names directly). Both were reverted after landing:default-groupsis now explicitly[](uv's own built-in fallback is["dev"], not "nothing", so this has to be set explicitly rather than omitted) because RTD's docs build has no way to opt out of default-groups and was silently installing the full dev/CI toolchain on every build as a result (see.readthedocs.yamlbelow). Every consumer that wants development+ci now passes--group development --group ciexplicitly.upgrade-one-python-dependency.ymlworkflow uses to bump a version pin in[tool.edx_lint].uv_constraintsvia a propertomlkitTOML round-trip, instead ofsed-patching a text file.[tool.coverage.*]in pyproject.toml.pre-requirements/pip-synctargets replaced withuv sync --group ...targets;compile-requirementsrewritten to runedx_lint write_uv_constraints->uv lockfor the root project (and the same lock step for each of the 5 uv-managed sub-projects), then re-export compatibility.txtfiles at the old paths for external tools (e.g. Tutor's Dockerfile) that stillpip install -r requirements/edx/base.txtdirectly. This export logic briefly lived in a separatescripts/compile_requirements_exports.pyPython script, but per feanil's request it's back in the Makefile as inline shell -- visibility into the exact commands being run mattered more here than the script's structured-data abstraction (verified byte-identical output before switching either way).runner = uv-venv-lock-runner+dependency_groups = testing; droppedqualityfromenvlist(quality now runs via a dedicated CI workflow/Makefile target, not tox) and the now-redundantusedevelop/commands_pre = make test-requirements.compile-sass/compile-sass-devnow runuv run python scripts/compile_sass.py ...instead of the bare executable (which relied on its own#!/usr/bin/env pythonshebang finding the right Python onPATH) -- this wayuvstays scoped to the actual Python invocation, and every caller ofnpm run build/build-dev(CI,scripts/watch_sass.sh, local dev) gets the right environment without needing to know to wrap the outernpmcommand itself.pip install -r requirements/edx/*.txttouv sync --group ....CI workflows
All follow the same mechanical pattern: drop the manual
pip cachesteps in favor ofastral-sh/setup-uv's built-in cache (enable-cache: true), and replace every barepip/pylint/mypy/./manage.py/etc. invocation withuv run <tool>-- not a$GITHUB_PATHsplice. An earlier draft of this PR did splice.venv/binonto$GITHUB_PATHjob-wide instead of fixing individual invocations; that approach is gone everywhere now except one line instatic-assets-check.ymlthat's unrelated to Python (node_modules/.bin, needed by npm-installed JS tooling).check-consistent-dependencies.yml: trigger-detection regex extended to also watchpyproject.toml/uv.lock/sub-project files, not justrequirements/.check_python_dependencies.yml: re-enabled (was temporarily disabled earlier in this PR's history pendingedx-repo-tools[find_dependencies]gaining the ability to scanuv.lockinstead of.in/.txtfiles -- that upstream fix, openedx/repo-tools#735, has since merged). Now runs viauvx --from 'edx-repo-tools[find_dependencies]' find_python_dependencies --req-file uv.lock ..., no persistent install, noactions/setup-python.ci-static-analysis.yml,js-tests.yml,lint-imports.yml,migrations-check.yml,pylint-checks.yml,quality-checks.yml,semgrep.yml,unit-tests.yml: mechanical pip->uv swap as described above.compile-python-requirements.yml,upgrade-one-python-dependency.yml: updated to operate onpyproject.toml/uv.lockinstead of.in/.txt/constraints.txt. The dependency-downgrade script now edits[tool.edx_lint].uv_constraintsvia a propertomlkitTOML round-trip instead ofsed-patching a text file. Both manually triggered and verified against this branch:compile-python-requirementsranmake compile-requirements, detected no diff, and correctly exited without opening a PR;upgrade-one-python-dependency(package=django-filter) opened a PR with exactly the expecteduv.lock+ compat-export changes.static-assets-check.yml: same pip->uv swap, scoped the Python-deps-install step touv sync --no-default-groups --group bundled --group assets --frozen(rather than pulling in the fulldevelopment+ciset this job doesn't need).npm run build-dev/npm run buildare called bare -- thepackage.jsonchange above is what getsscripts/compile_sass.pythe right Python, not this workflow.migrations-check.yml,unit-tests.yml: the non-pinnedDjango-version matrix entries nowuv sync --group testing --group django$VERSION --frozeninstead of bare-pip installing a different Django version on top of an already-resolved venv (which relied onpip checkas the only safety net for anything transitively broken). This resolves the whole environment fresh for that Django line -- confirmeddjango-filtergenuinely resolves differently underdjango42(25.1) vsdjango52(25.2), a real transitive divergence the old patch-on-top approach would have masked.units-test-scripts-structures-pruning.yml,units-test-scripts-user-retirement.yml: intentionally untouched -- they stillpip install -r scripts/.../requirements/testing.txt, and those compatibility files continue to be regenerated (viauv export) at the same paths, so these two workflows keep working unmodified.Documentation
.in/.txtworkflow, including how to replace the droppedprivate.inmechanism withuv sync --inexact, and how the 5 standalone sub-project directories (requirements/edx-sandbox,scripts/xblock,scripts/user_retirement,scripts/structures_pruning,scripts/semgrep) each carry their own independentpyproject.toml+uv.lock.[dependency-groups]entry -- semgrep's own dependency chain (viawcmatch) is incompatible with other root-project dependencies (e.g.openedx-authz'spycasbinpin) when resolved together in one shared graph.master's oldpip-compile-based setup avoided this by compilingsemgrep.txtas a fully separate run; the rootuv.lockneeds the same isolation, which only a genuinely separate sub-project (likerequirements/edx-sandbox/scripts/xblockalready were) provides.uv syncequivalents, scoped to the specific dependency-groups each workflow step actually needs.python.install: [{method: uv, command: sync, groups: [docs]}]), reading directly fromuv.lock-- this also installs the local package itself as an editable install automatically.groups: [docs](not--only-group docs) so[project.dependencies](Django/XBlock/etc.) resolves againstuv.lock's pins instead of an unconstrainedsetuptools>=82that breaksfs/pyfilesystem2'spkg_resourcesimport;docsalso directly includes thebundledgroup (matchingdoc.in's old-r base.txt), sincedocs/conf.pyimportscms.envs.common, which needs bundled XBlocks importable to succeed. Fixed a real regression found after initial landing: RTD'sgroups: [docs]maps touv sync --group docs, which -- with[tool.uv].default-groupspreviously set to["development", "ci"]-- also implicitly installed the full dev/CI toolchain on every docs build (confirmed empirically: 401 packages vs. 335 for a docs-only install), and RTD's config has no way to pass--no-default-groups. Fixed at the source instead of working around it in RTD config:default-groupsis now explicitly[], with every consumer that wants development+ci (make dev-requirements, thedevelopment.txtcompat export) passing--group development --group ciexplicitly. Verified against Read the Docs' actual build (not just locally) before landing, and re-verified after this fix.Dependency files (requirements/, uv.lock, scripts/*)
requirements/edx/*.in(all deleted): superseded by[project.dependencies]/[dependency-groups]in pyproject.toml.requirements/edx/base.txt,assets.txt,development.txt(kept, regenerated): machine-generateduv exportcompatibility exports at their historical paths for external tooling that still installs from them directly.base.txtexports[project.dependencies]plus thebundledgroup.requirements/edx/coverage.{in,txt},doc.{in,txt},testing.{in,txt},semgrep.{in,txt},bundled.in,github.in,kernel.in(all deleted): fully absorbed into pyproject.toml dependency-groups (or, forsemgrep, its own standalone sub-project); no external tooling installed from these paths directly, so no compatibility export was needed.requirements/edx/openstack.txt(deleted): absorbed into[project.optional-dependencies].openstack-- a PEP 621 extra, not a[dependency-groups]entry.requirements/edx/private.readme(deleted): documented theprivate.in/private.txtmechanism, which itself was never checked into git (git-ignored). No direct successor file -- see the mapping table above for theuv sync --inexactreplacement workflow.requirements/common_constraints.txt,requirements/constraints.txt(deleted): absorbed into[tool.uv].constraint-dependencies(machine-managed) and[tool.edx_lint].uv_constraints(hand-maintained) in pyproject.toml, with all original rationale comments and issue links preserved.requirements/pip-tools.{in,txt}(deleted): pip-tools itself is no longer needed.uv.lock,requirements/edx-sandbox/{pyproject.toml,uv.lock},scripts/{xblock,user_retirement,structures_pruning,semgrep}/{pyproject.toml,uv.lock}(new): machine-generated lockfiles for the root project and its 5 independent standalone sub-projects (codejail sandbox, XBlock scripts, user-retirement scripts, structures-pruning scripts, semgrep). Each sub-project sets[tool.uv] package = false(they're script bags, not installable packages) and carries only the subset of root constraints relevant to its own deps.scripts/*/requirements/{base,testing}.txt(kept, regenerated): same compatibility-export treatment asrequirements/edx/*.txt.Misc scripts
uv sync --no-default-groups --only-group assets --no-install-project), verified to actually compile Sass correctly with that reduced environment.pip install -r ...message updated touv sync --group development(the group that actually containswatchdog/watchmedo)..venvto the linter's skip-dirs list.What's intentionally NOT done here (tracked externally)
requirements/edx/{base,assets,development}.txtwith plainpip. Those stay asuv exportcompatibility artifacts rather than being deleted, so no action is required on Tutor's side right now -- but Tutor maintainers should be aware these paths are now machine-generated, not hand-compiled. Verifiedtutor images build openedx-devsucceeds against this branch ontutor22.0.0/Verawood (Python 3.12); the previous named release's Python 3.11 base can't satisfy this migration'srequires-python = ">=3.12"at all, independent of anything in this PR.Verification
uv lockresolves cleanly (419 packages, root project); all 5 sub-projects (requirements/edx-sandbox,scripts/xblock,scripts/user_retirement,scripts/structures_pruning,scripts/semgrep) sync cleanly withuv sync --frozen.make compile-requirementsend-to-end for the root project and all 5 uv sub-projects -- regenerated compatibility export files are byte-identical to what's committed, confirming consistency.compile-python-requirementsandupgrade-one-python-dependency(bothworkflow_dispatch-only, so they don't run automatically on this PR) against this branch -- see the CI workflows section above.docs/readthedocs.orgagainst the real Read the Docs build andtutor images build openedx-devagainst a local Verawood (Python 3.12) environment.🤖 Generated with Claude Code