Merge pull request #18 from mcataford/feat/python-namespace
feat: python namespace
This commit is contained in:
commit
3206d674cc
7 changed files with 90 additions and 48 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
59
cli/cli.go
59
cli/cli.go
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
24
python/cli.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
23
v.go
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue