├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitmodules
├── .releaserc.json
├── LICENSE
├── README.md
├── commitlint.config.js
├── runtest.sh
├── src
└── dpv
├── test
├── helper.sh
├── run_dpv.sh
├── test_commands.sh
└── test_functions.sh
└── themes
└── creator.sh
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | jobs:
13 | ubuntu-pyenv:
14 | runs-on: ubuntu-latest
15 | continue-on-error: true
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | include:
20 | - { test_shell: "ash" }
21 | - { test_shell: "bash" }
22 | - { test_shell: "dash" }
23 | - { test_shell: "ksh" }
24 | - { test_shell: "zsh" }
25 |
26 | steps:
27 | - uses: actions/checkout@v2
28 | with:
29 | submodules: recursive
30 |
31 | - name: Install shell
32 | run: sudo apt-get install "${{ matrix.test_shell }}"
33 |
34 | - name: Choose shell
35 | run: |
36 | echo 'TEST_SHELL=${{ matrix.test_shell }}' >> $GITHUB_ENV
37 | command -v ${{ matrix.test_shell }}
38 |
39 | - uses: gabrielfalcao/pyenv-action@v14
40 | with:
41 | default: "3.9.13"
42 |
43 | - name: Test functions with pyenv
44 | run: ./runtest.sh test/test_functions.sh --filter-tags vendor:pyenv
45 |
46 | - name: Test commands
47 | run: ./runtest.sh test/test_commands.sh
48 |
49 | macos-homebrew:
50 | runs-on: macos-latest
51 | strategy:
52 | matrix:
53 | python:
54 | - "3.9.13"
55 |
56 | steps:
57 | - uses: actions/checkout@v2
58 | with:
59 | submodules: recursive
60 |
61 | - name: Test functions with homebrew
62 | run: ./runtest.sh test/test_functions.sh --filter-tags vendor:homebrew
63 | env:
64 | BATS_MOCK_HOMEBREW: 0
65 |
66 | - name: Test commands
67 | run: ./runtest.sh test/test_commands.sh
68 |
69 | macos-pyenv:
70 | runs-on: macos-latest
71 | strategy:
72 | matrix:
73 | python:
74 | - "3.9.13"
75 |
76 | steps:
77 | - uses: actions/checkout@v2
78 | with:
79 | submodules: recursive
80 |
81 | - name: Install pyenv
82 | run: brew install pyenv
83 |
84 | - name: Install Python ${{ matrix.python }} with pyenv
85 | run: pyenv install "${{ matrix.python }}"
86 |
87 | - name: Test functions with pyenv
88 | run: ./runtest.sh test/test_functions.sh --filter-tags vendor:pyenv
89 | env:
90 | BATS_MOCK_PYENV: 0
91 |
92 | - name: Test commands
93 | run: ./runtest.sh test/test_commands.sh
94 |
95 | macos-uv:
96 | runs-on: macos-latest
97 | strategy:
98 | matrix:
99 | python:
100 | - "3.12.3"
101 |
102 | steps:
103 | - uses: actions/checkout@v2
104 | with:
105 | submodules: recursive
106 |
107 | - name: Set up uv
108 | # Install latest uv version using the installer
109 | run: curl -LsSf https://astral.sh/uv/install.sh | sh
110 |
111 | - name: Install Python ${{ matrix.python }} with uv
112 | run: uv python install "${{ matrix.python }}"
113 |
114 | - name: Test functions with uv
115 | run: ./runtest.sh test/test_functions.sh --filter-tags vendor:uv
116 |
117 | - name: Test commands
118 | run: ./runtest.sh test/test_commands.sh
119 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | permissions:
9 | contents: read # for checkout
10 |
11 | jobs:
12 | release:
13 | name: Release
14 | runs-on: ubuntu-latest
15 |
16 | permissions:
17 | contents: write # to be able to publish a GitHub release
18 | issues: write # to be able to comment on released issues
19 | pull-requests: write # to be able to comment on released pull requests
20 | id-token: write # to enable use of OIDC for npm provenance
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 | with:
26 | fetch-depth: 0
27 |
28 | - name: Setup Node.js
29 | uses: actions/setup-node@v3
30 | with:
31 | node-version: "lts/*"
32 |
33 | - name: Install dependencies
34 | run: |
35 | npm install commitlint \
36 | @commitlint/config-conventional \
37 | @semantic-release/exec \
38 | @semantic-release/git \
39 | semantic-release-replace-plugin \
40 | -D
41 |
42 | - name: Validate Pull Request title with commitlint
43 | if: github.event_name == 'pull_request'
44 | env:
45 | PR_TITLE: "${{ github.event.pull_request.title }}"
46 | run: |
47 | printenv "PR_TITLE" | npx commitlint
48 |
49 | - name: Release
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | run: npx semantic-release
53 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "test/bats"]
2 | path = test/bats
3 | url = https://github.com/bats-core/bats-core.git
4 | [submodule "test/test_helper/bats-support"]
5 | path = test/test_helper/bats-support
6 | url = https://github.com/bats-core/bats-support.git
7 | [submodule "test/test_helper/bats-assert"]
8 | path = test/test_helper/bats-assert
9 | url = https://github.com/bats-core/bats-assert.git
10 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main"],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer",
5 | [
6 | "semantic-release-replace-plugin",
7 | {
8 | "replacements": [
9 | {
10 | "files": ["src/dpv"],
11 | "from": "CFG_VERSION=\".*\"",
12 | "to": "CFG_VERSION=\"${nextRelease.version}\"",
13 | "results": [
14 | {
15 | "file": "src/dpv",
16 | "hasChanged": true,
17 | "numMatches": 1,
18 | "numReplacements": 1
19 | }
20 | ],
21 | "countMatches": true
22 | },
23 | {
24 | "files": ["README.md"],
25 | "from": "https://github.com/caioariede/dpv/releases/download/[^/]+/dpv",
26 | "to": "https://github.com/caioariede/dpv/releases/download/v${nextRelease.version}/dpv",
27 | "results": [
28 | {
29 | "file": "README.md",
30 | "hasChanged": true,
31 | "numMatches": 1,
32 | "numReplacements": 1
33 | }
34 | ],
35 | "countMatches": true
36 | }
37 | ]
38 | }
39 | ],
40 | "@semantic-release/release-notes-generator",
41 | [
42 | "@semantic-release/git",
43 | {
44 | "assets": ["src/dpv", "README.md"],
45 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
46 | }
47 | ],
48 | [
49 | "@semantic-release/github",
50 | {
51 | "assets": [
52 | {
53 | "path": "src/dpv",
54 | "name": "dpv",
55 | "label": "dpv"
56 | }
57 | ]
58 | }
59 | ]
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Caio Ariede
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | dpv (dee-pee-vee) is a dead simple alternative to [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) and [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/)
2 |
3 |
4 |
5 | [](https://github.com/caioariede/dpv/actions/workflows/ci.yml)
6 | 
7 | 
8 | 
9 | 
10 |
11 | ## Why?
12 |
13 | 1. It's simple. Just type `dpv` or `dpv ` and get the work done.
14 | 2. It's pure shell and POSIX-compliant tested with: ash bash dash ksh zsh
15 | 3. It's built to get out of the way!
16 |
17 | ## How?
18 |
19 | Think of `dpv` as an interface for whaterver of these tools you have installed: `uv`, `pyenv`, `brew`
20 |
21 | It will automatically pick the one that is available and use it for the job.
22 |
23 | ## Installation
24 |
25 | 1. **Download**
26 |
27 | ```bash
28 | sh -c 'curl -fsSLo $1 https://github.com/caioariede/dpv/releases/download/v0.14.0/dpv && chmod +x $1' -- /usr/local/bin/dpv
29 | ```
30 |
31 | _Optional: in case your `/usr/local/bin` directory is not writable yet, [see](https://superuser.com/a/717683)._
32 |
33 | ```bash
34 | sudo chown -R $(whoami) /usr/local/bin
35 | sudo chmod -R u=rwX,go=rX /usr/local/bin
36 | ```
37 |
38 | 2. **Configure** — Add the following line to your .bashrc, .zshrc, etc
39 |
40 | ```bash
41 | eval "$(dpv internal-load-shell)"
42 | ```
43 |
44 | Check out more details in the [installation instructions](https://github.com/caioariede/dpv/discussions/32)
45 |
46 | ## Usage
47 |
48 | Take a look at the [dpv cheatsheet](https://github.com/caioariede/dpv/discussions/38) - or for more detailed instructions, [check the documentation](https://github.com/caioariede/dpv/discussions/33)
49 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/runtest.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ex
3 |
4 | echo "Running tests using shell: $(command -v "$TEST_SHELL")"
5 |
6 | ./test/bats/bin/bats --show-output-of-passing-tests "$@"
7 |
--------------------------------------------------------------------------------
/src/dpv:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # the d̲ead simple P̲ython v̲irtualenv manager
4 | #
5 | #
6 | # LICENCE
7 | # =====================================================================================
8 | #
9 | # MIT License
10 | #
11 | # Copyright (c) 2023 Caio Ariede
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in all
21 | # copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 | # SOFTWARE.
30 | #
31 | #
32 | # INSTALLATION
33 | # =====================================================================================
34 | #
35 | # 1. Copy this file to /usr/local/bin/dpv
36 | # 2. Give execution permission: "chmod +x /usr/local/bin/dpv"
37 | # 3. Add this line to your ~/.zshrc (or whatever):
38 | #
39 | # eval "$(dpv internal-load-shell)"
40 | #
41 | #
42 | # USAGE
43 | # =====================================================================================
44 | #
45 | # Type: dpv help
46 | #
47 | #
48 | # CODE GUIDELINES
49 | # =====================================================================================
50 | #
51 | # 1. Functions must be prefixed with "dpv_"
52 | # 2. Unsafe functions (one that may exit) must be prefixed with "unsafe_dpv_"
53 | # 3. Stateful functions (depending on INTERNAL_ vars) must be prefixed with "dpv_internal_"
54 | # 4. Functions that perform operations over pipes must be prefixed with "dpv_pipe_"
55 | # 5. Functions that returns boolean must be prefixed with "dpv_check_"
56 | # 6. The ø character is used to define emptiness (empty value)
57 | # 7. Exit codes are stored in variables prefixed with "ERR_"
58 | # 8. Configuration values are stored in variables prefixed with "CFG_"
59 | # 9. Vendor functions are prefixed with "dpv_VENDOR_" (vendor must be in uppercase)
60 | #
61 |
62 | set -eu
63 |
64 | #
65 | # Error exit codes
66 | #
67 |
68 | ERR_MAIN_INVALID_ARGUMENT=131
69 | ERR_INSTALL_METHOD_NOT_SELECTED=132
70 | ERR_CANNOT_ACTIVATE_VIRTUALENV=133
71 | ERR_RUN_INVALID_ARGUMENT=134
72 | ERR_CANNOT_DETERMINE_PYTHON_VERSION=135
73 | ERR_NO_AVAILABLE_INSTALL_METHODS=136
74 | ERR_CANNOT_RESOLVE_PYTHON_VERSION=137
75 | ERR_VIRTUALENV_INSTALL_METHOD_NOT_AVAILABLE=138
76 | ERR_INSTALLATION_FAILED=139
77 | ERR_CANNOT_CREATE_VIRTUALENV=140
78 | ERR_CANNOT_INSTALL_DEPENDENCIES=142
79 | ERR_VIRTUALENV_CANNOT_FIND_PYTHON_EXECUTABLE=141
80 | ERR_TRY_MISSING_DEPENDENCY_ARG=143
81 |
82 | # VARS: Configuration
83 | CFG_VERSION="0.14.0"
84 | CFG_THEME="${DPV_THEME:-cat}"
85 | CFG_DIR="${DPV_DIR:-$HOME/.dpv}"
86 | CFG_VIRTUALENVS_DIR="${DPV_MOCK_VIRTUALENVS_DIR:-$CFG_DIR/virtualenvs}"
87 | CFG_HOMEBREW_EXECUTABLE="${HOMEBREW_EXECUTABLE:-brew}"
88 | CFG_PYENV_EXECUTABLE="${PYENV_EXECUTABLE:-pyenv}"
89 | CFG_UV_EXECUTABLE="${UV_EXECUTABLE:-uv}"
90 | CFG_PREFERRED_INSTALL_METHODS="UV PYENV HOMEBREW"
91 |
92 | # VARS: Internal
93 | INTERNAL_UV_PROJECT_DIR="${DPV_MOCK_UV_PROJECT_DIR:-ø}"
94 | INTERNAL_UV_INSTALLED_PYTHON_VERSIONS="$([ ! -z "${DPV_MOCK_UV_INSTALLED_PYTHON_VERSIONS+x}" ] && echo "$DPV_MOCK_UV_INSTALLED_PYTHON_VERSIONS" || echo "ø")"
95 | INTERNAL_UV_AVAILABLE_PYTHON_VERSIONS="${DPV_MOCK_UV_AVAILABLE_PYTHON_VERSIONS:-ø}"
96 | INTERNAL_PYENV_INSTALLED_PYTHON_VERSIONS="$([ ! -z "${DPV_MOCK_PYENV_INSTALLED_PYTHON_VERSIONS+x}" ] && echo "$DPV_MOCK_PYENV_INSTALLED_PYTHON_VERSIONS" || echo "ø")"
97 | INTERNAL_PYENV_AVAILABLE_PYTHON_VERSIONS="${DPV_MOCK_PYENV_AVAILABLE_PYTHON_VERSIONS:-ø}"
98 | INTERNAL_HOMEBREW_INSTALLED_PYTHON_VERSIONS="$([ ! -z "${DPV_MOCK_HOMEBREW_INSTALLED_PYTHON_VERSIONS+x}" ] && echo "$DPV_MOCK_HOMEBREW_INSTALLED_PYTHON_VERSIONS" || echo "ø")"
99 | INTERNAL_HOMEBREW_AVAILABLE_PYTHON_VERSIONS="${DPV_MOCK_HOMEBREW_AVAILABLE_PYTHON_VERSIONS:-ø}"
100 | INTERNAL_AVAILABLE_INSTALL_METHODS="${DPV_MOCK_AVAILABLE_INSTALL_METHODS:-ø}"
101 | INTERNAL_LOG_FILE="${DPV_MOCK_LOG_FILE:-ø}"
102 | INTERNAL_RESOLVE_PYTHON_VERSION="${DPV_MOCK_RESOLVE_PYTHON_VERSION:-ø}"
103 | INTERNAL_RESOLVE_INSTALL_METHOD="${DPV_MOCK_RESOLVE_INSTALL_METHOD:-ø}"
104 | INTERNAL_ARG_PYTHON_VERSION="${DPV_MOCK_ARG_PYTHON_VERSION:-ø}"
105 | INTERNAL_ARG_DEPS="${DPV_MOCK_ARG_DEPS:-ø}"
106 | INTERNAL_INITIALIZE_VIRTUALENV_install_method="${DPV_MOCK_VIRTUALENV_INSTALL_METHOD:-ø}"
107 | INTERNAL_INITIALIZE_VIRTUALENV_python_version="${DPV_MOCK_VIRTUALENV_PYTHON_VERSION:-ø}"
108 | INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir="${DPV_MOCK_VIRTUALENV_DIR:-${DPV_VIRTUALENV_DIR:-ø}}"
109 |
110 | DPV_MOCK_PIP="${DPV_MOCK_PIP:-ø}"
111 | DPV_MOCK_UV_INSTALL="${DPV_MOCK_UV_INSTALL:-ø}"
112 |
113 | #
114 | # Command functions
115 | #
116 | # These functions are called by the dpv_internal_main() function
117 | # Unsafe commands should set a trap to the dpv_internal_error_handling() function
118 | #
119 |
120 | dpv_cmd_help() {
121 | #
122 | # Command: dpv help / dpv --help / dpv -h
123 | # Displays available commands and options
124 | #
125 | echo "dpv $CFG_VERSION"
126 | echo
127 | echo "usage:"
128 | echo " dpv [python version]"
129 | echo
130 | echo "commands:"
131 | echo " dpv help - display these instructions"
132 | echo " dpv info - display information about the current virtualenv"
133 | echo " dpv list - list virtualenvs created with dpv"
134 | echo " dpv run [command] - run command inside virtualenv [default: \$SHELL]"
135 | echo " --python [version] - specify python version"
136 | echo " --temp/--tmp - use a temporary virtualenv"
137 | echo " --with/-w [dep] - specify dependencies to pre-install"
138 | echo " dpv try [dep] - install dependency and start python shell"
139 | echo " dpv try [dep] -- [cmd] - install dependency and run command"
140 | echo " --with/-w [dep] - install extra dependency before running command"
141 | echo " dpv versions - display available python versions"
142 | echo " --installed/-i - display installed python versions"
143 | echo " --all/-a - display extended list of available python versions"
144 | echo " dpv drop [name] - remove virtualenv"
145 | echo " --dry-run/-d - show directories that would be removed"
146 | echo
147 | echo "global arguments:"
148 | echo " --uv - use uv"
149 | echo " --pyenv - use pyenv"
150 | echo " --homebrew - use homebrew"
151 | echo
152 | echo "aliases:"
153 | echo " dpv run / dpv"
154 | echo " dpv [version] / dpv run --python [version]"
155 | echo " dpv info / dpv (when virtualenv is activated)"
156 | echo " dpv help / --help / -h"
157 | echo " dpv list / --list / ls / -l"
158 | echo " dpv versions / -v"
159 | echo " dpv versions --all / -a"
160 | echo
161 |
162 | dpv_print_config
163 | }
164 |
165 | dpv_internal_cmd_versions() {
166 | #
167 | # Command: dpv versions
168 | # Lists the available Python versions
169 | #
170 | unsafe_dpv_internal_set_available_install_methods
171 |
172 | case "${1:-}" in
173 | --installed | -i)
174 | #
175 | # Command: dpv versions --installed
176 | # Lists *only* installed versions
177 | #
178 | echo "command: installed python versions"
179 | echo
180 |
181 | while IFS= read -r install; do
182 | eval "dpv_internal_${install}_installed_python_versions"
183 |
184 | installed_versions_var="INTERNAL_${install}_INSTALLED_PYTHON_VERSIONS"
185 | installed_versions=$(eval "echo \"\$$installed_versions_var\"")
186 |
187 | echo "$(dpv_string_lowercase "$install"): $(echo "$installed_versions" | dpv_internal_pipe_format_python_versions "$install" --all | dpv_pipe_format_nl_to_space)"
188 | done <&2; exit' SIGUSR1"
317 | echo "source $INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir/bin/activate || exit $ERR_CANNOT_ACTIVATE_VIRTUALENV"
318 | }
319 |
320 | dpv_cmd_list() {
321 | #
322 | # Command: dpv list
323 | # Lists virtualenvs managed by dpv
324 | #
325 | echo "command: list of virtualenvs"
326 | echo
327 |
328 | mkdir -p "$CFG_VIRTUALENVS_DIR"
329 | sort_key="$(($(dpv_string_count_characters "$CFG_VIRTUALENVS_DIR" "/") + 3))"
330 |
331 | find "$CFG_VIRTUALENVS_DIR" -maxdepth 2 -mindepth 2 | dpv_pipe_sort_path "$sort_key"
332 | }
333 |
334 | unsafe_dpv_internal_cmd_info() {
335 | #
336 | # Command: dpv info
337 | # Provides multiple information:
338 | # - Current virtualenv: if there is a virtualenv associated with the current
339 | # directory, displays information about it
340 | # - Config: display config information
341 | # - Logs: if any logs are available, print them
342 | #
343 | # Unsafe:
344 | # - unsafe_dpv_internal_set_available_install_methods
345 | #
346 | #
347 | echo "command: info"
348 | echo
349 |
350 | # Check available installation methods
351 | # If not possible, this will error out with: ERR_NO_AVAILABLE_INSTALL_METHODS
352 | unsafe_dpv_internal_set_available_install_methods
353 |
354 | dpv_internal_scan_python_version 1>/dev/null || true
355 |
356 | # Current virtualenv information
357 | virtualenvs_output="$(dpv_internal_find_virtualenvs)"
358 | if ! dpv_check_string_is_empty "$virtualenvs_output"; then
359 | while IFS= read -r virtualenv_dir; do
360 | dpv_internal_print_virtualenv "$virtualenv_dir"
361 | printf "\n"
362 | done <"$dpv_python_startup"
522 |
523 | def dpv_get_modules(packages):
524 | try:
525 | from importlib.metadata import packages_distributions
526 | except ImportError:
527 | import logging
528 | logging.warning("dpv only auto-import modules with Python 3.10+")
529 | else:
530 | import pkg_resources
531 |
532 | packages = [r.name for r in pkg_resources.parse_requirements(packages)]
533 | print(f"loading packages: {', '.join(packages)}")
534 | print("")
535 | for m, names in packages_distributions().items():
536 | for n in names:
537 | if n in packages:
538 | yield m
539 |
540 | print("")
541 | print("welcome to dpv try shell! :)")
542 | print("")
543 |
544 | for m in dpv_get_modules("$py_packages".split(",")):
545 | locals()[m] = __import__(m)
546 | print(f"- dpv imported module: {m}")
547 |
548 | print("")
549 |
550 | EOF
551 | PYTHONSTARTUP="$dpv_python_startup" unsafe_dpv_internal_cmd_run_wrapper "--with $dep --temp $args"
552 | ;;
553 | esac
554 | }
555 |
556 | #
557 | # Internal functions
558 | #
559 | dpv_internal_print_shell_banner() {
560 | #
561 | # Prints the banner when starting a virtualenv
562 | #
563 | printf "command: shell\n\n"
564 |
565 | dpv_internal_print_virtualenv "$INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir"
566 | dpv_internal_print_logs
567 | }
568 |
569 | dpv_print_config() {
570 | #
571 | # Prints current configuration
572 | #
573 | printf "config:\n"
574 | printf " CFG_VIRTUALENVS_DIR=%s\n" "$CFG_VIRTUALENVS_DIR"
575 | printf " CFG_THEME=%s\n" "$CFG_THEME"
576 | printf " CFG_DIR=%s\n" "$CFG_DIR"
577 | printf " CFG_UV_EXECUTABLE=%s\n" "$CFG_UV_EXECUTABLE"
578 | printf " CFG_PYENV_EXECUTABLE=%s\n" "$CFG_PYENV_EXECUTABLE"
579 | printf " CFG_HOMEBREW_EXECUTABLE=%s\n" "$CFG_HOMEBREW_EXECUTABLE"
580 | printf " CFG_PREFERRED_INSTALL_METHODS=%s\n" "$CFG_PREFERRED_INSTALL_METHODS"
581 | }
582 |
583 | dpv_internal_print_virtualenv() {
584 | #
585 | # Prints information for the given virtualenv
586 | #
587 | virtualenv_dir="$1"
588 |
589 | dpv_internal_parse_virtualenv_config_file "$virtualenv_dir/dpv.cfg"
590 |
591 | printf "virtualenv:\n"
592 | printf " name: %s\n" "$(basename "$virtualenv_dir")"
593 | printf " virtualenv path: %s\n" "$virtualenv_dir"
594 | printf " project path: %s\n" "$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_path"
595 | printf " python version: %s\n" "$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_install_method"
596 | printf " installation method: %s\n" "$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_install_method"
597 | if dpv_string_compare_casefold "$INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir" "$virtualenv_dir"; then
598 | printf " status: activated\n"
599 | else
600 | printf " status: not activated\n"
601 | fi
602 | }
603 |
604 | dpv_internal_print_logs() {
605 | #
606 | # Prints logs (if any)
607 | #
608 | if dpv_check_is_set "$INTERNAL_LOG_FILE" && ! dpv_check_file_is_empty "$INTERNAL_LOG_FILE"; then
609 | printf "\nlogs:\n"
610 | dpv_pipe_quote "-" <"$INTERNAL_LOG_FILE"
611 | fi
612 | }
613 |
614 | unsafe_dpv_internal_initialize_virtualenv() {
615 | #
616 | # Tries to find an existing virtualenv matching the following:
617 | # 1. The directory path
618 | # 2. The Python version, if provided as an argument
619 | #
620 | # If not match, a Python version is asked to the user
621 | # Once a Python version is provided, look for the closest match:
622 | # 1. Already installed by one of the installation methods
623 | # 2. Available by one of the installation methods
624 | # 3. Error out (ERR_CANNOT_RESOLVE_PYTHON_VERSION)
625 | #
626 | # Loads:
627 | # - INTERNAL_INITIALIZE_VIRTUALENV_python_version: The version resolved for the virtualenv
628 | # - INTERNAL_INITIALIZE_VIRTUALENV_install_method: The installaton method resolved for the virtualenv
629 | # - INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir: The directory path for the virtualenv
630 | #
631 | # Unsafe:
632 | # - unsafe_dpv_internal_initialize_virtualenv
633 | # - unsafe_dpv_internal_set_available_install_methods
634 | # - unsafe_dpv_internal_install_deps
635 | #
636 | if dpv_internal_scan_virtualenv; then
637 | # An existing virtualenv has been found, just use it
638 | INTERNAL_INITIALIZE_VIRTUALENV_python_version=$INTERNAL_SCAN_VIRTUALENV_version
639 | INTERNAL_INITIALIZE_VIRTUALENV_install_method=$INTERNAL_SCAN_VIRTUALENV_install_method
640 | INTERNAL_INITIALIZE_VIRTUALENV_virtualenv_dir=$INTERNAL_SCAN_VIRTUALENV_virtualenv_dir
641 | return
642 | fi
643 |
644 | # Check available installation methods
645 | # If not possible, this will error out with: ERR_NO_AVAILABLE_INSTALL_METHODS
646 | unsafe_dpv_internal_set_available_install_methods
647 |
648 | # Try to determine the Python version to use
649 | # If not possible, this will error out
650 | if ! dpv_internal_scan_python_version; then
651 | exit "$ERR_CANNOT_DETERMINE_PYTHON_VERSION"
652 | fi
653 |
654 | # Prompt user to confirm this is the Python version wanted
655 | python_version_user_input=""
656 | printf "python version [default: %s source: %s]: " "$INTERNAL_SCAN_PYTHON_VERSION_version" "$INTERNAL_SCAN_PYTHON_VERSION_source"
657 | read -r python_version_user_input
658 | INTERNAL_INITIALIZE_VIRTUALENV_python_version="${python_version_user_input:-$INTERNAL_SCAN_PYTHON_VERSION_version}"
659 |
660 | # Resolve Python version
661 | # If not possible, this will error out with: ERR_CANNOT_RESOLVE_PYTHON_VERSION
662 | if dpv_internal_resolve_python_version "$INTERNAL_INITIALIZE_VIRTUALENV_python_version"; then
663 | INTERNAL_INITIALIZE_VIRTUALENV_install_method="$INTERNAL_RESOLVE_INSTALL_METHOD"
664 | INTERNAL_INITIALIZE_VIRTUALENV_python_version="$INTERNAL_RESOLVE_PYTHON_VERSION"
665 | else
666 | exit "$ERR_CANNOT_RESOLVE_PYTHON_VERSION"
667 | fi
668 |
669 | # Install Python version
670 | # If version is already installed, install will be skipped
671 | # If not possible, this will error out with: ERR_INSTALLATION_FAILED
672 | func=$(eval "echo \"unsafe_dpv_internal_\"$INTERNAL_INITIALIZE_VIRTUALENV_install_method\"_install\"")
673 | eval "$func" <&1) || echo >"$fail") | dpv_pipe_quote
704 | if [ ! -s "$fail" ]; then
705 | echo "uv: done"
706 | else
707 | dpv_pipe_quote <"$fail"
708 | echo "uv: failed"
709 | exit "$ERR_CANNOT_INSTALL_DEPENDENCIES"
710 | fi
711 | ;;
712 | *)
713 | if ! dpv_check_is_set "$DPV_MOCK_PIP"; then
714 | pip="$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir/bin/python -m pip"
715 | else
716 | pip="$DPV_MOCK_PIP"
717 | fi
718 | fail="$(dpv_make_temp_file)"
719 | echo "pip: installing dependencies"
720 | ( ($pip install setuptools $INTERNAL_ARG_DEPS 2>&1) || echo >"$fail") | dpv_pipe_quote
721 | if [ ! -s "$fail" ]; then
722 | echo "pip: done"
723 | else
724 | dpv_pipe_quote <"$fail"
725 | echo "pip: failed"
726 | exit "$ERR_CANNOT_INSTALL_DEPENDENCIES"
727 | fi
728 | ;;
729 | esac
730 | }
731 |
732 | unsafe_dpv_internal_create_virtualenv() {
733 | #
734 | # Creates a new virtualenv
735 | #
736 | # Unsafe:
737 | # - unsafe_dpv_internal_${install_method}_install
738 | # - ERR_CANNOT_CREATE_VIRTUALENV
739 | #
740 | if [ "${ARG_TEMP:-}" = "1" ]; then
741 | case "$INTERNAL_INITIALIZE_VIRTUALENV_install_method" in
742 | UV)
743 | INTERNAL_UV_PROJECT_DIR="$(dpv_make_temp_dir)"
744 | INTERNAL_CREATE_VIRTUALENV_virtualenv_dir="$INTERNAL_UV_PROJECT_DIR/.venv"
745 | mv "$(dpv_internal_mkdir_virtualenv_temporary)" "$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir"
746 | tmpname="$(basename "$INTERNAL_UV_PROJECT_DIR")"
747 | printf "[project]\nname = \"%s\"\nversion = \"0.0.0\"\nrequires-python = \"==%s\"\n" "$tmpname" "$INTERNAL_INITIALIZE_VIRTUALENV_python_version" >"$INTERNAL_UV_PROJECT_DIR/pyproject.toml"
748 | ;;
749 | *)
750 | INTERNAL_CREATE_VIRTUALENV_virtualenv_dir=$(dpv_internal_mkdir_virtualenv_temporary)
751 | tmpname="$(basename "$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir")"
752 | ;;
753 | esac
754 | else
755 | INTERNAL_CREATE_VIRTUALENV_virtualenv_dir=$(dpv_internal_mkdir_virtualenv)
756 | tmpname="$(basename "$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir")"
757 | fi
758 |
759 | vendor_prefix="$(dpv_string_lowercase "$INTERNAL_INITIALIZE_VIRTUALENV_install_method"):"
760 |
761 | echo "$vendor_prefix creating virtualenv..."
762 | func=$(eval "echo \"unsafe_dpv_internal_\"$INTERNAL_INITIALIZE_VIRTUALENV_install_method\"_create_virtualenv\"")
763 |
764 | fail="$(dpv_make_temp_file)"
765 | ( (eval "$($func)" 2>&1) || echo >"$fail") | dpv_pipe_quote
766 | if [ ! -s "$fail" ]; then
767 | printf "path = %s\nversion = %s\ninstall_method = %s\n" "$PWD" "$INTERNAL_INITIALIZE_VIRTUALENV_python_version" "$INTERNAL_INITIALIZE_VIRTUALENV_install_method" >"$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir/dpv.cfg"
768 | dpv_internal_log "created new virtualenv: $tmpname"
769 | echo "$vendor_prefix done"
770 | else
771 | rm -rf "$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir"
772 | echo "$vendor_prefix error"
773 | exit "$ERR_CANNOT_CREATE_VIRTUALENV"
774 | fi
775 | }
776 |
777 | dpv_internal_resolve_python_version() {
778 | #
779 | # Tries to resolve a given Python version matching with:
780 | # 1. An already installed Python version
781 | # 2. An available Python version
782 | #
783 | initial_version="$1"
784 | from_installed=""
785 | from_installed_version=""
786 | from_available=""
787 | from_available_version=""
788 |
789 | # Find matching Python versions among installed and available
790 | while IFS= read -r line; do
791 | for install in $line; do
792 | installed_python_versions_func=$(eval "echo \"dpv_internal_\"$install\"_installed_python_versions\"")
793 | available_python_versions_func=$(eval "echo \"dpv_internal_\"$install\"_available_python_versions\"")
794 |
795 | eval "$installed_python_versions_func"
796 | eval "$available_python_versions_func"
797 |
798 | installed_versions_var="INTERNAL_${install}_INSTALLED_PYTHON_VERSIONS"
799 | available_versions_var="INTERNAL_${install}_AVAILABLE_PYTHON_VERSIONS"
800 |
801 | installed_versions=$(eval "echo \"\$$installed_versions_var\"")
802 | available_versions=$(eval "echo \"\$$available_versions_var\"")
803 |
804 | func=$(eval "echo \"dpv_internal_\"\$install\"_resolve_python_version\"")
805 | resolved_version=$(
806 | eval "$func" < $INTERNAL_RESOLVE_PYTHON_VERSION"
840 | fi
841 | dpv_internal_log "$INTERNAL_RESOLVE_INSTALL_METHOD: version $from_installed_version already installed"
842 |
843 | elif ! dpv_check_string_is_empty "$from_available"; then
844 | # Otherwise offer an available Python version that needs to be installed, if any matches
845 | INTERNAL_RESOLVE_INSTALL_METHOD="$from_available"
846 | INTERNAL_RESOLVE_PYTHON_VERSION="$from_available_version"
847 |
848 | dpv_internal_log "$from_available method selected"
849 | if [ "$initial_version" != "$INTERNAL_RESOLVE_PYTHON_VERSION" ]; then
850 | dpv_internal_log "$INTERNAL_RESOLVE_INSTALL_METHOD: resolved version $initial_version -> $INTERNAL_RESOLVE_PYTHON_VERSION"
851 | fi
852 | dpv_internal_log "$INTERNAL_RESOLVE_INSTALL_METHOD: version $from_available_version needs to be installed"
853 |
854 | else
855 | # If there are not matches among installed and available Python versions
856 | return 1
857 | fi
858 | }
859 |
860 | dpv_internal_scan_virtualenv() {
861 | #
862 | # Tries to find an existing virtualenv for the project
863 | # The virtualenvs directory has the following tree layout:
864 | #
865 | # + root (stored in variable: CFG_VIRTUALENVS_DIR)
866 | # \
867 | # - venv-dir-1
868 | # - venv-dir-2
869 | # + venv-dir-3
870 | # \
871 | # + dpv.cfg
872 | # \
873 | # |------ file format ------|
874 | # |-------------------------|
875 | # | path = /path/to/project |
876 | # | version = 3.9.2 | --> this is the Python version
877 | # | install_method = PYENV |
878 | # |-------------------------|
879 | #
880 | found_virtualenv_dir="ø"
881 | found_version=""
882 | found_install_method=""
883 |
884 | # try to locate existing virtualenv
885 | virtualenvs_output="$(dpv_internal_find_virtualenvs)"
886 | if dpv_check_string_is_empty "$virtualenvs_output"; then
887 | return 1
888 | fi
889 |
890 | while IFS= read -r virtualenv_dir; do
891 | dpv_internal_parse_virtualenv_config_file "$virtualenv_dir/dpv.cfg"
892 |
893 | match_python_version=0
894 | if dpv_check_is_set "$INTERNAL_ARG_PYTHON_VERSION"; then
895 | case "$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_version" in
896 | "$INTERNAL_ARG_PYTHON_VERSION"*)
897 | match_python_version=1
898 | ;;
899 | esac
900 | else
901 | match_python_version=1
902 | fi
903 |
904 | if [ "$match_python_version" -eq 1 ]; then
905 | if ! dpv_check_is_set "$found_virtualenv_dir"; then
906 | dpv_internal_log "found virtualenv: $(basename "$virtualenv_dir")"
907 | found_version=$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_version
908 | found_install_method=$INTERNAL_PARSE_VIRTUALENV_CONFIG_FILE_install_method
909 | found_virtualenv_dir=$virtualenv_dir
910 | else
911 | dpv_internal_log "found virtualenv: $(basename "$virtualenv_dir") (skipped)"
912 | fi
913 | fi
914 | done <&1 || echo >"$fail") | dpv_pipe_quote
1441 | if [ ! -s "$fail" ]; then
1442 | echo "done"
1443 | else
1444 | echo "failed"
1445 | dpv_internal_log "uv: failed to install version $python_version"
1446 | exit "$ERR_INSTALLATION_FAILED"
1447 | fi
1448 | done
1449 | }
1450 |
1451 | #
1452 | # Pyenv utils
1453 | #
1454 | dpv_PYENV_is_available() {
1455 | #
1456 | # Check if pyenv is installed
1457 | #
1458 | dpv_command_exists "$CFG_PYENV_EXECUTABLE"
1459 | }
1460 |
1461 | dpv_PYENV_exec() {
1462 | #
1463 | # Wrapper around the pyenv executable
1464 | #
1465 | $CFG_PYENV_EXECUTABLE "$@"
1466 | }
1467 |
1468 | unsafe_dpv_internal_PYENV_create_virtualenv() {
1469 | virtualenv_dir="$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir"
1470 | python_executable="$(unsafe_dpv_internal_PYENV_get_python_executable)"
1471 |
1472 | "$python_executable" -m venv "$virtualenv_dir"
1473 | }
1474 |
1475 | unsafe_dpv_internal_PYENV_install() {
1476 | #
1477 | # Install Python version using pyenv
1478 | #
1479 | # Unsafe:
1480 | # - ERR_INSTALLATION_FAILED
1481 | #
1482 | while IFS= read -r python_version; do
1483 | case "$INTERNAL_PYENV_INSTALLED_PYTHON_VERSIONS" in
1484 | *"$python_version"*)
1485 | continue
1486 | ;;
1487 | esac
1488 |
1489 | echo "installing python $python_version using pyenv"
1490 | fail="$(dpv_make_temp_file)"
1491 | (dpv_PYENV_exec install "$python_version" 2>&1 || echo >"$fail") | dpv_pipe_quote
1492 | if [ ! -s "$fail" ]; then
1493 | echo "done"
1494 | else
1495 | echo "failed"
1496 | dpv_internal_log "pyenv: failed to install version $python_version"
1497 | exit "$ERR_INSTALLATION_FAILED"
1498 | fi
1499 | done
1500 | }
1501 |
1502 | unsafe_dpv_internal_PYENV_get_python_executable() {
1503 | #
1504 | # Gets the Python executable for the current virtualenv
1505 | #
1506 | # Unsafe:
1507 | # - ERR_VIRTUALENV_CANNOT_FIND_PYTHON_EXECUTABLE
1508 | #
1509 | if prefix="$(dpv_PYENV_exec prefix "$INTERNAL_INITIALIZE_VIRTUALENV_python_version")"; then
1510 | if [ -d "$prefix" ]; then
1511 | cmd="$prefix/bin/python"
1512 | if [ -x "$cmd" ]; then
1513 | echo "$cmd"
1514 | return
1515 | fi
1516 | fi
1517 | fi
1518 | exit "$ERR_VIRTUALENV_CANNOT_FIND_PYTHON_EXECUTABLE"
1519 | }
1520 |
1521 | dpv_internal_PYENV_installed_python_versions() {
1522 | #
1523 | # Lists the installed Python versions in pyenv
1524 | #
1525 | dpv_check_is_set "$INTERNAL_PYENV_INSTALLED_PYTHON_VERSIONS" && return
1526 |
1527 | INTERNAL_PYENV_INSTALLED_PYTHON_VERSIONS="$(dpv_internal_run_command_log_failure "$CFG_PYENV_EXECUTABLE versions --bare --skip-aliases" | while IFS= read -r line; do
1528 | cut -d/ -f1 <&1 || echo >"$fail") | dpv_pipe_quote
1636 | if [ ! -s "$fail" ]; then
1637 | echo "done"
1638 | else
1639 | echo "failed"
1640 | dpv_internal_log "homebrew: failed to install version $python_version"
1641 | exit "$ERR_INSTALLATION_FAILED"
1642 | fi
1643 | done
1644 | }
1645 |
1646 | dpv_HOMEBREW_exec() {
1647 | #
1648 | # Wrapper around the homebrew executable
1649 | #
1650 | HOMEBREW_NO_AUTO_UPDATE=1 "$CFG_HOMEBREW_EXECUTABLE" "$@"
1651 | }
1652 |
1653 | unsafe_dpv_internal_HOMEBREW_create_virtualenv() {
1654 | virtualenv_dir="$INTERNAL_CREATE_VIRTUALENV_virtualenv_dir"
1655 | python_executable="$(unsafe_dpv_internal_HOMEBREW_get_python_executable)"
1656 |
1657 | "$python_executable" -m venv "$virtualenv_dir"
1658 | }
1659 |
1660 | unsafe_dpv_internal_HOMEBREW_get_python_executable() {
1661 | #
1662 | # Gets path to Python executable installed with homebrew
1663 | #
1664 | cmd="$(dpv_HOMEBREW_exec --prefix "$(dpv_HOMEBREW_format_python_formula "$INTERNAL_INITIALIZE_VIRTUALENV_python_version")" 2>/dev/null)"/libexec/bin/python
1665 | if [ -x "$cmd" ]; then
1666 | echo "$cmd"
1667 | else
1668 | exit "$ERR_VIRTUALENV_CANNOT_FIND_PYTHON_EXECUTABLE"
1669 | fi
1670 | }
1671 |
1672 | dpv_internal_HOMEBREW_installed_python_versions() {
1673 | #
1674 | # Gets list of Python versions available with homebrew
1675 | #
1676 | dpv_check_is_set "$INTERNAL_HOMEBREW_INSTALLED_PYTHON_VERSIONS" && return
1677 |
1678 | dpv_internal_HOMEBREW_available_python_versions
1679 |
1680 | if result=$(dpv_internal_run_command_log_failure "$CFG_HOMEBREW_EXECUTABLE list" | grep 'python@' | cut -d@ -f2 | dpv_HOMEBREW_pipe_expand_python_version | dpv_pipe_sort_version); then
1681 | INTERNAL_HOMEBREW_INSTALLED_PYTHON_VERSIONS=$result
1682 | else
1683 | INTERNAL_HOMEBREW_INSTALLED_PYTHON_VERSIONS=""
1684 | return 1
1685 | fi
1686 | }
1687 |
1688 | dpv_internal_HOMEBREW_available_python_versions() {
1689 | #
1690 | # Gets list of available Python versions in homebrew
1691 | #
1692 | dpv_check_is_set "$INTERNAL_HOMEBREW_AVAILABLE_PYTHON_VERSIONS" && return
1693 |
1694 | if result=$(dpv_internal_run_command_log_failure "$CFG_HOMEBREW_EXECUTABLE search python"); then
1695 | result=$(echo "$result" | grep -o 'python@[^ ]\+' | cut -d@ -f2 | dpv_HOMEBREW_pipe_expand_python_version | dpv_pipe_sort_version)
1696 | INTERNAL_HOMEBREW_AVAILABLE_PYTHON_VERSIONS=$result
1697 | else
1698 | INTERNAL_HOMEBREW_AVAILABLE_PYTHON_VERSIONS=""
1699 | return 1
1700 | fi
1701 | }
1702 |
1703 | dpv_HOMEBREW_format_python_formula() {
1704 | #
1705 | # Gets the Python homebrew formula for the given version
1706 | #
1707 | version="$1"
1708 | echo "python@$(
1709 | cut -d. -f1,2 < 3.9.12
1757 | #
1758 | while read -r line; do
1759 | major_version="$(
1760 | cut -d. -f1,2 </dev/null)" || return 1
1765 | grep -o "$major_version\.[0-9]\+[a-z]*" "$formula_path" | while IFS= read -r version; do
1766 | echo "$version"
1767 | break
1768 | done
1769 | done
1770 | }
1771 |
1772 | #
1773 | # Internals
1774 | #
1775 | dpv_internal_mkdir_virtualenv() {
1776 | #
1777 | # Create a new virtualenv directory
1778 | #
1779 | dirname="$CFG_VIRTUALENVS_DIR/$INTERNAL_INITIALIZE_VIRTUALENV_python_version/$(basename "$PWD")-$INTERNAL_INITIALIZE_VIRTUALENV_python_version"
1780 | mkdir -p "$dirname"
1781 | printf "%s" "$dirname"
1782 | }
1783 |
1784 | dpv_internal_mkdir_virtualenv_temporary() {
1785 | #
1786 | # Create a new *temporary* virtualenv directory
1787 | #
1788 | dpv_make_temp_dir "$(basename "$PWD")-$INTERNAL_INITIALIZE_VIRTUALENV_python_version"
1789 | }
1790 |
1791 | dpv_internal_set_log_file() {
1792 | #
1793 | # Sets up the log file
1794 | #
1795 | dpv_check_is_set "$INTERNAL_LOG_FILE" && return
1796 |
1797 | INTERNAL_LOG_FILE=$(dpv_make_temp_file "dpv_log")
1798 | }
1799 |
1800 | unsafe_dpv_internal_set_available_install_methods() {
1801 | #
1802 | # Sets up the install methods that are available
1803 | #
1804 | # Unsafe:
1805 | # - ERR_NO_AVAILABLE_INSTALL_METHODS
1806 | #
1807 | dpv_check_is_set "$INTERNAL_AVAILABLE_INSTALL_METHODS" && return
1808 |
1809 | INTERNAL_AVAILABLE_INSTALL_METHODS="$(for install in $(dpv_string_uppercase "$CFG_PREFERRED_INSTALL_METHODS"); do
1810 | func=$(eval "echo \"dpv_${install}_is_available\"")
1811 | if eval "$func"; then
1812 | echo "$install"
1813 | fi
1814 | done)"
1815 |
1816 | if dpv_check_string_is_empty "$INTERNAL_AVAILABLE_INSTALL_METHODS"; then
1817 | exit "$ERR_NO_AVAILABLE_INSTALL_METHODS"
1818 | fi
1819 | }
1820 |
1821 | dpv_internal_run_command_log_failure() {
1822 | #
1823 | # Executes a command and logs the output
1824 | # In case of failure, displays the output
1825 | #
1826 | tmpfile=$(dpv_make_temp_file)
1827 | if "$(dpv_current_shell)" -c "$@" >"$tmpfile"; then
1828 | cat "$tmpfile"
1829 | return 0
1830 | else
1831 | dpv_internal_log "failed to run command: $*"
1832 | printf "%s\n" "$(dpv_pipe_quote <"$tmpfile")" >>"$INTERNAL_LOG_FILE"
1833 | return 1
1834 | fi
1835 | }
1836 |
1837 | dpv_internal_log() {
1838 | #
1839 | # Prints a log mesage to the log file
1840 | #
1841 | printf "%s\n" "$*" >>"$INTERNAL_LOG_FILE"
1842 | }
1843 |
1844 | dpv_internal_pipe_format_python_versions() {
1845 | # Input is a pipe with multiple lines *already sorted*, every line is a version
1846 | # Outputs multiple lines, every line is a version
1847 | #
1848 | # For each version in input, return the best candidate:
1849 | # 1. The exact same version is installed: 3.9.1* (star means it is installed)
1850 | # 2. Another minor version is installed: 3.9.2*
1851 | # 3. Another major version is installed: 3.9*
1852 | # 4. Otherwise return the version without the star: 3.8
1853 | #
1854 | # Versions should be de-duplicated by its major version
1855 | # For these versions: 3.9.1 3.9.2 3.8 3.7.1
1856 | # Only one for each major is returned: 3.9.1* 3.8 3.7.1
1857 | # Except, when the --all parameter is passed!
1858 | # In that case, all versions are returned: 3.9.1* 3.9.2 3.8 3.7.1
1859 |
1860 | _install_method="$1"
1861 | _options="${2:-}"
1862 |
1863 | installed_versions_var="INTERNAL_${_install_method}_INSTALLED_PYTHON_VERSIONS"
1864 | installed_versions=$(eval "echo \"\$$installed_versions_var\"")
1865 |
1866 | case " $_options " in
1867 | *" --all "*)
1868 |
1869 | while IFS= read -r line; do
1870 | # If it is installed, add a star to it
1871 | for version in $installed_versions; do
1872 | if [ "$version" = "$line" ]; then
1873 | echo "$version*"
1874 | continue 2
1875 | fi
1876 | done
1877 |
1878 | # Otherwise, no star
1879 | echo "$line"
1880 | done
1881 | ;;
1882 | *)
1883 | skip=""
1884 | while IFS= read -r version; do
1885 | major_version="$(
1886 | dpv_pipe_format_major_version <"$output" || (
1953 | # https://github.com/actions/runner-images/issues/3462
1954 | echo "/bin/sh"
1955 | exit 0
1956 | )
1957 |
1958 | while IFS= read -r line; do
1959 | # shellcheck disable=SC2086
1960 | set -- $(printf "%s" "$line")
1961 | [ "$1" = "$$" ] || continue
1962 | shift
1963 | for arg in "$@"; do
1964 | case "$arg" in
1965 | "{"*)
1966 | # skip curly braces used by busybox
1967 | # https://www.theunterminatedstring.com/busybox-ps-brackets-and-braces/
1968 | continue
1969 | ;;
1970 | *)
1971 | echo "$arg"
1972 | break 2
1973 | ;;
1974 | esac
1975 | done
1976 | done <"$output"
1977 | }
1978 |
1979 | dpv_command_exists() {
1980 | command -v "$1" 1>/dev/null
1981 | }
1982 |
1983 | dpv_make_temp_file() {
1984 | #
1985 | # Creates a temporary file
1986 | #
1987 | suffix="${1-}"
1988 | if dpv_check_string_is_empty "$suffix"; then
1989 | template="${TMPDIR:-/tmp/}dpv.XXXXXX"
1990 | else
1991 | template="${TMPDIR:-/tmp/}$suffix.XXXXXX"
1992 | fi
1993 | mktemp "$template"
1994 | }
1995 |
1996 | dpv_make_temp_dir() {
1997 | #
1998 | # Creates a temporary directory
1999 | #
2000 | suffix="${1-}"
2001 | if dpv_check_string_is_empty "$suffix"; then
2002 | template="${TMPDIR:-/tmp/}dpv.XXXXXX"
2003 | else
2004 | template="${TMPDIR:-/tmp/}$suffix.XXXXXX"
2005 | fi
2006 | mktemp -d "$template"
2007 | }
2008 |
2009 | dpv_pipe_sort_path() {
2010 | #
2011 | # Sort paths by the given index position
2012 | #
2013 | index="$1"
2014 |
2015 | # -f = ignore case
2016 | # -t = field separator
2017 | # -k = field position
2018 | sort -f -t/ -k "$index"
2019 | }
2020 |
2021 | dpv_pipe_sort_version() {
2022 | #
2023 | # Sort versions
2024 | #
2025 | # -t = field separator
2026 | # -k = field position
2027 | #
2028 | # n modifier: consider numeric
2029 | # r modifier: reverse order
2030 | sort -t. -k1,1nr -k2,2nr -k3,3nr
2031 | }
2032 |
2033 | dpv_pipe_format_nl_to_space() {
2034 | #
2035 | # Replaces newlines with spaces
2036 | #
2037 | tr '\n' ' '
2038 | }
2039 |
2040 | dpv_pipe_format_major_version() {
2041 | #
2042 | # Returns the major version for the given version
2043 | # Example: 3.9.12 -> 3.9
2044 | #
2045 | cut -d. -f1,2
2046 | }
2047 |
2048 | dpv_pipe_format_theme() {
2049 | #
2050 | # Applies theme to the output
2051 | #
2052 | "$CFG_THEME"
2053 | }
2054 |
2055 | dpv_pipe_quote() {
2056 | #
2057 | # Quote output
2058 | #
2059 | prefix="${1:->}"
2060 | sed "s/^/ $prefix /"
2061 | }
2062 |
2063 | dpv_check_file_is_empty() {
2064 | #
2065 | # Check if file is empty
2066 | #
2067 | # -s :: Size is > 0 bytes
2068 | ! [ -s "$1" ]
2069 | }
2070 |
2071 | dpv_check_string_is_empty() {
2072 | #
2073 | # Check if string is empty
2074 | #
2075 | # -z :: String is empty
2076 | [ -z "${1:-}" ]
2077 | }
2078 |
2079 | dpv_check_is_set() {
2080 | #
2081 | # Check if variable is set
2082 | #
2083 | # "ø" :: Special value for empty/unset
2084 | [ "${1:-}" != "ø" ]
2085 | }
2086 |
2087 | dpv_string_lstrip() {
2088 | #
2089 | # Removes spaces from the left
2090 | #
2091 | printf "%s" "$(sed 's/^[ \t]*//' < "fxx"
2125 | #
2126 | string="${1:-}"
2127 | from="${2:-}"
2128 | to="${3:-}"
2129 |
2130 | sed "s/$from/$to/" < 2
2139 | #
2140 | string="${1:-}"
2141 | char="${2:-}"
2142 | tr -cd "$char" </dev/null 2>&1 && pwd)"
8 |
9 | mkdir -p "$BATS_TEST_TMPDIR/myproject"
10 | cd "$BATS_TEST_TMPDIR/myproject" || exit
11 |
12 | #
13 | # mocks
14 | #
15 | MOCK_LOG_FILE="$(mktemp "${TMPDIR:-/tmp/}dpv_test_logs.XXXXX")"
16 |
17 | mock_log_file() {
18 | export DPV_MOCK_LOG_FILE="$MOCK_LOG_FILE"
19 | }
20 |
21 | mock_virtualenv_python_version() {
22 | INTERNAL_INITIALIZE_VIRTUALENV_python_version="$1"
23 |
24 | # for command tests:
25 | export DPV_MOCK_VIRTUALENV_PYTHON_VERSION="$1"
26 | }
27 |
28 | mock_available_install_methods() {
29 | export DPV_MOCK_AVAILABLE_INSTALL_METHODS="$@"
30 | INTERNAL_AVAILABLE_INSTALL_METHODS="$@"
31 | }
32 |
33 | mock_internal_installed_python_versions() {
34 | local install="$1"
35 | shift
36 | local versions=$(echo $@ | tr " " "\n")
37 | local mock_var="export DPV_MOCK_${install}_INSTALLED_PYTHON_VERSIONS"
38 | eval "$mock_var='$versions'"
39 | }
40 |
41 | mock_internal_available_python_versions() {
42 | local install="$1"
43 | shift
44 | local versions=$(echo $@ | tr " " "\n")
45 | local mock_var="export DPV_MOCK_${install}_AVAILABLE_PYTHON_VERSIONS"
46 | eval "$mock_var='$versions'"
47 | }
48 |
49 | mock_virtualenvs_dir() {
50 | export DPV_MOCK_VIRTUALENVS_DIR="$DPV_DIR/virtualenvs"
51 | CFG_VIRTUALENVS_DIR="$DPV_DIR/virtualenvs"
52 | }
53 |
54 | mock_virtualenv_install_method() {
55 | export DPV_MOCK_VIRTUALENV_INSTALL_METHOD="$1"
56 | }
57 |
58 | mock_virtualenv() {
59 | mock_virtualenvs_dir
60 |
61 | local install_method
62 | local python_version
63 | local project_path
64 | local activate=0
65 | local opt_echo=0
66 |
67 | while [[ "$#" -gt 0 ]]; do
68 | case "$1" in
69 | --install-method | --python-version | --project-path)
70 | declare "$(echo "${1:2}" | tr '-' '_')"="$2"
71 | shift
72 | shift
73 | ;;
74 | --activate)
75 | activate=1
76 | shift
77 | ;;
78 | *)
79 | echo "invalid argument: $1"
80 | exit
81 | ;;
82 | esac
83 | done
84 |
85 | local venv_name="$(basename "$project_path")"
86 |
87 | local venv_path="$DPV_MOCK_VIRTUALENVS_DIR/$python_version/$venv_name"
88 | mkdir -p "$venv_path"
89 | printf "path = $project_path\nversion = $python_version\ninstall_method = $install_method\n" >"$venv_path/dpv.cfg"
90 |
91 | if [[ "$activate" -eq "1" ]]; then
92 | export DPV_MOCK_VIRTUALENV_DIR="$venv_path"
93 | fi
94 |
95 | VENV_DIR="$venv_path"
96 | }
97 |
98 | #
99 | # custom asserts
100 | #
101 |
102 | assert_log_output() {
103 | run cat $MOCK_LOG_FILE
104 | assert_output "$@"
105 | }
106 |
107 | refute_log_output() {
108 | run cat $MOCK_LOG_FILE
109 | refute_output "$@"
110 | }
111 |
112 | run_oneline() {
113 | "$TEST_SHELL" "$DIR/../src/dpv" internal-function "$@"
114 | }
115 |
116 | run_script() {
117 | "$TEST_SHELL" "$DIR/../src/dpv" internal-function
118 | }
119 |
--------------------------------------------------------------------------------
/test/run_dpv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | DPV_DIR=/tmp/test_dpv ./src/dpv "$@"
4 |
--------------------------------------------------------------------------------
/test/test_commands.sh:
--------------------------------------------------------------------------------
1 | setup() {
2 | load 'test_helper/bats-support/load'
3 | load 'test_helper/bats-assert/load'
4 |
5 | DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" >/dev/null 2>&1 && pwd)"
6 |
7 | # test config
8 | TEST_CONFIG_MAJOR_PYTHON_VERSION=3.9
9 | TEST_CONFIG_MINOR_PYTHON_VERSION=3.9.10
10 |
11 | # shellcheck source=./helper.sh
12 | . "$DIR/helper.sh"
13 |
14 | unset DPV_THEME
15 | export EXEC="$TEST_SHELL $DIR/../src/dpv"
16 |
17 | mock_log_file
18 | }
19 |
20 | #test_coverage() {
21 | # # the poor's man simple code coverage
22 | # test_fn() {
23 | # local testcases="$(grep "^test_cmd_.*@test" "$BATS_TEST_FILENAME")"
24 | #
25 | # local missing="$(while IFS= read -r line; do
26 | # if [[ "$testcases" != *"test_cmd_$line"* ]]; then
27 | # echo $line
28 | # fi
29 | # done < <(grep -o "^cmd_[^(]\+" "$(which dpv)" | sed 's/cmd_//' | sort | uniq))"
30 | #
31 | # if [[ "$missing" != "" ]]; then
32 | # echo "commands not covered by tests: $missing"
33 | # exit 1
34 | # fi
35 | # }
36 | # run test_fn
37 | # assert_success
38 | #
39 | #}
40 |
41 | #test_dpv_cmd_help() {
42 | # test_fn() {
43 | # local help_output="$($EXEC help)"
44 | #
45 | # while IFS= read -r line; do
46 | # if [[ "$help_output" != *" dpv $line "* ]]; then
47 | # echo "command not present in help output: $line"
48 | # echo "$help_output"
49 | # exit 1
50 | # fi
51 | # done < <(grep -o "^cmd_[^(]\+" "$(which dpv)" | sed 's/cmd_//' | sort | uniq)
52 | # }
53 | # run test_fn
54 | # assert_output ""
55 | # assert_success
56 | #}
57 |
58 | test_dpv_internal_cmd_versions() { # @test
59 | test_fn() {
60 | mock_available_install_methods "$(printf "%s\n%s" "PYENV" "HOMEBREW")"
61 |
62 | mock_internal_available_python_versions "PYENV" "3.9.2 3.9.1 3.8"
63 | mock_internal_installed_python_versions "PYENV" "3.9.1"
64 | mock_internal_available_python_versions "HOMEBREW" "3.11.2 3.11.1 3.10"
65 | mock_internal_installed_python_versions "HOMEBREW" "3.11.2"
66 |
67 | $EXEC versions
68 | }
69 |
70 | run test_fn
71 |
72 | assert_success
73 | assert_output --partial "pyenv: 3.9.1* 3.8"
74 | assert_output --partial "homebrew: 3.11.2* 3.10"
75 | }
76 |
77 | test_dpv_internal_cmd_versions_all() { # @test
78 | test_fn() {
79 | mock_available_install_methods "$(printf "%s\n%s" "PYENV" "HOMEBREW")"
80 |
81 | mock_internal_available_python_versions "PYENV" "3.9.2 3.9.1 3.8"
82 | mock_internal_installed_python_versions "PYENV" "3.9.1"
83 | mock_internal_available_python_versions "HOMEBREW" "3.11.2 3.11.1 3.10"
84 | mock_internal_installed_python_versions "HOMEBREW" "3.11.2"
85 |
86 | $EXEC versions --all
87 | }
88 |
89 | run test_fn
90 |
91 | assert_output --partial "pyenv: 3.9.2 3.9.1* 3.8"
92 | assert_output --partial "homebrew: 3.11.2* 3.11.1 3.10"
93 | }
94 |
95 | test_dpv_internal_cmd_versions_installed() { # @test
96 | test_fn() {
97 | mock_available_install_methods "$(printf "%s\n%s" "PYENV" "HOMEBREW")"
98 |
99 | mock_internal_available_python_versions "PYENV" "3.9.2 3.9.1 3.8"
100 | mock_internal_installed_python_versions "PYENV" "3.9.1"
101 | mock_internal_available_python_versions "HOMEBREW" "3.11.2 3.11.1 3.10"
102 | mock_internal_installed_python_versions "HOMEBREW" "3.11.2"
103 |
104 | $EXEC versions --installed
105 | }
106 |
107 | run test_fn
108 |
109 | assert_output --partial "pyenv: 3.9.1*"
110 | assert_output --partial "homebrew: 3.11.2*"
111 | }
112 |
113 | test_cmd_drop_current_virtualenv() { # @test
114 | test_fn() {
115 | mock_virtualenv --install-method "pyenv" --python-version "3.9.2" --project-path "$(pwd)" --activate
116 |
117 | $EXEC drop
118 | }
119 |
120 | run test_fn
121 |
122 | [ ! -d "$DPV_MOCK_VIRTUALENV_DIR" ]
123 | }
124 |
125 | test_cmd_drop_another_virtualenv() { # @test
126 | mock_virtualenv --install-method "pyenv" --python-version "3.9.2" --project-path "$(pwd)/abc"
127 | VENV_A="$VENV_DIR"
128 | mock_virtualenv --install-method "pyenv" --python-version "3.9.2" --project-path "$(pwd)/def"
129 | VENV_B="$VENV_DIR"
130 |
131 | run $EXEC drop def # delete VENV_B
132 |
133 | assert_success
134 |
135 | [ -d "$VENV_A" ]
136 | [ ! -d "$VENV_B" ]
137 | }
138 |
139 | test_dpv_cmd_list() { # @test
140 | test_fn() {
141 | mock_virtualenv --install-method "pyenv" --python-version "3.9.1" --project-path "$(pwd)/def"
142 | mock_virtualenv --install-method "pyenv" --python-version "3.9.2" --project-path "$(pwd)/abc"
143 |
144 | $EXEC list
145 | }
146 |
147 | run test_fn
148 | assert_success
149 | assert_line --index 1 --partial "virtualenvs/3.9.2/abc"
150 | assert_line --index 2 --partial "virtualenvs/3.9.1/def"
151 | }
152 |
153 | test_dpv_internal_cmd_info_not_activated() { # @test
154 | test_fn() {
155 | mock_virtualenv --install-method "pyenv" --python-version "3.9.9" --project-path "$(pwd)"
156 |
157 | $EXEC info
158 | }
159 |
160 | run test_fn
161 |
162 | assert_success
163 | assert_output --partial "status: not activated"
164 |
165 | # should show config
166 | assert_output --partial "config:"
167 | # should show virtualenv config
168 | assert_output --partial "virtualenv:"
169 |
170 | }
171 |
172 | test_dpv_internal_cmd_info_activated() { # @test
173 | test_fn() {
174 | mock_virtualenv --install-method "pyenv" --python-version "3.9.2" --project-path "$(pwd)" --activate
175 |
176 | $EXEC info
177 | }
178 |
179 | run test_fn
180 |
181 | assert_success
182 | assert_output --partial "status: activated"
183 |
184 | # should show config
185 | assert_output --partial "config:"
186 | # should show virtualenv config
187 | assert_output --partial "virtualenv:"
188 | }
189 |
--------------------------------------------------------------------------------
/test/test_functions.sh:
--------------------------------------------------------------------------------
1 | setup() {
2 | load 'test_helper/bats-support/load'
3 | load 'test_helper/bats-assert/load'
4 |
5 | DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" >/dev/null 2>&1 && pwd)"
6 | export DIR
7 |
8 | # test config
9 | TEST_CONFIG_MAJOR_PYTHON_VERSION=3.9
10 | TEST_CONFIG_MINOR_PYTHON_VERSION=3.9.13
11 |
12 | # shellcheck source=./helper.sh
13 | . "$DIR/helper.sh"
14 |
15 | export ENV=""
16 |
17 | sed 's/#DPV-INTERNAL-EVAL#/if dpv_check_string_is_empty "$CMD_ARGS"; then while read -r line; do eval "$line"; done; else eval "$CMD_ARGS"; fi/' "$DIR/../src/dpv" >$BATS_TEST_TMPDIR/dpv-test
18 | chmod +x $BATS_TEST_TMPDIR/dpv-test
19 | export PATH="$PATH:$BATS_TEST_TMPDIR"
20 | function dpv-eval() {
21 | $TEST_SHELL $BATS_TEST_TMPDIR/dpv-test internal-eval "$@"
22 | }
23 | }
24 |
25 | #
26 | # utility tests
27 | #
28 | test_dpv_check_is_set_true() { # @test
29 | run dpv-eval dpv_check_is_set ""
30 | assert_success
31 | assert_output ""
32 | }
33 |
34 | test_dpv_check_is_set_false() { # @test
35 | run dpv-eval dpv_check_is_set "ø"
36 | assert_failure
37 | }
38 |
39 | test_dpv_check_string_is_empty_true() { # @test
40 | run dpv-eval dpv_check_string_is_empty ""
41 | assert_success
42 | }
43 |
44 | test_dpv_check_string_is_empty_false() { # @test
45 | run dpv-eval dpv_check_string_is_empty "ø"
46 | assert_failure
47 | }
48 |
49 | test_dpv_check_file_is_empty_true() { # @test
50 | test_fn() {
51 | local tmp_file="$(mktemp)"
52 | touch $tmp_file
53 | dpv-eval dpv_check_file_is_empty "$tmp_file"
54 | }
55 | run test_fn
56 | assert_success
57 | }
58 |
59 | test_dpv_check_file_is_empty_false() { # @test
60 | test_fn() {
61 | local tmp_file="$(mktemp)"
62 | touch $tmp_file
63 | echo "x" >>$tmp_file
64 | dpv-eval dpv_check_file_is_empty "$tmp_file"
65 | }
66 | run test_fn
67 | assert_failure
68 | }
69 |
70 | test_dpv_pipe_format_major_version() { # @test
71 | test_fn() {
72 | echo "3.9.2" | dpv-eval dpv_pipe_format_major_version
73 | echo "3.10" | dpv-eval dpv_pipe_format_major_version
74 | echo "3.11-dev" | dpv-eval dpv_pipe_format_major_version
75 | echo "3.8" | dpv-eval dpv_pipe_format_major_version
76 | }
77 | run test_fn
78 | assert_line --index 0 "3.9"
79 | assert_line --index 1 "3.10"
80 | assert_line --index 2 "3.11-dev"
81 | assert_line --index 3 "3.8"
82 | }
83 |
84 | test_dpv_pipe_sort_version() { # @test
85 | test_fn() {
86 | printf "3.9.2\n3.8.4\n3.11-dev" | dpv-eval dpv_pipe_sort_version
87 | }
88 | run test_fn
89 | assert_line --index 0 "3.11-dev"
90 | assert_line --index 1 "3.9.2"
91 | assert_line --index 2 "3.8.4"
92 | }
93 |
94 | test_dpv_pipe_format_nl_to_space() { # @test
95 | test_fn() {
96 | printf "3.9.2\n3.8.4\n3.11-dev" | dpv-eval dpv_pipe_format_nl_to_space
97 | }
98 | run test_fn
99 | assert_output "3.9.2 3.8.4 3.11-dev"
100 | }
101 |
102 | test_dpv_string_lstrip() { # @test
103 | test_fn() {
104 | printf "foobar" | dpv-eval dpv_string_lstrip 2
105 | }
106 | run test_fn
107 | assert_output "obar"
108 | }
109 |
110 | test_dpv_string_uppercase() { # @test
111 | run dpv-eval dpv_string_uppercase "foo"
112 | assert_output "FOO"
113 | }
114 |
115 | test_dpv_string_lowercase() { # @test
116 | run dpv-eval dpv_string_lowercase "FOO"
117 | assert_output "foo"
118 | }
119 |
120 | test_dpv_string_count_characters() { # @test
121 | run dpv-eval dpv_string_count_characters "foofoo" "o"
122 | assert_output "4"
123 | }
124 |
125 | test_dpv_string_regex_replace() { # @test
126 | run dpv-eval dpv_string_regex_replace "foobarfoo" "^foo" ""
127 | assert_output "barfoo"
128 | }
129 |
130 | #
131 | # internal utility tests
132 | #
133 |
134 | test_dpv_internal_mkdir_virtualenv_temporary() { # @test
135 | test_fn() {
136 | mock_virtualenv_python_version "99.9"
137 | dpv-eval dpv_internal_mkdir_virtualenv_temporary
138 | }
139 | run test_fn
140 | assert_success
141 | assert_output --partial "99.9"
142 | }
143 |
144 | test_dpv_internal_mkdir_virtualenv() { # @test
145 | test_fn() {
146 | mock_virtualenv_python_version "99.9"
147 | dpv-eval dpv_internal_mkdir_virtualenv
148 | }
149 | run test_fn
150 | assert_success
151 | assert_output --partial "99.9"
152 | }
153 |
154 | test_dpv_internal_pipe_format_python_versions() { # @test
155 | test_fn() {
156 | mock_available_install_methods "PYENV"
157 | mock_internal_installed_python_versions "PYENV" "3.9.1"
158 |
159 | printf "3.9.2\n3.9.1\n3.8\n2.7\n" | dpv-eval dpv_internal_pipe_format_python_versions "PYENV"
160 | }
161 | run test_fn
162 | assert_line --index 0 "3.9.1*"
163 | assert_line --index 1 "3.8"
164 | }
165 |
166 | test_dpv_internal_pipe_format_python_versions_all() { # @test
167 | test_fn() {
168 | mock_available_install_methods "PYENV"
169 | mock_internal_installed_python_versions "PYENV" "3.9.1"
170 |
171 | dpv-eval <<'EOF'
172 | printf "3.9.2\n3.9.1\n3.8\n2.7\n" | dpv_internal_pipe_format_python_versions "PYENV" --all
173 | EOF
174 |
175 | }
176 | run test_fn
177 | assert_line --index 0 "3.9.2"
178 | assert_line --index 1 "3.9.1*"
179 | assert_line --index 2 "3.8"
180 | }
181 |
182 | test_dpv_internal_parse_virtualenv_config_file() { # @test
183 | test_fn() {
184 | local project_path="$(pwd)/myproject"
185 |
186 | mock_virtualenv --install-method "PYENV" --python-version "3.9.9" --project-path "$project_path"
187 | dpv-eval < setuptools y==1 x[foo]<2"
604 | assert_line --index 2 "uv: done"
605 | }
606 |
607 | test_unsafe_dpv_internal_install_deps_success() { # @test
608 | test_fn() {
609 | export DPV_MOCK_ARG_DEPS="y==1 x[foo]<2"
610 | export DPV_MOCK_PIP="echo"
611 | dpv-eval unsafe_dpv_internal_install_deps
612 | }
613 |
614 | run test_fn
615 |
616 | assert_success
617 | assert_line --index 0 "pip: installing dependencies"
618 | assert_line --index 1 " > install setuptools y==1 x[foo]<2"
619 | assert_line --index 2 "pip: done"
620 | }
621 |
622 | test_unsafe_dpv_internal_install_deps_error() { # @test
623 | test_fn() {
624 | export DPV_MOCK_ARG_DEPS="y==1 x[foo]<2"
625 | export DPV_MOCK_PIP="false"
626 | dpv-eval unsafe_dpv_internal_install_deps
627 | }
628 |
629 | run test_fn
630 |
631 | assert_failure
632 | assert_line --index 0 "pip: installing dependencies"
633 | assert_line --index 2 "pip: failed"
634 | }
635 |
636 | test_dpv_internal_run_command_log_failure_fail() { # @test
637 | test_fn() {
638 | mock_log_file
639 | dpv-eval dpv_internal_run_command_log_failure "echo 'something failed' ; exit 1"
640 | }
641 | run test_fn
642 | assert_failure
643 | assert_log_output --partial "something failed"
644 | }
645 |
646 | test_dpv_internal_run_command_log_failure_success() { # @test
647 | run dpv-eval dpv_internal_run_command_log_failure "echo 'it works'"
648 | assert_success
649 | refute_log_output --partial "it works"
650 | }
651 |
652 | test_dpv_internal_print_logs_no_logs() { # @test
653 | run dpv-eval dpv_internal_print_logs
654 | assert_output ""
655 | }
656 |
657 | test_dpv_internal_print_logs_with_logs() { # @test
658 | mock_log_file
659 | run dpv-eval <>runtime.txt
698 | dpv_internal_scan_python_version
699 | echo \$INTERNAL_SCAN_PYTHON_VERSION_version
700 | echo \$INTERNAL_SCAN_PYTHON_VERSION_source
701 | EOF
702 | assert_success
703 | assert_line --index 0 "3.9.1"
704 | assert_line --index 1 "runtime.txt"
705 | }
706 |
707 | test_dpv_internal_scan_python_version_success_pyproject_toml() { # @test
708 | run dpv-eval <<'EOF'
709 | echo "python = 3.9.1" >>pyproject.toml
710 | dpv_internal_scan_python_version
711 | echo $INTERNAL_SCAN_PYTHON_VERSION_version
712 | echo $INTERNAL_SCAN_PYTHON_VERSION_source
713 | EOF
714 | assert_success
715 | assert_line --index 0 "3.9.1"
716 | assert_line --index 1 "pyproject.toml"
717 | }
718 |
719 | # bats test_tags=vendor:pyenv
720 | test_dpv_internal_scan_python_version_success_any_installed_version_PYENV() { # @test
721 | mock_available_install_methods "PYENV"
722 | mock_internal_installed_python_versions "PYENV" "3.9.2"
723 | run dpv-eval <<'EOF'
724 | dpv_internal_scan_python_version
725 | echo $INTERNAL_SCAN_PYTHON_VERSION_version
726 | echo $INTERNAL_SCAN_PYTHON_VERSION_source
727 | EOF
728 | assert_success
729 | assert_line --index 0 "3.9.2"
730 | assert_line --index 1 --partial "pyenv"
731 | }
732 |
733 | # bats test_tags=vendor:homebrew
734 | test_dpv_internal_scan_python_version_success_any_installed_version_HOMEBREW() { # @test
735 | mock_available_install_methods "HOMEBREW"
736 | mock_internal_installed_python_versions "HOMEBREW" "3.9.3"
737 | run dpv-eval <<'EOF'
738 | dpv_internal_scan_python_version
739 | echo $INTERNAL_SCAN_PYTHON_VERSION_version
740 | echo $INTERNAL_SCAN_PYTHON_VERSION_source
741 | EOF
742 | assert_success
743 | assert_line --index 0 "3.9.3"
744 | assert_line --index 1 --partial "homebrew"
745 | }
746 |
747 | # bats test_tags=vendor:pyenv
748 | test_dpv_internal_scan_python_version_success_any_available_version_PYENV() { # @test
749 | mock_available_install_methods "PYENV"
750 | mock_internal_installed_python_versions "PYENV" ""
751 | mock_internal_available_python_versions "PYENV" "3.9.2"
752 | run dpv-eval <<'EOF'
753 | dpv_internal_scan_python_version
754 | echo $INTERNAL_SCAN_PYTHON_VERSION_version
755 | echo $INTERNAL_SCAN_PYTHON_VERSION_source
756 | EOF
757 | assert_success
758 | assert_line --index 0 "3.9.2"
759 | assert_line --index 1 --partial "pyenv"
760 | }
761 |
762 | # bats test_tags=vendor:homebrew
763 | test_dpv_internal_scan_python_version_success_any_available_version_HOMEBREW() { # @test
764 | test_fn() {
765 | mock_available_install_methods "HOMEBREW"
766 | mock_internal_installed_python_versions "HOMEBREW" ""
767 | mock_internal_available_python_versions "HOMEBREW" "3.9.3"
768 | dpv-eval <<'EOF'
769 | dpv_internal_scan_python_version
770 | echo $INTERNAL_SCAN_PYTHON_VERSION_version
771 | echo $INTERNAL_SCAN_PYTHON_VERSION_source
772 | EOF
773 | }
774 | run test_fn
775 | assert_success
776 | assert_line --index 0 "3.9.3"
777 | assert_line --index 1 --partial "homebrew"
778 | }
779 |
780 | test_dpv_internal_scan_python_version_failure_no_internal_available_python_versions() { # @test
781 | mock_available_install_methods "PYENV HOMEBREW"
782 | mock_internal_installed_python_versions "HOMEBREW" ""
783 | mock_internal_available_python_versions "HOMEBREW" ""
784 | mock_internal_installed_python_versions "PYENV" ""
785 | mock_internal_available_python_versions "PYENV" ""
786 | run dpv-eval dpv_internal_scan_python_version
787 | assert_failure
788 | assert_output ""
789 | }
790 |
791 | test_dpv_internal_resolve_python_version_match_installed() { # @test
792 | run dpv-eval <