1
- # typed: true
1
+ # typed: strict
2
2
# frozen_string_literal: true
3
3
4
4
require "toml-rb"
@@ -13,12 +13,17 @@ module Dependabot
13
13
module Python
14
14
class FileParser
15
15
class PythonRequirementParser
16
+ extend T ::Sig
17
+
18
+ sig { returns ( T ::Array [ Dependabot ::DependencyFile ] ) }
16
19
attr_reader :dependency_files
17
20
21
+ sig { params ( dependency_files : T ::Array [ Dependabot ::DependencyFile ] ) . void }
18
22
def initialize ( dependency_files :)
19
23
@dependency_files = dependency_files
20
24
end
21
25
26
+ sig { returns ( T ::Array [ String ] ) }
22
27
def user_specified_requirements
23
28
[
24
29
pipfile_python_requirement ,
@@ -32,22 +37,28 @@ def user_specified_requirements
32
37
33
38
# TODO: Add better Python version detection using dependency versions
34
39
# (e.g., Django 2.x implies Python 3)
40
+ sig { returns ( T ::Array [ String ] ) }
35
41
def imputed_requirements
36
42
requirement_files . flat_map do |file |
37
- file . content . lines
38
- . select { |l | l . include? ( ";" ) && l . include? ( "python" ) }
39
- . filter_map { |l | l . match ( /python_version(?<req>.*?["'].*?['"])/ ) }
40
- . map { |re | re . named_captures . fetch ( "req" ) . gsub ( /['"]/ , "" ) }
41
- . select { |r | valid_requirement? ( r ) }
43
+ T . must ( file . content ) . lines
44
+ . select { |l | l . include? ( ";" ) && l . include? ( "python" ) }
45
+ . filter_map { |l | l . match ( /python_version(?<req>.*?["'].*?['"])/ ) }
46
+ . map { |re | T . must ( re . named_captures . fetch ( "req" ) ) . gsub ( /['"]/ , "" ) }
47
+ . select { |r | valid_requirement? ( r ) }
42
48
end
43
49
end
44
50
45
51
private
46
52
53
+ # Parses the Pipfile content to extract the Python version requirement.
54
+ #
55
+ # @return [String, nil] the Python version requirement if specified in the Pipfile,
56
+ # or nil if the requirement is not present or does not start with a digit.
57
+ sig { returns ( T . nilable ( String ) ) }
47
58
def pipfile_python_requirement
48
59
return unless pipfile
49
60
50
- parsed_pipfile = TomlRB . parse ( pipfile . content )
61
+ parsed_pipfile = TomlRB . parse ( T . must ( pipfile ) . content )
51
62
requirement =
52
63
parsed_pipfile . dig ( "requires" , "python_full_version" ) ||
53
64
parsed_pipfile . dig ( "requires" , "python_version" )
@@ -56,10 +67,11 @@ def pipfile_python_requirement
56
67
requirement
57
68
end
58
69
70
+ sig { returns ( T . nilable ( String ) ) }
59
71
def pyproject_python_requirement
60
72
return unless pyproject
61
73
62
- pyproject_object = TomlRB . parse ( pyproject . content )
74
+ pyproject_object = TomlRB . parse ( T . must ( pyproject ) . content )
63
75
64
76
# Check for PEP621 requires-python
65
77
pep621_python = pyproject_object . dig ( "project" , "requires-python" )
@@ -72,9 +84,10 @@ def pyproject_python_requirement
72
84
poetry_object &.dig ( "dev-dependencies" , "python" )
73
85
end
74
86
87
+ sig { returns ( T . nilable ( String ) ) }
75
88
def pip_compile_python_requirement
76
89
requirement_files . each do |file |
77
- next unless pip_compile_file_matcher . lockfile_for_pip_compile_file? ( file )
90
+ next unless T . must ( pip_compile_file_matcher ) . lockfile_for_pip_compile_file? ( file )
78
91
79
92
marker = /^# This file is autogenerated by pip-compile with [pP]ython (?<version>\d +.\d +)$/m
80
93
match = marker . match ( file . content )
@@ -86,98 +99,115 @@ def pip_compile_python_requirement
86
99
nil
87
100
end
88
101
102
+ sig { returns ( T . nilable ( String ) ) }
89
103
def python_version_file_version
90
104
return unless python_version_file
91
105
92
106
# read the content, split into lines and remove any lines with '#'
93
- content_lines = python_version_file . content . each_line . map do |line |
107
+ content_lines = T . must ( T . must ( python_version_file ) . content ) . each_line . map do |line |
94
108
line . sub ( /#.*$/ , " " ) . strip
95
109
end . reject ( &:empty? )
96
110
97
111
file_version = content_lines . first
98
112
return if file_version &.empty?
99
- return unless pyenv_versions . include? ( "#{ file_version } \n " )
113
+ return unless T . must ( pyenv_versions ) . include? ( "#{ file_version } \n " )
100
114
101
115
file_version
102
116
end
103
117
118
+ sig { returns ( T . nilable ( String ) ) }
104
119
def runtime_file_python_version
105
120
return unless runtime_file
106
121
107
- file_version = runtime_file . content
108
- . match ( /(?<=python-).*/ ) &.to_s &.strip
122
+ file_version = T . must ( T . must ( runtime_file ) . content )
123
+ . match ( /(?<=python-).*/ ) &.to_s &.strip
109
124
return if file_version &.empty?
110
- return unless pyenv_versions . include? ( "#{ file_version } \n " )
125
+ return unless T . must ( pyenv_versions ) . include? ( "#{ file_version } \n " )
111
126
112
127
file_version
113
128
end
114
129
130
+ sig { returns ( T . nilable ( String ) ) }
115
131
def setup_file_requirement
116
132
return unless setup_file
117
133
118
- req = setup_file . content
119
- . match ( /python_requires\s *=\s *['"](?<req>[^'"]+)['"]/ )
120
- &.named_captures &.fetch ( "req" ) &.strip
134
+ req = T . must ( T . must ( setup_file ) . content )
135
+ . match ( /python_requires\s *=\s *['"](?<req>[^'"]+)['"]/ )
136
+ &.named_captures &.fetch ( "req" ) &.strip
121
137
122
138
requirement_class . new ( req )
123
139
req
124
140
rescue Gem ::Requirement ::BadRequirementError
125
141
nil
126
142
end
127
143
144
+ sig { returns ( T . nilable ( String ) ) }
128
145
def pyenv_versions
129
- @pyenv_versions ||= run_command ( "pyenv install --list" )
146
+ @pyenv_versions = T . let ( run_command ( "pyenv install --list" ) , T . nilable ( String ) )
130
147
end
131
148
149
+ sig { params ( command : String , env : T ::Hash [ String , String ] ) . returns ( String ) }
132
150
def run_command ( command , env : { } )
133
151
SharedHelpers . run_shell_command ( command , env : env , stderr_to_stdout : true )
134
152
end
135
153
154
+ sig { returns ( T . nilable ( PipCompileFileMatcher ) ) }
136
155
def pip_compile_file_matcher
137
- @pip_compile_file_matcher ||= PipCompileFileMatcher . new ( pip_compile_files )
156
+ @pip_compile_file_matcher = T . let ( PipCompileFileMatcher . new ( pip_compile_files ) ,
157
+ T . nilable ( PipCompileFileMatcher ) )
138
158
end
139
159
160
+ sig { returns ( T . class_of ( Dependabot ::Python ::Requirement ) ) }
140
161
def requirement_class
141
162
Dependabot ::Python ::Requirement
142
163
end
143
164
165
+ sig { params ( req_string : String ) . returns ( T ::Boolean ) }
144
166
def valid_requirement? ( req_string )
145
167
requirement_class . new ( req_string )
146
168
true
147
169
rescue Gem ::Requirement ::BadRequirementError
148
170
false
149
171
end
150
172
173
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
151
174
def pipfile
152
175
dependency_files . find { |f | f . name == "Pipfile" }
153
176
end
154
177
178
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
155
179
def pipfile_lock
156
180
dependency_files . find { |f | f . name == "Pipfile.lock" }
157
181
end
158
182
183
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
159
184
def pyproject
160
185
dependency_files . find { |f | f . name == "pyproject.toml" }
161
186
end
162
187
188
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
163
189
def setup_file
164
190
dependency_files . find { |f | f . name == "setup.py" }
165
191
end
166
192
193
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
167
194
def python_version_file
168
195
dependency_files . find { |f | f . name == ".python-version" }
169
196
end
170
197
198
+ sig { returns ( T . nilable ( Dependabot ::DependencyFile ) ) }
171
199
def runtime_file
172
200
dependency_files . find { |f | f . name . end_with? ( "runtime.txt" ) }
173
201
end
174
202
203
+ sig { returns ( T ::Array [ Dependabot ::DependencyFile ] ) }
175
204
def requirement_files
176
205
dependency_files . select { |f | f . name . end_with? ( ".txt" ) }
177
206
end
178
207
208
+ sig { returns ( T ::Array [ DependencyFile ] ) }
179
209
def pip_compile_files
180
- dependency_files . select { |f | f . name . end_with? ( ".in" ) }
210
+ @pip_compile_files ||= T . let ( dependency_files . select { |f | f . name . end_with? ( ".in" ) } , T . untyped )
181
211
end
182
212
end
183
213
end
0 commit comments