Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
13 changes: 12 additions & 1 deletion cmd/omnibump/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
_ "github.com/chainguard-dev/omnibump/pkg/languages/golang" // Register Go
_ "github.com/chainguard-dev/omnibump/pkg/languages/java" // Register Java (Maven, Gradle, etc.)
_ "github.com/chainguard-dev/omnibump/pkg/languages/php" // Register PHP (Composer, etc.)
_ "github.com/chainguard-dev/omnibump/pkg/languages/python" // Register Python
_ "github.com/chainguard-dev/omnibump/pkg/languages/rust" // Register Rust
charmlog "github.com/charmbracelet/log"
"github.com/spf13/cobra"
Expand All @@ -40,6 +41,8 @@ type rootFlags struct {
dryRun bool
logLevel string
logPolicy []string
tool string
venv string
}

var flags rootFlags
Expand Down Expand Up @@ -75,13 +78,15 @@ func New() *cobra.Command {

// Add root command flags
f := cmd.Flags()
f.StringVarP(&flags.language, "language", "l", "auto", "language to use (auto, java, go, rust, or deprecated: maven)")
f.StringVarP(&flags.language, "language", "l", "auto", "language to use (auto, java, go, python, rust, or deprecated: maven)")
f.StringVar(&flags.depsFile, "deps", "", "dependencies file (deps.yaml, or legacy names)")
f.StringVar(&flags.propertiesFile, "properties", "", "properties file (properties.yaml)")
f.StringVar(&flags.packages, "packages", "", "inline package list (space-separated)")
f.StringVar(&flags.replaces, "replaces", "", "inline replace list (space-separated, format: oldpkg=newpkg@version)")
f.StringVar(&flags.properties, "props", "", "inline properties list (space-separated)")
f.StringVar(&flags.rootDir, "dir", ".", "project root directory")
f.StringVar(&flags.tool, "tool", "", "build tool override (Python: uv, pip, poetry, hatch, pdm, setuptools)")
f.StringVar(&flags.venv, "venv", "", "path to staged Python venv for in-place bumping (Python only)")
f.BoolVar(&flags.tidy, "tidy", false, "run tidy command after update")
f.BoolVar(&flags.showDiff, "show-diff", false, "show diff of changes")
f.BoolVar(&flags.dryRun, "dry-run", false, "simulate update without making changes")
Expand Down Expand Up @@ -329,6 +334,12 @@ func runUpdate(cmd *cobra.Command, _ []string) error { // args unused but requir
updateCfg.Tidy = flags.tidy
updateCfg.ShowDiff = flags.showDiff
updateCfg.DryRun = flags.dryRun
if flags.tool != "" {
updateCfg.Options["tool"] = flags.tool
}
if flags.venv != "" {
updateCfg.Options["venv"] = flags.venv
}

// Perform update
if err := lang.Update(ctx, updateCfg); err != nil {
Expand Down
Binary file added omnibump
Binary file not shown.
187 changes: 187 additions & 0 deletions pkg/languages/python/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
Copyright 2026 Chainguard, Inc.
SPDX-License-Identifier: Apache-2.0
*/

package python

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/chainguard-dev/clog"
"github.com/chainguard-dev/omnibump/pkg/analyzer"
)

// Analyzer implements analyzer.Analyzer for Python projects.
type Analyzer struct{}

// Verify Analyzer implements the analyzer.Analyzer interface at compile time.
var _ analyzer.Analyzer = (*Analyzer)(nil)

// Analyze parses all manifest files in projectPath and returns a dependency map.
func (a *Analyzer) Analyze(ctx context.Context, projectPath string) (*analyzer.AnalysisResult, error) {
_ = ctx

absPath, err := filepath.Abs(projectPath)
if err != nil {
return nil, fmt.Errorf("resolving path: %w", err)
}

manifest, err := DetectManifest(absPath)
if err != nil {
return nil, fmt.Errorf("detecting manifest in %s: %w", absPath, err)
}

result := &analyzer.AnalysisResult{
Language: "python",
Dependencies: make(map[string]*analyzer.DependencyInfo),
Properties: make(map[string]string),
PropertyUsage: make(map[string]int),
Metadata: map[string]any{"buildTool": string(manifest.BuildTool), "manifest": manifest.Type},
}

specs, err := readSpecsFromManifest(manifest)
if err != nil {
return nil, err
}

for _, spec := range specs {
result.Dependencies[spec.Package] = &analyzer.DependencyInfo{
Name: spec.Package,
Version: spec.Version,
UpdateStrategy: "direct",
Metadata: map[string]any{
"specifier": spec.Specifier,
"rawLine": spec.RawLine,
},
}
}

return result, nil
}

// AnalyzeRemote analyzes manifest files provided as raw bytes.
// files is a map of filename to content (e.g. "pyproject.toml" -> bytes).
func (a *Analyzer) AnalyzeRemote(ctx context.Context, files map[string][]byte) (*analyzer.RemoteAnalysisResult, error) {
log := clog.FromContext(ctx)
result := &analyzer.RemoteAnalysisResult{Language: "python"}

for _, name := range manifestPriority {
data, ok := files[name]
if !ok {
continue
}

bt := BuildToolUnknown
var specs []VersionSpec

switch name {
case ManifestPyprojectTOML:
// Write to a temp file to reuse DetectBuildToolFromPyproject
tmp, err := os.CreateTemp("", "pyproject-*.toml")
if err != nil {
return nil, err
}
tmpPath := filepath.Clean(tmp.Name())
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)

Check failure on line 91 in pkg/languages/python/analyzer.go

View workflow job for this annotation

GitHub Actions / lint

G703: Path traversal via taint analysis (gosec)
return nil, err
}
_ = tmp.Close()
defer func() {
if err := os.Remove(tmpPath); err != nil {
log.Warnf("failed to remove temp file: %v", err)
}
}()

bt, _ = DetectBuildToolFromPyproject(tmpPath)
specs, _ = ParsePyprojectDeps(data, bt)
case ManifestRequirementsTxt:
bt = BuildToolPip
specs = ParseRequirements(data)
case ManifestSetupCfg:
bt = BuildToolSetuptools
specs = ParseSetupCfg(data)
case ManifestSetupPy:
bt = BuildToolSetuptools
specs = ParseSetupPy(data)
case ManifestPipfile:
bt = BuildToolPip
specs, _ = ParsePipfile(data)
}

if len(specs) == 0 {
continue
}

ar := &analyzer.AnalysisResult{
Language: "python",
Dependencies: make(map[string]*analyzer.DependencyInfo),
Properties: make(map[string]string),
PropertyUsage: make(map[string]int),
Metadata: map[string]any{"buildTool": string(bt), "manifest": name},
}
for _, spec := range specs {
ar.Dependencies[spec.Package] = &analyzer.DependencyInfo{
Name: spec.Package,
Version: spec.Version,
UpdateStrategy: "direct",
Metadata: map[string]any{
"specifier": spec.Specifier,
"rawLine": spec.RawLine,
},
}
}
result.FileAnalyses = append(result.FileAnalyses, analyzer.FileAnalysis{
FilePath: name,
Analysis: ar,
})
break // Use only the highest-priority manifest
}

return result, nil
}

// RecommendStrategy always recommends direct updates for Python deps.
// Python doesn't have a "property" abstraction like Maven.
func (a *Analyzer) RecommendStrategy(_ context.Context, _ *analyzer.AnalysisResult, deps []analyzer.Dependency) (*analyzer.Strategy, error) {
strategy := &analyzer.Strategy{
DirectUpdates: make([]analyzer.Dependency, 0, len(deps)),
PropertyUpdates: make(map[string]string),
Warnings: []string{},
AffectedDependencies: make(map[string][]string),
}
strategy.DirectUpdates = append(strategy.DirectUpdates, deps...)
return strategy, nil
}

// readSpecsFromManifest reads dependency specs from a detected manifest.
func readSpecsFromManifest(manifest *ManifestInfo) ([]VersionSpec, error) {
data, err := os.ReadFile(manifest.Path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", manifest.Path, err)
}

switch manifest.Type {
case ManifestPyprojectTOML:
return ParsePyprojectDeps(data, manifest.BuildTool)
case ManifestRequirementsTxt:
return ParseRequirements(data), nil
case ManifestSetupCfg:
return ParseSetupCfg(data), nil
case ManifestSetupPy:
return ParseSetupPy(data), nil
case ManifestPipfile:
specs, err := ParsePipfile(data)
if err != nil {
return nil, err
}
return specs, nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnsupportedManifestType, manifest.Type)
}
}
Loading
Loading