From 3ccf410f17de276460f4616d45c6cc04ab3a9342 Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Mon, 27 Nov 2023 22:31:14 -0500 Subject: [PATCH] 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 --- commands.go | 4 ++- pythonversion.go | 42 ++++++++++++++++++++++++-- pythonversion_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/commands.go b/commands.go index 89ee540..1286709 100644 --- a/commands.go +++ b/commands.go @@ -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())) diff --git a/pythonversion.go b/pythonversion.go index ce38512..7cd27d6 100644 --- a/pythonversion.go +++ b/pythonversion.go @@ -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 `) 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) diff --git a/pythonversion_test.go b/pythonversion_test.go index 5ca4c0d..bf29a79 100644 --- a/pythonversion_test.go +++ b/pythonversion_test.go @@ -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) + } + +}