Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/uv support #284

Merged
merged 7 commits into from
Sep 1, 2024
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
18 changes: 9 additions & 9 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/backends/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
var languageBackends = []api.LanguageBackend{
python.PythonPoetryBackend,
python.PythonPipBackend,
python.PythonUvBackend,
nodejs.BunBackend,
nodejs.NodejsNPMBackend,
nodejs.NodejsPNPMBackend,
Expand Down
199 changes: 192 additions & 7 deletions internal/backends/python/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,27 @@ type pyprojectTOMLGroup struct {
// pyprojectTOML represents the relevant parts of a pyproject.toml
// file.
type pyprojectTOML struct {
BuildSystem *struct {
Requires []string `toml:"requires"`
BuildBackend string `toml:"build-backend"`
} `toml:"build-system"`
Project *struct {
Dependencies []string `toml:"dependencies"`
} `toml:"project"`
Tool struct {
Poetry *struct {
Name string `json:"name"`
Name string `toml:"name"`
// interface{} because they can be either
// strings or maps (why?? good lord).
Dependencies map[string]interface{} `json:"dependencies"`
DevDependencies map[string]interface{} `json:"dev-dependencies"`
Packages []pyprojectPackageCfg `json:"packages"`
Group map[string]pyprojectTOMLGroup `json:"group"`
} `json:"poetry"`
} `json:"tool"`
Dependencies map[string]interface{} `toml:"dependencies"`
DevDependencies map[string]interface{} `toml:"dev-dependencies"`
Packages []pyprojectPackageCfg `toml:"packages"`
Group map[string]pyprojectTOMLGroup `toml:"group"`
} `toml:"poetry"`
Uv *struct {
Sources map[string]interface{} `toml:"sources"`
} `toml:"uv"`
} `toml:"tool"`
}

// poetryLock represents the relevant parts of a poetry.lock file, in
Expand All @@ -80,6 +90,28 @@ type poetryLock struct {
} `json:"package"`
}

type uvLock struct {
Version int `toml:"version"`
RequiresPython string `toml:"requires-python"`
Packages []struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source struct {
Registry string `toml:"registry"`
} `toml:"source"`
Sdist struct {
URL string `toml:"url"`
Hash string `toml:"hash"`
Size int `toml:"size"`
} `toml:"sdist"`
Wheels []struct {
URL string `toml:"url"`
Hash string `toml:"hash"`
Size int `toml:"size"`
} `toml:"wheels"`
} `toml:"package"`
}

func pep440Join(name api.PkgName, spec api.PkgSpec) string {
if spec == "" {
return string(name)
Expand Down Expand Up @@ -490,6 +522,14 @@ func makePythonPipBackend() api.LanguageBackend {
b := api.LanguageBackend{
Name: "python3-pip",
Specfile: "requirements.txt",
IsSpecfileCompatible: func(path string) (bool, error) {
cfg, err := readPyproject()
if err != nil {
return true, nil
}

return cfg.Tool.Poetry == nil, nil
},
IsAvailable: func() bool {
_, err := exec.LookPath("pip")
return err == nil
Expand Down Expand Up @@ -657,6 +697,151 @@ func makePythonPipBackend() api.LanguageBackend {
return b
}

// makePythonUvBackend returns a backend for invoking uv.
func makePythonUvBackend() api.LanguageBackend {
listUvSpecfile := func() map[api.PkgName]api.PkgSpec {
cfg, err := readPyproject()
if err != nil {
return nil
}

pkgs := map[api.PkgName]api.PkgSpec{}

for _, dep := range cfg.Project.Dependencies {
var name *api.PkgName
var spec *api.PkgSpec

matches := matchPackageAndSpec.FindSubmatch([]byte(dep))
if len(matches) > 1 {
_name := api.PkgName(string(matches[1]))
name = &_name
}
if len(matches) > 2 {
_spec := api.PkgSpec(string(matches[2]))
spec = &_spec
} else {
_spec := api.PkgSpec("")
spec = &_spec
}
pkgs[*name] = *spec
}

return pkgs
}
b := api.LanguageBackend{
Name: "python3-uv",
Specfile: "pyproject.toml",
IsSpecfileCompatible: func(path string) (bool, error) {
cfg, err := readPyproject()
if err != nil {
return false, err
}

return cfg.Tool.Poetry == nil, nil
},
Lockfile: "uv.lock",
IsAvailable: func() bool {
_, err := exec.LookPath("uv")
return err == nil
},
Alias: "python-python3-uv",
FilenamePatterns: []string{"*.py"},
Quirks: api.QuirksAddRemoveAlsoInstalls | api.QuirksAddRemoveAlsoLocks,
NormalizePackageArgs: normalizePackageArgs,
NormalizePackageName: normalizePackageName,
GetPackageDir: func() string {
pkgdir := commonGuessPackageDir()
if pkgdir != "" {
return pkgdir
}

return ""
},
SortPackages: pkg.SortPrefixSuffix(normalizePackageName),

Search: searchPypi,
Info: info,
Add: func(ctx context.Context, pkgs map[api.PkgName]api.PkgSpec, projectName string) {
//nolint:ineffassign,wastedassign,staticcheck
span, ctx := tracer.StartSpanFromContext(ctx, "uv (init) add")
defer span.Finish()
// Initalize the specfile if it doesnt exist
if !util.Exists("pyproject.toml") {
cmd := []string{"uv", "init", "--no-progress"}

if projectName != "" {
cmd = append(cmd, "--name", projectName)
}

util.RunCmd(cmd)
}

cmd := []string{"uv", "add"}
for name, spec := range pkgs {
if found, ok := moduleToPypiPackageAliases[string(name)]; ok {
delete(pkgs, name)
name = api.PkgName(found)
pkgs[api.PkgName(name)] = api.PkgSpec(spec)
}

cmd = append(cmd, pep440Join(name, spec))
}
util.RunCmd(cmd)
},
Lock: func(ctx context.Context) {
//nolint:ineffassign,wastedassign,staticcheck
span, ctx := tracer.StartSpanFromContext(ctx, "poetry lock")
defer span.Finish()
util.RunCmd([]string{"uv", "lock"})
},
Remove: func(ctx context.Context, pkgs map[api.PkgName]bool) {
//nolint:ineffassign,wastedassign,staticcheck
span, ctx := tracer.StartSpanFromContext(ctx, "uv uninstall")
defer span.Finish()

cmd := []string{"uv", "remove"}
for name := range pkgs {
cmd = append(cmd, string(name))
}
util.RunCmd(cmd)
},
Install: func(ctx context.Context) {
//nolint:ineffassign,wastedassign,staticcheck
span, ctx := tracer.StartSpanFromContext(ctx, "uv install")
defer span.Finish()

util.RunCmd([]string{"uv", "sync"})
},
ListSpecfile: func(mergeAllGroups bool) map[api.PkgName]api.PkgSpec {
pkgs := listUvSpecfile()
return pkgs
},
ListLockfile: func() map[api.PkgName]api.PkgVersion {
var cfg uvLock
if _, err := toml.DecodeFile("uv.lock", &cfg); err != nil {
util.DieProtocol("%s", err.Error())

}
pkgs := map[api.PkgName]api.PkgVersion{}
for _, pkgObj := range cfg.Packages {
pkgs[api.PkgName(pkgObj.Name)] = api.PkgVersion(pkgObj.Version)
}
return pkgs
},
GuessRegexps: pythonGuessRegexps,
Guess: guess,
InstallReplitNixSystemDependencies: func(ctx context.Context, pkgs []api.PkgName) {
// Ignore the error here, because if we can't read the specfile,
// we still want to add the deps from above at least.
specfilePkgs := listUvSpecfile()
commonInstallNixDeps(ctx, pkgs, specfilePkgs)
},
}

return b
}

// PythonPoetryBackend is a UPM backend for Python 3 that uses Poetry.
var PythonPoetryBackend = makePythonPoetryBackend()
var PythonPipBackend = makePythonPipBackend()
var PythonUvBackend = makePythonUvBackend()
2 changes: 2 additions & 0 deletions nix/devshell/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
python310Full,
python310Packages,
golangci-lint,
uv,
}:
mkShell {
name = "upm";
Expand All @@ -24,5 +25,6 @@ mkShell {
python310Packages.pip
poetry
python310Full
uv
];
}
2 changes: 1 addition & 1 deletion test-suite/Add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestAdd(t *testing.T) {
case "bun":
pkgs = []string{"lodash", "react", "@replit/protocol"}

case "python3-poetry", "python3-pip":
case "python3-poetry", "python3-pip", "python3-uv":
pkgs = []string{"replit-ai", "flask >=2", "pyyaml", "discord-py 2.3.2"}

default:
Expand Down
2 changes: 1 addition & 1 deletion test-suite/Guess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestGuess(t *testing.T) {
}
}

case "python3-poetry", "python3-pip":
case "python3-poetry", "python3-pip", "python3-uv":
for _, ext := range []string{"py"} {
_, ok := tests[ext]
if !ok {
Expand Down
2 changes: 1 addition & 1 deletion test-suite/Info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestInfo(t *testing.T) {
case "bun":
doInfo(bt, "express", "@replit/crosis", "@distube/spotify")

case "python3-poetry", "python3-pip":
case "python3-poetry", "python3-pip", "python3-uv":
doInfo(bt, "Flask", "replit-ai")

default:
Expand Down
1 change: 1 addition & 0 deletions test-suite/Install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var testInstalls = map[string]bool{
"nodejs-yarn": true,
"python3-poetry": true,
"python3-pip": true,
"python3-uv": true,
}

func TestInstall(t *testing.T) {
Expand Down
12 changes: 1 addition & 11 deletions test-suite/List_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestList(t *testing.T) {
},
}

case "python3-poetry":
case "python3-poetry", "python3-pip", "python3-uv":
templatesToPackages = map[string][]string{
"no-deps": {},
"one-dep": {"django"},
Expand All @@ -44,16 +44,6 @@ func TestList(t *testing.T) {
},
}

case "python3-pip":
templatesToPackages = map[string][]string{
"no-deps": {},
"one-dep": {"django"},
"many-deps": {
"django",
"boatman",
"ws4py",
},
}
default:
t.Run(bt.Backend.Name, func(t *testing.T) {
t.Skip("no test")
Expand Down
1 change: 1 addition & 0 deletions test-suite/Lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var testLocks = map[string]bool{
"nodejs-pnpm": true,
"nodejs-yarn": true,
"python3-poetry": true,
"python3-uv": true,
}

func TestLock(t *testing.T) {
Expand Down
8 changes: 1 addition & 7 deletions test-suite/Remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@ func TestRemove(t *testing.T) {
"many-deps": {"express", "eslint", "svelte"},
}

case "python3-poetry":
pkgsToRemove = map[string][]string{
"one-dep": {"django"},
"many-deps": {"django", "boatman", "ws4py"},
}

case "python3-pip":
case "python3-poetry", "python3-pip", "python3-uv":
pkgsToRemove = map[string][]string{
"one-dep": {"django"},
"many-deps": {"django", "boatman", "ws4py"},
Expand Down
2 changes: 1 addition & 1 deletion test-suite/Search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestSearch(t *testing.T) {
{"@replit", "@replit/crosis"},
})

case "python3-poetry", "python3-pip":
case "python3-poetry", "python3-pip", "python3-uv":
doSearch(bt, []searchTest{
{"flask", "Flask"},
{"replit-ai", "replit-ai"},
Expand Down
1 change: 1 addition & 0 deletions test-suite/WhichLanguage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var testWhichLanguage = map[string]bool{
"nodejs-yarn": true,
"python3-poetry": true,
"python3-pip": true,
"python3-uv": true,
}

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