refactor(python): move python-specific logic to own module

This commit is contained in:
Marc 2024-01-25 23:09:52 -05:00
parent a5c7242045
commit c0d8d31ace
Signed by: marc
GPG key ID: 048E042F22B5DC79
8 changed files with 83 additions and 72 deletions

View file

@ -5,6 +5,7 @@ import (
"slices" "slices"
cli "v/cli" cli "v/cli"
logger "v/logger" logger "v/logger"
python "v/python"
state "v/state" state "v/state"
) )
@ -58,12 +59,12 @@ func UninstallPython(args []string, flags cli.Flags, currentState state.State) e
func InstallPython(args []string, flags cli.Flags, currentState state.State) error { func InstallPython(args []string, flags cli.Flags, currentState state.State) error {
version := args[1] version := args[1]
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose) return python.InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
} }
func Use(args []string, flags cli.Flags, currentState state.State) error { func Use(args []string, flags cli.Flags, currentState state.State) error {
version := args[1] version := args[1]
if err := ValidateVersion(version); err != nil { if err := python.ValidateVersion(version); err != nil {
return err return err
} }
@ -79,7 +80,7 @@ func Use(args []string, flags cli.Flags, currentState state.State) error {
if !found { if !found {
logger.InfoLogger.Println("Version not installed. Installing it first.") logger.InfoLogger.Println("Version not installed. Installing it first.")
InstallPythonDistribution(version, flags.NoCache, flags.Verbose) python.InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
} }
state.WriteState(version) state.WriteState(version)
@ -88,7 +89,7 @@ func Use(args []string, flags cli.Flags, currentState state.State) error {
return nil return nil
} }
func ListVersions(args []string, flags cli.Flags, currentState state.State) error { func ListVersions(args []string, flags cli.Flags, currentState state.State) error {
installedVersions, err := ListInstalledVersions() installedVersions, err := python.ListInstalledVersions()
if err != nil { if err != nil {
return err return err
@ -108,17 +109,17 @@ func ListVersions(args []string, flags cli.Flags, currentState state.State) erro
// Which 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 Which(args []string, flags cli.Flags, currentState state.State) error { func Which(args []string, flags cli.Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState) selectedVersion, _ := python.DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions() installedVersions, _ := python.ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version) isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
var printedPath string var printedPath string
if selectedVersion.Source == "system" { if selectedVersion.Source == "system" {
_, sysPath := DetermineSystemPython() _, sysPath := python.DetermineSystemPython()
printedPath = sysPath + " (system)" printedPath = sysPath + " (system)"
} else if isInstalled { } else if isInstalled {
tag := VersionStringToStruct(selectedVersion.Version) tag := python.VersionStringToStruct(selectedVersion.Version)
printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor()) printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
} else { } else {
logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version) logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
@ -130,7 +131,7 @@ func Which(args []string, flags cli.Flags, currentState state.State) error {
if flags.RawOutput { if flags.RawOutput {
prefix = "" prefix = ""
} else { } else {
printedPath = Bold(printedPath) printedPath = logger.Bold(printedPath)
} }
logger.InfoLogger.Printf("%s%s\n", prefix, printedPath) logger.InfoLogger.Printf("%s%s\n", prefix, printedPath)
@ -141,12 +142,12 @@ func Which(args []string, flags cli.Flags, currentState state.State) error {
// and what configures it. If the version is configured by a file, the file is returned // 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. // under "source", if the system Python is used, "system" is returned as a source.
func CurrentVersion(args []string, flags cli.Flags, currentState state.State) error { func CurrentVersion(args []string, flags cli.Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState) selectedVersion, _ := python.DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions() installedVersions, _ := python.ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version) isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
if !isInstalled { if !isInstalled {
logger.InfoLogger.Println(Bold(Yellow("WARNING: This version is not installed."))) logger.InfoLogger.Println(logger.Bold(logger.Yellow("WARNING: This version is not installed.")))
} }
if flags.RawOutput { if flags.RawOutput {
@ -154,6 +155,6 @@ func CurrentVersion(args []string, flags cli.Flags, currentState state.State) er
return nil return nil
} }
logger.InfoLogger.Printf("Python version: %s\nSource: %s\n", Bold(selectedVersion.Version), Bold(selectedVersion.Source)) logger.InfoLogger.Printf("Python version: %s\nSource: %s\n", logger.Bold(selectedVersion.Version), logger.Bold(selectedVersion.Source))
return nil return nil
} }

View file

@ -8,10 +8,11 @@ import (
cli "v/cli" cli "v/cli"
logger "v/logger" logger "v/logger"
state "v/state" state "v/state"
testutils "v/testutils"
) )
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) { func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
os.Mkdir(state.GetStatePath("runtimes"), 0750) os.Mkdir(state.GetStatePath("runtimes"), 0750)
var out bytes.Buffer var out bytes.Buffer
@ -28,7 +29,7 @@ func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
} }
func TestListVersionOutputsVersionsInstalled(t *testing.T) { func TestListVersionOutputsVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750) os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
var out bytes.Buffer var out bytes.Buffer
@ -45,7 +46,7 @@ func TestListVersionOutputsVersionsInstalled(t *testing.T) {
} }
func TestListVersionReturnsErrorOnFailure(t *testing.T) { func TestListVersionReturnsErrorOnFailure(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer var out bytes.Buffer
@ -65,7 +66,7 @@ func TestListVersionReturnsErrorOnFailure(t *testing.T) {
} }
func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) { func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer var out bytes.Buffer
@ -81,7 +82,7 @@ func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
} }
func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) { func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer var out bytes.Buffer
@ -99,7 +100,7 @@ func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
} }
func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) { func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer var out bytes.Buffer
@ -116,7 +117,7 @@ func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
} }
func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) { func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer var out bytes.Buffer

View file

@ -1,26 +1,11 @@
package main package exec
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
) )
func VersionStringToStruct(version string) VersionTag {
splitVersion := strings.Split(version, ".")
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
}
func ValidateVersion(version string) error {
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
return errors.New("Invalid version string. Expected format 'a.b.c'.")
}
return nil
}
// RunCommand is a thin wrapper around running command-line calls // RunCommand is a thin wrapper around running command-line calls
// programmatically. It abstracts common configuration like routing // programmatically. It abstracts common configuration like routing
// output and handling the directory the calls are made from. // output and handling the directory the calls are made from.

View file

@ -1,4 +1,4 @@
package main package logger
const ( const (
RESET = "\033[0m" RESET = "\033[0m"

View file

@ -1,4 +1,4 @@
package main package python
import ( import (
"errors" "errors"
@ -9,6 +9,7 @@ import (
"path" "path"
"strings" "strings"
"time" "time"
exec "v/exec"
logger "v/logger" logger "v/logger"
state "v/state" state "v/state"
) )
@ -58,7 +59,7 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
client := http.Client{} client := http.Client{}
logger.InfoLogger.Println(Bold("Downloading source for Python " + version)) logger.InfoLogger.Println(logger.Bold("Downloading source for Python " + version))
logger.InfoLogger.SetPrefix(" ") logger.InfoLogger.SetPrefix(" ")
defer logger.InfoLogger.SetPrefix("") defer logger.InfoLogger.SetPrefix("")
@ -88,7 +89,7 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
} }
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) { func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
logger.InfoLogger.Println(Bold("Building from source")) logger.InfoLogger.Println(logger.Bold("Building from source"))
logger.InfoLogger.SetPrefix(" ") logger.InfoLogger.SetPrefix(" ")
defer logger.InfoLogger.SetPrefix("") defer logger.InfoLogger.SetPrefix("")
@ -96,7 +97,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath) logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose) _, untarErr := exec.RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose)
if untarErr != nil { if untarErr != nil {
return pkgMeta, untarErr return pkgMeta, untarErr
@ -108,14 +109,14 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
targetDirectory := state.GetStatePath("runtimes", "py-"+pkgMeta.Version) targetDirectory := state.GetStatePath("runtimes", "py-"+pkgMeta.Version)
_, configureErr := RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose) _, configureErr := exec.RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)
if configureErr != nil { if configureErr != nil {
return pkgMeta, configureErr return pkgMeta, configureErr
} }
logger.InfoLogger.Println("Building") logger.InfoLogger.Println("Building")
_, buildErr := RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose) _, buildErr := exec.RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
if buildErr != nil { if buildErr != nil {
return pkgMeta, buildErr return pkgMeta, buildErr

View file

@ -1,4 +1,4 @@
package main package python
import ( import (
"encoding/json" "encoding/json"
@ -9,26 +9,11 @@ import (
"slices" "slices"
"testing" "testing"
state "v/state" state "v/state"
testutils "v/testutils"
) )
// 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())
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
return func() {
os.Unsetenv("V_ROOT")
}
}
func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) { func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
ioutil.WriteFile(state.GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777) ioutil.WriteFile(state.GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
mockSystemPythonPath := t.TempDir() mockSystemPythonPath := t.TempDir()
@ -49,7 +34,7 @@ func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
} }
func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T) { func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
// Writing a mock user-defined state. // Writing a mock user-defined state.
mockState := state.State{GlobalVersion: "1.0.0"} mockState := state.State{GlobalVersion: "1.0.0"}
@ -69,7 +54,7 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
} }
func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) { func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
// Writing a mock user-defined state. // Writing a mock user-defined state.
mockState := state.State{GlobalVersion: "1.0.0"} mockState := state.State{GlobalVersion: "1.0.0"}
@ -85,7 +70,7 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
} }
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) { func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
version, err := DetermineSelectedPythonVersion(state.ReadState()) version, err := DetermineSelectedPythonVersion(state.ReadState())
@ -95,7 +80,7 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
} }
func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) { func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir() temporaryWd := t.TempDir()
os.Chdir(temporaryWd) os.Chdir(temporaryWd)
@ -109,7 +94,7 @@ func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
} }
func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) { func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir() temporaryWd := t.TempDir()
@ -126,7 +111,7 @@ func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
} }
func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) { func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
versionFound, found := SearchForPythonVersionFile() versionFound, found := SearchForPythonVersionFile()
@ -136,7 +121,7 @@ func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
} }
func TestListInstalledVersion(t *testing.T) { func TestListInstalledVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
versions := []string{"1.2.3", "4.5.6", "7.8.9"} versions := []string{"1.2.3", "4.5.6", "7.8.9"}
@ -153,7 +138,7 @@ func TestListInstalledVersion(t *testing.T) {
} }
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) { func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
os.Mkdir(state.GetStatePath("runtimes"), 0750) os.Mkdir(state.GetStatePath("runtimes"), 0750)
@ -165,7 +150,7 @@ func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
} }
func TestListInstalledVersionNoRuntimesDir(t *testing.T) { func TestListInstalledVersionNoRuntimesDir(t *testing.T) {
defer SetupAndCleanupEnvironment(t)() defer testutils.SetupAndCleanupEnvironment(t)()
installedVersions, err := ListInstalledVersions() installedVersions, err := ListInstalledVersions()

View file

@ -1,13 +1,29 @@
package main package python
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"strings" "strings"
exec "v/exec"
state "v/state" state "v/state"
) )
func VersionStringToStruct(version string) VersionTag {
splitVersion := strings.Split(version, ".")
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
}
func ValidateVersion(version string) error {
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
return errors.New("Invalid version string. Expected format 'a.b.c'.")
}
return nil
}
type SelectedVersion struct { type SelectedVersion struct {
Version string Version string
Source string Source string
@ -84,7 +100,7 @@ func DetermineSelectedPythonVersion(currentState state.State) (SelectedVersion,
// DetermineSystemPython returns the unshimmed Python version and path. // DetermineSystemPython returns the unshimmed Python version and path.
// It assumes that /bin/python is where system Python lives. // It assumes that /bin/python is where system Python lives.
func DetermineSystemPython() (string, string) { func DetermineSystemPython() (string, string) {
versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true) versionOut, _ := exec.RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python") detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), "/bin/python" return strings.TrimSpace(detectedVersion), "/bin/python"
} }

22
testutils/setup.go Normal file
View file

@ -0,0 +1,22 @@
package testutils
import (
"os"
"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())
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
return func() {
os.Unsetenv("V_ROOT")
}
}