diff --git a/.circleci/test-deploy.yml b/.circleci/test-deploy.yml index e9a6af0..8746b5f 100644 --- a/.circleci/test-deploy.yml +++ b/.circleci/test-deploy.yml @@ -139,6 +139,18 @@ jobs: working_directory: ~/project/sample_poetry command: |- poetry run pytest + uv-test: + executor: python/default + steps: + - checkout + - python/install-packages: + app-dir: ~/project/sample_uv + cache-version: << pipeline.parameters.cache-version >> + pkg-manager: uv + - run: + working_directory: ~/project/sample_uv + command: |- + uvx run pytest workflows: test-deploy: jobs: @@ -155,6 +167,8 @@ workflows: filters: *filters - poetry-test: filters: *filters + - uv-test: + filters: *filters - dist-test: name: "dist-test-wheel" filters: *filters @@ -180,6 +194,19 @@ workflows: name: Verify cache was successful working_directory: ~/project/sample_poetry command: 'cat install_output.txt | grep "No dependencies to install or update"' + - python/test: + filters: *filters + name: job-test-uv + pkg-manager: uv + cache-version: uv-<< pipeline.parameters.cache-version >> + args: "| tee install_output.txt" + include-branch-in-cache-key: false + app-dir: ~/project/sample_uv + post-steps: + - run: + name: Verify cache was successful + working_directory: ~/project/sample_uv + command: 'cat install_output.txt | grep "No dependencies to install or update"' - python/test: filters: *filters name: job-test-pipenv @@ -213,6 +240,18 @@ workflows: name: Verify cache was successful working_directory: ~/project/sample_poetry command: 'cat install_output.txt | grep "No dependencies to install or update"' + - python/test: + filters: *filters + name: job-auto-test-uv + cache-version: uv-auto-<< pipeline.parameters.cache-version >> + args: "| tee install_output.txt" + include-branch-in-cache-key: false + app-dir: ~/project/sample_uv + post-steps: + - run: + name: Verify cache was successful + working_directory: ~/project/sample_uv + command: 'cat install_output.txt | grep "No dependencies to install or update"' - python/test: filters: *filters name: job-auto-test-pipenv @@ -286,13 +325,16 @@ workflows: - pip-install-test-args - pipenv-test - poetry-test + - uv-test - pip-install-rel-dir - job-test-poetry + - job-test-uv - job-test-pipenv - job-test-pip - job-test-pip-dist - job-test-pip-dist-pyproject - job-auto-test-poetry + - job-auto-test-uv - job-auto-test-pipenv - job-auto-test-pip - dist-test-wheel diff --git a/sample_uv/pyproject.toml b/sample_uv/pyproject.toml new file mode 100644 index 0000000..a67d172 --- /dev/null +++ b/sample_uv/pyproject.toml @@ -0,0 +1,17 @@ +# Example toml for integration testing - this is not used by the orb in anyway +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +authors = ["Test"] +name = "test" +description = "none" +version = "0.0.1" +requires-python = ">=3.9" +dependencies = [] + +[dependency-groups] +dev = [ + "pytest" +] diff --git a/sample_uv/tests/__init__.py b/sample_uv/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample_uv/tests/test_job.py b/sample_uv/tests/test_job.py new file mode 100644 index 0000000..9969986 --- /dev/null +++ b/sample_uv/tests/test_job.py @@ -0,0 +1,7 @@ + +import unittest + +class TestCase(unittest.TestCase): + + def test_true(self): + assert True diff --git a/sample_uv/uv.lock b/sample_uv/uv.lock new file mode 100644 index 0000000..742e4dd --- /dev/null +++ b/sample_uv/uv.lock @@ -0,0 +1,88 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "test" +version = "0.0.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest" }] + +[[package]] +name = "tomli" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, +] diff --git a/src/commands/install-packages.yml b/src/commands/install-packages.yml index 803aa56..104a180 100755 --- a/src/commands/install-packages.yml +++ b/src/commands/install-packages.yml @@ -7,7 +7,7 @@ description: > parameters: pkg-manager: type: enum - enum: [auto, poetry, pipenv, pip, pip-dist] + enum: [auto, poetry, pipenv, pip, pip-dist, uv] default: auto description: Which package management tool to use, pipenv, pip or poetry with dependency file. Use `pip-dist` to install with project setup.py or PEP621 (pyproject.toml). path-args: @@ -143,6 +143,16 @@ steps: working_directory: << parameters.app-dir >> command: | poetry install --no-ansi << parameters.args >> + - when: + condition: + equal: [uv, << parameters.pkg-manager >>] + steps: + - run: + name: "Install dependencies with uv using project pyproject.toml and uv.lock" + no_output_timeout: << parameters.no_output_timeout >> + working_directory: << parameters.app-dir >> + command: | + uv sync << parameters.args >> - when: condition: # if pip == pkgmanager and args != "" or pip-dependency-file != "" diff --git a/src/examples/work-with-uv.yml b/src/examples/work-with-uv.yml new file mode 100644 index 0000000..d3857db --- /dev/null +++ b/src/examples/work-with-uv.yml @@ -0,0 +1,23 @@ +description: | + An example of working with the uv cache on CircleCI to speed up builds. +usage: + version: 2.1 + orbs: + python: circleci/python@3.0.0 + workflows: + main: + jobs: + - build + jobs: + build: + executor: python/default + steps: + - checkout + - python/install-packages: + pkg-manager: uv + - run: + name: "Test it" + # pytest would have to be defined in pyproject.toml + # inline packages are not allowed with uv sync + command: | + uv run pytest --version diff --git a/src/jobs/test.yml b/src/jobs/test.yml index 84a508d..d2e620f 100644 --- a/src/jobs/test.yml +++ b/src/jobs/test.yml @@ -11,13 +11,13 @@ parameters: The name of executor to use. pkg-manager: type: enum - enum: [auto, pip, pipenv, poetry, pip-dist] + enum: [auto, pip, pipenv, poetry, pip-dist, uv] default: "auto" description: Select the package manager to use. Default is pip pip-dependency-file: type: string default: requirements.txt - description: Name of the requirements file that needs to be installed with pip. Prepended with `app-dir`. If using pipenv or poetry, this is ignored. + description: Name of the requirements file that needs to be installed with pip. Prepended with `app-dir`. If using pipenv, poetry or uv, this is ignored. app-dir: type: string default: "~/project" @@ -33,7 +33,7 @@ parameters: args: type: string default: "" - description: Arguments to pass to install command for pipenv and poetry. Override '-r requirements.txt' for pip. + description: Arguments to pass to install command for pipenv, poetry or uv. Override '-r requirements.txt' for pip. setup: type: steps description: Provide any optional steps you would like to run prior to install the python project. @@ -144,6 +144,7 @@ steps: or: - equal: [poetry, << parameters.pkg-manager >>] - equal: [pipenv, << parameters.pkg-manager >>] + - equal: [uv, << parameters.pkg-manager >>] steps: - run: name: Run tests with <> run @@ -176,6 +177,7 @@ steps: or: - equal: [poetry, << parameters.pkg-manager >>] - equal: [pipenv, << parameters.pkg-manager >>] + - equal: [uv, << parameters.pkg-manager >>] steps: - run: name: Run tests with <> run diff --git a/src/scripts/auto-install-command.sh b/src/scripts/auto-install-command.sh index 46764d4..b938f9d 100755 --- a/src/scripts/auto-install-command.sh +++ b/src/scripts/auto-install-command.sh @@ -4,13 +4,18 @@ source "$AUTO_DETECT_ENV_SCRIPT" case ${DETECT_PKG_MNGR:-${PARAM_PKG_MNGR}} in pip) PYTHON_INSTALL_ARGS="-r ${PARAM_DEPENDENCY_FILE:-requirements.txt}" + eval "${PYTHON_ENV_TOOL:-pip} install ${PYTHON_INSTALL_ARGS} ${PARAM_ADDITIONAL_ARGS}" ;; pip-dist) PYTHON_INSTALL_ARGS="-e ${PARAM_PATH_ARGS}" + eval "${PYTHON_ENV_TOOL:-pip} install ${PYTHON_INSTALL_ARGS} ${PARAM_ADDITIONAL_ARGS}" ;; poetry) PYTHON_INSTALL_ARGS="--no-ansi" + eval "poetry install ${PYTHON_INSTALL_ARGS} ${PARAM_ADDITIONAL_ARGS}" + ;; + uv) + PYTHON_INSTALL_ARGS="" + eval "uv sync ${PYTHON_INSTALL_ARGS} ${PARAM_ADDITIONAL_ARGS}" ;; esac - -eval "${PYTHON_ENV_TOOL:-pip} install ${PYTHON_INSTALL_ARGS} ${PARAM_ADDITIONAL_ARGS}" \ No newline at end of file diff --git a/src/scripts/cache-link-lockfile.sh b/src/scripts/cache-link-lockfile.sh index d53ee00..6bb3f4e 100755 --- a/src/scripts/cache-link-lockfile.sh +++ b/src/scripts/cache-link-lockfile.sh @@ -17,6 +17,9 @@ if [ ! -f "${LOCKFILE_PATH}" ]; then poetry) LOCK_FILE="poetry.lock" ;; + uv) + LOCK_FILE="uv.lock" + ;; esac if [ -z "${LOCK_FILE}" ]; then diff --git a/src/scripts/cache-save.sh b/src/scripts/cache-save.sh index 951b9db..870ea35 100755 --- a/src/scripts/cache-save.sh +++ b/src/scripts/cache-save.sh @@ -23,6 +23,11 @@ case ${DETECT_PKG_MNGR:-${PARAM_PKG_MNGR}} in VENV_PATHS='[ "/home/circleci/.cache/pypoetry/virtualenvs" ]' CACHE_PATHS='[ "/home/circleci/.cache/pip" ]' ;; + uv) + LOCK_FILE="uv.lock" + VENV_PATHS="[ \"${CIRCLE_WORKING_DIRECTORY}/.venv\" ]" + CACHE_PATHS='[ "/home/circleci/.cache/uv" ]' + ;; esac if [ -n "${PARAM_VENV_PATH}" ]; then diff --git a/src/scripts/detect-env.sh b/src/scripts/detect-env.sh index f4d9d95..070d548 100755 --- a/src/scripts/detect-env.sh +++ b/src/scripts/detect-env.sh @@ -8,6 +8,9 @@ if [ "${PARAM_PKG_MNGR}" = "auto" ]; then elif [ -f "Pipfile" ]; then export DETECT_PKG_MNGR="pipenv" export PYTHON_ENV_TOOL="pipenv" + elif [ -f "uv.lock" ]; then + export DETECT_PKG_MNGR="uv" + export PYTHON_ENV_TOOL="uv" elif [ -f "pyproject.toml" ]; then export DETECT_PKG_MNGR="poetry" export PYTHON_ENV_TOOL="poetry" diff --git a/src/scripts/ensure-test-tool.sh b/src/scripts/ensure-test-tool.sh index 80d359a..2f53a40 100755 --- a/src/scripts/ensure-test-tool.sh +++ b/src/scripts/ensure-test-tool.sh @@ -17,6 +17,10 @@ case ${DETECT_PKG_MNGR:-${PARAM_PKG_MNGR}} in REQUIREMENTS_PATH="pyproject.toml" PYTHON_ENV_TOOL="poetry" ;; + uv) + REQUIREMENTS_PATH="uv.lock" + PYTHON_ENV_TOOL="uv" + ;; esac if [ -f ${REQUIREMENTS_PATH} ]; then @@ -39,8 +43,13 @@ if [ "${PARAM_TEST_TOOL}" != "unittest" ]; then # If the test package is not detected, install using PYTHON_INSTALL_TOOL if [ -z "$DETECT_TEST_TOOL" ]; then echo "INFO: Test package ${PARAM_TEST_TOOL} was not found. Installing..." - eval "${PYTHON_ENV_TOOL:-pip} install ${PYTHON_INSTALL_ARGS} ${PARAM_TEST_TOOL}" - INSTALL_RESULT=$? + if [ "$PYTHON_ENV_TOOL" = "uv" ]; then + eval "uv add ${PYTHON_INSTALL_ARGS} ${PARAM_TEST_TOOL}" + INSTALL_RESULT=$? + else + eval "${PYTHON_ENV_TOOL:-pip} install ${PYTHON_INSTALL_ARGS} ${PARAM_TEST_TOOL}" + INSTALL_RESULT=$? + fi else echo "INFO: Detected test package: $DETECT_TEST_TOOL" fi diff --git a/src/scripts/export-detect-env.sh b/src/scripts/export-detect-env.sh index 13316f9..ff71bea 100644 --- a/src/scripts/export-detect-env.sh +++ b/src/scripts/export-detect-env.sh @@ -9,6 +9,9 @@ echo 'if [ "${PARAM_PKG_MNGR}" = "auto" ]; then elif [ -f "Pipfile" ]; then export DETECT_PKG_MNGR="pipenv" export PYTHON_ENV_TOOL="pipenv" + elif [ -f "uv.lock" ]; then + export DETECT_PKG_MNGR="uv" + export PYTHON_ENV_TOOL="uv" elif [ -f "pyproject.toml" ]; then export DETECT_PKG_MNGR="poetry" export PYTHON_ENV_TOOL="poetry"