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
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 {
return err
}
@ -99,18 +99,18 @@ 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 {
// Which prints out the system path to the executable being used by `python`.
func Which(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
var printedPath string
if selectedVersion == "SYSTEM" {
if selectedVersion.Source == "system" {
_, 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()))
tag := VersionStringToStruct(selectedVersion.Version)
printedPath = GetStatePath("runtimes", fmt.Sprintf("py-%s", selectedVersion.Version), "bin", fmt.Sprintf("python%s", tag.MajorMinor()))
}
prefix := "Python path: "
@ -125,28 +125,17 @@ func Where(args []string, flags Flags, currentState State) error {
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 {
// CurrentVersion (called via `v version`) outputs the currently selected version
// 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.
func CurrentVersion(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
printedVersion := selectedVersion
if selectedVersion == "SYSTEM" {
sysVersion, _ := DetermineSystemPython()
printedVersion = fmt.Sprintf("%s (system)", sysVersion)
}
prefix := "Python version: "
if flags.RawOutput {
prefix = ""
} else {
printedVersion = Bold(printedVersion)
fmt.Println(selectedVersion.Version)
return nil
}
fmt.Printf("%s%s\n", prefix, printedVersion)
fmt.Printf("Python version: %s\nSource: %s\n", Bold(selectedVersion.Version), Bold(selectedVersion.Source))
return nil
}

View file

@ -4,13 +4,17 @@ import (
"io/ioutil"
"os"
"path"
"slices"
"strings"
)
type SelectedVersion struct {
Version string
Source string
}
// SearchForPythonVersionFile crawls up to the system root to find any
// .python-version file that could set the current version.
func SearchForPythonVersionFile() (string, bool) {
func SearchForPythonVersionFile() (SelectedVersion, bool) {
currentPath, _ := os.Getwd()
var versionFound string
for {
@ -30,7 +34,11 @@ func SearchForPythonVersionFile() (string, bool) {
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
@ -40,7 +48,7 @@ func SearchForPythonVersionFile() (string, bool) {
// 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) {
func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error) {
pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile()
if pythonFileVersionFound {
@ -48,29 +56,17 @@ func DetermineSelectedPythonVersion(currentState State) (string, error) {
}
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.
// This is done by inspected the output of `which` and `python --version` if v's shims
// are not in $PATH.
// It assumes that /bin/python is where system Python lives.
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, ":"))
defer os.Setenv("PATH", currentPathEnv)
whichOut, _ := RunCommand([]string{"which", "python"}, GetStatePath(), true)
versionOut, _ := RunCommand([]string{"python", "--version"}, GetStatePath(), true)
versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), strings.TrimSpace(whichOut)
return strings.TrimSpace(detectedVersion), "/bin/python"
}

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)
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))
os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", GetStatePath("shims")))
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 sysVersion == "4.5.6" {
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)
}
}
@ -62,8 +61,8 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
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)
if err != nil || version.Version != "1.2.3" {
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())
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)
}
}
@ -88,7 +87,7 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
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)
}
}
@ -102,7 +101,7 @@ func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
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)
}
}
@ -118,7 +117,7 @@ func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
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)
}
@ -129,8 +128,8 @@ func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
versionFound, found := SearchForPythonVersionFile()
if versionFound != "" || found {
t.Errorf("Did not expect any result, found %s.", versionFound)
if versionFound.Version != "" || found {
t.Errorf("Did not expect any result, found %s.", versionFound.Version)
}
}

6
v.go
View file

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