|
1 | 1 | # pylint: disable=fixme, line-too-long, import-error, no-name-in-module
|
2 | 2 | #
|
3 |
| -# Copyright (c) 2024 Black Duck Software, Inc. |
| 3 | +# Copyright (c) 2025 Black Duck Software Inc. |
4 | 4 | #
|
5 | 5 | # Licensed to the Apache Software Foundation (ASF) under one
|
6 | 6 | # or more contributor license agreements. See the NOTICE file
|
|
35 | 35 | from os import path
|
36 | 36 | import sys
|
37 | 37 | from re import split
|
38 |
| -from pkg_resources import working_set, Requirement |
39 | 38 |
|
40 | 39 | import pip
|
41 | 40 | pip_major_version = int(pip.__version__.split(".")[0])
|
@@ -97,9 +96,6 @@ def resolve_project_node(project_name):
|
97 | 96 | def populate_dependency_tree(project_root_node, requirements_path):
|
98 | 97 | """Resolves the dependencies of the user-provided requirements.txt and appends them to the dependency tree"""
|
99 | 98 | try:
|
100 |
| - # This line is pretty much the only reason why we call the internal pip APIs anymore. We should consider if we |
101 |
| - # can do this with a more generalized approach. |
102 |
| - # --rotte DEC 2020 |
103 | 99 | parsed_requirements = parse_requirements(requirements_path, session=PipSession())
|
104 | 100 | for parsed_requirement in parsed_requirements:
|
105 | 101 | package_name = None
|
@@ -128,43 +124,66 @@ def populate_dependency_tree(project_root_node, requirements_path):
|
128 | 124 |
|
129 | 125 | def recursively_resolve_dependencies(package_name, history):
|
130 | 126 | """Forms a DependencyNode by recursively resolving its dependencies. Tracks history for cyclic dependencies."""
|
131 |
| - package = get_package_by_name(package_name) |
| 127 | + dependency_node, child_names = get_package_by_name(package_name) |
132 | 128 |
|
133 |
| - if package is None: |
| 129 | + if dependency_node is None: |
134 | 130 | return None
|
135 | 131 |
|
136 |
| - dependency_node = DependencyNode(package.project_name, package.version) |
137 |
| - |
138 |
| - if package_name.lower() not in history: |
139 |
| - history.append(package_name.lower()) |
140 |
| - for package_dependency in package.requires(): |
141 |
| - child_node = recursively_resolve_dependencies(package_dependency.key, history) |
| 132 | + if dependency_node.name not in history: |
| 133 | + history.append(dependency_node.name) |
| 134 | + for child_name in child_names: |
| 135 | + child_node = recursively_resolve_dependencies(child_name, history) |
142 | 136 | if child_node is not None:
|
143 | 137 | dependency_node.children = dependency_node.children + [child_node]
|
144 | 138 |
|
145 | 139 | return dependency_node
|
146 | 140 |
|
| 141 | +use_pip_internal_to_search_packages = False |
| 142 | +if sys.version_info.major > 3 or sys.version_info.major == 3 and sys.version_info.minor >= 12: |
| 143 | + # Python version 3.12 is used as a cutoff for the following reasons: |
| 144 | + # * it is the version in which pkg_resources is no longer included by default |
| 145 | + # * a test confirmed that this version of python is incompatible with PIP versions below 21.2, |
| 146 | + # which was the most recent version in which PIP search_packages_info interface changed |
| 147 | + use_pip_internal_to_search_packages = True |
147 | 148 |
|
148 |
| -def get_package_by_name(package_name): |
149 |
| - """Looks up a package from the pip cache""" |
150 |
| - if package_name is None: |
151 |
| - return None |
| 149 | +if use_pip_internal_to_search_packages: |
| 150 | + from pip._internal.commands.show import search_packages_info |
152 | 151 |
|
153 |
| - package_dict = working_set.by_key |
154 |
| - try: |
155 |
| - # TODO: By using pkg_resources.Requirement.parse to get the correct key, we may not need to attempt the other |
156 |
| - # methods. Robust tests are needed to confirm. |
157 |
| - return package_dict[Requirement.parse(package_name).key] |
158 |
| - except: |
159 |
| - pass |
| 152 | + def get_package_by_name(package_name): |
| 153 | + """Looks up a package from the pip cache using internal pip API. |
| 154 | + This should not be used when PIP is older than 21.2 since the internal API was slightly different then. |
| 155 | + """ |
| 156 | + if package_name is None: |
| 157 | + return None, None |
160 | 158 |
|
161 |
| - name_variants = (package_name, package_name.lower(), package_name.replace('-', '_'), package_name.replace('_', '-')) |
162 |
| - for name_variant in name_variants: |
163 |
| - if name_variant in package_dict: |
164 |
| - return package_dict[name_variant] |
| 159 | + package_info = next(search_packages_info([package_name.strip()]), None) |
165 | 160 |
|
166 |
| - return None |
| 161 | + if package_info is None: |
| 162 | + return None, None |
167 | 163 |
|
| 164 | + return DependencyNode(package_info.name, package_info.version), package_info.requires |
| 165 | +else: |
| 166 | + from pkg_resources import working_set, Requirement |
| 167 | + |
| 168 | + def get_package_by_name(package_name): |
| 169 | + """Looks up a package from the pip cache using pkg_resouces""" |
| 170 | + if package_name is None: |
| 171 | + return None, None |
| 172 | + |
| 173 | + package = None |
| 174 | + |
| 175 | + package_dict = working_set.by_key |
| 176 | + try: |
| 177 | + package = package_dict[Requirement.parse(package_name).key] |
| 178 | + except: |
| 179 | + name_variants = (package_name, package_name.lower(), package_name.replace('-', '_'), package_name.replace('_', '-')) |
| 180 | + for name_variant in name_variants: |
| 181 | + if name_variant in package_dict: |
| 182 | + return package_dict[name_variant] |
| 183 | + |
| 184 | + if package is None: |
| 185 | + return None, None |
| 186 | + return DependencyNode(package.project_name, package.version), [requirement.key for requirement in package.requires()] |
168 | 187 |
|
169 | 188 | class DependencyNode(object):
|
170 | 189 | """Represents a python dependency in a tree graph with a name, version, and array of children DependencyNodes"""
|
|
0 commit comments