1
1
"""Dependency parsing utilities for PyPI packages."""
2
2
3
+ import logging
3
4
import re
4
- from typing import Any , Dict , List , Optional , Set , Tuple
5
+ from typing import Any
6
+
5
7
from packaging .requirements import Requirement
6
- from packaging .specifiers import SpecifierSet
7
8
from packaging .version import Version
8
- import logging
9
9
10
10
logger = logging .getLogger (__name__ )
11
11
12
12
13
13
class DependencyParser :
14
14
"""Parser for Python package dependencies."""
15
-
15
+
16
16
def __init__ (self ):
17
- self .parsed_cache : Dict [str , List [Requirement ]] = {}
18
-
19
- def parse_requirements (self , requires_dist : List [str ]) -> List [Requirement ]:
17
+ self .parsed_cache : dict [str , list [Requirement ]] = {}
18
+
19
+ def parse_requirements (self , requires_dist : list [str ]) -> list [Requirement ]:
20
20
"""Parse requirements from requires_dist list.
21
-
21
+
22
22
Args:
23
23
requires_dist: List of requirement strings from PyPI metadata
24
-
24
+
25
25
Returns:
26
26
List of parsed Requirement objects
27
27
"""
28
28
requirements = []
29
-
29
+
30
30
for req_str in requires_dist or []:
31
31
if not req_str or not req_str .strip ():
32
32
continue
33
-
33
+
34
34
try :
35
35
req = Requirement (req_str )
36
36
requirements .append (req )
37
37
except Exception as e :
38
38
logger .warning (f"Failed to parse requirement '{ req_str } ': { e } " )
39
39
continue
40
-
40
+
41
41
return requirements
42
-
42
+
43
43
def filter_requirements_by_python_version (
44
- self ,
45
- requirements : List [Requirement ],
44
+ self ,
45
+ requirements : list [Requirement ],
46
46
python_version : str
47
- ) -> List [Requirement ]:
47
+ ) -> list [Requirement ]:
48
48
"""Filter requirements based on Python version.
49
-
49
+
50
50
Args:
51
51
requirements: List of Requirement objects
52
52
python_version: Target Python version (e.g., "3.10")
53
-
53
+
54
54
Returns:
55
55
Filtered list of requirements applicable to the Python version
56
56
"""
57
57
filtered = []
58
-
58
+
59
59
try :
60
60
target_version = Version (python_version )
61
61
except Exception as e :
62
62
logger .warning (f"Invalid Python version '{ python_version } ': { e } " )
63
63
return requirements
64
-
64
+
65
65
for req in requirements :
66
66
if self ._is_requirement_applicable (req , target_version ):
67
67
filtered .append (req )
68
-
68
+
69
69
return filtered
70
-
70
+
71
71
def _is_requirement_applicable (self , req : Requirement , python_version : Version ) -> bool :
72
72
"""Check if a requirement is applicable for the given Python version.
73
-
73
+
74
74
Args:
75
75
req: Requirement object
76
76
python_version: Target Python version
77
-
77
+
78
78
Returns:
79
79
True if requirement applies to the Python version
80
80
"""
81
81
if not req .marker :
82
82
return True
83
-
83
+
84
84
# Create environment for marker evaluation
85
85
env = {
86
86
'python_version' : str (python_version ),
@@ -90,22 +90,22 @@ def _is_requirement_applicable(self, req: Requirement, python_version: Version)
90
90
'implementation_name' : 'cpython' ,
91
91
'implementation_version' : str (python_version ),
92
92
}
93
-
93
+
94
94
try :
95
95
return req .marker .evaluate (env )
96
96
except Exception as e :
97
97
logger .warning (f"Failed to evaluate marker for { req } : { e } " )
98
98
return True # Include by default if evaluation fails
99
-
99
+
100
100
def categorize_dependencies (
101
- self ,
102
- requirements : List [Requirement ]
103
- ) -> Dict [str , List [Requirement ]]:
101
+ self ,
102
+ requirements : list [Requirement ]
103
+ ) -> dict [str , list [Requirement ]]:
104
104
"""Categorize dependencies into runtime, development, and optional groups.
105
-
105
+
106
106
Args:
107
107
requirements: List of Requirement objects
108
-
108
+
109
109
Returns:
110
110
Dictionary with categorized dependencies
111
111
"""
@@ -115,15 +115,15 @@ def categorize_dependencies(
115
115
'optional' : {},
116
116
'extras' : {}
117
117
}
118
-
118
+
119
119
for req in requirements :
120
120
if not req .marker :
121
121
# No marker means it's a runtime dependency
122
122
categories ['runtime' ].append (req )
123
123
continue
124
-
124
+
125
125
marker_str = str (req .marker )
126
-
126
+
127
127
# Check for extra dependencies
128
128
if 'extra ==' in marker_str :
129
129
extra_match = re .search (r'extra\s*==\s*["\']([^"\']+)["\']' , marker_str )
@@ -133,45 +133,45 @@ def categorize_dependencies(
133
133
categories ['extras' ][extra_name ] = []
134
134
categories ['extras' ][extra_name ].append (req )
135
135
continue
136
-
136
+
137
137
# Check for development dependencies
138
138
if any (keyword in marker_str .lower () for keyword in ['dev' , 'test' , 'lint' , 'doc' ]):
139
139
categories ['development' ].append (req )
140
140
else :
141
141
categories ['runtime' ].append (req )
142
-
142
+
143
143
return categories
144
-
145
- def extract_package_names (self , requirements : List [Requirement ]) -> Set [str ]:
144
+
145
+ def extract_package_names (self , requirements : list [Requirement ]) -> set [str ]:
146
146
"""Extract package names from requirements.
147
-
147
+
148
148
Args:
149
149
requirements: List of Requirement objects
150
-
150
+
151
151
Returns:
152
152
Set of package names
153
153
"""
154
154
return {req .name .lower () for req in requirements }
155
-
156
- def get_version_constraints (self , req : Requirement ) -> Dict [str , Any ]:
155
+
156
+ def get_version_constraints (self , req : Requirement ) -> dict [str , Any ]:
157
157
"""Get version constraints from a requirement.
158
-
158
+
159
159
Args:
160
160
req: Requirement object
161
-
161
+
162
162
Returns:
163
163
Dictionary with version constraint information
164
164
"""
165
165
if not req .specifier :
166
166
return {'constraints' : [], 'allows_any' : True }
167
-
167
+
168
168
constraints = []
169
169
for spec in req .specifier :
170
170
constraints .append ({
171
171
'operator' : spec .operator ,
172
172
'version' : str (spec .version )
173
173
})
174
-
174
+
175
175
return {
176
176
'constraints' : constraints ,
177
177
'allows_any' : len (constraints ) == 0 ,
0 commit comments