mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-30 03:27:31 +00:00 
			
		
		
		
	Warn-and-ignore for unsupported requirements.txt options (#10420)
				
					
				
			## Summary Closes https://github.com/astral-sh/uv/issues/10366.
This commit is contained in:
		
							parent
							
								
									a0494bb059
								
							
						
					
					
						commit
						14b685d9fb
					
				
					 4 changed files with 135 additions and 15 deletions
				
			
		
							
								
								
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -5485,6 +5485,7 @@ dependencies = [ | ||||||
|  "uv-normalize", |  "uv-normalize", | ||||||
|  "uv-pep508", |  "uv-pep508", | ||||||
|  "uv-pypi-types", |  "uv-pypi-types", | ||||||
|  |  "uv-warnings", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  |  | ||||||
|  | @ -16,13 +16,14 @@ doctest = false | ||||||
| workspace = true | workspace = true | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| uv-distribution-types = { workspace = true } |  | ||||||
| uv-pep508 = { workspace = true } |  | ||||||
| uv-pypi-types = { workspace = true } |  | ||||||
| uv-client = { workspace = true } | uv-client = { workspace = true } | ||||||
|  | uv-configuration = { workspace = true } | ||||||
|  | uv-distribution-types = { workspace = true } | ||||||
| uv-fs = { workspace = true } | uv-fs = { workspace = true } | ||||||
| uv-normalize = { workspace = true } | uv-normalize = { workspace = true } | ||||||
| uv-configuration = { workspace = true } | uv-pep508 = { workspace = true } | ||||||
|  | uv-pypi-types = { workspace = true } | ||||||
|  | uv-warnings = { workspace = true } | ||||||
| 
 | 
 | ||||||
| fs-err = { workspace = true } | fs-err = { workspace = true } | ||||||
| regex = { workspace = true } | regex = { workspace = true } | ||||||
|  |  | ||||||
|  | @ -88,6 +88,8 @@ enum RequirementsTxtStatement { | ||||||
|     NoBinary(NoBinary), |     NoBinary(NoBinary), | ||||||
|     /// `--only-binary`
 |     /// `--only-binary`
 | ||||||
|     OnlyBinary(NoBuild), |     OnlyBinary(NoBuild), | ||||||
|  |     /// An unsupported option (e.g., `--trusted-host`).
 | ||||||
|  |     UnsupportedOption(UnsupportedOption), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in
 | /// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in
 | ||||||
|  | @ -384,6 +386,28 @@ impl RequirementsTxt { | ||||||
|                 RequirementsTxtStatement::OnlyBinary(only_binary) => { |                 RequirementsTxtStatement::OnlyBinary(only_binary) => { | ||||||
|                     data.only_binary.extend(only_binary); |                     data.only_binary.extend(only_binary); | ||||||
|                 } |                 } | ||||||
|  |                 RequirementsTxtStatement::UnsupportedOption(flag) => { | ||||||
|  |                     if requirements_txt == Path::new("-") { | ||||||
|  |                         if flag.cli() { | ||||||
|  |                             uv_warnings::warn_user!("Ignoring unsupported option from stdin: `{flag}` (hint: pass `{flag}` on the command line instead)", flag = flag.green()); | ||||||
|  |                         } else { | ||||||
|  |                             uv_warnings::warn_user!( | ||||||
|  |                                 "Ignoring unsupported option from stdin: `{flag}`", | ||||||
|  |                                 flag = flag.green() | ||||||
|  |                             ); | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         if flag.cli() { | ||||||
|  |                             uv_warnings::warn_user!("Ignoring unsupported option in `{path}`: `{flag}` (hint: pass `{flag}` on the command line instead)", path = requirements_txt.user_display().cyan(), flag = flag.green()); | ||||||
|  |                         } else { | ||||||
|  |                             uv_warnings::warn_user!( | ||||||
|  |                                 "Ignoring unsupported option in `{path}`: `{flag}`", | ||||||
|  |                                 path = requirements_txt.user_display().cyan(), | ||||||
|  |                                 flag = flag.green() | ||||||
|  |                             ); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         Ok(data) |         Ok(data) | ||||||
|  | @ -416,15 +440,70 @@ impl RequirementsTxt { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// An unsupported option (e.g., `--trusted-host`).
 | ||||||
|  | ///
 | ||||||
|  | /// See: <https://pip.pypa.io/en/stable/reference/requirements-file-format/#global-options>
 | ||||||
|  | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||||
|  | enum UnsupportedOption { | ||||||
|  |     PreferBinary, | ||||||
|  |     RequireHashes, | ||||||
|  |     Pre, | ||||||
|  |     TrustedHost, | ||||||
|  |     UseFeature, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl UnsupportedOption { | ||||||
|  |     /// The name of the unsupported option.
 | ||||||
|  |     fn name(self) -> &'static str { | ||||||
|  |         match self { | ||||||
|  |             UnsupportedOption::PreferBinary => "--prefer-binary", | ||||||
|  |             UnsupportedOption::RequireHashes => "--require-hashes", | ||||||
|  |             UnsupportedOption::Pre => "--pre", | ||||||
|  |             UnsupportedOption::TrustedHost => "--trusted-host", | ||||||
|  |             UnsupportedOption::UseFeature => "--use-feature", | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Returns `true` if the option is supported on the CLI.
 | ||||||
|  |     fn cli(self) -> bool { | ||||||
|  |         match self { | ||||||
|  |             UnsupportedOption::PreferBinary => false, | ||||||
|  |             UnsupportedOption::RequireHashes => true, | ||||||
|  |             UnsupportedOption::Pre => true, | ||||||
|  |             UnsupportedOption::TrustedHost => true, | ||||||
|  |             UnsupportedOption::UseFeature => false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Returns an iterator over all unsupported options.
 | ||||||
|  |     fn iter() -> impl Iterator<Item = UnsupportedOption> { | ||||||
|  |         [ | ||||||
|  |             UnsupportedOption::PreferBinary, | ||||||
|  |             UnsupportedOption::RequireHashes, | ||||||
|  |             UnsupportedOption::Pre, | ||||||
|  |             UnsupportedOption::TrustedHost, | ||||||
|  |             UnsupportedOption::UseFeature, | ||||||
|  |         ] | ||||||
|  |         .iter() | ||||||
|  |         .copied() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Display for UnsupportedOption { | ||||||
|  |     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         write!(f, "{}", self.name()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /// Returns `true` if the character is a newline or a comment character.
 | /// Returns `true` if the character is a newline or a comment character.
 | ||||||
| const fn is_terminal(c: char) -> bool { | const fn is_terminal(c: char) -> bool { | ||||||
|     matches!(c, '\n' | '\r' | '#') |     matches!(c, '\n' | '\r' | '#') | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Parse a single entry, that is a requirement, an inclusion or a comment line
 | /// Parse a single entry, that is a requirement, an inclusion or a comment line.
 | ||||||
| ///
 | ///
 | ||||||
| /// Consumes all preceding trivia (whitespace and comments). If it returns None, we've reached
 | /// Consumes all preceding trivia (whitespace and comments). If it returns `None`, we've reached
 | ||||||
| /// the end of file
 | /// the end of file.
 | ||||||
| fn parse_entry( | fn parse_entry( | ||||||
|     s: &mut Scanner, |     s: &mut Scanner, | ||||||
|     content: &str, |     content: &str, | ||||||
|  | @ -595,14 +674,20 @@ fn parse_entry( | ||||||
|             hashes, |             hashes, | ||||||
|         }) |         }) | ||||||
|     } else if let Some(char) = s.peek() { |     } else if let Some(char) = s.peek() { | ||||||
|         let (line, column) = calculate_row_column(content, s.cursor()); |         // Identify an unsupported option, like `--trusted-host`.
 | ||||||
|         return Err(RequirementsTxtParserError::Parser { |         if let Some(option) = UnsupportedOption::iter().find(|option| s.eat_if(option.name())) { | ||||||
|             message: format!( |             s.eat_while(|c: char| !is_terminal(c)); | ||||||
|                 "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" |             RequirementsTxtStatement::UnsupportedOption(option) | ||||||
|             ), |         } else { | ||||||
|             line, |             let (line, column) = calculate_row_column(content, s.cursor()); | ||||||
|             column, |             return Err(RequirementsTxtParserError::Parser { | ||||||
|         }); |                 message: format!( | ||||||
|  |                     "Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement" | ||||||
|  |                 ), | ||||||
|  |                 line, | ||||||
|  |                 column, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|     } else { |     } else { | ||||||
|         // EOF
 |         // EOF
 | ||||||
|         return Ok(None); |         return Ok(None); | ||||||
|  |  | ||||||
|  | @ -486,6 +486,39 @@ fn install_requirements_txt() -> Result<()> { | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Warn (but don't fail) when unsupported flags are set in the `requirements.txt`.
 | ||||||
|  | #[test] | ||||||
|  | fn install_unsupported_flag() -> Result<()> { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     let requirements_txt = context.temp_dir.child("requirements.txt"); | ||||||
|  |     requirements_txt.write_str(indoc! {r" | ||||||
|  |         --pre | ||||||
|  |         --prefer-binary :all: | ||||||
|  |         iniconfig | ||||||
|  |     "})?;
 | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.pip_install() | ||||||
|  |         .arg("-r") | ||||||
|  |         .arg("requirements.txt") | ||||||
|  |         .arg("--strict"), @r###" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     warning: Ignoring unsupported option in `requirements.txt`: `--pre` (hint: pass `--pre` on the command line instead) | ||||||
|  |     warning: Ignoring unsupported option in `requirements.txt`: `--prefer-binary` | ||||||
|  |     Resolved 1 package in [TIME] | ||||||
|  |     Prepared 1 package in [TIME] | ||||||
|  |     Installed 1 package in [TIME] | ||||||
|  |      + iniconfig==2.0.0 | ||||||
|  |     "###
 | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /// Install a requirements file with pins that conflict
 | /// Install a requirements file with pins that conflict
 | ||||||
| ///
 | ///
 | ||||||
| /// This is likely to occur in the real world when compiled on one platform then installed on another.
 | /// This is likely to occur in the real world when compiled on one platform then installed on another.
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Charlie Marsh
						Charlie Marsh