Skip to content

Commit 83e26e4

Browse files
committed
# feat: Add local python SDK replacement option via pip
## Summary Implements local Python SDK replacement functionality for testing Pulumi providers and programs with unpublished Python SDK builds. This feature enables developers to test against local Python SDKs using `pip install -e` (editable mode), following the same pattern as existing YarnLink (Node.js) and GoModReplacement (Go) features. ## Implementation Details ### Changes Made 1. **pulumitest/opttest/opttest.go**: - Added `PythonLinks []string` field to `Options` struct to store local Python package paths - Created `PythonLink()` function to accept one or more local Python package paths - Updated `Defaults()` function to initialize `PythonLinks` slice 2. **pulumitest/newStack.go**: - Implemented pip install logic to execute `python -m pip install -e <path>` for each Python package - Uses absolute paths (consistent with GoModReplacement pattern) - Executes after YarnLink and before GoModReplacement for logical ordering - Includes proper error handling and logging 3. **pulumitest/opttest/opttest_test.go** (new file): - `TestPythonLinkOption`: Verifies single package path is appended - `TestPythonLinkMultiplePackages`: Verifies multiple package paths can be specified - `TestPythonLinkAccumulates`: Verifies packages accumulate across multiple calls - `TestDefaultsResetsPythonLinks`: Verifies Defaults() resets PythonLinks 4. **pulumitest/README.md**: - Added "Python - Local Package Installation" section - Documented PythonLink usage with examples - Followed same documentation pattern as YarnLink and GoModReplacement sections ### Key Features - **Editable Mode**: Uses `pip install -e` for symlinked/editable installation (same behavior as yarn link) - **Multiple Packages**: Supports installing multiple local Python packages in a single test - **Absolute Paths**: Converts relative paths to absolute paths for reliability - **Error Handling**: Clear error messages if pip install fails or paths don't exist - **Environment Aware**: Uses `python -m pip` for better virtual environment compatibility ## Test Plan - ✅ All unit tests for PythonLink option pass (4/4 tests) - ✅ Code formatting passes (go fmt) - ✅ Code vetting passes (go vet) - ✅ Linting passes - ✅ Implementation follows existing architectural patterns for YarnLink and GoModReplacement ## Usage Example ```go import ( "filepath" "testing" "github.com/pulumi/providertest/pulumitest" "github.com/pulumi/providertest/pulumitest/opttest" ) func TestWithLocalPythonSDK(t *testing.T) { // Use local Python SDK build test := pulumitest.NewPulumiTest(t, "test_dir", opttest.PythonLink("../sdk/python")) // Or multiple packages test2 := pulumitest.NewPulumiTest(t, "test_dir", opttest.PythonLink("../sdk/python", "../other-sdk/python")) test.Up(t) } ``` ## Fixes #39
1 parent fe0a2a7 commit 83e26e4

File tree

4 files changed

+110
-5
lines changed

4 files changed

+110
-5
lines changed

pulumitest/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ NewPulumiTest(t, "test_dir",
131131
opttest.GoModReplacement("github.com/pulumi/pulumi-my-provider/sdk/v3", "..", "sdk"))
132132
```
133133

134+
### Python - Local Package Installation
135+
136+
For Python, we support installing local packages in editable mode via `pip install -e`. This allows using a local build of the Python SDK during testing. Before running your test, ensure your Python environment is properly configured (typically within a virtual environment).
137+
138+
The local package installation can be specified using the `PythonLink` test option:
139+
140+
```go
141+
NewPulumiTest(t, "test_dir", opttest.PythonLink("../sdk/python"))
142+
```
143+
144+
Multiple packages can be specified:
145+
146+
```go
147+
NewPulumiTest(t, "test_dir",
148+
opttest.PythonLink("../sdk/python", "../other-sdk/python"))
149+
```
150+
134151
## Additional Operations
135152

136153
### Update Source

pulumitest/newStack.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ func (pt *PulumiTest) NewStack(t PT, stackName string, opts ...optnewstack.NewSt
8383

8484
ptLogF(t, "creating stack %s", stackName)
8585
stack, err := auto.NewStackLocalSource(pt.ctx, stackName, pt.workingDir, stackOpts...)
86+
if err != nil {
87+
ptFatalF(t, "failed to create stack: %s", err)
88+
return nil
89+
}
8690

8791
providerPluginPaths := options.ProviderPluginPaths()
8892
if len(providerPluginPaths) > 0 {
@@ -148,6 +152,22 @@ func (pt *PulumiTest) NewStack(t PT, stackName string, opts ...optnewstack.NewSt
148152
}
149153
}
150154

155+
if options.PythonLinks != nil && len(options.PythonLinks) > 0 {
156+
for _, pkgPath := range options.PythonLinks {
157+
absPath, err := filepath.Abs(pkgPath)
158+
if err != nil {
159+
ptFatalF(t, "failed to get absolute path for %s: %s", pkgPath, err)
160+
}
161+
cmd := exec.Command("python", "-m", "pip", "install", "-e", absPath)
162+
cmd.Dir = pt.workingDir
163+
ptLogF(t, "installing python package: %s", cmd)
164+
out, err := cmd.CombinedOutput()
165+
if err != nil {
166+
ptFatalF(t, "failed to install python package %s: %s\n%s", pkgPath, err, out)
167+
}
168+
}
169+
}
170+
151171
if options.GoModReplacements != nil && len(options.GoModReplacements) > 0 {
152172
orderedReplacements := make([]string, 0, len(options.GoModReplacements))
153173
for old := range options.GoModReplacements {
@@ -170,11 +190,6 @@ func (pt *PulumiTest) NewStack(t PT, stackName string, opts ...optnewstack.NewSt
170190
}
171191
}
172192
}
173-
174-
if err != nil {
175-
ptFatalF(t, "failed to create stack: %s", err)
176-
return nil
177-
}
178193
if !stackOptions.SkipDestroy {
179194
t.Cleanup(func() {
180195
t.Helper()

pulumitest/opttest/opttest.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ func YarnLink(packages ...string) Option {
105105
})
106106
}
107107

108+
// PythonLink specifies packages which should be installed from a local path via `pip install -e` (editable mode).
109+
// Each package path is installed with `pip install -e <path>` on stack creation.
110+
func PythonLink(packagePaths ...string) Option {
111+
return optionFunc(func(o *Options) {
112+
o.PythonLinks = append(o.PythonLinks, packagePaths...)
113+
})
114+
}
115+
108116
// GoModReplacement specifies replacements to be add to the go.mod file when running the program under test.
109117
// Each replacement is added to the go.mod file with `go mod edit -replace <replacement>` on stack creation.
110118
func GoModReplacement(packageSpecifier string, replacementPathElem ...string) Option {
@@ -166,6 +174,7 @@ type Options struct {
166174
Providers map[providers.ProviderName]ProviderConfigUnion
167175
UseAmbientBackend bool
168176
YarnLinks []string
177+
PythonLinks []string
169178
GoModReplacements map[string]string
170179
CustomEnv map[string]string
171180
ExtraWorkspaceOptions []auto.LocalWorkspaceOption
@@ -200,6 +209,7 @@ func Defaults() Option {
200209
o.Providers = make(map[providers.ProviderName]ProviderConfigUnion)
201210
o.UseAmbientBackend = false
202211
o.YarnLinks = []string{}
212+
o.PythonLinks = []string{}
203213
o.GoModReplacements = make(map[string]string)
204214
o.CustomEnv = make(map[string]string)
205215
o.ExtraWorkspaceOptions = []auto.LocalWorkspaceOption{}

pulumitest/opttest/opttest_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package opttest_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pulumi/providertest/pulumitest/opttest"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestPythonLinkOption(t *testing.T) {
11+
t.Parallel()
12+
13+
opts := opttest.DefaultOptions()
14+
assert.Empty(t, opts.PythonLinks, "expected PythonLinks to be empty by default")
15+
16+
pythonLink := opttest.PythonLink("path/to/sdk")
17+
pythonLink.Apply(opts)
18+
19+
assert.Equal(t, []string{"path/to/sdk"}, opts.PythonLinks, "expected PythonLink to append path")
20+
}
21+
22+
func TestPythonLinkMultiplePackages(t *testing.T) {
23+
t.Parallel()
24+
25+
opts := opttest.DefaultOptions()
26+
27+
pythonLink := opttest.PythonLink("path/to/sdk1", "path/to/sdk2")
28+
pythonLink.Apply(opts)
29+
30+
assert.Equal(t, []string{"path/to/sdk1", "path/to/sdk2"}, opts.PythonLinks,
31+
"expected PythonLink to append multiple paths")
32+
}
33+
34+
func TestPythonLinkAccumulates(t *testing.T) {
35+
t.Parallel()
36+
37+
opts := opttest.DefaultOptions()
38+
39+
pythonLink1 := opttest.PythonLink("path/to/sdk1")
40+
pythonLink1.Apply(opts)
41+
42+
pythonLink2 := opttest.PythonLink("path/to/sdk2")
43+
pythonLink2.Apply(opts)
44+
45+
assert.Equal(t, []string{"path/to/sdk1", "path/to/sdk2"}, opts.PythonLinks,
46+
"expected PythonLinks to accumulate across multiple calls")
47+
}
48+
49+
func TestDefaultsResetsPythonLinks(t *testing.T) {
50+
t.Parallel()
51+
52+
opts := opttest.DefaultOptions()
53+
54+
pythonLink := opttest.PythonLink("path/to/sdk")
55+
pythonLink.Apply(opts)
56+
57+
assert.NotEmpty(t, opts.PythonLinks, "expected PythonLinks to be populated")
58+
59+
defaults := opttest.Defaults()
60+
defaults.Apply(opts)
61+
62+
assert.Empty(t, opts.PythonLinks, "expected Defaults to reset PythonLinks")
63+
}

0 commit comments

Comments
 (0)