1
1
import logging
2
2
import mimetypes
3
3
import os
4
- import pathlib
5
- from typing import Callable , Iterable , Optional , Tuple
4
+ from collections import defaultdict
5
+ from typing import Callable , Dict , Iterable , List , Optional , Tuple
6
+
7
+ from pip ._vendor .packaging .utils import (
8
+ InvalidSdistFilename ,
9
+ InvalidVersion ,
10
+ InvalidWheelFilename ,
11
+ canonicalize_name ,
12
+ parse_sdist_filename ,
13
+ parse_wheel_filename ,
14
+ )
6
15
7
16
from pip ._internal .models .candidate import InstallationCandidate
8
17
from pip ._internal .models .link import Link
@@ -36,6 +45,53 @@ def _is_html_file(file_url: str) -> bool:
36
45
return mimetypes .guess_type (file_url , strict = False )[0 ] == "text/html"
37
46
38
47
48
+ class _FlatDirectoryToUrls :
49
+ """Scans directory and caches results"""
50
+
51
+ def __init__ (self , path : str ) -> None :
52
+ self ._path = path
53
+ self ._page_candidates : List [str ] = []
54
+ self ._project_name_to_urls : Dict [str , List [str ]] = defaultdict (list )
55
+ self ._scanned_directory = False
56
+
57
+ def _scan_directory (self ) -> None :
58
+ """Scans directory once and populates both page_candidates
59
+ and project_name_to_urls at the same time
60
+ """
61
+ for entry in os .scandir (self ._path ):
62
+ url = path_to_url (entry .path )
63
+ if _is_html_file (url ):
64
+ self ._page_candidates .append (url )
65
+ continue
66
+
67
+ # File must have a valid wheel or sdist name,
68
+ # otherwise not worth considering as a package
69
+ try :
70
+ project_filename = parse_wheel_filename (entry .name )[0 ]
71
+ except (InvalidWheelFilename , InvalidVersion ):
72
+ try :
73
+ project_filename = parse_sdist_filename (entry .name )[0 ]
74
+ except (InvalidSdistFilename , InvalidVersion ):
75
+ continue
76
+
77
+ self ._project_name_to_urls [project_filename ].append (url )
78
+ self ._scanned_directory = True
79
+
80
+ @property
81
+ def page_candidates (self ) -> List [str ]:
82
+ if not self ._scanned_directory :
83
+ self ._scan_directory ()
84
+
85
+ return self ._page_candidates
86
+
87
+ @property
88
+ def project_name_to_urls (self ) -> Dict [str , List [str ]]:
89
+ if not self ._scanned_directory :
90
+ self ._scan_directory ()
91
+
92
+ return self ._project_name_to_urls
93
+
94
+
39
95
class _FlatDirectorySource (LinkSource ):
40
96
"""Link source specified by ``--find-links=<path-to-dir>``.
41
97
@@ -45,30 +101,34 @@ class _FlatDirectorySource(LinkSource):
45
101
* ``file_candidates``: Archives in the directory.
46
102
"""
47
103
104
+ _paths_to_urls : Dict [str , _FlatDirectoryToUrls ] = {}
105
+
48
106
def __init__ (
49
107
self ,
50
108
candidates_from_page : CandidatesFromPage ,
51
109
path : str ,
110
+ project_name : str ,
52
111
) -> None :
53
112
self ._candidates_from_page = candidates_from_page
54
- self ._path = pathlib .Path (os .path .realpath (path ))
113
+ self ._project_name = canonicalize_name (project_name )
114
+
115
+ # Get existing instance of _FlatDirectoryToUrls if it exists
116
+ if path in self ._paths_to_urls :
117
+ self ._path_to_urls = self ._paths_to_urls [path ]
118
+ else :
119
+ self ._path_to_urls = _FlatDirectoryToUrls (path = path )
120
+ self ._paths_to_urls [path ] = self ._path_to_urls
55
121
56
122
@property
57
123
def link (self ) -> Optional [Link ]:
58
124
return None
59
125
60
126
def page_candidates (self ) -> FoundCandidates :
61
- for path in self ._path .iterdir ():
62
- url = path_to_url (str (path ))
63
- if not _is_html_file (url ):
64
- continue
127
+ for url in self ._path_to_urls .page_candidates :
65
128
yield from self ._candidates_from_page (Link (url ))
66
129
67
130
def file_links (self ) -> FoundLinks :
68
- for path in self ._path .iterdir ():
69
- url = path_to_url (str (path ))
70
- if _is_html_file (url ):
71
- continue
131
+ for url in self ._path_to_urls .project_name_to_urls [self ._project_name ]:
72
132
yield Link (url )
73
133
74
134
@@ -170,6 +230,7 @@ def build_source(
170
230
page_validator : PageValidator ,
171
231
expand_dir : bool ,
172
232
cache_link_parsing : bool ,
233
+ project_name : str ,
173
234
) -> Tuple [Optional [str ], Optional [LinkSource ]]:
174
235
path : Optional [str ] = None
175
236
url : Optional [str ] = None
@@ -203,6 +264,7 @@ def build_source(
203
264
source = _FlatDirectorySource (
204
265
candidates_from_page = candidates_from_page ,
205
266
path = path ,
267
+ project_name = project_name ,
206
268
)
207
269
else :
208
270
source = _IndexDirectorySource (
0 commit comments