├── release ├── __init__.py ├── requirements.txt ├── version.py ├── __main__.py └── tarball.py ├── requirements.in ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── release.yml └── workflows │ ├── devskim.yml │ ├── cargo-audit.yml │ ├── dependency-review.yml │ ├── main.yml │ ├── pr-check.yml │ └── release.yml ├── .vscode ├── settings.json └── tasks.json ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── netlify.toml ├── benches └── list.rs ├── README.md ├── LICENSE ├── Cargo.toml ├── completions └── py.fish ├── src ├── HELP.txt ├── main.rs ├── cli.rs └── lib.rs ├── docs ├── cli.md ├── development.md ├── faq.md ├── install.md └── index.md ├── mkdocs.yml ├── tests ├── lib_system_tests.rs ├── common │ └── mod.rs ├── main_tests.rs └── cli_system_tests.rs ├── justfile ├── man-page ├── py.1.md └── py.1 ├── CODE_OF_CONDUCT.md ├── requirements.txt └── Cargo.lock /release/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | mkdocs-material 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: brettcannon 2 | -------------------------------------------------------------------------------- /release/requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | tomli 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | __pycache__ 4 | .venv 5 | .pytest_cache 6 | *.tar.xz 7 | site/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Please see https://github.com/brettcannon/python-launcher/releases for the release notes of any specific release. 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See https://python-launcher.app/development or https://github.com/brettcannon/python-launcher/tree/main/docs/development.md for the latest version of this document. 2 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/rust:1 2 | 3 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 4 | && apt-get -y install --no-install-recommends pandoc python3-dev python3-venv python3-pip 5 | 6 | USER vscode 7 | 8 | RUN cargo install just 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Feature request 3 | url: https://github.com/brettcannon/python-launcher/discussions/categories/ideas 4 | about: Open an "Ideas" discussion 5 | - name: Help 6 | url: https://github.com/brettcannon/python-launcher/discussions/categories/q-a 7 | about: Open a "Q&A" discussion 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "python -m mkdocs build" 3 | publish = "site" 4 | 5 | [[redirects]] 6 | from = "http://python-launcher.app/*" 7 | to = " https://pythoncode.run/:splat" 8 | status = 301 9 | force = true 10 | 11 | [[redirects]] 12 | from = "https://python-launcher.app/*" 13 | to = " https://pythoncode.run/:splat" 14 | status = 301 15 | force = true 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "cargo", 6 | "command": "clippy", 7 | "problemMatcher": [ 8 | "$rustc" 9 | ], 10 | "group": "none", 11 | "label": "rust: cargo clippy", 12 | "args": [ 13 | "--all-targets", 14 | "--all-features", 15 | "--", 16 | "-D", 17 | "warnings" 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /benches/list.rs: -------------------------------------------------------------------------------- 1 | use std::hint; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | 5 | fn criterion_benchmark(c: &mut Criterion) { 6 | c.bench_function("List executables", |b| { 7 | b.iter(|| { 8 | let executables = python_launcher::all_executables(); 9 | hint::black_box(executables); 10 | }); 11 | }); 12 | } 13 | 14 | criterion_group!(benches, criterion_benchmark); 15 | criterion_main!(benches); 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - "dependabot" 5 | categories: 6 | - title: "Breaking Changes 🛠️" 7 | labels: 8 | - "impact-breaking" 9 | - title: "New Features ✨" 10 | labels: 11 | - "impact-enhancement" 12 | - title: "Bugfixes 🐜" 13 | labels: 14 | - "impact-bugfix" 15 | - title: "Documentation 📄" 16 | labels: 17 | - "impact-docs" 18 | - title: "Maintenance 👷‍♀️" 19 | labels: 20 | - "impact-maintenance" 21 | -------------------------------------------------------------------------------- /release/version.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | try: 4 | import tomllib 5 | except ModuleNotFoundError: 6 | import tomli as tomllib 7 | 8 | CARGO_FILE = pathlib.Path(__file__).parent.parent / "Cargo.toml" 9 | 10 | 11 | def get_version(): 12 | """Read the version from Cargo.toml.""" 13 | with CARGO_FILE.open("rb") as file: 14 | cargo_toml = tomllib.load(file) 15 | 16 | return cargo_toml["package"]["version"] 17 | 18 | 19 | def main(args): 20 | """Print the version of the project.""" 21 | version = get_version() 22 | 23 | if args.tag: 24 | print("v", end="") 25 | print(version) 26 | -------------------------------------------------------------------------------- /release/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from . import tarball 4 | from . import version 5 | 6 | if __name__ == "__main__": 7 | arg_parser = argparse.ArgumentParser() 8 | arg_subparsers = arg_parser.add_subparsers() 9 | version_parser = arg_subparsers.add_parser("version") 10 | version_parser.add_argument("--tag", action="store_true") 11 | version_parser.set_defaults(func=version.main) 12 | 13 | tarball_parser = arg_subparsers.add_parser("tarball") 14 | tarball_parser.add_argument("--target", required=True) 15 | tarball_parser.set_defaults(func=tarball.main) 16 | 17 | args = arg_parser.parse_args() 18 | args.func(args) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. ... 16 | 17 | **Expected behaviour** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **System Details (please complete the following information):** 24 | - OS: [e.g. macOS] 25 | - Shell: [e.g. zsh] 26 | - Launcher Version: [found at the top of the output from `py --help`] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Python Launcher for Unix 2 | 3 | - [Documentation](https://python-launcher.app) 4 | - [Rust crate](https://crates.io/crates/python-launcher) 5 | 6 | ## Motivation 7 | 8 | Launch your Python interpreter the lazy/smart way! 9 | 10 | This project is an implementation of the `py` command for Unix-based platforms 11 | (with some potential experimentation for good measure 😉). 12 | 13 | ## Example 14 | 15 | Typical usage would be: 16 | 17 | ``` 18 | py -m venv .venv 19 | py ... # Normal `python` usage. 20 | ``` 21 | 22 | This creates a virtual environment in a `.venv` directory using the latest 23 | version of Python installed. Subsequent uses of `py` will then use that virtual 24 | environment as long as it is in the current (or higher) directory; no 25 | environment activation required (although the Python Launcher supports activated 26 | environments as well)! 27 | -------------------------------------------------------------------------------- /.github/workflows/devskim.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: DevSkim 7 | 8 | on: 9 | push: 10 | branches: [ "main" ] 11 | pull_request: 12 | branches: [ "main" ] 13 | schedule: 14 | - cron: '42 2 * * 3' 15 | 16 | jobs: 17 | lint: 18 | name: DevSkim 19 | runs-on: ubuntu-latest 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Run DevSkim scanner 29 | uses: microsoft/DevSkim-Action@v1 30 | 31 | - name: Upload DevSkim scan results to GitHub Security tab 32 | uses: github/codeql-action/upload-sarif@v3 33 | with: 34 | sarif_file: devskim-results.sarif 35 | -------------------------------------------------------------------------------- /.github/workflows/cargo-audit.yml: -------------------------------------------------------------------------------- 1 | name: cargo audit 2 | on: 3 | schedule: 4 | - cron: "0 0 * * 5" 5 | # TODO: bring back once https://github.com/brettcannon/python-launcher/issues/211 is fixed. 6 | # pull_request: 7 | # paths: 8 | # - "**/Cargo.toml" 9 | # - "**/Cargo.lock" 10 | 11 | jobs: 12 | audit: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.cargo/registry 21 | ~/.cargo/git 22 | target 23 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 24 | 25 | # actions-rs/audit-check isn't updated fast enough for our use 26 | # of stable. 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | override: true 31 | 32 | - name: "Install cargo-audit" 33 | run: cargo install cargo-audit 34 | 35 | - name: "Audit" 36 | run: cargo audit 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2018 Brett Cannon 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 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: 9 | pull_request: 10 | paths: 11 | - "**/Cargo.toml" 12 | - "**/Cargo.lock" 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | dependency-review: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: 'Checkout Repository' 22 | uses: actions/checkout@v4 23 | - name: 'Dependency Review' 24 | uses: actions/dependency-review-action@v4 25 | with: 26 | deny-licenses: AGPL-3.0-only, AGPL-3.0-or-later, GPL-2.0-only, GPL-2.0-or-later, GPL-3.0-only, GPL-3.0-or-later 27 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://github.com/microsoft/vscode-dev-containers/blob/main/containers/rust/.devcontainer/devcontainer.json 3 | "name": "Rust", 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "runArgs": [ 8 | "--cap-add=SYS_PTRACE", 9 | "--security-opt", 10 | "seccomp=unconfined" 11 | ], 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": { 14 | "lldb.executable": "/usr/bin/lldb", 15 | // VS Code won't watch files under ./target 16 | "files.watcherExclude": { 17 | "**/target/**": true 18 | }, 19 | "rust-analyzer.checkOnSave.command": "clippy" 20 | }, 21 | // Add the IDs of extensions you want installed when the container is created. 22 | "extensions": [ 23 | "vadimcn.vscode-lldb", 24 | "matklad.rust-analyzer", 25 | "tamasfe.even-better-toml", 26 | "serayuzgur.crates", 27 | "skellock.just" 28 | ], 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | // "forwardPorts": [], 31 | // Use 'postCreateCommand' to run commands after the container is created. 32 | // "postCreateCommand": "rustc --version", 33 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 34 | "remoteUser": "vscode" 35 | } -------------------------------------------------------------------------------- /release/tarball.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import tarfile 3 | 4 | from . import version 5 | 6 | 7 | def main(args): 8 | """Create a tarball and print the path to it.""" 9 | semver = version.get_version() 10 | repo_path = pathlib.Path(__file__).parent.parent 11 | sub_dir = f"python_launcher-{semver}" 12 | tar_contents = pathlib.Path(repo_path / sub_dir) 13 | 14 | binary_path = repo_path / "target" / args.target / "release" / "py" 15 | binary_tar = f"{sub_dir}/bin/py" 16 | 17 | license_path = repo_path / "LICENSE" 18 | license_tar = f"{sub_dir}/share/doc/py/LICENSE" 19 | 20 | readme_path = repo_path / "README.md" 21 | readme_tar = f"{sub_dir}/share/doc/py/README.md" 22 | 23 | man_path = repo_path / "man-page" / "py.1" 24 | man_tar = f"{sub_dir}/share/man/man1/py.1" 25 | 26 | fish_path = repo_path / "completions" / "py.fish" 27 | fish_tar = f"{sub_dir}/share/fish/vendor_completions.d/py.fish" 28 | 29 | tar_path = repo_path / f"{sub_dir}-{args.target}.tar.xz" 30 | with tarfile.open(tar_path, "w:xz") as tar_file: 31 | tar_file.add(binary_path, binary_tar) 32 | tar_file.add(license_path, license_tar) 33 | tar_file.add(readme_path, readme_tar) 34 | tar_file.add(man_path, man_tar) 35 | tar_file.add(fish_path, fish_tar) 36 | 37 | print(tar_path) 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.cargo/registry 20 | ~/.cargo/git 21 | target 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 23 | 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | override: true 28 | 29 | - uses: actions/setup-python@v5 30 | with: 31 | python-version: "3.11" 32 | 33 | - name: Install distro packages 34 | run: sudo apt-get install -qq pandoc 35 | 36 | - name: Install `just` 37 | uses: extractions/setup-just@v2 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Run `just` 42 | run: just 43 | 44 | format: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Install latest stable 51 | uses: actions-rs/toolchain@v1 52 | with: 53 | toolchain: stable 54 | override: true 55 | components: rustfmt 56 | 57 | - run: cargo fmt --check 58 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR Check 2 | on: 3 | pull_request: 4 | types: 5 | - "opened" 6 | - "reopened" 7 | - "synchronize" 8 | - "labeled" 9 | - "unlabeled" 10 | 11 | jobs: 12 | files-changed: 13 | name: Files up-to-date 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Cargo.lock up-to-date 19 | uses: brettcannon/check-for-changed-files@v1 20 | with: 21 | prereq-pattern: Cargo.toml 22 | file-pattern: Cargo.lock 23 | skip-label: skip Cargo.lock 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Man page up-to-date 28 | uses: brettcannon/check-for-changed-files@v1 29 | with: 30 | prereq-pattern: man-page/py.1.md 31 | file-pattern: man-page/py.1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | impact: 36 | name: Impact specified 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: "`impact` label specified" 40 | uses: mheap/github-action-required-labels@v5 41 | with: 42 | mode: "exactly" 43 | count: 1 44 | labels: "impact-breaking, impact-enhancement, impact-bugfix, impact-docs, impact-maintenance" 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "python-launcher" 3 | description = "The Python launcher for Unix" 4 | version = "1.0.1" 5 | authors = ["Brett Cannon "] 6 | homepage = "https://python-launcher.app" 7 | repository = "https://github.com/brettcannon/python-launcher" 8 | readme = "README.md" 9 | include = [ 10 | "/src/", 11 | "/tests/", 12 | "/benches/", 13 | "/completions/", 14 | "/man-page/py.1", 15 | "/README.md", 16 | "/CHANGELOG.md", 17 | "/LICENSE", 18 | ] 19 | license = "MIT" 20 | keywords = ["Python"] 21 | categories = ["command-line-utilities"] 22 | edition = "2021" 23 | rust-version = "1.66" 24 | 25 | [badges] 26 | maintenance = { status = "actively-developed" } 27 | 28 | [[bench]] 29 | name = "list" 30 | harness = false 31 | 32 | [[bin]] 33 | name = "py" 34 | path = "src/main.rs" 35 | 36 | [dependencies] 37 | comfy-table = "7.1.1" 38 | exitcode = "1.1.2" 39 | human-panic = "2.0.0" 40 | log = "0.4.21" 41 | nix = {version = "0.29.0", features = ["process"]} 42 | stderrlog = "0.6.0" 43 | 44 | [dev-dependencies] 45 | assert_cmd = "2.0.14" 46 | criterion = "0.5.1" 47 | predicates = "3.1.0" 48 | serial_test = "3.0.0" 49 | tempfile = "3.10.1" 50 | test-case = "3.3.1" 51 | 52 | [profile.dev] 53 | split-debuginfo = "unpacked" 54 | 55 | [profile.release] 56 | # https://github.com/johnthagen/min-sized-rust 57 | lto = true 58 | strip = true 59 | codegen-units = 1 60 | -------------------------------------------------------------------------------- /completions/py.fish: -------------------------------------------------------------------------------- 1 | # Wrap the `python` command. 2 | # While wrapping the latest Python version specifically would provide 3 | # the most accurate completions, fish only ships with completions for 4 | # `python` itself. 5 | complete -c py --wraps python 6 | 7 | # Statically-known completions. 8 | complete -c py --long-option list --no-files -d "List all known interpreters" 9 | complete -c py --short-option h --long-option help --no-files -d "Display help and exit" 10 | 11 | # Dynamic/system-specific completions. 12 | set -l seen_major_versions 13 | py --list | while read -d " │ " -l padded_version padded_path 14 | # Complete on the `major.minor` version. 15 | set -l full_version (string trim $padded_version) 16 | set -l executable_path (string trim $padded_path) 17 | complete -c py --old-option $full_version -d "Launch $executable_path" 18 | # Complete on the major version. 19 | # Assume that `py --list` emits a sorted list of versions, so the 20 | # first instance of any major version is the one that will be used. 21 | set -l major_version (string split --fields 1 . $full_version) 22 | if not contains $major_version $seen_major_versions 23 | # Must use `--old-option` in case the major version ever goes multi-digit. 24 | complete -c py --old-option $major_version -d "Launch $executable_path" 25 | set --append seen_major_versions $major_version 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/HELP.txt: -------------------------------------------------------------------------------- 1 | Python Launcher for Unix {} 2 | usage: {} [launcher-args] [python-args] 3 | 4 | Launcher arguments: 5 | -h/--help: This output; must be specified on its own. 6 | --list : List all known interpreters (except activated virtual environment); 7 | must be specified on its own. 8 | -[X] : Launch the latest Python `X` version (e.g. `-3` for the latest 9 | Python 3); PY_PYTHON[X] overrides what is considered the latest 10 | (e.g. `PY_PYTHON3=3.6` will cause `-3` to search for Python 3.6). 11 | -[X.Y] : Launch the specified Python version (e.g. `-3.6` for Python 3.6). 12 | 13 | Other environment variables: 14 | PY_PYTHON : Specify the version of Python to search for when no Python 15 | version is explicitly requested (must be formatted as 'X.Y'; 16 | e.g. `3.6` to use Python 3.6 by default). 17 | PY_PYTHON* : Specify the version of Python to search for when only a major 18 | version is specified (must be formatted as 'X.Y', e.g. set 19 | `PY_PYTHON3` to `3.6` to cause `-3` to use Python 3.6). 20 | PYLAUNCH_DEBUG: Log details to stderr about how the Launcher is operating. 21 | VIRTUAL_ENV : Path to a directory containing virtual environment to use when no 22 | Python version is explicitly requested; typically set by 23 | activating a virtual environment. 24 | 25 | The following help text is from {}: 26 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | In general, the `py` command passes on its arguments to the selected Python interpreter. Below are the Launcher-specific arguments that are supported. 4 | 5 | ## Arguments 6 | 7 | ### `-[X]` 8 | 9 | Specifies the major Python version desired, e.g. `-3`. Specifying such a restriction causes the equivalent [`PY_PYTHON[X]`](#py_pythonx) environment variable to be used if set. 10 | 11 | See the [overview](index.md#on-the-command-line) for more details. 12 | 13 | ### `-[X.Y]` 14 | 15 | Specifies the major and minor Python version desired, e.g. `-3.6` for Python 3.6. 16 | 17 | See the [overview](index.md#on-the-command-line) for more details. 18 | 19 | ### `--list` 20 | 21 | Lists all Python interpreters found on the `PATH` environment variable. 22 | 23 | ## Environment variables 24 | 25 | ### `PY_PYTHON` 26 | 27 | Specifies a version restriction when none is specified on the command line, i.e. `py` is used. This is useful for setting the default Python version to always use. 28 | 29 | See the [overview](index.md#environment-variables) for more details. 30 | 31 | ### `PY_PYTHON[X]` 32 | 33 | Specifies the version restriction when the equivalent major Python version is specified on the command line, e.g. `-3` causes `PY_PYTHON3` to be used if set. 34 | 35 | See the [overview](index.md#environment-variables) for more details. 36 | 37 | ### `PYLAUNCH_DEBUG` 38 | 39 | When set, causes the Python Launcher to print out information about its interpreter search to stderr. 40 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "Python Launcher for Unix" 2 | site_url: "https://python-launcher.app" 3 | repo_url: "https://github.com/brettcannon/python-launcher" 4 | edit_uri: "https://github.dev/brettcannon/python-launcher/blob/main/docs/" 5 | site_description: "Launch your Python interpreter the lazy/smart way!" 6 | site_author: "Brett Cannon" 7 | copyright: "© 2022 Brett Cannon" 8 | docs_dir: docs 9 | site_dir: site 10 | theme: 11 | name: "material" 12 | icon: 13 | logo: octicons/terminal-16 14 | features: 15 | - "navigation.sections" 16 | - "navigation.tabs" 17 | - "navigation.tabs.sticky" 18 | - "toc.integrate" 19 | markdown_extensions: 20 | - "admonition" 21 | - "pymdownx.details" 22 | - "pymdownx.magiclink" 23 | - pymdownx.superfences: 24 | custom_fences: 25 | - name: mermaid 26 | class: mermaid 27 | format: !!python/name:pymdownx.superfences.fence_code_format 28 | - pymdownx.tabbed: 29 | alternate_style: true 30 | extra: 31 | social: 32 | - icon: fontawesome/solid/rss 33 | link: https://snarky.ca/ 34 | - icon: fontawesome/brands/github 35 | link: https://github.com/brettcannon/ 36 | - icon: fontawesome/brands/mastodon 37 | link: https://fosstodon.org/@brettcannon 38 | - icon: fontawesome/brands/linkedin 39 | link: https://www.linkedin.com/in/drbrettcannon/ 40 | nav: 41 | - Overview: "index.md" 42 | - "install.md" 43 | - "cli.md" 44 | - FAQ: "faq.md" 45 | - "development.md" 46 | -------------------------------------------------------------------------------- /tests/lib_system_tests.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use serial_test::serial; 4 | 5 | use python_launcher::{ExactVersion, RequestedVersion}; 6 | 7 | use common::EnvState; 8 | 9 | #[test] 10 | #[serial] 11 | fn all_executables() { 12 | let env_state = EnvState::new(); 13 | 14 | let executables = python_launcher::all_executables(); 15 | 16 | assert_eq!(executables.len(), 3); 17 | 18 | let python27_version = ExactVersion { major: 2, minor: 7 }; 19 | assert!(executables.contains_key(&python27_version)); 20 | assert_eq!( 21 | executables.get(&python27_version), 22 | Some(&env_state.python27) 23 | ); 24 | 25 | let python36_version = ExactVersion { major: 3, minor: 6 }; 26 | assert!(executables.contains_key(&python27_version)); 27 | assert_eq!( 28 | executables.get(&python36_version), 29 | Some(&env_state.python36) 30 | ); 31 | 32 | let python37_version = ExactVersion { major: 3, minor: 7 }; 33 | assert!(executables.contains_key(&python37_version)); 34 | assert_eq!( 35 | executables.get(&python37_version), 36 | Some(&env_state.python37) 37 | ); 38 | } 39 | 40 | #[test] 41 | #[serial] 42 | fn find_executable() { 43 | let env_state = EnvState::new(); 44 | 45 | assert_eq!( 46 | python_launcher::find_executable(RequestedVersion::Any), 47 | Some(env_state.python37) 48 | ); 49 | 50 | assert_eq!( 51 | python_launcher::find_executable(RequestedVersion::MajorOnly(2)), 52 | Some(env_state.python27) 53 | ); 54 | 55 | assert_eq!( 56 | python_launcher::find_executable(RequestedVersion::Exact(3, 6)), 57 | Some(env_state.python36) 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Code 4 | 5 | The Python Launcher is _mostly_ run as a typical Rust project. The only 6 | potential differences is the automation tool used (for convenience). 7 | 8 | 9 | We use [just](https://github.com/casey/just) as a task runner. Some rules require Python >= 3.11 to be installed. Some rules will also use `py` itself via `cargo run`, so the source code needs to be working. 10 | 11 | ## Website 12 | 13 | The website is built using [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). 14 | 15 | While developing the website, you can run `just docs-dev` to start a local server that will automatically reload when you make changes. This will create a virtual environment in `.venv` and install the necessary dependencies. 16 | 17 | To build the docs, use `just docs`. 18 | 19 | ## Releasing 20 | 21 | ### [GitHub Releases](https://github.com/brettcannon/python-launcher/releases) 22 | 23 | 1. Adjust the version number in [`Cargo.toml`](https://github.com/brettcannon/python-launcher/blob/main/Cargo.toml) (previous [releases](https://github.com/brettcannon/python-launcher/releases)). 24 | 1. Check that the relevant [action workflows](https://github.com/brettcannon/python-launcher/actions) are passing. 25 | 1. Run the [`release` pipeline](https://github.com/brettcannon/python-launcher/actions/workflows/release.yml). 26 | 1. Publish the [release](https://github.com/brettcannon/python-launcher/releases). 27 | 1. Make sure the docs are up-to-date based on the published version (e.g. the [installation instructions](install.md) link to the newest files) 28 | 29 | ## Useful links 30 | 31 | - [Repository](https://github.com/brettcannon/python-launcher/) 32 | - [crates.io page](https://crates.io/crates/python-launcher) 33 | - [API docs](https://docs.rs/python-launcher/) 34 | 35 | ## Appendix 36 | 37 | ### PEPs 38 | 39 | - [PEP 397: Python launcher for Windows](https://www.python.org/dev/peps/pep-0397/) 40 | - [PEP 486: Make the Python Launcher aware of virtual environments](https://www.python.org/dev/peps/pep-0486/) 41 | 42 | ### Python Launcher for Windows 43 | 44 | - [Documentation](https://docs.python.org/3/using/windows.html#launcher) 45 | - [Source](https://github.com/python/cpython/blob/master/PC/launcher.c) 46 | - [Experimental source](https://github.com/python/cpython/blob/main/PC/launcher2.c) 47 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How do I have [Starship](https://starship.rs/) use the Python Launcher to display the Python version? 4 | 5 | Add the following to your [Starship configuration file](https://starship.rs/config/): 6 | 7 | ```TOML 8 | [python] 9 | python_binary = ["py"] 10 | # The following isn't necessary, but convenient. 11 | detect_folders = [".venv"] 12 | ``` 13 | 14 | This will then have your prompt list the Python version that will be used if you run `py`: 15 | 16 | ```console 17 | /tmp/starship-demo via 🐍 v3.11.0 18 | ❯ py -3.9 -m venv .venv 19 | 20 | /tmp/starship-demo via 🐍 v3.9.15 21 | ❯ 22 | ``` 23 | 24 | ## How do I get a table of Python executables in [Nushell](https://www.nushell.sh/)? 25 | 26 | ```console 27 | py --list | lines | split column "│" version path | str trim 28 | ``` 29 | 30 | Do note that the character that is being split on is **not** the traditional [U+007C/"Vertical Line"/pipe character](https://www.compart.com/en/unicode/U+007C) (`|`), but [U+2502/"Box Drawings Light Vertical"](https://www.compart.com/en/unicode/U+2502) (`│`). 31 | 32 | 33 | ## How can I make the Python Launcher use my default Python version from [pyenv](https://github.com/pyenv/pyenv)? 34 | 35 | If you're using [pyenv](https://github.com/pyenv/pyenv) to manage your Python versions, you'll want to set the version the Launcher uses to the pyenv [global version](https://github.com/pyenv/pyenv/blob/master/COMMANDS.md#pyenv-global). 36 | 37 | 38 | === "bash/zsh" 39 | 40 | Add this line to your `.zshrc` or `.bashrc` file: 41 | 42 | ```console 43 | export PY_PYTHON=$(pyenv exec python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))") 44 | ``` 45 | 46 | === "fish" 47 | 48 | Add this line to your `~/.config/fish/config.fish` file: 49 | 50 | ```console 51 | set -gx PY_PYTHON (pyenv exec python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))") 52 | ``` 53 | 54 | ## How do I disable the automatic search/usage of the `.venv` virtual environment? 55 | 56 | If you look at the [diagram of how the Launcher chooses what Python interpreter/environment to use](index.md#diagram-of-how-the-python-launcher-selects-a-python-interpreter), you will notice that the `.venv` virtual environment is only selected if you **don't** specify a version restriction, e.g. `py -3` disables the search. 57 | 58 | The thinking behind this is that if you want a specific Python version then you aren't interested in a specific virtual environmen., Thus the search for `.venv` is skipped when **any** specific version is requested. 59 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | ROOT := justfile_directory() 4 | MAN_MD := join(ROOT, "man-page", "py.1.md") 5 | MAN_FILE := join(ROOT, "man-page", "py.1") 6 | CARGO_TOML := join(ROOT, "Cargo.toml") 7 | VENV := join(ROOT, ".venv") 8 | INSTALL_DOCS := join(ROOT, "docs", "install.md") 9 | 10 | # Set default recipes 11 | _default: lint test man docs 12 | 13 | # Run the unit tests 14 | test: 15 | cargo --quiet test 16 | 17 | # Run linting on source files 18 | lint: 19 | cargo fmt --quiet --all -- --check 20 | cargo clippy --quiet --all-targets --all-features -- -D warnings 21 | 22 | # Install from source 23 | install: 24 | cargo install --quiet --path . 25 | 26 | # Convert the markdown-formatted man page to the man file format 27 | _man-md: 28 | pandoc {{ MAN_MD }} --standalone -t man -o {{ MAN_FILE }} 29 | 30 | # Build the man page (requires Python >= 3.11) 31 | man: _man-md 32 | #!/usr/bin/env python3 33 | 34 | import datetime 35 | import pathlib 36 | import re 37 | import tomllib 38 | 39 | 40 | with open("{{ CARGO_TOML }}", "rb") as file: 41 | cargo_data = tomllib.load(file) 42 | 43 | try: 44 | version = cargo_data["package"]["version"] 45 | except KeyError as exc: 46 | raise ValueError("'version' not found in {{ CARGO_TOML }}") from exc 47 | 48 | with open("{{ MAN_FILE }}", "r", encoding="utf-8") as file: 49 | man_text = file.read() 50 | 51 | man_text_with_version = man_text.replace("LAUNCHER_VERSION", version) 52 | new_man_text = man_text_with_version.replace( 53 | "CURRENT_DATE", datetime.date.today().isoformat() 54 | ) 55 | 56 | with open("{{ MAN_FILE }}", "w", encoding="utf-8") as file: 57 | file.write(new_man_text) 58 | 59 | # Create a lock file for docs/ 60 | docs-lock: 61 | pipx run --spec pip-tools pip-compile --generate-hashes --allow-unsafe -o requirements.txt requirements.in 62 | 63 | # Update insfall instructions for a specific version 64 | docs-install: 65 | #!/usr/bin/env bash 66 | set -euxo pipefail 67 | version=`cargo run -- -m release version` 68 | pipx run cogapp -D VERSION=${version} -D TAG=v${version} -r {{ INSTALL_DOCS }} 69 | 70 | # Create a virtual environment for building the docs 71 | docs-venv: 72 | #!/usr/bin/env bash 73 | set -euxo pipefail 74 | if [ -d {{ VENV }} ]; then 75 | rm -rf {{ VENV }} 76 | fi 77 | cargo run -- -m venv {{ VENV }} 78 | cargo run -- -m pip install --quiet --disable-pip-version-check -r requirements.txt 79 | 80 | # Launch the documentation dev server 81 | docs-dev: docs-venv 82 | cargo run -- -m mkdocs serve 83 | 84 | # Build the documentation 85 | docs: docs-venv docs-install 86 | cargo run -- -m mkdocs build 87 | -------------------------------------------------------------------------------- /man-page/py.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PY 3 | section: 1 4 | header: Python Launcher 5 | footer: Python Launcher LAUNCHER_VERSION 6 | date: CURRENT_DATE 7 | --- 8 | 9 | # NAME 10 | 11 | py - launch a Python interpreter 12 | 13 | # SYNOPSIS 14 | 15 | **py** [**-[X]/[X.Y]**] ... 16 | 17 | # DESCRIPTION 18 | 19 | **py** launches the most appropriate Python interpreter it can find. It is meant 20 | to act as a shorthand for launching **python** without having to think about 21 | _which_ Python interpreter is the most desired. The Python Launcher is not meant 22 | to substitute all ways of launching Python, e.g. if a specific Python 23 | interpreter is desired then it is assumed it will be directly executed. 24 | 25 | # SPECIFYING A PYTHON VERSION 26 | 27 | If a command-line option is provided in the form of **-X** or **-X.Y** where _X_ 28 | and _Y_ are integers, then that version of Python will be launched 29 | (if available). For instance, providing **-3** will launch the newest version of 30 | Python 3 while **-3.6** will try to launch Python 3.6. 31 | 32 | # SEARCHING FOR PYTHON INTERPRETERS 33 | 34 | When no command-line arguments are provided to the launcher, what is deemed the 35 | most "appropriate" interpreter is searched for as follows: 36 | 37 | 1. An activated virtual environment (launched immediately if available) 38 | 2. A **.venv** directory in the current working directory or any parent 39 | directory containing a virtual environment 40 | (launched immediately if available) 41 | 3. If a file path is provided as the first argument, look for a shebang line 42 | containing **/usr/bin/python**, **/usr/local/bin/python**, 43 | **/usr/bin/env python** or **python** and any version specification in the 44 | executable name is treated as a version specifier (like with **-X**/**-X.Y** 45 | command-line options) 46 | 4. Check for any appropriate environment variable (see **ENVIRONMENT**) 47 | 5. Search **PATH** for all **pythonX.Y** executables 48 | 6. Launch the newest version of Python (while matching any version restrictions 49 | previously specified) 50 | 51 | All unrecognized command-line arguments are passed on to the launched Python 52 | interpreter. 53 | 54 | # OPTIONS 55 | 56 | **-h**, **\--help** 57 | : Print a help message and exit; must be specified on its own. 58 | 59 | **\--list** 60 | : List all known interpreters (except virtual environments); 61 | must be specified on its own. 62 | 63 | **-[X]** 64 | : Launch the latest Python _X_ version (e.g. **-3** for the latest 65 | Python 3). See **ENVIRONMENT** for details on the **PY_VERSION[X]** environment 66 | variable. 67 | 68 | **-[X.Y]** 69 | : Launch the specified Python version (e.g. **-3.6** for Python 3.6). 70 | 71 | # ENVIRONMENT 72 | 73 | **PY_PYTHON** 74 | : Specify the version of Python to search for when no Python 75 | version is explicitly requested (must be formatted as 'X.Y'; e.g. **3.6** to use 76 | Python 3.6 by default). 77 | 78 | **PY_PYTHON[X]** 79 | : Specify the version of Python to search for when only a major 80 | version is specified (must be formatted as 'X.Y'; e.g. set **PY_PYTHON3** to 81 | **3.6** to cause **-3** to use Python 3.6). 82 | 83 | **PYLAUNCH_DEBUG** 84 | : Log details to stderr about how the Launcher is operating. 85 | 86 | **VIRTUAL_ENV** 87 | : Path to a directory containing virtual environment to use when no 88 | Python version is explicitly requested; typically set by 89 | activating a virtual environment. 90 | 91 | **PATH** 92 | : Used to search for Python interpreters. 93 | 94 | # AUTHORS 95 | 96 | Copyright © 2018 Brett Cannon. 97 | 98 | Licensed under MIT. 99 | 100 | # HOMEPAGE 101 | 102 | https://github.com/brettcannon/python-launcher/ 103 | 104 | # SEE ALSO 105 | 106 | python(1), python3(1). 107 | -------------------------------------------------------------------------------- /man-page/py.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.2.1 2 | .\" 3 | .TH "PY" "1" "2024-06-30" "Python Launcher 1.0.1" "Python Launcher" 4 | .SH NAME 5 | py \- launch a Python interpreter 6 | .SH SYNOPSIS 7 | \f[B]py\f[R] [\f[B]\-[X]/[X.Y]\f[R]] \&... 8 | .SH DESCRIPTION 9 | \f[B]py\f[R] launches the most appropriate Python interpreter it can 10 | find. 11 | It is meant to act as a shorthand for launching \f[B]python\f[R] without 12 | having to think about \f[I]which\f[R] Python interpreter is the most 13 | desired. 14 | The Python Launcher is not meant to substitute all ways of launching 15 | Python, e.g.\ if a specific Python interpreter is desired then it is 16 | assumed it will be directly executed. 17 | .SH SPECIFYING A PYTHON VERSION 18 | If a command\-line option is provided in the form of \f[B]\-X\f[R] or 19 | \f[B]\-X.Y\f[R] where \f[I]X\f[R] and \f[I]Y\f[R] are integers, then 20 | that version of Python will be launched (if available). 21 | For instance, providing \f[B]\-3\f[R] will launch the newest version of 22 | Python 3 while \f[B]\-3.6\f[R] will try to launch Python 3.6. 23 | .SH SEARCHING FOR PYTHON INTERPRETERS 24 | When no command\-line arguments are provided to the launcher, what is 25 | deemed the most \[lq]appropriate\[rq] interpreter is searched for as 26 | follows: 27 | .IP "1." 3 28 | An activated virtual environment (launched immediately if available) 29 | .IP "2." 3 30 | A \f[B].venv\f[R] directory in the current working directory or any 31 | parent directory containing a virtual environment (launched immediately 32 | if available) 33 | .IP "3." 3 34 | If a file path is provided as the first argument, look for a shebang 35 | line containing \f[B]/usr/bin/python\f[R], 36 | \f[B]/usr/local/bin/python\f[R], \f[B]/usr/bin/env python\f[R] or 37 | \f[B]python\f[R] and any version specification in the executable name is 38 | treated as a version specifier (like with \f[B]\-X\f[R]/\f[B]\-X.Y\f[R] 39 | command\-line options) 40 | .IP "4." 3 41 | Check for any appropriate environment variable (see 42 | \f[B]ENVIRONMENT\f[R]) 43 | .IP "5." 3 44 | Search \f[B]PATH\f[R] for all \f[B]pythonX.Y\f[R] executables 45 | .IP "6." 3 46 | Launch the newest version of Python (while matching any version 47 | restrictions previously specified) 48 | .PP 49 | All unrecognized command\-line arguments are passed on to the launched 50 | Python interpreter. 51 | .SH OPTIONS 52 | .TP 53 | \f[B]\-h\f[R], \f[B]\-\-help\f[R] 54 | Print a help message and exit; must be specified on its own. 55 | .TP 56 | \f[B]\-\-list\f[R] 57 | List all known interpreters (except virtual environments); must be 58 | specified on its own. 59 | .TP 60 | \f[B]\-[X]\f[R] 61 | Launch the latest Python \f[I]X\f[R] version (e.g.\ \f[B]\-3\f[R] for 62 | the latest Python 3). 63 | See \f[B]ENVIRONMENT\f[R] for details on the \f[B]PY_VERSION[X]\f[R] 64 | environment variable. 65 | .TP 66 | \f[B]\-[X.Y]\f[R] 67 | Launch the specified Python version (e.g.\ \f[B]\-3.6\f[R] for Python 68 | 3.6). 69 | .SH ENVIRONMENT 70 | .TP 71 | \f[B]PY_PYTHON\f[R] 72 | Specify the version of Python to search for when no Python version is 73 | explicitly requested (must be formatted as `X.Y'; e.g.\ \f[B]3.6\f[R] to 74 | use Python 3.6 by default). 75 | .TP 76 | \f[B]PY_PYTHON[X]\f[R] 77 | Specify the version of Python to search for when only a major version is 78 | specified (must be formatted as `X.Y'; e.g.\ set \f[B]PY_PYTHON3\f[R] to 79 | \f[B]3.6\f[R] to cause \f[B]\-3\f[R] to use Python 3.6). 80 | .TP 81 | \f[B]PYLAUNCH_DEBUG\f[R] 82 | Log details to stderr about how the Launcher is operating. 83 | .TP 84 | \f[B]VIRTUAL_ENV\f[R] 85 | Path to a directory containing virtual environment to use when no Python 86 | version is explicitly requested; typically set by activating a virtual 87 | environment. 88 | .TP 89 | \f[B]PATH\f[R] 90 | Used to search for Python interpreters. 91 | .SH AUTHORS 92 | Copyright © 2018 Brett Cannon. 93 | .PP 94 | Licensed under MIT. 95 | .SH HOMEPAGE 96 | https://github.com/brettcannon/python\-launcher/ 97 | .SH SEE ALSO 98 | python(1), python3(1). 99 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Providing a convenient way to launch `python` 2 | //! 3 | //! The binary is meant to make the `py` command your go-to command for 4 | //! launching a Python interpreter while writing code. It does this by trying 5 | //! to find the Python interpreter that you most likely want to use. 6 | //! 7 | //! # Examples 8 | //! 9 | //! Launch the newest version of Python installed. 10 | //! ```text 11 | //! > py 12 | //! ``` 13 | //! It will also launch any activated virtual environments (as set by the 14 | //! `$VIRTUAL_ENV` environment variable) or any virtual environment in a 15 | //! `.venv` subdirectory in any of the current or parent directories. 16 | //! 17 | //! You can also launch a specific version of Python. 18 | //! ```text 19 | //! > py -3.6 20 | //! ``` 21 | //! 22 | //! If you only care about the major version of Python, you can specify only 23 | //! that. 24 | //! ```text 25 | //! > py -3 26 | //! ``` 27 | //! 28 | //! # Important environment variables 29 | //! 30 | //! - `VIRTUAL_ENV`: an activated virtual environment. 31 | //! - `PYLAUNCH_DEBUG`: turn on logging. 32 | //! - `PY_PYTHON`: specify what Python version to use by default. 33 | //! - `PY_PYTHON*`: specify the Python version to use by default for a major 34 | //! version (e.g. `PY_PYTHON3` for `-3`). 35 | 36 | use std::{env, ffi::CString, os::unix::ffi::OsStrExt, path::Path}; 37 | 38 | use human_panic::Metadata; 39 | 40 | use nix::errno::Errno; 41 | use nix::unistd; 42 | 43 | use python_launcher::cli; 44 | 45 | fn main() { 46 | human_panic::setup_panic!(Metadata::new( 47 | env!("CARGO_PKG_DESCRIPTION"), 48 | env!("CARGO_PKG_VERSION") 49 | ) 50 | .authors(env!("CARGO_PKG_AUTHORS")) 51 | .homepage(env!("CARGO_PKG_REPOSITORY"))); 52 | 53 | let log_level = if env::var_os("PYLAUNCH_DEBUG").is_some() { 54 | 3 55 | } else { 56 | 0 57 | }; 58 | // - `error!` is for errors 59 | // - `info!` is to communicate what the launcher is doing/checking 60 | // - `debug!` is communicating about specific values 61 | stderrlog::new() 62 | .module(module_path!()) 63 | .module("python_launcher") 64 | .show_level(false) 65 | .verbosity(log_level) // [error, warn, info, debug, trace] 66 | .init() 67 | .unwrap(); 68 | 69 | match cli::Action::from_main(&env::args().collect::>()) { 70 | Ok(action) => match action { 71 | cli::Action::Help(message, executable) => { 72 | print!("{message}"); 73 | run(&executable, &["--help".to_string()]) 74 | .map_err(|message| log_exit(Errno::last_raw(), message)) 75 | .unwrap() 76 | } 77 | cli::Action::List(output) => print!("{output}"), 78 | cli::Action::Execute { 79 | executable, args, .. 80 | } => run(&executable, &args) 81 | .map_err(|message| log_exit(Errno::last_raw(), message)) 82 | .unwrap(), 83 | }, 84 | Err(message) => log_exit(message.exit_code(), message), 85 | } 86 | } 87 | 88 | fn log_exit(return_code: i32, message: impl std::error::Error) { 89 | log::error!("{message}"); 90 | std::process::exit(return_code); 91 | } 92 | 93 | fn run(executable: &Path, args: &[String]) -> nix::Result<()> { 94 | let printable_executable = executable.display(); 95 | if executable.is_file() { 96 | log::info!("Executing {printable_executable} with {args:?}"); 97 | } else { 98 | log::error!("{printable_executable}: No such file"); 99 | std::process::exit(1); 100 | } 101 | let executable_as_cstring = CString::new(executable.as_os_str().as_bytes()).unwrap(); 102 | let mut argv = vec![executable_as_cstring.clone()]; 103 | argv.extend(args.iter().map(|arg| CString::new(arg.as_str()).unwrap())); 104 | 105 | unistd::execv(&executable_as_cstring, &argv).map(|_| ()) 106 | } 107 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::ffi::{OsStr, OsString}; 4 | use std::fs::File; 5 | use std::path::PathBuf; 6 | 7 | use tempfile::TempDir; 8 | 9 | pub struct EnvVarState { 10 | changed: HashMap>, 11 | } 12 | 13 | impl Drop for EnvVarState { 14 | fn drop(&mut self) { 15 | self.changed.iter().for_each(|(k, v)| match &v { 16 | Some(original_v) => env::set_var(k, original_v), 17 | None => env::remove_var(k), 18 | }); 19 | } 20 | } 21 | 22 | impl EnvVarState { 23 | pub fn new() -> Self { 24 | Self { 25 | changed: HashMap::new(), 26 | } 27 | } 28 | 29 | #[allow(dead_code)] 30 | pub fn empty() -> Self { 31 | let mut state = Self::new(); 32 | state.change("PATH", None); 33 | for env_var in ["VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON3", "PY_PYTHON2"].iter() { 34 | state.change(env_var, None); 35 | } 36 | 37 | state 38 | } 39 | 40 | pub fn change(&mut self, k: &str, v: Option<&str>) { 41 | let os_k = OsStr::new(k); 42 | if !self.changed.contains_key(os_k) { 43 | let original_v = env::var_os(k); 44 | self.changed.insert(os_k.to_os_string(), original_v); 45 | } 46 | match v { 47 | Some(new_v) => env::set_var(k, new_v), 48 | None => env::remove_var(k), 49 | } 50 | } 51 | } 52 | 53 | #[allow(dead_code)] 54 | pub struct CurrentDir { 55 | _original_dir: PathBuf, 56 | pub dir: TempDir, 57 | } 58 | 59 | impl Drop for CurrentDir { 60 | fn drop(&mut self) { 61 | let _ = env::set_current_dir(&self._original_dir); 62 | } 63 | } 64 | 65 | impl CurrentDir { 66 | #[allow(dead_code)] 67 | pub fn new() -> Self { 68 | let _original_dir = env::current_dir().unwrap(); 69 | let dir = TempDir::new().unwrap(); 70 | env::set_current_dir(dir.path()).unwrap(); 71 | Self { _original_dir, dir } 72 | } 73 | } 74 | 75 | pub fn touch_file(path: PathBuf) -> PathBuf { 76 | let file = File::create(&path).unwrap(); 77 | file.sync_all().unwrap(); 78 | path 79 | } 80 | 81 | #[allow(dead_code)] 82 | pub struct EnvState { 83 | _dir1: TempDir, 84 | _dir2: TempDir, 85 | pub env_vars: EnvVarState, 86 | pub python27: PathBuf, 87 | pub python36: PathBuf, 88 | pub python37: PathBuf, 89 | } 90 | 91 | impl EnvState { 92 | /// Create a testing environment within the OS. 93 | /// - Create two temp directories (referred to as `dir1` and `dir2` from now on) 94 | /// - `dir1/python2.7` 95 | /// - `dir1/python3.6` 96 | /// - `dir2/python3.6` 97 | /// - `dir2/python3.7` 98 | /// - `PATH` environment variable is set to `dir1` and `dir2` 99 | /// - `VIRTUAL_ENV` is unset 100 | /// - `PY_PYTHON` is unset 101 | /// - `PY_PYTHON3` is unset 102 | /// - `PY_PYTHON2` is unset 103 | #[allow(dead_code)] 104 | pub fn new() -> Self { 105 | let dir1 = TempDir::new().unwrap(); 106 | let dir2 = TempDir::new().unwrap(); 107 | 108 | let python27 = touch_file(dir1.path().join("python2.7")); 109 | let python36 = touch_file(dir1.path().join("python3.6")); 110 | touch_file(dir2.path().join("python3.6")); 111 | let python37 = touch_file(dir2.path().join("python3.7")); 112 | 113 | let new_path = env::join_paths([dir1.path(), dir2.path()].iter()).unwrap(); 114 | let mut env_changes = EnvVarState::new(); 115 | env_changes.change("PATH", Some(new_path.to_str().unwrap())); 116 | for env_var in ["VIRTUAL_ENV", "PY_PYTHON", "PY_PYTHON3", "PY_PYTHON2"].iter() { 117 | env_changes.change(env_var, None); 118 | } 119 | 120 | Self { 121 | _dir1: dir1, 122 | _dir2: dir2, 123 | env_vars: env_changes, 124 | python27, 125 | python36, 126 | python37, 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/main_tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::items_after_test_module)] 2 | 3 | mod common; 4 | 5 | use std::path::PathBuf; 6 | 7 | use common::CurrentDir; 8 | 9 | use python_launcher::{ExactVersion, RequestedVersion}; 10 | 11 | use assert_cmd::Command; 12 | use predicates::str; 13 | use test_case::test_case; 14 | 15 | fn py_executable() -> Command { 16 | Command::cargo_bin("py").expect("binary 'py' not found") 17 | } 18 | 19 | #[test_case("-h"; "short")] 20 | #[test_case("--help"; "long")] 21 | fn help_flags(help_flag: &str) { 22 | let python = python_launcher::find_executable(RequestedVersion::Any) 23 | .expect("no Python executable found"); 24 | let result = py_executable().arg(help_flag).assert(); 25 | 26 | result 27 | .success() 28 | .stdout(str::contains(python.to_string_lossy())) 29 | .stderr(str::is_empty()); 30 | } 31 | 32 | #[test] 33 | fn list_output() { 34 | let pythons = python_launcher::all_executables(); 35 | let mut result = py_executable().arg("--list").assert(); 36 | 37 | result = result.success(); 38 | 39 | for (version, path) in pythons.iter() { 40 | result = result 41 | .stdout(str::contains(version.to_string())) 42 | .stdout(str::contains(path.to_string_lossy())); 43 | } 44 | } 45 | 46 | #[test] 47 | fn any_version() { 48 | let python = python_launcher::find_executable(RequestedVersion::Any) 49 | .expect("no Python executable found"); 50 | let version = ExactVersion::from_path(&python).unwrap(); 51 | let result = py_executable() 52 | .args(["-c", "import sys; print(sys.version)"]) 53 | .assert(); 54 | 55 | result 56 | .success() 57 | .stdout(str::starts_with(version.to_string())) 58 | .stderr(str::is_empty()); 59 | } 60 | 61 | #[test] 62 | fn major_version() { 63 | let python = python_launcher::find_executable(RequestedVersion::Any) 64 | .expect("no Python executable found"); 65 | let version = ExactVersion::from_path(&python).unwrap(); 66 | let version_flag = format!("-{}", version.major); 67 | let result = py_executable() 68 | .args([ 69 | version_flag.as_str(), 70 | "-c", 71 | "import sys; print(sys.version)", 72 | ]) 73 | .assert(); 74 | 75 | result 76 | .success() 77 | .stdout(str::starts_with(version.to_string())) 78 | .stderr(str::is_empty()); 79 | } 80 | 81 | #[test] 82 | fn exact_version() { 83 | let python = python_launcher::find_executable(RequestedVersion::Any) 84 | .expect("no Python executable found"); 85 | let version = ExactVersion::from_path(&python).unwrap(); 86 | let version_flag = format!("-{version}"); 87 | let result = py_executable() 88 | .args([ 89 | version_flag.as_str(), 90 | "-c", 91 | "import sys; print(sys.version)", 92 | ]) 93 | .assert(); 94 | 95 | result 96 | .success() 97 | .stdout(str::starts_with(version.to_string())) 98 | .stderr(str::is_empty()); 99 | } 100 | 101 | #[test] 102 | fn logging_output() { 103 | let result = py_executable() 104 | .args(["-c", "pass"]) 105 | .env("PYLAUNCH_DEBUG", "1") 106 | .assert(); 107 | 108 | result 109 | .success() 110 | .stdout(str::is_empty()) 111 | .stderr(str::contains("Executing")); 112 | } 113 | 114 | #[test_case("3."; "invalid version specifier")] 115 | #[test_case("0.1"; "non-existent version")] 116 | fn version_failure(version: &str) { 117 | let flag = format!("-{version}"); 118 | let result = py_executable().arg(flag).assert(); 119 | 120 | result.failure().stdout(str::is_empty()); 121 | } 122 | 123 | #[test] 124 | fn nonexistent_activated_virtual_env_dir() { 125 | let result = py_executable() 126 | .env("VIRTUAL_ENV", "this does not exist") 127 | .assert(); 128 | 129 | result.failure().stdout(str::is_empty()); 130 | } 131 | 132 | #[test] 133 | fn empty_activated_virtual_env() { 134 | let cwd = CurrentDir::new(); 135 | let result = py_executable() 136 | .env("VIRTUAL_ENV", cwd.dir.path().as_os_str()) 137 | .assert(); 138 | 139 | result.failure(); 140 | } 141 | 142 | #[test] 143 | fn no_executable() { 144 | let cwd = CurrentDir::new(); 145 | let cwd_name = cwd.dir.path().as_os_str(); 146 | let fake_python = PathBuf::from("python0.1"); 147 | common::touch_file(fake_python); 148 | let result = py_executable().env("PATH", cwd_name).assert(); 149 | 150 | result.failure(); 151 | } 152 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | 8 | env: 9 | toolchain: stable 10 | 11 | jobs: 12 | details: 13 | name: Release details 14 | runs-on: ubuntu-latest 15 | 16 | outputs: 17 | version: ${{ steps.version.outputs.version }} 18 | tag: ${{ steps.version.outputs.tag }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | # OS-specific 24 | - uses: actions/cache@v4 25 | with: 26 | path: ~/.cache/pip 27 | key: ${{ runner.os }}-pip-${{ hashFiles('release/requirements.txt') }} 28 | restore-keys: | 29 | ${{ runner.os }}-pip- 30 | 31 | - name: Install Python 32 | uses: actions/setup-python@v5 33 | with: 34 | python-version: 3.x 35 | 36 | - name: Install dependencies 37 | run: python -m pip install -r release/requirements.txt 38 | shell: bash 39 | 40 | - name: Version details 41 | id: version 42 | run: | 43 | echo "version=`python -m release version`" >> $GITHUB_OUTPUT 44 | echo "tag=`python -m release version --tag`" >> $GITHUB_OUTPUT 45 | shell: bash 46 | 47 | release: 48 | name: GitHub release 49 | needs: details 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | # Necessary to implicitly specify the repo to create the release for. 54 | - uses: actions/checkout@v4 55 | 56 | - name: Create release 57 | run: gh release create ${{ needs.details.outputs.tag }} --draft --generate-notes 58 | shell: bash 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | build: 63 | name: Build ${{ matrix.target }} 64 | needs: [details, release] 65 | runs-on: ${{ matrix.os }} 66 | 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | # https://doc.rust-lang.org/stable/rustc/platform-support.html 71 | # https://github.com/rust-embedded/cross#supported-targets 72 | include: 73 | - target: aarch64-apple-darwin 74 | os: macos-latest 75 | use-cross: false 76 | - target: x86_64-apple-darwin 77 | os: macos-latest 78 | use-cross: false 79 | - target: x86_64-unknown-linux-gnu 80 | os: ubuntu-latest 81 | use-cross: false 82 | - target: aarch64-unknown-linux-gnu 83 | os: ubuntu-latest 84 | use-cross: true 85 | - target: riscv64gc-unknown-linux-gnu 86 | os: ubuntu-latest 87 | use-cross: true 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | 92 | # OS-specific 93 | - uses: actions/cache@v4 94 | with: 95 | path: ~/.cache/pip 96 | key: ${{ runner.os }}-pip-${{ hashFiles('release/requirements.txt') }} 97 | restore-keys: | 98 | ${{ runner.os }}-pip- 99 | 100 | - name: Install Python 101 | uses: actions/setup-python@v5 102 | with: 103 | python-version: 3.x 104 | 105 | - name: Install dependencies 106 | run: python -m pip install -r release/requirements.txt 107 | shell: bash 108 | 109 | - name: Install Rust toolchain 110 | uses: actions-rs/toolchain@v1 111 | with: 112 | toolchain: ${{ env.toolchain }} 113 | target: ${{ matrix.target }} 114 | override: true 115 | profile: minimal 116 | 117 | - name: Cargo build 118 | uses: actions-rs/cargo@v1 119 | with: 120 | toolchain: ${{ env.toolchain }} 121 | use-cross: ${{ matrix.use-cross }} 122 | command: build 123 | args: --release --target ${{ matrix.target }} 124 | 125 | - name: Create tarball 126 | run: | 127 | TARBALL_PATH=`python -m release tarball --target ${{ matrix.target }}` 128 | du -h $TARBALL_PATH 129 | tar -tvf $TARBALL_PATH 130 | echo "tarball_path=$TARBALL_PATH" >> $GITHUB_ENV 131 | shell: bash 132 | 133 | - name: Upload tarball 134 | run: gh release upload ${{ needs.details.outputs.tag }} ${{ env.tarball_path }} --clobber 135 | shell: bash 136 | env: 137 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 138 | 139 | publish: 140 | name: Publish to crates.io 141 | needs: release 142 | runs-on: ubuntu-latest 143 | 144 | steps: 145 | - uses: actions/checkout@v4 146 | 147 | - name: Install Rust toolchain 148 | uses: actions-rs/toolchain@v1 149 | with: 150 | toolchain: ${{ env.toolchain }} 151 | override: true 152 | profile: minimal 153 | 154 | - name: Publish 155 | run: cargo publish --locked 156 | env: 157 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 158 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | brett@python.org. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 27 | 28 | 29 | There are three ways to install the Python Launcher for Unix: 30 | 31 | 1. A supported package manager 32 | 2. A pre-built binary available from the project's [releases page](https://github.com/brettcannon/python-launcher/releases) 33 | 3. From source 34 | 35 | Which option is available and best for you will depend on your operating system and your own personal preferences. 36 | 37 | [![Packaging status](https://repology.org/badge/vertical-allrepos/python-launcher.svg)](https://repology.org/project/python-launcher/versions) 38 | 39 | ## Linux 40 | 41 | ### [Linuxbrew](https://docs.brew.sh/Homebrew-on-Linux) 42 | 43 | ```console 44 | brew install python-launcher 45 | ``` 46 | 47 | - https://formulae.brew.sh/formula/python-launcher 48 | 49 | ### [Arch](https://archlinux.org/) 50 | 51 | ```console 52 | yay -S python-launcher 53 | ``` 54 | 55 | - https://aur.archlinux.org/packages/python-launcher 56 | 57 | ### [Fedora](https://getfedora.org/) 58 | 59 | ```console 60 | sudo dnf install python-launcher 61 | ``` 62 | 63 | Requires Fedora 34 or higher. 64 | 65 | - https://src.fedoraproject.org/rpms/rust-python-launcher/ 66 | 67 | ### [NixOS](https://nixos.org/) 68 | 69 | To try the Launcher out: 70 | ```console 71 | nix-shell -p python-launcher 72 | ``` 73 | 74 | - https://search.nixos.org/packages?type=packages&query=python-launcher 75 | - https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/development/tools/misc/python-launcher/default.nix 76 | 77 | ### Pre-built binaries 78 | 79 | #### RISC-V 80 | 83 | 84 | 1. [Download `python_launcher-1.0.1-riscv64gc-unknown-linux-gnu.tar.xz`](https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-riscv64gc-unknown-linux-gnu.tar.xz): 85 | 86 | ``` 87 | curl --location --remote-name https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-riscv64gc-unknown-linux-gnu.tar.xz 88 | ``` 89 | 90 | 2. Install into, e.g. `/usr/local`: 91 | 92 | ``` 93 | tar --extract --strip-components 1 --directory /usr/local --file python_launcher-1.0.1-riscv64gc-unknown-linux-gnu.tar.xz 94 | ``` 95 | 96 | 97 | 98 | #### AArch64 99 | 102 | 103 | 1. [Download `python_launcher-1.0.1-aarch64-unknown-linux-gnu.tar.xz`](https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-aarch64-unknown-linux-gnu.tar.xz): 104 | 105 | ``` 106 | curl --location --remote-name https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-aarch64-unknown-linux-gnu.tar.xz 107 | ``` 108 | 109 | 2. Install into, e.g. `/usr/local`: 110 | 111 | ``` 112 | tar --extract --strip-components 1 --directory /usr/local --file python_launcher-1.0.1-aarch64-unknown-linux-gnu.tar.xz 113 | ``` 114 | 115 | 116 | 117 | #### x86-64 118 | 121 | 122 | 1. [Download `python_launcher-1.0.1-x86_64-unknown-linux-gnu.tar.xz`](https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-x86_64-unknown-linux-gnu.tar.xz): 123 | 124 | ``` 125 | curl --location --remote-name https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-x86_64-unknown-linux-gnu.tar.xz 126 | ``` 127 | 128 | 2. Install into, e.g. `/usr/local`: 129 | 130 | ``` 131 | tar --extract --strip-components 1 --directory /usr/local --file python_launcher-1.0.1-x86_64-unknown-linux-gnu.tar.xz 132 | ``` 133 | 134 | 135 | 136 | ## macOS 137 | 138 | ### [Homebrew](https://brew.sh/) 139 | 140 | ```console 141 | brew install python-launcher 142 | ``` 143 | 144 | - https://formulae.brew.sh/formula/python-launcher 145 | 146 | !!! note 147 | 148 | If you have multiple installs of Python via [Homebrew](https://brew.sh/) but 149 | they are not all being found (as verified via `py --list`), chances are Homebrew 150 | didn't symlink an installation due to the `python` symlink already being 151 | defined. For each installation you are missing you will need to tell Homebrew to 152 | ignore the conflict so that the version-specific `python` symlink gets created. 153 | 154 | For instance, if your Python 3.10 installation isn't being found (due to 155 | `python3.10` not existing), try running: 156 | 157 | ```console 158 | brew link --overwrite python@3.10 159 | ``` 160 | 161 | That will symlink the `python3.10` command. It will also overwrite 162 | what `python3` points at, meaning it may not point at the newest release of 163 | Python. Luckily the Python Launcher for Unix deals with this exact issue. 😁 164 | 165 | 166 | ### Pre-built binaries 167 | 168 | #### Apple Silicon 169 | 172 | 173 | 1. [Download `python_launcher-1.0.1-aarch64-apple-darwin.tar.xz`](https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-aarch64-apple-darwin.tar.xz): 174 | 175 | ``` 176 | curl --location --remote-name https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-aarch64-apple-darwin.tar.xz 177 | ``` 178 | 179 | 2. Install into, e.g. `/usr/local`: 180 | 181 | ``` 182 | tar --extract --strip-components 1 --directory /usr/local --file python_launcher-1.0.1-aarch64-apple-darwin.tar.xz 183 | ``` 184 | 185 | 186 | 187 | #### x86-64 188 | 191 | 192 | 1. [Download `python_launcher-1.0.1-x86_64-apple-darwin.tar.xz`](https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-x86_64-apple-darwin.tar.xz): 193 | 194 | ``` 195 | curl --location --remote-name https://github.com/brettcannon/python-launcher/releases/download/v1.0.1/python_launcher-1.0.1-x86_64-apple-darwin.tar.xz 196 | ``` 197 | 198 | 2. Install into, e.g. `/usr/local`: 199 | 200 | ``` 201 | tar --extract --strip-components 1 --directory /usr/local --file python_launcher-1.0.1-x86_64-apple-darwin.tar.xz 202 | ``` 203 | 204 | 205 | 206 | ## From source 207 | 208 | ### [Crates.io](https://crates.io) 209 | 210 | ```console 211 | cargo install python-launcher 212 | ``` 213 | 214 | - https://crates.io/crates/python-launcher 215 | 216 | ### Repository checkout 217 | 218 | ```console 219 | cargo install --path . 220 | ``` 221 | 222 | - https://github.com/brettcannon/python-launcher.git 223 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # The Python Launcher for Unix 2 | 3 | ## Why this tool exists 4 | 5 | The goal of the Python Launcher for Unix is to figure out what Python interpreter you want when your run the `py` command. Its preference is to find the interpreter using the newest version of Python that is as specific as possible to your current context. This allows you to not have to think about or perform extra work. 6 | 7 | ??? info "Differences from the Python Launcher for Windows" 8 | 9 | While similar, the Python Launcher for Unix differs from the [official Windows Launcher](https://docs.python.org/3/using/windows.html#launcher) a few key ways (beyond what operating systems they support): 10 | 11 | 1. This project is not official (it's a [personal](https://github.com/brettcannon/) project) 12 | 2. This project is not shipped with CPython (see the [installation instructions](install.md)) 13 | 3. No support for `-V:`/`-version:` (Windows-specific) 14 | 4. No support for `py.ini` files (use [environment variables](cli.md#environment-variables) instead) 15 | 16 | ## A common scenario 17 | 18 | The Python Launcher is optimized for a workflow where you create a virtual environment for each project in its directory/workspace (although it is not restricted to this workflow). When starting a project, use the Python Launcher to create a virtual environment in a `.venv` directory: 19 | 20 | ```console 21 | py -m venv .venv 22 | ``` 23 | 24 | This will create a virtual environment in the `.venv` directory using the newest version of Python that the Python Launcher can find. Subsequent uses of `py` will then use that virtual environment as long as it is in the current (or higher) directory. 25 | 26 | ```console 27 | py -c "import sys; print(sys.version); print(); print(sys.executable)" 28 | ``` 29 | 30 | While this is the most common scenario, the Python Launcher is not limited to this. It can be used in other ways to select the appropriate Python interpreter for you. 31 | 32 | ## Advantages over custom solutions 33 | 34 | Many people have taken the time to develop their own solutions for launching the appropriate Python interpreter for the situation. Typically this takes the form of some custom shell integration that does something specific/magical based on some context in the current directory. The Python Launcher, on the other hand, is meant to be a general solution that works for all situations and is not tied to any specific shell. That way, if you haven't put in the time and effort to develop your own solution to simplifying how you launch a Python interpreter, the hope is the Python Launcher can fill that need for you. 35 | 36 | If you already have a custom workflow set up which works for you, then great! You probably don't have a direct need for the Python Launcher and should continue to use the workflow that works for you. But if you don't have a custom solution and want to simplify how you launch Python interpreters, then the Python Launcher is a good option to consider. 37 | 38 | ## Non-goals 39 | 40 | A non-goal of this project is to become the way to launch the Python 41 | interpreter _all the time_. If you know the exact interpreter you want to launch 42 | then you should launch it directly; same goes for when you have 43 | requirements on the type of interpreter you want (e.g. 32-bit, framework build 44 | on macOS, etc.). The Python Launcher should be viewed as a tool of convenience, 45 | not necessity. 46 | 47 | ## Selecting a Python interpreter 48 | 49 | The Python Launcher tries to launch the correct Python interpreter for your current situation. 50 | 51 | The Python Launcher will always prefer a Python interpreter associated with the current context over a globally installed interpreter (e.g. a virtual environment in the current directory is better than an interpreter on `PATH`). Beyond that, the Python interpreter always prefers the newest Python version available. 52 | 53 | To accomplish all of this is a two-step process of gathering inputs in its decision-making process: 54 | 55 | 1. The Python Launcher looking at your current context to gather a list of possible interpreters. 56 | 2. You specifying any restrictions on the interpreter you want to use to filter that list down. 57 | 58 | ### Specifying interpreter requirements 59 | 60 | The Python Launcher lets you restrict, to varying specificity, what Python version you are after. If you specify nothing then the Python Launcher will simply choose the interpreter with the newest Python version. But you can also specify a major version or a major and minor version to restrict potential Python versions. 61 | 62 | #### On the command line 63 | 64 | The `py` command supports a single flag that Python itself does not: a version restriction. The argument can take one of two forms: 65 | 66 | 1. Major version restriction, e.g. `-3` for Python 3.x. 67 | 2. Major and minor version restriction, e.g. `-3.6` for Python 3.6. 68 | 69 | If no such argument is provided, the Python Launcher assumes **any** Python version is acceptable. If such an argument is provided it **must** be the first argument to `py` (i.e. before any other arguments; `py -3.11 -c "import sys; print(sys.executable)"`). 70 | 71 | #### Environment variables 72 | 73 | The Python Launcher also supports environment variables to specify the Python versions that are acceptable. Which environment variable is used is dependent upon what/whether a [command line argument](#on-the-command-line) was specified to restrict the Python version. If no argument is specified then the `PY_PYTHON` environment variable is used. If a major version argument is specified then that version number is appended to the environment variable name that is used, e.g. the `PY_PYTHON3` environment variable is used if `-3` was specified. 74 | 75 | !!! note 76 | 77 | There is no environment variable for a major and minor version restriction. This is because the Python Launcher does not support a more specific restriction than a major and minor version. 78 | 79 | 80 | The format of the environment variable is similar to the command line argument: a major or major and minor version to restrict what Python interpreters are considered acceptable. As an example, setting `PY_PYTHON` to `3.11` means you want a Python 3.11 interpreter. Setting `PY_PYTHON` to `3` means you want any Python 3 interpreter. 81 | 82 | !!! tip 83 | If you have an in-development version of Python installed (i.e. an alpha, beta, or rc release), but you don't want the Python Launcher to select it by default, set `PY_PYTHON` to newest _stable_ version of Python you have installed. For instance, if you have Python 3.12.0a3 installed, but want to use Python 3.11.1 day-to-day, then set `PY_PYTHON` to `3.11`. 84 | 85 | ### Searching for interpreters 86 | 87 | The Python Launcher searches for interpreters based on its current context. That involves looking locally, then globally, for Python interpreters. 88 | 89 | #### Activated virtual environment 90 | 91 | If you have an activated virtual environment, the `py` command will immediately use that. This is determined by the `VIRTUAL_ENV` environment variable that is set by the `activate` script of the virtual environment. 92 | 93 | !!! note 94 | In general, this feature is not needed. If you create a virtual environment in the current directory in a `.venv` directory, the Python Launcher will automatically use that. This is discussed in more detail [below](#local-virtual-environment). 95 | 96 | #### Local virtual environment 97 | 98 | The Python Launcher will search the current directory for a `.venv` directory. If it finds one and it contains a virtual environment, it will use that Python interpreter. Otherwise it will search the parent directory, and so on, until it finds a `.venv` directory or reaches the root of the filesystem. 99 | 100 | #### `PATH` 101 | 102 | If no local virtual environment is found, the Python Launcher will search the `PATH` environment variable for a Python interpreter. The Python Launcher will search for the newest Python interpreter that meets the [version restriction](#specifying-interpreter-requirements). When the same Python version is available in multiple directories on `PATH`, the Python Launcher will use the first one it finds. 103 | 104 | ## Determining the selected interpreter 105 | 106 | The easiest way to tell what Python interpreter the Python Launcher will select is to lean on the fact the `py` command passes its arguments (other than any [version restriction argument](#on-the-command-line)) on to the selected Python interpreter. 107 | 108 | To print out the `sys.executable` attribute of the interpreter: 109 | 110 | ```console 111 | py -c "import sys; print(sys.executable)" 112 | ``` 113 | 114 | If you simply care about the Python version, you can use `--version` flag: 115 | 116 | ```console 117 | py --version 118 | ``` 119 | 120 | ## Diagram of how the Python Launcher selects a Python interpreter 121 | 122 | The Python Launcher follows the logic drawn out below for selecting the appropriate Python interpreter (with Python 3.6, Python 3, and the newest version of Python installed as examples): 123 | 124 | ```mermaid 125 | graph TD 126 | start[py ...] --> major_minor[-3.6] 127 | start --> major_only[-3] 128 | start --> best[No option specified] 129 | 130 | major_minor --> $PATH 131 | 132 | major_only --> PY_PYTHON3 133 | 134 | $PATH --> exec 135 | 136 | best --> env_var{$VIRTUAL_ENV} 137 | 138 | env_var --> venv_dir{.venv} 139 | 140 | venv_dir --> shebang{#! ...} 141 | venv_dir --> exec 142 | 143 | shebang --> PY_PYTHON([$PY_PYTHON]) 144 | shebang --> PY_PYTHON3([$PY_PYTHON3]) 145 | 146 | PY_PYTHON3 --> $PATH 147 | PY_PYTHON --> $PATH 148 | shebang --> $PATH 149 | 150 | env_var --> exec[Execute Python] 151 | ``` 152 | -------------------------------------------------------------------------------- /tests/cli_system_tests.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use std::env; 4 | use std::fs; 5 | use std::fs::File; 6 | use std::io::Write; 7 | use std::path::PathBuf; 8 | 9 | use serial_test::serial; 10 | 11 | use python_launcher::cli; 12 | use python_launcher::cli::Action; 13 | use python_launcher::Error; 14 | use python_launcher::RequestedVersion; 15 | 16 | use common::{EnvState, EnvVarState}; 17 | 18 | #[test] 19 | #[serial] 20 | fn from_main_help() { 21 | let env_state = EnvState::new(); 22 | for flag in ["-h", "--help"].iter() { 23 | let launcher_path = "/path/to/py"; 24 | 25 | match Action::from_main(&[launcher_path.to_string(), (*flag).to_string()]) { 26 | Ok(Action::Help(message, python_path)) => { 27 | assert!(message.contains(launcher_path)); 28 | assert_eq!(env_state.python37, python_path); 29 | assert!(message.contains(python_path.to_str().unwrap())); 30 | } 31 | _ => panic!("{flag:?} flag did not return Action::Help"), 32 | } 33 | } 34 | } 35 | 36 | #[test] 37 | #[serial] 38 | fn from_main_help_missing_interpreter() { 39 | let _state = EnvVarState::empty(); 40 | for flag in ["-h", "--help"].iter() { 41 | let launcher_path = "/path/to/py"; 42 | let help = Action::from_main(&[launcher_path.to_string(), (*flag).to_string()]); 43 | assert_eq!( 44 | help, 45 | Err(crate::Error::NoExecutableFound(RequestedVersion::Any)) 46 | ); 47 | } 48 | } 49 | 50 | #[test] 51 | #[serial] 52 | fn from_main_list() { 53 | let env_state = EnvState::new(); 54 | 55 | match Action::from_main(&["/path/to/py".to_string(), "--list".to_string()]) { 56 | Ok(Action::List(output)) => { 57 | assert!(output.contains(env_state.python27.to_str().unwrap())); 58 | assert!(output.contains(env_state.python36.to_str().unwrap())); 59 | assert!(output.contains(env_state.python37.to_str().unwrap())); 60 | } 61 | _ => panic!("'--list' did not return Action::List"), 62 | } 63 | } 64 | 65 | #[test] 66 | #[serial] 67 | fn from_main_by_flag() { 68 | let _working_dir = common::CurrentDir::new(); 69 | let env_state = common::EnvState::new(); 70 | let launcher_location = "/path/to/py".to_string(); 71 | let no_argv = Action::from_main(std::slice::from_ref(&launcher_location)); 72 | 73 | match no_argv { 74 | Ok(Action::Execute { 75 | launcher_path, 76 | executable, 77 | args, 78 | }) => { 79 | assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path); 80 | assert_eq!(executable, env_state.python37); 81 | assert_eq!(args.len(), 0); 82 | } 83 | Ok(Action::Help(_, _)) => panic!("Got back help"), 84 | Ok(Action::List(_)) => panic!("Got back a list of executables"), 85 | Err(error) => panic!("No executable found in default case: {error:?}"), 86 | } 87 | 88 | match Action::from_main(&[launcher_location.clone(), "-2".to_string()]) { 89 | Ok(Action::Execute { 90 | launcher_path, 91 | executable, 92 | args, 93 | }) => { 94 | assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path); 95 | assert_eq!(executable, env_state.python27); 96 | assert_eq!(args.len(), 0); 97 | } 98 | _ => panic!("No executable found in `-3` case"), 99 | } 100 | 101 | match Action::from_main(&[launcher_location.clone(), "-3.6".to_string()]) { 102 | Ok(Action::Execute { 103 | launcher_path, 104 | executable, 105 | args, 106 | }) => { 107 | assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path); 108 | assert_eq!(executable, env_state.python36); 109 | assert_eq!(args.len(), 0); 110 | } 111 | _ => panic!("No executable found in `-3.6` case"), 112 | } 113 | 114 | match Action::from_main(&[ 115 | launcher_location.clone(), 116 | "-3.6".to_string(), 117 | "-I".to_string(), 118 | ]) { 119 | Ok(Action::Execute { 120 | launcher_path, 121 | executable, 122 | args, 123 | }) => { 124 | assert_eq!(PathBuf::from(launcher_location), launcher_path); 125 | assert_eq!(executable, env_state.python36); 126 | assert_eq!(args, ["-I".to_string()]); 127 | } 128 | _ => panic!("No executable found in `-3.6` case"), 129 | } 130 | } 131 | 132 | #[test] 133 | #[serial] 134 | fn from_main_activated_virtual_env() { 135 | let venv_path = "/path/to/venv"; 136 | let mut env_state = common::EnvState::new(); 137 | env_state.env_vars.change("VIRTUAL_ENV", Some(venv_path)); 138 | 139 | match Action::from_main(&["/path/to/py".to_string()]) { 140 | Ok(Action::Execute { executable, .. }) => { 141 | let mut expected = PathBuf::from(venv_path); 142 | expected.push("bin"); 143 | expected.push("python"); 144 | assert_eq!(executable, expected); 145 | } 146 | _ => panic!("No executable found in `VIRTUAL_ENV` case"), 147 | } 148 | 149 | // VIRTUAL_ENV gets ignored if any specific version is requested. 150 | match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) { 151 | Ok(Action::Execute { executable, .. }) => { 152 | assert_eq!(executable, env_state.python37); 153 | } 154 | _ => panic!("No executable found in `VIRTUAL_ENV` case"), 155 | } 156 | } 157 | 158 | #[test] 159 | #[serial] 160 | fn from_main_default_cwd_venv_path() { 161 | let _working_dir = common::CurrentDir::new(); 162 | let env_state = common::EnvState::new(); 163 | let mut expected = PathBuf::new(); 164 | expected.push(cli::DEFAULT_VENV_DIR); 165 | expected.push("bin"); 166 | fs::create_dir_all(&expected).unwrap(); 167 | expected.push("python"); 168 | common::touch_file(expected.clone()); 169 | 170 | match Action::from_main(&["/path/to/py".to_string()]) { 171 | Ok(Action::Execute { executable, .. }) => { 172 | assert_eq!(executable, expected.canonicalize().unwrap()); 173 | } 174 | _ => panic!("No executable found in default virtual environment case"), 175 | } 176 | 177 | // VIRTUAL_ENV gets ignored if any specific version is requested. 178 | match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) { 179 | Ok(Action::Execute { executable, .. }) => { 180 | assert_eq!(executable, env_state.python37); 181 | } 182 | _ => panic!("No executable found in default virtual environment case"), 183 | } 184 | } 185 | 186 | #[test] 187 | #[serial] 188 | fn from_main_default_parent_venv_path() { 189 | let working_dir = common::CurrentDir::new(); 190 | let temp_dir = working_dir.dir.path().to_path_buf(); 191 | let env_state = common::EnvState::new(); 192 | let mut expected = temp_dir.clone(); 193 | expected.push(cli::DEFAULT_VENV_DIR); 194 | expected.push("bin"); 195 | fs::create_dir_all(&expected).unwrap(); 196 | expected.push("python"); 197 | common::touch_file(expected.clone()); 198 | 199 | let subdir = temp_dir.join("subdir"); 200 | fs::create_dir(&subdir).unwrap(); 201 | env::set_current_dir(&subdir).unwrap(); 202 | 203 | match Action::from_main(&["/path/to/py".to_string()]) { 204 | Ok(Action::Execute { executable, .. }) => { 205 | assert_eq!(executable, expected.canonicalize().unwrap()); 206 | } 207 | _ => panic!("No executable found in default virtual environment case"), 208 | } 209 | 210 | // VIRTUAL_ENV gets ignored if any specific version is requested. 211 | match Action::from_main(&["/path/to/py".to_string(), "-3".to_string()]) { 212 | Ok(Action::Execute { executable, .. }) => { 213 | assert_eq!(executable, env_state.python37); 214 | } 215 | _ => panic!("No executable found in default virtual environment case"), 216 | } 217 | } 218 | 219 | #[test] 220 | #[serial] 221 | fn from_main_shebang() { 222 | let _working_dir = common::CurrentDir::new(); 223 | let env_state = common::EnvState::new(); 224 | let temp_dir = tempfile::tempdir().unwrap(); 225 | let file_path = temp_dir.path().join("shebang.py"); 226 | let mut file = File::create(&file_path).unwrap(); 227 | writeln!(file, "#! /usr/bin/env python2.7").unwrap(); 228 | 229 | match Action::from_main(&[ 230 | "/path/to/py".to_string(), 231 | file_path.to_str().unwrap().to_string(), 232 | ]) { 233 | Ok(Action::Execute { executable, .. }) => { 234 | assert_eq!(executable, env_state.python27); 235 | } 236 | _ => panic!("No executable found in shebang case"), 237 | } 238 | 239 | // Shebang checking only works for the first argument to avoid accidentally 240 | // reading from arguments to Python code itself. 241 | match Action::from_main(&[ 242 | "/path/to/py".to_string(), 243 | "-m".to_string(), 244 | "my_app".to_string(), 245 | file_path.to_str().unwrap().to_string(), 246 | ]) { 247 | Ok(Action::Execute { executable, .. }) => { 248 | assert_eq!(executable, env_state.python37); 249 | } 250 | _ => panic!("No executable found in shebang case"), 251 | } 252 | } 253 | 254 | #[test] 255 | #[serial] 256 | fn from_main_env_var() { 257 | let _working_dir = common::CurrentDir::new(); 258 | let mut env_state = common::EnvState::new(); 259 | env_state.env_vars.change("PY_PYTHON", Some("3.6")); 260 | let launcher_location = "/path/to/py".to_string(); 261 | 262 | match Action::from_main(std::slice::from_ref(&launcher_location)) { 263 | Ok(Action::Execute { 264 | launcher_path, 265 | executable, 266 | args, 267 | }) => { 268 | assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path); 269 | assert_eq!(executable, env_state.python36); 270 | assert_eq!(args.len(), 0); 271 | } 272 | _ => panic!("No executable found in PY_PYTHON case"), 273 | } 274 | 275 | env_state.env_vars.change("PY_PYTHON3", Some("3.6")); 276 | 277 | match Action::from_main(&[launcher_location.clone(), "-3".to_string()]) { 278 | Ok(Action::Execute { 279 | launcher_path, 280 | executable, 281 | args, 282 | }) => { 283 | assert_eq!(PathBuf::from(launcher_location.clone()), launcher_path); 284 | assert_eq!(executable, env_state.python36); 285 | assert_eq!(args.len(), 0); 286 | } 287 | _ => panic!("No executable found in PY_PYTHON3 case"), 288 | } 289 | 290 | env_state.env_vars.change("PY_PYTHON3", Some("3.8.10")); 291 | 292 | if Action::from_main(&[launcher_location, "-3".to_string()]).is_ok() { 293 | panic!("Invalid PY_PYTHON3 did not error out"); 294 | } 295 | } 296 | 297 | #[test] 298 | #[serial] 299 | fn from_main_no_executable_found() { 300 | let _env_state = common::EnvState::new(); 301 | assert_eq!( 302 | Action::from_main(&["/path/to/py".to_string(), "-42.13".to_string()]), 303 | Err(Error::NoExecutableFound(RequestedVersion::Exact(42, 13))) 304 | ); 305 | } 306 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Parsing of CLI flags 2 | //! 3 | //! The [`Action`] enum represents what action to perform based on the 4 | //! command-line arguments passed to the program. 5 | 6 | use std::{ 7 | collections::HashMap, 8 | env, 9 | fmt::Write, 10 | fs::File, 11 | io::{BufRead, BufReader, Read}, 12 | path::{Path, PathBuf}, 13 | str::FromStr, 14 | string::ToString, 15 | }; 16 | 17 | use comfy_table::{Table, TableComponent}; 18 | 19 | use crate::{ExactVersion, RequestedVersion}; 20 | 21 | /// The expected directory name for virtual environments. 22 | pub static DEFAULT_VENV_DIR: &str = ".venv"; 23 | 24 | /// Represents the possible outcomes based on CLI arguments. 25 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 26 | pub enum Action { 27 | /// The help string for the Python Launcher along with the path to a Python 28 | /// executable. 29 | /// 30 | /// The executable path is so that it can be executed with `-h` to append 31 | /// Python's own help output. 32 | Help(String, PathBuf), 33 | /// A string listing all found executables on `PATH`. 34 | /// 35 | /// The string is formatted to be human-readable. 36 | List(String), 37 | /// Details for executing a Python executable. 38 | Execute { 39 | /// The Python Launcher used to find the Python executable. 40 | launcher_path: PathBuf, 41 | /// The Python executable to run. 42 | executable: PathBuf, 43 | /// Arguments to the executable. 44 | args: Vec, 45 | }, 46 | } 47 | 48 | impl Action { 49 | /// Parses CLI arguments to determine what action should be taken. 50 | /// 51 | /// The first argument -- `argv[0]` -- is considered the path to the 52 | /// Launcher itself (i.e. [`Action::Execute::launcher_path`]). 53 | /// 54 | /// The second argument -- `argv.get(1)` -- is used to determine if/what 55 | /// argument has been provided for the Launcher. 56 | /// 57 | /// # Launcher Arguments 58 | /// 59 | /// ## `-h`/`--help` 60 | /// 61 | /// Returns [`Action::Help`]. 62 | /// 63 | /// The search for the Python executable to use is done using 64 | /// [`crate::find_executable`] with an [`RequestedVersion::Any`] argument. 65 | /// 66 | /// ## `--list` 67 | /// 68 | /// Returns [`Action::List`]. 69 | /// 70 | /// The list of executable is gathered via [`crate::all_executables`]. 71 | /// 72 | /// ## Version Restriction 73 | /// 74 | /// Returns the appropriate [`Action::Execute`] instance for the requested 75 | /// Python version. 76 | /// 77 | /// [`crate::find_executable`] is used to perform the search. 78 | /// 79 | /// ## No Arguments for the Launcher 80 | /// 81 | /// Returns an [`Action::Execute`] instance. 82 | /// 83 | /// As a first step, a check is done for an activated virtual environment 84 | /// via the `VIRTUAL_ENV` environment variable. If none is set, look for a 85 | /// virtual environment in a directory named by [`DEFAULT_VENV_DIR`] in the 86 | /// current or any parent directories. 87 | /// 88 | /// If no virtual environment is found, a shebang line is searched for in 89 | /// the first argument to the Python interpreter. If one is found then it 90 | /// is used to (potentially) restrict the requested version searched for. 91 | /// 92 | /// The search for an interpreter proceeds using [`crate::find_executable`]. 93 | /// 94 | /// # Errors 95 | /// 96 | /// If `-h`, `--help`, or `--list` are specified as the first argument but 97 | /// there are other arguments, [`crate::Error::IllegalArgument`] is returned. 98 | /// 99 | /// If no executable could be found for [`Action::Help`] or 100 | /// [`Action::List`], [`crate::Error::NoExecutableFound`] is returned. 101 | /// 102 | /// # Panics 103 | /// 104 | /// - If a [`writeln!`] call fails. 105 | /// - If the current directory cannot be accessed. 106 | pub fn from_main(argv: &[String]) -> crate::Result { 107 | let launcher_path = PathBuf::from(&argv[0]); // Strip the path to this executable. 108 | 109 | match argv.get(1) { 110 | Some(flag) if flag == "-h" || flag == "--help" || flag == "--list" => { 111 | if argv.len() > 2 { 112 | Err(crate::Error::IllegalArgument( 113 | launcher_path, 114 | flag.to_string(), 115 | )) 116 | } else if flag == "--list" { 117 | Ok(Action::List(list_executables(&crate::all_executables())?)) 118 | } else { 119 | crate::find_executable(RequestedVersion::Any) 120 | .ok_or(crate::Error::NoExecutableFound(RequestedVersion::Any)) 121 | .map(|executable_path| { 122 | Action::Help( 123 | help_message(&launcher_path, &executable_path), 124 | executable_path, 125 | ) 126 | }) 127 | } 128 | } 129 | Some(version) if version_from_flag(version).is_some() => { 130 | Ok(Action::Execute { 131 | launcher_path, 132 | // Make sure to skip the app path and version specification. 133 | executable: find_executable(version_from_flag(version).unwrap(), &argv[2..])?, 134 | args: argv[2..].to_vec(), 135 | }) 136 | } 137 | Some(_) | None => Ok(Action::Execute { 138 | launcher_path, 139 | // Make sure to skip the app path. 140 | executable: find_executable(RequestedVersion::Any, &argv[1..])?, 141 | args: argv[1..].to_vec(), 142 | }), 143 | } 144 | } 145 | } 146 | 147 | fn help_message(launcher_path: &Path, executable_path: &Path) -> String { 148 | let mut message = String::new(); 149 | writeln!( 150 | message, 151 | include_str!("HELP.txt"), 152 | env!("CARGO_PKG_VERSION"), 153 | launcher_path.to_string_lossy(), 154 | executable_path.to_string_lossy() 155 | ) 156 | .unwrap(); 157 | message 158 | } 159 | 160 | /// Attempts to find a version specifier from a CLI argument. 161 | /// 162 | /// It is assumed that the flag from the command-line is passed as-is 163 | /// (i.e. the flag starts with `-`). 164 | fn version_from_flag(arg: &str) -> Option { 165 | if !arg.starts_with('-') { 166 | None 167 | } else { 168 | RequestedVersion::from_str(&arg[1..]).ok() 169 | } 170 | } 171 | 172 | fn list_executables(executables: &HashMap) -> crate::Result { 173 | if executables.is_empty() { 174 | return Err(crate::Error::NoExecutableFound(RequestedVersion::Any)); 175 | } 176 | 177 | let mut executable_pairs = Vec::from_iter(executables); 178 | executable_pairs.sort_unstable(); 179 | executable_pairs.reverse(); 180 | 181 | let mut table = Table::new(); 182 | table.load_preset(comfy_table::presets::NOTHING); 183 | // Using U+2502/"Box Drawings Light Vertical" over 184 | // U+007C/"Vertical Line"/pipe simply because it looks better. 185 | // Leaving out a header and other decorations to make it easier 186 | // parse the output. 187 | table.set_style(TableComponent::VerticalLines, '│'); 188 | 189 | for (version, path) in executable_pairs { 190 | table.add_row(vec![version.to_string(), path.display().to_string()]); 191 | } 192 | 193 | Ok(table.to_string() + "\n") 194 | } 195 | 196 | fn relative_venv_path(add_default: bool) -> PathBuf { 197 | let mut path = PathBuf::new(); 198 | if add_default { 199 | path.push(DEFAULT_VENV_DIR); 200 | } 201 | path.push("bin"); 202 | path.push("python"); 203 | path 204 | } 205 | 206 | /// Returns the path to the activated virtual environment's executable. 207 | /// 208 | /// A virtual environment is determined to be activated based on the 209 | /// existence of the `VIRTUAL_ENV` environment variable. 210 | fn venv_executable_path(venv_root: &str) -> PathBuf { 211 | PathBuf::from(venv_root).join(relative_venv_path(false)) 212 | } 213 | 214 | fn activated_venv() -> Option { 215 | log::info!("Checking for VIRTUAL_ENV environment variable"); 216 | env::var_os("VIRTUAL_ENV").map(|venv_root| { 217 | log::debug!("VIRTUAL_ENV set to {venv_root:?}"); 218 | venv_executable_path(&venv_root.to_string_lossy()) 219 | }) 220 | } 221 | 222 | fn venv_path_search() -> Option { 223 | if env::current_dir().is_err() { 224 | log::warn!("current working directory is invalid"); 225 | None 226 | } else { 227 | let cwd = env::current_dir().unwrap(); 228 | let printable_cwd = cwd.display(); 229 | log::info!("Searching for a venv in {printable_cwd} and parent directories"); 230 | cwd.ancestors().find_map(|path| { 231 | let venv_path = path.join(relative_venv_path(true)); 232 | let printable_venv_path = venv_path.display(); 233 | log::info!("Checking {printable_venv_path}"); 234 | // bool::then_some() makes more sense, but still experimental. 235 | venv_path.is_file().then_some(venv_path) 236 | }) 237 | } 238 | } 239 | 240 | fn venv_executable() -> Option { 241 | activated_venv().or_else(venv_path_search) 242 | } 243 | 244 | // https://en.m.wikipedia.org/wiki/Shebang_(Unix) 245 | fn parse_python_shebang(reader: &mut impl Read) -> Option { 246 | let mut shebang_buffer = [0; 2]; 247 | log::info!("Looking for a Python-related shebang"); 248 | if reader.read(&mut shebang_buffer).is_err() || shebang_buffer != [0x23, 0x21] { 249 | // Doesn't start w/ `#!` in ASCII/UTF-8. 250 | log::debug!("No '#!' at the start of the first line of the file"); 251 | return None; 252 | } 253 | 254 | let mut buffered_reader = BufReader::new(reader); 255 | let mut first_line = String::new(); 256 | 257 | if buffered_reader.read_line(&mut first_line).is_err() { 258 | log::debug!("Can't read first line of the file"); 259 | return None; 260 | }; 261 | 262 | // Whitespace between `#!` and the path is allowed. 263 | let line = first_line.trim(); 264 | 265 | let accepted_paths = [ 266 | "python", 267 | "/usr/bin/python", 268 | "/usr/local/bin/python", 269 | "/usr/bin/env python", 270 | ]; 271 | 272 | for acceptable_path in &accepted_paths { 273 | if !line.starts_with(acceptable_path) { 274 | continue; 275 | } 276 | 277 | log::debug!("Found shebang: {acceptable_path}"); 278 | let version = line[acceptable_path.len()..].to_string(); 279 | log::debug!("Found version: {version}"); 280 | return RequestedVersion::from_str(&version).ok(); 281 | } 282 | 283 | None 284 | } 285 | 286 | fn find_executable(version: RequestedVersion, args: &[String]) -> crate::Result { 287 | let mut requested_version = version; 288 | let mut chosen_path: Option = None; 289 | 290 | if requested_version == RequestedVersion::Any { 291 | if let Some(venv_path) = venv_executable() { 292 | chosen_path = Some(venv_path); 293 | } else if !args.is_empty() { 294 | // Using the first argument because it's the simplest and sanest. 295 | // We can't use the last argument because that could actually be an argument 296 | // to the Python module being executed. This is the same reason we can't go 297 | // searching for the first/last file path that we find. The only safe way to 298 | // get the file path regardless of its position is to replicate Python's arg 299 | // parsing and that's a **lot** of work for little gain. Hence we only care 300 | // about the first argument. 301 | let possible_file = &args[0]; 302 | log::info!("Checking {possible_file:?} for a shebang"); 303 | if let Ok(mut open_file) = File::open(possible_file) { 304 | if let Some(shebang_version) = parse_python_shebang(&mut open_file) { 305 | requested_version = shebang_version; 306 | } 307 | } 308 | } 309 | } 310 | 311 | if chosen_path.is_none() { 312 | if let Some(env_var) = requested_version.env_var() { 313 | log::info!("Checking the {env_var} environment variable"); 314 | if let Ok(env_var_value) = env::var(&env_var) { 315 | if !env_var_value.is_empty() { 316 | log::debug!("{env_var} = '{env_var_value}'"); 317 | let env_requested_version = RequestedVersion::from_str(&env_var_value)?; 318 | requested_version = env_requested_version; 319 | } 320 | } else { 321 | log::info!("{env_var} not set"); 322 | }; 323 | } 324 | 325 | if let Some(executable_path) = crate::find_executable(requested_version) { 326 | chosen_path = Some(executable_path); 327 | } 328 | } 329 | 330 | chosen_path.ok_or(crate::Error::NoExecutableFound(requested_version)) 331 | } 332 | 333 | #[cfg(test)] 334 | mod tests { 335 | use test_case::test_case; 336 | 337 | use super::*; 338 | 339 | #[test_case(&["py".to_string(), "--help".to_string(), "--list".to_string()] => Err(crate::Error::IllegalArgument(PathBuf::from("py"), "--help".to_string())))] 340 | #[test_case(&["py".to_string(), "--list".to_string(), "--help".to_string()] => Err(crate::Error::IllegalArgument(PathBuf::from("py"), "--list".to_string())))] 341 | fn from_main_illegal_argument_tests(argv: &[String]) -> crate::Result { 342 | Action::from_main(argv) 343 | } 344 | 345 | #[test_case("-S" => None ; "unrecognized short flag is None")] 346 | #[test_case("--something" => None ; "unrecognized long flag is None")] 347 | #[test_case("-3" => Some(RequestedVersion::MajorOnly(3)) ; "major version")] 348 | #[test_case("-3.6" => Some(RequestedVersion::Exact(3, 6)) ; "Exact/major.minor")] 349 | #[test_case("-42.13" => Some(RequestedVersion::Exact(42, 13)) ; "double-digit major & minor versions")] 350 | #[test_case("-3.6.4" => None ; "version flag with micro version is None")] 351 | fn version_from_flag_tests(flag: &str) -> Option { 352 | version_from_flag(flag) 353 | } 354 | 355 | #[test] 356 | fn test_help_message() { 357 | let launcher_path = "/some/path/to/launcher"; 358 | let python_path = "/a/path/to/python"; 359 | 360 | let help = help_message(&PathBuf::from(launcher_path), &PathBuf::from(python_path)); 361 | assert!(help.contains(env!("CARGO_PKG_VERSION"))); 362 | assert!(help.contains(launcher_path)); 363 | assert!(help.contains(python_path)); 364 | } 365 | 366 | #[test] 367 | fn test_list_executables() { 368 | let mut executables: HashMap = HashMap::new(); 369 | 370 | assert_eq!( 371 | list_executables(&executables), 372 | Err(crate::Error::NoExecutableFound(RequestedVersion::Any)) 373 | ); 374 | 375 | let python27_path = "/path/to/2/7/python"; 376 | executables.insert( 377 | ExactVersion { major: 2, minor: 7 }, 378 | PathBuf::from(python27_path), 379 | ); 380 | let python36_path = "/path/to/3/6/python"; 381 | executables.insert( 382 | ExactVersion { major: 3, minor: 6 }, 383 | PathBuf::from(python36_path), 384 | ); 385 | let python37_path = "/path/to/3/7/python"; 386 | executables.insert( 387 | ExactVersion { major: 3, minor: 7 }, 388 | PathBuf::from(python37_path), 389 | ); 390 | 391 | // Tests try not to make any guarantees about explicit formatting, just 392 | // that the interpreters are in descending order of version and the 393 | // interpreter version comes before the path (i.e. in column order). 394 | let executables_list = list_executables(&executables).unwrap(); 395 | // No critical data is missing. 396 | assert!(executables_list.contains("2.7")); 397 | assert!(executables_list.contains(python27_path)); 398 | assert!(executables_list.contains("3.6")); 399 | assert!(executables_list.contains(python36_path)); 400 | assert!(executables_list.contains("3.7")); 401 | assert!(executables_list.contains(python37_path)); 402 | 403 | // Interpreters listed in the expected order. 404 | assert!(executables_list.find("3.7").unwrap() < executables_list.find("3.6").unwrap()); 405 | assert!(executables_list.find("3.6").unwrap() < executables_list.find("2.7").unwrap()); 406 | 407 | // Columns are in the expected order. 408 | assert!( 409 | executables_list.find("3.6").unwrap() < executables_list.find(python36_path).unwrap() 410 | ); 411 | assert!( 412 | executables_list.find("3.7").unwrap() < executables_list.find(python36_path).unwrap() 413 | ); 414 | } 415 | 416 | #[test] 417 | fn test_venv_executable_path() { 418 | let venv_root = "/path/to/venv"; 419 | assert_eq!( 420 | venv_executable_path(venv_root), 421 | PathBuf::from("/path/to/venv/bin/python") 422 | ); 423 | } 424 | 425 | #[test_case("/usr/bin/python" => None ; "missing shebang comment")] 426 | #[test_case("# /usr/bin/python" => None ; "missing exclamation point")] 427 | #[test_case("! /usr/bin/python" => None ; "missing octothorpe")] 428 | #[test_case("#! /bin/sh" => None ; "non-Python shebang")] 429 | #[test_case("#! /usr/bin/env python" => Some(RequestedVersion::Any) ; "typical 'env python'")] 430 | #[test_case("#! /usr/bin/python" => Some(RequestedVersion::Any) ; "typical 'python'")] 431 | #[test_case("#! /usr/local/bin/python" => Some(RequestedVersion::Any) ; "/usr/local")] 432 | #[test_case("#! python" => Some(RequestedVersion::Any) ; "bare 'python'")] 433 | #[test_case("#! /usr/bin/env python3.7" => Some(RequestedVersion::Exact(3, 7)) ; "typical 'env python' with minor version")] 434 | #[test_case("#! /usr/bin/python3.7" => Some(RequestedVersion::Exact(3, 7)) ; "typical 'python' with minor version")] 435 | #[test_case("#! python3.7" => Some(RequestedVersion::Exact(3, 7)) ; "bare 'python' with minor version")] 436 | #[test_case("#!/usr/bin/python" => Some(RequestedVersion::Any) ; "no space between shebang and path")] 437 | fn parse_python_shebang_tests(shebang: &str) -> Option { 438 | parse_python_shebang(&mut shebang.as_bytes()) 439 | } 440 | 441 | #[test_case(&[0x23, 0x21, 0xc0, 0xaf] => None ; "invalid UTF-8")] 442 | fn parse_python_sheban_include_invalid_bytes_tests( 443 | mut shebang: &[u8], 444 | ) -> Option { 445 | parse_python_shebang(&mut shebang) 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Search for Python interpreters in the environment 2 | //! 3 | //! This crate provides the code to both find Python interpreters installed and 4 | //! utilities to implement a CLI which mimic the [Python Launcher for Windows]. 5 | //! 6 | //! # Layout 7 | //! 8 | //! At the top-level, the code directly related to searching is provided. 9 | //! The [`RequestedVersion`] enum represents the constraints the user has placed 10 | //! upon what version of Python they are searching for (ranging from any to a 11 | //! `major.minor` version). The [`ExactVersion`] struct represents an exact 12 | //! `major.minor` version of Python which was found. 13 | //! 14 | //! The [`cli`] module contains all code related to providing a CLI like the one 15 | //! the [Python Launcher for Windows] provides. 16 | //! 17 | //! [Python Launcher for Windows]: https://docs.python.org/3/using/windows.html#launcher 18 | 19 | pub mod cli; 20 | 21 | use std::{ 22 | collections::HashMap, 23 | convert::From, 24 | env, fmt, 25 | fmt::Display, 26 | num::ParseIntError, 27 | path::{Path, PathBuf}, 28 | str::FromStr, 29 | }; 30 | 31 | /// [`std::result::Result`] type with [`Error`] as the error type. 32 | pub type Result = std::result::Result; 33 | 34 | /// Error enum for the entire crate. 35 | #[derive(Clone, Debug, PartialEq, Eq)] 36 | pub enum Error { 37 | /// Parsing a digit component from a string fails. 38 | ParseVersionComponentError(ParseIntError, String), 39 | /// String parsing fails due to `.` missing. 40 | DotMissing, 41 | /// A [`Path`] lacks a file name when it is required. 42 | FileNameMissing, 43 | /// A file name cannot be converted to a string. 44 | FileNameToStrError, 45 | /// A file name is not structured appropriately. 46 | PathFileNameError, 47 | /// No Python executable could be found based on the constraints provided. 48 | NoExecutableFound(RequestedVersion), 49 | /// An illegal combination of CLI flags are provided. 50 | IllegalArgument(PathBuf, String), 51 | } 52 | 53 | impl fmt::Display for Error { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | match self { 56 | Error::ParseVersionComponentError(int_error, bad_value) => { 57 | write!(f, "Error parsing '{bad_value}' as an integer: {int_error}") 58 | } 59 | Self::DotMissing => write!(f, "'.' missing from the version"), 60 | Self::FileNameMissing => write!(f, "Path object lacks a file name"), 61 | Self::FileNameToStrError => write!(f, "Failed to convert file name to `str`"), 62 | Self::PathFileNameError => write!(f, "File name not of the format `pythonX.Y`"), 63 | Self::NoExecutableFound(requested_version) => { 64 | write!(f, "No executable found for {requested_version}") 65 | } 66 | Self::IllegalArgument(launcher_path, flag) => { 67 | let printable_path = launcher_path.to_string_lossy(); 68 | write!( 69 | f, 70 | "The `{flag}` flag must be specified on its own; see `{printable_path} --help` for details" 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | 77 | impl std::error::Error for Error { 78 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 79 | match self { 80 | Self::ParseVersionComponentError(int_error, _) => Some(int_error), 81 | Self::DotMissing => None, 82 | Self::FileNameMissing => None, 83 | Self::FileNameToStrError => None, 84 | Self::PathFileNameError => None, 85 | Self::NoExecutableFound(_) => None, 86 | Self::IllegalArgument(_, _) => None, 87 | } 88 | } 89 | } 90 | 91 | impl Error { 92 | /// Returns the appropriate [exit code](`exitcode::ExitCode`) for the error. 93 | pub fn exit_code(&self) -> exitcode::ExitCode { 94 | match self { 95 | Self::ParseVersionComponentError(_, _) => exitcode::USAGE, 96 | Self::DotMissing => exitcode::USAGE, 97 | Self::FileNameMissing => exitcode::USAGE, 98 | Self::FileNameToStrError => exitcode::SOFTWARE, 99 | Self::PathFileNameError => exitcode::SOFTWARE, 100 | Self::NoExecutableFound(_) => exitcode::USAGE, 101 | Self::IllegalArgument(_, _) => exitcode::USAGE, 102 | } 103 | } 104 | } 105 | 106 | /// The integral part of a version specifier (e.g. the `3` or `10` of `3.10`). 107 | pub type ComponentSize = u16; 108 | 109 | /// The version of Python being searched for. 110 | /// 111 | /// The constraints of what is being searched for can very from being 112 | /// open-ended/broad (i.e. [`RequestedVersion::Any`]) to as specific as 113 | /// `major.minor` (e.g. [`RequestedVersion::Exact`] to search for Python 3.10). 114 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 115 | pub enum RequestedVersion { 116 | /// Any version of Python is acceptable. 117 | Any, 118 | /// A major version of Python is required (e.g. `3.x`). 119 | MajorOnly(ComponentSize), 120 | /// A specific `major.minor` version of Python is required (e.g. `3.9`). 121 | Exact(ComponentSize, ComponentSize), 122 | } 123 | 124 | impl Display for RequestedVersion { 125 | /// Format to a readable name of the Python version requested, e.g. `Python 3.9`. 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | let repr = match self { 128 | Self::Any => "Python".to_string(), 129 | Self::MajorOnly(major) => format!("Python {major}"), 130 | Self::Exact(major, minor) => format!("Python {major}.{minor}"), 131 | }; 132 | write!(f, "{repr}") 133 | } 134 | } 135 | 136 | impl FromStr for RequestedVersion { 137 | type Err = Error; 138 | 139 | fn from_str(version_string: &str) -> Result { 140 | if version_string.is_empty() { 141 | Ok(Self::Any) 142 | } else if version_string.contains('.') { 143 | let exact_version = ExactVersion::from_str(version_string)?; 144 | Ok(Self::Exact(exact_version.major, exact_version.minor)) 145 | } else { 146 | match version_string.parse::() { 147 | Ok(number) => Ok(Self::MajorOnly(number)), 148 | Err(parse_error) => Err(Error::ParseVersionComponentError( 149 | parse_error, 150 | version_string.to_string(), 151 | )), 152 | } 153 | } 154 | } 155 | } 156 | 157 | impl RequestedVersion { 158 | /// Returns the [`String`] representing the environment variable for the 159 | /// requested version (if applicable). 160 | /// 161 | /// # Examples 162 | /// 163 | /// Searching for [`RequestedVersion::Any`] provides an environment variable 164 | /// which can be used to specify the default version of Python to use 165 | /// (e.g. `3.10`). 166 | /// 167 | /// ``` 168 | /// let any_version = python_launcher::RequestedVersion::Any; 169 | /// 170 | /// assert_eq!(Some("PY_PYTHON".to_string()), any_version.env_var()); 171 | /// ``` 172 | /// 173 | /// [`RequestedVersion::MajorOnly`] uses an environment variable which is 174 | /// scoped to providing the default version for when the major version is 175 | /// only specified. 176 | /// 177 | /// ``` 178 | /// let major_version = python_launcher::RequestedVersion::MajorOnly(3); 179 | /// 180 | /// assert_eq!(Some("PY_PYTHON3".to_string()), major_version.env_var()); 181 | /// ``` 182 | /// 183 | /// When [`RequestedVersion::Exact`] is specified, there is no "default" to 184 | /// provide/interpreter, and so no environment variable exists. 185 | /// 186 | /// ``` 187 | /// let exact_version = python_launcher::RequestedVersion::Exact(3, 10); 188 | /// 189 | /// assert!(exact_version.env_var().is_none()); 190 | /// ``` 191 | pub fn env_var(self) -> Option { 192 | match self { 193 | Self::Any => Some("PY_PYTHON".to_string()), 194 | Self::MajorOnly(major) => Some(format!("PY_PYTHON{major}")), 195 | _ => None, 196 | } 197 | } 198 | } 199 | 200 | /// Specifies the `major.minor` version of a Python executable. 201 | /// 202 | /// This struct is typically used to represent a found executable's version. 203 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] 204 | pub struct ExactVersion { 205 | /// The major version of Python, e.g. `3` of `3.10`. 206 | pub major: ComponentSize, 207 | /// The minor version of Python, e.g. `10` of `3.10`. 208 | pub minor: ComponentSize, 209 | } 210 | 211 | impl From for RequestedVersion { 212 | fn from(version: ExactVersion) -> Self { 213 | Self::Exact(version.major, version.minor) 214 | } 215 | } 216 | 217 | impl Display for ExactVersion { 218 | /// Format to the format specifier, e.g. `3.9`. 219 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 220 | let major = self.major; 221 | let minor = self.minor; 222 | write!(f, "{major}.{minor}") 223 | } 224 | } 225 | 226 | impl FromStr for ExactVersion { 227 | type Err = Error; 228 | 229 | fn from_str(version_string: &str) -> Result { 230 | match version_string.find('.') { 231 | Some(dot_index) => { 232 | let major_str = &version_string[..dot_index]; 233 | let major = match major_str.parse::() { 234 | Ok(number) => number, 235 | Err(parse_error) => { 236 | return Err(Error::ParseVersionComponentError( 237 | parse_error, 238 | major_str.to_string(), 239 | )) 240 | } 241 | }; 242 | let minor_str = &version_string[dot_index + 1..]; 243 | 244 | match minor_str.parse::() { 245 | Ok(minor) => Ok(Self { major, minor }), 246 | Err(parse_error) => Err(Error::ParseVersionComponentError( 247 | parse_error, 248 | minor_str.to_string(), 249 | )), 250 | } 251 | } 252 | None => Err(Error::DotMissing), 253 | } 254 | } 255 | } 256 | 257 | fn acceptable_file_name(file_name: &str) -> bool { 258 | file_name.len() >= "python3.0".len() && file_name.starts_with("python") 259 | } 260 | 261 | impl ExactVersion { 262 | /// Construct an instance of [`ExactVersion`]. 263 | pub fn new(major: ComponentSize, minor: ComponentSize) -> Self { 264 | ExactVersion { major, minor } 265 | } 266 | 267 | /// Constructs a [`ExactVersion`] from a `pythonX.Y` file path. 268 | /// 269 | /// # Errors 270 | /// 271 | /// If the [`Path`] is missing a file name component, 272 | /// [`Error::FileNameMissing`] is returned. 273 | /// 274 | /// If the file name is not formatted appropriately, 275 | /// [`Error::PathFileNameError`] is returned. 276 | /// 277 | /// When the [`Path`] cannot be converted to a [`&str`], 278 | /// [`Error::FileNameToStrError`] is returned. 279 | /// 280 | /// # Examples 281 | /// 282 | /// ``` 283 | /// let expected = python_launcher::ExactVersion::new(3, 10); 284 | /// let executable_path = std::path::Path::new("python3.10"); 285 | /// let exact_version = python_launcher::ExactVersion::from_path(executable_path); 286 | /// 287 | /// assert_eq!(Ok(expected), exact_version); 288 | /// ``` 289 | pub fn from_path(path: &Path) -> Result { 290 | path.file_name() 291 | .ok_or(Error::FileNameMissing) 292 | .and_then(|raw_file_name| match raw_file_name.to_str() { 293 | Some(file_name) if acceptable_file_name(file_name) => { 294 | Self::from_str(&file_name["python".len()..]) 295 | } 296 | Some(_) => Err(Error::PathFileNameError), 297 | None => Err(Error::FileNameToStrError), 298 | }) 299 | } 300 | 301 | /// Tests whether this [`ExactVersion`] satisfies the [`RequestedVersion`]. 302 | /// 303 | /// # Examples 304 | /// 305 | /// ``` 306 | /// let py3_10 = python_launcher::ExactVersion::new(3, 10); 307 | /// let any_version = python_launcher::RequestedVersion::Any; 308 | /// let py3_version = python_launcher::RequestedVersion::MajorOnly(3); 309 | /// let py3_10_version = python_launcher::RequestedVersion::Exact(3, 10); 310 | /// 311 | /// assert!(py3_10.supports(any_version)); 312 | /// assert!(py3_10.supports(py3_version)); 313 | /// assert!(py3_10.supports(py3_10_version)); 314 | /// ``` 315 | pub fn supports(&self, requested: RequestedVersion) -> bool { 316 | match requested { 317 | RequestedVersion::Any => true, 318 | RequestedVersion::MajorOnly(major_version) => self.major == major_version, 319 | RequestedVersion::Exact(major_version, minor_version) => { 320 | self.major == major_version && self.minor == minor_version 321 | } 322 | } 323 | } 324 | } 325 | 326 | fn env_path() -> Vec { 327 | // Would love to have a return type of `impl Iterator 328 | // and return just SplitPaths and iter::empty(), but Rust 329 | // complains about differing return types. 330 | match env::var_os("PATH") { 331 | Some(path_val) => env::split_paths(&path_val).collect(), 332 | None => Vec::new(), 333 | } 334 | } 335 | 336 | fn flatten_directories( 337 | directories: impl IntoIterator, 338 | ) -> impl Iterator { 339 | directories 340 | .into_iter() 341 | .filter_map(|p| p.read_dir().ok()) // Filter to Ok(ReadDir). 342 | .flatten() // Flatten out `for DirEntry in ReadDir`. 343 | .filter_map(|e| e.ok()) // Filter to Ok(DirEntry). 344 | .map(|e| e.path()) // Get the PathBuf from the DirEntry. 345 | } 346 | 347 | fn all_executables_in_paths( 348 | paths: impl IntoIterator, 349 | ) -> HashMap { 350 | let mut executables = HashMap::new(); 351 | paths.into_iter().for_each(|path| { 352 | ExactVersion::from_path(&path).map_or((), |version| { 353 | executables.entry(version).or_insert(path); 354 | }) 355 | }); 356 | 357 | let found_executables = executables.values(); 358 | log::debug!("Found executables: {found_executables:?}",); 359 | executables 360 | } 361 | 362 | /// Finds all possible Python executables on `PATH`. 363 | pub fn all_executables() -> HashMap { 364 | log::info!("Checking PATH environment variable"); 365 | let path_entries = env_path(); 366 | log::debug!("PATH: {path_entries:?}"); 367 | let paths = flatten_directories(path_entries); 368 | all_executables_in_paths(paths) 369 | } 370 | 371 | fn find_executable_in_hashmap( 372 | requested: RequestedVersion, 373 | found_executables: &HashMap, 374 | ) -> Option { 375 | let mut iter = found_executables.iter(); 376 | match requested { 377 | RequestedVersion::Any => iter.max(), 378 | RequestedVersion::MajorOnly(_) => iter.filter(|pair| pair.0.supports(requested)).max(), 379 | RequestedVersion::Exact(_, _) => iter.find(|pair| pair.0.supports(requested)), 380 | } 381 | .map(|pair| pair.1.clone()) 382 | } 383 | 384 | /// Attempts to find an executable that satisfies a specified 385 | /// [`RequestedVersion`] on `PATH`. 386 | pub fn find_executable(requested: RequestedVersion) -> Option { 387 | let found_executables = all_executables(); 388 | find_executable_in_hashmap(requested, &found_executables) 389 | } 390 | 391 | #[cfg(test)] 392 | mod tests { 393 | use super::*; 394 | 395 | use std::cmp::Ordering; 396 | 397 | use test_case::test_case; 398 | 399 | #[test_case(RequestedVersion::Any => "Python" ; "Any")] 400 | #[test_case(RequestedVersion::MajorOnly(3) => "Python 3" ; "Major")] 401 | #[test_case(RequestedVersion::Exact(3, 8) => "Python 3.8" ; "Exact/major.minor")] 402 | fn requestedversion_to_string_tests(requested_version: RequestedVersion) -> String { 403 | requested_version.to_string() 404 | } 405 | 406 | #[test_case(".3" => matches Err(Error::ParseVersionComponentError(_, _)) ; "missing major version is an error")] 407 | #[test_case("3." => matches Err(Error::ParseVersionComponentError(_, _)) ; "missing minor version is an error")] 408 | #[test_case("h" => matches Err(Error::ParseVersionComponentError(_, _)) ; "non-number, non-emptry string is an error")] 409 | #[test_case("3.b" => matches Err(Error::ParseVersionComponentError(_, _)) ; "major.minor where minor is a non-number is an error")] 410 | #[test_case("a.7" => matches Err(Error::ParseVersionComponentError(_, _)) ; "major.minor where major is a non-number is an error")] 411 | #[test_case("" => Ok(RequestedVersion::Any) ; "empty string is Any")] 412 | #[test_case("3" => Ok(RequestedVersion::MajorOnly(3)) ; "major-only version")] 413 | #[test_case("3.8" => Ok(RequestedVersion::Exact(3, 8)) ; "major.minor")] 414 | #[test_case("42.13" => Ok(RequestedVersion::Exact(42, 13)) ; "double digit version components")] 415 | #[test_case("3.6.5" => matches Err(Error::ParseVersionComponentError(_, _)) ; "specifying a micro version is an error")] 416 | fn requestedversion_from_str_tests(version_str: &str) -> Result { 417 | RequestedVersion::from_str(version_str) 418 | } 419 | 420 | #[test_case(RequestedVersion::Any => Some("PY_PYTHON".to_string()) ; "Any is PY_PYTHON")] 421 | #[test_case(RequestedVersion::MajorOnly(3) => Some("PY_PYTHON3".to_string()) ; "major-only is PY_PYTHON{major}")] 422 | #[test_case(RequestedVersion::MajorOnly(42) => Some("PY_PYTHON42".to_string()) ; "double-digit major component")] 423 | #[test_case(RequestedVersion::Exact(42, 13) => None ; "exact/major.minor has no environment variable")] 424 | fn requstedversion_env_var_tests(requested_version: RequestedVersion) -> Option { 425 | requested_version.env_var() 426 | } 427 | 428 | #[test] 429 | fn test_requestedversion_from_exactversion() { 430 | assert_eq!( 431 | RequestedVersion::from(ExactVersion { 432 | major: 42, 433 | minor: 13 434 | }), 435 | RequestedVersion::Exact(42, 13) 436 | ); 437 | } 438 | 439 | #[test] // For some reason, having Ordering breaks test-case 1.0.0. 440 | fn exactversion_comparisons() { 441 | let py2_7 = ExactVersion { major: 2, minor: 7 }; 442 | let py3_0 = ExactVersion { major: 3, minor: 0 }; 443 | let py3_6 = ExactVersion { major: 3, minor: 6 }; 444 | let py3_10 = ExactVersion { 445 | major: 3, 446 | minor: 10, 447 | }; 448 | 449 | // == 450 | assert_eq!(py3_10.cmp(&py3_10), Ordering::Equal); 451 | // < 452 | assert_eq!(py3_0.cmp(&py3_6), Ordering::Less); 453 | // > 454 | assert_eq!(py3_6.cmp(&py3_0), Ordering::Greater); 455 | // Differ by major version. 456 | assert_eq!(py2_7.cmp(&py3_0), Ordering::Less); 457 | assert_eq!(py3_0.cmp(&py2_7), Ordering::Greater); 458 | // Sort order different from lexicographic order. 459 | assert_eq!(py3_6.cmp(&py3_10), Ordering::Less); 460 | assert_eq!(py3_10.cmp(&py3_6), Ordering::Greater); 461 | } 462 | 463 | #[test_case(3, 8 => "3.8" ; "single digits")] 464 | #[test_case(42, 13 => "42.13" ; "double digits")] 465 | fn exactversion_to_string_tests(major: ComponentSize, minor: ComponentSize) -> String { 466 | ExactVersion { major, minor }.to_string() 467 | } 468 | 469 | #[test_case("" => Err(Error::DotMissing) ; "empty string is an error")] 470 | #[test_case("3" => Err(Error::DotMissing) ; "major-only version is an error")] 471 | #[test_case(".7" => matches Err(Error::ParseVersionComponentError(_, _)) ; "missing major version is an error")] 472 | #[test_case("3." => matches Err(Error::ParseVersionComponentError(_, _)) ; "missing minor version is an error")] 473 | #[test_case("3.Y" => matches Err(Error::ParseVersionComponentError(_, _)) ; "non-digit minor version is an error")] 474 | #[test_case("X.7" => matches Err(Error::ParseVersionComponentError(_, _)) ; "non-digit major version is an error")] 475 | #[test_case("42.13" => Ok(ExactVersion {major: 42, minor: 13 }) ; "double digit version components")] 476 | fn exactversion_from_str_tests(version_str: &str) -> Result { 477 | ExactVersion::from_str(version_str) 478 | } 479 | 480 | #[test_case("/" => Err(Error::FileNameMissing) ; "path missing a file name is an error")] 481 | #[test_case("/notpython" => Err(Error::PathFileNameError) ; "path not ending with 'python' is an error")] 482 | #[test_case("/python3" => Err(Error::PathFileNameError) ; "filename lacking a minor component is an error")] 483 | #[test_case("/pythonX.Y" => matches Err(Error::ParseVersionComponentError(_, _)) ; "filename with non-digit version is an error")] 484 | #[test_case("/python42.13" => Ok(ExactVersion { major: 42, minor: 13 }) ; "double digit version components")] 485 | fn exactversion_from_path_tests(path: &str) -> Result { 486 | ExactVersion::from_path(&PathBuf::from(path)) 487 | } 488 | 489 | #[test] 490 | fn exactversion_from_path_invalid_utf8() { 491 | // From https://doc.rust-lang.org/std/ffi/struct.OsStr.html#examples-2. 492 | use std::ffi::OsStr; 493 | use std::os::unix::ffi::OsStrExt; 494 | 495 | let source = [0x66, 0x6f, 0x80, 0x6f]; 496 | let os_str = OsStr::from_bytes(&source[..]); 497 | let path = PathBuf::from(os_str); 498 | assert_eq!( 499 | ExactVersion::from_path(&path), 500 | Err(Error::FileNameToStrError) 501 | ); 502 | } 503 | 504 | #[allow(clippy::bool_assert_comparison)] 505 | #[test_case(RequestedVersion::Any => true ; "Any supports all versions")] 506 | #[test_case(RequestedVersion::MajorOnly(2) => false ; "major-only mismatch")] 507 | #[test_case(RequestedVersion::MajorOnly(3) => true ; "major-only match")] 508 | #[test_case(RequestedVersion::Exact(2, 7) => false ; "older major version")] 509 | #[test_case(RequestedVersion::Exact(3, 5) => false ; "older minor version")] 510 | #[test_case(RequestedVersion::Exact(4, 0) => false ; "newer major version")] 511 | #[test_case(RequestedVersion::Exact(3, 7) => false ; "newer minor version")] 512 | #[test_case(RequestedVersion::Exact(3, 6) => true ; "same version")] 513 | fn exactversion_supports_tests(requested_version: RequestedVersion) -> bool { 514 | let example = ExactVersion { major: 3, minor: 6 }; 515 | example.supports(requested_version) 516 | } 517 | 518 | #[test_case(2, 7, "/dir1/python2.7" ; "first directory")] 519 | #[test_case(3, 6, "/dir1/python3.6" ; "matches in multiple directories")] 520 | #[test_case(3, 7, "/dir2/python3.7" ; "last directory")] 521 | fn all_executables_in_paths_tests(major: ComponentSize, minor: ComponentSize, path: &str) { 522 | let python27_path = PathBuf::from("/dir1/python2.7"); 523 | let python36_dir1_path = PathBuf::from("/dir1/python3.6"); 524 | let python36_dir2_path = PathBuf::from("/dir2/python3.6"); 525 | let python37_path = PathBuf::from("/dir2/python3.7"); 526 | let files = vec![ 527 | python27_path, 528 | python36_dir1_path, 529 | python36_dir2_path, 530 | python37_path, 531 | ]; 532 | 533 | let executables = all_executables_in_paths(files); 534 | assert_eq!(executables.len(), 3); 535 | 536 | let version = ExactVersion { major, minor }; 537 | assert!(executables.contains_key(&version)); 538 | assert_eq!(executables.get(&version), Some(&PathBuf::from(path))); 539 | } 540 | 541 | #[test_case(RequestedVersion::Any => Some(PathBuf::from("/python3.7")) ; "Any version chooses newest version")] 542 | #[test_case(RequestedVersion::MajorOnly(42) => None ; "major-only version newer than any options")] 543 | #[test_case(RequestedVersion::MajorOnly(3) => Some(PathBuf::from("/python3.7")) ; "matching major version chooses newest minor version")] 544 | #[test_case(RequestedVersion::Exact(3, 8) => None ; "version not available")] 545 | #[test_case(RequestedVersion::Exact(3, 6) => Some(PathBuf::from("/python3.6")) ; "exact version match")] 546 | fn find_executable_in_hashmap_tests(requested_version: RequestedVersion) -> Option { 547 | let mut executables = HashMap::new(); 548 | assert_eq!( 549 | find_executable_in_hashmap(RequestedVersion::Any, &executables), 550 | None 551 | ); 552 | 553 | let python36_path = PathBuf::from("/python3.6"); 554 | executables.insert(ExactVersion { major: 3, minor: 6 }, python36_path); 555 | 556 | let python37_path = PathBuf::from("/python3.7"); 557 | executables.insert(ExactVersion { major: 3, minor: 7 }, python37_path); 558 | 559 | find_executable_in_hashmap(requested_version, &executables) 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --allow-unsafe --generate-hashes --output-file=docs/requirements.txt docs/requirements.in 6 | # 7 | babel==2.13.1 \ 8 | --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \ 9 | --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed 10 | # via mkdocs-material 11 | certifi==2024.7.4 \ 12 | --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ 13 | --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 14 | # via requests 15 | charset-normalizer==3.3.1 \ 16 | --hash=sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5 \ 17 | --hash=sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93 \ 18 | --hash=sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a \ 19 | --hash=sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d \ 20 | --hash=sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c \ 21 | --hash=sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1 \ 22 | --hash=sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58 \ 23 | --hash=sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2 \ 24 | --hash=sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557 \ 25 | --hash=sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147 \ 26 | --hash=sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041 \ 27 | --hash=sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2 \ 28 | --hash=sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2 \ 29 | --hash=sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7 \ 30 | --hash=sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296 \ 31 | --hash=sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690 \ 32 | --hash=sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67 \ 33 | --hash=sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57 \ 34 | --hash=sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597 \ 35 | --hash=sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846 \ 36 | --hash=sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b \ 37 | --hash=sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97 \ 38 | --hash=sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c \ 39 | --hash=sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62 \ 40 | --hash=sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa \ 41 | --hash=sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f \ 42 | --hash=sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e \ 43 | --hash=sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821 \ 44 | --hash=sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3 \ 45 | --hash=sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4 \ 46 | --hash=sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb \ 47 | --hash=sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727 \ 48 | --hash=sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514 \ 49 | --hash=sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d \ 50 | --hash=sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761 \ 51 | --hash=sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55 \ 52 | --hash=sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f \ 53 | --hash=sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c \ 54 | --hash=sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034 \ 55 | --hash=sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6 \ 56 | --hash=sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae \ 57 | --hash=sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1 \ 58 | --hash=sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14 \ 59 | --hash=sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1 \ 60 | --hash=sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228 \ 61 | --hash=sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708 \ 62 | --hash=sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48 \ 63 | --hash=sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f \ 64 | --hash=sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5 \ 65 | --hash=sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f \ 66 | --hash=sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4 \ 67 | --hash=sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8 \ 68 | --hash=sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff \ 69 | --hash=sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61 \ 70 | --hash=sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b \ 71 | --hash=sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97 \ 72 | --hash=sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b \ 73 | --hash=sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605 \ 74 | --hash=sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728 \ 75 | --hash=sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d \ 76 | --hash=sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c \ 77 | --hash=sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf \ 78 | --hash=sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673 \ 79 | --hash=sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1 \ 80 | --hash=sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b \ 81 | --hash=sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41 \ 82 | --hash=sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8 \ 83 | --hash=sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f \ 84 | --hash=sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4 \ 85 | --hash=sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008 \ 86 | --hash=sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9 \ 87 | --hash=sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5 \ 88 | --hash=sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f \ 89 | --hash=sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e \ 90 | --hash=sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273 \ 91 | --hash=sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45 \ 92 | --hash=sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e \ 93 | --hash=sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656 \ 94 | --hash=sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e \ 95 | --hash=sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c \ 96 | --hash=sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2 \ 97 | --hash=sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72 \ 98 | --hash=sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056 \ 99 | --hash=sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397 \ 100 | --hash=sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42 \ 101 | --hash=sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd \ 102 | --hash=sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3 \ 103 | --hash=sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213 \ 104 | --hash=sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf \ 105 | --hash=sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67 106 | # via requests 107 | click==8.1.7 \ 108 | --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ 109 | --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de 110 | # via mkdocs 111 | colorama==0.4.6 \ 112 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 113 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 114 | # via mkdocs-material 115 | ghp-import==2.1.0 \ 116 | --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ 117 | --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 118 | # via mkdocs 119 | idna==3.7 \ 120 | --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ 121 | --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 122 | # via requests 123 | jinja2==3.1.5 \ 124 | --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ 125 | --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb 126 | # via 127 | # mkdocs 128 | # mkdocs-material 129 | markdown==3.10 \ 130 | --hash=sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e \ 131 | --hash=sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c 132 | # via 133 | # mkdocs 134 | # mkdocs-material 135 | # pymdown-extensions 136 | markupsafe==2.1.3 \ 137 | --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ 138 | --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ 139 | --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ 140 | --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ 141 | --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ 142 | --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ 143 | --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ 144 | --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ 145 | --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ 146 | --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ 147 | --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ 148 | --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ 149 | --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ 150 | --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ 151 | --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ 152 | --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ 153 | --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ 154 | --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ 155 | --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ 156 | --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ 157 | --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ 158 | --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ 159 | --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ 160 | --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ 161 | --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ 162 | --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ 163 | --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ 164 | --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ 165 | --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ 166 | --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ 167 | --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ 168 | --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ 169 | --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ 170 | --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ 171 | --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ 172 | --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ 173 | --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ 174 | --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ 175 | --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ 176 | --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ 177 | --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ 178 | --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ 179 | --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ 180 | --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ 181 | --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ 182 | --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ 183 | --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ 184 | --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ 185 | --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ 186 | --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ 187 | --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ 188 | --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ 189 | --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ 190 | --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ 191 | --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ 192 | --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ 193 | --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ 194 | --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ 195 | --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ 196 | --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 197 | # via 198 | # jinja2 199 | # mkdocs 200 | mergedeep==1.3.4 \ 201 | --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ 202 | --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 203 | # via 204 | # mkdocs 205 | # mkdocs-get-deps 206 | mkdocs==1.6.0 \ 207 | --hash=sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7 \ 208 | --hash=sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512 209 | # via mkdocs-material 210 | mkdocs-get-deps==0.2.0 \ 211 | --hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \ 212 | --hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134 213 | # via mkdocs 214 | mkdocs-material==9.5.26 \ 215 | --hash=sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326 \ 216 | --hash=sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312 217 | # via -r requirements.in 218 | mkdocs-material-extensions==1.3 \ 219 | --hash=sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc \ 220 | --hash=sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd 221 | # via mkdocs-material 222 | packaging==23.2 \ 223 | --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ 224 | --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 225 | # via mkdocs 226 | paginate==0.5.6 \ 227 | --hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d 228 | # via mkdocs-material 229 | pathspec==0.11.2 \ 230 | --hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \ 231 | --hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3 232 | # via mkdocs 233 | platformdirs==3.11.0 \ 234 | --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ 235 | --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e 236 | # via mkdocs-get-deps 237 | pygments==2.16.1 \ 238 | --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ 239 | --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 240 | # via mkdocs-material 241 | pymdown-extensions==10.16.1 \ 242 | --hash=sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91 \ 243 | --hash=sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d 244 | # via mkdocs-material 245 | python-dateutil==2.8.2 \ 246 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 247 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 248 | # via ghp-import 249 | pyyaml==6.0.1 \ 250 | --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ 251 | --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ 252 | --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ 253 | --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ 254 | --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ 255 | --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ 256 | --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ 257 | --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ 258 | --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ 259 | --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ 260 | --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ 261 | --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ 262 | --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ 263 | --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ 264 | --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ 265 | --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ 266 | --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ 267 | --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ 268 | --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ 269 | --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ 270 | --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ 271 | --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ 272 | --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ 273 | --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ 274 | --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ 275 | --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ 276 | --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ 277 | --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ 278 | --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ 279 | --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ 280 | --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ 281 | --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ 282 | --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ 283 | --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ 284 | --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ 285 | --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ 286 | --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ 287 | --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ 288 | --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ 289 | --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ 290 | --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ 291 | --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ 292 | --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ 293 | --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ 294 | --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ 295 | --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ 296 | --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ 297 | --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ 298 | --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ 299 | --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f 300 | # via 301 | # mkdocs 302 | # mkdocs-get-deps 303 | # pymdown-extensions 304 | # pyyaml-env-tag 305 | pyyaml-env-tag==0.1 \ 306 | --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ 307 | --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 308 | # via mkdocs 309 | regex==2023.10.3 \ 310 | --hash=sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a \ 311 | --hash=sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07 \ 312 | --hash=sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca \ 313 | --hash=sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58 \ 314 | --hash=sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54 \ 315 | --hash=sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed \ 316 | --hash=sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff \ 317 | --hash=sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528 \ 318 | --hash=sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9 \ 319 | --hash=sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971 \ 320 | --hash=sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14 \ 321 | --hash=sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af \ 322 | --hash=sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302 \ 323 | --hash=sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec \ 324 | --hash=sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597 \ 325 | --hash=sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b \ 326 | --hash=sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd \ 327 | --hash=sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767 \ 328 | --hash=sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f \ 329 | --hash=sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6 \ 330 | --hash=sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293 \ 331 | --hash=sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be \ 332 | --hash=sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41 \ 333 | --hash=sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc \ 334 | --hash=sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29 \ 335 | --hash=sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964 \ 336 | --hash=sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d \ 337 | --hash=sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a \ 338 | --hash=sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc \ 339 | --hash=sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55 \ 340 | --hash=sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af \ 341 | --hash=sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930 \ 342 | --hash=sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e \ 343 | --hash=sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d \ 344 | --hash=sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863 \ 345 | --hash=sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c \ 346 | --hash=sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f \ 347 | --hash=sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e \ 348 | --hash=sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d \ 349 | --hash=sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368 \ 350 | --hash=sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb \ 351 | --hash=sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52 \ 352 | --hash=sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8 \ 353 | --hash=sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4 \ 354 | --hash=sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac \ 355 | --hash=sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e \ 356 | --hash=sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2 \ 357 | --hash=sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a \ 358 | --hash=sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4 \ 359 | --hash=sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa \ 360 | --hash=sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533 \ 361 | --hash=sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b \ 362 | --hash=sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588 \ 363 | --hash=sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0 \ 364 | --hash=sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915 \ 365 | --hash=sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841 \ 366 | --hash=sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a \ 367 | --hash=sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988 \ 368 | --hash=sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292 \ 369 | --hash=sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3 \ 370 | --hash=sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c \ 371 | --hash=sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f \ 372 | --hash=sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420 \ 373 | --hash=sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9 \ 374 | --hash=sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f \ 375 | --hash=sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0 \ 376 | --hash=sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b \ 377 | --hash=sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037 \ 378 | --hash=sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b \ 379 | --hash=sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee \ 380 | --hash=sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c \ 381 | --hash=sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b \ 382 | --hash=sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353 \ 383 | --hash=sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051 \ 384 | --hash=sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039 \ 385 | --hash=sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a \ 386 | --hash=sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b \ 387 | --hash=sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e \ 388 | --hash=sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5 \ 389 | --hash=sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf \ 390 | --hash=sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94 \ 391 | --hash=sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991 \ 392 | --hash=sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711 \ 393 | --hash=sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a \ 394 | --hash=sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab \ 395 | --hash=sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a \ 396 | --hash=sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11 \ 397 | --hash=sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48 398 | # via mkdocs-material 399 | requests==2.32.4 \ 400 | --hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \ 401 | --hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422 402 | # via mkdocs-material 403 | six==1.16.0 \ 404 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 405 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 406 | # via python-dateutil 407 | urllib3==2.6.0 \ 408 | --hash=sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f \ 409 | --hash=sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1 410 | # via requests 411 | watchdog==3.0.0 \ 412 | --hash=sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a \ 413 | --hash=sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100 \ 414 | --hash=sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8 \ 415 | --hash=sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc \ 416 | --hash=sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae \ 417 | --hash=sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41 \ 418 | --hash=sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0 \ 419 | --hash=sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f \ 420 | --hash=sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c \ 421 | --hash=sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9 \ 422 | --hash=sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3 \ 423 | --hash=sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709 \ 424 | --hash=sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83 \ 425 | --hash=sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759 \ 426 | --hash=sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9 \ 427 | --hash=sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3 \ 428 | --hash=sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7 \ 429 | --hash=sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f \ 430 | --hash=sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346 \ 431 | --hash=sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674 \ 432 | --hash=sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397 \ 433 | --hash=sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96 \ 434 | --hash=sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d \ 435 | --hash=sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a \ 436 | --hash=sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64 \ 437 | --hash=sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44 \ 438 | --hash=sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33 439 | # via mkdocs 440 | 441 | # The following packages are considered to be unsafe in a requirements file: 442 | setuptools==78.1.1 \ 443 | --hash=sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561 \ 444 | --hash=sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d 445 | # via babel 446 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android-tzdata" 31 | version = "0.1.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 34 | 35 | [[package]] 36 | name = "android_system_properties" 37 | version = "0.1.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 40 | dependencies = [ 41 | "libc", 42 | ] 43 | 44 | [[package]] 45 | name = "anes" 46 | version = "0.1.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 49 | 50 | [[package]] 51 | name = "anstream" 52 | version = "0.6.14" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 55 | dependencies = [ 56 | "anstyle", 57 | "anstyle-parse", 58 | "anstyle-query", 59 | "anstyle-wincon", 60 | "colorchoice", 61 | "is_terminal_polyfill", 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle" 67 | version = "1.0.7" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 70 | 71 | [[package]] 72 | name = "anstyle-parse" 73 | version = "0.2.4" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 76 | dependencies = [ 77 | "utf8parse", 78 | ] 79 | 80 | [[package]] 81 | name = "anstyle-query" 82 | version = "1.1.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 85 | dependencies = [ 86 | "windows-sys", 87 | ] 88 | 89 | [[package]] 90 | name = "anstyle-wincon" 91 | version = "3.0.3" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 94 | dependencies = [ 95 | "anstyle", 96 | "windows-sys", 97 | ] 98 | 99 | [[package]] 100 | name = "assert_cmd" 101 | version = "2.0.14" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" 104 | dependencies = [ 105 | "anstyle", 106 | "bstr", 107 | "doc-comment", 108 | "predicates", 109 | "predicates-core", 110 | "predicates-tree", 111 | "wait-timeout", 112 | ] 113 | 114 | [[package]] 115 | name = "autocfg" 116 | version = "1.3.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 119 | 120 | [[package]] 121 | name = "backtrace" 122 | version = "0.3.73" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 125 | dependencies = [ 126 | "addr2line", 127 | "cc", 128 | "cfg-if", 129 | "libc", 130 | "miniz_oxide", 131 | "object", 132 | "rustc-demangle", 133 | ] 134 | 135 | [[package]] 136 | name = "bitflags" 137 | version = "2.6.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 140 | 141 | [[package]] 142 | name = "bstr" 143 | version = "1.9.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 146 | dependencies = [ 147 | "memchr", 148 | "regex-automata", 149 | "serde", 150 | ] 151 | 152 | [[package]] 153 | name = "bumpalo" 154 | version = "3.16.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 157 | 158 | [[package]] 159 | name = "cast" 160 | version = "0.3.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 163 | 164 | [[package]] 165 | name = "cc" 166 | version = "1.0.102" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" 169 | 170 | [[package]] 171 | name = "cfg-if" 172 | version = "1.0.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 175 | 176 | [[package]] 177 | name = "cfg_aliases" 178 | version = "0.2.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 181 | 182 | [[package]] 183 | name = "chrono" 184 | version = "0.4.38" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 187 | dependencies = [ 188 | "android-tzdata", 189 | "iana-time-zone", 190 | "js-sys", 191 | "num-traits", 192 | "wasm-bindgen", 193 | "windows-targets", 194 | ] 195 | 196 | [[package]] 197 | name = "ciborium" 198 | version = "0.2.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 201 | dependencies = [ 202 | "ciborium-io", 203 | "ciborium-ll", 204 | "serde", 205 | ] 206 | 207 | [[package]] 208 | name = "ciborium-io" 209 | version = "0.2.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 212 | 213 | [[package]] 214 | name = "ciborium-ll" 215 | version = "0.2.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 218 | dependencies = [ 219 | "ciborium-io", 220 | "half", 221 | ] 222 | 223 | [[package]] 224 | name = "clap" 225 | version = "4.5.8" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" 228 | dependencies = [ 229 | "clap_builder", 230 | ] 231 | 232 | [[package]] 233 | name = "clap_builder" 234 | version = "4.5.8" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" 237 | dependencies = [ 238 | "anstyle", 239 | "clap_lex", 240 | ] 241 | 242 | [[package]] 243 | name = "clap_lex" 244 | version = "0.7.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 247 | 248 | [[package]] 249 | name = "colorchoice" 250 | version = "1.0.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 253 | 254 | [[package]] 255 | name = "comfy-table" 256 | version = "7.1.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" 259 | dependencies = [ 260 | "crossterm", 261 | "strum", 262 | "strum_macros", 263 | "unicode-width", 264 | ] 265 | 266 | [[package]] 267 | name = "core-foundation-sys" 268 | version = "0.8.6" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 271 | 272 | [[package]] 273 | name = "criterion" 274 | version = "0.5.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 277 | dependencies = [ 278 | "anes", 279 | "cast", 280 | "ciborium", 281 | "clap", 282 | "criterion-plot", 283 | "is-terminal", 284 | "itertools", 285 | "num-traits", 286 | "once_cell", 287 | "oorandom", 288 | "plotters", 289 | "rayon", 290 | "regex", 291 | "serde", 292 | "serde_derive", 293 | "serde_json", 294 | "tinytemplate", 295 | "walkdir", 296 | ] 297 | 298 | [[package]] 299 | name = "criterion-plot" 300 | version = "0.5.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 303 | dependencies = [ 304 | "cast", 305 | "itertools", 306 | ] 307 | 308 | [[package]] 309 | name = "crossbeam-deque" 310 | version = "0.8.5" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 313 | dependencies = [ 314 | "crossbeam-epoch", 315 | "crossbeam-utils", 316 | ] 317 | 318 | [[package]] 319 | name = "crossbeam-epoch" 320 | version = "0.9.18" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 323 | dependencies = [ 324 | "crossbeam-utils", 325 | ] 326 | 327 | [[package]] 328 | name = "crossbeam-utils" 329 | version = "0.8.20" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 332 | 333 | [[package]] 334 | name = "crossterm" 335 | version = "0.27.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 338 | dependencies = [ 339 | "bitflags", 340 | "crossterm_winapi", 341 | "libc", 342 | "parking_lot", 343 | "winapi", 344 | ] 345 | 346 | [[package]] 347 | name = "crossterm_winapi" 348 | version = "0.9.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 351 | dependencies = [ 352 | "winapi", 353 | ] 354 | 355 | [[package]] 356 | name = "crunchy" 357 | version = "0.2.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 360 | 361 | [[package]] 362 | name = "difflib" 363 | version = "0.4.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 366 | 367 | [[package]] 368 | name = "doc-comment" 369 | version = "0.3.3" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 372 | 373 | [[package]] 374 | name = "either" 375 | version = "1.13.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 378 | 379 | [[package]] 380 | name = "equivalent" 381 | version = "1.0.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 384 | 385 | [[package]] 386 | name = "errno" 387 | version = "0.3.9" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 390 | dependencies = [ 391 | "libc", 392 | "windows-sys", 393 | ] 394 | 395 | [[package]] 396 | name = "exitcode" 397 | version = "1.1.2" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 400 | 401 | [[package]] 402 | name = "fastrand" 403 | version = "2.1.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 406 | 407 | [[package]] 408 | name = "float-cmp" 409 | version = "0.9.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 412 | dependencies = [ 413 | "num-traits", 414 | ] 415 | 416 | [[package]] 417 | name = "futures" 418 | version = "0.3.30" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 421 | dependencies = [ 422 | "futures-channel", 423 | "futures-core", 424 | "futures-executor", 425 | "futures-io", 426 | "futures-sink", 427 | "futures-task", 428 | "futures-util", 429 | ] 430 | 431 | [[package]] 432 | name = "futures-channel" 433 | version = "0.3.30" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 436 | dependencies = [ 437 | "futures-core", 438 | "futures-sink", 439 | ] 440 | 441 | [[package]] 442 | name = "futures-core" 443 | version = "0.3.30" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 446 | 447 | [[package]] 448 | name = "futures-executor" 449 | version = "0.3.30" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 452 | dependencies = [ 453 | "futures-core", 454 | "futures-task", 455 | "futures-util", 456 | ] 457 | 458 | [[package]] 459 | name = "futures-io" 460 | version = "0.3.30" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 463 | 464 | [[package]] 465 | name = "futures-sink" 466 | version = "0.3.30" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 469 | 470 | [[package]] 471 | name = "futures-task" 472 | version = "0.3.30" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 475 | 476 | [[package]] 477 | name = "futures-util" 478 | version = "0.3.30" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 481 | dependencies = [ 482 | "futures-channel", 483 | "futures-core", 484 | "futures-io", 485 | "futures-sink", 486 | "futures-task", 487 | "memchr", 488 | "pin-project-lite", 489 | "pin-utils", 490 | "slab", 491 | ] 492 | 493 | [[package]] 494 | name = "getrandom" 495 | version = "0.2.15" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 498 | dependencies = [ 499 | "cfg-if", 500 | "libc", 501 | "wasi", 502 | ] 503 | 504 | [[package]] 505 | name = "gimli" 506 | version = "0.29.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 509 | 510 | [[package]] 511 | name = "half" 512 | version = "2.4.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 515 | dependencies = [ 516 | "cfg-if", 517 | "crunchy", 518 | ] 519 | 520 | [[package]] 521 | name = "hashbrown" 522 | version = "0.14.5" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 525 | 526 | [[package]] 527 | name = "heck" 528 | version = "0.5.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 531 | 532 | [[package]] 533 | name = "hermit-abi" 534 | version = "0.3.9" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 537 | 538 | [[package]] 539 | name = "human-panic" 540 | version = "2.0.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "a4c5d0e9120f6bca6120d142c7ede1ba376dd6bf276d69dd3dbe6cbeb7824179" 543 | dependencies = [ 544 | "anstream", 545 | "anstyle", 546 | "backtrace", 547 | "os_info", 548 | "serde", 549 | "serde_derive", 550 | "toml", 551 | "uuid", 552 | ] 553 | 554 | [[package]] 555 | name = "iana-time-zone" 556 | version = "0.1.60" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 559 | dependencies = [ 560 | "android_system_properties", 561 | "core-foundation-sys", 562 | "iana-time-zone-haiku", 563 | "js-sys", 564 | "wasm-bindgen", 565 | "windows-core", 566 | ] 567 | 568 | [[package]] 569 | name = "iana-time-zone-haiku" 570 | version = "0.1.2" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 573 | dependencies = [ 574 | "cc", 575 | ] 576 | 577 | [[package]] 578 | name = "indexmap" 579 | version = "2.2.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 582 | dependencies = [ 583 | "equivalent", 584 | "hashbrown", 585 | ] 586 | 587 | [[package]] 588 | name = "is-terminal" 589 | version = "0.4.12" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 592 | dependencies = [ 593 | "hermit-abi", 594 | "libc", 595 | "windows-sys", 596 | ] 597 | 598 | [[package]] 599 | name = "is_terminal_polyfill" 600 | version = "1.70.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 603 | 604 | [[package]] 605 | name = "itertools" 606 | version = "0.10.5" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 609 | dependencies = [ 610 | "either", 611 | ] 612 | 613 | [[package]] 614 | name = "itoa" 615 | version = "1.0.11" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 618 | 619 | [[package]] 620 | name = "js-sys" 621 | version = "0.3.69" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 624 | dependencies = [ 625 | "wasm-bindgen", 626 | ] 627 | 628 | [[package]] 629 | name = "libc" 630 | version = "0.2.155" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 633 | 634 | [[package]] 635 | name = "linux-raw-sys" 636 | version = "0.4.14" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 639 | 640 | [[package]] 641 | name = "lock_api" 642 | version = "0.4.12" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 645 | dependencies = [ 646 | "autocfg", 647 | "scopeguard", 648 | ] 649 | 650 | [[package]] 651 | name = "log" 652 | version = "0.4.22" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 655 | 656 | [[package]] 657 | name = "memchr" 658 | version = "2.7.4" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 661 | 662 | [[package]] 663 | name = "miniz_oxide" 664 | version = "0.7.4" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 667 | dependencies = [ 668 | "adler", 669 | ] 670 | 671 | [[package]] 672 | name = "nix" 673 | version = "0.29.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 676 | dependencies = [ 677 | "bitflags", 678 | "cfg-if", 679 | "cfg_aliases", 680 | "libc", 681 | ] 682 | 683 | [[package]] 684 | name = "normalize-line-endings" 685 | version = "0.3.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 688 | 689 | [[package]] 690 | name = "num-traits" 691 | version = "0.2.19" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 694 | dependencies = [ 695 | "autocfg", 696 | ] 697 | 698 | [[package]] 699 | name = "object" 700 | version = "0.36.1" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 703 | dependencies = [ 704 | "memchr", 705 | ] 706 | 707 | [[package]] 708 | name = "once_cell" 709 | version = "1.19.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 712 | 713 | [[package]] 714 | name = "oorandom" 715 | version = "11.1.3" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 718 | 719 | [[package]] 720 | name = "os_info" 721 | version = "3.8.2" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" 724 | dependencies = [ 725 | "log", 726 | "serde", 727 | "windows-sys", 728 | ] 729 | 730 | [[package]] 731 | name = "parking_lot" 732 | version = "0.12.3" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 735 | dependencies = [ 736 | "lock_api", 737 | "parking_lot_core", 738 | ] 739 | 740 | [[package]] 741 | name = "parking_lot_core" 742 | version = "0.9.10" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 745 | dependencies = [ 746 | "cfg-if", 747 | "libc", 748 | "redox_syscall", 749 | "smallvec", 750 | "windows-targets", 751 | ] 752 | 753 | [[package]] 754 | name = "pin-project-lite" 755 | version = "0.2.14" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 758 | 759 | [[package]] 760 | name = "pin-utils" 761 | version = "0.1.0" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 764 | 765 | [[package]] 766 | name = "plotters" 767 | version = "0.3.6" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" 770 | dependencies = [ 771 | "num-traits", 772 | "plotters-backend", 773 | "plotters-svg", 774 | "wasm-bindgen", 775 | "web-sys", 776 | ] 777 | 778 | [[package]] 779 | name = "plotters-backend" 780 | version = "0.3.6" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" 783 | 784 | [[package]] 785 | name = "plotters-svg" 786 | version = "0.3.6" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" 789 | dependencies = [ 790 | "plotters-backend", 791 | ] 792 | 793 | [[package]] 794 | name = "predicates" 795 | version = "3.1.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 798 | dependencies = [ 799 | "anstyle", 800 | "difflib", 801 | "float-cmp", 802 | "normalize-line-endings", 803 | "predicates-core", 804 | "regex", 805 | ] 806 | 807 | [[package]] 808 | name = "predicates-core" 809 | version = "1.0.6" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 812 | 813 | [[package]] 814 | name = "predicates-tree" 815 | version = "1.0.9" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 818 | dependencies = [ 819 | "predicates-core", 820 | "termtree", 821 | ] 822 | 823 | [[package]] 824 | name = "proc-macro2" 825 | version = "1.0.86" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 828 | dependencies = [ 829 | "unicode-ident", 830 | ] 831 | 832 | [[package]] 833 | name = "python-launcher" 834 | version = "1.0.1" 835 | dependencies = [ 836 | "assert_cmd", 837 | "comfy-table", 838 | "criterion", 839 | "exitcode", 840 | "human-panic", 841 | "log", 842 | "nix", 843 | "predicates", 844 | "serial_test", 845 | "stderrlog", 846 | "tempfile", 847 | "test-case", 848 | ] 849 | 850 | [[package]] 851 | name = "quote" 852 | version = "1.0.36" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 855 | dependencies = [ 856 | "proc-macro2", 857 | ] 858 | 859 | [[package]] 860 | name = "rayon" 861 | version = "1.10.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 864 | dependencies = [ 865 | "either", 866 | "rayon-core", 867 | ] 868 | 869 | [[package]] 870 | name = "rayon-core" 871 | version = "1.12.1" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 874 | dependencies = [ 875 | "crossbeam-deque", 876 | "crossbeam-utils", 877 | ] 878 | 879 | [[package]] 880 | name = "redox_syscall" 881 | version = "0.5.2" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" 884 | dependencies = [ 885 | "bitflags", 886 | ] 887 | 888 | [[package]] 889 | name = "regex" 890 | version = "1.10.5" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 893 | dependencies = [ 894 | "aho-corasick", 895 | "memchr", 896 | "regex-automata", 897 | "regex-syntax", 898 | ] 899 | 900 | [[package]] 901 | name = "regex-automata" 902 | version = "0.4.7" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 905 | dependencies = [ 906 | "aho-corasick", 907 | "memchr", 908 | "regex-syntax", 909 | ] 910 | 911 | [[package]] 912 | name = "regex-syntax" 913 | version = "0.8.4" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 916 | 917 | [[package]] 918 | name = "rustc-demangle" 919 | version = "0.1.24" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 922 | 923 | [[package]] 924 | name = "rustix" 925 | version = "0.38.34" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 928 | dependencies = [ 929 | "bitflags", 930 | "errno", 931 | "libc", 932 | "linux-raw-sys", 933 | "windows-sys", 934 | ] 935 | 936 | [[package]] 937 | name = "rustversion" 938 | version = "1.0.17" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 941 | 942 | [[package]] 943 | name = "ryu" 944 | version = "1.0.18" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 947 | 948 | [[package]] 949 | name = "same-file" 950 | version = "1.0.6" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 953 | dependencies = [ 954 | "winapi-util", 955 | ] 956 | 957 | [[package]] 958 | name = "scc" 959 | version = "2.1.1" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" 962 | dependencies = [ 963 | "sdd", 964 | ] 965 | 966 | [[package]] 967 | name = "scopeguard" 968 | version = "1.2.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 971 | 972 | [[package]] 973 | name = "sdd" 974 | version = "0.2.0" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" 977 | 978 | [[package]] 979 | name = "serde" 980 | version = "1.0.203" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 983 | dependencies = [ 984 | "serde_derive", 985 | ] 986 | 987 | [[package]] 988 | name = "serde_derive" 989 | version = "1.0.203" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 992 | dependencies = [ 993 | "proc-macro2", 994 | "quote", 995 | "syn", 996 | ] 997 | 998 | [[package]] 999 | name = "serde_json" 1000 | version = "1.0.118" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" 1003 | dependencies = [ 1004 | "itoa", 1005 | "ryu", 1006 | "serde", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "serde_spanned" 1011 | version = "0.6.6" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" 1014 | dependencies = [ 1015 | "serde", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "serial_test" 1020 | version = "3.1.1" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" 1023 | dependencies = [ 1024 | "futures", 1025 | "log", 1026 | "once_cell", 1027 | "parking_lot", 1028 | "scc", 1029 | "serial_test_derive", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "serial_test_derive" 1034 | version = "3.1.1" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" 1037 | dependencies = [ 1038 | "proc-macro2", 1039 | "quote", 1040 | "syn", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "slab" 1045 | version = "0.4.9" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1048 | dependencies = [ 1049 | "autocfg", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "smallvec" 1054 | version = "1.13.2" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1057 | 1058 | [[package]] 1059 | name = "stderrlog" 1060 | version = "0.6.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b" 1063 | dependencies = [ 1064 | "chrono", 1065 | "is-terminal", 1066 | "log", 1067 | "termcolor", 1068 | "thread_local", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "strum" 1073 | version = "0.26.3" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1076 | 1077 | [[package]] 1078 | name = "strum_macros" 1079 | version = "0.26.4" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1082 | dependencies = [ 1083 | "heck", 1084 | "proc-macro2", 1085 | "quote", 1086 | "rustversion", 1087 | "syn", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "syn" 1092 | version = "2.0.68" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" 1095 | dependencies = [ 1096 | "proc-macro2", 1097 | "quote", 1098 | "unicode-ident", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "tempfile" 1103 | version = "3.10.1" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1106 | dependencies = [ 1107 | "cfg-if", 1108 | "fastrand", 1109 | "rustix", 1110 | "windows-sys", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "termcolor" 1115 | version = "1.1.3" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1118 | dependencies = [ 1119 | "winapi-util", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "termtree" 1124 | version = "0.4.1" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1127 | 1128 | [[package]] 1129 | name = "test-case" 1130 | version = "3.3.1" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" 1133 | dependencies = [ 1134 | "test-case-macros", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "test-case-core" 1139 | version = "3.3.1" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" 1142 | dependencies = [ 1143 | "cfg-if", 1144 | "proc-macro2", 1145 | "quote", 1146 | "syn", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "test-case-macros" 1151 | version = "3.3.1" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" 1154 | dependencies = [ 1155 | "proc-macro2", 1156 | "quote", 1157 | "syn", 1158 | "test-case-core", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "thread_local" 1163 | version = "1.1.8" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1166 | dependencies = [ 1167 | "cfg-if", 1168 | "once_cell", 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "tinytemplate" 1173 | version = "1.2.1" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 1176 | dependencies = [ 1177 | "serde", 1178 | "serde_json", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "toml" 1183 | version = "0.8.14" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" 1186 | dependencies = [ 1187 | "serde", 1188 | "serde_spanned", 1189 | "toml_datetime", 1190 | "toml_edit", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "toml_datetime" 1195 | version = "0.6.6" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" 1198 | dependencies = [ 1199 | "serde", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "toml_edit" 1204 | version = "0.22.14" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" 1207 | dependencies = [ 1208 | "indexmap", 1209 | "serde", 1210 | "serde_spanned", 1211 | "toml_datetime", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "unicode-ident" 1216 | version = "1.0.12" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1219 | 1220 | [[package]] 1221 | name = "unicode-width" 1222 | version = "0.1.13" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 1225 | 1226 | [[package]] 1227 | name = "utf8parse" 1228 | version = "0.2.2" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1231 | 1232 | [[package]] 1233 | name = "uuid" 1234 | version = "1.9.1" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" 1237 | dependencies = [ 1238 | "getrandom", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "wait-timeout" 1243 | version = "0.2.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1246 | dependencies = [ 1247 | "libc", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "walkdir" 1252 | version = "2.5.0" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1255 | dependencies = [ 1256 | "same-file", 1257 | "winapi-util", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "wasi" 1262 | version = "0.11.0+wasi-snapshot-preview1" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1265 | 1266 | [[package]] 1267 | name = "wasm-bindgen" 1268 | version = "0.2.92" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1271 | dependencies = [ 1272 | "cfg-if", 1273 | "wasm-bindgen-macro", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "wasm-bindgen-backend" 1278 | version = "0.2.92" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1281 | dependencies = [ 1282 | "bumpalo", 1283 | "log", 1284 | "once_cell", 1285 | "proc-macro2", 1286 | "quote", 1287 | "syn", 1288 | "wasm-bindgen-shared", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "wasm-bindgen-macro" 1293 | version = "0.2.92" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1296 | dependencies = [ 1297 | "quote", 1298 | "wasm-bindgen-macro-support", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "wasm-bindgen-macro-support" 1303 | version = "0.2.92" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1306 | dependencies = [ 1307 | "proc-macro2", 1308 | "quote", 1309 | "syn", 1310 | "wasm-bindgen-backend", 1311 | "wasm-bindgen-shared", 1312 | ] 1313 | 1314 | [[package]] 1315 | name = "wasm-bindgen-shared" 1316 | version = "0.2.92" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1319 | 1320 | [[package]] 1321 | name = "web-sys" 1322 | version = "0.3.69" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1325 | dependencies = [ 1326 | "js-sys", 1327 | "wasm-bindgen", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "winapi" 1332 | version = "0.3.9" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1335 | dependencies = [ 1336 | "winapi-i686-pc-windows-gnu", 1337 | "winapi-x86_64-pc-windows-gnu", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "winapi-i686-pc-windows-gnu" 1342 | version = "0.4.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1345 | 1346 | [[package]] 1347 | name = "winapi-util" 1348 | version = "0.1.8" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 1351 | dependencies = [ 1352 | "windows-sys", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "winapi-x86_64-pc-windows-gnu" 1357 | version = "0.4.0" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1360 | 1361 | [[package]] 1362 | name = "windows-core" 1363 | version = "0.52.0" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1366 | dependencies = [ 1367 | "windows-targets", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "windows-sys" 1372 | version = "0.52.0" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1375 | dependencies = [ 1376 | "windows-targets", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "windows-targets" 1381 | version = "0.52.5" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1384 | dependencies = [ 1385 | "windows_aarch64_gnullvm", 1386 | "windows_aarch64_msvc", 1387 | "windows_i686_gnu", 1388 | "windows_i686_gnullvm", 1389 | "windows_i686_msvc", 1390 | "windows_x86_64_gnu", 1391 | "windows_x86_64_gnullvm", 1392 | "windows_x86_64_msvc", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "windows_aarch64_gnullvm" 1397 | version = "0.52.5" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1400 | 1401 | [[package]] 1402 | name = "windows_aarch64_msvc" 1403 | version = "0.52.5" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1406 | 1407 | [[package]] 1408 | name = "windows_i686_gnu" 1409 | version = "0.52.5" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1412 | 1413 | [[package]] 1414 | name = "windows_i686_gnullvm" 1415 | version = "0.52.5" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1418 | 1419 | [[package]] 1420 | name = "windows_i686_msvc" 1421 | version = "0.52.5" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1424 | 1425 | [[package]] 1426 | name = "windows_x86_64_gnu" 1427 | version = "0.52.5" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1430 | 1431 | [[package]] 1432 | name = "windows_x86_64_gnullvm" 1433 | version = "0.52.5" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1436 | 1437 | [[package]] 1438 | name = "windows_x86_64_msvc" 1439 | version = "0.52.5" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1442 | --------------------------------------------------------------------------------