feat: which + version commands (#13)

* refactor: simplify logic that determines which Python version is used

* feat: which to output path only, version to output full current version meta

* fix: command used in shims

* chore: bump version to 0.0.7
This commit is contained in:
Marc 2023-11-28 00:19:34 -05:00 committed by GitHub
parent 3ccf410f17
commit 38450e0f72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 46 additions and 62 deletions

View file

@ -21,7 +21,7 @@ var SHIMS = []string{
const DEFAULT_PERMISSION = 0775 const DEFAULT_PERMISSION = 0775
func writeShim(shimPath string) error { func writeShim(shimPath string) error {
shimContent := []byte("#!/bin/bash\n$(v where --raw) $@") shimContent := []byte("#!/bin/bash\n$(v which --raw) $@")
if err := os.WriteFile(shimPath, shimContent, DEFAULT_PERMISSION); err != nil { if err := os.WriteFile(shimPath, shimContent, DEFAULT_PERMISSION); err != nil {
return err return err
} }
@ -99,18 +99,18 @@ func ListVersions(args []string, flags Flags, currentState State) error {
return nil return nil
} }
// Where prints out the system path to the executable being used by `python`. // Which prints out the system path to the executable being used by `python`.
func Where(args []string, flags Flags, currentState State) error { func Which(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState) selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
var printedPath string var printedPath string
if selectedVersion == "SYSTEM" { if selectedVersion.Source == "system" {
_, sysPath := DetermineSystemPython() _, sysPath := DetermineSystemPython()
printedPath = fmt.Sprintf("%s (system)", sysPath) printedPath = fmt.Sprintf("%s (system)", sysPath)
} else { } else {
tag := VersionStringToStruct(selectedVersion) tag := VersionStringToStruct(selectedVersion.Version)
printedPath = GetStatePath("runtimes", fmt.Sprintf("py-%s", selectedVersion), "bin", fmt.Sprintf("python%s", tag.MajorMinor())) printedPath = GetStatePath("runtimes", fmt.Sprintf("py-%s", selectedVersion.Version), "bin", fmt.Sprintf("python%s", tag.MajorMinor()))
} }
prefix := "Python path: " prefix := "Python path: "
@ -125,28 +125,17 @@ func Where(args []string, flags Flags, currentState State) error {
return nil return nil
} }
// Which prints out the Python version that will be used by shims. It can be invoked // CurrentVersion (called via `v version`) outputs the currently selected version
// directly by the `v which` command. // and what configures it. If the version is configured by a file, the file is returned
// // under "source", if the system Python is used, "system" is returned as a source.
// If no version is set (i.e. none is installed, the specified version is not installed), func CurrentVersion(args []string, flags Flags, currentState State) error {
// the system version is used and 'SYSTEM' is printed by Which.
func Which(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState) selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
printedVersion := selectedVersion
if selectedVersion == "SYSTEM" {
sysVersion, _ := DetermineSystemPython()
printedVersion = fmt.Sprintf("%s (system)", sysVersion)
}
prefix := "Python version: "
if flags.RawOutput { if flags.RawOutput {
prefix = "" fmt.Println(selectedVersion.Version)
} else { return nil
printedVersion = Bold(printedVersion) }
}
fmt.Printf("Python version: %s\nSource: %s\n", Bold(selectedVersion.Version), Bold(selectedVersion.Source))
fmt.Printf("%s%s\n", prefix, printedVersion)
return nil return nil
} }

View file

@ -4,13 +4,17 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"slices"
"strings" "strings"
) )
type SelectedVersion struct {
Version string
Source string
}
// SearchForPythonVersionFile crawls up to the system root to find any // SearchForPythonVersionFile crawls up to the system root to find any
// .python-version file that could set the current version. // .python-version file that could set the current version.
func SearchForPythonVersionFile() (string, bool) { func SearchForPythonVersionFile() (SelectedVersion, bool) {
currentPath, _ := os.Getwd() currentPath, _ := os.Getwd()
var versionFound string var versionFound string
for { for {
@ -30,7 +34,11 @@ func SearchForPythonVersionFile() (string, bool) {
currentPath = nextPath currentPath = nextPath
} }
return versionFound, versionFound != "" if versionFound == "" {
return SelectedVersion{}, false
}
return SelectedVersion{Version: versionFound, Source: path.Join(currentPath, ".python-version")}, true
} }
// DetermineSelectedPythonVersion returns the Python runtime version that should be // DetermineSelectedPythonVersion returns the Python runtime version that should be
@ -40,7 +48,7 @@ func SearchForPythonVersionFile() (string, bool) {
// file that would indicate which version is preferred. If none are found, the global // file that would indicate which version is preferred. If none are found, the global
// user-defined version (via `v use <version>`) is used. If there is none, the system // user-defined version (via `v use <version>`) is used. If there is none, the system
// Python version is used. // Python version is used.
func DetermineSelectedPythonVersion(currentState State) (string, error) { func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error) {
pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile() pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile()
if pythonFileVersionFound { if pythonFileVersionFound {
@ -48,29 +56,17 @@ func DetermineSelectedPythonVersion(currentState State) (string, error) {
} }
if len(currentState.GlobalVersion) != 0 { if len(currentState.GlobalVersion) != 0 {
return currentState.GlobalVersion, nil return SelectedVersion{Version: currentState.GlobalVersion, Source: GetStatePath("state.json")}, nil
} }
return "SYSTEM", nil systemVersion, _ := DetermineSystemPython()
return SelectedVersion{Source: "system", Version: systemVersion}, nil
} }
// DetermineSystemPython returns the unshimmed Python version and path. // DetermineSystemPython returns the unshimmed Python version and path.
// This is done by inspected the output of `which` and `python --version` if v's shims // It assumes that /bin/python is where system Python lives.
// are not in $PATH.
func DetermineSystemPython() (string, string) { func DetermineSystemPython() (string, string) {
currentPathEnv := os.Getenv("PATH") versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, GetStatePath(), true)
pathWithoutShims := slices.DeleteFunc(strings.Split(currentPathEnv, ":"), func(element string) bool {
return element == GetStatePath("shims")
})
// FIXME: This should be set through RunCommand instead.
os.Setenv("PATH", strings.Join(pathWithoutShims, ":"))
defer os.Setenv("PATH", currentPathEnv)
whichOut, _ := RunCommand([]string{"which", "python"}, GetStatePath(), true)
versionOut, _ := RunCommand([]string{"python", "--version"}, GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python") detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), "/bin/python"
return strings.TrimSpace(detectedVersion), strings.TrimSpace(whichOut)
} }

View file

@ -31,18 +31,17 @@ func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
ioutil.WriteFile(GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777) ioutil.WriteFile(GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
mockSystemPythonPath := t.TempDir() mockSystemPythonPath := t.TempDir()
mockSystemPythonExecPath := path.Join(mockSystemPythonPath, "python") mockSystemPythonExecPath := path.Join(mockSystemPythonPath, "python")
ioutil.WriteFile(mockSystemPythonExecPath, []byte("#!/bin/bash\necho \"Python 1.2.3\""), 0777)
oldPath := os.Getenv("PATH") oldPath := os.Getenv("PATH")
os.Setenv("PATH", fmt.Sprintf("%s:%s:/usr/bin", GetStatePath("shims"), mockSystemPythonPath)) os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", GetStatePath("shims")))
defer os.Setenv("PATH", oldPath) defer os.Setenv("PATH", oldPath)
sysVersion, sysPath := DetermineSystemPython() sysVersion, sysPath := DetermineSystemPython()
if sysVersion != "1.2.3" { if sysVersion == "4.5.6" {
t.Errorf("Expected system Python to be 1.2.3, found %s instead.", sysVersion) t.Errorf("Expected system Python to not match the shim, found %s instead.", sysVersion)
} }
if sysPath != mockSystemPythonExecPath { if sysPath != "/bin/python" {
t.Errorf("Expected system Python path to be %s, found %s instead.", mockSystemPythonExecPath, sysPath) t.Errorf("Expected system Python path to be %s, found %s instead.", mockSystemPythonExecPath, sysPath)
} }
} }
@ -62,8 +61,8 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
version, err := DetermineSelectedPythonVersion(ReadState()) version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != "1.2.3" { if err != nil || version.Version != "1.2.3" {
t.Errorf("Expected version to be %s, got %s instead.", "1.2.3", version) t.Errorf("Expected version to be %s, got %s instead.", "1.2.3", version.Version)
} }
} }
@ -78,7 +77,7 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
version, err := DetermineSelectedPythonVersion(ReadState()) version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != mockState.GlobalVersion { if err != nil || version.Version != mockState.GlobalVersion {
t.Errorf("Expected version to be %s, got %s instead.", mockState.GlobalVersion, version) t.Errorf("Expected version to be %s, got %s instead.", mockState.GlobalVersion, version)
} }
} }
@ -88,7 +87,7 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
version, err := DetermineSelectedPythonVersion(ReadState()) version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != "SYSTEM" { if err != nil || version.Source != "system" {
t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version) t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version)
} }
} }
@ -102,7 +101,7 @@ func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
versionFound, found := SearchForPythonVersionFile() versionFound, found := SearchForPythonVersionFile()
if versionFound != "1.2.3" || !found { if versionFound.Version != "1.2.3" || !found {
t.Errorf("Expected \"1.2.3\", found %s", versionFound) t.Errorf("Expected \"1.2.3\", found %s", versionFound)
} }
} }
@ -118,7 +117,7 @@ func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
versionFound, found := SearchForPythonVersionFile() versionFound, found := SearchForPythonVersionFile()
if versionFound != "1.2.3" || !found { if versionFound.Version != "1.2.3" || !found {
t.Errorf("Expected \"1.2.3\", found %s", versionFound) t.Errorf("Expected \"1.2.3\", found %s", versionFound)
} }
@ -129,8 +128,8 @@ func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
versionFound, found := SearchForPythonVersionFile() versionFound, found := SearchForPythonVersionFile()
if versionFound != "" || found { if versionFound.Version != "" || found {
t.Errorf("Did not expect any result, found %s.", versionFound) t.Errorf("Did not expect any result, found %s.", versionFound.Version)
} }
} }

6
v.go
View file

@ -5,7 +5,7 @@ import (
) )
const ( const (
Version = "0.0.6" Version = "0.0.7"
Author = "Marc Cataford <hello@karnov.club>" Author = "Marc Cataford <hello@karnov.club>"
Homepage = "https://github.com/mcataford/v" Homepage = "https://github.com/mcataford/v"
) )
@ -30,9 +30,9 @@ func main() {
).AddCommand( ).AddCommand(
"ls", ListVersions, "v ls", "Lists the installed Python versions.", "ls", ListVersions, "v ls", "Lists the installed Python versions.",
).AddCommand( ).AddCommand(
"where", Where, "v where", "Prints the path to the current Python version.", "version", CurrentVersion, "v version", "Prints the current version and its source.",
).AddCommand( ).AddCommand(
"which", Which, "v which", "Prints the current Python version.", "which", Which, "v which", "Prints the path to the current Python version.",
).AddCommand( ).AddCommand(
"init", Initialize, "v init", "Initializes the v state.", "init", Initialize, "v init", "Initializes the v state.",
).Run(args, currentState) ).Run(args, currentState)