Skip to content

Commit f40505b

Browse files
author
Arjun Sreedharan
authored
Parse python version from Pipfile and request it (#11)
Resolves #10
1 parent 1221cc7 commit f40505b

10 files changed

Lines changed: 212 additions & 18 deletions

File tree

detect.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type Parser interface {
3131
//
3232
// Detection will contribute a Build Plan that provides site-packages,
3333
// and requires cpython and pipenv at build.
34-
func Detect(pipfileLockParser Parser) packit.DetectFunc {
34+
func Detect(pipfileParser, pipfileLockParser Parser) packit.DetectFunc {
3535
return func(context packit.DetectContext) (packit.DetectResult, error) {
3636
_, err := os.Stat(filepath.Join(context.WorkingDir, "Pipfile"))
3737
if err != nil {
@@ -49,18 +49,40 @@ func Detect(pipfileLockParser Parser) packit.DetectFunc {
4949
},
5050
}
5151

52-
cpythonVersion, err := pipfileLockParser.ParseVersion(context.WorkingDir)
52+
lockFileExists, err := fileExists(filepath.Join(context.WorkingDir, "Pipfile.lock"))
5353
if err != nil {
54-
if !errors.Is(err, os.ErrNotExist) {
55-
return packit.DetectResult{}, err
56-
}
54+
return packit.DetectResult{}, packit.Fail.WithMessage("failed trying to stat Pipfile.lock: %w", err)
5755
}
5856

59-
if cpythonVersion != "" {
60-
cpythonRequirement.Metadata = BuildPlanMetadata{
61-
Build: true,
62-
Version: cpythonVersion,
63-
VersionSource: "Pipfile.lock",
57+
if lockFileExists {
58+
cpythonVersion, err := pipfileLockParser.ParseVersion(context.WorkingDir)
59+
if err != nil {
60+
if !errors.Is(err, os.ErrNotExist) {
61+
return packit.DetectResult{}, err
62+
}
63+
}
64+
65+
if cpythonVersion != "" {
66+
cpythonRequirement.Metadata = BuildPlanMetadata{
67+
Build: true,
68+
Version: cpythonVersion,
69+
VersionSource: "Pipfile.lock",
70+
}
71+
}
72+
} else {
73+
cpythonVersion, err := pipfileParser.ParseVersion(context.WorkingDir)
74+
if err != nil {
75+
if !errors.Is(err, os.ErrNotExist) {
76+
return packit.DetectResult{}, err
77+
}
78+
}
79+
80+
if cpythonVersion != "" {
81+
cpythonRequirement.Metadata = BuildPlanMetadata{
82+
Build: true,
83+
Version: cpythonVersion,
84+
VersionSource: "Pipfile",
85+
}
6486
}
6587
}
6688

@@ -82,3 +104,14 @@ func Detect(pipfileLockParser Parser) packit.DetectFunc {
82104
}, nil
83105
}
84106
}
107+
108+
func fileExists(path string) (bool, error) {
109+
_, err := os.Stat(path)
110+
if err != nil {
111+
if errors.Is(err, os.ErrNotExist) {
112+
return false, nil
113+
}
114+
return false, err
115+
}
116+
return true, nil
117+
}

detect_test.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import (
1616

1717
func testDetect(t *testing.T, context spec.G, it spec.S) {
1818
var (
19-
Expect = NewWithT(t).Expect
20-
detect packit.DetectFunc
21-
lockParser *fakes.Parser
22-
workingDir string
19+
Expect = NewWithT(t).Expect
20+
detect packit.DetectFunc
21+
lockParser *fakes.Parser
22+
pipfileParser *fakes.Parser
23+
workingDir string
2324
)
2425

2526
it.Before(func() {
@@ -30,9 +31,10 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
3031
err = ioutil.WriteFile(filepath.Join(workingDir, "Pipfile"), []byte{}, 0644)
3132
Expect(err).NotTo(HaveOccurred())
3233

34+
pipfileParser = &fakes.Parser{}
3335
lockParser = &fakes.Parser{}
3436

35-
detect = pipenvinstall.Detect(lockParser)
37+
detect = pipenvinstall.Detect(pipfileParser, lockParser)
3638
})
3739

3840
context("detection", func() {
@@ -61,7 +63,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
6163
},
6264
},
6365
}))
64-
Expect(lockParser.ParseVersionCall.Receives.Path).To(Equal(workingDir))
66+
Expect(pipfileParser.ParseVersionCall.Receives.Path).To(Equal(workingDir))
6567
})
6668

6769
context("when there is no Pipfile", func() {
@@ -77,6 +79,25 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
7779
})
7880
})
7981

82+
context("when there is a Pipfile.lock", func() {
83+
it.Before(func() {
84+
err := ioutil.WriteFile(filepath.Join(workingDir, "Pipfile.lock"), []byte{}, 0644)
85+
Expect(err).NotTo(HaveOccurred())
86+
})
87+
88+
it.After(func() {
89+
Expect(os.Remove(filepath.Join(workingDir, "Pipfile.lock"))).To(Succeed())
90+
})
91+
92+
it("calls Pipfile lock parser", func() {
93+
_, err := detect(packit.DetectContext{
94+
WorkingDir: workingDir,
95+
})
96+
Expect(err).NotTo(HaveOccurred())
97+
Expect(lockParser.ParseVersionCall.Receives.Path).To(Equal(workingDir))
98+
})
99+
})
100+
80101
context("failure cases", func() {
81102
context("when the Pipfile cannot be read", func() {
82103
it.Before(func() {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ require (
77
github.com/onsi/gomega v1.12.0
88
github.com/paketo-buildpacks/occam v0.1.2
99
github.com/paketo-buildpacks/packit v0.11.0
10+
github.com/pelletier/go-toml v1.9.1 // indirect
1011
github.com/sclevine/spec v1.4.0
1112
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
382382
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
383383
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
384384
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
385+
github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=
386+
github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
385387
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
386388
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
387389
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

init_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ func TestUnitPipenvInstall(t *testing.T) {
1313
suite("Build", testBuild)
1414
suite("InstallProcess", testInstallProcess)
1515
suite("LockParser", testLockParser)
16+
suite("PipfileParser", testPipfileParser)
1617
suite.Run(t)
1718
}

integration/default_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ func testDefault(t *testing.T, context spec.G, it spec.S) {
8383
MatchRegexp(fmt.Sprintf(` PATH -> "/layers/%s/packages/[\w_-]+/bin:\$PATH"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
8484
MatchRegexp(fmt.Sprintf(` PYTHONUSERBASE -> "/layers/%s/packages/[\w_-]+"`, strings.ReplaceAll(buildpackInfo.Buildpack.ID, "/", "_"))),
8585
))
86+
Expect(logs).To(ContainLines(
87+
// Due to Pipfile requirement
88+
MatchRegexp(` Installing CPython 3.7.\d+`),
89+
))
8690

8791
container, err = docker.Container.Run.
8892
WithCommand("gunicorn server:app").

integration/testdata/default_app/Pipfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ itsdangerous = "==0.24"
1111
tox = "*"
1212
coverage = "*"
1313
"flake8" = "*"
14-
flask-testing = "*"
14+
flask-testing = "*"
15+
16+
[requires]
17+
python_version = "3.7"

pipfile_parser.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package pipenvinstall
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/pelletier/go-toml"
8+
)
9+
10+
type PipfileParser struct{}
11+
12+
func NewPipfileParser() PipfileParser {
13+
return PipfileParser{}
14+
}
15+
16+
func (p PipfileParser) ParseVersion(path string) (version string, err error) {
17+
18+
fp, err := os.Open(filepath.Join(path, "Pipfile"))
19+
if err != nil {
20+
return "", err
21+
}
22+
23+
var Pipfile struct {
24+
Requires struct {
25+
PythonVersion string `toml:"python_version"`
26+
} `toml:"requires"`
27+
}
28+
29+
err = toml.NewDecoder(fp).Decode(&Pipfile)
30+
if err != nil {
31+
return "", err
32+
}
33+
34+
return Pipfile.Requires.PythonVersion, nil
35+
}

pipfile_parser_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package pipenvinstall_test
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
. "github.com/onsi/gomega"
10+
pipenvinstall "github.com/paketo-community/pipenv-install"
11+
"github.com/sclevine/spec"
12+
)
13+
14+
func testPipfileParser(t *testing.T, context spec.G, it spec.S) {
15+
var (
16+
Expect = NewWithT(t).Expect
17+
18+
workingDir string
19+
parser pipenvinstall.PipfileParser
20+
)
21+
22+
it.Before(func() {
23+
var err error
24+
workingDir, err = ioutil.TempDir("", "working-dir")
25+
Expect(err).NotTo(HaveOccurred())
26+
27+
parser = pipenvinstall.NewPipfileParser()
28+
})
29+
30+
context("Calling ParseVersion", func() {
31+
context("when Pipfile is valid and specifies a CPython version", func() {
32+
33+
it.Before(func() {
34+
Expect(ioutil.WriteFile(
35+
filepath.Join(workingDir, "Pipfile"),
36+
[]byte(`
37+
[requires]
38+
python_version = "3.8"
39+
`), os.ModePerm)).To(Succeed())
40+
})
41+
42+
it.After(func() {
43+
Expect(os.Remove(filepath.Join(workingDir, "Pipfile"))).To(Succeed())
44+
})
45+
46+
it("parses the Python version", func() {
47+
version, err := parser.ParseVersion(workingDir)
48+
Expect(err).ToNot(HaveOccurred())
49+
Expect(version).To(Equal("3.8"))
50+
})
51+
})
52+
53+
context("failure cases", func() {
54+
context("when Pipfile file cannot be read", func() {
55+
it.Before(func() {
56+
Expect(ioutil.WriteFile(
57+
filepath.Join(workingDir, "Pipfile"),
58+
[]byte(`{}`), os.ModePerm)).To(Succeed())
59+
Expect(os.Chmod(filepath.Join(workingDir, "Pipfile"), 0000)).To(Succeed())
60+
})
61+
62+
it.After(func() {
63+
Expect(os.Remove(filepath.Join(workingDir, "Pipfile"))).To(Succeed())
64+
})
65+
66+
it("returns an error", func() {
67+
_, err := parser.ParseVersion(workingDir)
68+
Expect(err).To(MatchError(ContainSubstring("permission denied")))
69+
})
70+
})
71+
72+
context("when the contents of the Pipfile file are malformed", func() {
73+
it.Before(func() {
74+
Expect(ioutil.WriteFile(
75+
filepath.Join(workingDir, "Pipfile"),
76+
[]byte(`%%%%%%%%`), os.ModePerm)).To(Succeed())
77+
})
78+
79+
it.After(func() {
80+
Expect(os.Remove(filepath.Join(workingDir, "Pipfile"))).To(Succeed())
81+
})
82+
83+
it("returns an error", func() {
84+
_, err := parser.ParseVersion(workingDir)
85+
Expect(err).To(MatchError(ContainSubstring("parsing error")))
86+
})
87+
})
88+
})
89+
})
90+
}

run/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ func main() {
1515
planner := draft.NewPlanner()
1616
logger := scribe.NewEmitter(os.Stdout)
1717
installProcess := pipenvinstall.NewPipenvInstallProcess(pexec.NewExecutable("pipenv"), logger)
18+
pipfileParser := pipenvinstall.NewPipfileParser()
1819
lockParser := pipenvinstall.NewPipfileLockParser()
1920

2021
packit.Run(
21-
pipenvinstall.Detect(lockParser),
22+
pipenvinstall.Detect(
23+
pipfileParser,
24+
lockParser,
25+
),
2226
pipenvinstall.Build(
2327
planner,
2428
installProcess,

0 commit comments

Comments
 (0)