Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ registry:
python:
package_type: "stub-only"
# https://github.com/nipunn1313/mypy-protobuf/blob/v5.1.0/pyproject.toml#L21
requires_python: ">=3.8"
# Bumped to match protobuf requirement.
requires_python: ">=3.9"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protobuf version below requires 3.9 so needed to bump these.

deps:
# https://github.com/nipunn1313/mypy-protobuf/tree/v5.1.0#requirements-to-run-typecheckers-on-stubs-generated-by-mypy-protobuf
# > Earlier releases might work, but aren't tested.
Expand Down
3 changes: 2 additions & 1 deletion plugins/community/nipunn1313-mypy/v5.1.0/buf.plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ registry:
python:
package_type: "stub-only"
# https://github.com/nipunn1313/mypy-protobuf/blob/v5.1.0/pyproject.toml#L21
requires_python: ">=3.8"
# Bumped to match protobuf requirement.
requires_python: ">=3.9"
deps:
# https://github.com/nipunn1313/mypy-protobuf/tree/v5.1.0#requirements-to-run-typecheckers-on-stubs-generated-by-mypy-protobuf
# > Earlier releases might work, but aren't tested.
Expand Down
119 changes: 99 additions & 20 deletions tests/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os/exec"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -59,6 +60,25 @@ plugins:
protocGenPluginTemplate = template.Must(template.New("protoc-gen-plugin").Parse(`#!/bin/bash

exec docker run --log-driver=none --rm -i {{.ImageName}}:{{.Version}} "$@"
`))
pyprojectTemplate = template.Must(template.New("pyproject.toml").Parse(`[project]
name = {{ printf "%q" .Name }}
version = {{ printf "%q" .Version }}
{{- if .RequiresPython }}
requires-python = {{ printf "%q" .RequiresPython }}
{{- end }}
dependencies = [
{{- range .Deps }}
{{ printf "%q" . }},
{{- end }}
]
{{- if .Sources }}

[tool.uv.sources]
{{- range .Sources }}
{{ .Name }} = { path = {{ printf "%q" .Path }} }
{{- end }}
{{- end }}
`))
images = []string{
"eliza",
Expand Down Expand Up @@ -458,34 +478,93 @@ func cargoReqMatches(req, version string) bool {
func TestPyPIDependencies(t *testing.T) {
t.Parallel()
ctx := t.Context()
plugins := loadFilteredPlugins(t)
for _, p := range plugins {
if p.Registry.Python == nil || len(p.Registry.Python.Deps) == 0 {
allPlugins := loadAllPlugins(t)
pluginByRef := make(map[string]*plugin.Plugin, len(allPlugins))
for _, p := range allPlugins {
pluginByRef[p.String()] = p
}
for _, p := range loadFilteredPlugins(t) {
if p.Registry.Python == nil {
continue
}
// https://docs.astral.sh/uv/getting-started/installation/
_, err := exec.LookPath("uv")
require.NoError(t, err, "uv must be installed to run this test")
t.Run(fmt.Sprintf("%s/%s@%s", p.Identity.Owner(), p.Identity.Plugin(), p.PluginVersion), func(t *testing.T) {
t.Parallel()
_, err := exec.LookPath("uv")
require.NoError(t, err, "uv must be installed to run this test")
// Model every plugin in the dependency closure as its own
// distribution, matching how the BSR publishes one SDK package per
// plugin. Resolving the graph rather than a flattened list of PyPI
// specifiers also verifies that each plugin's requires_python is
// compatible with the SDKs it depends on.
tmpdir := t.TempDir()
writePythonSDKProjects(t, tmpdir, p, pluginByRef, make(map[string]bool))
// uv lock resolves the whole graph against PyPI without creating a
// virtualenv or installing anything.
uvLockCmd := exec.CommandContext(ctx, "uv", "lock")
uvLockCmd.Dir = filepath.Join(tmpdir, pythonSDKDirName(p))
output, err := uvLockCmd.CombinedOutput()
require.NoErrorf(t, err, "uv lock failed - output: %s", string(output))
})
}
}

uvInitCmd := exec.CommandContext(ctx, "uv", "init")
uvInitCmd.Dir = tmpdir
output, err := uvInitCmd.CombinedOutput()
require.NoErrorf(t, err, "uv init failed - output: %s", string(output))

// Make sure we can add all dependencies with `uv add`, which will lookup
// the dependency specifier from pypi and make sure that we can resolve
// versions.
for _, dep := range p.Registry.Python.Deps {
uvInitCmd := exec.CommandContext(ctx, "uv", "add", dep)
uvInitCmd.Dir = tmpdir
output, err := uvInitCmd.CombinedOutput()
require.NoErrorf(t, err, "uv add failed - output: %s", string(output))
}
// pythonSDKDistName returns the distribution name used to model the Python SDK
// generated by a plugin.
func pythonSDKDistName(p *plugin.Plugin) string {
return p.Identity.Owner() + "-" + p.Identity.Plugin()
}

// pythonSDKDirName returns the directory holding a plugin's generated pyproject.toml.
func pythonSDKDirName(p *plugin.Plugin) string {
return pythonSDKDistName(p) + "-" + strings.TrimPrefix(p.PluginVersion, "v")
}

// writePythonSDKProjects writes a pyproject.toml for pythonPlugin and for every
// plugin in its dependency closure, wiring plugin deps as path dependencies.
func writePythonSDKProjects(
t *testing.T,
dir string,
pythonPlugin *plugin.Plugin,
pluginByRef map[string]*plugin.Plugin,
written map[string]bool,
) {
t.Helper()
if written[pythonPlugin.String()] {
return
}
written[pythonPlugin.String()] = true
type pathSource struct {
Name string
Path string
}
deps := slices.Clone(pythonPlugin.Registry.Python.Deps)
var sources []pathSource
for _, pluginDependency := range pythonPlugin.Deps {
pluginDep, ok := pluginByRef[pluginDependency.Plugin]
require.Truef(t, ok, "dependency %q of plugin %q not found", pluginDependency.Plugin, pythonPlugin.String())
if pluginDep.Registry.Python == nil {
continue
}
deps = append(deps, pythonSDKDistName(pluginDep))
sources = append(sources, pathSource{
Name: pythonSDKDistName(pluginDep),
Path: "../" + pythonSDKDirName(pluginDep),
})
writePythonSDKProjects(t, dir, pluginDep, pluginByRef, written)
}
slices.Sort(deps)
slices.SortFunc(sources, func(a, b pathSource) int { return cmp.Compare(a.Name, b.Name) })
projectDir := filepath.Join(dir, pythonSDKDirName(pythonPlugin))
require.NoError(t, os.MkdirAll(projectDir, 0o755))
var pyproject bytes.Buffer
require.NoError(t, pyprojectTemplate.Execute(&pyproject, map[string]any{
"Name": pythonSDKDistName(pythonPlugin),
"Version": strings.TrimPrefix(pythonPlugin.PluginVersion, "v"),
"RequiresPython": pythonPlugin.Registry.Python.RequiresPython,
"Deps": deps,
"Sources": sources,
}))
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "pyproject.toml"), pyproject.Bytes(), 0o644))
}

func TestRegistryDepsHaveRegistryConfig(t *testing.T) {
Expand Down
Loading