Skip to content

Commit 3a50007

Browse files
authored
Merge pull request #948 from julia-thorn/use-pip-defaults-homepage
Pip: Fallback Homepage to Project-URLs
2 parents 30a7f6a + 5db6877 commit 3a50007

2 files changed

Lines changed: 150 additions & 5 deletions

File tree

lib/licensed/sources/pip.rb

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def enumerate_dependencies
2020
metadata: {
2121
"type" => self.class.type,
2222
"summary" => package["Summary"],
23-
"homepage" => package["Home-page"]
23+
"homepage" => homepage(package)
2424
}
2525
)
2626
end
@@ -83,14 +83,30 @@ def package_names
8383
end
8484

8585
# Returns a hash filled with package info parsed from the email-header formatted output
86-
# returned by `pip show`
86+
# returned by `pip show --verbose`, including continuation lines for multi-line fields.
8787
def parse_package_info(package_info)
88+
current_key = nil
89+
8890
package_info.lines.each_with_object(Hash.new(0)) do |pkg, a|
89-
next if pkg.start_with?(/^\s/)
91+
if pkg.match?(/^\s/)
92+
if current_key
93+
current_value = a[current_key]
94+
continuation = pkg.strip
95+
a[current_key] =
96+
if current_value.to_s.empty?
97+
continuation
98+
else
99+
"#{current_value}\n#{continuation}"
100+
end
101+
end
102+
next
103+
end
90104

91105
k, v = pkg.split(":", 2)
92106
next if k.nil? || k.empty?
93-
a[k.strip] = v&.strip
107+
108+
current_key = k.strip
109+
a[current_key] = v&.strip
94110
end
95111
end
96112

@@ -101,7 +117,39 @@ def pip_list_command
101117

102118
# Returns the output from `pip show <package> <package> ...`
103119
def pip_show_command(package)
104-
Licensed::Shell.execute(*pip_command, "--disable-pip-version-check", "show", package)
120+
Licensed::Shell.execute(*pip_command, "--disable-pip-version-check", "show", "--verbose", package)
121+
end
122+
123+
# Returns the package homepage from pip package metadata
124+
def homepage(package)
125+
home_page = package["Home-page"]
126+
return home_page unless home_page.to_s.empty?
127+
128+
homepage_from_project_urls(package["Project-URL"] || package["Project-URLs"]) || home_page
129+
end
130+
131+
# Returns best-effort homepage URL extracted from Project-URL(s) metadata
132+
# With priority given to Home > Repository > Source, otherwise the first URL
133+
def homepage_from_project_urls(project_urls)
134+
return if project_urls.to_s.empty?
135+
136+
entries = project_urls
137+
.to_s
138+
.split("\n")
139+
.map(&:strip)
140+
.reject(&:empty?)
141+
142+
candidates = entries.filter_map do |entry|
143+
label, url = entry.split(",", 2).map { |value| value&.strip }
144+
next unless url&.match?(%r{^https?://})
145+
146+
[label.to_s, url]
147+
end
148+
149+
preferred = candidates.find { |label, _| label.match?(/home/i) } ||
150+
candidates.find { |label, _| label.match?(/repo/i) } ||
151+
candidates.find { |label, _| label.match?(/source/i) }
152+
preferred&.last || candidates.first&.last
105153
end
106154

107155
def virtual_env_dir

test/sources/pip_test.rb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,103 @@
44
require "tmpdir"
55

66
describe Licensed::Sources::Pip do
7+
class TestablePipSource < Licensed::Sources::Pip
8+
public :parse_package_info, :homepage
9+
end
10+
11+
let(:config) { Licensed::AppConfiguration.new({ "source_path" => Dir.pwd }) }
12+
let(:source) { TestablePipSource.new(config) }
13+
14+
it "parses pip show continuation lines" do
15+
parsed = source.parse_package_info(
16+
<<~INFO
17+
Name: azure-core
18+
Project-URLs:
19+
Repository, https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
20+
INFO
21+
)
22+
23+
assert_equal(
24+
"Repository, https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core",
25+
parsed["Project-URLs"]
26+
)
27+
end
28+
29+
it "falls back to Project-URLs when Home-page is empty" do
30+
homepage = source.homepage(
31+
{
32+
"Home-page" => "",
33+
"Project-URLs" => "Repository, https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core"
34+
}
35+
)
36+
37+
assert_equal "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core", homepage
38+
end
39+
40+
it "prefers Home URL from Project-URLs even when listed last" do
41+
homepage = source.homepage(
42+
{
43+
"Home-page" => "",
44+
"Project-URLs" => [
45+
"Documentation, https://learn.microsoft.com/azure/",
46+
"Repository, https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core",
47+
"Home, https://azure.microsoft.com/en-us/products/"
48+
].join("\n")
49+
}
50+
)
51+
52+
assert_equal "https://azure.microsoft.com/en-us/products/", homepage
53+
end
54+
55+
it "prefers Repository URL when Home is not present" do
56+
homepage = source.homepage(
57+
{
58+
"Home-page" => "",
59+
"Project-URLs" => [
60+
"Documentation, https://learn.microsoft.com/azure/",
61+
"Repository, https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core",
62+
"Source, https://example.com/source"
63+
].join("\n")
64+
}
65+
)
66+
67+
assert_equal "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core", homepage
68+
end
69+
70+
it "filters malformed and non-http Project-URLs entries" do
71+
homepage = source.homepage(
72+
{
73+
"Home-page" => "",
74+
"Project-URLs" => [
75+
"NoCommaEntry",
76+
"Documentation, ftp://learn.microsoft.com/azure/",
77+
"Documentation, https://learn.microsoft.com/azure/",
78+
"Source, not-a-url"
79+
].join("\n")
80+
}
81+
)
82+
83+
assert_equal "https://learn.microsoft.com/azure/", homepage
84+
end
85+
86+
it "prefers Home-page when present" do
87+
homepage = source.homepage(
88+
{
89+
"Home-page" => "https://example.com/home",
90+
"Project-URLs" => "Repository, https://github.com/example/repo"
91+
}
92+
)
93+
94+
assert_equal "https://example.com/home", homepage
95+
end
96+
97+
it "returns empty Home-page when Project-URLs is a non-string value" do
98+
package = Hash.new(0)
99+
package["Home-page"] = ""
100+
101+
assert_equal "", source.homepage(package)
102+
end
103+
7104
it "finds lowercase dist-info directories for mixed-case package names" do
8105
Dir.mktmpdir do |dir|
9106
dist_info = File.join(dir, "pyjwt-2.12.0.dist-info")

0 commit comments

Comments
 (0)