@@ -930,124 +930,170 @@ impl ScriptPython {
930
930
}
931
931
}
932
932
933
- /// Initialize a virtual environment for the current project.
934
- pub ( crate ) async fn get_or_init_environment (
935
- workspace : & Workspace ,
936
- python : Option < PythonRequest > ,
937
- install_mirrors : & PythonInstallMirrors ,
938
- python_preference : PythonPreference ,
939
- python_downloads : PythonDownloads ,
940
- connectivity : Connectivity ,
941
- native_tls : bool ,
942
- allow_insecure_host : & [ TrustedHost ] ,
943
- no_config : bool ,
944
- active : Option < bool > ,
945
- cache : & Cache ,
946
- printer : Printer ,
947
- ) -> Result < PythonEnvironment , ProjectError > {
948
- // Lock the project environment to avoid synchronization issues.
949
- let _lock = ProjectInterpreter :: lock ( workspace ) . await ? ;
933
+ /// The Python environment for a project.
934
+ # [ derive ( Debug ) ]
935
+ enum ProjectEnvironment {
936
+ /// An existing [`PythonEnvironment`] was discovered, which satisfies the project's requirements.
937
+ Existing ( PythonEnvironment ) ,
938
+ /// An existing [`PythonEnvironment`] was discovered, but did not satisfy the project's
939
+ /// requirements, and so was replaced.
940
+ ///
941
+ /// In `--dry-run` mode, the environment will not be replaced, but this variant will still be
942
+ /// returned.
943
+ Replaced ( PythonEnvironment , PathBuf ) ,
944
+ /// A new [`PythonEnvironment`] was created.
945
+ ///
946
+ /// In `--dry-run` mode, the environment will not be created, but this variant will still be
947
+ /// returned.
948
+ New ( PythonEnvironment , PathBuf ) ,
949
+ }
950
950
951
- match ProjectInterpreter :: discover (
952
- workspace,
953
- workspace. install_path ( ) . as_ref ( ) ,
954
- python,
955
- python_preference,
956
- python_downloads,
957
- connectivity,
958
- native_tls,
959
- allow_insecure_host,
960
- install_mirrors,
961
- no_config,
962
- active,
963
- cache,
964
- printer,
965
- )
966
- . await ?
967
- {
968
- // If we found an existing, compatible environment, use it.
969
- ProjectInterpreter :: Environment ( environment) => Ok ( environment) ,
970
-
971
- // Otherwise, create a virtual environment with the discovered interpreter.
972
- ProjectInterpreter :: Interpreter ( interpreter) => {
973
- let venv = workspace. venv ( active) ;
974
-
975
- // Avoid removing things that are not virtual environments
976
- let should_remove = match ( venv. try_exists ( ) , venv. join ( "pyvenv.cfg" ) . try_exists ( ) ) {
977
- // It's a virtual environment we can remove it
978
- ( _, Ok ( true ) ) => true ,
979
- // It doesn't exist at all, we should use it without deleting it to avoid TOCTOU bugs
980
- ( Ok ( false ) , Ok ( false ) ) => false ,
981
- // If it's not a virtual environment, bail
982
- ( Ok ( true ) , Ok ( false ) ) => {
983
- // Unless it's empty, in which case we just ignore it
984
- if venv. read_dir ( ) . is_ok_and ( |mut dir| dir. next ( ) . is_none ( ) ) {
985
- false
986
- } else {
951
+ impl ProjectEnvironment {
952
+ /// Initialize a virtual environment for the current project.
953
+ pub ( crate ) async fn get_or_init (
954
+ workspace : & Workspace ,
955
+ python : Option < PythonRequest > ,
956
+ install_mirrors : & PythonInstallMirrors ,
957
+ python_preference : PythonPreference ,
958
+ python_downloads : PythonDownloads ,
959
+ connectivity : Connectivity ,
960
+ native_tls : bool ,
961
+ allow_insecure_host : & [ TrustedHost ] ,
962
+ no_config : bool ,
963
+ active : Option < bool > ,
964
+ cache : & Cache ,
965
+ dry_run : bool ,
966
+ printer : Printer ,
967
+ ) -> Result < Self , ProjectError > {
968
+ // Lock the project environment to avoid synchronization issues.
969
+ let _lock = ProjectInterpreter :: lock ( workspace) . await ?;
970
+
971
+ match ProjectInterpreter :: discover (
972
+ workspace,
973
+ workspace. install_path ( ) . as_ref ( ) ,
974
+ python,
975
+ python_preference,
976
+ python_downloads,
977
+ connectivity,
978
+ native_tls,
979
+ allow_insecure_host,
980
+ install_mirrors,
981
+ no_config,
982
+ active,
983
+ cache,
984
+ printer,
985
+ )
986
+ . await ?
987
+ {
988
+ // If we found an existing, compatible environment, use it.
989
+ ProjectInterpreter :: Environment ( environment) => Ok ( Self :: Existing ( environment) ) ,
990
+
991
+ // Otherwise, create a virtual environment with the discovered interpreter.
992
+ ProjectInterpreter :: Interpreter ( interpreter) => {
993
+ let venv = workspace. venv ( active) ;
994
+
995
+ // Avoid removing things that are not virtual environments
996
+ let replace = match ( venv. try_exists ( ) , venv. join ( "pyvenv.cfg" ) . try_exists ( ) ) {
997
+ // It's a virtual environment we can remove it
998
+ ( _, Ok ( true ) ) => true ,
999
+ // It doesn't exist at all, we should use it without deleting it to avoid TOCTOU bugs
1000
+ ( Ok ( false ) , Ok ( false ) ) => false ,
1001
+ // If it's not a virtual environment, bail
1002
+ ( Ok ( true ) , Ok ( false ) ) => {
1003
+ // Unless it's empty, in which case we just ignore it
1004
+ if venv. read_dir ( ) . is_ok_and ( |mut dir| dir. next ( ) . is_none ( ) ) {
1005
+ false
1006
+ } else {
1007
+ return Err ( ProjectError :: InvalidProjectEnvironmentDir (
1008
+ venv,
1009
+ "it is not a compatible environment but cannot be recreated because it is not a virtual environment" . to_string ( ) ,
1010
+ ) ) ;
1011
+ }
1012
+ }
1013
+ // Similarly, if we can't _tell_ if it exists we should bail
1014
+ ( _, Err ( err) ) | ( Err ( err) , _) => {
987
1015
return Err ( ProjectError :: InvalidProjectEnvironmentDir (
988
1016
venv,
989
- "it is not a compatible environment but cannot be recreated because it is not a virtual environment" . to_string ( ) ,
1017
+ format ! ( "it is not a compatible environment but cannot be recreated because uv cannot determine if it is a virtual environment: {err}" ) ,
990
1018
) ) ;
991
1019
}
1020
+ } ;
1021
+
1022
+ // Under `--dry-run`, avoid modifying the environment.
1023
+ if dry_run {
1024
+ let environment = PythonEnvironment :: from_interpreter ( interpreter) ;
1025
+ return Ok ( if replace {
1026
+ Self :: Replaced ( environment, venv)
1027
+ } else {
1028
+ Self :: New ( environment, venv)
1029
+ } ) ;
992
1030
}
993
- // Similarly, if we can't _tell_ if it exists we should bail
994
- ( _, Err ( err) ) | ( Err ( err) , _) => {
995
- return Err ( ProjectError :: InvalidProjectEnvironmentDir (
996
- venv,
997
- format ! ( "it is not a compatible environment but cannot be recreated because uv cannot determine if it is a virtual environment: {err}" ) ,
998
- ) ) ;
999
- }
1000
- } ;
1001
1031
1002
- // Remove the existing virtual environment if it doesn't meet the requirements.
1003
- if should_remove {
1004
- match fs_err:: remove_dir_all ( & venv) {
1005
- Ok ( ( ) ) => {
1006
- writeln ! (
1007
- printer. stderr( ) ,
1008
- "Removed virtual environment at: {}" ,
1009
- venv. user_display( ) . cyan( )
1010
- ) ?;
1032
+ // Remove the existing virtual environment if it doesn't meet the requirements.
1033
+ if replace {
1034
+ match fs_err:: remove_dir_all ( & venv) {
1035
+ Ok ( ( ) ) => {
1036
+ writeln ! (
1037
+ printer. stderr( ) ,
1038
+ "Removed virtual environment at: {}" ,
1039
+ venv. user_display( ) . cyan( )
1040
+ ) ?;
1041
+ }
1042
+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => { }
1043
+ Err ( e) => return Err ( e. into ( ) ) ,
1011
1044
}
1012
- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => { }
1013
- Err ( e) => return Err ( e. into ( ) ) ,
1014
1045
}
1015
- }
1016
1046
1017
- writeln ! (
1018
- printer. stderr( ) ,
1019
- "Creating virtual environment at: {}" ,
1020
- venv. user_display( ) . cyan( )
1021
- ) ?;
1047
+ writeln ! (
1048
+ printer. stderr( ) ,
1049
+ "Creating virtual environment at: {}" ,
1050
+ venv. user_display( ) . cyan( )
1051
+ ) ?;
1052
+
1053
+ // Determine a prompt for the environment, in order of preference:
1054
+ //
1055
+ // 1) The name of the project
1056
+ // 2) The name of the directory at the root of the workspace
1057
+ // 3) No prompt
1058
+ let prompt = workspace
1059
+ . pyproject_toml ( )
1060
+ . project
1061
+ . as_ref ( )
1062
+ . map ( |p| p. name . to_string ( ) )
1063
+ . or_else ( || {
1064
+ workspace
1065
+ . install_path ( )
1066
+ . file_name ( )
1067
+ . map ( |f| f. to_string_lossy ( ) . to_string ( ) )
1068
+ } )
1069
+ . map ( uv_virtualenv:: Prompt :: Static )
1070
+ . unwrap_or ( uv_virtualenv:: Prompt :: None ) ;
1071
+
1072
+ let environment = uv_virtualenv:: create_venv (
1073
+ & venv,
1074
+ interpreter,
1075
+ prompt,
1076
+ false ,
1077
+ false ,
1078
+ false ,
1079
+ false ,
1080
+ ) ?;
1081
+
1082
+ if replace {
1083
+ Ok ( Self :: Replaced ( environment, venv) )
1084
+ } else {
1085
+ Ok ( Self :: New ( environment, venv) )
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1022
1090
1023
- // Determine a prompt for the environment, in order of preference:
1024
- //
1025
- // 1) The name of the project
1026
- // 2) The name of the directory at the root of the workspace
1027
- // 3) No prompt
1028
- let prompt = workspace
1029
- . pyproject_toml ( )
1030
- . project
1031
- . as_ref ( )
1032
- . map ( |p| p. name . to_string ( ) )
1033
- . or_else ( || {
1034
- workspace
1035
- . install_path ( )
1036
- . file_name ( )
1037
- . map ( |f| f. to_string_lossy ( ) . to_string ( ) )
1038
- } )
1039
- . map ( uv_virtualenv:: Prompt :: Static )
1040
- . unwrap_or ( uv_virtualenv:: Prompt :: None ) ;
1041
-
1042
- Ok ( uv_virtualenv:: create_venv (
1043
- & venv,
1044
- interpreter,
1045
- prompt,
1046
- false ,
1047
- false ,
1048
- false ,
1049
- false ,
1050
- ) ?)
1091
+ /// Convert the [`ProjectEnvironment`] into a [`PythonEnvironment`].
1092
+ pub ( crate ) fn into_environment ( self ) -> PythonEnvironment {
1093
+ match self {
1094
+ Self :: Existing ( environment) => environment,
1095
+ Self :: Replaced ( environment, ..) => environment,
1096
+ Self :: New ( environment, ..) => environment,
1051
1097
}
1052
1098
}
1053
1099
}
0 commit comments