3636from 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 ,
0 commit comments