Skip to content

Commit 063a98e

Browse files
authored
feat: support -r flag in requirements.txt files (#174)
1 parent 7aa4c79 commit 063a98e

File tree

6 files changed

+121
-6
lines changed

6 files changed

+121
-6
lines changed

pkg/lockfile/fixtures/pip/file-format-example.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ coverage != 3.5 # Version Exclusion. Anything except version 3.5
1212
Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.*
1313

1414
###### Refer to other requirements files ######
15-
-r other-requirements.txt
15+
-r other-file.txt
1616

1717
###### A particular file ######
1818
./downloads/numpy-1.9.2-cp34-none-win32.whl
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
django==2.2.24
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests==1.2.3
2+
3+
-r ./does-not-exist.txt
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-r ./one-package-constrained.txt
2+
-r ./multiple-packages-mixed.txt
3+
4+
requests==1.2.3
5+
pandas==0.23.4

pkg/lockfile/parse-requirements-txt.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"fmt"
66
"os"
7+
"path/filepath"
78
"regexp"
89
"strings"
910
)
@@ -91,11 +92,11 @@ func isNotRequirementLine(line string) bool {
9192
}
9293

9394
func ParseRequirementsTxt(pathToLockfile string) ([]PackageDetails, error) {
94-
var packages []PackageDetails
95+
packages := map[string]PackageDetails{}
9596

9697
file, err := os.Open(pathToLockfile)
9798
if err != nil {
98-
return packages, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
99+
return []PackageDetails{}, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
99100
}
100101
defer file.Close()
101102

@@ -104,16 +105,33 @@ func ParseRequirementsTxt(pathToLockfile string) ([]PackageDetails, error) {
104105
for scanner.Scan() {
105106
line := removeComments(scanner.Text())
106107

108+
if strings.HasPrefix(line, "-r ") {
109+
details, err := ParseRequirementsTxt(
110+
filepath.Join(filepath.Dir(pathToLockfile), strings.TrimPrefix(line, "-r ")),
111+
)
112+
113+
if err != nil {
114+
return []PackageDetails{}, fmt.Errorf("failed to include %s: %w", line, err)
115+
}
116+
117+
for _, detail := range details {
118+
packages[detail.Name+"@"+detail.Version] = detail
119+
}
120+
121+
continue
122+
}
123+
107124
if isNotRequirementLine(line) {
108125
continue
109126
}
110127

111-
packages = append(packages, parseLine(line))
128+
detail := parseLine(line)
129+
packages[detail.Name+"@"+detail.Version] = detail
112130
}
113131

114132
if err := scanner.Err(); err != nil {
115-
return packages, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
133+
return []PackageDetails{}, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
116134
}
117135

118-
return packages, nil
136+
return pkgDetailsMapToSlice(packages), nil
119137
}

pkg/lockfile/parse-requirements-txt_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ func TestParseRequirementsTxt_FileFormatExample(t *testing.T) {
292292
Ecosystem: lockfile.PipEcosystem,
293293
CompareAs: lockfile.PipEcosystem,
294294
},
295+
{
296+
Name: "django",
297+
Version: "2.2.24",
298+
Ecosystem: lockfile.PipEcosystem,
299+
CompareAs: lockfile.PipEcosystem,
300+
},
295301
})
296302
}
297303

@@ -344,3 +350,85 @@ func TestParseRequirementsTxt_NonNormalizedNames(t *testing.T) {
344350
},
345351
})
346352
}
353+
354+
func TestParseRequirementsTxt_WithMultipleROptions(t *testing.T) {
355+
t.Parallel()
356+
357+
packages, err := lockfile.ParseRequirementsTxt("fixtures/pip/with-multiple-r-options.txt")
358+
359+
if err != nil {
360+
t.Errorf("Got unexpected error: %v", err)
361+
}
362+
363+
expectPackages(t, packages, []lockfile.PackageDetails{
364+
{
365+
Name: "flask",
366+
Version: "0.0.0",
367+
Ecosystem: lockfile.PipEcosystem,
368+
CompareAs: lockfile.PipEcosystem,
369+
},
370+
{
371+
Name: "flask-cors",
372+
Version: "0.0.0",
373+
Ecosystem: lockfile.PipEcosystem,
374+
CompareAs: lockfile.PipEcosystem,
375+
},
376+
{
377+
Name: "pandas",
378+
Version: "0.23.4",
379+
Ecosystem: lockfile.PipEcosystem,
380+
CompareAs: lockfile.PipEcosystem,
381+
},
382+
{
383+
Name: "numpy",
384+
Version: "1.16.0",
385+
Ecosystem: lockfile.PipEcosystem,
386+
CompareAs: lockfile.PipEcosystem,
387+
},
388+
{
389+
Name: "scikit-learn",
390+
Version: "0.20.1",
391+
Ecosystem: lockfile.PipEcosystem,
392+
CompareAs: lockfile.PipEcosystem,
393+
},
394+
{
395+
Name: "sklearn",
396+
Version: "0.0.0",
397+
Ecosystem: lockfile.PipEcosystem,
398+
CompareAs: lockfile.PipEcosystem,
399+
},
400+
{
401+
Name: "requests",
402+
Version: "0.0.0",
403+
Ecosystem: lockfile.PipEcosystem,
404+
CompareAs: lockfile.PipEcosystem,
405+
},
406+
{
407+
Name: "gevent",
408+
Version: "0.0.0",
409+
Ecosystem: lockfile.PipEcosystem,
410+
CompareAs: lockfile.PipEcosystem,
411+
},
412+
{
413+
Name: "requests",
414+
Version: "1.2.3",
415+
Ecosystem: lockfile.PipEcosystem,
416+
CompareAs: lockfile.PipEcosystem,
417+
},
418+
{
419+
Name: "django",
420+
Version: "2.2.24",
421+
Ecosystem: lockfile.PipEcosystem,
422+
CompareAs: lockfile.PipEcosystem,
423+
},
424+
})
425+
}
426+
427+
func TestParseRequirementsTxt_WithBadROption(t *testing.T) {
428+
t.Parallel()
429+
430+
packages, err := lockfile.ParseRequirementsTxt("fixtures/pip/with-bad-r-option.txt")
431+
432+
expectErrContaining(t, err, "could not open")
433+
expectPackages(t, packages, []lockfile.PackageDetails{})
434+
}

0 commit comments

Comments
 (0)