diff --git a/doc/user/project/repository/code_suggestions/repository_xray.md b/doc/user/project/repository/code_suggestions/repository_xray.md index e1894a273b468927d122d20897789f54c3af612c..54d58cece0ac17c7096475ee2fb903b91cd54ff3 100644 --- a/doc/user/project/repository/code_suggestions/repository_xray.md +++ b/doc/user/project/repository/code_suggestions/repository_xray.md @@ -47,6 +47,7 @@ The Repository X-Ray searches a maximum of two directory levels from the reposit | Java | Gradle | `build.gradle` | 17.4 or later | | Java | Maven | `pom.xml` | 17.4 or later | | Kotlin | Gradle | `build.gradle.kts` | 17.5 or later | +| Python | Pip | `requirements.txt` | 17.5 or later | | Ruby | RubyGems | `Gemfile.lock` | 17.4 or later | ## Enable Repository X-Ray in your CI pipeline (deprecated) diff --git a/ee/lib/ai/context/dependencies/config_files/constants.rb b/ee/lib/ai/context/dependencies/config_files/constants.rb index 06db8164c7cd7ddaca6a2727427e636258fefba1..bd70a8a008c808195a9c04c5030128eb408c4b6d 100644 --- a/ee/lib/ai/context/dependencies/config_files/constants.rb +++ b/ee/lib/ai/context/dependencies/config_files/constants.rb @@ -23,6 +23,7 @@ module Constants ConfigFiles::JavaGradle, ConfigFiles::JavaMaven, ConfigFiles::KotlinGradle, + ConfigFiles::PythonPip, ConfigFiles::RubyGemsLock ].freeze end diff --git a/ee/lib/ai/context/dependencies/config_files/python_pip.rb b/ee/lib/ai/context/dependencies/config_files/python_pip.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ffec846df13d2d4193343fdc686d67eb46de9b5 --- /dev/null +++ b/ee/lib/ai/context/dependencies/config_files/python_pip.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Ai + module Context + module Dependencies + module ConfigFiles + class PythonPip < Base + OPTION_REGEX = /^-/ + NAME_VERSION_REGEX = /(?<name>^[^!=><~]+)(?<version>[!=><~]+.*$)?/ + OTHER_SPECIFIERS_REGEX = /[@;]+.*$/ # Matches URL or other non-version specifiers at the end of line + COMMENT_ONLY_REGEX = /^#/ + INLINE_COMMENT_REGEX = /\s+#.*$/ + + def self.file_name_glob + 'requirements.txt' + end + + def self.lang_name + 'Python' + end + + private + + ### Example format: + # + # requests>=2.0,<3.0 # Version range + # numpy==1.26.4 # Exact version match + # fastapi-health!=0.3.0 # Exclusion + # + # # New supported formats + # pytest >= 2.6.4 ; python_version < '3.8' + # openpyxl == 3.1.2 + # urllib3 @ https://github.com/path/main.zip + # + # # Nested requirement files currently not supported + # # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/491800 + # -r other_requirements.txt + # # Other options + # -i https://pypi.org/simple + # --python-version 3 + # + def extract_libs + content.each_line.filter_map do |line| + line.strip! + next if line.blank? || Regexp.union(COMMENT_ONLY_REGEX, OPTION_REGEX).match?(line) + + parse_lib(line) + end + end + + def parse_lib(line) + line.gsub!(Regexp.union(INLINE_COMMENT_REGEX, OTHER_SPECIFIERS_REGEX), '') + match = NAME_VERSION_REGEX.match(line) + + Lib.new(name: match[:name], version: match[:version]) if match + end + end + end + end + end +end diff --git a/ee/spec/lib/ai/context/dependencies/config_files/python_pip_spec.rb b/ee/spec/lib/ai/context/dependencies/config_files/python_pip_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f0209ab189d4cef20d5edff863e801d473187225 --- /dev/null +++ b/ee/spec/lib/ai/context/dependencies/config_files/python_pip_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ai::Context::Dependencies::ConfigFiles::PythonPip, feature_category: :code_suggestions do + it 'returns the expected language value' do + expect(described_class.lang).to eq('python') + end + + it_behaves_like 'parsing a valid dependency config file' do + let(:config_file_content) do + <<~CONTENT + requests>=2.0,<3.0 # Version range + numpy==1.26.4 # Exact version match + python_dateutil>=2.5.3 + fastapi-health!=0.3.0 + + # New supported formats + pytest >= 2.6.4 ; python_version < '3.8' + openpyxl == 3.1.2 + urllib3 @ https://github.com/path/main.zip + requests [security] >= 2.8.1, == 2.8.* + + # Nested requirement files currently not supported + -r other_requirements.txt + # Other options + -i https://pypi.org/simple + --python-version 3 + --no-clean + -e . + CONTENT + end + + let(:expected_formatted_lib_names) do + [ + 'requests (>=2.0,<3.0)', + 'numpy (==1.26.4)', + 'python_dateutil (>=2.5.3)', + 'fastapi-health (!=0.3.0)', + 'pytest (>= 2.6.4)', + 'openpyxl (== 3.1.2)', + 'urllib3', + 'requests [security] (>= 2.8.1, == 2.8.*)' + ] + end + end + + it_behaves_like 'parsing an invalid dependency config file' do + let(:invalid_config_file_content) { '' } + let(:expected_parsing_error_message) { 'file empty' } + end + + describe '.matches?' do + using RSpec::Parameterized::TableSyntax + + where(:path, :matches) do + 'requirements.txt' | true + 'dir/requirements.txt' | true + 'dir/subdir/requirements.txt' | true + 'dir/requirements' | false + 'xrequirements.txt' | false + 'Requirements.txt' | false + 'requirements_txt' | false + 'requirements' | false + end + + with_them do + it 'matches the file name glob pattern at various directory levels' do + expect(described_class.matches?(path)).to eq(matches) + end + end + end +end