feat: default to system python if no version specified (#10)

* refactor: extract shared logic determining which python version is used

* feat: default to unshimmed python if no version specified

docs: mention of how system-python is evaluated

* refactor: extract python version determining funcs

* wip: ensure that output from system python check is cleaned for stray spaces

* test: coverage for system python fallback

* refactor: unused constant

* chore: update version to 0.0.5
This commit is contained in:
Marc 2023-11-27 18:16:57 -05:00 committed by GitHub
parent 9d144d7ddc
commit 5bf3f36559
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 7 deletions

View file

@ -99,14 +99,35 @@ func ListVersions(args []string, flags Flags, currentState State) error {
return nil
}
// Where prints out the system path to the executable being used by `python`.
func Where(args []string, flags Flags, currentState State) error {
version := currentState.GlobalVersion
tag := VersionStringToStruct(version)
fmt.Println(GetStatePath("runtimes", fmt.Sprintf("py-%s", currentState.GlobalVersion), "bin", fmt.Sprintf("python%s", tag.MajorMinor())))
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
if selectedVersion == "SYSTEM" {
_, sysPath := DetermineSystemPython()
fmt.Println(sysPath)
return nil
}
func Which(args []string, flags Flags, currentState State) error {
fmt.Println(currentState.GlobalVersion)
tag := VersionStringToStruct(selectedVersion)
fmt.Println(GetStatePath("runtimes", fmt.Sprintf("py-%s", selectedVersion), "bin", fmt.Sprintf("python%s", tag.MajorMinor())))
return nil
}
// Which prints out the Python version that will be used by shims. It can be invoked
// directly by the `v which` command.
//
// If no version is set (i.e. none is installed, the specified version is not installed),
// the system version is used and 'SYSTEM' is printed by Which.
func Which(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
if selectedVersion == "SYSTEM" {
sysVersion, _ := DetermineSystemPython()
fmt.Println(sysVersion)
return nil
}
fmt.Println(selectedVersion)
return nil
}

38
pythonversion.go Normal file
View file

@ -0,0 +1,38 @@
package main
import (
"os"
"slices"
"strings"
)
// DetermineSelectedPythonVersion returns the Python runtime version that should be
// used according to v.
//
// By default, 'SYSTEM' is returned, which signals that the non-v-managed Python
// runtime is used.
func DetermineSelectedPythonVersion(currentState State) (string, error) {
if len(currentState.GlobalVersion) != 0 {
return currentState.GlobalVersion, nil
}
return "SYSTEM", nil
}
// DetermineSystemPython returns the unshimmed Python version and path.
// This is done by inspected the output of `which` and `python --version` if v's shims
// are not in $PATH.
func DetermineSystemPython() (string, string) {
currentPathEnv := os.Getenv("PATH")
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, ":"))
whichOut, _ := RunCommand([]string{"which", "python"}, GetStatePath(), true)
versionOut, _ := RunCommand([]string{"python", "--version"}, GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), strings.TrimSpace(whichOut)
}

70
pythonversion_test.go Normal file
View file

@ -0,0 +1,70 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
)
// SetupAndCleanupEnvironment sets up a test directory and
// environment variables before the test and returns a cleanup
// function that can be deferred to cleanup any changes to the
// system.
func SetupAndCleanupEnvironment(t *testing.T) func() {
os.Setenv("V_ROOT", t.TempDir())
return func() {
os.Unsetenv("V_ROOT")
}
}
func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
ioutil.WriteFile(GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
mockSystemPythonPath := t.TempDir()
mockSystemPythonExecPath := path.Join(mockSystemPythonPath, "python")
ioutil.WriteFile(mockSystemPythonExecPath, []byte("#!/bin/bash\necho \"Python 1.2.3\""), 0777)
oldPath := os.Getenv("PATH")
os.Setenv("PATH", fmt.Sprintf("%s:%s:/usr/bin", GetStatePath("shims"), mockSystemPythonPath))
defer os.Setenv("PATH", oldPath)
sysVersion, sysPath := DetermineSystemPython()
if sysVersion != "1.2.3" {
t.Errorf("Expected system Python to be 1.2.3, found %s instead.", sysVersion)
}
if sysPath != mockSystemPythonExecPath {
t.Errorf("Expected system Python path to be %s, found %s instead.", mockSystemPythonExecPath, sysPath)
}
}
func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
// Writing a mock user-defined state.
mockState := State{GlobalVersion: "1.0.0"}
statePath := GetStatePath("state.json")
stateData, _ := json.Marshal(mockState)
ioutil.WriteFile(statePath, stateData, 0750)
version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != mockState.GlobalVersion {
t.Errorf("Expected version to be %s, got %s instead.", mockState.GlobalVersion, version)
}
}
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
defer SetupAndCleanupEnvironment(t)
version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != "SYSTEM" {
t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version)
}
}

13
util.go
View file

@ -30,19 +30,28 @@ func ValidateVersion(version string) error {
return nil
}
// RunCommand is a thin wrapper around running command-line calls
// programmatically. It abstracts common configuration like routing
// output and handling the directory the calls are made from.
func RunCommand(command []string, cwd string, quiet bool) (string, error) {
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = cwd
var out strings.Builder
var errOut strings.Builder
if !quiet {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
cmd.Stdout = &out
cmd.Stderr = &errOut
}
if err := cmd.Run(); err != nil {
return "", err
return errOut.String(), err
}
return "", nil
return out.String(), nil
}

2
v.go
View file

@ -5,7 +5,7 @@ import (
)
const (
Version = "0.0.4"
Version = "0.0.5"
Author = "Marc Cataford <hello@karnov.club>"
Homepage = "https://github.com/mcataford/v"
)