@@ -90,7 +90,7 @@ def logger_debug(*args):
9090
9191@attr .s ()
9292class PythonPackage (models .Package ):
93- metafiles = ('metadata.json' , '*setup.py' , 'PKG-INFO' , '*.whl' , '*.egg' )
93+ metafiles = ('metadata.json' , '*setup.py' , 'PKG-INFO' , '*.whl' , '*.egg' , '*requirements*.txt' , '*requirements*.in' )
9494 extensions = ('.egg' , '.whl' , '.pyz' , '.pex' ,)
9595 default_type = 'pypi'
9696 default_primary_language = 'Python'
@@ -119,6 +119,8 @@ def parse(location):
119119 file_name = fileutils .file_name (location )
120120 parsers = {
121121 'setup.py' : parse_setup_py ,
122+ 'requirements.txt' : parse_requirements_txt ,
123+ 'requirements.in' : parse_requirements_txt ,
122124 'metadata.json' : parse_metadata ,
123125 'PKG-INFO' : parse_pkg_info ,
124126 '.whl' : parse_wheel ,
@@ -194,12 +196,29 @@ def parse_dependencies(location, package):
194196 package .dependencies = dependencies
195197
196198
199+ dependency_type_by_extensions = {
200+ ('.txt' , '.in' ): 'requirements.txt' ,
201+ }
202+
203+
204+ def get_dependency_type (file_name , dependency_type_by_extensions = dependency_type_by_extensions ):
205+ """
206+ Return the type of a dependency as a string or None given a `file_name` string.
207+ """
208+ for extensions , dependency_type in dependency_type_by_extensions .items ():
209+ if file_name .endswith (extensions ):
210+ return dependency_type
211+
212+
197213def parse_with_dparse (location ):
198214 is_dir = filetype .is_dir (location )
199215 if is_dir :
200216 return
201217 file_name = fileutils .file_name (location )
202- if file_name not in (filetypes .requirements_txt ,
218+
219+ dependency_type = get_dependency_type (file_name )
220+
221+ if dependency_type not in (filetypes .requirements_txt ,
203222 filetypes .conda_yml ,
204223 filetypes .tox_ini ,
205224 filetypes .pipfile ,
@@ -211,27 +230,54 @@ def parse_with_dparse(location):
211230 mode = 'r'
212231 with open (location , mode ) as f :
213232 content = f .read ()
214- df = dparse .parse (content , file_type = file_name )
215- df_dependencies = df .dependencies
216- if not df_dependencies :
217- return
218- package_dependencies = []
219- for df_dependency in df_dependencies :
220- specs = df_dependency .specs
221- requirement = None
222- if specs :
223- requirement = str (specs )
224- package_dependencies .append (
225- models .DependentPackage (
226- purl = PackageURL (
227- type = 'pypi' , name = df_dependency .name ).to_string (),
228- scope = 'dependencies' ,
229- is_runtime = True ,
230- is_optional = False ,
231- requirement = requirement ,
232- )
233+
234+ df = dparse .parse (content , file_type = dependency_type )
235+ df_dependencies = df .dependencies
236+
237+ if not df_dependencies :
238+ return
239+
240+ package_dependencies = []
241+ for df_dependency in df_dependencies :
242+ specs = list (df_dependency .specs ._specs )
243+ is_resolved = False
244+ requirement = None
245+ purl = PackageURL (
246+ type = 'pypi' ,
247+ name = df_dependency .name
248+ ).to_string ()
249+ if specs :
250+ requirement = str (df_dependency .specs )
251+ for spec in specs :
252+ operator = spec .operator
253+ version = spec .version
254+ if any (operator == element for element in ('==' , '===' )):
255+ is_resolved = True
256+ purl = PackageURL (
257+ type = 'pypi' ,
258+ name = df_dependency .name ,
259+ version = version
260+ ).to_string ()
261+ package_dependencies .append (
262+ models .DependentPackage (
263+ purl = purl ,
264+ scope = 'dependencies' ,
265+ is_runtime = True ,
266+ is_optional = False ,
267+ is_resolved = is_resolved ,
268+ requirement = requirement
233269 )
234- return package_dependencies
270+ )
271+
272+ return package_dependencies
273+
274+
275+ def parse_requirements_txt (location ):
276+ """
277+ Return a package built from requirements.txt.
278+ """
279+ package_dependencies = parse_with_dparse (location )
280+ return PythonPackage (dependencies = package_dependencies )
235281
236282
237283def parse_setup_py (location ):
@@ -349,7 +395,7 @@ def parse_metadata(location):
349395 parties .append (models .Party (type = models .party_person , name = name , role = 'contact' ))
350396
351397 description = build_description (
352- infos .get ('summary' ) ,
398+ infos .get ('summary' ),
353399 infos .get ('description' )
354400 )
355401
0 commit comments