Skip to content

Commit

Permalink
feat/uv support (#284)
Browse files Browse the repository at this point in the history
* Combining identical test blocks

* Turns out embed requires all:* in order to get __init__.py

Secret bonus feature

* Adding uv

* Adding uv tests

* Filtering poetry IsSpecfileCompatible on poetry. prefix
  • Loading branch information
blast-hardcheese authored Sep 1, 2024
1 parent 1d9b996 commit e95ee42
Show file tree
Hide file tree
Showing 23 changed files with 394 additions and 40 deletions.
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

0 comments on commit e95ee42

Please sign in to comment.