Skip to content

Commit 4f4d4f3

Browse files
committed
feat: add session sync for uv
1 parent d7072e3 commit 4f4d4f3

File tree

3 files changed

+154
-4
lines changed

3 files changed

+154
-4
lines changed

docs/tutorial.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,32 @@ You can also pass environment variables:
252252
See :func:`nox.sessions.Session.run` for more options and examples for running
253253
programs.
254254

255+
Using uv to manage your project
256+
-------------------------------
257+
258+
While the ``session.run_install`` can use ``uv`` as backend, it is also
259+
possible to sync your project with ``session.sync()``. Nox will handle
260+
the virtual env for you.
261+
262+
A sync will not remove extraneous packages present in the environment.
263+
264+
.. code-block:: python
265+
266+
@nox.session
267+
def tests(session):
268+
session.sync()
269+
session.install("pytest")
270+
session.run("pytest")
271+
272+
If you work with workspaces, install only given packages.
273+
274+
.. code-block:: python
275+
276+
@nox.session
277+
def tests(session):
278+
session.sync(package="mypackage")
279+
session.run("mypackage")
280+
255281
Selecting which sessions to run
256282
-------------------------------
257283

nox/sessions.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from typing import (
3737
TYPE_CHECKING,
3838
Any,
39+
Literal,
3940
NoReturn,
4041
)
4142

@@ -713,7 +714,7 @@ def install(
713714
*args: str,
714715
env: Mapping[str, str] | None = None,
715716
include_outer_env: bool = True,
716-
silent: bool | None = None,
717+
silent: bool = True,
717718
success_codes: Iterable[int] | None = None,
718719
log: bool = True,
719720
external: ExternalType | None = None,
@@ -781,9 +782,6 @@ def install(
781782
if self._runner.global_config.no_install and venv._reused:
782783
return
783784

784-
if silent is None:
785-
silent = True
786-
787785
if isinstance(venv, VirtualEnv) and venv.venv_backend == "uv":
788786
cmd = ["uv", "pip", "install"]
789787
else:
@@ -803,6 +801,98 @@ def install(
803801
terminate_timeout=terminate_timeout,
804802
)
805803

804+
def sync(
805+
self,
806+
*args: str,
807+
packages: Iterable[str] | None = None,
808+
extras: Iterable[str] | Literal["all"] | None = None,
809+
inexact: bool = True,
810+
frozen: bool = True,
811+
omit: Literal["dev", "non-dev"] | None = None,
812+
env: Mapping[str, str] | None = None,
813+
include_outer_env: bool = True,
814+
silent: bool = True,
815+
success_codes: Iterable[int] | None = None,
816+
log: bool = True,
817+
external: ExternalType | None = None,
818+
stdout: int | IO[str] | None = None,
819+
stderr: int | IO[str] | None = subprocess.STDOUT,
820+
interrupt_timeout: float | None = DEFAULT_INTERRUPT_TIMEOUT,
821+
terminate_timeout: float | None = DEFAULT_TERMINATE_TIMEOUT,
822+
) -> None:
823+
"""Install invokes `uv`_ to sync packages inside of the session's
824+
virtualenv.
825+
826+
:param packages: Sync for a specific package in the workspace.
827+
:param extras: Include optional dependencies from the extra group name.
828+
:param inexact: Do not remove extraneous packages present in the environment. ``True`` by default.
829+
:param frozen: Sync without updating the `uv.lock` file. ``True`` by default.
830+
:param omit: Omit dependencies.
831+
832+
Additional keyword args are the same as for :meth:`run`.
833+
834+
.. note::
835+
836+
Other then ``uv pip``, ``uv sync`` did not automatically install
837+
packages into the virtualenv directory. To do so, it's mandatory
838+
to setup ``UV_PROJECT_ENVIRONMENT`` to the virtual env folder. This
839+
will be done in the sync command.
840+
841+
.. _uv: https://docs.astral.sh/uv/concepts/projects
842+
"""
843+
venv = self._runner.venv
844+
845+
if isinstance(venv, VirtualEnv) and venv.venv_backend == "uv":
846+
overlay_env = env or {}
847+
uv_venv = {"UV_PROJECT_ENVIRONMENT": venv.location}
848+
env = {**uv_venv, **overlay_env}
849+
elif not isinstance(venv, PassthroughEnv):
850+
raise ValueError(
851+
"A session without a uv environment can not install dependencies"
852+
" with uv."
853+
)
854+
855+
if self._runner.global_config.no_install and venv._reused:
856+
return
857+
858+
cmd = ["uv", "sync"]
859+
860+
extraopts: list[str] = []
861+
if isinstance(packages, list):
862+
extraopts.extend(["--package", *packages])
863+
864+
if isinstance(extras, list):
865+
extraopts.extend(["--extra", *extras])
866+
elif extras == "all":
867+
extraopts.append("--all-extras")
868+
869+
if frozen:
870+
extraopts.append("--frozen")
871+
872+
if inexact:
873+
extraopts.append("--inexact")
874+
875+
if omit == "dev":
876+
extraopts.append("--no-dev")
877+
elif omit == "non-dev":
878+
extraopts.append("--only-dev")
879+
880+
self._run(
881+
*cmd,
882+
*args,
883+
*extraopts,
884+
env=env,
885+
include_outer_env=include_outer_env,
886+
external="error",
887+
silent=silent,
888+
success_codes=success_codes,
889+
log=log,
890+
stdout=stdout,
891+
stderr=stderr,
892+
interrupt_timeout=interrupt_timeout,
893+
terminate_timeout=terminate_timeout,
894+
)
895+
806896
def notify(
807897
self,
808898
target: str | SessionRunner,

tests/test_sessions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,40 @@ class SessionNoSlots(nox.sessions.Session):
928928
"urllib3",
929929
)
930930

931+
def test_sync_uv(self):
932+
runner = nox.sessions.SessionRunner(
933+
name="test",
934+
signatures=["test"],
935+
func=mock.sentinel.func,
936+
global_config=_options.options.namespace(posargs=[]),
937+
manifest=mock.create_autospec(nox.manifest.Manifest),
938+
)
939+
runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
940+
runner.venv.env = {}
941+
runner.venv.venv_backend = "uv"
942+
runner.venv.location = "/project/.nox"
943+
944+
class SessionNoSlots(nox.sessions.Session):
945+
pass
946+
947+
session = SessionNoSlots(runner=runner)
948+
949+
with mock.patch.object(session, "_run", autospec=True) as run:
950+
session.sync(packages=["myproject"], silent=False)
951+
run.assert_called_once_with(
952+
"uv",
953+
"sync",
954+
"--package",
955+
"myproject",
956+
"--frozen",
957+
"--inexact",
958+
**_run_with_defaults(
959+
silent=False,
960+
external="error",
961+
env={"UV_PROJECT_ENVIRONMENT": "/project/.nox"},
962+
),
963+
)
964+
931965
def test___slots__(self):
932966
session, _ = self.make_session_and_runner()
933967
with pytest.raises(AttributeError):

0 commit comments

Comments
 (0)