Using rust-based python tooling moving forward

I’ve become a huge fan of uv and ruff as tools for managing python code/projects.

  • uv is “A single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more.”
    • Instead of dealing with requirements.in+.txt files and pip-compile, you just specify dependencies in the standard pyproject.toml file, and prefix commands with uv run. You don’t have to worry about virtualenvs or python versions, as uv handles that all transparently.
  • ruff is a linter and code formatter that replaces pylint, pycodestyle, black, etc.
    • Since it’s written in Rust, it’s way, way faster than pylint.
  • In the future, I’d probably also recommend ty instead of mypy because it’s way faster, but it’s not mature enough yet.

I’d like to propose we standardize on these tools for all OpenCraft python projects, and also recommend that Open edX folks migrate toward them over time as well.

I’ve opened some PRs for our docs, where you can see the (fairly minimal) changes required, and how it mostly simplifies things:

and

Any thoughts? I also need reviewers for these MRs.

Ticket to log time: Log in - OpenCraft

9 Likes

Oh yeah, and on frontend projects I highly recommend using oxlint instead of eslint, and node:test instead of Jest. Both are so much faster and simpler.

I’ve been discussing this with Open edX frontend folks, and there’s strong interest in migrating to them (especially replacing Jest), but it’s going to be a big project.

2 Likes

A big +1 from me on uv and ruff - I’ve only ever had excellent experiences with them. :grin:

+1 to standardizing and recommending upstream. It would be nice to move away from the myriad of requirements*.{in,txt} files and get other ergonomic improvements in general. :+1: :+1:

1 Like

I’m happy to review these too. :slight_smile:

@braden I’ve been using uv a lot personally as well, so I’m on board for this change.

I’d like to mention another change while we’re at it. I’ve started using hatch as the build tool and it also allows replacing tox and make entirely by having support for multiple environments and scripts I think uv also has a build tool now be AFAIK hatch can use uv, and has been around for longer and aims to be standards compliant.

I switched a recent platform plugin to use hatch instead of tox and here is the pyproject.toml. With that file you can run hatch test:test or hatch run test:converge, you can upgrade dependencies with hatch run requirements:compile and hatch run requirements:upgrade which still use pip-tools for backwards compatibility.

1 Like

I have heard good opinions about https://pyrefly.org/. You have a look at it as well.

1 Like

Thanks for sharing @braden!

Speaking of tooling, do you know if there is any reason not to use pnpm over npm on the node side for openedx?

1 Like

+1 to this. A few months ago, I switched this repo to use uv for dependency management and package publishing, and it’s working well (the only side effect is that the automatic dependency PRs do not have the changelog in their description due to a different format, but it’s not a big deal). It’s also using ruff for both linting and formatting. Additionally, I replaced Makefile with mise, as it makes writing more complex helper scripts (like this one) much easier.

As a side note, I also use mise for local development. E.g., I have the following helpers defined for my devstacks (these are just personal snippets, so they are a bit rough). They allow installing local deps from a mounted volume with commands like mise run install openedx-ltistore and running tests with mise test lms/djangoapps/courseware/tests/test_access.py.

[tasks.install]
description = "Install an XBlock or a plugin."
run = """
#!/usr/bin/env bash
XBLOCK="{{ arg(name="xblock", var=true) }}"

# If the argument starts with git+https, use it as-is; otherwise, prepend the shared source path.
if [[ "$XBLOCK" == git+https* ]]; then
  INSTALL_ARG="$XBLOCK"
else
  INSTALL_ARG="-e /openedx/shared-src/$XBLOCK"
fi

tutor dev exec lms pip install $INSTALL_ARG
tutor dev restart lms
tutor dev exec cms pip install $INSTALL_ARG
tutor dev restart cms
"""

[tasks.test]
description = "Runs a specific pytest path, setting the correct environment variables."
run = """
#!/usr/bin/env bash
set -euo pipefail

# Get the required test path argument from the 'mise run' command.
TEST_PATH="{{ arg(name="path", var=true) }}"

# Exit if the TEST_PATH argument is missing.
if [[ -z "$TEST_PATH" ]]; then
    echo "Error: A test path must be provided." >&2
    echo "Usage: mise run test -- path/to/your/test" >&2
    exit 1
fi

# Run the test script inside the container.
# The -T flag is required to prevent the 'not a TTY' error.
tutor dev exec -T lms bash -s <<'EOF' "$TEST_PATH"
set -euxo pipefail

# This is the test path passed from the outer script.
INNER_TEST_PATH="$1"

echo "--- Preparing environment for test path: $INNER_TEST_PATH ---"

# Set environment variables based on the beginning of the test path.
case "$INNER_TEST_PATH" in
  lms/*)
    echo "Setting environment for LMS test..."
    export DJANGO_SETTINGS_MODULE=lms.envs.tutor.test
    unset SERVICE_VARIANT
    ;;
  cms/*)
    echo "Setting environment for CMS test..."
    export DJANGO_SETTINGS_MODULE=cms.envs.tutor.test
    unset SERVICE_VARIANT
    ;;
  *)
    echo "Setting environment for common/shared tests..."
    unset DJANGO_SETTINGS_MODULE
    unset SERVICE_VARIANT
    ;;
esac

echo "--- Running pytest with colors: $INNER_TEST_PATH ---"

# Use the 'script' command to trick pytest into thinking it is running
# in an interactive terminal (TTY), which forces it to generate color.
#
# -q: Quiet mode, suppresses 'script' start/end messages.
# -e: Returns the exit code of the child process (pytest).
# -c '...': Specifies the command to run.
# /dev/null: Discards the log file.
script -q -e -c "pytest --color=yes -p no:warnings '$INNER_TEST_PATH'" /dev/null
# script -q -e -c "pytest --color=yes -p no:warnings '$INNER_TEST_PATH' -k 'test_enforce_masquerade_start_dates_flag'" /dev/null
# NOTE: Add `-x` to fail fast.
#       Add `-k 'some_name` to run all parametrized tests with `some_name`.

echo "--- Test run finished ---"
EOF
"""
1 Like

Yeah, Pyrefly and Ty are the two “new generation” type checkers that aim to replace mypy and pyright. Pyrefly and Ty are kind of competing with each other, but the maintainers gave a joint talk at [Pycon?] when they announced them, so they’re also somewhat collaborative. I hope that one or both of them succeed because the current tools aren’t great. I also hope they can push to standardize type checking better.

Nice, looks great for projects that need a build tool or multiple environments. I haven’t tried it myself.

Yeah, mise is awesome. Our onboarding course mentions it for devstack management.

I’ve found there are sometimes incompatibilities between the different package managers, so it’s usually best to stick to the same one everyone else is using on any giving project. You can definitely use pnpm to install dependencies on an MFE, for example, if it works for you; just make sure not to commit any package*.json changes generated by pnpm which may be different than those generated by NPM.

For our own projects, it could make sense to recommend pnpm, but I don’t have a lot of experience with it.

2 Likes

I may have jumped the gun here with offering to review sorry - I didn’t realise it’s in this sprint, and with my lowered availability this week I need to focus on other things. :confused:

Looks like @Fox has already started reviewing :slight_smile:

+1 from me on migrate to uv and ruff :rocket: :rocket:

+1 to this too. If the project takes off, I can help :grin:

1 Like