Skip to content

Commit c3e82b2

Browse files
authored
Merge pull request #480 from github/pip-fixes
Use pip list to get packages for pip source
2 parents 2f2b0e0 + 69aeffd commit c3e82b2

File tree

4 files changed

+39
-116
lines changed

4 files changed

+39
-116
lines changed

.licenses/bundler/bundler.dep.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: bundler
3-
version: 2.3.8
3+
version: 2.3.9
44
type: bundler
55
summary: The best way to manage your application's dependencies
66
homepage: https://bundler.io

docs/sources/pip.md

-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
The pip source uses `pip` CLI commands to enumerate dependencies and properties. It is expected that `pip` is available in the `virtual_env_dir` specific directory before running `licensed`.
44

5-
Your repository root should also contain a `requirements.txt` file which contains all the packages and dependences that are needed. You can generate one with `pip` using the command:
6-
```
7-
pip freeze > requirements.txt
8-
```
9-
105
A `virtualenv` directory is required before running `licensed`. You can setup a `virtualenv` by running the command:
116
```
127
virtualenv <your_venv_dir>

lib/licensed/sources/pip.rb

+34-17
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,14 @@
77
module Licensed
88
module Sources
99
class Pip < Source
10-
VERSION_OPERATORS = %w(< > <= >= == !=).freeze
11-
PACKAGE_REGEX = /^([\w\.-]+)(#{VERSION_OPERATORS.join("|")})?/
10+
PACKAGE_INFO_SEPARATOR = "\n---\n"
1211

1312
def enabled?
14-
return unless virtual_env_pip && Licensed::Shell.tool_available?(virtual_env_pip)
15-
File.exist?(config.pwd.join("requirements.txt"))
13+
virtual_env_pip && Licensed::Shell.tool_available?(virtual_env_pip)
1614
end
1715

1816
def enumerate_dependencies
19-
Parallel.map(packages_from_requirements_txt, in_threads: Parallel.processor_count) do |package_name|
20-
package = package_info(package_name)
17+
packages.map do |package|
2118
location = File.join(package["Location"], package["Name"].gsub("-", "_") + "-" + package["Version"] + ".dist-info")
2219
Dependency.new(
2320
name: package["Name"],
@@ -34,25 +31,45 @@ def enumerate_dependencies
3431

3532
private
3633

37-
def packages_from_requirements_txt
38-
File.read(config.pwd.join("requirements.txt"))
39-
.lines
40-
.reject { |line| line.include?("://") }
41-
.map { |line| line.strip.match(PACKAGE_REGEX) { |match| match.captures.first } }
42-
.compact
34+
# Returns parsed information for all packages used by the project,
35+
# using `pip list` to determine what packages are used and `pip show`
36+
# to gather package information
37+
def packages
38+
all_packages = pip_show_command(package_names)
39+
all_packages.split(PACKAGE_INFO_SEPARATOR).reduce([]) do |accum, val|
40+
accum << parse_package_info(val)
41+
end
42+
end
43+
44+
# Returns the names of all of the packages used by the current project,
45+
# as returned from `pip list`
46+
def package_names
47+
@package_names ||= begin
48+
JSON.parse(pip_list_command).map { |package| package["name"] }
49+
rescue JSON::ParserError => e
50+
message = "Licensed was unable to parse the output from 'npm list'. JSON Error: #{e.message}"
51+
raise Licensed::Sources::Source::Error, message
52+
end
4353
end
4454

45-
def package_info(package_name)
46-
p_info = pip_command(package_name).lines
47-
p_info.each_with_object(Hash.new(0)) { |pkg, a|
55+
# Returns a hash filled with package info parsed from the email-header formatted output
56+
# returned by `pip show`
57+
def parse_package_info(package_info)
58+
package_info.lines.each_with_object(Hash.new(0)) { |pkg, a|
4859
k, v = pkg.split(":", 2)
4960
next if k.nil? || k.empty?
5061
a[k.strip] = v&.strip
5162
}
5263
end
5364

54-
def pip_command(*args)
55-
Licensed::Shell.execute(virtual_env_pip, "--disable-pip-version-check", "show", *args)
65+
# Returns the output from `pip list --format=json`
66+
def pip_list_command
67+
Licensed::Shell.execute(virtual_env_pip, "--disable-pip-version-check", "list", "--format=json")
68+
end
69+
70+
# Returns the output from `pip show <package> <package> ...`
71+
def pip_show_command(packages)
72+
Licensed::Shell.execute(virtual_env_pip, "--disable-pip-version-check", "show", *packages)
5673
end
5774

5875
def virtual_env_pip

test/sources/pip_test.rb

+4-93
Original file line numberDiff line numberDiff line change
@@ -25,109 +25,20 @@
2525
end
2626

2727
describe "dependencies" do
28-
it "detects dependencies without a version constraint" do
29-
Dir.chdir fixtures do
30-
dep = source.dependencies.detect { |d| d.name == "scapy" }
31-
assert dep
32-
assert_equal "pip", dep.record["type"]
33-
assert dep.record["homepage"]
34-
assert dep.record["summary"]
35-
end
36-
end
37-
38-
it "detects dependencies with == version constraint" do
28+
it "detects explicit dependencies" do
3929
Dir.chdir fixtures do
4030
dep = source.dependencies.detect { |d| d.name == "Jinja2" }
4131
assert dep
32+
assert_equal "2.9.6", dep.version
4233
assert_equal "pip", dep.record["type"]
4334
assert dep.record["homepage"]
4435
assert dep.record["summary"]
4536
end
4637
end
4738

48-
it "detects dependencies with >= version constraint" do
49-
Dir.chdir fixtures do
50-
dep = source.dependencies.detect { |d| d.name == "requests" }
51-
assert dep
52-
assert_equal "pip", dep.record["type"]
53-
assert dep.record["homepage"]
54-
assert dep.record["summary"]
55-
end
56-
end
57-
58-
it "detects dependencies with <= version constraint" do
59-
Dir.chdir fixtures do
60-
dep = source.dependencies.detect { |d| d.name == "tqdm" }
61-
assert dep
62-
assert_equal "pip", dep.record["type"]
63-
assert dep.record["homepage"]
64-
assert dep.record["summary"]
65-
end
66-
end
67-
68-
it "detects dependencies with < version constraint" do
69-
Dir.chdir fixtures do
70-
dep = source.dependencies.detect { |d| d.name == "Pillow" }
71-
assert dep
72-
assert_equal "pip", dep.record["type"]
73-
assert dep.record["homepage"]
74-
assert dep.record["summary"]
75-
end
76-
end
77-
78-
it "detects dependencies with > version constraint" do
79-
Dir.chdir fixtures do
80-
dep = source.dependencies.detect { |d| d.name == "Scrapy" }
81-
assert dep
82-
assert_equal "pip", dep.record["type"]
83-
assert dep.record["homepage"]
84-
assert dep.record["summary"]
85-
end
86-
end
87-
88-
it "detects dependencies with != version constraint" do
89-
Dir.chdir fixtures do
90-
dep = source.dependencies.detect { |d| d.name == "numpy" }
91-
assert dep
92-
assert_equal "pip", dep.record["type"]
93-
assert dep.record["homepage"]
94-
assert dep.record["summary"]
95-
end
96-
end
97-
98-
it "detects dependencies with whitespace between the package name and version operator" do
99-
Dir.chdir fixtures do
100-
dep = source.dependencies.detect { |d| d.name == "botocore" }
101-
assert dep
102-
assert_equal "pip", dep.record["type"]
103-
assert dep.record["homepage"]
104-
assert dep.record["summary"]
105-
end
106-
end
107-
108-
it "detects dependencies with multiple version constraints" do
109-
Dir.chdir fixtures do
110-
dep = source.dependencies.detect { |d| d.name == "boto3" }
111-
assert dep
112-
assert_equal "pip", dep.record["type"]
113-
assert dep.record["homepage"]
114-
assert dep.record["summary"]
115-
end
116-
end
117-
118-
it "detects dependencies with hyphens in package name" do
119-
Dir.chdir fixtures do
120-
dep = source.dependencies.detect { |d| d.name == "lazy-object-proxy" }
121-
assert dep
122-
assert_equal "pip", dep.record["type"]
123-
assert dep.record["homepage"]
124-
assert dep.record["summary"]
125-
end
126-
end
127-
128-
it "detects dependencies with dots in package name" do
39+
it "detects transitive dependencies" do
12940
Dir.chdir fixtures do
130-
dep = source.dependencies.detect { |d| d.name == "backports.shutil-get-terminal-size" }
41+
dep = source.dependencies.detect { |d| d.name == "MarkupSafe" }
13142
assert dep
13243
assert_equal "pip", dep.record["type"]
13344
assert dep.record["homepage"]

0 commit comments

Comments
 (0)