Merge pull request #18 from mcataford/feat/python-namespace

feat: python namespace
This commit is contained in:
Marc 2024-01-26 00:49:49 -05:00 committed by GitHub
commit 3206d674cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 90 additions and 48 deletions

View file

@ -36,7 +36,7 @@ This will handle adding shim paths to your shell without hassle.
`v` will print a helpful list of available commands. `v` will print a helpful list of available commands.
The most important things to know include `v install <version>` to install new versions and `v use <installed version>` to use a specific version of Python. The most important things to know include `v python install <version>` to install new versions and `v python use <installed version>` to use a specific version of Python.
## Contributing ## Contributing

View file

@ -25,27 +25,40 @@ type Command struct {
// Must be initialized with commands via AddCommand before running // Must be initialized with commands via AddCommand before running
// with Run. // with Run.
type CLI struct { type CLI struct {
// Commands in enumeration order. Namespaces map[string]Namespace
Metadata map[string]string
}
type Namespace struct {
Label string
Commands map[string]Command
OrderedCommands []string OrderedCommands []string
// Command metadata entries. }
Commands map[string]Command
Metadata map[string]string func (c *CLI) AddNamespace(namespace Namespace) *CLI {
if c.Namespaces == nil {
c.Namespaces = map[string]Namespace{}
}
c.Namespaces[namespace.Label] = namespace
return c
} }
// Registers a command. // Registers a command.
// This specifies a label that is used to route the user input to // This specifies a label that is used to route the user input to
// the right command, a handler that is called when the label is used, // the right command, a handler that is called when the label is used,
// and usage/description details that get included in autogenerated help messaging. // and usage/description details that get included in autogenerated help messaging.
func (c CLI) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) CLI { func (n *Namespace) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) *Namespace {
if c.Commands == nil { if n.Commands == nil {
c.Commands = map[string]Command{} n.Commands = map[string]Command{}
c.OrderedCommands = []string{} n.OrderedCommands = []string{}
} }
c.OrderedCommands = append(c.OrderedCommands, label) n.OrderedCommands = append(n.OrderedCommands, label)
c.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description} n.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description}
return c return n
} }
// Executes one of the registered commands if any match the provided // Executes one of the registered commands if any match the provided
@ -56,24 +69,36 @@ func (c CLI) Run(args []string, currentState state.State) error {
return nil return nil
} }
command := args[0] action := args[0]
if command == "help" { if action == "help" {
c.Help() c.Help()
return nil return nil
} }
flags := collectFlags(args) flags := collectFlags(args)
return c.Commands[command].Handler(args, flags, currentState)
namespace, isNamespace := c.Namespaces[action]
if isNamespace {
action = args[1]
return namespace.Commands[action].Handler(args[1:], flags, currentState)
}
rootNamespace := c.Namespaces[""]
return rootNamespace.Commands[action].Handler(args, flags, currentState)
} }
// Prints autogenerated help documentation specifying command usage // Prints autogenerated help documentation specifying command usage
// and descriptions based on registered commands (see: AddCommand). // and descriptions based on registered commands (see: AddCommand).
func (c CLI) Help() { func (c CLI) Help() {
logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"]) logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"])
for _, commandLabel := range c.OrderedCommands { for _, namespace := range c.Namespaces {
command := c.Commands[commandLabel] for _, commandLabel := range namespace.OrderedCommands {
logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description) command := namespace.Commands[commandLabel]
logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
}
} }
} }

View file

@ -9,9 +9,9 @@ go build .
TARGET_VERSION="3.10.0" TARGET_VERSION="3.10.0"
V_ROOT=/tmp/v ./v init V_ROOT=/tmp/v ./v init
V_ROOT=/tmp/v ./v install $TARGET_VERSION --no-cache V_ROOT=/tmp/v ./v python install $TARGET_VERSION --no-cache
INSTALLED_VERSIONS=$(V_ROOT=/tmp/v ./v ls) INSTALLED_VERSIONS=$(V_ROOT=/tmp/v ./v python ls)
if [ -z "$(echo $INSTALLED_VERSIONS | grep $TARGET_VERSION)" ]; then if [ -z "$(echo $INSTALLED_VERSIONS | grep $TARGET_VERSION)" ]; then
echo "FAIL: Could not find target version." echo "FAIL: Could not find target version."

24
python/cli.go Normal file
View file

@ -0,0 +1,24 @@
package python
import (
cli "v/cli"
)
func GetNamespace() cli.Namespace {
pythonCommands := cli.Namespace{Label: "python"}
pythonCommands.AddCommand(
"install", installPython, "v python install <version>", "Downloads, builds and installs a new version of Python.",
).AddCommand(
"uninstall", uninstallPython, "v python uninstall <version>", "Uninstalls the given Python version.",
).AddCommand(
"use", use, "v python use <version>", "Selects which Python version to use.",
).AddCommand(
"ls", listVersions, "v python ls", "Lists the installed Python versions.",
).AddCommand(
"version", currentVersion, "v python version", "Prints the current version and its source.",
).AddCommand(
"which", which, "v python which", "Prints the path to the current Python version.",
)
return pythonCommands
}

View file

@ -8,19 +8,19 @@ import (
state "v/state" state "v/state"
) )
func UninstallPython(args []string, flags cli.Flags, currentState state.State) error { func uninstallPython(args []string, flags cli.Flags, currentState state.State) error {
runtimePath := state.GetStatePath("runtimes", "py-"+args[1]) runtimePath := state.GetStatePath("runtimes", "py-"+args[1])
err := os.RemoveAll(runtimePath) err := os.RemoveAll(runtimePath)
return err return err
} }
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 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 := ValidateVersion(version); err != nil {
return err return err
@ -47,7 +47,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 := ListInstalledVersions()
if err != nil { if err != nil {
@ -67,7 +67,7 @@ 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, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions() installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version) isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
@ -100,7 +100,7 @@ func Which(args []string, flags cli.Flags, currentState state.State) error {
// CurrentVersion (called via `v version`) outputs the currently selected version // 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 // 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, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions() installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version) isInstalled := slices.Contains(installedVersions, selectedVersion.Version)

View file

@ -20,7 +20,7 @@ func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
logger.InfoLogger.SetOutput(&out) logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, cli.Flags{}, state.State{}) listVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String() captured := out.String()
if captured != "No versions installed!\n" { if captured != "No versions installed!\n" {
@ -37,7 +37,7 @@ func TestListVersionOutputsVersionsInstalled(t *testing.T) {
logger.InfoLogger.SetOutput(&out) logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, cli.Flags{}, state.State{}) listVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String() captured := out.String()
if captured != "1.2.3\n" { if captured != "1.2.3\n" {
@ -53,7 +53,7 @@ func TestListVersionReturnsErrorOnFailure(t *testing.T) {
logger.InfoLogger.SetOutput(&out) logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
err := ListVersions([]string{}, cli.Flags{}, state.State{}) err := listVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String() captured := out.String()
if captured != "" { if captured != "" {
@ -73,7 +73,7 @@ func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
logger.InfoLogger.SetOutput(&out) logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"}) which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := out.String() captured := out.String()
if captured != "The desired version (1.2.3) is not installed.\n" { if captured != "The desired version (1.2.3) is not installed.\n" {
@ -90,7 +90,7 @@ func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750) os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"}) which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String()) captured := strings.TrimSpace(out.String())
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2") expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
@ -107,7 +107,7 @@ func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
logger.InfoLogger.SetOutput(&out) logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
Which([]string{}, cli.Flags{RawOutput: true}, state.State{}) which([]string{}, cli.Flags{RawOutput: true}, state.State{})
captured := strings.TrimSpace(out.String()) captured := strings.TrimSpace(out.String())
@ -125,7 +125,7 @@ func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
defer logger.InfoLogger.SetOutput(os.Stdout) defer logger.InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750) os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"}) which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String()) captured := strings.TrimSpace(out.String())
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2") expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")

23
v.go
View file

@ -18,27 +18,20 @@ func main() {
args := os.Args[1:] args := os.Args[1:]
currentState := state.ReadState() currentState := state.ReadState()
root := cli.Namespace{Label: ""}
root.AddCommand(
"init", Initialize, "v init", "Initializes the v state.",
)
cli := cli.CLI{ cli := cli.CLI{
Metadata: map[string]string{ Metadata: map[string]string{
"Version": Version, "Version": Version,
}, },
} }
err := cli.AddCommand( cli.AddNamespace(root).AddNamespace(python.GetNamespace())
"install", python.InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
).AddCommand( err := cli.Run(args, currentState)
"uninstall", python.UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
).AddCommand(
"use", python.Use, "v use <version>", "Selects which Python version to use.",
).AddCommand(
"ls", python.ListVersions, "v ls", "Lists the installed Python versions.",
).AddCommand(
"version", python.CurrentVersion, "v version", "Prints the current version and its source.",
).AddCommand(
"which", python.Which, "v which", "Prints the path to the current Python version.",
).AddCommand(
"init", Initialize, "v init", "Initializes the v state.",
).Run(args, currentState)
if err != nil { if err != nil {
panic(err) panic(err)