@@ -40,7 +40,7 @@ use std::io;
4040use std:: path:: { Path , PathBuf } ;
4141use std:: str:: FromStr ;
4242
43- use rustc_hash:: FxHashSet ;
43+ use rustc_hash:: { FxHashMap , FxHashSet } ;
4444use tracing:: instrument;
4545use unscanny:: { Pattern , Scanner } ;
4646use url:: Url ;
@@ -66,6 +66,9 @@ use crate::shquote::unquote;
6666mod requirement;
6767mod shquote;
6868
69+ /// A cache of file contents, keyed by path, to avoid re-reading files from disk.
70+ pub type SourceCache = FxHashMap < PathBuf , String > ;
71+
6972/// We emit one of those for each `requirements.txt` entry.
7073enum RequirementsTxtStatement {
7174 /// `-r` inclusion filename
@@ -171,12 +174,39 @@ impl RequirementsTxt {
171174 requirements_txt : impl AsRef < Path > ,
172175 working_dir : impl AsRef < Path > ,
173176 client_builder : & BaseClientBuilder < ' _ > ,
177+ ) -> Result < Self , RequirementsTxtFileError > {
178+ Self :: parse_with_cache (
179+ requirements_txt,
180+ working_dir,
181+ client_builder,
182+ & mut SourceCache :: default ( ) ,
183+ )
184+ . await
185+ }
186+
187+ /// Parse a `requirements.txt` file, using the given cache to avoid re-reading files from disk.
188+ #[ instrument(
189+ skip_all,
190+ fields( requirements_txt = requirements_txt. as_ref( ) . as_os_str( ) . to_str( ) )
191+ ) ]
192+ pub async fn parse_with_cache (
193+ requirements_txt : impl AsRef < Path > ,
194+ working_dir : impl AsRef < Path > ,
195+ client_builder : & BaseClientBuilder < ' _ > ,
196+ cache : & mut SourceCache ,
174197 ) -> Result < Self , RequirementsTxtFileError > {
175198 let mut visited = VisitedFiles :: Requirements {
176199 requirements : & mut FxHashSet :: default ( ) ,
177200 constraints : & mut FxHashSet :: default ( ) ,
178201 } ;
179- Self :: parse_impl ( requirements_txt, working_dir, client_builder, & mut visited) . await
202+ Self :: parse_impl (
203+ requirements_txt,
204+ working_dir,
205+ client_builder,
206+ & mut visited,
207+ cache,
208+ )
209+ . await
180210 }
181211
182212 /// See module level documentation
@@ -189,49 +219,64 @@ impl RequirementsTxt {
189219 working_dir : impl AsRef < Path > ,
190220 client_builder : & BaseClientBuilder < ' _ > ,
191221 visited : & mut VisitedFiles < ' _ > ,
222+ cache : & mut SourceCache ,
192223 ) -> Result < Self , RequirementsTxtFileError > {
193224 let requirements_txt = requirements_txt. as_ref ( ) ;
194225 let working_dir = working_dir. as_ref ( ) ;
195226
196- let content =
197- if requirements_txt. starts_with ( "http://" ) | requirements_txt. starts_with ( "https://" ) {
198- #[ cfg( not( feature = "http" ) ) ]
199- {
227+ let content = if let Some ( content) = cache. get ( requirements_txt) {
228+ // Use cached content if available.
229+ content. clone ( )
230+ } else if requirements_txt. starts_with ( "http://" ) | requirements_txt. starts_with ( "https://" )
231+ {
232+ #[ cfg( not( feature = "http" ) ) ]
233+ {
234+ return Err ( RequirementsTxtFileError {
235+ file : requirements_txt. to_path_buf ( ) ,
236+ error : RequirementsTxtParserError :: Io ( io:: Error :: new (
237+ io:: ErrorKind :: InvalidInput ,
238+ "Remote file not supported without `http` feature" ,
239+ ) ) ,
240+ } ) ;
241+ }
242+
243+ #[ cfg( feature = "http" ) ]
244+ {
245+ // Avoid constructing a client if network is disabled already
246+ if client_builder. is_offline ( ) {
200247 return Err ( RequirementsTxtFileError {
201248 file : requirements_txt. to_path_buf ( ) ,
202249 error : RequirementsTxtParserError :: Io ( io:: Error :: new (
203250 io:: ErrorKind :: InvalidInput ,
204- "Remote file not supported without `http` feature" ,
251+ format ! (
252+ "Network connectivity is disabled, but a remote requirements file was requested: {}" ,
253+ requirements_txt. display( )
254+ ) ,
205255 ) ) ,
206256 } ) ;
207257 }
208258
209- #[ cfg( feature = "http" ) ]
210- {
211- // Avoid constructing a client if network is disabled already
212- if client_builder. is_offline ( ) {
213- return Err ( RequirementsTxtFileError {
214- file : requirements_txt. to_path_buf ( ) ,
215- error : RequirementsTxtParserError :: Io ( io:: Error :: new (
216- io:: ErrorKind :: InvalidInput ,
217- format ! ( "Network connectivity is disabled, but a remote requirements file was requested: {}" , requirements_txt. display( ) ) ,
218- ) ) ,
219- } ) ;
220- }
221-
222- let client = client_builder. build ( ) ;
223- read_url_to_string ( & requirements_txt, client) . await
224- }
225- } else {
226- // Ex) `file:///home/ferris/project/requirements.txt`
227- uv_fs:: read_to_string_transcode ( & requirements_txt)
259+ let client = client_builder. build ( ) ;
260+ let content = read_url_to_string ( & requirements_txt, client)
228261 . await
229- . map_err ( RequirementsTxtParserError :: Io )
262+ . map_err ( |err| RequirementsTxtFileError {
263+ file : requirements_txt. to_path_buf ( ) ,
264+ error : err,
265+ } ) ?;
266+ cache. insert ( requirements_txt. to_path_buf ( ) , content. clone ( ) ) ;
267+ content
230268 }
231- . map_err ( |err| RequirementsTxtFileError {
232- file : requirements_txt. to_path_buf ( ) ,
233- error : err,
234- } ) ?;
269+ } else {
270+ // Ex) `file:///home/ferris/project/requirements.txt`
271+ let content = uv_fs:: read_to_string_transcode ( & requirements_txt)
272+ . await
273+ . map_err ( |err| RequirementsTxtFileError {
274+ file : requirements_txt. to_path_buf ( ) ,
275+ error : RequirementsTxtParserError :: Io ( err) ,
276+ } ) ?;
277+ cache. insert ( requirements_txt. to_path_buf ( ) , content. clone ( ) ) ;
278+ content
279+ } ;
235280
236281 let requirements_dir = requirements_txt. parent ( ) . unwrap_or ( working_dir) ;
237282 let data = Self :: parse_inner (
@@ -241,6 +286,7 @@ impl RequirementsTxt {
241286 client_builder,
242287 requirements_txt,
243288 visited,
289+ cache,
244290 )
245291 . await
246292 . map_err ( |err| RequirementsTxtFileError {
@@ -264,6 +310,7 @@ impl RequirementsTxt {
264310 client_builder : & BaseClientBuilder < ' _ > ,
265311 requirements_txt : & Path ,
266312 visited : & mut VisitedFiles < ' _ > ,
313+ cache : & mut SourceCache ,
267314 ) -> Result < Self , RequirementsTxtParserError > {
268315 let mut s = Scanner :: new ( content) ;
269316
@@ -318,6 +365,7 @@ impl RequirementsTxt {
318365 working_dir,
319366 client_builder,
320367 visited,
368+ cache,
321369 ) )
322370 . await
323371 . map_err ( |err| RequirementsTxtParserError :: Subfile {
@@ -394,6 +442,7 @@ impl RequirementsTxt {
394442 working_dir,
395443 client_builder,
396444 & mut visited,
445+ cache,
397446 ) )
398447 . await
399448 . map_err ( |err| RequirementsTxtParserError :: Subfile {
0 commit comments