vexx 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vexx-0.1.0/LICENSE +20 -0
- vexx-0.1.0/PKG-INFO +151 -0
- vexx-0.1.0/README.md +124 -0
- vexx-0.1.0/pyproject.toml +93 -0
- vexx-0.1.0/vex/__init__.py +5 -0
- vexx-0.1.0/vex/_version.py +1 -0
- vexx-0.1.0/vex/config.py +165 -0
- vexx-0.1.0/vex/exceptions.py +60 -0
- vexx-0.1.0/vex/functional_tests/__init__.py +0 -0
- vexx-0.1.0/vex/functional_tests/not_python +2 -0
- vexx-0.1.0/vex/functional_tests/test_basic.py +233 -0
- vexx-0.1.0/vex/main.py +201 -0
- vexx-0.1.0/vex/make.py +95 -0
- vexx-0.1.0/vex/options.py +103 -0
- vexx-0.1.0/vex/remove.py +17 -0
- vexx-0.1.0/vex/run.py +88 -0
- vexx-0.1.0/vex/shell_config.py +50 -0
- vexx-0.1.0/vex/shell_configs/bash +64 -0
- vexx-0.1.0/vex/shell_configs/fish +81 -0
- vexx-0.1.0/vex/shell_configs/zsh +37 -0
- vexx-0.1.0/vex/tests/__init__.py +8 -0
- vexx-0.1.0/vex/tests/fakes.py +86 -0
- vexx-0.1.0/vex/tests/tempdir.py +26 -0
- vexx-0.1.0/vex/tests/tempvenv.py +46 -0
- vexx-0.1.0/vex/tests/tempvexrcfile.py +24 -0
- vexx-0.1.0/vex/tests/test_config.py +176 -0
- vexx-0.1.0/vex/tests/test_fakes.py +26 -0
- vexx-0.1.0/vex/tests/test_main.py +129 -0
- vexx-0.1.0/vex/tests/test_run.py +142 -0
- vexx-0.1.0/vex/tests/test_shell_config.py +189 -0
- vexx-0.1.0/vex/tests/test_vex.py +7 -0
vexx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2014 Sasha Hart
|
|
2
|
+
Copyright (c) 2026 Adam Henderson
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
9
|
+
so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
vexx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vexx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run commands in a virtualenv
|
|
5
|
+
Project-URL: Homepage, https://github.com/a-hendo/vex
|
|
6
|
+
Project-URL: Repository, https://github.com/a-hendo/vex
|
|
7
|
+
Author-email: Adam Henderson <adamhenderson90@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: deployment,installation,virtualenv,virtualenvwrapper,workon
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: System :: Shells
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: virtualenv
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Vex
|
|
29
|
+
|
|
30
|
+
**Run a command in a virtualenv.**
|
|
31
|
+
|
|
32
|
+
Vex is an elegant, shell-agnostic tool for running commands inside virtual environments. Unlike traditional methods that rely on "activating" and "deactivating" environments by mutating your current shell's state, Vex simply launches a new process with the correct environment variables (like `PATH` and `VIRTUAL_ENV`) already set.
|
|
33
|
+
|
|
34
|
+
When the command finishes, the environment is gone. No cleanup, no `deactivate`, no shell-specific hacks.
|
|
35
|
+
|
|
36
|
+
## Modernization
|
|
37
|
+
|
|
38
|
+
Vex has been modernized to support the latest Python standards:
|
|
39
|
+
|
|
40
|
+
- **Python 3.12+ Required**: Leveraging modern features and built-in type safety.
|
|
41
|
+
- **UV Integration**: Vex now detects and uses [uv](https://github.com/astral-sh/uv) for lightning-fast virtualenv creation when using `--make`.
|
|
42
|
+
- **Pyproject.toml**: Fully migrated to modern PEP 517/518 packaging.
|
|
43
|
+
- **Type Safety**: The entire codebase is now fully type-hinted.
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
`vex` runs any command in a virtualenv without modifying your current shell.
|
|
48
|
+
|
|
49
|
+
The standard way to use a virtualenv involves sourcing an `activate` script which modifies your shell's environment and adds a `deactivate` function. This is often brittle and shell-specific.
|
|
50
|
+
|
|
51
|
+
Vex takes a simpler approach: it calculates the environment required for the virtualenv and passes it directly to a new subprocess. This makes it naturally compatible with **bash, zsh, fish, PowerShell, cmd.exe**, and any other shell or executable.
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
- `vex myenv bash`: Launch a bash shell with virtualenv `myenv` enabled. Exit the shell (Ctrl-D) to "deactivate".
|
|
56
|
+
- `vex myenv python`: Launch a Python interpreter inside virtualenv `myenv`.
|
|
57
|
+
- `vex myenv pip install requests`: Install a package into `myenv` without activating it.
|
|
58
|
+
- `vex -m ephemeral echo "Hello"`: Create a new virtualenv named `ephemeral`, run the command, and then immediately **remove** the virtualenv.
|
|
59
|
+
- `vex --path ./venv python script.py`: Run a script using a virtualenv located at a specific path.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
The recommended way to install Vex is using [uv](https://github.com/astral-sh/uv):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv tool install vexx
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Alternatively, you can use pip:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install --user vexx
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Usage with UV
|
|
76
|
+
|
|
77
|
+
Vex is designed to work seamlessly with `uv`. If `uv` is installed on your system, `vex --make` will automatically use `uv venv` to create environments, which is significantly faster than traditional `virtualenv`.
|
|
78
|
+
|
|
79
|
+
## Config
|
|
80
|
+
|
|
81
|
+
Vex looks for an optional configuration file at `~/.vexrc`.
|
|
82
|
+
|
|
83
|
+
```ini
|
|
84
|
+
shell=bash
|
|
85
|
+
virtualenvs=~/.virtualenvs
|
|
86
|
+
python=python3.12
|
|
87
|
+
env:
|
|
88
|
+
ANSWER=42
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **shell**: The default shell to run if no command is provided (e.g., `vex myenv`).
|
|
92
|
+
- **virtualenvs**: The directory where your named virtualenvs are stored (defaults to `$WORKON_HOME` or `~/.virtualenvs`).
|
|
93
|
+
- **python**: The default Python executable to use when creating new environments with `--make`.
|
|
94
|
+
- **env**: Custom environment variables to inject into every Vex-managed process.
|
|
95
|
+
|
|
96
|
+
## Environment Variables
|
|
97
|
+
|
|
98
|
+
### WORKON_HOME
|
|
99
|
+
|
|
100
|
+
Vex uses the `WORKON_HOME` environment variable to determine where named virtual environments are stored. By default, this is `~/.virtualenvs`.
|
|
101
|
+
|
|
102
|
+
If you are using **uv** in a project and want Vex to target virtual environments local to your current directory (like a `.venv` folder), it is recommended to set `WORKON_HOME` to `./`:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export WORKON_HOME="./"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This allows you to use `vex .venv python` to run commands in your local project environment seamlessly.
|
|
109
|
+
|
|
110
|
+
## Shell Prompts
|
|
111
|
+
|
|
112
|
+
Vex does not automatically change your prompt. To see the current virtualenv in your prompt, you can use the `$VIRTUAL_ENV` variable in your shell configuration.
|
|
113
|
+
|
|
114
|
+
**Bash Example (~/.bashrc):**
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
function virtualenv_prompt() {
|
|
118
|
+
if [ -n "$VIRTUAL_ENV" ]; then
|
|
119
|
+
echo "(${VIRTUAL_ENV##*/}) "
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
export PS1='$(virtualenv_prompt)\u@\H> '
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Shell Completion
|
|
126
|
+
|
|
127
|
+
Vex provides completion for virtualenv names. To enable it, add the following to your shell config:
|
|
128
|
+
|
|
129
|
+
**Bash (~/.bashrc):**
|
|
130
|
+
`eval "$(vex --shell-config bash)"`
|
|
131
|
+
|
|
132
|
+
**Zsh (~/.zshrc):**
|
|
133
|
+
`eval "$(vex --shell-config zsh)"`
|
|
134
|
+
|
|
135
|
+
**Fish (~/.config/fish/config.fish):**
|
|
136
|
+
`vex --shell-config fish | source`
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
Vex development is now managed with `uv`.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git clone https://github.com/a-hendo/vex
|
|
144
|
+
cd vex
|
|
145
|
+
uv sync
|
|
146
|
+
uv run pytest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Credits
|
|
150
|
+
|
|
151
|
+
Vex was originally created by [Sasha Hart](https://github.com/sashahart). This version is a modernized fork of the [original project](https://github.com/sashahart/vex).
|
vexx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Vex
|
|
2
|
+
|
|
3
|
+
**Run a command in a virtualenv.**
|
|
4
|
+
|
|
5
|
+
Vex is an elegant, shell-agnostic tool for running commands inside virtual environments. Unlike traditional methods that rely on "activating" and "deactivating" environments by mutating your current shell's state, Vex simply launches a new process with the correct environment variables (like `PATH` and `VIRTUAL_ENV`) already set.
|
|
6
|
+
|
|
7
|
+
When the command finishes, the environment is gone. No cleanup, no `deactivate`, no shell-specific hacks.
|
|
8
|
+
|
|
9
|
+
## Modernization
|
|
10
|
+
|
|
11
|
+
Vex has been modernized to support the latest Python standards:
|
|
12
|
+
|
|
13
|
+
- **Python 3.12+ Required**: Leveraging modern features and built-in type safety.
|
|
14
|
+
- **UV Integration**: Vex now detects and uses [uv](https://github.com/astral-sh/uv) for lightning-fast virtualenv creation when using `--make`.
|
|
15
|
+
- **Pyproject.toml**: Fully migrated to modern PEP 517/518 packaging.
|
|
16
|
+
- **Type Safety**: The entire codebase is now fully type-hinted.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
`vex` runs any command in a virtualenv without modifying your current shell.
|
|
21
|
+
|
|
22
|
+
The standard way to use a virtualenv involves sourcing an `activate` script which modifies your shell's environment and adds a `deactivate` function. This is often brittle and shell-specific.
|
|
23
|
+
|
|
24
|
+
Vex takes a simpler approach: it calculates the environment required for the virtualenv and passes it directly to a new subprocess. This makes it naturally compatible with **bash, zsh, fish, PowerShell, cmd.exe**, and any other shell or executable.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
- `vex myenv bash`: Launch a bash shell with virtualenv `myenv` enabled. Exit the shell (Ctrl-D) to "deactivate".
|
|
29
|
+
- `vex myenv python`: Launch a Python interpreter inside virtualenv `myenv`.
|
|
30
|
+
- `vex myenv pip install requests`: Install a package into `myenv` without activating it.
|
|
31
|
+
- `vex -m ephemeral echo "Hello"`: Create a new virtualenv named `ephemeral`, run the command, and then immediately **remove** the virtualenv.
|
|
32
|
+
- `vex --path ./venv python script.py`: Run a script using a virtualenv located at a specific path.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
The recommended way to install Vex is using [uv](https://github.com/astral-sh/uv):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv tool install vexx
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Alternatively, you can use pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install --user vexx
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Usage with UV
|
|
49
|
+
|
|
50
|
+
Vex is designed to work seamlessly with `uv`. If `uv` is installed on your system, `vex --make` will automatically use `uv venv` to create environments, which is significantly faster than traditional `virtualenv`.
|
|
51
|
+
|
|
52
|
+
## Config
|
|
53
|
+
|
|
54
|
+
Vex looks for an optional configuration file at `~/.vexrc`.
|
|
55
|
+
|
|
56
|
+
```ini
|
|
57
|
+
shell=bash
|
|
58
|
+
virtualenvs=~/.virtualenvs
|
|
59
|
+
python=python3.12
|
|
60
|
+
env:
|
|
61
|
+
ANSWER=42
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **shell**: The default shell to run if no command is provided (e.g., `vex myenv`).
|
|
65
|
+
- **virtualenvs**: The directory where your named virtualenvs are stored (defaults to `$WORKON_HOME` or `~/.virtualenvs`).
|
|
66
|
+
- **python**: The default Python executable to use when creating new environments with `--make`.
|
|
67
|
+
- **env**: Custom environment variables to inject into every Vex-managed process.
|
|
68
|
+
|
|
69
|
+
## Environment Variables
|
|
70
|
+
|
|
71
|
+
### WORKON_HOME
|
|
72
|
+
|
|
73
|
+
Vex uses the `WORKON_HOME` environment variable to determine where named virtual environments are stored. By default, this is `~/.virtualenvs`.
|
|
74
|
+
|
|
75
|
+
If you are using **uv** in a project and want Vex to target virtual environments local to your current directory (like a `.venv` folder), it is recommended to set `WORKON_HOME` to `./`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export WORKON_HOME="./"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This allows you to use `vex .venv python` to run commands in your local project environment seamlessly.
|
|
82
|
+
|
|
83
|
+
## Shell Prompts
|
|
84
|
+
|
|
85
|
+
Vex does not automatically change your prompt. To see the current virtualenv in your prompt, you can use the `$VIRTUAL_ENV` variable in your shell configuration.
|
|
86
|
+
|
|
87
|
+
**Bash Example (~/.bashrc):**
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
function virtualenv_prompt() {
|
|
91
|
+
if [ -n "$VIRTUAL_ENV" ]; then
|
|
92
|
+
echo "(${VIRTUAL_ENV##*/}) "
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
95
|
+
export PS1='$(virtualenv_prompt)\u@\H> '
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Shell Completion
|
|
99
|
+
|
|
100
|
+
Vex provides completion for virtualenv names. To enable it, add the following to your shell config:
|
|
101
|
+
|
|
102
|
+
**Bash (~/.bashrc):**
|
|
103
|
+
`eval "$(vex --shell-config bash)"`
|
|
104
|
+
|
|
105
|
+
**Zsh (~/.zshrc):**
|
|
106
|
+
`eval "$(vex --shell-config zsh)"`
|
|
107
|
+
|
|
108
|
+
**Fish (~/.config/fish/config.fish):**
|
|
109
|
+
`vex --shell-config fish | source`
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
Vex development is now managed with `uv`.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/a-hendo/vex
|
|
117
|
+
cd vex
|
|
118
|
+
uv sync
|
|
119
|
+
uv run pytest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Credits
|
|
123
|
+
|
|
124
|
+
Vex was originally created by [Sasha Hart](https://github.com/sashahart). This version is a modernized fork of the [original project](https://github.com/sashahart/vex).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vexx"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Run commands in a virtualenv"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["virtualenv", "virtualenvwrapper", "workon", "installation", "deployment"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Adam Henderson", email = "adamhenderson90@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Topic :: Utilities",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
"Topic :: System :: Shells",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: System Administrators",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Operating System :: POSIX",
|
|
26
|
+
"Operating System :: POSIX :: Linux",
|
|
27
|
+
"Operating System :: Microsoft :: Windows",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"virtualenv",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/a-hendo/vex"
|
|
37
|
+
Repository = "https://github.com/a-hendo/vex"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
vex = "vex.main:main"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "vex/_version.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["vex"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = [
|
|
50
|
+
"/vex",
|
|
51
|
+
"/LICENSE",
|
|
52
|
+
"/README.md",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
dev = [
|
|
57
|
+
"pytest",
|
|
58
|
+
"mypy",
|
|
59
|
+
"ruff",
|
|
60
|
+
"tox",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 88
|
|
65
|
+
target-version = "py312"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
69
|
+
ignore = []
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.12"
|
|
73
|
+
strict = true
|
|
74
|
+
warn_return_any = true
|
|
75
|
+
warn_unused_configs = true
|
|
76
|
+
disallow_untyped_defs = true
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["vex/tests", "vex/functional_tests"]
|
|
80
|
+
|
|
81
|
+
[tool.tox]
|
|
82
|
+
legacy_tox_ini = """
|
|
83
|
+
[tox]
|
|
84
|
+
envlist = py312, py313
|
|
85
|
+
isolated_build = True
|
|
86
|
+
|
|
87
|
+
[testenv]
|
|
88
|
+
description = run tests
|
|
89
|
+
deps =
|
|
90
|
+
pytest
|
|
91
|
+
commands =
|
|
92
|
+
pytest {posargs:vex/tests vex/functional_tests}
|
|
93
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.1.0"
|
vexx-0.1.0/vex/config.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Config file processing (.vexrc)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import Generator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from re import Pattern
|
|
12
|
+
|
|
13
|
+
_IDENTIFIER_PATTERN: str = "[a-zA-Z][_a-zA-Z0-9]*"
|
|
14
|
+
_SQUOTE_RE: Pattern[str] = re.compile(r"'([^']*)'\Z") # NO squotes inside
|
|
15
|
+
_DQUOTE_RE: Pattern[str] = re.compile(r'"([^"]*)"\Z') # NO dquotes inside
|
|
16
|
+
_HEADING_RE: Pattern[str] = re.compile(rf"^({_IDENTIFIER_PATTERN}):[ \t\n\r]*\Z")
|
|
17
|
+
_VAR_RE: Pattern[str] = re.compile(
|
|
18
|
+
rf"[ \t]*({_IDENTIFIER_PATTERN}) *= *(.*)[ \t\n\r]*$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidConfigError(Exception):
|
|
23
|
+
"""Raised when there is an error during a .vexrc file parse."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, filename: str, errors: list[tuple[int, str]]):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.filename = filename
|
|
28
|
+
self.errors = errors
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return f"errors in {self.filename!r}, lines {[tup[0] for tup in self.errors]!r}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Vexrc:
|
|
35
|
+
"""Parsed representation of a .vexrc config file."""
|
|
36
|
+
|
|
37
|
+
default_heading: str = "root"
|
|
38
|
+
default_encoding: str = "utf-8"
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
self.encoding: str = self.default_encoding
|
|
42
|
+
self.headings: dict[str, dict[str, str]] = OrderedDict()
|
|
43
|
+
self.headings[self.default_heading] = OrderedDict()
|
|
44
|
+
self.headings["env"] = OrderedDict()
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: str) -> dict[str, str] | None:
|
|
47
|
+
return self.headings.get(key)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_file(cls, path: str, environ: dict[str, str]) -> "Vexrc":
|
|
51
|
+
"""Make a Vexrc instance from given file in given environ."""
|
|
52
|
+
instance = cls()
|
|
53
|
+
instance.read(path, environ)
|
|
54
|
+
return instance
|
|
55
|
+
|
|
56
|
+
def read(self, path: str, environ: dict[str, str]) -> None:
|
|
57
|
+
"""Read data from file into this vexrc instance."""
|
|
58
|
+
try:
|
|
59
|
+
with open(path, "rb") as inp:
|
|
60
|
+
parsing = parse_vexrc(inp, environ)
|
|
61
|
+
for heading, key, value in parsing:
|
|
62
|
+
heading = self.default_heading if heading is None else heading
|
|
63
|
+
if heading not in self.headings:
|
|
64
|
+
self.headings[heading] = OrderedDict()
|
|
65
|
+
self.headings[heading][key] = value
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def get_ve_base(self, environ: dict[str, str]) -> str:
|
|
70
|
+
"""Find a directory to look for virtualenvs in."""
|
|
71
|
+
# set ve_base to a path we can look for virtualenvs:
|
|
72
|
+
# 1. .vexrc
|
|
73
|
+
# 2. WORKON_HOME (as defined for virtualenvwrapper's benefit)
|
|
74
|
+
# 3. $HOME/.virtualenvs
|
|
75
|
+
ve_base_value: str | None = self.headings[self.default_heading].get(
|
|
76
|
+
"virtualenvs"
|
|
77
|
+
)
|
|
78
|
+
ve_base: str = ""
|
|
79
|
+
if ve_base_value:
|
|
80
|
+
ve_base = os.path.expanduser(ve_base_value)
|
|
81
|
+
else:
|
|
82
|
+
ve_base = environ.get("WORKON_HOME", "")
|
|
83
|
+
|
|
84
|
+
if not ve_base:
|
|
85
|
+
home: str = ""
|
|
86
|
+
if platform.system() == "Windows":
|
|
87
|
+
_win_drive: str = environ.get("HOMEDRIVE", "")
|
|
88
|
+
home_path: str = environ.get("HOMEPATH", "")
|
|
89
|
+
if home_path:
|
|
90
|
+
home = os.path.join(_win_drive, home_path)
|
|
91
|
+
else:
|
|
92
|
+
home = os.path.expanduser("~")
|
|
93
|
+
else:
|
|
94
|
+
home = environ.get("HOME", "")
|
|
95
|
+
if not home:
|
|
96
|
+
home = os.path.expanduser("~")
|
|
97
|
+
|
|
98
|
+
if not home:
|
|
99
|
+
return ""
|
|
100
|
+
ve_base = os.path.join(home, ".virtualenvs")
|
|
101
|
+
|
|
102
|
+
return ve_base
|
|
103
|
+
|
|
104
|
+
def get_shell(self, environ: dict[str, str]) -> list[str] | None:
|
|
105
|
+
"""Find a command to run."""
|
|
106
|
+
command: str | None = self.headings[self.default_heading].get("shell")
|
|
107
|
+
if not command and os.name != "nt":
|
|
108
|
+
command = environ.get("SHELL", "")
|
|
109
|
+
return shlex.split(command) if command else None
|
|
110
|
+
|
|
111
|
+
def get_default_python(self, environ: dict[str, str]) -> str | None:
|
|
112
|
+
"""Find a command to run."""
|
|
113
|
+
return self.headings[self.default_heading].get("python")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def extract_heading(line: str) -> str | None:
|
|
117
|
+
"""Return heading in given line or None if it's not a heading."""
|
|
118
|
+
match: Any = _HEADING_RE.match(line)
|
|
119
|
+
return match.group(1) if match else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def extract_key_value(line: str, environ: dict[str, str]) -> tuple[str, str] | None:
|
|
123
|
+
"""Return key, value from given line if present, else return None."""
|
|
124
|
+
segments: list[str] = line.split("=", 1)
|
|
125
|
+
if len(segments) < 2:
|
|
126
|
+
return None
|
|
127
|
+
key, value = segments
|
|
128
|
+
value = value.strip()
|
|
129
|
+
if value:
|
|
130
|
+
if value.startswith("'") and _SQUOTE_RE.match(value):
|
|
131
|
+
value = value[1:-1]
|
|
132
|
+
elif value.startswith('"') and _DQUOTE_RE.match(value):
|
|
133
|
+
template: str = value[1:-1]
|
|
134
|
+
try:
|
|
135
|
+
value = template.format(**environ)
|
|
136
|
+
except KeyError:
|
|
137
|
+
# Fallback to literal if env var missing?
|
|
138
|
+
pass
|
|
139
|
+
value = value.strip()
|
|
140
|
+
key = key.strip()
|
|
141
|
+
return key, value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def parse_vexrc(
|
|
145
|
+
inp: Any, environ: dict[str, str]
|
|
146
|
+
) -> Generator[tuple[str | None, str, str], None, None]:
|
|
147
|
+
"""Iterator yielding key/value pairs from given stream."""
|
|
148
|
+
heading: str | None = None
|
|
149
|
+
errors: list[Any] = []
|
|
150
|
+
for line_number, line_bytes in enumerate(inp):
|
|
151
|
+
line = line_bytes.decode("utf-8")
|
|
152
|
+
if not line.strip():
|
|
153
|
+
continue
|
|
154
|
+
extracted_heading: str | None = extract_heading(line)
|
|
155
|
+
if extracted_heading is not None:
|
|
156
|
+
heading = extracted_heading
|
|
157
|
+
continue
|
|
158
|
+
kv_tuple: tuple[str, str] | None = extract_key_value(line, environ)
|
|
159
|
+
if kv_tuple is None:
|
|
160
|
+
errors.append((line_number, line))
|
|
161
|
+
continue
|
|
162
|
+
yield heading, kv_tuple[0], kv_tuple[1]
|
|
163
|
+
|
|
164
|
+
if errors:
|
|
165
|
+
raise InvalidConfigError(getattr(inp, "name", "unknown"), errors)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Exceptions for vex."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InvalidArgument(Exception):
|
|
5
|
+
"""Base class for exceptions raised by anything under main()."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str) -> None:
|
|
8
|
+
self.message: str = message
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoVirtualenvName(InvalidArgument):
|
|
13
|
+
"""No virtualenv name was given."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NoVirtualenvsDirectory(InvalidArgument):
|
|
17
|
+
"""There is no directory to find named virtualenvs in."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OtherShell(InvalidArgument):
|
|
21
|
+
"""The given argument to --shell-config is not recognized."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnknownArguments(InvalidArgument):
|
|
25
|
+
"""Unknown arguments were given on the command line."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InvalidVexrc(InvalidArgument):
|
|
29
|
+
"""Config file specified or required but absent or unparseable."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvalidVirtualenv(InvalidArgument):
|
|
33
|
+
"""No usable virtualenv was found."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidCommand(InvalidArgument):
|
|
37
|
+
"""No runnable command was found."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InvalidCwd(InvalidArgument):
|
|
41
|
+
"""cwd specified or required but unusable."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BadConfig(InvalidArgument):
|
|
45
|
+
"""Fatal conditions encountered on the way to run."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class VirtualenvAlreadyMade(InvalidArgument):
|
|
49
|
+
"""Virtualenv already exists."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VirtualenvNotMade(InvalidArgument):
|
|
53
|
+
"""Could not make virtualenv."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class VirtualenvNotRemoved(InvalidArgument):
|
|
57
|
+
"""Virtualenv could not be removed."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
CommandNotFoundError = FileNotFoundError
|
|
File without changes
|