├── .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 | Bem-te-vi, Great Kiskadee, Pitangus sulphuratus, credit of Helena Stainer 4 | 5 | [![CI](https://github.com/caioariede/dpv/actions/workflows/ci.yml/badge.svg)](https://github.com/caioariede/dpv/actions/workflows/ci.yml) 6 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/caioariede/dpv) 7 | ![GitHub file size in bytes](https://img.shields.io/github/size/caioariede/dpv/src/dpv) 8 | ![Platform](https://img.shields.io/badge/platform-linux%20and%20macos-lightgrey) 9 | ![GitHub](https://img.shields.io/github/license/caioariede/dpv) 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 <