mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat : adds pipx support to PMG (#292)
* adding the pipx support to project Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * create excutors for pipx Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * add pipx yml Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * chores Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * pipx to use standard executor pattern Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * Address PR feedback for pipx support Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> * Update pipx flags comments Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> --------- Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com> Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: Sahil Bansal <bansalsahil315@gmail.com>
This commit is contained in:
co-authored by
Abhisek Datta
Sahil Bansal
parent
ab1a5386d3
commit
7620097613
@@ -149,6 +149,7 @@ PMG supports the tools you already use:
|
|||||||
| | `npx` | `npx <pkg>` |
|
| | `npx` | `npx <pkg>` |
|
||||||
| | `pnpx` | `pnpx <pkg>` |
|
| | `pnpx` | `pnpx <pkg>` |
|
||||||
| **Python** | `pip` | `pip install <pkg>` |
|
| **Python** | `pip` | `pip install <pkg>` |
|
||||||
|
| | `pipx` | `pipx run <pkg>` |
|
||||||
| | `poetry` | `poetry add <pkg>` |
|
| | `poetry` | `poetry add <pkg>` |
|
||||||
| | `uv` | `uv add <pkg>` |
|
| | `uv` | `uv add <pkg>` |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package executors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/safedep/pmg/config"
|
||||||
|
"github.com/safedep/pmg/internal/analytics"
|
||||||
|
"github.com/safedep/pmg/internal/flows"
|
||||||
|
"github.com/safedep/pmg/internal/ui"
|
||||||
|
"github.com/safedep/pmg/packagemanager"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPipxCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "pipx [action] [package]",
|
||||||
|
Short: "Guard pipx package executor",
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := executePipxFlow(cmd.Context(), args)
|
||||||
|
if err != nil {
|
||||||
|
ui.ExitFromCommandError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func executePipxFlow(ctx context.Context, args []string) error {
|
||||||
|
analytics.TrackCommandPipx()
|
||||||
|
|
||||||
|
packageExecutor, err := packagemanager.NewPypiPackageExecutor(packagemanager.DefaultPipxPackageExecutorConfig())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create pipx package executor proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := config.Get()
|
||||||
|
parsedCommand, err := packageExecutor.ParseCommand(args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageResolverConfig := packagemanager.NewDefaultPypiDependencyResolverConfig()
|
||||||
|
packageResolverConfig.IncludeTransitiveDependencies = config.Config.Transitive
|
||||||
|
packageResolverConfig.TransitiveDepth = config.Config.TransitiveDepth
|
||||||
|
packageResolverConfig.IncludeDevDependencies = config.Config.IncludeDevDependencies
|
||||||
|
packageResolverConfig.PackageInstallTargets = parsedCommand.InstallTargets
|
||||||
|
|
||||||
|
packageResolver, err := packagemanager.NewPypiDependencyResolver(packageResolverConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create dependency resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.IsProxyModeEnabled() {
|
||||||
|
return flows.Common(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows.ProxyFlow(packageExecutor, packageResolver).Run(ctx, args, parsedCommand)
|
||||||
|
}
|
||||||
@@ -145,6 +145,10 @@ sandbox:
|
|||||||
enabled: true
|
enabled: true
|
||||||
profile: pypi-restrictive
|
profile: pypi-restrictive
|
||||||
|
|
||||||
|
pipx:
|
||||||
|
enabled: true
|
||||||
|
profile: pypi-restrictive
|
||||||
|
|
||||||
poetry:
|
poetry:
|
||||||
enabled: true
|
enabled: true
|
||||||
profile: pypi-restrictive
|
profile: pypi-restrictive
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func DefaultConfig() AliasConfig {
|
|||||||
|
|
||||||
return AliasConfig{
|
return AliasConfig{
|
||||||
RcFileName: ".pmg.rc",
|
RcFileName: ".pmg.rc",
|
||||||
PackageManagers: []string{"npm", "pip", "pip3", "pnpm", "bun", "uv", "yarn", "poetry", "npx", "pnpx"},
|
PackageManagers: []string{"npm", "pip", "pip3", "pipx", "pnpm", "bun", "uv", "yarn", "poetry", "npx", "pnpx"},
|
||||||
Shells: shells,
|
Shells: shells,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
eventCommandPip3 = "pmg_command_pip3"
|
eventCommandPip3 = "pmg_command_pip3"
|
||||||
eventCommandUv = "pmg_command_uv"
|
eventCommandUv = "pmg_command_uv"
|
||||||
eventCommandPoetry = "pmg_command_poetry"
|
eventCommandPoetry = "pmg_command_poetry"
|
||||||
|
eventCommandPipx = "pmg_command_pipx"
|
||||||
|
|
||||||
eventCommandNpx = "pmg_command_npx"
|
eventCommandNpx = "pmg_command_npx"
|
||||||
eventCommandPnpx = "pmg_command_pnpx"
|
eventCommandPnpx = "pmg_command_pnpx"
|
||||||
@@ -63,6 +64,10 @@ func TrackCommandPoetry() {
|
|||||||
TrackEvent(eventCommandPoetry)
|
TrackEvent(eventCommandPoetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TrackCommandPipx() {
|
||||||
|
TrackEvent(eventCommandPipx)
|
||||||
|
}
|
||||||
|
|
||||||
func TrackCommandGenerateEnvDocker() {
|
func TrackCommandGenerateEnvDocker() {
|
||||||
TrackEvent(eventPmgGenerateEnvDocker)
|
TrackEvent(eventPmgGenerateEnvDocker)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func mapPackageManager(name string) controltowerv1.PmgPackageManager {
|
|||||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_YARN
|
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_YARN
|
||||||
case "bun":
|
case "bun":
|
||||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN
|
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN
|
||||||
case "pip", "pip3":
|
case "pip", "pip3", "pipx":
|
||||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP
|
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP
|
||||||
case "poetry":
|
case "poetry":
|
||||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY
|
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ func TestMapPackageManager(t *testing.T) {
|
|||||||
{"bun", "bun", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN},
|
{"bun", "bun", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN},
|
||||||
{"pip", "pip", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
{"pip", "pip", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
||||||
{"pip3", "pip3", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
{"pip3", "pip3", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
||||||
|
{"pipx", "pipx", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
||||||
{"poetry", "poetry", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY},
|
{"poetry", "poetry", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY},
|
||||||
{"uv", "uv", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UV},
|
{"uv", "uv", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UV},
|
||||||
{"unknown", "cargo", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UNSPECIFIED},
|
{"unknown", "cargo", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UNSPECIFIED},
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func main() {
|
|||||||
cmd.AddCommand(pypi.NewPip3Command())
|
cmd.AddCommand(pypi.NewPip3Command())
|
||||||
cmd.AddCommand(pypi.NewUvCommand())
|
cmd.AddCommand(pypi.NewUvCommand())
|
||||||
cmd.AddCommand(pypi.NewPoetryCommand())
|
cmd.AddCommand(pypi.NewPoetryCommand())
|
||||||
|
cmd.AddCommand(executors.NewPipxCommand())
|
||||||
cmd.AddCommand(version.NewVersionCommand())
|
cmd.AddCommand(version.NewVersionCommand())
|
||||||
cmd.AddCommand(setup.NewSetupCommand())
|
cmd.AddCommand(setup.NewSetupCommand())
|
||||||
cmd.AddCommand(setup.NewRemoveCommand())
|
cmd.AddCommand(setup.NewRemoveCommand())
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package packagemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PypiPackageExecutorConfig struct {
|
||||||
|
CommandName string
|
||||||
|
InstallCommands []string
|
||||||
|
NonDownloadCommands []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultPipxPackageExecutorConfig() PypiPackageExecutorConfig {
|
||||||
|
return PypiPackageExecutorConfig{
|
||||||
|
CommandName: "pipx",
|
||||||
|
InstallCommands: []string{"install", "inject", "run", "upgrade", "upgrade-all", "reinstall", "reinstall-all"},
|
||||||
|
NonDownloadCommands: []string{
|
||||||
|
"list", "uninstall", "uninstall-all", "completions", "uninject", "ensurepath", "environment",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pypiPackageExecutor struct {
|
||||||
|
Config PypiPackageExecutorConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPypiPackageExecutor(config PypiPackageExecutorConfig) (*pypiPackageExecutor, error) {
|
||||||
|
return &pypiPackageExecutor{
|
||||||
|
Config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PackageManager = &pypiPackageExecutor{}
|
||||||
|
|
||||||
|
func (p *pypiPackageExecutor) Name() string {
|
||||||
|
return p.Config.CommandName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pypiPackageExecutor) Ecosystem() packagev1.Ecosystem {
|
||||||
|
return packagev1.Ecosystem_ECOSYSTEM_PYPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pypiPackageExecutor) ParseCommand(args []string) (*ParsedCommand, error) {
|
||||||
|
if len(args) > 0 && args[0] == "pipx" {
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
command := Command{Exe: p.Config.CommandName, Args: args}
|
||||||
|
if len(args) < 1 {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipx run <pkg> downloads and executes a package without globally installing it.
|
||||||
|
// We extract the package name so it can be audited before execution.
|
||||||
|
if args[0] == "run" {
|
||||||
|
return p.parseRunCommand(command, args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipx inject <target-venv> <pkg1> [<pkg2> ...] injects packages into an
|
||||||
|
// existing venv. The first positional arg is the target venv (already installed),
|
||||||
|
// not a package to audit — we skip it and only audit the injected packages.
|
||||||
|
if args[0] == "inject" {
|
||||||
|
return p.parseInjectCommand(command, args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var installCmdIndex = -1
|
||||||
|
for idx, arg := range args {
|
||||||
|
if slices.Contains(p.Config.InstallCommands, arg) {
|
||||||
|
installCmdIndex = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if installCmdIndex == -1 {
|
||||||
|
return &ParsedCommand{Command: command, IsKnownNonDownloadCommand: IsFirstNonFlagArgInList(args, p.Config.NonDownloadCommands)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
installArgs := args[installCmdIndex+1:]
|
||||||
|
|
||||||
|
flagSet := pflag.NewFlagSet("pipx install", pflag.ContinueOnError)
|
||||||
|
flagSet.ParseErrorsAllowlist.UnknownFlags = true
|
||||||
|
flagSet.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
// Define known pipx install flags. We register flags that take values to prevent
|
||||||
|
// their values from being misidentified as package names, and boolean flags
|
||||||
|
// to prevent the flag itself from being treated as an unknown argument.
|
||||||
|
// registers --pip-args, --python, --spec so their values aren't picked up as packages
|
||||||
|
setupCommonPipxFlags(flagSet)
|
||||||
|
flagSet.Bool("force", false, "")
|
||||||
|
flagSet.Bool("include-deps", false, "")
|
||||||
|
flagSet.Bool("system-site-packages", false, "")
|
||||||
|
|
||||||
|
err := flagSet.Parse(installArgs)
|
||||||
|
if err != nil {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
packages := flagSet.Args()
|
||||||
|
return p.buildInstallTargets(command, packages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRunCommand handles `pipx run [flags] <package> [args...]`.
|
||||||
|
// Only the first positional argument is the package; the rest are arguments
|
||||||
|
// to the executed program.
|
||||||
|
func (p *pypiPackageExecutor) parseRunCommand(command Command, runArgs []string) (*ParsedCommand, error) {
|
||||||
|
if len(runArgs) == 0 {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flagSet := pflag.NewFlagSet("pipx run", pflag.ContinueOnError)
|
||||||
|
flagSet.ParseErrorsAllowlist.UnknownFlags = true
|
||||||
|
flagSet.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
// Define known pipx run flags. We register flags that take values to prevent
|
||||||
|
// their values from being misidentified as package names, and boolean flags
|
||||||
|
// to prevent the flag itself from being treated as an unknown argument.
|
||||||
|
// registers --pip-args, --python, --spec so their values aren't picked up as packages
|
||||||
|
_, _, specPkg := setupCommonPipxFlags(flagSet)
|
||||||
|
flagSet.Bool("no-cache", false, "")
|
||||||
|
|
||||||
|
err := flagSet.Parse(runArgs)
|
||||||
|
if err != nil {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --spec is provided, that's the package to audit, not the positional arg
|
||||||
|
if *specPkg != "" {
|
||||||
|
return p.buildInstallTargets(command, []string{*specPkg})
|
||||||
|
}
|
||||||
|
|
||||||
|
packages := flagSet.Args()
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the first positional arg is the package
|
||||||
|
return p.buildInstallTargets(command, []string{packages[0]})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInjectCommand handles `pipx inject [flags] <target-venv> <pkg1> [<pkg2> ...]`.
|
||||||
|
// The first positional argument is the target venv (already installed, not audited).
|
||||||
|
// Subsequent positional arguments are the packages being injected.
|
||||||
|
func (p *pypiPackageExecutor) parseInjectCommand(command Command, injectArgs []string) (*ParsedCommand, error) {
|
||||||
|
if len(injectArgs) == 0 {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flagSet := pflag.NewFlagSet("pipx inject", pflag.ContinueOnError)
|
||||||
|
flagSet.ParseErrorsAllowlist.UnknownFlags = true
|
||||||
|
flagSet.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
// Define known pipx inject flags. We register flags that take values to prevent
|
||||||
|
// their values from being misidentified as package names, and boolean flags
|
||||||
|
// to prevent the flag itself from being treated as an unknown argument.
|
||||||
|
// registers --pip-args, --python, --spec so their values aren't picked up as packages
|
||||||
|
setupCommonPipxFlags(flagSet)
|
||||||
|
flagSet.Bool("force", false, "")
|
||||||
|
flagSet.Bool("include-apps", false, "")
|
||||||
|
flagSet.Bool("include-deps", false, "")
|
||||||
|
|
||||||
|
err := flagSet.Parse(injectArgs)
|
||||||
|
if err != nil {
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
packages := flagSet.Args()
|
||||||
|
if len(packages) < 2 {
|
||||||
|
// Need at least target-venv + one package to inject
|
||||||
|
return &ParsedCommand{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the first positional arg (target venv), audit the rest
|
||||||
|
return p.buildInstallTargets(command, packages[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildInstallTargets creates install targets from a list of package specifiers.
|
||||||
|
func (p *pypiPackageExecutor) buildInstallTargets(command Command, packages []string) (*ParsedCommand, error) {
|
||||||
|
var installTargets []*PackageInstallTarget
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
packageName, version, extras, err := pypiParsePackageInfo(pkg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrFailedToParsePackage.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isExplicit := version != ""
|
||||||
|
version, err = pypiGetMatchingVersion(packageName, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrFailedToResolveVersion.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installTargets = append(installTargets, &PackageInstallTarget{
|
||||||
|
PackageVersion: &packagev1.PackageVersion{
|
||||||
|
Package: &packagev1.Package{
|
||||||
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
|
||||||
|
Name: packageName,
|
||||||
|
},
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
Extras: extras,
|
||||||
|
IsExplicitVersion: isExplicit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedCommand{
|
||||||
|
Command: command,
|
||||||
|
InstallTargets: installTargets,
|
||||||
|
IsManifestInstall: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupCommonPipxFlags(flagSet *pflag.FlagSet) (pipArgs, pythonPath, specPkg *string) {
|
||||||
|
pipArgs = flagSet.String("pip-args", "", "")
|
||||||
|
pythonPath = flagSet.String("python", "", "")
|
||||||
|
specPkg = flagSet.String("spec", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package packagemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPipxExecutorParseCommand(t *testing.T) {
|
||||||
|
pm, err := NewPypiPackageExecutor(DefaultPipxPackageExecutorConfig())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectedManifest bool
|
||||||
|
expectedTargets int
|
||||||
|
expectedPackages []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "pipx install simple package",
|
||||||
|
args: []string{"install", "black"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"black"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx install with specific version",
|
||||||
|
args: []string{"install", "black==22.3.0"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"black"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx install with --force flag",
|
||||||
|
args: []string{"install", "--force", "ruff"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"ruff"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx install with --pip-args flag",
|
||||||
|
args: []string{"install", "--pip-args", "--no-cache-dir", "black"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"black"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx install with --python flag",
|
||||||
|
args: []string{"install", "--python", "python3.11", "black"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"black"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx run simple package",
|
||||||
|
args: []string{"run", "cowsay", "hello"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"cowsay"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx run with --spec flag",
|
||||||
|
args: []string{"run", "--spec", "black==22.3.0", "black", "--check", "."},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"black"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx run with --no-cache flag",
|
||||||
|
args: []string{"run", "--no-cache", "cowsay", "hello"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"cowsay"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx run bare (no package)",
|
||||||
|
args: []string{"run"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject skips target venv",
|
||||||
|
args: []string{"inject", "poetry", "poetry-plugin-export"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"poetry-plugin-export"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject multiple packages",
|
||||||
|
args: []string{"inject", "myapp", "requests", "flask"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 2,
|
||||||
|
expectedPackages: []string{"requests", "flask"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject with --force flag",
|
||||||
|
args: []string{"inject", "--force", "poetry", "poetry-plugin-export"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"poetry-plugin-export"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject with --include-apps flag",
|
||||||
|
args: []string{"inject", "--include-apps", "myapp", "flask"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"flask"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject with --pip-args flag",
|
||||||
|
args: []string{"inject", "--pip-args", "--no-deps", "myapp", "flask"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 1,
|
||||||
|
expectedPackages: []string{"flask"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject only target venv (no packages)",
|
||||||
|
args: []string{"inject", "poetry"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject bare (no args)",
|
||||||
|
args: []string{"inject"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx list",
|
||||||
|
args: []string{"list"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx uninstall",
|
||||||
|
args: []string{"uninstall", "black"},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bare pipx invocation",
|
||||||
|
args: []string{},
|
||||||
|
expectedManifest: false,
|
||||||
|
expectedTargets: 0,
|
||||||
|
expectedPackages: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := pm.ParseCommand(tc.args)
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectedManifest, result.HasManifestInstall(), "HasManifestInstall mismatch")
|
||||||
|
assert.Equal(t, tc.expectedTargets, len(result.InstallTargets), "Number of install targets mismatch")
|
||||||
|
|
||||||
|
for i, expectedPkg := range tc.expectedPackages {
|
||||||
|
if i < len(result.InstallTargets) {
|
||||||
|
target := result.InstallTargets[i]
|
||||||
|
assert.Equal(t, expectedPkg, target.PackageVersion.Package.Name, "Package name mismatch for package %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipxExecutorProxyBehavior(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
command string
|
||||||
|
isKnownNonDownloadCmd bool
|
||||||
|
isInstallationCommand bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "pipx install — proxy runs",
|
||||||
|
command: "pipx install black",
|
||||||
|
isKnownNonDownloadCmd: false,
|
||||||
|
isInstallationCommand: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx run — proxy runs",
|
||||||
|
command: "pipx run cowsay moo",
|
||||||
|
isKnownNonDownloadCmd: false,
|
||||||
|
isInstallationCommand: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx inject — proxy runs",
|
||||||
|
command: "pipx inject myapp requests",
|
||||||
|
isKnownNonDownloadCmd: false,
|
||||||
|
isInstallationCommand: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx upgrade — proxy runs (downloads newer version)",
|
||||||
|
command: "pipx upgrade black",
|
||||||
|
isKnownNonDownloadCmd: false,
|
||||||
|
isInstallationCommand: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx upgrade-all — proxy runs (downloads newer versions)",
|
||||||
|
command: "pipx upgrade-all",
|
||||||
|
isKnownNonDownloadCmd: false,
|
||||||
|
isInstallationCommand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx list — proxy skipped",
|
||||||
|
command: "pipx list",
|
||||||
|
isKnownNonDownloadCmd: true,
|
||||||
|
isInstallationCommand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx uninstall — proxy skipped",
|
||||||
|
command: "pipx uninstall black",
|
||||||
|
isKnownNonDownloadCmd: true,
|
||||||
|
isInstallationCommand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx uninstall-all — proxy skipped",
|
||||||
|
command: "pipx uninstall-all",
|
||||||
|
isKnownNonDownloadCmd: true,
|
||||||
|
isInstallationCommand: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pipx completions — proxy skipped",
|
||||||
|
command: "pipx completions",
|
||||||
|
isKnownNonDownloadCmd: true,
|
||||||
|
isInstallationCommand: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
pm, err := NewPypiPackageExecutor(DefaultPipxPackageExecutorConfig())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
parsed, err := pm.ParseCommand(strings.Split(tc.command, " "))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.isKnownNonDownloadCmd, parsed.IsKnownNonDownloadCommand)
|
||||||
|
assert.Equal(t, tc.isInstallationCommand, parsed.IsInstallationCommand())
|
||||||
|
assert.Equal(t, !tc.isKnownNonDownloadCmd, parsed.MayDownloadPackages())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: pipx
|
||||||
|
description: Profile for pipx executor with write access to current directory
|
||||||
|
inherits: pypi-restrictive
|
||||||
|
|
||||||
|
package_managers:
|
||||||
|
- pipx
|
||||||
|
|
||||||
|
# pipx requires PTY access to work, especially for commands like:
|
||||||
|
# pipx run cowsay hello
|
||||||
|
#
|
||||||
|
# Explicitly setting this so that it is enabled even if we decide to turn this off
|
||||||
|
# in parent profiles in the future.
|
||||||
|
allow_pty: true
|
||||||
|
|
||||||
|
# pipx-executed tools may need to bind to localhost ports (e.g., dev servers)
|
||||||
|
allow_network_bind: true
|
||||||
|
|
||||||
|
filesystem:
|
||||||
|
allow_read:
|
||||||
|
# pipx installs and manages packages in ~/.local/pipx
|
||||||
|
- ${HOME}/.local/pipx/**
|
||||||
|
- ${HOME}/.local/bin/**
|
||||||
|
|
||||||
|
# Add write permissions for pipx-specific paths
|
||||||
|
allow_write:
|
||||||
|
- ${CWD}/**
|
||||||
|
- ${HOME}/.local/pipx/**
|
||||||
|
- ${HOME}/.local/bin/**
|
||||||
|
|
||||||
|
# Additional deny rules for extra security
|
||||||
|
deny_write:
|
||||||
|
- ${CWD}/.env
|
||||||
|
- ${CWD}/.env.*
|
||||||
Reference in New Issue
Block a user