@@ -125,11 +125,8 @@ def read(self):
125
125
return content .encode ()
126
126
127
127
128
- async def package_info (
129
- release_files : tuple [model .File , ...],
130
- repository : SimpleRepository ,
131
- project_name : str ,
132
- ) -> tuple [model .File , PackageInfo ]:
128
+ def _select_best_file (release_files : tuple [model .File , ...]) -> model .File :
129
+ """Select the best file from release files based on priority criteria."""
133
130
files = sorted (
134
131
release_files ,
135
132
key = lambda file : (
@@ -140,100 +137,137 @@ async def package_info(
140
137
file .upload_time , # Distinguish conflicts by picking the earliest one.
141
138
),
142
139
)
140
+ return files [0 ]
143
141
144
- files_info : typing .Dict [str , FileInfo ] = {}
145
142
146
- # Get the size from the repository files
147
- for file in files :
143
+ def _create_files_info_mapping (
144
+ release_files : tuple [model .File , ...],
145
+ ) -> typing .Dict [str , FileInfo ]:
146
+ """Create mapping of filename to FileInfo for files with size information."""
147
+ files_info : typing .Dict [str , FileInfo ] = {}
148
+ for file in release_files :
148
149
if file .size :
149
150
files_info [file .filename ] = FileInfo (
150
151
size = file .size or 0 ,
151
152
)
153
+ return files_info
152
154
153
- file = files [0 ]
154
155
156
+ async def _fetch_metadata_resource (
157
+ repository : SimpleRepository ,
158
+ project_name : str ,
159
+ file : model .File ,
160
+ tmp_file_path : str ,
161
+ ) -> tuple [model .File , pkginfo .Distribution ]:
162
+ """Fetch metadata resource and return updated file and package info."""
155
163
if file .dist_info_metadata :
156
164
resource_name = file .filename + ".metadata"
157
165
else :
158
166
raise ValueError (f"Metadata not available for { file } " )
159
167
160
168
logging .debug (f"Downloading metadata for { file .filename } from { resource_name } " )
161
169
162
- with tempfile .NamedTemporaryFile (
163
- suffix = os .path .splitext (file .filename )[1 ],
164
- ) as tmp :
165
- resource = await repository .get_resource (project_name , resource_name )
170
+ resource = await repository .get_resource (project_name , resource_name )
166
171
167
- if isinstance (resource , model .TextResource ):
172
+ if isinstance (resource , model .TextResource ):
173
+ with open (tmp_file_path , "wb" ) as tmp :
168
174
tmp .write (resource .text .encode ())
169
- if not file .upload_time :
170
- # If the repository doesn't provide information about the upload time, estimate
171
- # it from the headers of the resource, if they exist.
172
- if ct := resource .context .get ("creation-date" ):
173
- if isinstance (ct , str ):
174
- file = dataclasses .replace (
175
- file , upload_time = datetime .datetime .fromisoformat (ct )
176
- )
177
- elif isinstance (resource , model .HttpResource ):
178
- await fetch_file (resource .url , tmp . name )
179
- else :
180
- raise ValueError (f"Unhandled resource type ({ type (resource )} )" )
175
+ if not file .upload_time :
176
+ # If the repository doesn't provide information about the upload time, estimate
177
+ # it from the headers of the resource, if they exist.
178
+ if ct := resource .context .get ("creation-date" ):
179
+ if isinstance (ct , str ):
180
+ file = dataclasses .replace (
181
+ file , upload_time = datetime .datetime .fromisoformat (ct )
182
+ )
183
+ elif isinstance (resource , model .HttpResource ):
184
+ await fetch_file (resource .url , tmp_file_path )
185
+ else :
186
+ raise ValueError (f"Unhandled resource type ({ type (resource )} )" )
181
187
182
- tmp .flush ()
183
- tmp .seek (0 )
184
- info = PkgInfoFromFile (tmp .name )
185
- description = generate_safe_description_html (info )
188
+ info = PkgInfoFromFile (tmp_file_path )
189
+ return file , info
186
190
187
- # If there is email information, but not a name in the "author" or "maintainer"
188
- # attribute, extract this information from the first person's email address.
189
- # Will take something like ``"Ivan" [email protected] `` and extract the "Ivan" part.
190
- def extract_usernames (emails ):
191
- names = []
192
- parsed = email .parser .Parser (policy = email .policy .default ).parsestr (
193
- f"To: { info .author_email } " ,
194
- )
195
- for address in parsed ["to" ].addresses :
196
- names .append (address .display_name )
197
- return ", " .join (names )
198
-
199
- if not info .author and info .author_email :
200
- info .author = extract_usernames (info .author_email )
201
-
202
- if not info .maintainer and info .maintainer_email :
203
- info .maintainer = extract_usernames (info .maintainer_email )
204
-
205
- project_urls = {
206
- url .split ("," )[0 ].strip ().title (): url .split ("," )[1 ].strip ()
207
- for url in info .project_urls or []
208
- }
209
- # Ensure that a Homepage exists in the project urls
210
- if info .home_page and "Homepage" not in project_urls :
211
- project_urls ["Homepage" ] = info .home_page
212
-
213
- sorted_urls = {
214
- name : url
215
- for name , url in sorted (
216
- project_urls .items (),
217
- key = lambda item : (item [0 ] != "Homepage" , item [0 ]),
218
- )
219
- }
220
191
221
- reqs : list [Requirement | InvalidRequirementSpecification ] = []
222
- for req in info .requires_dist :
223
- try :
224
- reqs .append (Requirement (req ))
225
- except InvalidRequirement :
226
- reqs .append (InvalidRequirementSpecification (req ))
192
+ def _enhance_author_maintainer_info (info : pkginfo .Distribution ) -> None :
193
+ """Extract author/maintainer names from email addresses if names are missing."""
194
+
195
+ def extract_usernames (emails : str ) -> str :
196
+ names = []
197
+ parsed = email .parser .Parser (policy = email .policy .default ).parsestr (
198
+ f"To: { emails } " ,
199
+ )
200
+ for address in parsed ["to" ].addresses :
201
+ names .append (address .display_name )
202
+ return ", " .join (names )
203
+
204
+ if not info .author and info .author_email :
205
+ info .author = extract_usernames (info .author_email )
206
+
207
+ if not info .maintainer and info .maintainer_email :
208
+ info .maintainer = extract_usernames (info .maintainer_email )
209
+
210
+
211
+ def _process_project_urls (info : pkginfo .Distribution ) -> typing .Dict [str , str ]:
212
+ """Process and sort project URLs, ensuring Homepage is first."""
213
+ project_urls = {
214
+ url .split ("," )[0 ].strip ().title (): url .split ("," )[1 ].strip ()
215
+ for url in info .project_urls or []
216
+ }
217
+ # Ensure that a Homepage exists in the project urls
218
+ if info .home_page and "Homepage" not in project_urls :
219
+ project_urls ["Homepage" ] = info .home_page
220
+
221
+ sorted_urls = {
222
+ name : url
223
+ for name , url in sorted (
224
+ project_urls .items (),
225
+ key = lambda item : (item [0 ] != "Homepage" , item [0 ]),
226
+ )
227
+ }
228
+ return sorted_urls
229
+
230
+
231
+ def _parse_requirements (info : pkginfo .Distribution ) -> RequirementsSequence :
232
+ """Parse requirements from distribution info, handling invalid requirements."""
233
+ reqs : list [Requirement | InvalidRequirementSpecification ] = []
234
+ for req in info .requires_dist :
235
+ try :
236
+ reqs .append (Requirement (req ))
237
+ except InvalidRequirement :
238
+ reqs .append (InvalidRequirementSpecification (req ))
239
+ return RequirementsSequence (reqs )
240
+
241
+
242
+ async def package_info (
243
+ release_files : tuple [model .File , ...],
244
+ repository : SimpleRepository ,
245
+ project_name : str ,
246
+ ) -> tuple [model .File , PackageInfo ]:
247
+ files_info = _create_files_info_mapping (release_files )
248
+ file = _select_best_file (release_files )
249
+
250
+ with tempfile .NamedTemporaryFile (
251
+ suffix = os .path .splitext (file .filename )[1 ],
252
+ ) as tmp :
253
+ file , info = await _fetch_metadata_resource (
254
+ repository , project_name , file , tmp .name
255
+ )
256
+
257
+ description = generate_safe_description_html (info )
258
+ _enhance_author_maintainer_info (info )
259
+ project_urls = _process_project_urls (info )
260
+ requires_dist = _parse_requirements (info )
227
261
228
262
pkg = PackageInfo (
229
263
summary = info .summary or "" ,
230
264
description = description ,
231
265
author = info .author ,
232
266
maintainer = info .maintainer ,
233
267
classifiers = info .classifiers ,
234
- project_urls = sorted_urls ,
268
+ project_urls = project_urls ,
235
269
requires_python = info .requires_python ,
236
- requires_dist = RequirementsSequence ( reqs ) ,
270
+ requires_dist = requires_dist ,
237
271
# We include files info as it is the only way to influence the file.size of
238
272
# all files (for the files list page). In the future, this can be a standalone
239
273
# component.
0 commit comments