feat: support .python-version to set version (#12)

* feat: add support for defining version via  .python-version

* test: coverage for .python-version support

* fix: double-printing due to untrimmed .python-version read
This commit is contained in:
Marc 2023-11-27 22:31:14 -05:00 committed by GitHub
parent d6bce67296
commit 3ccf410f17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 4 deletions

View file

@ -104,8 +104,10 @@ func Where(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
var printedPath string
if selectedVersion == "SYSTEM" {
_, printedPath = DetermineSystemPython()
_, sysPath := DetermineSystemPython()
printedPath = fmt.Sprintf("%s (system)", sysPath)
} else {
tag := VersionStringToStruct(selectedVersion)
printedPath = GetStatePath("runtimes", fmt.Sprintf("py-%s", selectedVersion), "bin", fmt.Sprintf("python%s", tag.MajorMinor()))

View file

@ -1,17 +1,52 @@
package main
import (
"io/ioutil"
"os"
"path"
"slices"
"strings"
)
// SearchForPythonVersionFile crawls up to the system root to find any
// .python-version file that could set the current version.
func SearchForPythonVersionFile() (string, bool) {
currentPath, _ := os.Getwd()
var versionFound string
for {
content, err := ioutil.ReadFile(path.Join(currentPath, ".python-version"))
if err == nil {
versionFound = strings.TrimSpace(string(content))
break
}
nextPath := path.Dir(currentPath)
if currentPath == nextPath {
break
}
currentPath = nextPath
}
return versionFound, versionFound != ""
}
// 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.
// First, v will look in the current directory and all its parents for a .python-version
// 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
// Python version is used.
func DetermineSelectedPythonVersion(currentState State) (string, error) {
pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile()
if pythonFileVersionFound {
return pythonFileVersion, nil
}
if len(currentState.GlobalVersion) != 0 {
return currentState.GlobalVersion, nil
}
@ -27,8 +62,11 @@ func DetermineSystemPython() (string, string) {
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)

View file

@ -16,6 +16,10 @@ import (
func SetupAndCleanupEnvironment(t *testing.T) func() {
os.Setenv("V_ROOT", t.TempDir())
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
return func() {
os.Unsetenv("V_ROOT")
}
@ -43,6 +47,26 @@ func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
}
}
func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(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)
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
ioutil.WriteFile(path.Join(temporaryWd, ".python-version"), []byte("1.2.3"), 0750)
version, err := DetermineSelectedPythonVersion(ReadState())
if err != nil || version != "1.2.3" {
t.Errorf("Expected version to be %s, got %s instead.", "1.2.3", version)
}
}
func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
@ -60,7 +84,7 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
}
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
defer SetupAndCleanupEnvironment(t)
defer SetupAndCleanupEnvironment(t)()
version, err := DetermineSelectedPythonVersion(ReadState())
@ -68,3 +92,45 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version)
}
}
func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
ioutil.WriteFile(path.Join(temporaryWd, ".python-version"), []byte("1.2.3"), 0750)
versionFound, found := SearchForPythonVersionFile()
if versionFound != "1.2.3" || !found {
t.Errorf("Expected \"1.2.3\", found %s", versionFound)
}
}
func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir()
ioutil.WriteFile(path.Join(temporaryWd, ".python-version"), []byte("1.2.3"), 0750)
os.Mkdir(path.Join(temporaryWd, "child"), 0750)
os.Chdir(path.Join(temporaryWd, "child"))
versionFound, found := SearchForPythonVersionFile()
if versionFound != "1.2.3" || !found {
t.Errorf("Expected \"1.2.3\", found %s", versionFound)
}
}
func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
versionFound, found := SearchForPythonVersionFile()
if versionFound != "" || found {
t.Errorf("Did not expect any result, found %s.", versionFound)
}
}