@@ -15,12 +15,14 @@ use uv_cache_info::Timestamp;
1515use uv_cli:: ExternalCommand ;
1616use uv_client:: { BaseClientBuilder , Connectivity } ;
1717use uv_configuration:: { Concurrency , PreviewMode , TrustedHost } ;
18+ use uv_distribution_types:: UnresolvedRequirement ;
1819use uv_distribution_types:: { Name , UnresolvedRequirementSpecification } ;
1920use uv_installer:: { SatisfiesResult , SitePackages } ;
2021use uv_normalize:: PackageName ;
2122use uv_pep440:: { VersionSpecifier , VersionSpecifiers } ;
2223use uv_pep508:: MarkerTree ;
2324use uv_pypi_types:: { Requirement , RequirementSource } ;
25+ use uv_python:: VersionRequest ;
2426use uv_python:: {
2527 EnvironmentPreference , PythonDownloads , PythonEnvironment , PythonInstallation ,
2628 PythonPreference , PythonRequest ,
@@ -183,12 +185,17 @@ pub(crate) async fn run(
183185
184186 // We check if the provided command is not part of the executables for the `from` package.
185187 // If the command is found in other packages, we warn the user about the correct package to use.
186- warn_executable_not_provided_by_package (
187- executable,
188- & from. name ,
189- & site_packages,
190- invocation_source,
191- ) ;
188+ match & from {
189+ ToolRequirement :: Python => { }
190+ ToolRequirement :: Package ( from) => {
191+ warn_executable_not_provided_by_package (
192+ executable,
193+ & from. name ,
194+ & site_packages,
195+ invocation_source,
196+ ) ;
197+ }
198+ }
192199
193200 let handle = match process. spawn ( ) {
194201 Ok ( handle) => Ok ( handle) ,
@@ -216,11 +223,15 @@ pub(crate) async fn run(
216223/// Returns an exit status if the caller should exit after hinting.
217224fn hint_on_not_found (
218225 executable : & str ,
219- from : & Requirement ,
226+ from : & ToolRequirement ,
220227 site_packages : & SitePackages ,
221228 invocation_source : ToolRunCommand ,
222229 printer : Printer ,
223230) -> anyhow:: Result < Option < ExitStatus > > {
231+ let from = match from {
232+ ToolRequirement :: Python => return Ok ( None ) ,
233+ ToolRequirement :: Package ( from) => from,
234+ } ;
224235 match get_entrypoints ( & from. name , site_packages) {
225236 Ok ( entrypoints) => {
226237 writeln ! (
@@ -397,6 +408,23 @@ fn warn_executable_not_provided_by_package(
397408 }
398409}
399410
411+ // Clippy isn't happy about the difference in size between these variants, but
412+ // [`ToolRequirement::Package`] is the more common case and it seems annoying to box it.
413+ #[ allow( clippy:: large_enum_variant) ]
414+ pub ( crate ) enum ToolRequirement {
415+ Python ,
416+ Package ( Requirement ) ,
417+ }
418+
419+ impl std:: fmt:: Display for ToolRequirement {
420+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
421+ match self {
422+ ToolRequirement :: Python => write ! ( f, "python" ) ,
423+ ToolRequirement :: Package ( requirement) => write ! ( f, "{requirement}" ) ,
424+ }
425+ }
426+ }
427+
400428/// Get or create a [`PythonEnvironment`] in which to run the specified tools.
401429///
402430/// If the target tool is already installed in a compatible environment, returns that
@@ -420,15 +448,50 @@ async fn get_or_create_environment(
420448 cache : & Cache ,
421449 printer : Printer ,
422450 preview : PreviewMode ,
423- ) -> Result < ( Requirement , PythonEnvironment ) , ProjectError > {
451+ ) -> Result < ( ToolRequirement , PythonEnvironment ) , ProjectError > {
424452 let client_builder = BaseClientBuilder :: new ( )
425453 . connectivity ( connectivity)
426454 . native_tls ( native_tls)
427455 . allow_insecure_host ( allow_insecure_host. to_vec ( ) ) ;
428456
429457 let reporter = PythonDownloadReporter :: single ( printer) ;
430458
431- let python_request = python. map ( PythonRequest :: parse) ;
459+ // Check if the target is `python`
460+ let python_request = if target. is_python ( ) {
461+ let target_request = match target {
462+ Target :: Unspecified ( _) => None ,
463+ Target :: Version ( _, version) | Target :: FromVersion ( _, _, version) => {
464+ Some ( PythonRequest :: Version (
465+ VersionRequest :: from_str ( & version. to_string ( ) ) . map_err ( anyhow:: Error :: from) ?,
466+ ) )
467+ }
468+ // TODO(zanieb): Add `PythonRequest::Latest`
469+ Target :: Latest ( _) | Target :: FromLatest ( _, _) => {
470+ return Err ( anyhow:: anyhow!(
471+ "Requesting the 'latest' Python version is not yet supported"
472+ )
473+ . into ( ) )
474+ }
475+ // From the definition of `is_python`, this can only be a bare `python`
476+ Target :: From ( _, from) => {
477+ debug_assert_eq ! ( * from, "python" ) ;
478+ None
479+ }
480+ } ;
481+
482+ if let Some ( target_request) = & target_request {
483+ if let Some ( python) = python {
484+ return Err ( anyhow:: anyhow!(
485+ "Received multiple Python version requests: `{python}` and `{target_request}`"
486+ )
487+ . into ( ) ) ;
488+ }
489+ }
490+
491+ target_request. or_else ( || python. map ( PythonRequest :: parse) )
492+ } else {
493+ python. map ( PythonRequest :: parse)
494+ } ;
432495
433496 // Discover an interpreter.
434497 let interpreter = PythonInstallation :: find_or_download (
@@ -448,66 +511,80 @@ async fn get_or_create_environment(
448511 // Initialize any shared state.
449512 let state = PlatformState :: default ( ) ;
450513
451- // Resolve the `--from` requirement.
452- let from = match target {
453- // Ex) `ruff`
454- Target :: Unspecified ( name) => Requirement {
455- name : PackageName :: from_str ( name) ?,
456- extras : vec ! [ ] ,
457- groups : vec ! [ ] ,
458- marker : MarkerTree :: default ( ) ,
459- source : RequirementSource :: Registry {
460- specifier : VersionSpecifiers :: empty ( ) ,
461- index : None ,
462- conflict : None ,
514+ let from = if target. is_python ( ) {
515+ ToolRequirement :: Python
516+ } else {
517+ ToolRequirement :: Package ( match target {
518+ // Ex) `ruff`
519+ Target :: Unspecified ( name) => Requirement {
520+ name : PackageName :: from_str ( name) ?,
521+ extras : vec ! [ ] ,
522+ groups : vec ! [ ] ,
523+ marker : MarkerTree :: default ( ) ,
524+ source : RequirementSource :: Registry {
525+ specifier : VersionSpecifiers :: empty ( ) ,
526+ index : None ,
527+ conflict : None ,
528+ } ,
529+ origin : None ,
463530 } ,
464- origin : None ,
465- } ,
466- 467- Target :: Version ( name , version ) | Target :: FromVersion ( _ , name , version ) => Requirement {
468- name : PackageName :: from_str ( name ) ? ,
469- extras : vec ! [ ] ,
470- groups : vec ! [ ] ,
471- marker : MarkerTree :: default ( ) ,
472- source : RequirementSource :: Registry {
473- specifier : VersionSpecifiers :: from ( VersionSpecifier :: equals_version (
474- version . clone ( ) ,
475- ) ) ,
476- index : None ,
477- conflict : None ,
531+ 532+ Target :: Version ( name , version ) | Target :: FromVersion ( _ , name , version ) => Requirement {
533+ name : PackageName :: from_str ( name ) ? ,
534+ extras : vec ! [ ] ,
535+ groups : vec ! [ ] ,
536+ marker : MarkerTree :: default ( ) ,
537+ source : RequirementSource :: Registry {
538+ specifier : VersionSpecifiers :: from ( VersionSpecifier :: equals_version (
539+ version . clone ( ) ,
540+ ) ) ,
541+ index : None ,
542+ conflict : None ,
543+ } ,
544+ origin : None ,
478545 } ,
479- origin : None ,
480- } ,
481- // Ex) `ruff@latest`
482- Target :: Latest ( name ) | Target :: FromLatest ( _ , name ) => Requirement {
483- name : PackageName :: from_str ( name ) ? ,
484- extras : vec ! [ ] ,
485- groups : vec ! [ ] ,
486- marker : MarkerTree :: default ( ) ,
487- source : RequirementSource :: Registry {
488- specifier : VersionSpecifiers :: empty ( ) ,
489- index : None ,
490- conflict : None ,
546+ // Ex) `ruff@latest`
547+ Target :: Latest ( name ) | Target :: FromLatest ( _ , name ) => Requirement {
548+ name : PackageName :: from_str ( name ) ? ,
549+ extras : vec ! [ ] ,
550+ groups : vec ! [ ] ,
551+ marker : MarkerTree :: default ( ) ,
552+ source : RequirementSource :: Registry {
553+ specifier : VersionSpecifiers :: empty ( ) ,
554+ index : None ,
555+ conflict : None ,
556+ } ,
557+ origin : None ,
491558 } ,
492- origin : None ,
493- } ,
494- // Ex) `ruff>=0.6.0`
495- Target :: From ( _, from) => resolve_names (
496- vec ! [ RequirementsSpecification :: parse_package( from) ?] ,
497- & interpreter,
498- settings,
499- & state,
500- connectivity,
501- concurrency,
502- native_tls,
503- allow_insecure_host,
504- cache,
505- printer,
506- preview,
507- )
508- . await ?
509- . pop ( )
510- . unwrap ( ) ,
559+ // Ex) `ruff>=0.6.0`
560+ Target :: From ( _, from) => {
561+ let spec = RequirementsSpecification :: parse_package ( from) ?;
562+ if let UnresolvedRequirement :: Named ( requirement) = & spec. requirement {
563+ if requirement. name . as_str ( ) == "python" {
564+ return Err ( anyhow:: anyhow!(
565+ "Using `--from python<specifier>` is not supported. Use `python@<version>` instead."
566+ )
567+ . into ( ) ) ;
568+ }
569+ }
570+ resolve_names (
571+ vec ! [ spec] ,
572+ & interpreter,
573+ settings,
574+ & state,
575+ connectivity,
576+ concurrency,
577+ native_tls,
578+ allow_insecure_host,
579+ cache,
580+ printer,
581+ preview,
582+ )
583+ . await ?
584+ . pop ( )
585+ . unwrap ( )
586+ }
587+ } )
511588 } ;
512589
513590 // Read the `--with` requirements.
@@ -522,7 +599,10 @@ async fn get_or_create_environment(
522599 // Resolve the `--from` and `--with` requirements.
523600 let requirements = {
524601 let mut requirements = Vec :: with_capacity ( 1 + with. len ( ) ) ;
525- requirements. push ( from. clone ( ) ) ;
602+ match & from {
603+ ToolRequirement :: Python => { }
604+ ToolRequirement :: Package ( requirement) => requirements. push ( requirement. clone ( ) ) ,
605+ }
526606 requirements. extend (
527607 resolve_names (
528608 spec. requirements . clone ( ) ,
@@ -547,35 +627,36 @@ async fn get_or_create_environment(
547627 let installed_tools = InstalledTools :: from_settings ( ) ?. init ( ) ?;
548628 let _lock = installed_tools. lock ( ) . await ?;
549629
550- let existing_environment =
551- installed_tools
552- . get_environment ( & from . name , cache) ?
630+ if let ToolRequirement :: Package ( requirement ) = & from {
631+ let existing_environment = installed_tools
632+ . get_environment ( & requirement . name , cache) ?
553633 . filter ( |environment| {
554634 python_request. as_ref ( ) . map_or ( true , |python_request| {
555635 python_request. satisfied ( environment. interpreter ( ) , cache)
556636 } )
557637 } ) ;
558- if let Some ( environment) = existing_environment {
559- // Check if the installed packages meet the requirements.
560- let site_packages = SitePackages :: from_environment ( & environment) ?;
638+ if let Some ( environment) = existing_environment {
639+ // Check if the installed packages meet the requirements.
640+ let site_packages = SitePackages :: from_environment ( & environment) ?;
561641
562- let requirements = requirements
563- . iter ( )
564- . cloned ( )
565- . map ( UnresolvedRequirementSpecification :: from)
566- . collect :: < Vec < _ > > ( ) ;
567- let constraints = [ ] ;
568-
569- if matches ! (
570- site_packages. satisfies(
571- & requirements,
572- & constraints,
573- & interpreter. resolver_marker_environment( )
574- ) ,
575- Ok ( SatisfiesResult :: Fresh { .. } )
576- ) {
577- debug ! ( "Using existing tool `{}`" , from. name) ;
578- return Ok ( ( from, environment) ) ;
642+ let requirements = requirements
643+ . iter ( )
644+ . cloned ( )
645+ . map ( UnresolvedRequirementSpecification :: from)
646+ . collect :: < Vec < _ > > ( ) ;
647+ let constraints = [ ] ;
648+
649+ if matches ! (
650+ site_packages. satisfies(
651+ & requirements,
652+ & constraints,
653+ & interpreter. resolver_marker_environment( )
654+ ) ,
655+ Ok ( SatisfiesResult :: Fresh { .. } )
656+ ) {
657+ debug ! ( "Using existing tool `{}`" , requirement. name) ;
658+ return Ok ( ( from, environment) ) ;
659+ }
579660 }
580661 }
581662 }
0 commit comments