Skip to content

Commit 2f0f1f5

Browse files
committed
Version 1.0.0
Signed-off-by: Stefan Mejlgaard <[email protected]>
1 parent 52fac90 commit 2f0f1f5

20 files changed

+778
-0
lines changed

dev-requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
black ~= 23.1
2+
isort ~= 5.12
3+
mypy ~= 1.0
4+
pytest ~= 7.4
5+
pytest-order ~= 1.1
6+
pytest-xdist ~= 3.3

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[tool.mypy]
2+
python_version = "3.10"
3+
files = "**/*.py"
4+
5+
[tool.isort]
6+
profile = "black"
7+
line_length = 120
8+
combine_as_imports = true
9+
src_paths = ["src", "tests"]
10+
11+
[tool.black]
12+
line_length = 120
13+
14+
[tool.pytest.ini_options]
15+
pythonpath = ["."]

src/venv-cli/venv.sh

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
#!/bin/bash
2+
3+
_normal="\033[00m"
4+
_green="\033[32m"
5+
_yellow="\033[01;33m"
6+
_red="\033[31m"
7+
8+
_version="1.0.0"
9+
10+
venv::_version() {
11+
echo "venv-cli ${_version}"
12+
return 0
13+
}
14+
15+
venv::color_echo() {
16+
local color="$1"
17+
local string="$2"
18+
echo -e "${color}${string}${_normal}"
19+
}
20+
21+
venv::raise() {
22+
if [ -n "$1" ]; then
23+
venv::color_echo "${_red}" "$1"
24+
fi
25+
return 1
26+
}
27+
28+
venv::_check_if_help_requested() {
29+
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
30+
return 0
31+
fi
32+
return 1
33+
}
34+
35+
venv::_check_install_requirements_file() {
36+
local file_pattern="^.*?requirements\.(txt|lock)"
37+
if [[ ! "$1" =~ $file_pattern ]]; then
38+
venv::raise "Input file name must have format '*requirements.txt' or '*requirements.lock'"
39+
return "$?"
40+
fi
41+
}
42+
43+
venv::_check_lock_requirements_file() {
44+
local file_pattern="^.*?requirements\.lock$"
45+
if [[ ! "$1" =~ $file_pattern ]]; then
46+
venv::raise "Lock file name must have format '*requirements.lock'"
47+
return "$?"
48+
fi
49+
}
50+
51+
52+
venv::create() {
53+
if venv::_check_if_help_requested "$1"; then
54+
echo "venv create <python-version> [<name>]"
55+
echo
56+
echo "Create a new virtual environment using python version <python-version>."
57+
echo "The virtual environment will be placed in '.venv', "
58+
echo "and when activated will be named the same as the containing folder."
59+
echo "It is also possible to specify the name that will be used in the shell prompt."
60+
echo
61+
echo 'Requires an executable python of version <python-version> on $PATH'
62+
echo
63+
echo "Examples:"
64+
echo "$ venv create 3.9 my-39-env"
65+
echo "This will create a virtual environment in '.venv' called 'my-39-env' using python3.9."
66+
echo
67+
echo "$ venv create 3.9"
68+
echo "When run from a folder called 'my-folder', this wil create a virtual environment "
69+
echo "called 'my-folder' using python3.9."
70+
return 0
71+
fi
72+
73+
if [ -z "$1" ]; then
74+
venv::raise "You need to specify the python version to use, e.g. 'venv-create 3.10'"
75+
return "$?"
76+
fi
77+
78+
local python_version="$1"
79+
local venv_prompt='.'
80+
local venv_name
81+
venv_name="$(basename "${PWD}")"
82+
83+
# Check if a specific name for the "--prompt" was specified
84+
if [ -n "$2" ]; then
85+
venv_prompt="$2"
86+
venv_name="${venv_prompt}"
87+
fi
88+
89+
# Check if python command exists
90+
local python_executable="python${python_version}"
91+
if ! command -v "${python_executable}" > /dev/null; then
92+
venv::raise "Couldn't locate '${python_executable}'. Please make sure it is installed and on \$PATH."
93+
return "$?"
94+
fi
95+
96+
venv::color_echo "${_green}" "Creating virtual environment '${venv_name}' using python${python_version}"
97+
${python_executable} -m venv .venv --prompt "${venv_prompt}"
98+
}
99+
100+
101+
venv::activate() {
102+
103+
if venv::_check_if_help_requested "$1"; then
104+
echo "venv activate"
105+
echo
106+
echo "Activate the virtual environment located in the current folder."
107+
echo
108+
echo "Examples:"
109+
echo "$ venv activate"
110+
return 0
111+
fi
112+
113+
. ./.venv/bin/activate
114+
}
115+
116+
117+
venv::deactivate() {
118+
if venv::_check_if_help_requested "$1"; then
119+
echo "venv deactivate"
120+
echo
121+
echo "Deactivate the currently activated virtual environment."
122+
echo
123+
echo "Examples:"
124+
echo "$ venv deactivate"
125+
return 0
126+
fi
127+
128+
if ! deactivate 2> /dev/null; then
129+
venv::color_echo "${_yellow}" "No virtual environment currently active, nothing to deactivate."
130+
fi
131+
}
132+
133+
134+
venv::install() {
135+
if venv::_check_if_help_requested "$1"; then
136+
echo "venv install [<requirements file>] [--skip-lock]"
137+
echo
138+
echo "Install requirements from <requirements file>, like 'requirements.txt' or 'requirements.lock'."
139+
echo "Installed packages are then locked into the corresponding .lock-file, "
140+
echo "e.g. 'venv install requirements.txt' will lock packages into 'requirements.lock'."
141+
echo "This step is skipped if '--skip-lock' is specified, or when installing directly from a .lock-file."
142+
echo
143+
echo "The <requirements file> must be in the form '*requirements.[txt|lock]'."
144+
echo "If no arguments are passed, a default file name of 'requirements.txt' will be used."
145+
echo
146+
echo "Examples:"
147+
echo "$ venv install"
148+
echo
149+
echo "$ venv install dev-requirements.txt"
150+
echo
151+
echo "$ venv install requirements.txt --skip-lock"
152+
return 0
153+
fi
154+
155+
local requirements_file
156+
if [ -z "$1" ] || [ "$1" == "--skip-lock" ]; then
157+
# If no argument passed
158+
requirements_file="requirements.txt"
159+
160+
else
161+
if ! venv::_check_install_requirements_file "$1"; then
162+
# Fail if file name doesn't match required format
163+
return 1
164+
fi
165+
166+
# If full requirements file (.txt or .lock) passed
167+
requirements_file="$1"
168+
shift
169+
fi
170+
171+
local skip_lock=false
172+
if [ "$1" == "--skip-lock" ]; then
173+
skip_lock=true
174+
shift
175+
fi
176+
177+
venv::color_echo "${_green}" "Installing requirements from ${requirements_file}"
178+
pip install --require-virtualenv -r "${requirements_file}" "$@"
179+
180+
local lock_file="${requirements_file/.txt/.lock}" # Replace ".txt" with ".lock"
181+
if "${skip_lock}" || [ "${requirements_file}" == "${lock_file}" ]; then
182+
venv::color_echo "${_yellow}" "Skipping locking packages to ${lock_file}"
183+
return 0
184+
fi
185+
186+
venv::lock "${lock_file}"
187+
return "$?" # Return exit status from venv::lock command
188+
}
189+
190+
191+
venv::lock() {
192+
if venv::_check_if_help_requested "$1"; then
193+
echo "venv lock [<lock file>|<lock file prefix>]"
194+
echo
195+
echo "Lock all installed package versions and write them to <lock file>."
196+
echo "The <lock file> must be in the form '*requirements.lock'."
197+
echo
198+
echo "If <lock file prefix> is specified instead, locks the requirements to "
199+
echo "a file called '<lock file prefix>-requirements.lock'."
200+
echo
201+
echo "If no <lock file> is specified, defaults to 'requirements.lock'."
202+
echo
203+
echo "Examples:"
204+
echo "$ venv lock"
205+
echo
206+
echo "$ venv lock dev-requirements.lock"
207+
echo
208+
echo "$ venv lock dev"
209+
echo "This will lock the requirements into a file named 'dev-requirements.lock'."
210+
return 0
211+
fi
212+
213+
local lock_file
214+
# If nothing passed, default to "requirements.lock"
215+
if [ -z "$1" ]; then
216+
lock_file="requirements.lock"
217+
218+
elif [[ "$1" == *"."* ]]; then
219+
if venv::_check_lock_requirements_file "$1"; then
220+
lock_file="$1"
221+
shift
222+
else
223+
return 1
224+
fi
225+
226+
else
227+
lock_file="$1-requirements.lock"
228+
fi
229+
230+
pip freeze --require-virtualenv > "${lock_file}"
231+
venv::color_echo "${_green}" "Locked requirements in ${lock_file}"
232+
}
233+
234+
235+
venv::clear() {
236+
if venv::_check_if_help_requested "$1"; then
237+
echo "venv clear"
238+
echo
239+
echo "Clear the virtual environment by uninstalling all packages."
240+
echo
241+
echo "Examples:"
242+
echo "$ venv clear"
243+
return 0
244+
fi
245+
246+
venv::color_echo "${_yellow}" "Removing all packages from virtual environment ..."
247+
pip freeze --require-virtualenv \
248+
| cut -d "@" -f1 \
249+
| xargs --no-run-if-empty pip uninstall --require-virtualenv -y
250+
venv::color_echo "${_green}" "All packages removed!"
251+
}
252+
253+
254+
venv::sync() {
255+
if venv::_check_if_help_requested "$1"; then
256+
echo "venv sync [<lock file>]"
257+
echo
258+
echo "Remove all installed packages from the environment (venv clear) "
259+
echo "and install all packages specified in <lock file>."
260+
echo "The <lock file> must be in the form '*requirements.lock'."
261+
echo
262+
echo "If no <lock file> is specified, defaults to 'requirements.lock'."
263+
echo
264+
echo "Examples:"
265+
echo "$ venv sync dev-requirements.lock"
266+
echo "Clears the environment and installs requirements from 'dev-requirements.lock'."
267+
echo
268+
echo "$ venv sync"
269+
echo "Tries to install from 'requirements.lock'."
270+
echo "Clears the environment and installs requirements from 'requirements.lock'."
271+
return 0
272+
fi
273+
274+
local lock_file
275+
if [ -z "$1" ]; then
276+
# If no argument passed
277+
lock_file="requirements.lock"
278+
279+
# If full lock file passed
280+
else
281+
if ! venv::_check_lock_requirements_file "$1" "Can only sync using .lock file"; then
282+
return 1
283+
fi
284+
285+
lock_file="$1"
286+
shift
287+
fi
288+
289+
venv::clear
290+
venv::install "${lock_file}" "$@"
291+
return "$?" # Return exit status from venv::install command
292+
}
293+
294+
295+
venv::help() {
296+
echo "Utility to help create and manage python virtual environments."
297+
echo "Lightweight wrapper around pip and venv."
298+
echo
299+
echo "Syntax: venv [-h|--help] [-v|--version] <command> [<args>|-h|--help]"
300+
echo
301+
echo "The available commands are:"
302+
echo
303+
echo "create Create a new virtual environment in the current folder"
304+
echo "activate Activate the virtual environment in the current folder"
305+
echo "install Install requirements from a requirements file in the current environment"
306+
echo "lock Lock installed requirements in a '.lock'-file"
307+
echo "clear Remove all installed packages in the current environment"
308+
echo "sync Run 'venv clear', then install locked requirements from a "
309+
echo " '.lock'-file in the current environment"
310+
echo "deactivate Deactivate the currently activated virtual environment"
311+
echo "-h, --help Show this help and exit"
312+
echo "-v, --version Show the venv-cli version number and exit"
313+
echo
314+
echo "You can also run 'venv <command> --help' to get help with each subcommand."
315+
}
316+
317+
318+
venv::main() {
319+
subcommand="$1"
320+
321+
case "${subcommand}" in
322+
"" | "-h" | "--help")
323+
venv::help
324+
;;
325+
326+
"-V" | "--version")
327+
venv::_version
328+
;;
329+
330+
create \
331+
| activate \
332+
| install \
333+
| lock \
334+
| clear \
335+
| sync \
336+
| deactivate \
337+
)
338+
shift
339+
venv::"${subcommand}" "$@"
340+
;;
341+
342+
*)
343+
echo $"Unknown subcommand '${subcommand}'. Try 'venv --help' to see available commands."
344+
;;
345+
346+
esac
347+
}
348+
349+
venv() {
350+
venv::main "$@"
351+
}

0 commit comments

Comments
 (0)