Skip to content
代码片段 群组 项目
未验证 提交 33d9e49c 编辑于 作者: Leaminn Ma's avatar Leaminn Ma 提交者: GitLab
浏览文件

Add support for multiples files for Python Pip config file class

Adds support to find and parse multiple files for the
PythonPip config class. This is because Pip may have
more than one 'requirements' file.

This means that the ConfigFileParser may produce an
array of objects where more than one object may have
the same lang value.

In the next MR, we will update the Repository X-Ray
ScanDependencies service so that it combines the
payloads of config file objects belonging to the
same lang.
上级 431f105b
No related branches found
No related tags found
无相关合并请求
...@@ -55,12 +55,14 @@ def latest_commit_sha ...@@ -55,12 +55,14 @@ def latest_commit_sha
def config_file_classes_by_path def config_file_classes_by_path
CONFIG_FILE_CLASSES.group_by(&:lang).each_with_object({}) do |(_lang, klasses), hash| CONFIG_FILE_CLASSES.group_by(&:lang).each_with_object({}) do |(_lang, klasses), hash|
klasses.each do |klass| klasses.each do |klass|
matching_path = worktree_paths.find { |path| klass.matches?(path) } matching_paths = klass.matching_paths(worktree_paths)
next unless matching_path matching_paths.each do |path|
hash[path] ||= []
hash[path] << klass
end
hash[matching_path] ||= [] break if matching_paths.any?
hash[matching_path] << klass
end end
end end
end end
......
...@@ -36,6 +36,15 @@ def self.matches?(path) ...@@ -36,6 +36,15 @@ def self.matches?(path)
File.fnmatch?("**/#{file_name_glob}", path, File::FNM_PATHNAME | File::FNM_DOTMATCH) File.fnmatch?("**/#{file_name_glob}", path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
end end
# Set to `true` if the dependency manager supports more than one config file
def self.supports_multiple_files?
false
end
def self.matching_paths(paths)
supports_multiple_files? ? paths.select { |p| matches?(p) } : Array.wrap(paths.find { |p| matches?(p) })
end
def initialize(blob) def initialize(blob)
@blob = blob @blob = blob
@content = blob.data @content = blob.data
......
...@@ -13,7 +13,7 @@ module Constants ...@@ -13,7 +13,7 @@ module Constants
# #supported-languages-and-package-managers. # #supported-languages-and-package-managers.
# #
# This ordering affects the result of # This ordering affects the result of
# ConfigFileParser#find_config_file_paths_with_class. # ConfigFileParser#config_file_classes_by_path.
# #
CONFIG_FILE_CLASSES = [ CONFIG_FILE_CLASSES = [
ConfigFiles::CConanPy, ConfigFiles::CConanPy,
......
...@@ -11,14 +11,20 @@ class PythonPip < Base ...@@ -11,14 +11,20 @@ class PythonPip < Base
COMMENT_ONLY_REGEX = /^#/ COMMENT_ONLY_REGEX = /^#/
INLINE_COMMENT_REGEX = /\s+#.*$/ INLINE_COMMENT_REGEX = /\s+#.*$/
# We support nested requirements files by processing all files matching
# this glob. See https://gitlab.com/gitlab-org/gitlab/-/issues/491800.
def self.file_name_glob def self.file_name_glob
'requirements.txt' '*requirements*.txt'
end end
def self.lang_name def self.lang_name
'Python' 'Python'
end end
def self.supports_multiple_files?
true
end
private private
### Example format: ### Example format:
...@@ -32,10 +38,8 @@ def self.lang_name ...@@ -32,10 +38,8 @@ def self.lang_name
# openpyxl == 3.1.2 # openpyxl == 3.1.2
# urllib3 @ https://github.com/path/main.zip # urllib3 @ https://github.com/path/main.zip
# #
# # Nested requirement files currently not supported # # Options
# # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/491800 # -r other_requirements.txt # A nested requirements file
# -r other_requirements.txt
# # Other options
# -i https://pypi.org/simple # -i https://pypi.org/simple
# --python-version 3 # --python-version 3
# #
......
...@@ -57,7 +57,9 @@ ...@@ -57,7 +57,9 @@
lang: 'go', lang: 'go',
valid: true, valid: true,
error_message: nil, error_message: nil,
payload: a_hash_including(libs: [{ name: 'abc.org/mylib (1.3.0)' }, { name: 'golang.org/x/mod (0.5.0)' }]) payload: a_hash_including(
fileName: 'dir1/dir2/go.mod',
libs: [{ name: 'abc.org/mylib (1.3.0)' }, { name: 'golang.org/x/mod (0.5.0)' }])
} }
) )
end end
...@@ -81,17 +83,124 @@ ...@@ -81,17 +83,124 @@
lang: 'c', lang: 'c',
valid: true, valid: true,
error_message: nil, error_message: nil,
payload: a_hash_including(libs: [{ name: 'libiconv (1.17)' }, { name: 'poco (>1.0,<1.9)' }]) payload: a_hash_including(
fileName: 'dir1/dir2/conanfile.txt',
libs: [{ name: 'libiconv (1.17)' }, { name: 'poco (>1.0,<1.9)' }])
}, },
{ {
lang: 'cpp', lang: 'cpp',
valid: true, valid: true,
error_message: nil, error_message: nil,
payload: a_hash_including(libs: [{ name: 'libiconv (1.17)' }, { name: 'poco (>1.0,<1.9)' }]) payload: a_hash_including(
fileName: 'dir1/dir2/conanfile.txt',
libs: [{ name: 'libiconv (1.17)' }, { name: 'poco (>1.0,<1.9)' }])
} }
) )
end end
end end
context 'with files matching multiple config file classes for the same language' do
let_it_be(:project) do
create(:project, :custom_repo, files:
{
'pom.xml' =>
<<~CONTENT,
<project>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
</project>
CONTENT
'dir1/dir2/build.gradle' =>
<<~CONTENT
dependencies {
implementation 'org.codehaus.groovy:groovy:3.+'
"implementation" 'org.ow2.asm:asm:9.6'
}
CONTENT
})
end
it 'returns an object of only the first matching config file class in the order of `CONFIG_FILE_CLASSES`' do
expect(config_files_array).to contain_exactly(
{
lang: 'java',
valid: true,
error_message: nil,
payload: a_hash_including(
fileName: 'dir1/dir2/build.gradle',
libs: [{ name: 'groovy (3.+)' }, { name: 'asm (9.6)' }])
}
)
end
end
context 'with multiple files matching the same config file class' do
context 'when the config file class does not support multiple files' do
let_it_be(:project) do
create(:project, :custom_repo, files:
{
'go.mod' =>
<<~CONTENT,
require abc.org/mylib v1.3.0
CONTENT
'dir1/dir2/go.mod' =>
<<~CONTENT
require golang.org/x/mod v0.5.0
CONTENT
})
end
it 'returns a config file object for only one of the matching files' do
# We can't be sure which file is found first because it depends on the order of the worktree paths
expect(config_files_array.size).to eq(1)
end
end
context 'when the config file class supports multiple files' do
let_it_be(:project) do
create(:project, :custom_repo, files:
{
'requirements.txt' =>
<<~CONTENT,
requests>=2.0,<3.0
numpy==1.26.4
-r dir1/dir2/dev-requirements.txt
CONTENT
'dir1/dir2/dev-requirements.txt' =>
<<~CONTENT
python_dateutil>=2.5.3
fastapi-health!=0.3.0
CONTENT
})
end
it 'returns a config file object for each matching file' do
expect(config_files_array).to contain_exactly(
{
lang: 'python',
valid: true,
error_message: nil,
payload: a_hash_including(
fileName: 'requirements.txt',
libs: [{ name: 'requests (>=2.0,<3.0)' }, { name: 'numpy (==1.26.4)' }])
},
{
lang: 'python',
valid: true,
error_message: nil,
payload: a_hash_including(
fileName: 'dir1/dir2/dev-requirements.txt',
libs: [{ name: 'python_dateutil (>=2.5.3)' }, { name: 'fastapi-health (!=0.3.0)' }])
}
)
end
end
end
end end
end end
......
...@@ -34,6 +34,7 @@ def extract_libs ...@@ -34,6 +34,7 @@ def extract_libs
expect { described_class.file_name_glob }.to raise_error(NotImplementedError) expect { described_class.file_name_glob }.to raise_error(NotImplementedError)
expect { described_class.lang_name }.to raise_error(NotImplementedError) expect { described_class.lang_name }.to raise_error(NotImplementedError)
expect { described_class.new(blob).parse! }.to raise_error(NotImplementedError) expect { described_class.new(blob).parse! }.to raise_error(NotImplementedError)
expect(described_class.supports_multiple_files?).to eq(false)
end end
it 'returns the expected language value' do it 'returns the expected language value' do
...@@ -133,4 +134,34 @@ def extract_libs ...@@ -133,4 +134,34 @@ def extract_libs
end end
end end
end end
describe '.matching_paths' do
let(:paths) { ['other.rb', 'dir/test.json', 'test.txt', 'test.json', 'README.md'] }
subject(:matching_paths) { config_file_class.matching_paths(paths) }
it 'returns the first matching path' do
expect(matching_paths).to contain_exactly('dir/test.json')
end
context 'when multiple files are supported' do
before do
stub_const('ConfigFileClass',
Class.new(described_class) do
def self.file_name_glob
'test.json'
end
def self.supports_multiple_files?
true
end
end
)
end
it 'returns all matching paths' do
expect(matching_paths).to contain_exactly('dir/test.json', 'test.json')
end
end
end
end end
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
expect(described_class.lang).to eq('python') expect(described_class.lang).to eq('python')
end end
it 'supports multiple files' do
expect(described_class.supports_multiple_files?).to eq(true)
end
it_behaves_like 'parsing a valid dependency config file' do it_behaves_like 'parsing a valid dependency config file' do
let(:config_file_content) do let(:config_file_content) do
<<~CONTENT <<~CONTENT
...@@ -21,9 +25,8 @@ ...@@ -21,9 +25,8 @@
urllib3 @ https://github.com/path/main.zip urllib3 @ https://github.com/path/main.zip
requests [security] >= 2.8.1, == 2.8.* requests [security] >= 2.8.1, == 2.8.*
# Nested requirement files currently not supported # Options
-r other_requirements.txt -r other_requirements.txt
# Other options
-i https://pypi.org/simple -i https://pypi.org/simple
--python-version 3 --python-version 3
--no-clean --no-clean
...@@ -54,14 +57,15 @@ ...@@ -54,14 +57,15 @@
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:path, :matches) do where(:path, :matches) do
'requirements.txt' | true 'requirements.txt' | true
'dir/requirements.txt' | true 'dir/requirements_other.txt' | true
'dir/subdir/requirements.txt' | true 'dir/subdir/dev-requirements.txt' | true
'dir/requirements' | false 'dir/requirements.c' | false
'xrequirements.txt' | false 'test_requirements.txt' | true
'Requirements.txt' | false 'devrequirements.txt' | true
'requirements_txt' | false 'Requirements.txt' | false
'requirements' | false 'requirements_txt' | false
'requirements' | false
end end
with_them do with_them do
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册