pax_global_header00006660000000000000000000000064141650050360014512gustar00rootroot0000000000000052 comment=078f2601c7469751f15db20a59e2960e1666df7d pipx-1.0.0/000077500000000000000000000000001416500503600124705ustar00rootroot00000000000000pipx-1.0.0/.deepsource.toml000066400000000000000000000002701416500503600156000ustar00rootroot00000000000000version = 1 test_patterns = ["tests/**"] [[analyzers]] name = "python" enabled = true [analyzers.meta] runtime_version = "3.x.x" [[transformers]] name = "black" enabled = true pipx-1.0.0/.flake8000066400000000000000000000001371416500503600136440ustar00rootroot00000000000000[flake8] max-line-length = 88 ignore = E501, E203, W503 # line length, whitespace before ':' pipx-1.0.0/.github/000077500000000000000000000000001416500503600140305ustar00rootroot00000000000000pipx-1.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001416500503600162135ustar00rootroot00000000000000pipx-1.0.0/.github/ISSUE_TEMPLATE/bug.md000066400000000000000000000010211416500503600173040ustar00rootroot00000000000000--- name: Bug about: Report a bug or unexpected behavior. --- **Describe the bug** **How to reproduce** **Expected behavior** pipx-1.0.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006471416500503600217470ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea or new feature for this project --- **How would this feature be useful?** **Describe the solution you'd like** **Describe alternatives you've considered** pipx-1.0.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000004461416500503600176350ustar00rootroot00000000000000 * [ ] I have added an entry to `docs/changelog.md` ## Summary of changes ## Test plan Tested by running ``` # command(s) to exercise these changes ``` pipx-1.0.0/.github/workflows/000077500000000000000000000000001416500503600160655ustar00rootroot00000000000000pipx-1.0.0/.github/workflows/create_tests_package_lists.yml000066400000000000000000000016501416500503600241700ustar00rootroot00000000000000name: Create tests package lists for offline tests on: workflow_dispatch: jobs: create_package_lists: name: Create package lists runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip python -m pip install nox - name: Create lists run: | nox --non-interactive --session create_test_package_list-${{ matrix.python-version }} -- ./new_tests_packages - name: Store reports as artifacts uses: actions/upload-artifact@v2 with: name: lists path: ./new_tests_packages pipx-1.0.0/.github/workflows/on-push.yml000066400000000000000000000072101416500503600202010ustar00rootroot00000000000000# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions # https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ # # Needed GitHub Repository Secret: # PYPI_PASSWORD - PyPI API token allowing write, release access to PyPI. # Limit scope to just project. # (See package.python.org example link above.) name: Check, possibly Publish on: pull_request: push: # If changing default-python be sure to change job "tests" matrix: include: also env: default-python: "3.10" jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python ${{ env.default-python }} uses: actions/setup-python@v2 with: python-version: ${{ env.default-python }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip python -m pip install nox - name: Lint run: | nox --error-on-missing-interpreters --non-interactive --session lint verify-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python ${{ env.default-python }} uses: actions/setup-python@v2 with: python-version: ${{ env.default-python }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip python -m pip install nox - name: Verify Docs run: | nox --error-on-missing-interpreters --non-interactive --session build_docs tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] include: - os: windows-latest python-version: "3.10" - os: macos-latest python-version: "3.10" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Get pip cache dir id: pip-cache run: | echo "::set-output name=dir::$(pip cache dir)" - name: Persistent Github pip cache uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip${{ matrix.python-version }} - name: Persistent .pipx_tests/package_cache uses: actions/cache@v2 with: path: ${{ github.workspace }}/.pipx_tests/package_cache/${{ matrix.python-version }} key: pipx-tests-package-cache-${{ runner.os }}-${{ matrix.python-version }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip python -m pip install nox - name: Execute Tests run: | nox --non-interactive --session tests-${{ matrix.python-version }} pypi-publish: name: Publish pipx to PyPI on release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: [verify-docs, lint, tests] runs-on: ubuntu-latest steps: - name: Checkout ${{ github.ref }} uses: actions/checkout@v2 - name: Set up Python ${{ env.default-python }} uses: actions/setup-python@v2 with: python-version: ${{ env.default-python }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip pip install nox - name: Build sdist and wheel run: | nox --error-on-missing-interpreters --non-interactive --session build - name: Publish to PyPi uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.pypi_password }} pipx-1.0.0/.github/workflows/publish-testpypi.yml000066400000000000000000000021331416500503600221340ustar00rootroot00000000000000# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions # # Needed GitHub Repository Secret: # TEST_PYPI_PASSWORD - TestPyPI API token allowing access to TestPyPI. # Limit scope to just project. # (See package.python.org example link above.) name: Publish pipx to Test PyPI on: workflow_dispatch: env: default-python: "3.10" jobs: testpypi-publish: name: Publish to Test PyPI runs-on: ubuntu-latest steps: - name: Checkout ${{ github.ref }} uses: actions/checkout@v2 - name: Set up Python ${{ env.default-python }} uses: actions/setup-python@v2 with: python-version: ${{ env.default-python }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip pip install nox - name: Build sdist and wheel run: | nox --error-on-missing-interpreters --non-interactive --session build - name: Publish to Test PyPi uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.test_pypi_password }} pipx-1.0.0/.github/workflows/publish_docs.yml000066400000000000000000000015041416500503600212660ustar00rootroot00000000000000name: Publish docs via GitHub Pages on: workflow_dispatch: release: types: [published] env: default-python: "3.10" jobs: build: name: Deploy docs runs-on: ubuntu-latest steps: - name: Checkout master uses: actions/checkout@v2 - name: Set up Python ${{ env.default-python }} uses: actions/setup-python@v2 with: python-version: ${{ env.default-python }} - name: Install nox run: | python -m pip install --upgrade pip pip install nox - name: Build Docs run: | nox --error-on-missing-interpreters --non-interactive --session build_docs - name: Deploy docs if: ${{ success() }} uses: mhausenblas/mkdocs-deploy-gh-pages@1.13 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pipx-1.0.0/.github/workflows/test_all_packages_slow.yml000066400000000000000000000041111416500503600233160ustar00rootroot00000000000000name: Test all packages (slow) on: workflow_dispatch: jobs: test_all_packages: name: Test all packages continue-on-error: true runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Upgrade pip, Install nox run: | python -m pip install --upgrade pip python -m pip install nox - name: Execute Tests continue-on-error: true run: | nox --non-interactive --session test_all_packages-${{ matrix.python-version }} - name: Store reports as artifacts uses: actions/upload-artifact@v2 with: name: reports-raw path: reports report_all_packages: name: Collate test reports needs: test_all_packages runs-on: ubuntu-latest steps: - name: Get report artifacts uses: actions/download-artifact@v2 with: name: reports-raw - name: Collate reports run: | ls # DEBUG mkdir reports cat all_packages_report_legend.txt > all_nodeps_reports_lf.txt cat all_packages_nodeps_report_* >> all_nodeps_reports_lf.txt tr -d '\r' < all_nodeps_reports_lf.txt > reports/all_nodeps_reports.txt cat all_packages_nodeps_errors_* > all_nodeps_errors_lf.txt tr -d '\r' < all_nodeps_errors_lf.txt > reports/all_nodeps_errors.txt cat all_packages_report_legend.txt > all_deps_reports_lf.txt cat all_packages_deps_report_* >> all_deps_reports_lf.txt tr -d '\r' < all_deps_reports_lf.txt > reports/all_deps_reports.txt cat all_packages_deps_errors_* > all_deps_errors_lf.txt tr -d '\r' < all_deps_errors_lf.txt > reports/all_deps_errors.txt - name: Store collated and raw reports as artifacts uses: actions/upload-artifact@v2 with: name: reports-final path: reports pipx-1.0.0/.gitignore000066400000000000000000000002401416500503600144540ustar00rootroot00000000000000/.nox/ /.coverage *egg* .mypy_cache .vscode build dist activate __pypackages__ venv .DS_Store .tox __pycache__ site docs/docs.md pipx.1 .pipx_tests /.coverage* pipx-1.0.0/.pre-commit-config.yaml000066400000000000000000000024201416500503600167470ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/#installation for installation instructions # See https://pre-commit.com/hooks.html for more hooks # # use `git commit --no-verify` to disable git hooks for this commit repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: end-of-file-fixer - id: check-added-large-files #- id: trailing-whitespace #- id: check-yaml - repo: https://github.com/PyCQA/isort rev: 5.10.1 hooks: - id: isort args: ['--profile','black'] - repo: https://github.com/psf/black rev: 21.12b0 hooks: - id: black - repo: https://gitlab.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [ 'flake8-bugbear==21.11.29' ] # mypy args: # must include --ignore-missing-imports for mypy. It is included by default # if no arguments are supplied, but we must supply it ourselves since we # specify args # cannot use --warn-unused-ignores because it conflicts with # --ignore-missing-imports - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.930 hooks: - id: mypy args: ['--ignore-missing-imports', '--strict-equality','--no-implicit-optional'] exclude: 'testdata/test_package_specifier/local_extras/setup.py' pipx-1.0.0/LICENSE000066400000000000000000000020741416500503600135000ustar00rootroot00000000000000MIT License Copyright (c) 2018 Chad Smith and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pipx-1.0.0/MANIFEST.in000066400000000000000000000005411416500503600142260ustar00rootroot00000000000000graft src include *.md pipx_demo.gif logo.png LICENSE exclude .isort.cfg .pre-commit-config.yaml get-pipx.py makefile generate_docs.py .flake8 mkdocs.yml noxfile.py .coveragerc .deepsource.toml recursive-exclude testdata * prune templates prune tests prune docs prune scripts global-exclude *.py[cod] global-exclude __pycache__ global-exclude .*.swp pipx-1.0.0/README.md000066400000000000000000000270241416500503600137540ustar00rootroot00000000000000

# pipx — Install and Run Python Applications in Isolated Environments

Test CI PyPI version

**Documentation**: https://pypa.github.io/pipx/ **Source Code**: https://github.com/pypa/pipx _For comparison to other tools including pipsi, see [Comparison to Other Tools](https://pypa.github.io/pipx/comparisons/)._ ## Install pipx ### On macOS ``` brew install pipx pipx ensurepath ``` Upgrade pipx with `brew update && brew upgrade pipx`. ### On Linux, install via pip (requires pip 19.0 or later) ``` python3 -m pip install --user pipx python3 -m pipx ensurepath ``` Upgrade pipx with `python3 -m pip install --user -U pipx`. ### On Windows, install via pip (requires pip 19.0 or later) ``` # If you installed python using the app-store, replace `python` with `python3` in the next line. python -m pip install --user pipx ``` It is possible (even most likely) the above finishes with a WARNING looking similar to this: ``` WARNING: The script pipx.exe is installed in `\AppData\Roaming\Python\Python3x\Scripts` which is not on PATH ``` If so, go to the mentioned folder, allowing you to run the pipx executable directly. Enter the following line (even if you did not get the warning): ``` pipx ensurepath ``` This will add both the above mentioned path and the `%USERPROFILE%.local\bin` folder to your search path. Restart your terminal session and verify `pipx` does run. Upgrade pipx with `python3 -m pip install --user -U pipx`. ### Shell completions Shell completions are available by following the instructions printed with this command: ``` pipx completions ``` For more details, see the [installation instructions](https://pypa.github.io/pipx/installation/). ## Overview: What is `pipx`? pipx is a tool to help you install and run end-user applications written in Python. It's roughly similar to macOS's `brew`, JavaScript's [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b), and Linux's `apt`. It's closely related to pip. In fact, it uses pip, but is focused on installing and managing Python packages that can be run from the command line directly as applications. ### How is it Different from pip? pip is a general-purpose package installer for both libraries and apps with no environment isolation. pipx is made specifically for application installation, as it adds isolation yet still makes the apps available in your shell: pipx creates an isolated environment for each application and its associated packages. pipx does not ship with pip, but installing it is often an important part of bootstrapping your system. ### Where Does `pipx` Install Apps From? By default, pipx uses the same package index as pip, [PyPI](https://pypi.org/). pipx can also install from all other sources pip can, such as a local directory, wheel, git url, etc. Python and PyPI allow developers to distribute code with "console script entry points". These entry points let users call into Python code from the command line, effectively acting like standalone applications. pipx is a tool to install and run any of these thousands of application-containing packages in a safe, convenient, and reliable way. **In a way, it turns Python Package Index (PyPI) into a big app store for Python applications.** Not all Python packages have entry points, but many do. If you would like to make your package compatible with pipx, all you need to do is add a [console scripts](https://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point) entry point. If you're a poetry user, use [these instructions](https://python-poetry.org/docs/pyproject/#scripts). ## Features `pipx` enables you to - Expose CLI entrypoints of packages ("apps") installed to isolated environments with the `install` command. This guarantees no dependency conflicts and clean uninstalls! - Easily list, upgrade, and uninstall packages that were installed with pipx - Run the latest version of a Python application in a temporary environment with the `run` command Best of all, pipx runs with regular user permissions, never calling `sudo pip install` (you aren't doing that, are you? 😄). ### Walkthrough: Installing a Package and its Applications With `pipx` You can globally install an application by running ``` pipx install PACKAGE ``` This automatically creates a virtual environment, installs the package, and adds the package's associated applications (entry points) to a location on your `PATH`. For example, `pipx install pycowsay` makes the `pycowsay` command available globally, but sandboxes the pycowsay package in its own virtual environment. **pipx never needs to run as sudo to do this.** Example: ``` >> pipx install pycowsay installed package pycowsay 2.0.3, Python 3.7.3 These apps are now globally available - pycowsay done! ✨ 🌟 ✨ >> pipx list venvs are in /home/user/.local/pipx/venvs apps are exposed on your $PATH at /home/user/.local/bin package pycowsay 2.0.3, Python 3.7.3 - pycowsay # Now you can run pycowsay from anywhere >> pycowsay mooo ____ < mooo > ==== \ \ ^__^ (oo)\_______ (__)\ )\/\ ||----w | || || ``` ### Installing from Source Control You can also install from a git repository. Here, `black` is used as an example. ``` pipx install git+https://github.com/psf/black.git pipx install git+https://github.com/psf/black.git@branch # branch of your choice pipx install git+https://github.com/psf/black.git@ce14fa8b497bae2b50ec48b3bd7022573a59cdb1 # git hash pipx install https://github.com/psf/black/archive/18.9b0.zip # install a release ``` ### Walkthrough: Running an Application in a Temporary Virtual Environment This is an alternative to `pipx install`. `pipx run` downloads and runs the above mentioned Python "apps" in a one-time, temporary environment, leaving your system untouched afterwards. This can be handy when you need to run the latest version of an app, but don't necessarily want it installed on your computer. You may want to do this when you are initializing a new project and want to set up the right directory structure, when you want to view the help text of an application, or if you simply want to run an app in a one-off case and leave your system untouched afterwards. For example, the blog post [How to set up a perfect Python project](https://sourcery.ai/blog/python-best-practices/) uses `pipx run` to kickstart a new project with [cookiecutter](https://github.com/cookiecutter/cookiecutter), a tool that creates projects from project templates. A nice side benefit is that you don't have to remember to upgrade the app since `pipx run` will automatically run a recent version for you. Okay, let's see what this looks like in practice! ``` pipx run APP [ARGS...] ``` This will install the package in an isolated, temporary directory and invoke the app. Give it a try: ``` > pipx run pycowsay moo --- < moo > --- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` Notice that you **don't need to execute any install commands to run the app**. Any arguments after the application name will be passed directly to the application: ``` > pipx run pycowsay these arguments are all passed to pycowsay! ------------------------------------------- < these arguments are all passed to pycowsay! > ------------------------------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` Re-running the same app is quick because pipx caches Virtual Environments on a per-app basis. The caches only last a few days, and when they expire, pipx will again use the latest version of the package. This way you can be sure you're always running a new version of the package without having to manually upgrade. If the app name does not match that package name, you can use the `--spec` argument to specify the package to install and app to run separately: ``` pipx run --spec PACKAGE APP ``` You can also specify specific versions, version ranges, or extras: ``` pipx run APP==1.0.0 ``` ### Running from Source Control You can also run from a git repository. Here, `black` is used as an example. ``` pipx run --spec git+https://github.com/psf/black.git black pipx run --spec git+https://github.com/psf/black.git@branch black # branch of your choice pipx run --spec git+https://github.com/psf/black.git@ce14fa8b497bae2b50ec48b3bd7022573a59cdb1 black # git hash pipx run --spec https://github.com/psf/black/archive/18.9b0.zip black # install a release ``` ### Running from URL You can run .py files directly, too. ``` pipx run https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py pipx is working! ``` ### Summary That's it! Those are the most important commands `pipx` offers. To see all of pipx's documentation, run `pipx --help` or see the [docs](https://pypa.github.io/pipx/docs/). ## Testimonials
"Thanks for improving the workflow that pipsi has covered in the past. Nicely done!"
Jannis Leidel, PSF fellow, former pip and Django core developer, and founder of the Python Packaging Authority (PyPA)

"My setup pieces together pyenv, poetry, and pipx. [...] For the things I need, it’s perfect."
Jacob Kaplan-Moss, co-creator of Django in blog post My Python Development Environment, 2020 Edition

"I'm a big fan of pipx. I think pipx is super cool."
Michael Kennedy, co-host of PythonBytes podcast in episode 139

## Credits pipx was inspired by [pipsi](https://github.com/mitsuhiko/pipsi) and [npx](https://github.com/npm/npx). It was created by [Chad Smith](https://github.com/cs01/) and has had lots of help from [contributors](https://github.com/pypa/pipx/graphs/contributors). The logo was created by [@IrishMorales](https://github.com/IrishMorales). pipx is maintained by a team of volunteers (in alphabetical order) - [Bernát Gábor](https://github.com/gaborbernat) - [Chad Smith](https://github.com/cs01) - co-lead maintainer - [Matthew Clapp](https://github.com/itsayellow) - co-lead maintainer - [Tzu-ping Chung](https://github.com/uranusjr) ## Contributing Issues and Pull Requests are definitely welcome! Check out [Contributing](https://pypa.github.io/pipx/contributing/) to get started. Everyone who interacts with the pipx project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md). pipx-1.0.0/docs/000077500000000000000000000000001416500503600134205ustar00rootroot00000000000000pipx-1.0.0/docs/changelog.md000066400000000000000000000470131416500503600156760ustar00rootroot000000000000001.0.0 - Support [argcomplete 2.0.0](https://pypi.org/project/argcomplete/2.0.0) (#790) - Include machinery to build a manpage for pipx with [argparse-manpage](https://pypi.org/project/argparse-manpage/). - Add better handling for 'app not found' when a single app is present in the project, and an improved error message (#733) - Fixed animations sending output to stdout, which can break JSON output. (#769) - Fix typo in `pipx upgrade-all` output 0.17.0 - Support `pipx run` with version constraints and extras. (#697) 0.16.5 - Fixed `pipx list` output phrasing to convey that python version displayed is the one with which package was installed. - Fixed `pipx install` to provide return code 0 if venv already exists, similar to pip’s behavior. (#736) - [docs] Update ansible's install command in [Programs to Try document](https://pypa.github.io/pipx/programs-to-try/#ansible) to work with Ansible 2.10+ (#742) 0.16.4 - Fix to `pipx ensurepath` to fix behavior in user locales other than UTF-8, to fix #644. The internal change is to use userpath v1.6.0 or greater. (#700) - Fix virtual environment inspection for Python releases that uses an int for its release serial number. (#706) - Fix PermissionError in windows when pipx manages itself. (#718) 0.16.3 - Organization: pipx is extremely pleased to now be a project of the Python Packaging Authority (PyPA)! Note that our github URL has changed to [pypa/pipx](https://github.com/pypa/pipx) - Fixed `pipx list --json` to return valid json with no venvs installed. Previously would return and empty string to stdout. (#681) - Changed `pipx ensurepath` bash behavior so that only one of {`~/.profile`, `~/.bash\_profile`} is modified with the extra pipx paths, not both. Previously, if a `.bash_profile` file was created where one didn't exist, it could cause problems, e.g. #456. The internal change is to use userpath v1.5.0 or greater. (#684) - Changed default nox tests, Github Workflow tests, and pytest behavior to use local pypi server with fixed lists of available packages. This allows greater test isolation (no network pypi access needed) and determinism (fixed available dependencies.) It also allows running the tests offline with some extra preparation beforehand (See [Running Unit Tests Offline](https://pypa.github.io/pipx/contributing/#running-unit-tests-offline)). The old style tests that use the internet to access pypi.org are still available using `nox -s tests_internet` or `pytest --net-pypiserver tests`. (#686) * Colorama is now only installed on Windows. (#691) 0.16.2.1 - Changed non-venv-info warnings and notices from `pipx list` to print to stderr. This especially prevents `pipx list --json` from printing invalid json to stdout. (#680) - Fixed bug that could cause uninstall on Windows with injected packages to uninstall too many apps from the local binary directory. (#679) 0.16.2.0 - Fixed bug #670 where uninstalling a venv could erroneously uninstall other apps from the local binary directory. (#672) - Added `--json` switch to `pipx list` to output rich json-metadata for all venvs. - Ensured log files are utf-8 encoded to preven Unicode encoding errors from occurring with emojis. (#646) - Fixed issue which made pipx incorrectly list apps as part of a venv when they were not installed by pipx. (#650) - Fixed old regression that would prevent pipx uninstall from cleaning up linked binaries if the venv was old and did not have pipx metadata. (#651) - Fixed bugs with suffixed-venvs on Windows. Now properly summarizes install, and actually uninstalls associated binaries for suffixed-venvs. (#653) - Changed venv minimum python version to 3.6, removing python 3.5 which is End of Life. (#666) 0.16.1.0 - Introduce the `pipx.run` entry point group as an alternative way to declare an application for `pipx run`. - Fix cursor show/hide to work with older versions of Windows. (#610) - Support text colors on Windows. (#612) - Better platform unicode detection to avoid errors and allow showing emojis when possible. (#614) - Don't emit show cursor or hide cursor codes if STDERR is not a tty. (#620) - Sped up `pipx list` (#624). - pip errors no longer stream to the shell when pip fails during a pipx install. pip's output is now saved to a log file. In the shell, pipx will tell you the location of the log file and attempt to summarize why pip failed. (#625) - For `reinstall-all`, fixed bug where missing python executable would cause error. (#634) - Fix regression which prevented pipx from working with pythonloc (and `__pypackages__` folder). (#636) 0.16.0.0 - New venv inspection! The code that pipx uses to examine and determine metadata in an installed venv has been made faster, better, and more reliable. It now uses modern python libraries like `packaging` and `importlib.metadata` to examine installed venvs. It also now properly handles installed package extras. In addition, some problems pipx has had with certain characters (like periods) in package names should be remedied. - Added reinstall command for reinstalling a single venv. - Changed `pipx run` on non-Windows systems to actually replace pipx process with the app process instead of running it as a subprocess. (Now using python's `os.exec*`) - [bugfix] Fixed bug with reinstall-all command when package have been installed using a specifier. Now the initial specifier is used. - [bugfix] Override display of `PIPX_DEFAULT_PYTHON` value when generating web documentation for `pipx install` #523 - [bugfix] Wrap help documentation for environment variables. - [bugfix] Fixed uninstall crash that could happen on Windows for certain packages - [feature] Venv package name arguments now do not have to match exactly as pipx has them stored, but can be specified in any python-package-name-equivalent way. (i.e. case does not matter, and `.`, `-`, `_` characters are interchangeable.) - [change] Venvs with a suffix: A suffix can contain any characters, but for purposes of uniqueness, python package name rules apply--upper- and lower-case letters are equivalent, and any number of `.`, `-`, or `_` characters in a row are equivalent. (e.g. if you have a suffixed venv `pylint_1.0A` you could not add another suffixed venv called `pylint--1-0a`, as it would not be a unique name.) - [implementation detail] Pipx shared libraries (providing pip, setuptools, wheel to pipx) are no longer installed using pip arguments taken from the last regular pipx install. If you need to apply pip arguments to pipx's use of pip for its internal shared libraries, use PIP_\* environment variables. - [feature] Autocomplete for venv names is no longer restricted to an exact match to the literal venv name, but will autocomplete any logically-similar python package name (i.e. case does not matter, and `.`, `-`, `_` characters are all equivalent.) - pipx now reinstalls its internal shared libraries when the user executes `reinstall-all`. - Made sure shell exit codes from every pipx command are correct. In the past some (like from `pipx upgrade`) were wrong. The exit code from `pipx runpip` is now the exit code from the `pip` command run. The exit code from `pipx list` will be 1 if one or more venvs have problems that need to be addressed. - pipx now writes a log file for each pipx command executed to `$PIPX_HOME/logs`, typically `~/.local/pipx/logs`. pipx keeps the most recent 10 logs and deletes others. - `pipx upgrade` and `pipx upgrade-all` now have a `--upgrade-injected` option which directs pipx to also upgrade injected packages. - `pipx list` now detects, identifies, and suggests a remedy for venvs with old-internal data (internal venv names) that need to be updated. - Added a "Troubleshooting" page to the pipx web documentation for common problems pipx users may encounter. - pipx error, warning, and other messages now word-wrap so words are not split across lines. Their appearance is also now more consistent. 0.15.6.0 - [docs] Update license - [docs] Display a more idiomatic command for registering completions on fish. - [bugfix] Fixed regression in list, inject, upgrade, reinstall-all commands when suffixed packages are used. - [bugfix] Do not reset package url during upgrade when main package is `pipx` - Updated help text to show description for `ensurepath` and `completions` help - Added support for user-defined default python interpreter via new `PIPX_DEFAULT_PYTHON`. Helpful for use with pyenv among other uses. - [bugfix] Fixed bug where extras were ignored with a PEP 508 package specification with a URL. 0.15.5.1 - [bugfix] Fixed regression of 0.15.5.0 which erroneously made installing from a local path with package extras not possible. 0.15.5.0 - pipx now parses package specification before install. It removes (with warning) the `--editable` install option for any package specification that is not a local path. It also removes (with warning) any environment markers. - Disabled animation when we cannot determine terminal size or if the number of columns is too small. (Fixes #444) - [feature] Version of each injected package is now listed after name for `pipx list --include-injected` - Change metadata recorded from version-specified install to allow upgrades in future. Adds pipx dependency on `packaging` package. - [bugfix] Prevent python error in case where package has no pipx metadata and advise user how to fix. - [feature] `ensurepath` now also ensures that pip user binary path containing pipx itself is in user's PATH if pipx was installed using `pip install --user`. - [bugfix] For `pipx install`, fixed failure to install if user has `PIP_USER=1` or `user=true` in pip.conf. (#110) - [bugfix] Requiring userpath v1.4.1 or later so ensure Windows bug is fixed for `ensurepath` (#437) - [feature] log pipx version (#423) - [feature] `--suffix` option for `install` to allow multiple versions of same tool to be installed (#445) - [feature] pipx can now be used with the Windows embeddable Python distribution 0.15.4.0 - [feature] `list` now has a new option `--include-injected` to show the injected packages in the main apps - [bugfix] Fixed bug that can cause crash when installing an app 0.15.3.1 - [bugfix] Workaround multiprocessing issues on certain platforms (#229) 0.15.3.0 - [feature] Use symlinks on Windows when symlinks are available 0.15.2.0 - [bugfix] Improved error reporting during venv metadata inspection. - [bugfix] Fixed incompatibility with pypy as venv interpreter (#372). - [bugfix] Replaced implicit dependency on setuptools with an explicit dependency on packaging (#339). - [bugfix] Continue reinstalling packages after failure - [bugfix] Hide cursor while pipx runs - [feature] Add environment variable `USE_EMOJI` to allow enabling/disabling emojis (#376) - [refactor] Moved all commands to separate files within the commands module (#255). - [bugfix] Ignore system shared libraries when installing shared libraries pip, wheel, and setuptools. This also fixes an incompatibility with Debian/Ubuntu's version of pip (#386). 0.15.1.3 - [bugfix] On Windows, pipx now lists correct Windows apps (#217) - [bugfix] Fixed a `pipx install` bug causing incorrect python binary to be used when using the optional --python argument in certain situations, such as running pipx from a Framework python on macOS and specifying a non-Framework python. 0.15.1.2 - [bugfix] Fix recursive search of dependencies' apps so no apps are missed. - `upgrade-all` now skips editable packages, because pip disallows upgrading editable packages. 0.15.1.1 - [bugfix] fix regression that caused installing with --editable flag to fail package name determination. 0.15.1.0 - Add Python 3.8 to PyPI classifier and travis test matrix - [feature] auto-upgrade shared libraries, including pip, if older than one month. Hide all pip warnings that a new version is available. (#264) - [bugfix] pass pip arguments to pip when determining package name (#320) 0.15.0.0 Upgrade instructions: When upgrading to 0.15.0.0 or above from a pre-0.15.0.0 version, you must re-install all packages to take advantage of the new persistent pipx metadata files introduced in this release. These metadata files store pip specification values, injected packages, any custom pip arguments, and more in each main package's venv. You can do this by running `pipx reinstall-all` or `pipx uninstall-all`, then reinstalling manually. - `install` now has no `--spec` option. You may specify any valid pip specification for `install`'s main argument. - `inject` will now accept pip specifications for dependency arguments - Metadata is now stored for each application installed, including install options like `--spec`, and injected packages. This information allows upgrade, upgrade-all and reinstall-all to work properly even with non-pypi installed packages. (#222) - `upgrade` options `--spec` and `--include-deps` were removed. Pipx now uses the original options used to install each application instead. (#222) - `upgrade-all` options `--include-deps`, `--system-site-packages`, `--index-url`, `--editable`, and `--pip-args` were removed. Pipx now uses the original options used to install each application instead. (#222) - `reinstall-all` options `--include-deps`, `--system-site-packages`, `--index-url`, `--editable`, and `--pip-args` were removed. Pipx now uses the original options used to install each application instead. (#222) - Handle missing interpreters more gracefully (#146) - Change `reinstall-all` to use system python by default for apps. Now use `--python` option to specify a different python version. - Remove the PYTHONPATH environment variable when executing any command to prevent conflicts between pipx dependencies and package dependencies when pipx is installed via homebrew. Homebrew can use PYTHONPATH manipulation instead of virtual environments. (#233) - Add printed summary after successful call to `pipx inject` - Support associating apps with Python 3.5 - Improvements to animation status text - Make `--python` argument in `reinstall-all` command optional - Use threads on OS's without support for semaphores - Stricter parsing when passing `--` argument as delimiter 0.14.0.0 - Speed up operations by using shared venv for `pip`, `setuptools`, and `wheel`. You can see more detail in the 'how pipx works' section of the documentation. (#164, @pfmoore) - Breaking change: for the `inject` command, change `--include-binaries` to `--include-apps` - Change all terminology from `binary` to `app` or `application` - Improve argument parsing for `pipx run` and `pipx runpip` - If `--force` is passed, remove existing files in PIPX_BIN_DIR - Move animation to start of line, hide cursor when animating 0.13.2.3 - Fix regression when installing a package that doesn't have any entry points 0.13.2.2 - Remove unnecessary and sometimes incorrect check after `pipx inject` (#195) - Make status text/animation reliably disappear before continuing - Update animation symbols 0.13.2.1 - Remove virtual environment if installation did not complete. For example, if it was interrupted by ctrl+c or if an exception occurred for any reason. (#193) 0.13.2.0 - Add shell autocompletions. Also add `pipx completions` command to print instructions on how to add pipx completions to your shell. - Un-deprecate `ensurepath`. Use `userpath` internally instead of instructing users to run the `userpath` cli command. - Improve detection of PIPX_BIN_DIR not being on PATH - Improve error message when an existing symlink exists in PIPX_BIN_DIR and points to the wrong location - Improve handling of unexpected files in PIPX_HOME (@uranusjr) - swap out of order logic in order to correctly recommend --include-deps (@joshuarli) - [dev] Migrate from tox to nox 0.13.1.1 - Do not raise bare exception if no binaries found (#150) - Update pipsi migration script 0.13.1.0 - Deprecate `ensurepath` command. Use `userpath append ~/.local/bin` - Support redirects and proxies when downloading python files (i.e. `pipx run http://url/file.py`) - Use tox for document generation and CI testing (CI tests are now functional rather than static tests on style and formatting!) - Use mkdocs for documentation - Change default cache duration for `pipx run` from 2 to 14 days 0.13.0.1 - Fix upgrade-all and reinstall-all regression 0.13.0.0 - Add `runpip` command to run arbitrary pip commands in pipx-managed virtual environments - Do not raise error when running `pipx install PACKAGE` and the package has already been installed by pipx (#125). This is the cause of the major version change from 0.12 to 0.13. - Add `--skip` argument to `upgrade-all` and `reinstall-all` commands, to let the user skip particular packages 0.12.3.3 - Update logic in determining a package's binaries during installation. This removes spurious binaries from the installation. (#104) - Improve compatibility with Debian distributions by using `shutil.which` instead of `distutils.spawn.find_executable` (#102) 0.12.3.2 - Fix infinite recursion error when installing package such as `cloudtoken==0.1.84` (#103) - Fix windows type errors (#96, #98) 0.12.3.1 - Fix "WindowsPath is not iterable" bug 0.12.3.0 - Add `--include-deps` argument to include binaries of dependent packages when installing with pipx. This improves compatibility with packages that depend on other installed packages, such as `jupyter`. - Speed up `pipx list` output (by running multiple processes in parallel) and by collecting all metadata in a single subprocess call - More aggressive cache directory removal when `--no-cache` is passed to `pipx run` - [dev] Move inline text passed to subprocess calls to their own files to enable autoformatting, linting, unit testing 0.12.2.0 - Add support for PEP 582's `__pypackages__` (experimental). `pipx run BINARY` will first search in `__pypackages__` for binary, then fallback to installing from PyPI. `pipx run --pypackages BINARY` will raise an error if the binary is not found in `__pypackages__`. - Fix regression when installing with `--editable` flag (#93) - [dev] improve unit tests 0.12.1.0 - Cache and reuse temporary Virtual Environments created with `pipx run` (#61) - Update binary discovery logic to find "scripts" like awscli (#91) - Forward `--pip-args` to the pip upgrade command (previously the args were forwarded to install/upgrade commands for packages) (#77) - When using environment variable PIPX_HOME, Virtual Environments will now be created at `$PIPX_HOME/venvs` rather than at `$PIPX_HOME`. - [dev] refactor into multiple files, add more unit tests 0.12.0.4 - Fix parsing bug in pipx run 0.12.0.3 - list python2 as supported language so that pip installs with python2 will no longer install the pipx on PyPI from the original pipx owner. Running pipx with python2 will fail, but at least it will not be as confusing as running the pipx package from the original owner. 0.12.0.2 - forward arguments to run command correctly #90 0.12.0.1 - stop using unverified context #89 0.12.0.0 - Change installation instructions to use `pipx` PyPI name - Add `ensurepath` command 0.11.0.2 - add version argument parsing back in (fixes regression) 0.11.0.1 - add version check, command check, fix printed version update installation instructions 0.11.0.0 - Replace `pipx BINARY` with `pipx run BINARY` to run a binary in an ephemeral environment. This is a breaking API change so the major version has been incremented. (Issue #69) - upgrade pip when upgrading packages (Issue #72) - support --system-site-packages flag (Issue #64) 0.10.4.1 - Fix version printed when `pipx --version` is run 0.10.4.0 - Add --index-url, --editable, and --pip-args flags - Updated README with pipsi migration instructions 0.10.3.0 - Display python version in list - Do not reinstall package if already installed (added `--force` flag to override) - When upgrading all packages, print message only when package is updated - Avoid accidental execution of pipx.**main** pipx-1.0.0/docs/comparisons.md000066400000000000000000000103521416500503600163000ustar00rootroot00000000000000## pipx vs pip * pip is a general Python package installer. It can be used to install libraries or cli applications with entrypoints. * pipx is a specialized package installer. It can only be used to install packages with cli entrypoints. * pipx and pip both install packages from PyPI (or locally) * pipx relies on pip (and venv) * pipx replaces a subset of pip's functionality; it lets you install cli applications but NOT libraries that you import in your code. * you can install pipx with pip Example interaction: Install pipx with pip: `pip install --user pipx` ## pipx vs poetry and pipenv * pipx is used solely for application consumption: you install cli apps with it * pipenv and poetry are cli apps used to develop applications and libraries * all three tools wrap pip and virtual environments for more convenient workflows Example interaction: Install pipenv and poetry with pipx: `pipx install poetry` Run pipenv or poetry with pipx: `pipx run poetry --help` ## pipx vs venv * venv is part of Python's standard library in Python 3.2 and above * venv creates "virtual environments" which are sandboxed python installations * pipx heavily relies on the venv package Example interaction: pipx installs packages to environments created with venv. `pipx install black --verbose` ## pipx vs pyenv * pyenv manages python versions on your system. It helps you install versions like Python 3.6, 3.7, etc. * pipx installs packages in virtual environments and exposes their entrypoints on your PATH Example interaction: Install a Python interpreter with pyenv, then install a package using pipx and that new interpreter: `pipx install black --python=python3.7` where python3.7 was installed on the system with pyenv ## pipx vs pipsi * pipx and pipsi both install packages in a similar way * pipx is under active development. pipsi is no longer maintained. * pipx always makes sure you're using the latest version of pip * pipx has the ability to run a app in one line, leaving your system unchanged after it finishes (`pipx run APP`) where pipsi does not * pipx has the ability to recursively install binaries from dependent packages * pipx adds more useful information to its output * pipx has more CLI options such as upgrade-all, reinstall-all, uninstall-all * pipx is more modern. It uses Python 3.6+, and the `venv` package in the Python3 standard library instead of the python 2 package `virtualenv`. * pipx works with Python homebrew installations while pipsi does not (at least on my machine) * pipx defaults to less verbose output * pipx allows you to see each command it runs by passing the --verbose flag * pipx prints emojies 😀 Example interaction: None. Either one or the other should be used. These tools compete for a similar workflow. ### Migrating to pipx from pipsi After you have installed pipx, run [migrate_pipsi_to_pipx.py](https://raw.githubusercontent.com/pypa/pipx/main/scripts/migrate_pipsi_to_pipx.py). Why not do this with your new pipx installation? ``` pipx run https://raw.githubusercontent.com/pypa/pipx/main/scripts/migrate_pipsi_to_pipx.py ``` ## pipx vs brew * Both brew and pipx install cli tools * They install them from different sources. brew uses a curated repository specifically for brew, and pipx generally uses PyPI. Example interaction: brew can be used to install pipx, but they generally don't interact much. ## pipx vs npx * Both can run cli tools (npx will search for them in node_modules, and if not found run in a temporary environment. `pipx run` will search in `__pypackages__` and if not found run in a temporary environment) * npx works with JavaScript and pipx works with Python * Both tools attempt to make running executables written in a dynamic language (JS/Python) as easy as possible * pipx can also install tools globally; npx cannot Example interaction: None. These tools work for different languages. ## pipx vs pip-run [pip-run](https://github.com/jaraco/pip-run) is focused on running **arbitrary Python code in ephemeral environments** while pipx is focused on running **Python binaries in ephemeral and non-ephemeral environments**. For example these two commands both install poetry to an ephemeral environment and invoke poetry with `--help`. ``` pipx run poetry --help pip-run poetry -- -m poetry --help ``` Example interaction: None. pipx-1.0.0/docs/contributing.md000066400000000000000000000176541416500503600164660ustar00rootroot00000000000000Thanks for your interest in contributing to pipx! Everyone who interacts with the pipx project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the [PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md). ## Running pipx From Source Tree To run the pipx executable from your source tree during development, run pipx from the src directory: ``` python src/pipx --version ``` ## Pre-commit The use of [pre-commit](https://pre-commit.com/) is recommended. It can show all and fix some lint errors before commit, saving you the trouble of finding out later that it failed CI Lint errors, and saving you from having to run `nox -s lint` separately. In the pipx git repository is a `.pre-commit-config.yaml` configuration file tailored just for pipx and its lint requirements. To use pre-commit in your clone of the pipx repository, you need to do the following **one-time setup procedure**: 1. Install pre-commit using `pipx install pre-commit` 2. In the top level directory of your clone of the pipx repository, execute `pre-commit install` Afterwards whenever you commit in this repository, it will first run pipx's personalized lint checks. If it makes a fix to a file (e.g. using `black` or `isort`), you will need to `git add` that file again before committing it again. If it can't fix your commit itself, it will tell you what's wrong, and you can fix it manually before re-adding the edited files and committing again. If for some reason you want to commit and skip running pre-commit, you can use the switch `git commit --no-verify`. ## Running Tests ### Setup pipx uses an automation tool called [nox](https://pypi.org/project/nox/) for development, continuous integration testing, and various tasks. `nox` defines tasks or "sessions" in `noxfile.py` which can be run with `nox -s SESSION_NAME`. Session names can be listed with `nox -l`. Install nox for pipx development: ``` python -m pip install --user nox ``` Tests are defined as `nox` sessions. You can see all nox sessions with ``` nox -l ``` At the time of this writing, the output looks like this ``` - refresh_packages_cache-3.6 -> Populate .pipx_tests/package_cache - refresh_packages_cache-3.7 -> Populate .pipx_tests/package_cache - refresh_packages_cache-3.8 -> Populate .pipx_tests/package_cache - refresh_packages_cache-3.9 -> Populate .pipx_tests/package_cache - tests_internet-3.6 -> Tests using internet pypi only - tests_internet-3.7 -> Tests using internet pypi only - tests_internet-3.8 -> Tests using internet pypi only - tests_internet-3.9 -> Tests using internet pypi only * tests-3.6 -> Tests using local pypiserver only * tests-3.7 -> Tests using local pypiserver only * tests-3.8 -> Tests using local pypiserver only * tests-3.9 -> Tests using local pypiserver only - test_all_packages-3.6 - test_all_packages-3.7 - test_all_packages-3.8 - test_all_packages-3.9 - cover -> Coverage analysis * lint - develop-3.6 - develop-3.7 - develop-3.8 - develop-3.9 - build - publish * build_docs - publish_docs - watch_docs - pre_release - post_release - create_test_package_list-3.6 - create_test_package_list-3.7 - create_test_package_list-3.8 - create_test_package_list-3.9 ``` ### Unit Tests To run unit tests in Python3.9, you can run ``` nox -s tests-3.9 ``` !!! tip You can run a specific unit test by passing arguments to pytest, the test runner pipx uses: ``` nox -s tests-3.8 -- -k EXPRESSION ``` `EXPRESSION` can be a test name, such as ``` nox -s tests-3.8 -- -k test_uninstall ``` Coverage errors can usually be ignored when only running a subset of tests. ### Running Unit Tests Offline Running the unit tests requires a directory `.pipx_tests/package_cache` to be populated from a fixed list of package distribution files (wheels or source files). If you have network access, `nox -s tests` automatically makes sure this directory is populated (including downloading files if necessary) as a first step. Thus if you are running the tests with network access, you can ignore the rest of this section. If, however, you wish to run tests offline without the need for network access, you can populate `.pipx_tests/package_cache` yourself manually beforehand when you do have network access. #### Populating the cache directory using nox To populate `.pipx_tests/package_cache` manually using nox, execute: ``` nox -s refresh_packages_cache ``` This will sequence through available python executable versions to populate the cache directory for each version of python on your platform. #### Populating the cache directory without nox An alternate method to populate `.pipx_tests/package_cache` without nox is to execute: ``` mkdir -p .pipx_tests/package_cache python3 scripts/update_package_cache.py testdata/tests_packages .pipx_tests/package_cache ``` You must do this using every python version that you wish to use to run the tests. ### Lint Tests ``` nox -s lint ``` ### Installing or injecting new packages in tests If the tests are modified such that a new package / version combination is `pipx install`ed or `pipx inject`ed that wasn't used in other tests, then one must make sure it's added properly to the packages lists in `testdata/tests_packages`. To accomplish this: * Edit `testdata/tests_packages/primary_packages.txt` to add the new package(s) that you wish to `pipx install` or `pipx inject` in the tests. Then using Github workflows to generate all platforms in the Github CI: * Manually activate the Github workflow: Create tests package lists for offline tests * Download the artifact `lists` and put the files from it into `testdata/tests_packages/` Or to locally generate these lists from `testdata/tests_packages/primary_packages.txt`, on the target platform execute: * `nox -s create_test_package_list` Finally, check-in the new or modified list files in the directory `testdata/tests_packages` ## Testing pipx on Continuous Integration builds When you push a new git branch, tests will automatically be run against your code as defined in `.github/workflows/on-push.yml`. ## Building Documentation `pipx` autogenerates API documentation, and also uses templates. When updating pipx docs, make sure you are either modifying a file in the `templates` directory, or the `docs` directory. If in the `docs` directory, make sure the file was not autogenerated from the `templates` directory. Autogenerated files have a note at the top of the file. You can generate the documentation with ``` nox -s build_docs ``` This will capture CLI documentation for any pipx argument modifications, as well as generate templates to the docs directory. To preview changes, including live reloading, open another terminal and run ``` nox -s watch_docs ``` ### Publishing Doc Changes to GitHub pages ``` nox -s publish_docs ``` ## Releasing New `pipx` Versions ### Pre-release First, make sure the changelog is complete. Next decide what the next version number will be. Then, from a clone of the main pypa pipx repo (not a fork) execute: ``` nox -s pre_release ``` Enter the new version number when asked. When the script is finished, check the diff it produces. If the diff looks correct, commit the changes as the script instructs, and push the result. The script will modify `src/pipx/version.py` to contain the new version, and also update the changelog (`docs/changelog.md`) to specify the new version. ### Release To publish to PyPI simply create a "published" release on Github. This will trigger Github workflows that both publish the pipx version to PyPI and publish the pipx documentation to the pipx website. ### Post-release From a clone of the main pypa pipx repo (not a fork) execute: ``` nox -s post_release ``` When the script is finished, check the diff it produces. If the diff looks correct, commit the changes as the script instructs, and push the result. This will update pipx's version in `src/pipx/version.py` by adding a suffix `"dev0"` for unreleased development, and will update the changelog to start a new section at the top entitled `dev`. pipx-1.0.0/docs/examples.md000066400000000000000000000042731416500503600155660ustar00rootroot00000000000000## `pipx install` examples ``` pipx install pycowsay pipx install --python python3.6 pycowsay pipx install --python python3.7 pycowsay pipx install git+https://github.com/psf/black pipx install git+https://github.com/psf/black.git@branch-name pipx install git+https://github.com/psf/black.git@git-hash pipx install https://github.com/psf/black/archive/18.9b0.zip pipx install black[d] pipx install --include-deps jupyter ``` ## `pipx run` examples pipx enables you to test various combinations of Python versions and package versions in ephemeral environments: ``` pipx run BINARY # latest version of binary is run with python3 pipx run --spec PACKAGE==2.0.0 BINARY # specific version of package is run pipx run --python 3.4 BINARY # Installed and invoked with specific Python version pipx run --python 3.7 --spec PACKAGE=1.7.3 BINARY pipx run --spec git+https://url.git BINARY # latest version on default branch is run pipx run --spec git+https://url.git@branch BINARY pipx run --spec git+https://url.git@hash BINARY pipx run pycowsay moo pipx --version # prints pipx version pipx run pycowsay --version # prints pycowsay version pipx run --python pythonX pycowsay pipx run pycowsay==2.0 --version pipx run pycowsay[dev] --version pipx run --spec git+https://github.com/psf/black.git black pipx run --spec git+https://github.com/psf/black.git@branch-name black pipx run --spec git+https://github.com/psf/black.git@git-hash black pipx run --spec https://github.com/psf/black/archive/18.9b0.zip black --help pipx run https://gist.githubusercontent.com/cs01/fa721a17a326e551ede048c5088f9e0f/raw/6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py ``` ## `pipx inject` example One use of the inject command is setting up a REPL with some useful extra packages. ``` pipx install ptpython pipx inject ptpython requests pendulum ``` After running the above commands, you will be able to import and use the `requests` and `pendulum` packages inside a `ptpython` repl. ## `pipx list` example ``` > pipx list venvs are in /Users/user/.local/pipx/venvs binaries are exposed on your $PATH at /Users/user/.local/bin package black 18.9b0, Python 3.7.0 - black - blackd package pipx 0.10.0, Python 3.7.0 - pipx ``` pipx-1.0.0/docs/getting-started.md000066400000000000000000000005231416500503600170470ustar00rootroot00000000000000Now that you have pipx installed, you can install a program: ``` pipx install PACKAGE ``` for example ``` pipx install pycowsay ``` You can list programs installed: ``` pipx list ``` Or you can run a program without installing it: ``` pipx run pycowsay moooo! ``` You can view documentation for all commands by running `pipx --help`. pipx-1.0.0/docs/how-pipx-works.md000066400000000000000000000051131416500503600166600ustar00rootroot00000000000000## How it Works When installing a package and its binaries (`pipx install package`) pipx will - create directory `~/.local/pipx/venvs/PACKAGE` - create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/pipx/shared/` - ensure all packaging libraries are updated to their latest versions - create a Virtual Environment in `~/.local/pipx/venvs/PACKAGE` that uses the shared pip mentioned above but otherwise is isolated (pipx uses a [.pth file]( https://docs.python.org/3/library/site.html) to do this) - install the desired package in the Virtual Environment - expose binaries at `~/.local/bin` that point to new binaries in `~/.local/pipx/venvs/PACKAGE/bin` (such as `~/.local/bin/black` -> `~/.local/pipx/venvs/black/bin/black`) - As long as `~/.local/bin/` is on your PATH, you can now invoke the new binaries globally When running a binary (`pipx run BINARY`), pipx will - create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/pipx/shared/` - ensure all packaging libraries are updated to their latest versions - create a temporary directory (or reuse a cached virtual environment for this package) with a name based on a hash of the attributes that make the run reproducible. This includes things like the package name, spec, python version, and pip arguments. - create a Virtual Environment inside it with `python -m venv` - install the desired package in the Virtual Environment - invoke the binary These are all things you can do yourself, but pipx automates them for you. If you are curious as to what pipx is doing behind the scenes, you can always pass the `--verbose` flag to see every single command and argument being run. ## Developing for pipx If you are a developer and want to be able to run ``` pipx install MY_PACKAGE ``` make sure you include an `entry_points` section in your `setup.py` file. ``` setup( # other arguments here... entry_points={ 'console_scripts': [ 'foo = my_package.some_module:main_func', 'bar = other_module:some_func', ], 'gui_scripts': [ 'baz = my_package_gui:start_func', ] } ) ``` In this case `main_func` and `some_func` would be available to pipx after installing the above example package. For a real-world example, see [pycowsay](https://github.com/cs01/pycowsay/blob/master/setup.py)'s `setup.py` source code. You can read more about entry points [here](https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation). pipx-1.0.0/docs/index.md000077700000000000000000000000001416500503600165362../README.mdustar00rootroot00000000000000pipx-1.0.0/docs/installation.md000066400000000000000000000047501416500503600164510ustar00rootroot00000000000000## System Requirements python 3.6+ is required to install pipx. pipx can run binaries from packages with Python 3.3+. Don't have Python 3.6 or later? See [Python 3 Installation & Setup Guide](https://realpython.com/installing-python/). You also need to have `pip` installed on your machine for `python3`. Installing it varies from system to system. Consult [pip's installation instructions](https://pip.pypa.io/en/stable/installing/). Installing on Linux works best with a [Linux Package Manager](https://packaging.python.org/guides/installing-using-linux-tools/#installing-pip-setuptools-wheel-with-linux-package-managers). pipx works on macOS, linux, and Windows. ## Install pipx On macOS: ``` brew install pipx pipx ensurepath ``` Otherwise, install via pip (requires pip 19.0 or later): ``` python3 -m pip install --user pipx python3 -m pipx ensurepath ``` ### Installation Options The default binary location for pipx-installed apps is `~/.local/bin`. This can be overridden with the environment variable `PIPX_BIN_DIR`. pipx's default virtual environment location is `~/.local/pipx`. This can be overridden with the environment variable `PIPX_HOME`. ## Upgrade pipx On macOS: ``` brew update && brew upgrade pipx ``` Otherwise, upgrade via pip: ``` python3 -m pip install --user -U pipx ``` ### Note: Upgrading pipx from a pre-0.15.0.0 version to 0.15.0.0 or later After upgrading to pipx 0.15.0.0 or above from a pre-0.15.0.0 version, you must re-install all packages to take advantage of the new persistent pipx metadata files introduced in the 0.15.0.0 release. These metadata files store pip specification values, injected packages, any custom pip arguments, and more in each main package's venv. If you have no packages installed using the `--spec` option, and no venvs with injected packages, you can do this by running `pipx reinstall-all`. If you have any packages installed using the `--spec` option or venvs with injected packages, you should reinstall packages manually using `pipx uninstall-all`, followed by `pipx install` and possibly `pipx inject`. ## Shell Completion You can easily get your shell's tab completions working by following instructions printed with this command: ``` pipx completions ``` ## Install pipx Development Versions New versions of pipx are published as beta or release candidates. These versions look something like `0.13.0b1`, where `b1` signifies the first beta release of version 0.13. These releases can be tested with ``` pip install --user pipx --upgrade --dev ``` pipx-1.0.0/docs/programs-to-try.md000066400000000000000000000034621416500503600170350ustar00rootroot00000000000000## Programs Here are some programs you can try out. If you've never used the program before, make sure you add the `--help` flag so it doesn't do something you don't expect. If you decide you want to install, you can run `pipx install PACKAGE` instead. ### ansible IT automation ``` pipx install --include-deps ansible ``` ### asciinema Record and share your terminal sessions, the right way. ``` pipx run asciinema ``` ### black uncompromising Python code formatter ``` pipx run black ``` ### pybabel internationalizing and localizing Python applications ``` pipx run --spec=babel pybabel --help ``` ### chardetect detect file encoding ``` pipx run --spec=chardet chardetect --help ``` ### cookiecutter creates projects from project templates ``` pipx run cookiecutter ``` ### create-python-package easily create and publish new Python packages ``` pipx run create-python-package ``` ### flake8 tool for style guide enforcement ``` pipx run flake8 ``` ### gdbgui browser-based gdb debugger ``` pipx run gdbgui ``` ### hexsticker create hexagon stickers automatically ``` pipx run hexsticker ``` ### ipython powerful interactive Python shell ``` pipx run ipython ``` ### jupyter web-based notebook environment for interactive computing ``` pipx run jupyter ``` ### pipenv python dependency/environment management ``` pipx run pipenv ``` ### poetry python dependency/environment/packaging management ``` pipx run poetry ``` ### pylint source code analyzer ``` pipx run pylint ``` ### pyinstaller bundles a Python application and all its dependencies into a single package ``` pipx run pyinstaller ``` ### pyxtermjs fully functional terminal in the browser   ``` pipx run pyxtermjs ``` ### shell-functools Functional programming tools for the shell ``` pipx install shell-functools ``` pipx-1.0.0/docs/troubleshooting.md000066400000000000000000000067511416500503600172020ustar00rootroot00000000000000## `reinstall-all` fixes most issues The following command should fix many problems you may encounter as a pipx user: ``` pipx reinstall-all ``` This is a good fix for the following problems: * System python was upgraded and the python used with a pipx-installed package is no longer available * pipx upgrade causes issues with old pipx-installed packages pipx has been upgraded a lot over the years. If you are a long-standing pipx user (thanks, by the way!) then you may have old pipx-installed packages that have internal data that is different than what pipx currently expects. By executing `pipx reinstall-all`, pipx will re-write its internal data and this should fix many of issues you may encounter. **Note:** If your pipx-installed package was installed using a pipx version before 0.15.0.0 and you want to specify particular options, then you may want to uninstall and install it manually: ``` pipx uninstall pipx install ``` ## Diagnosing problems using `list` ``` pipx list ``` will not only list all of your pipx-installed packages, but can also diagnose some problems with them, as well as suggest solutions. ## Specifying pipx options The most reliable method to specify command-line options that require an argument is to use an `=`-sign. An example: ``` pipx install pycowsay --pip-args="--no-cache-dir" ``` Another example for ignoring ssl/tls errors: ``` pipx install termpair --pip-args '--trusted-host files.pythonhosted.org --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host github.com'" ``` ## Check for `PIP_*` environment variables pipx uses `pip` to install and manage packages. If you see pipx exhibiting strange behavior on install or upgrade, check that you don't have special environment variables that affect `pip`'s behavior in your environment. To check for `pip` environment variables, execute the following depending on your system: ### Unix or macOS ``` env | grep '^PIP_' ``` ### Windows PowerShell ``` ls env:PIP_* ``` ### Windows `cmd` ``` set PIP_ ``` Reference: [pip Environment Variables](https://pip.pypa.io/en/stable/user_guide/#environment-variables) ## `pipx` log files Pipx records a verbose log file for every `pipx` command invocation. The logs for the last 10 `pipx` commands can be found in `$PIPX_HOME/logs`. For most users this location is `~/.local/pipx/logs`, where `~` is your home directory. ## Debian, Ubuntu issues If you have issues using pipx on Debian, Ubuntu, or other Debian-based linux distributions, make sure you have the following packages installed on your system. (Debian systems do not install these by default with their python installations.) ``` sudo apt install python3-venv python3-pip ``` Reference: [Python Packaging User Guide: Installing pip/setuptools/wheel with Linux Package Managers](https://packaging.python.org/guides/installing-using-linux-tools) ## Does it work to install your package with `pip`? This is a tip for advanced users. An easy way to check if pipx is the problem or a package you're trying to install is the problem, is to try installing it using `pip`. For example: ### Unix or macOS ``` python3 -m venv test_venv test_venv/bin/python3 -m pip install ``` ### Windows ``` python -m venv test_venv test_venv/Scripts/python -m pip install ``` If installation into this "virtual environment" using pip fails, then it's likely that the problem is with the package or your host system. To clean up after this experiment: ``` rm -rf test_venv ``` pipx-1.0.0/get-pipx.py000066400000000000000000000005271416500503600146030ustar00rootroot00000000000000#!/usr/bin/env python3 import sys def fail(msg): sys.stderr.write(msg + "\n") sys.stderr.flush() sys.exit(1) def main(): fail( "This installation method has been deprecated. " "See https://github.com/pypa/pipx for current installation " "instructions." ) if __name__ == "__main__": main() pipx-1.0.0/logo.png000066400000000000000000002141201416500503600141360ustar00rootroot00000000000000PNG  IHDRxh]K= cHRMz&u0`:pQ<bKGDC pHYs..*' tIME*:igzTXtRaw profile type exifhYv#9E^'r8;EHvk;ӒPxx}ÿ-PŚvW__*zCG˷7X}>l>',~̓q}_7/njJT"?_(~\}>vq_/_Kr_O=ҷ+ھݽ{^w7R) NJY)=ߝ-.:{S7մH7qk>xy֊Ӕf=@ҟE /ϵJ+đ/;~~_<ѽ] >kźcN'Gѐt5w_{B)sGSLI߰U>X,~qma1ЁH-gK:63Xy.5O:DNқR=cP h Djc!Ej&]*j5X515fF+6iڬ(Lv ࢃS=8bgui>>.Ylyov}P:c~kznk~Zsߵw>ٷ)ӉxXw@gYlŞ t-7g''ew:} RTou.xoݻsǣ(iO4 qi#=O<vQfab)5ux'-7M1iΓx|[ >5ơ:'mLйd3x_=g3gG:k֬iuuu]nV8xg~nf]-޲ :99q̶f%w:=ond9\ܺ0CޚޤE8!+B;&pfeX:r$UfG&U%c<ӏSZ;Vrk҄f 8g+9NKqIzhfAP4m0r{EUl(Z,l?uMG~' ļZQP˧g.p` ы:Urf:/0+Z" >Oe]>{6uA@qΖi7nXSg#3]@nk>va\ަebq<}19DLPudKK${-*z1(TU˧o NNԆyzaՇ̡mKќZ]uN[/"R`p39UUf>ԆPלw-24Lmi׫"`e \82HXq/ulB /i[Xx#5Я䴏ӄfo OkilCVxskY|)k/JcBԠ4ct&w]WA/< pذ+ !j^ @nvgQ .X|=6uǶ7أ(]|3c 23ڙL  [Y"zZLrG :cJ]9fؑ5Ήk@ .8q\\|˖cBѬicknA p4VcU-g4>,ROcgߕ *;E,)1H"v4ykjaN<&TQlnY`1ZCNOA-\.Kc#qp0A , BN'a%ųֵ)f|Gnba(8{0N_ыL Iܘ{ oN3²@k`o=>7w` K#L vdvas4UI܇5*b`m⧬%۞rc&ܿ٥zs 0m^!*X@5͙ 8(wh~݁kǎ=O (%NW:q BR(/S]qh *T$ Ipv0&b;;uvPHZ/eQW ?2qOLl%Qdn7wR4|o\JhDo|H ZL10b0@!Dv1y<\&gMO\'Vl@uI}q'lƤsI;ۃ\sc{=8Q J<vs˝:[ 8Cn?@Xg.#m f1HSx DžL*b7䆠N$\\CT!19%{BCv'a*.M{o Cv5d b<{աHg})p e5$_5PE v$6ȅuQDte:.}(%lq^7d$Q&\қzRl… l^ '| 4OX:Q}>ࣈx1w+A1w=++߀(d3ŁoE?}E$DWA1,e_CUXV*,xE+PغnMNdZ锎FނVHa>9Cn#FA=\}\[B0vcR ;c$QIu 9n6!\~C\][ 8wB>d̘dTj") X-(n4(' ~A^!G sR&~(%,0H8-b{EF˷'F7-0FShbDZM6sJ[3ȝ7}wok!Ѹ!A:XsN`Ua᎟]TaG),!@9pGB.B}7 V">?d4>' O.=D0ڼ.҃`;O>@|u{l'kOVn4Y8+n6(EoOc֡.GӃ_?(3hg;g-L?,x۠7u,c窾eؖ})Ƨz6=:ρ=NĥƐs׾ =!@-0BT|,EɍΪk;@cIHY7.CrF]b*ʼnQ0D_[ū8{/e2IDATxwdy?kPι9gBe!,Klrg왱}g|Ͼx|qd$-+H,Dt7xrycn>9]j]w}_x%2222N(?/[eqx!qey{7###ΩhhrX4 x1IdwFCFFFFQX?D#Q($ *FP(= E,C63222^Y,L1'fͣYb/ng]*9XÄ@rL3222};ub(r2U*x,,iNC# _8o| njy9Zl_c/}/V;HUPl@M.Q+|'^xg_SфV6f%RKIBjsY?Tj~A{D&;YoɝQXA(AT;<9 ' .6PۑAwڼ;^6"q&JG؄X,Fy+͝j}i&ⓒ>20E >T ~Ť5tz!CS/c |FF O* $"!)s|On;rImD"n=Nz9XY],־ "bu1 MǕ |FF MQ#=bc*!Zu(] _<gwoF8E;}c5!(B)(-rps9~x\ aDe?,q²N =Xt۽)IJ+!+b$W!@S;χ ׬Y=JQ ^CYiEصW#5h y;e|FF livtJ<{hJQO[mIΥXF5!w$ }Sh{OP[)-f˽! ~e?L322NH0^/QZf:Tzs ܓGS%gK)*yS4>صEQɹ-j߅\ՐkÃ|ُ1Bqk+ctpLJ\\+IhNN\wɊF!w-=+b qؑ6#uw$LY R_e][\S~Ő_ %HǙ |FF I X94yTUy;3E*MQ^c.myK2Ln)Dwe3lgGnG,qe3A=vJټ l XTh6ɚqB!pg~7./r{Q]e+֠oCp(;-4N,N5U"?1uM=h vc/6b7aS {[΁[A>322NJSorHR1嗆C7J!x֥ R/WS$AڍT<+,c'H8ś@p}l<|FRXނn;3 4a{|ǜE'yW{(fC@ocYqб.jCPMSX@%}@$;Y;ÃLCw wR/囀Ӂh 6ófnf+5,xG_c" $U'ݸN'o%g ' X$sS;dr҂a| | Pa'-4v`mV]BNK O> K>GFF랡OJMo 8 C;۩Iț҈@،[5ǗGIܷؠ$DRMS$oe_FiWxA.90&5,*)7d_qg|FF0dxPМv$6[?&,2bN3RNnrsZ?ÒACua>V? E0}ǝ |FFKZfWumNmqN,*3HoWӝrbRlfx-e!sV.p6`1$1{(`Òu:6ShefGg_zL322^JBs**iɦGqx#P:r-B[=X1=+էAKlg;B~FZ G7ܟVV/###YY,1-sM̹hrhn.G%6I.vɍ5]2M4f8]br+mG [S'3tKk\=.p ~)[{hlP/*@^AA&K*1o/NaM.InI 0ѻ@OT|}0l9PҽI٘GhZ${bE}㩓*g.#{p1Z\tǟou1Z3p{X9`HQ%1>##uG1K/&x?ٍ׷˞arN>6ĸgdH83^NG#ږ\/B:ep6M=qk"%ࢴu$>[f)?"sp+Q0+F|` ʹ6v/gddX>IAXD*%jBe53x1Ktz*$H6+XD5g,bI,kTwqO\`.jh^tj-$\9=eg+ul%ʅ ZmV@O=+JCzbY5fJA/IVɶuTT\9$]$A"eBOCq!@Ֆ܂+HuE:# k, dB,Au6gddn$cϴl3{c: G#_I( K;P& ]H-K7N9m[N$%_@/}ݦ~lZ0Zb3L:9%輇v26/y)-X>B5G& ֥e α5Llv Gt,.GQ9|{31u\JmHdužxSs p^d8({ms *N\tD 'GɅ9(8 ^1>##u&R: Ŧ5:^4+.ժxr##5Xx;F[}HWDFքx[ܐoG1#DAogdٸswU]oX,݅A'Z$J%2֣֫\ݕv;;do"CTܧyCKD‚],.̈́? ͇.e/dG%bbDX%EQnV]yH9C.71OG좰NPJSHjK)6#uK: `T1,WF>1]Tr(D:<׸s)[Y?9^~ -r |FFkrqbҚAl\(KhqŠA$y,"1/m(82/ CW3PĢEk^S䭻Hܵ7f@$liuM)##5M!D] gǣ@ouSRd.n~X MB.B--h(_R4IEww$ 3ޫ{..ס8] u =u,,GJRe*l@Q͠x(7-ieg[%0TVҝYDTt*$E9zQ%7Aх ]Cec ;L>; ;f?A:c-ѱ1 jY%Smn[ʍ۩0ch>)ʨFж.jooDoS \ J[,G3e_jvNǏs[(X!Snf_{E)#"ǒ,l\maKƝwmZzo~$-cq[t#S,b[CEOJIJu`_ftX6,H⽲lytKp7ha?w$rv _q>##5"u;ۛ]u jqo'=(84$ĩ/>/r#rR  @ J h1rtkJv6$A CEX=4c<"꽓h1_Dު}Tj'XV^J9xoc |FFkHepylrӊ}9N #qfܑ%u耭smj$vE1@Sp@' -@>KgBy<5sf 5Kj3XFo\+ ]c_b\'Ź ,#sKTӁҐvJ! _P>87  ߀g0?P1=D-%0V~=]Nr斏5m=. |FFkݿd=[HǺ VrUߎ@r(8edah+)j WXA4[)lu> b3,a}-ӽEhPoar/WtE/֝?[ۘegqInDrInYׯs )w7]q=X쨩g,1ASbe5&)fh)Jy@tF'(:>5Q]XLu0<.>vgddjN>A~&dɈj ®"ihL@Aj>OUlFȓ}NjFJ+ %0506VJÄ'o X0RgJѣL8?% _>##UKרZ=G6\\u{t,ĎW1Ͱb?FQKnPIw`G>@I.Bف;'Pɧ,rꠋ9l~/gddz %^>&zk:g"H dZ32X>#Zq# P=y&MH5Eh #^BkJf|($'a6iCۛ gB\}ۛ>##UY iRs^^$ SJ҂4{{Qhwq)NkLQȄ~NeWE廘DTr[ٰԚN˰dhF3Kddd*X}ݏYvzM+񦠼mxPK ,kA݆B X b .cES5dӤg, -jG0\)UL>gkqrc]StB`$O3ۘf`f3>9嗔F Ky-8 !PӈrQPfYǑS>MOւ+P-RL&z)XZS*F+5͛t$U{TJ;|mL3Șwu Ռ\fY y9b*ɇϑ0/ij}0cC(@B36W}\ J)O<@7_W"VVQKHfV8k&N% YXvigxK;eόFez%Q]R#7Z2sޤvY< jJMfdd+'MV[/{c KV^pUM6c 鐐_ڲ+[ +A+gJʨd(ڄYk@la ck?\Cj ~/t(F=2YF!yN|u@>T{.{U,raww p#8gddއ`Ni e]Դ [HZҤKSp4E$e̻'(ziBҴ (`XT4#,vJ B73bڗ8#(MOKmz^׍rJ/>gqY~ ,SmHuH}mZ4 < kk%bq# fNtTDcT?>P^X,Ta'ΙgddwK5-žOӲ{9]E*,ыSB(j`U8jX'$uKҰfh7MڑDFe:4 T=[4„/ѻrw3d))YPXèh׼s6ɚq\Y>M+ A[EqDTGV*"61 k2c!rcg Q ^ %\n56QB+2nJ@,A3!ZeA R^{)NS.ؾMp^:8e?kMZ0)+[q^$15T)H }*jTxv8t zgMYQBѲ(ڽ% C/ɞ`$g0<  ?#o}i9w`1܃A]*JC>Y&#(8.G B.y?kD:e6uZM%a i!iU&*P"FහYhܑۏi8Ѝ6.ޥcyYЎ4#L32&0qb /oDESԦp~bo.Z"$#T-y"0It0m;4k`,ZMWh݄40F03 EX^>rFThxo_E/)8ʤNt#-xgq$hAP(L䏙ZqViwQՒH'A墠N H M@J 9!!9K=L> e{08߾W ߃o*ga ^~}}=Fp0Zv3hyo@p7i Zzd␥@5rx^<>՛IoV'i"HFCq h+46،cӀL䏁|@L^L783e"s7DoxG 3$Päg| ܻOxX )v$┤ &EӉMD/Psa&==(]ܟ W|Ǣn'BcyiEO=BOmO%q>#B$=lB$u,b/ۃu%sh ,Fmb,1;iq~(h89aMc<3j3* ج!Ê%0X}q{1q L0L=1 ,+?'N4~IC OEkVX~[n^K#x vG1.AC5B`i jH[6W_ē7s7TV>jA0Cܳf,` x\&€f9XiBIT{:M ,C1U3D3̲=dxY60{#h%ݸHB70.@qIrD><7eܳ2Ɍ>h ȡ96 E6zho @a &z'xI9,lbYjRF +,U%F f`$!H65.&LjzDBEڱAhZ:hDht¦4FbT͌fq#`؁E#}pRw  nr5A(b87^6IUfM`(?1ocE}RQ ݂ n\1;b}'6#yAu)-nWGhN_ I;">IQ4!y)|/a"* , Ms܆\{j] 7 LMf` o jQ"B<r̨MCtP1Δ1wY<|"R4|- 1P ڮY%4]bZmjJ&5Ӌag1ܯ SفN1,}/{ۚ.֤F*MN6CCw2EoB\s?̧&ZA<; ^sq=gF4`-"ZQU@pr  Sx u`Ao؂ zPNpwH3uvz2JEC֏YaGĢHA>H;J !PӢknr=>i؞L3Nx 9qR5\lmMES lt`?8$k>7KSw,I̝(q u_Qof/  |#+ ]1e4f0FU[xƗ  q@@BPM f[)#yXl2z E [YTS‹`_A'|JP?o\Ьda'|Q?,v5 -K9T=܍X-<6Pф/g0OoVMWoS0&Qp8t)jZ?u8Ok U*OSWWJ؂T\ @B4~}֣[󠯄CA%PˆGBA0rqiD1aCgL Vfvbsi*b'\Lz.9y`<˂Ԋ{a`t(D# r.'JUg'>}\H&' p1YL!Ĉ"E?@xyOY>ܪM:>`] @6J9+') n&N %(6`oMEOzk,DrIBStT܌f =£ Nù 20Oacn nއXa8mQ1\D֣4|] L<lBplE\];cŻO;=-ߨX󊋄g]E+?65M=g8&:fhbƑ˷⮯c؅;8fħ<|Oo_Ja)"FhX*X|*k^es>,z foc6t HaS}+aZU$d&&mh[06]wvb'>¢5ײϫF|ȂrTrc_70 IFimu+|z|BFT |`^L&' "g{(֓F/ /0Oe׃p{03c/- @-ܨi337Cre`"*#`i~mմ)i S>G,SJ44g<Ը]Uʩg 2kv4ﳷs5XD .Q#ZDiښrp5Dy;?g00^`x(69֤aMBd kcc \)?w#Z9mb8o>&D⢈{p0k1L~@-Ր,ov.0+)`p:!aG¬mFW&~ @@_UG$^ɤ+>D&z+J':Ue:<(%RL" VO7'>"@[$M1\܃[Pq+t)G lZJxW4 pE6<ų"{2b  Apݨ!m AZo1Ǿ7t h#elR$wBZQkN݂Д4f#rO5`O4dT:K --k N3Tah}B .f0iGrՕ!!B\0_L3Nܹq*a3)F#ֹC| М83I>~̧zG} k"8Dž/P BxCt!P K1c_ B^,ZP!ĤTqU3&!8Eбa޳bY*sCxr8i ,+,,monBӋ,&:)J_X$GpJx8;4G&' 1x.ѰECMn`7]obakڏ@ȃtB<1D+j+ R&{nNp,'}$Snz7c.v _ IG{ѧ[@ At$EâOĤII|/VurYL X^P>X-3Gĩ0h_M;%!'DEN07$3kT>Mg8|Ql;S6kX’޸{?H׽Qڙ5]qr {G,sxPY .?EVoAEkOr~}X-ۆL*4iT`%@G*9(4=bD(s n$&ct]k sd1cws4@P b: w?07o-|& a QTm,L:9{KijUKF)QHr%BF vքYO]:D$QzUNMS&oܣ(a,0V@1yl~ujHBU%M=ނ>z[2:s. [X}7" S`ˇ)~``,2u|>28ha4Al]xtN]u B9#JU[ /g# 8򟑂rR*8o]~ZA7DrV=(M$jmgOWHɌ bfFh*8w!R'hhZ&5#Ra60#m}P@2 rЍ: %B)V )_y_=셚òn$$Gz@t$3pz$įXm ԴKPdnG 4ZTp.b#Efz62BAدyG&}/GJ%z8 Qzh]5|` j-w>d3)p'&,*xGz\;_[>)VG+y.ys/Ɣ x ގ-~l#d!FQ*h&/ / -~N޽ыi퓸-v˿=٩ҡGE 9zTOS9kd|iCWyŰȜ# zO#LLznԤ $fQK4CBeOsx5~22^Q?NCt?5">v2`$@]%!ˆyUwἷ!쟢I^(~Ylc 5,ގbԲhoL ~A0⛥L)iЁ91 w h2D = NHFc@GI40QOv Sf"h0'@X²bɐL?| | ǃb= Q,GO> mҖQ!yS9jmI"̥D#c&eV"7K l\41 /wNL> Y1 E$NQQ\;(B&YrdDPhZ }Ϩ9!I$|=|@5RsQ݅)p4ȯEeG28!A"ѼC贘޻k~}ԐB$(3zhB|*.YږH;fgƬZՑ0~3G@S) 7gu4x| | I;)EݤG9`o: bXmQ&c̜b sHYaVrR .Uy=>D٨H4k1ʿ% ymb'!+BќUu[ZyF$$$B∔"zi~0q.(XМ{_" GQXpމ,{~%|wS~b8s-8,[2oX>$N&'!z,4)GE.th 3Ah6`>/H| Btң:$q\F&Cs3 bҞ4鍴YA0addnj)8G}ems}N` ˈŵjXXgpVh701g28!y2|U$Cn޼LѠn){6/Dy] t83р  qaĘ[w Zn2w<㘋ߨJDbr~1 ygxEdQEXkS>z_P)<<+@ ItgXf(6ɩ = Dk/}5I<0jՊJINZ ~oߘeŐa`|丝B&'$ۀS B!C#'@BRΕXp0[n&:_1ؔhVp7l@EsxM4z|5wQ (׉'<)!BzQXE;D vI?Q*!Q0&Vy%qsc#1zi}.xN}f>1/Q=L: z匒c$@-$2l4_@GpYñ1 tшr׵a&x؜@DE>Ó -r|1Hr"=H҂Ams(PPƤ#Df c)Cl.r(m-\{T ɣ?ڥ V.?n~uWL3NpF%x@}_Yk nQ~栛 &"0obDT<2ϱ5hR ' +Hy[_6hr\;CtY7Q%&Bh vBk{51H>gszx(/[Ԥiẻ{*VXbMbQy곱\ ČUbIă'lwo%pk'-s8gघfu_R/.a82NPx~OV91#aaVʺkG]@ )\:B奲AxT(Y"xS̸)aځ baјIt 䗖V,qlѻ7OzY$z_>BhF&6@;1y1^V̮z%`]9|c34_pcZޱUH&'4OI/!؛AL#tH!Ll.1\-c~4/ L ,G rX tFWRtb[(gc<,#bSz\ nCb*grHr&L-ʋX+'ţ_t 49$5 KLUc1A.ס]4d`LUCgd`LͳDv+]bݥA71.69$׶14'Gq1Qjch:7Lfx ndD;h"uYle۹-~9j%REA n0C=Q))QCc##1V;'.g.*-A+mm#Ԥgq&ˇU[~|򁄋7hp 5&'ĕhrHs2.}tIzu"_2W$DtXh)&fݘAϲ_)|zo؁R3@8[_\J)zDRUTY+酪--R#jԂz7A'eKJ$&%Ba#2d|e]aET`a.tgUÌme5M,2"KH n ?Oaa kre$ NbZXGӚ.Lh +ڞ tTM E^ܬt=b}07]RY ĭo9@R^bdYBXB@\ˤ6x1C}^AAA-%)3aLt[ZDM?&MMz:lx|5=ɿ?AB%}4,(o ?~*6\d Łk5di[Q\HYP<@s,f('hb>ÙmJim3R7vD2KʁuѸQeJ 39k6<݆S)5D!oыڝJ/$tհ1g*vC=hİg&@s1DŽFȘO-U~S~W   ~L+VHW\mpp㱢0ݻ K`yO9JQ?~nZ"X{ba{*&U ph"@Qr$V@0 |S:4\ܯi`Eq``N.'po|9< 6Aw(6/ɥ$B 16eoai2jfl_(H=n+gNM>GQNN'.XMNKH1ug)2_R0/Ę$m#ٝKfx֞͟&-%۱IŅiWDZ¦[kWDС )5С .VL,r ͎;SQ`y7\2i` l.[A^qu؁iO. OP FmyrɢPR'(:$$mil  th>GE(wҷwQ-873_l\#(L8BO- 4'uoo |m1"PJ 4 c`յi fSU>̕MMa:ZWpUלs? U~/&6[|elru WR霔S&+M3Pv)9H˒ 'C5 uxlB1cXE?VC;#--qxr H/=yU%[RD3Pq$"z41yz{zLs־/lp{,}M??dww7j~Ȅ!#Bck): vQݢksa&n#%K` b%n$>H$17mBpng#@h՟h 3~^WJzM(rL^KgcVwayŠ2E+mE ȻuWTB-Lf]¸{sr W~8kM'av@>z |FK>@6$rZߏ4 x~LJ/lsVLa1&) B5) w|ޮ1_K&alDy>T1\c/ 't)E*pJ"lC70HeQ[Z-E)%xa)II |qplFyu%4^c\CuKm|L32@ Z~S&/w͈n=^vpҘ4M#M BRbZ Ej%, ɬL/G YH AӀ[\7# yQ4gXhcMt+=qJҖo!nfbiVLeMBr4uLY. =OC^&zkf,u,&s(|ܿp _ w'I^h?28+>Fcg ;'0N#} w18!ORJCʦğL됐 rbfaJ`Z'\פiX4>~LEQDOŚ?+jxmޜą~Q/=usFZGkIMBқh' QC"ޣf:߃>}10Tklc pZx1p7븑 |FѶ`-5GX~ "={]*k4yUr-SIVۦk:$h"L.pXCMtPpNy7Z[ gN%'6+hRvB>WX 2wE)hSc޴L3N> )ْ0^ e(_XB4o>Fo&6ժ ռEY~`n+TV m&:d|ly#u'_ӔsSK'ý͆n"cub&EG2ylC I\iA7lZiGI̕_ 4.#s#%ձ 4/\0 x ~ SA|?db/?ӭ%ޭ~T!Ha Vר\NfxRz!wHѩOUM,i 3McTԌy%}ܻ>}Ş^? U4~X gdk>m3맊K0CkMs!F0m M/TLa@M_/\6fH K0XFEiG$TdN,sVFMfɓ|_{K*1W4\Q0p T5Q~S}dӅ)yPEĤT gX6ZvlwL dq,$y(WnWXz}~9JJKCU:E#ABB#)R(9$1 C Rv ك"NޠƶxKB}~Hd$TLeat TGpj&E&&xCASMۃ=(m"}"w;,xwY8>{M +;ߞy#cBMa0(*nLtHu0-r!WZa s-RDbcմ!t062]P1+Be}&̽÷Yq&Xؔo!*QhCեKkʠ\~aqhtQަmڇGa3-S5k.r%mJTTA){JY6 Uex7g^@!oc}W7 ˊwW~ )BvtW7`~. w1@@SZDa]ϴqP14I|Xp(i 'pm0肸Q!ݱe"qK^cQyFN7?HA~uE,rfT /@B\$˜ue,Po;0Jz!=1Nn3Cfin~k \4CH1xo̼ ?x?ҏ@%io0Rl%JiϴH&d.s9ws±+ "z:6=""%\$|)/P(qs-dpskn)b8r_dv_ ߛ$zZ!bHmeBVtZڰxKTp0âwMS”i^jF*IJ_aT tQ.dvx/&{1eR$eW |i8pST+p̩`"w0 TF6v` 6e$pp*SBr[)q٭W,ڼI;<l˔ai75-(W1M$D $2%q .Lfbӯy/Wl8 F2\>;_>5/Ep ydskҿTN5e8j1i߾ƣ[jh,,P aMȡz6&5JCTW lda G"fi #(SL557݃\⇿8mj+}_;l<0ۃD#XRv /Ԕ ǖ{QF u$t]$Rhw`܃]_U S51Y?0i>X L#KddXTr\L# ?.{lnqYc"v`-|03 \I'G3Ofh4f=z~ڍ}?f)Kf rA)5ElJ``";D݀^yz-҆>}iQ->`,*>DP~v縷Eʕ+z)y1Ϟ={^x1,8upYzU/P(R̭Ծ{(~S.yq`8V{B%t9#S `E A AJuޠOc*> c!-gv7>v?O- <v~+ghΚZC%ƌF+]'V1)\$I? &J}o.<;B,4i'Y\׬ [lOe6l*d?<Ӈo?p7|} \\svcR^ӽJ ]¶sisfqT2Bq  XPI鐢 m}%kTZtM M2#jՒFT >p[p]~Syof`,uEF5 )qaSǧ(~ݻ8Lfbm] f 7-3m&o7UB,X@RAkM{P!>. 344DTnϔqt^ׂc#T)IvFXA PtIHRYe3+$} ߤ5 fnVhHJ*Hj].^;"B#H:(4z3]h @SV=T*6RO/HLOD]Ũ mirI?zنH@y3K#>L#IxIuf~b^Պ+~=>Šݛ](8B\% ùZk-PZcA]5RJENZJI%2=޽x wW .5M|5wO?Lp/Y(Vw|=thR&CL>tvs7EZa0-â,OC1[/FNgw{dQL<$π/h2FPEK,΋OPLQO*w۟Xmg%;3oՉ}:Mi E`8w]vL &<&ύU?{j4ҌIT~[op*| cS^ύZ}B ]6/Ͽ/-?D߃w yഥh]vw[la߾}lݺH !4R! B !v_l|sJUt4MZG)u%Ih,5\C$MSPgܷo)dN6i~K0عG0h4(3 @MrƖUuUQ EcڠOR vL UUmVjHR(6|b$̈%(2eGB8AzCJVJE# 늻W- cx՟9~tn YӔ|׍vGlݺU+PJaYBO>1ǫ_]^B<ϥBjy)Uz#siO|J3US١a`ʜ`*UFLSn4gDAǟ`1MP*QܬV+-5|^  kY#S^׵钙Si٦ՇI 98yjpA|fz|r?EFS8&uqe׿^GQ !eIu.u!B/X1q1b9r!uEJπ[,kRY$eYksקcWSJ,'Pᦐa-;*P%E 3gM6Ci{ X XfSZ. %hPkTJ#Ak92 s 3Dn.ј/P$ݶB M}%G2jOzPtI#C !m-;4{Eݟ1)]Ͷ{cq|S߀W>ޘc={Ry8o $@KmRXBy9&RsLqqRGr 1zucNMJr9Rj<`p&Hc|tܬ^?3_Bl[~):  O IH"NP )Qk 1>B/3v&)o;Xa4?F#Hqf"IVVVBX@~19f\C/'<# i!ķkֹZ:;Zpo DɗQ$t1h:Vm 0Fp_~/5;FCyE  dZ8`y)NWB !QJel޼\9d5" }`aJ^F#R#E@4|t.LkDYA& { m4 )SqŷP7BisLјvPJi+r )$%ML{F%%Xsi?֚ͰP_]KEp3OqQm?p3yE'uͯ؁Y+IRQOk_ eYJ)0dӦMz &u^[CQk6aKqRJѝ^+$WG kz e[F+ɡd t fUt+(G*6Ma@E)}j9O3jiElPtISE'S /ٰڅ]Clsӵz 30[,qY83u|"xux<UN(9V.! yem۶s}g:| $7$E"$YHJ Q#\5igKf<4 V]i6-K,^'pK\Y\) =E"4V,81'sʁ,;!,%Tks[YW׬_E%|x֯k8'yO= r<ϟdqK,`6&u?!pc}K@뼓Qː S޿p |rwg *.B2Yф-:( ~b[DCcJe2ƽMBDN ½иq Ih6,$#_@ݿ֌e+jl,)F~ś9+puf@5# =Hp\=n~'x>v;>]ǛaSޜe* !ą3 >,g<_xE%D=t7 T(QD"w/ft lL|k]lb0A["A*Q`U[5Sǹz6XޑFxFyf_sO#韛 =SR;* :H8}\aszDƄtq (!٨i0>KPuy2eu H^464f|\[6! @TQtI)aS:ȎN= GjÞ5U31up6*`^6#+iRX*E n8M<)92 pb0")jE5%0=C o(:[!vE2MS577˳|>_O+ o 뒮{PT. %$Q&xά9m-}W}3.]XW.a/[VN9б"m!MJH*u\,r9&1q 8GIݿ -ϋz\x4|P)>q|#KncCK)"MS}"Q[~Y)7#-ˁ~ \JkjtԖ-[pݓ 3=1*|MpW|\{u +a_>5l*RƢQO.3.8v>K*phB(` )#L9(Ⰾjmp,l,!1 !DYi4CQTo!a*Xr&Rx鰽ϪR A.z|fxc69yBX#Q!0BkUeJ)WJ%b-ǯy(pmVV Fޅ|߀5#gëH79CO=TӵTԻ4/9T.aNbi)M,Q *$G#*U@^'!I$d,r wjX7G@ yg*\gzL_C_\4 S~LŅ)"7㖁?B|6:[0yJ1%n8JJ޽{[,tAvm݆8yΨRi-۶AZkLk8G`0NZbV !H]BdN[xpݏҍ~@3T@CPA'CqL\@a_T%dpwTFh:||-! 0k kA[AsdopL)|?,𽻘.|go?c+ !8\YvxG'2y?ݻue5Rw1it-"" Ԁ\XZZ q[֯;ιó|^'P,[zO ~.Y#q3δ!Y2\r"⑭Z'`]DsьA2]E'_NQ.ۄ"NQ8U)1DTpQIȻ,g& "sH.F@`ANY_1;_|+xj^pIFM[R_E4MɲGǑyG{9V@0VrT*q<[שs,36(@]/ B=ʹT#קck#EJZk&UV )$Le$ MLX@KT%FRDi\,41e=|sBHngaVImn(37w?RFRJ;s9;0UJsOhrIDAT}{BdNxЊQOl$쫱HI1R4nOP$lYjHA !tWhaDNp)`*2 MXy_O׉e5J&l+/񔟅4 ſwƆ{u}ࣣRJNP1 k!x &{Q!)6)%Bnݺs2gzpWB- ,PDw є(R@R}L$&8¥4" vCrm\"SRU"aEZ'90\EIOŠ0~d:Pfċ#G@r/ܤxG  |"1lq8` ZϏ3?S|cjM~&EN-FĘ(~Cada/'D76bd]ް1:Ev׵&)) A>9ˣi&|#1j=XN ktOlA d_*gl=&zFeJ)= O`cq^W|[:+t`2_' ~mpSdw=BL&ǘ`߳dr  8.T|Cm28: DO3;yOgĶmR(Z؋lQ A_Ȟgr,r$\iFGIBi4`4CűQFRTu`c|EV_&ҐKbFHM Ȫ90").er2ba z|T;5#,EP.3忦Q1z?74 `OJ"&.x-0ݘi q쳞=<\ ͠& PXt_ ub"8ģZycgSjMqsElTa}6zСON 8Ԩ>a1H# b"FphhrEmN@)wx/ 7:-xޯ̏G1!x`8⺮BZ1 l梋.R\84g zCl0?uiaw14>>8}sl"MyZbm(BAAlH> Ad1 '$E Ve EHlxQ\iDʣ{@ڊzy7Y^1oExE0So ޽!RJq_ PqL"ӏG|mxRgQ=W{?FQJ#QQc:[J44M?E(0LmhZh,lJIA* EWM~(`I]][Pu4'į_vv۶mcTIz8;Lp𰟯hxPˡZ Z׽N^\Jru HĨ&_T֔QYle:(VV$IIbl4C 8LMr1y6} \L)Y2 ]/7;{:=0aC| MSk9#buSkl}.e!$PlM.M"!CXw.-fAI20ĜG!&Uc4q\ªk$3 .%yG`9sH(X> oX6 HJ+wL-g^@gr+5|oEpu!&s.1G(p,6%mQw>G$u GV°zSYMhLC&}&܎mL.^P%V77dpfRp5'wͪ|/!=.)"WNA+yvBiKp? 7C|gvKL~kΎ~>غ<59<+a ,~M44^h>/F2_`p$dC1_{,o g'HΆX_B*2=mY'pGZ=̡l@SfKE?'/IM4XoƬC[VavܝLKGK$eƟ2aL@6'(˂Xx (PF!9z(2l,EPa_53ɝ߶ZeϘCěW N# 7޼y3I0xj?֙:0Sĥ}DEMLap. H@!ZYͨ,\Kj*+Y7s I!@_ )X0X l͚#(Qڤ(RA",h(d6]4Qkjl(柽kgl&x8~OR3mYk!D1e36sssgz&@]lTꚻ ڧ^O(Fj+6aD-$XT84UBd,e` ,]Fff9TsV(Ż ɉɏtaF/d~Yµ"T ?̍ ñW"R gBoƍ8 ~c $ߕ?g:E\?KG:eb,r8Wʌзz^(H}…!a7V)N;'Sk, Jҡ<-в!fA5g*GF*BPE-Rr"rJv5 B{ue4_;#7FckR<*,bӦM탶n݊8dYR{0^_><ݻwQ<{)ri:{Ι 2f$~zJZh{,۔Jbj84 $ VS,KR, 0TaIYeH&HJD k4`dRTͦ"=*bSˆ H5Trv݉g3?-Dt]d;1>WϞ1|#x82DL%q!Yebງ0.cģr Dr2_'9A8GE754ׁ5m\5#) %a3bWx@gN+'W"QtHiD |ac1 *9q8h4mR^˘ Am"zJ+F .J31<@/1cRJ۶mضeYPZԘ=Xk @kmy a|.W Ά8a"unW̠%eSA hKdk\,\2 +G)%av o$+Q-58XEm׺g.^jCQ*p(6-5IO\Va=OY)wROpq>22RJ()țg˖-yRJH)u8f@#RX"?)&uP:B6X&"wroW0i$В?\ȨI|f2i>fFcZ")t)@\ZbqA(kZ/4%۶_ާF%sZ)Z)cB<!J)Q.(<_8?O}΃)cI~2=B)T۶m>w1.ן# Tʑ#-vԠ"OD ʹ'a(o,`Ys-fшu.`JSu(]:txH]l7CF7-A%RA պjo T(9BJ"6SE#PEA) Vcg/,zÛz5ӾO ~n8[ ֔ <+ȣ(-YBu]۲,<} (q<+1x($W!ąHcL UBW6]s鿚t+{MNQ j})6)V\S,gXGC Cݡۂ,Քg|,)nU%K| @tX DҤj3&}3dbEǤdxؔEC jIQUKix^آP! N0׋z뭟}}ɥ E crlr 4Ǣӱr{RKeYrzD+W9pv+a7{N47ý)n (hb)U$Ųb&Mے)Ãkԧmi>Maku) %oa`Rɤhe̾Wlh&bwx7\6aY)D@AX <P`] R8Nziv/?ʑh9@S خzaMpr)pYhL1&r) Z1YB۶BdFؙvϟEFj/KpM-Q{INe[rEs%#0=grtȖmz? &4MEh niRr]J @@;1z4Կw=ƐMWh(O{~FaIq/:Ʌbi)K?ED0>r:S!~Kk}DkmyRidC/MY˥?!|HFIpKZ12M4պcˣ9.QA_%& P< !݅>a9D<,7auZΡLSۆQgD*)c[ܡ)XB l"UXCFONp*8~*cI><*a=!gGeY.4L mf_fǗuZvzIM}X q7IzV3R&S%1%*xD:c)Q-inf)jVUhW:Ìf%ưЃ#\5 7\@kIIBXڃE}g3m/YD8S=8 ޖRvR>r/9&|eyJ))RJʑ|Q¡W3LMN3B:ƣJ 7kSJ}'+ )SP$tr7ch[FnWQu)@7!aHʡT1ԁUTnޫm%RNς V#XAt֖~R]YDH0mVkHJ)K)oB>4sxLzaRBkR?_Sëf鮄O\$O9|&ق֐EeJ  z+Z1Zj9:>A@WS3DQI(n(`$,YmV([P `Am 03cX˩}* DV]-].Mtœ-a$Usp><,K`,fj;sW4<ϳ=Z:׸k\\}f]d/J+6W&])VTZ)$&T h)e6Eǹ!P;nK# 6Δ}KHD'%.4S#W+ɧZA"iG4ӯލsS(; Hϰ9;DZʲβ,< _@s1 K]#V`̟s5_{d}8-=ҡxn?I; (IM WRA9v4Ų{/BaaȖVE`xCQ(FmRwuHV$}-ŷцL"`:۱̱$l V&& :];puY\" MO|Lp/KHh acQs *^vLB,j۶ GDK)ٿT^q?@U!ZkJhgcvG5zRsx׮g:,Ɇ96}H,"Ɉ:4&=#DVTܴwB@Pue$ ` .GJ)FE+l`VڐDzɪ|NlEՃH})yN/ľ,EѸVTk}1Xvmdjl>r!_yeY&ZL 6{q d(/ mQeq(i/D-pi`Qm*|sA5)Kȿ9ٚsw/Lf6'KA7.(! ~ԆM΍ReQ˝4iÖ9ժ 2 U[=?XcYX|?_\w{y(p{:r^N&-C 8yiy^369>\TSRaOf)23p]#A0\ X-$hPP '5P7z▹XLz+8׳yS}15B_ SV-ḵ_*`; cMgw38BS\#`SAUBg |cYnT\tʣ*6VИﵱ*APW1Ƥn6ZhZ-Ca.3ЉaoDV}֧sf@`nam[`{udTuܢe3?9xlsׇQ-뚸mn3_Z[ 2jصቜiƄ'dx:p _~q6˃!(iKWP9CCoG5HQ9q[J3+xu1ZSiBZV&:HC\Vl,WUQ\_hbovp(M[a;}H1ƢfSnu4dQ(~ 膠rM, Ig c]t r5^NxNEHy -kq~񂿹i;ȴ@Z ls h,T{ռ(ڃHQ*k+ PF/erbJL4Le̎f&Mj lW܆9ƴR70vq>n+Xr9;p(B}9܋q,!Ryμ~׹G &?'ȴd3 ^jt K/>[R=we5j8"s=L~ oCX@? O?|ZF ȫ̗PIN%&T{XI"nI)Z&#·44 8ޛ¨@.MD09}'u]?GsL'RJaڿSNIM݁mE1~ qfcUTkJʘ6a&ؔc&h{b[On"dE8ۘ<3'?K%!6ЎOo?45 UJBh =+NW3lc*1]c ,L#c lv#B`FT/~JS_ .~4 zlr9N&\ 16ѨyO)1]0ё,%}8OcdC<{E zxi+ips:=<7qX]Gc{A3S]Q8@5ABw t$Lt[0S a ~`6V-Fwu"u_ZMUTOS){ۚo5~U|LP0L TmZO邅c:!c$4ilm[OVQ#0F"=Ϙ(lTCPzr.kP*/RŒzǤ4`BLp" { l|<'3Qx9c535!J(6p1ԣAMEM{2` lBeshat]VBׇm\Z9S.6WtC:K)j!ז_teo5\] \RҨl.Y 3+shܙU42)%vQEM;fqo*YG>_u!5R"ׂ!cBLp"|n5͂0},QFT /2$dmɣUNv ڦLZchJn9ML ,AY`Ark.Fcd_zgޟxŒw+ȵ&8?yҸ?%9PƐţm6Qݭi*LA֗2ĐbtpȫK"4Y??$L~ ?9' o?6$o/`/=cӸAPZ15#G(D,,5ݠ 7:IaKfva63 !?80baU`Ql(aC-4owW P(kr[H;x"U?( +4lv91ՃlMXasca)S O0Z|O}߅PoCY꾄0zJ < i/eg] UdW:km 9PhEM 1$t&g}8%L}uO⛨[KxsxQBӬZ-W~pZḾyДv6*t佛a*7723z6$3eJ#7D~֌J%,bjxXbBLQ O]]8ode ~4zJvVp R*Yg죻fc5LsmLll3Sҵ ̍b R¬i0꿓6d<{P᪟?)'c[Wh C\saa#(<$ `15|nx LE'\:mHlt$ 4^ʤ]Bu`X̙5rSuҴMJk`\Yl `Ccr4kGM}х౯iYmJ~>?uWEE*k*j֍Vwxt*=g&iŧ'1ihTu&tGwߗB뾼"/ҍK@0d&?_a8¡ۿֈǓ!g˂5 ;k؞Mk(J9WVVv~K#7鎖e4b# V`eE\# R0!aydwh'1}EVOSklVmmo`B}Trˮ4+ 󑦼&_M= "Ov|U:KJb~ϹJvWʯ3(ElggڜKKS)r(tX>o)葫TfcMpwu UsU[؃NjdCH>^'<[S%oԘL%oKY`aK6 +L+BӍh4kԜT m:tVLиRFo_!RɎxzB,&8eL~ $oKuAC4Vz$ >YV)-*S6 ͐1rjptžk=3XBӂ0m%\:Fy ƚ~o{z,(?Z}lf~#X45k\GB#&0ДLmCQ'MkGeej뫰d 7DJӷJӡ?j(4 AqɤL}#ܛuk迀5dʣ5\1K(7\+SULK2$c=>sy3wo7FXoO-[|Rn>?>KpEGK濿Uu6W6m\T+H+}#,nM7mD9b^Ϝ}}s ta }ABs뗦fy%|6NpO w1 F1z8S4}ž/Ԣ- n'q6۹) cf\ Qv`&[,RV 6sʢ֨Uy߷2 MBÒ m` r ,R1jYĚ[K6V|UHQsb?h'Jr,&S+#)O(aF"ˤlmHLco'&TM.~kt.EAVjٗ5KHT\_5Nm-@k}*)[@&ɶ|D|MVϡ`_K50vNQV5]h\_P@2szpjat@t7L/( 86e5ɐKlI`][nM^ʧzxEI_)ɶ SNc|2T  69{ !l(a3> &x(HS݄84E砸)\  뙺[ZbE^T-9L w54D}ص lЬr ;;/ցJna*[Y{iM(7sV&%f$ $fWrx&ڤc:l1}_哮6P i(&%48C6Ǩ|~1:" M$Us 8m֭[-Z . ~7 ɮGc-R,2T 6EPT=G TĜ(|*gMp I]}3fSnҚwn'A~/k\Ef{C0W>R rwk2od/k8oGL*P``We(p҉W'۫,\\Gi#{<?_e 2V` Xle ggƦDal669ANҲ ~5e@JjC݇38O/nocr7Z-xINfq-Fb/BηK[ҕ2]_}oЎa]`6[;)&Tzǝ/a?w&Pf"﬙Ձ/@"M"7__o M,6S%Ԝ&Vv@ /xvɧnL^`*g /$ogpDP}AC7H)x9b`?W\: MWtSC)F,`1+A|4$_I-8lÜowQB"U4GrͺiCNE|^n(D(- Q]|7;Ўfk6##Fz}1LE͆8Vc|Ҫoy '!F`aq٤.^&ml8Ԃ@Ϟ72]պb0 %K2GgT-H'9tl))340| 8v:+o)l?J@ʷB*<9jEɑt*Y Ija[R"7o Fu} .(7#5@̢7F!Y4Ys$a֗>T\WtIȽO, a[jj[}^ط^쉹簁 -Otj Op!PujA bj)]`;P,zqtXB,C}u$ 4sxÑ.L )K/"wR-|z?pz]P.KK?wNF#2XYbۮhطRq=0uX^%M4 GR H"kGFr'Hr(kV5 n%*9oqU'OL~ 1HhΈӀg#O0v?xI# W'݊+x+10w|yw%:.%Ml}/JWeq&~CWN*<"%fv"ͦr8tBl = [ &Mgܞb BS%P$=Akjt{jcC"ЃAm"~錮o/n׫$d 6Z]v- AU~ˁO;?}W3xo|+$qx'ix @hN+.=8N-\)L{ӧ/sbQexeXWN?"6ߥNmpeͤO M٤t,S%G/ tePkH`j e2_5׵CQ='uu} )$E cȺA .Aw#B^Ryzts;0T-HBm{ `݁IF-Z_<" |} ťӚo~;#qB.^ILƏ\ cUߠGh%߇54Mt#Ɣ= idl] Y43kxKC1ZW"Gq–YmBR$O~ eʞ_Ad 3_!m`P|\)/:en az]oRݏܙD٣hn*K;h~䓠V>43ky4l[h3SrEكNhJ +0&# :0FL.>@X&r_ !!qmRSYMcU'4mw~-'h]Q'P 6?Jgm="2xwqPGM'`V /l )xYl-v=T`[I} lgC"q%Q>Li@ Fi2pGhd@sh ]Te:ʅiJ0Á!|r17uGxY M&Ѱj(\6Vc^2r,a0! .<e͝P#(t#|orgokqp{( L [\= e~/|jV?wn1uu=?<qryF5cq[ksoS ?(6F+ 3)An((/zFi75g:mhF}Ca:%Ks84Ri)XI|l>EPg/M"2\ƒVG B(\l.Q/y5ʕ"ra  ~y3^7l{_xgwQ|EoJkbC5@3k\B(rw|M +aZ¦:\4nj30Na y6MUM34Eat{bM(^b|[K \ f|#rX0Z7iwgx?N"Ez8:&?':7|g[H.YX@dէc7`vSDu]z 2g ȔA^<4w?! )G5k4a!0ZiZq*:Pte`̓~f޿3"-C=3gGB.YϕzH*&?&oiÖM\8po<].|C Ѧ/͡5-xeQg q;sۥ%b+x֋_8ZkԋݡɉTbx ¾!ծpm;2G7"cцi)tfGJW#X@ "ZAۑnb7鑑=ףyV1! .<X U PՅr{V,.kT> 'xvkX0C܂K̬qyХQ%J5[pyNly#MpA]MT;bϤjf6%skG񎪀^+iޯ0@̆%`ڇ(7> t,G=1j{Ei|~+5ٺ &8x>_oT k! ElńY\CFQJS$v:7l{c[BЇ5݁ÉixJR86 "}h'Mjkbn 5sCH )(ǼJJ>PL)c7C[mS4:\@93Sc2b{au6kq\1:պ{E|Ñ4gĐoΣ쮪Do)5dbPDdQڶ^ Y.q\?i;<պNȋ80 4J`A`HBHTjuxT6 $ ZY$!ugo1nDcw%l3O2=s1`qᮔ \u|O $S|#{ҁ_((7aOF3-vK]EWцZԸc&9"7SE]n@#,S48 84%K(RH^Yl'F/.xП2;. B tWj5]25{_N0I(}ʵt ۂ`X}똤I!0+^}&qhzEktSt&2F2nqE%]?hZ0)aXjSgŗlh˱:T P-OM)OvͿR!m"J5&q -Q<<)8Z SXBư(tv4`kEq=z$@SO==hZkC$i$HQSg|fCDnɫ{ ak1saǗ goCJ%|` vY<7xgz0= 1)֝8,eO7z_ 0#XxMZ}XrA]f`_w r[O 0#c{8vŲ &{)8 .D TSp]86Hut߂6W?^ R g EzNPz86Di'h\-|k?ޚ=vb~7ۑ?Je',K<2aGU3`A- q ?Gձ A[bW)4S{f(%Hhr|Ӄ rCMEx"E·%YxPs$'cqNl]#r*YAmK>v)|MoƑ@@*`VD8zJuvqڧ7CUb2 ;\st&07vDPO耏0e$*V0{?9:yby'jihrzzQw'KQdaYb=+?y>/9Ik lKgEr>HFm='ޱ%D9Jg= J6$47~?JW6g$՛jc{ɹ#v7QDƹr#bQ5!Q…o]&M7 []p5j/#.!+i. Mϫ[yD_h_PL0MX\5J Pgu7c &K97c?.}bº wߜ^C==^rT-{*M#!pեpݭ>уfJ4ayʇe%p7HR=HuXZ2N6}]f9pL7MgV&uP꿘`8meTZTs7nPڰ'eY>ez*4g AxG>̓f2x0kokޭ/Y`~7$ (^N3-NU-xtH7Z_ŧRgY:l)E8&;g;մL,O#Q)8o3;B&cABނ/S}Q&7 !,~_  ڲZMpL,tHm@86$'auzaYP-պ_GM[QJNlUFѰ &XE_q7@Xd{ɹc(~H*^ҝkJg$89A%!Kp6qN/&De 뮟y8*.XNc^]iJ4ar>}Mtz!;Ng*\:`x{[<Toz x*Dno3J(:W Pd[@f %((bz6$#89"egY:?a?M0 ckk+J]g~IٛP)U$8=CPZl=&oI gUx*YGш|x0f׬B.ZmҖ1ou J1,v+sdB%ADû}nP\-ȅ lB0&h&TP-7V26fӭ,F!AI. @$RIG CVw?Mh9LYOlg˜E92Ϋ2>j#K'rVٶy :rfէsLo{&䳊o.i$E^|kTA#V䲂|}?z9{gRvlq?9lׁr3΅s?}%e_?6 X4Ė WL'KaMMG_oaǂ0\zp`zco&'KɹTj*m7/PJF>Cga sm@BwOҞ(T]rV;ezEUVq[ZDq:$Ja\^Sg|ɽyBFO'}s]gHGCIO琙orU$4SXo\ $n2b8j$]Ⱥ$gw" Kgc|H9Ihwk',?|~; Y` p0Q0aキ ۮWoǺ܄ V#76@ DchNƃm](xѻKoZ __B[Ɛ-9yWx"&ʣٽoƫBB >|M}č (v(`׾A#ԯ`QALZN=7~ E8s7_ '^ çBW? VKfa™[tR*H,!O}ƹo 9ZR9˟O?›G E&dsV!10kaOD,Qwe5t …u* 5;" .Ji:ႏ髰[ϻiƐq 'tNukZ} Ø,Ь)#ظUn VFx (u?PQ:̲P*!VΆ[׷řNa]>g˅P7>Di줞%L/0 8X ½߆}LP 8_ݬafqyZ$@=fxθCRgm&#$[@V{?0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0dF&iexIfII*88nt|(1 2vvGIMP 2.10.42019:02:09 13:13:39 JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+Gq,~m(1W R=X/@x['L}{.=9+ZT9 hja ̴Il?seMlRpԺ@o#)g&+/(]jyV|5-bcVʮ1ʨVljoUe @:qֱ^-(FEPEPEPEPEPEPEPEPEPEPEPEP8bRHUGrNz𿇢,`'<|2ؽXt6nW[Bp4}?5.]zXz wS.4.V#_zk\IU|O7W[ ԭB1Er~>4+YT$P61Ɗ<՜MJ>e?c袊Т((((((((((CCU֭,Lc@a]C^2ko|K|A8KFWV^9=A\YVMj|mTQ˿&[KmROb:U)[ *7zQv>*jǯ*敧O}Y(?*{/o -kJD)HRg#=z[hT0 X=sN/(U8W mX>cC=M~sʽ]u2MtsU=Oa\ߍeP"fpۀߞ~IWqb>WMK(,`;>?MtX}japE\[Kjk?um6YҮ,.S|3V^tVOKF3ԱqS:ז`!׭%Uo?j{L8w:_μ+3SXY\_ 0CIUdz{}kFn"CmuSr4m^]WQ]of^?隞Z:[frj*srwgp5QEAQEQEQEQEQEK)dq֏\U7C*io݇S@UϥWԊ!QէD=ր1;:犼}1JPp [/8EzTZ`1pim??k8\eˎgQI%ьpOl*Xtr@x ^Il,A{!\6(nZR,vGb)B5JU\X,П@3U-k,*7C-q5a==GVIl~GֵVgs:U^ ;F{]vwYO$(B8}k<]}b88#PO]]z:踖k;kԵu;4*0xAZ#b, W~֮~vS;Z_B(L((((((7Ҡ/>-ͺ.3 cֽ&3tvPբQ@>S}+!n,1ZiqkJ=dVm}vx4V~@Wtsj[+*5F yjחQ[M+D^JM}S? No-cOܤV/x4& : 2Ŀ|}<{b|ﳳ?5q+X+.~d`鸫:b;Wxy>ږ!r^i09~S?oc\ؒIں5+8N*Л@֤\woU?X6Q˛32V9 yf՝rgְR~QH((((((4QEQEQEdEQEvm>ӫ\`[s{G"G +Mķ11GhӽwoŨhPI2\GN1^9f+]Ԃ/sW)-2KwpH9^+4O3+fcԚPR'fqUjt0TݝMKUdP0GQ^]:DQ^oq%WY#`}zZwV֚rAu=#ZwYg/si/:JAƠ>\^/?k8-}A`V ?譬vNae'S(nOx:^ӣ==4QEf}QEQEQEQEQEQEQEQEQEQEQEQEQEW_R53R㘋\*;FYNC8;G+-zu%NA k]I ƺx^-_~}Z3Z<\f hmZ"d(>^~a\%T7}OG Xh8E]~ER:B((((((((((((((((((((((((((((((((((((((((((((((((((+.`%tEXtdate:create2019-07-23T17:42:58-07:00 mG%tEXtdate:modify2019-07-23T17:42:58-07:00vTtEXtexif:BitsPerSample8, 8, 8>'!tEXtexif:DateTime2019:02:09 13:13:39tEXtexif:ImageLength1080L8tEXtexif:ImageWidth1080ϻtEXtexif:SoftwareGIMP 2.10.4@:fIENDB`pipx-1.0.0/makefile000066400000000000000000000005261416500503600141730ustar00rootroot00000000000000.PHONY: test docs develop build publish publish_docs lint man develop: pipx run nox -s develop lint: pipx run nox -s lint test: pipx run nox publish: pipx run nox -s publish docs: pipx run nox -s docs -r watch_docs: pipx run nox -s watch_docs -r publish_docs: pipx run nox -s publish_docs -r man: pipx run nox -s build_man -r pipx-1.0.0/mkdocs.yml000066400000000000000000000013771416500503600145030ustar00rootroot00000000000000site_name: pipx site_description: execute binaries from Python packages in isolated environments theme: name: "material" google_analytics: - 'UA-90243909-2' - 'auto' repo_name: pypa/pipx repo_url: https://github.com/pypa/pipx edit_uri: edit/main/docs/ nav: - Home: "index.md" - Installation: "installation.md" - Getting Started: "getting-started.md" - Docs: "docs.md" - Troubleshooting: "troubleshooting.md" - Examples: "examples.md" - Comparison to Other Tools: "comparisons.md" - How pipx works: "how-pipx-works.md" - Programs to Try: "programs-to-try.md" - Contributing: "contributing.md" - Changelog: "changelog.md" markdown_extensions: - admonition # note blocks, warning blocks -- https://github.com/mkdocs/mkdocs/issues/1659 pipx-1.0.0/noxfile.py000066400000000000000000000206341416500503600145130ustar00rootroot00000000000000import subprocess import sys from pathlib import Path import nox # type: ignore PYTHON_ALL_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"] PYTHON_DEFAULT_VERSION = "3.10" DOC_DEPENDENCIES = [".", "jinja2", "mkdocs", "mkdocs-material"] MAN_DEPENDENCIES = [".", "argparse-manpage"] LINT_DEPENDENCIES = [ "black==21.12b0", "flake8==4.0.1", "flake8-bugbear==21.11.29", "mypy==0.930", "types-jinja2", "check-manifest==0.47", "packaging>=20.0", "isort==5.10.1", ] # Packages whose dependencies need an intact system PATH to compile # pytest setup clears PATH. So pre-build some wheels to the pip cache. PREBUILD_PACKAGES = { "all": ["jupyter==1.0.0"], "macos": ["black==20.8b1"], "unix": [], "win": [], } PIPX_TESTS_CACHE_DIR = Path("./.pipx_tests/package_cache") PIPX_TESTS_PACKAGE_LIST_DIR = Path("testdata/tests_packages") # Platform logic if sys.platform == "darwin": PLATFORM = "macos" elif sys.platform == "win32": PLATFORM = "win" else: PLATFORM = "unix" # Set nox options if PLATFORM == "win": # build_docs fail on Windows, even if `chcp.com 65001` is used nox.options.sessions = ["tests", "lint", "build_man"] else: nox.options.sessions = ["tests", "lint", "build_docs", "build_man"] nox.options.reuse_existing_virtualenvs = True def prebuild_wheels(session, prebuild_dict): prebuild_list = prebuild_dict.get("all", []) + prebuild_dict.get(PLATFORM, []) session.install("wheel") wheel_dir = Path(session.virtualenv.location) / "prebuild_wheels" wheel_dir.mkdir(exist_ok=True) for prebuild in prebuild_list: session.run("pip", "wheel", f"--wheel-dir={wheel_dir}", prebuild, silent=True) def has_changes(): status = ( subprocess.run( "git status --porcelain", shell=True, check=True, stdout=subprocess.PIPE ) .stdout.decode() .strip() ) return len(status) > 0 def get_branch(): return ( subprocess.run( "git rev-parse --abbrev-ref HEAD", shell=True, check=True, stdout=subprocess.PIPE, ) .stdout.decode() .strip() ) def on_main_no_changes(session): if has_changes(): session.error("All changes must be committed or removed before publishing") branch = get_branch() if branch != "main": session.error(f"Must be on 'main' branch. Currently on {branch!r} branch") @nox.session(python=PYTHON_ALL_VERSIONS) def refresh_packages_cache(session): """Populate .pipx_tests/package_cache""" print("Updating local tests package spec file cache...") PIPX_TESTS_CACHE_DIR.mkdir(exist_ok=True, parents=True) session.run( "python", "scripts/update_package_cache.py", str(PIPX_TESTS_PACKAGE_LIST_DIR), str(PIPX_TESTS_CACHE_DIR), ) def tests_with_options(session, net_pypiserver): session.run("python", "-m", "pip", "install", "--upgrade", "pip") prebuild_wheels(session, PREBUILD_PACKAGES) session.install("-e", ".", "pytest", "pytest-cov") tests = session.posargs or ["tests"] if net_pypiserver: pypiserver_option = ["--net-pypiserver"] else: session.install("pypiserver") refresh_packages_cache(session) pypiserver_option = [] session.run("pytest", *pypiserver_option, "--cov=pipx", "--cov-report=", *tests) session.notify("cover") @nox.session(python=PYTHON_ALL_VERSIONS) def tests_internet(session): """Tests using internet pypi only""" tests_with_options(session, net_pypiserver=True) @nox.session(python=PYTHON_ALL_VERSIONS) def tests(session): """Tests using local pypiserver only""" tests_with_options(session, net_pypiserver=False) @nox.session(python=PYTHON_ALL_VERSIONS) def test_all_packages(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("-e", ".", "pytest") tests = session.posargs or ["tests"] session.run( "pytest", "-v", "--tb=no", "--show-capture=no", "--net-pypiserver", "--all-packages", *tests, ) @nox.session def cover(session): """Coverage analysis""" session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("coverage") session.run("coverage", "report", "--show-missing", "--fail-under=70") session.run("coverage", "erase") @nox.session(python=PYTHON_DEFAULT_VERSION) def lint(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*LINT_DEPENDENCIES) files = [str(Path("src") / "pipx"), "tests", "scripts"] + [ str(p) for p in Path(".").glob("*.py") ] session.run("isort", "--check", "--diff", "--profile", "black", *files) session.run("black", "--check", *files) session.run("flake8", *files) session.run( "mypy", "--strict-equality", "--no-implicit-optional", "--warn-unused-ignores", *files, ) session.run("check-manifest") session.run("python", "setup.py", "check", "--metadata", "--strict") @nox.session(python=PYTHON_ALL_VERSIONS) def develop(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*DOC_DEPENDENCIES, *LINT_DEPENDENCIES) session.install("-e", ".") @nox.session(python=PYTHON_DEFAULT_VERSION) def build(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("build") session.run("rm", "-rf", "dist", "build", external=True) session.run("python", "-m", "build") @nox.session(python=PYTHON_DEFAULT_VERSION) def publish(session): on_main_no_changes(session) session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("twine") build(session) print("REMINDER: Has the changelog been updated?") session.run("python", "-m", "twine", "upload", "dist/*") @nox.session(python=PYTHON_DEFAULT_VERSION) def build_docs(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*DOC_DEPENDENCIES) session.env[ "PIPX__DOC_DEFAULT_PYTHON" ] = "typically the python used to execute pipx" session.run("python", "scripts/generate_docs.py") session.run("mkdocs", "build") @nox.session(python=PYTHON_DEFAULT_VERSION) def publish_docs(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*DOC_DEPENDENCIES) build_docs(session) session.run("mkdocs", "gh-deploy") @nox.session(python=PYTHON_DEFAULT_VERSION) def watch_docs(session): session.install(*DOC_DEPENDENCIES) session.run("mkdocs", "serve") @nox.session(python=PYTHON_DEFAULT_VERSION) def build_man(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install(*MAN_DEPENDENCIES) session.env[ "PIPX__DOC_DEFAULT_PYTHON" ] = "typically the python used to execute pipx" session.run("python", "scripts/generate_man.py") @nox.session(python=PYTHON_DEFAULT_VERSION) def pre_release(session): on_main_no_changes(session) session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("mypy") if session.posargs: new_version = session.posargs[0] else: new_version = input("Enter new version: ") session.run("python", "scripts/pipx_prerelease.py", new_version) session.run("git", "--no-pager", "diff", external=True) print("") session.log( "If `git diff` above looks ok, execute the following command:\n\n" f" git commit -a -m 'Pre-release {new_version}.'\n" ) @nox.session(python=PYTHON_DEFAULT_VERSION) def post_release(session): on_main_no_changes(session) session.run("python", "-m", "pip", "install", "--upgrade", "pip") session.install("mypy") session.run("python", "scripts/pipx_postrelease.py") session.run("git", "--no-pager", "diff", external=True) print("") session.log( "If `git diff` above looks ok, execute the following command:\n\n" " git commit -a -m 'Post-release.'\n" ) @nox.session(python=PYTHON_ALL_VERSIONS) def create_test_package_list(session): session.run("python", "-m", "pip", "install", "--upgrade", "pip") output_dir = ( session.posargs[0] if session.posargs else str(PIPX_TESTS_PACKAGE_LIST_DIR) ) session.run( "python", "scripts/list_test_packages.py", str(PIPX_TESTS_PACKAGE_LIST_DIR / "primary_packages.txt"), output_dir, ) pipx-1.0.0/pipx_demo.gif000066400000000000000000010075171416500503600151560ustar00rootroot00000000000000GIF89a$1 H IN EEP=#E 6(7 7 1*T $"0_# $8$ ('.'%/)#) ) * +(+#,, &,B,:g- - - 1.1#H33>3O44 4,U5)8 8C`99:E;,_;Ar<3D=Pu>>5h>;R??F}A;BC&QFG.GVdH0EH1;JJ+#K(NWsOOH~ORP3P0ZQT*"T.7T4JT7VTBnV+WBYY*[f]1^Y_52`Gs`V_actd7CdQvephDUi3&mSEn:nJ`npW`s:1sR touK.uL2uM6w~xzJUzTf{^u{`V|SA}SX}~bgq}qACpODQ|^eesj]Ô\Ui@ss]~xti@ٜqģt䦲秳i^wgdͰﳓٴtõ sϸ@nÕo`<Ⱦ̖ηЪѰѼӥԛԬԱԿս֥fֺȶݹޟ8並欖8篡. Ti³9, /9ѷ0! NETSCAPE2.0! ,$-H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳh۷pE9s˷[uLIiE̸ǐGo˘3kL7MZ8L^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@ZJ2tX!R;<` hK.$"(DcL- d/@ "LMq7@D"v6:*N#ޠ8`0~ 7N*Dhc_9wB.zW筿*fn[njJy宣.8+ȇ;^{Q`=7z:8} ~~Ooh|<`x1p?|`~ (  rpB(B0><{.B g6l sxCP=,!}(찈!%Nȝ RQ|t#$IQ,#3HLrrdr )$$*U)cIF򕛼)[KYV%0]aF\-ib25|&iT)Dq&6mB3ޜ6ék2*ӄf ])N߂ҝ%>KU+4=[T괩e)TbԩRVJUr,XU*V0%+ZzӵUnWʕҕ)[Q׭䵯C+`ej[k2'|}T+ٜ?,fmuufC+Ȓv)=K.ڛ,,lx䵶EisXPp+ܑȶ(rB~dIsˑR$ӽnF]X-xW{$w"}ol+_ .~c+d}|$>D! ,#! ,$5H&`Ç#JHŋ3jȱǏ CIɓ(S\ɲH0I͛8sɳϟ@PУH*]ʴӧP%PիXjʵWTJٳhӪ]3쿱lʝKݻ:)P0A[x La<1aÐ#KL*#"E[Mӓ4(LE@7۸sc{;rELpȓ+_^B<5baqƙkν{W3i{_Ͼ{n"yϿ&Q~hpF(aMNhfxQvb h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n;.pbyI|m/fΟ }eNO_f}]~⏟e_~Ue?IMޏd;XpH<`1=z w$ 樂3X rpF`B(-:! W.< cC6rC !%$!BFLx%2@N|b(P"ۣ-^ø19f0P;+1@òl:CsJ -`I:!4,4o D!A:^ lƏuXϠ1l! ,6\5(¿b H`„ |QD#JtQ#C1tdž3ZP"ɔNlp%͌!^ %L tvNC%Xʳ)ϥ! ,>5HP@ǰaC #&DСC NSlP@?82?!߼~0Ȅ‹ ! ,#! ,#! ,P5H!\C :D1bÉ+FĘѢ@ 6h&LP5)Q… y +/x8TJ HXŠ[4iA&F h̕8Sv1:LLƎH@2C! ,#! ,#! ,#! ,#! ,m%H*\ȰA #Jఢŋ1NܘǏ7NIIM\yeD0c"t QMhBNPD4Xd0H† 0gJIիXqv`5r}CG,: Pԩ=b&)R` y7oֿH)x/>b]cw s;H&lf\pqDzIVȄð 35JӨ_J\xl9rLP!VFXcW)#Xi>raOgXšNa>\Iyq0ACv{#Pc=5t; ^Ť a^a@U 0$%䕴c~8pE?`X.v2 zM0 8 6Txc1D>a# I!1QLps\!G 4HWLyB *A p"#2zh0D{yԀj(j"0" hP`j&j~@<@y)kqd*-0( (` $S\gug, A Gm¸A.›дd *z0e{P}؈${c~hN=ݼw8n2O=X75#/*=ԀpS~= *,P-z|: )h I9䀄T  ꁈ1 |`k AX7aC6Oz$@ 3 x&p}4Y ";0AB:п 9o/ȏ^@#ۆ :@"* Bsf`idyQx=cQL0ilӝ  7<nļG0\1؆2` >p1 4@Ј w&ɣpr@ |ҁ9B )NGD5*ROЃm!$@x\aLB:y ]y<Ao,"Y0JGy-w2k)0V92[XK) @مT7xK:Gш52S:7.e ! 7 1FC@/D?񂹸tsvxB6 X.G#)%N B@%AuCn`I0Pͩ `g#8,@&H+`70CI1)lȯ};R`  A1 p,G|}os%ah% E)w  0Q9C>83\`a7 t'[6?BvIÉ/"w66V0786\{0S_E@XF5Lj6y3|7H0#}cR0`/an  @ }"N 0(#>q-* ./ ,@C`*;}2AD'Kq0V3)/bT00<+'R)<-ٕ*@1L )b.bע0fv/_U,•)&`є @,! @(U'7 +B  " G0#1PV).ye0Z0 G 1! 9t SI6F(A ɝ.q@!0Y 0"© `0 fm1zj b@ >RUYXڡ]-T`|`J?,ڢ.1</t0:<=@B:DZFzHJKPO`RzX* @ e y!Y:dzBh7eڦnJvhP&2ozJx 6{Z:pP.Jp 7'J!bک:zz! ,#! ,#! ,#! ,#! ,$ 1 8Tῄ !*C flG|I R! ,$5H)\"dH!āRdhbČ66({8H(_NjӥH @x !C *(P67_=enư0HC 95Y: C'\XK^0ЋFR˶ˮx\ІYyMZm'˘l/0e`0A_%_!8ps 778@)Pӻw`~+L ) B  s/E1_`@! ,#! ,#! ,#! ,#! ,$$5(¿ H`„t`D(V(QC =Bl RC?SfeŖ,SS0nZpi z x\ІBJYvYf{рMKR8A^78@)X$L )%^yNc #cp! ,,$5H!\C :D1bÉ+FĘѢ@ T A PRdnX(qVBh(h;L"|a@e&LV!ڣsG>r (cX-ѢeyQI-,W' i9:$h D"3! ,#! ,6$1\C 5Xpῇ!BHH1aÆ3NH"Ì7rD!ȇ"Ga&OHJ?]/cpc `iN#5Ta>a82=rT3^@a*^ KqYGC&El׸C̽:  $Ha/:y#셹0)*Р)Td%!! ,#! ,H$5H)\"dH!āRdhbČ66(dH:^42eH" HaDK hKW>毟2%>QB`$!?\aA⧀K^Xq/jQIf-M1C4E8OS$!B$XES?cP6 9ăuAkC^Ί"剗fJK]*X 'ea2ebWj*Tl03 r떬+:IRW4X/Hpb jSaj@A"Ҵ(_Duߖ|! ,#! ,t$5H!\C :D1bÉ!La >\BĘq8p"!"FFr%7xPE&IF9 B{}y4)Qs:ɴh0AM6pȃ Л9^zD*LAT5jþ~)È1 ! ,#! ,#! ,~$5H)\"dH!āRdhbČ66(dH:^42%I 

! ,$hH [Ȑ!#&LذÈ'*"Ɓ7rw#Ȑ#4@J8lP@)$P` D|ÝjiX*Wf(xmʢ_lX@cB{iF8VE2ȖtK#2TCfzxpІY( ¶g #Vk\ |B=yul>Բ}`.b#NJ-PxXŪQ!)n0j^77&V$Dt\lXpM4@P`„&Ā!c.{R<Ɩ'0 ! ,#! ,#! ,#! ,#! ,#! ,#! ,$5(¿b H`„ |QD#JtQ#C1Pؠ@*T|ÉGWd%xz  '$fV:TZZPO$FPBC HȰ'Iƥ{}qg_ T\gVhpR)Z bacXC! ,$5H!\C :D1bÉ+FĘѢ@ &h!BC\A%G#|+[bx V9Vn؀wȰzӃNbm ' "Ab@-7'pGbXS5IR`$HHNjs "! ,#! ,#! ,#! ,$hHp & 2t0Ć!Z7;f܈a*TpBy$fJ%GX @s>&|H& dD3Aƾ8F/NN+D]`BA DckɆ5ц)Y۝ ByeK#^nhKE1DaVOӐ3@Bf3ҨK! ,#! ,$ #5H*\B !"ŋ3ppc@! ,#! ,6/\x E *\ȰÇ#JHŋ) (SCIɌ`gɗ0cʜ ʖ4s3!UI  qB k"a kM{8TIV*؀8$_&  d5mBW[CJ1)D"6&Hǐ6P :lg*p/LϩKK`\Sc+"t j1N=BBxnc%$@%}TTHxڜ+\A>TO>wTSϑ)$`1-TU b%=xUhS5F:8i yln1(̘CxC_p>yPQ@tP/Ќ*(%ȁD**5|QE3PjF`cSE IFl4L=iPj)&4B_@6|@$NA2pZ (YI` D28 &2@BMܰ?QuH 4ۆ ]o/=T6 1*9/h)1cE:KD1HL !K+VȰO %g 4 @C=ܒ*Z ̰B 8TÏ$',BzXX(B#46ĀۉPP Üm0C16{l!ڼ 6;JR{h&9#W@`8?PSZ !,[6ѝtۇn \v] z@8@D6|@o@'Q ѫ aHԨeP>@; jȼ|A(`s! Z0`Dy$p L6(:p0d$Ay@:EkȐ -HAŔƴ0` Np P -D8Q82a1)ve&r``D!*؀) 1ሻ$/&@d8 <,CT @a3#@57et"܀91."ƀk!/tպV0 *X618^\:pa((0r!bٍ@:FAP x)XW@F&T0=TG&` L2E t գKДMʇ)4u0 `>wR,LHA`aP6( p)LeJS/Et:~U X)Ų8J#$o6xz r`esPCt*s^sT(,Rdft C];I0 KiMX 6D$ n1-la Z=G!@y)rh#vP(I+`f De )EbTm*zO_ \@l( yYOd ٯLd! ,6 8PȐ! 1!D0T…J1)pPT8xi /!M|Nic*T4HPF ! ,#! ,6[*\ȰÇ#JHŋ3jȱǏ <ɓ(S\ɲ˔#ItaC :pA:g J4#O ?]RF * Nק<+L КT$PjVʮʳAT/"TeW`"R4 A) 0B%@x%o`!"Ac&H4׽&"ϔ]N)s?Cxa-Ia  ]Xc=^;z3L޿3{ -<gMFC5PE('P j`d %`m՗iEZ $ Xdf5 &؎)ȄЋ,Z8bbp &4`#xb ePN U6TN;ؘwM6 6|PTK5hagETbSя"5e \ V|!#2uФD6` ē~(tBxS@a5O@BZ! C?0]TXO.|A ?,"%tQr_>p+Ug bz[6)Bz-H{j)SI. Dl#6|`HtqaBY,μ3>L54_N)k.4A 0ÅUl P,`CP4H %q c@v (F$8!? 1U,琝m$ AA{ ixA {A`vъ`@*X/p0 bچ ?*QW $>E`Q3-`cL N@x6@ x[Ep׊?9h/ǐ\h6}Hh!@dPhQl [lfrA3ȥs3R  `\1 `@N;}!)|PD`~3`ѱ!@`p$ yjQd͸ E/>̰?zCtlvc f hXE.lcf NBRy~|B=9 @A?D|`CX7*󑨟t D!* [N%YD3d [D()-`mO~ԁBAA JeZHDu61 Xk` } H8ꀃ)#pUCA= &B`6aR AC !֠fpj )5!-"zU=unp!uC1h&(A-ߢˠE3QNs#C@0Hc8E܃~UxrߎS ,` ^@Q́ p ` c2#wUÓhv'`p0L;q8e /3=Á7pDwj#av  `, *{ 6w@0*%@ ` g P` @*\pBG0Eh J@  :aP,pݐ ,@#S-3 iCA@D Ѕ\q16Aw2N`Q1.CC~]p-M#}@Ȑ *7w0e6 s!X4QڑzqpQa@"q x6тM傪6a50ă D#+a/ `(%~-t])E%![%$tr@P*EAP#Kb Q ^& ~'26!VR*p$5Q97y# `bgk& #,. kG,s)#g65[0Z 01!Ђrf Po9A+ gZ@ sF1i{ɎA9xYP 1&j{Qd RdD. \qT 0B Cud"ӗD!{Hġti:YzGz*4<0ĠW1~-1!i[f %` T4+ڢy1  3.0zv79)F (KM 8 /` Y]*#G8 @N1\F 2Grim OlpkAPtҖC%=b P ,)T)Ub"BJ Q,!@,9@!!Ki!q )%^( & * -Hѫ3C48DC(ժDҭXՑxꬣ *޺JNu!6ۑa/30101 0eB/)*y0nr-"@-:p.1x,wWm787/ `V@P/Ex0dvwk0@C! ..k7+j  Ea°#  w2RRJ* zV;S;!P ҕ\qW`ODKDO4z=s!\p+=>y$#<kE08<::k A;c_<7&@5OH=q1۾vp=; 6 kQ9 KdK,C)0n?KTK4JJ!3 xHHݱ`8 5H1I|Id`{Ĉ a$eR(ARdW0GQ0D@iGm @E<4k4kMULI\d]9L> Y\GQJd }c5@|JGCL;54ŋ\Ikh.c4 c4 UĆ /c36h$b$fbsL8 (afaL _B4PfԊh?ˬ`1pC3) ` g}a-ӂyz=6dP b#Vb'6>4&Yښbapc&_Ӭ-GM4֧9ԫRԔn$@!HUnр(2 & ȗ@Akl3' lyhjst*T|ݓQLMt'p  TP ` +sڦy"54&GE|IkJ$6P02 U4/pmr[ v`` !:$hyn$0^ޔ 7$ kMn)ۼ,HzzBp4v_^pWzzkgy'#̐DԺw|W[: 'xf:ivmjt}R 8??<*#wys#'=hξ]`TWxq>M&s > Oc~yPp+ אH{ y7tp XwW7z#E`ޤ M}U/!\o7 !@>gx9X-Q1 ۠ @7=~RgO YI.q7  0  ;UTUj\Z# jVXe͞%ZmݾW\uśW^}X`… {B c*Tpx% T A&*TP@)Ng0AˈmVqTcpƹ2,|6CSt\i:8Q:$4oHݿÌ#˛/&1Pf!f 8X aʘ" HYc4excE bX " EH bF%&8…cadH!B $`rqk!fds0h|adł@b5 {p 4'-n[$JGI~B!ȁ]6AL%lMSOVb0HAMԓ >TP&8\>ZX0!Фc+ pa {$YIA9bZ`09T0("y2 EPtff18x+iv) >Z&8 dp'دGxc@BDB8=x‹fti>ȢE:F)Th-z~3Ƹ9Jz` |=z2a! b$9lThvk@B 2f p(@MT`_"8FZs*( Rg F6 sV񼉈 qE^g!7?R@C ; "zaY60DrdA1~d@h4<4AJd ܠ H. CıHHSG9 DWXè B:pXP; 8႒GE-^&zIM&k~?E(d,q9 !IP@p 3C- )j +&B&ف4 'u;vCW*fլ0@<{:X`n/=1 B]D2K@a 0XR/C \~@ux" Y!/4ުvĕ%liA67$5r0?j Ԡ EC|>6* `B@U!|Ce!'Էm~ >7"0B^wEs~)}Cu2oh A! ~ბdZP0#GE,V!Bx+4A>Q F2G0~ $ wW:gp$ j @/ h`w2}.1෮t1;]`)|5IFA}" Wb+  UF"F0:s j!;ш4"/APr83WJհ5mhfk8EMY@*т PUp hw f!V }'"X*}5 &@G1V!wH1.#sϨ'q0yH00 x-g`x(ba[!9CLr/QH>2!,@ B: $`^7Hѽt(_Cn0>1F 6c vDw{#CHbCτtO4wQ$@~#>;o3[|??'}d^g]{Fǽ{n{;f>a~|eG^|e͇>\}e>Y}keT2,! ,#! ,~5H)\"dH!āRdhbČ66(dH:^42% &H@d8OL`xHS$*#"E=7BHЀƣ|2A ŗ+W5R聧YF,l\caAnjV:%DE%* aL9p"ɻ>nqI2kp0 ! ,#! ,~ 5H*\ȰÇ#Jb@! ,#! ,~(¿b H`„ |QD#JtQ#C14h၄ )L|(2Q1Kd/04a*4.a6_MDJeVա^4`K)ӡu/ϯͫP !ABgjīpL-qĀ! ,~5HP@ǰaC #&DСC N"Fcxv 0+y2823 b`AI` Ao E8й͛֗%_1)}ͫP BBE_a"$HHq=le 7;|}St0Ȅg! ,#! ,+~hH࿃BXa‡ :|ā+^$`č:[X $B$HBH1b-[)^BG3F)DEaB;K:ؘ0aR>0P;+1@òl:CsJ -`I:!4,4o D!A:^ lƏuXϠ1l! ,#! ,#! ,6~\5(¿b H`„ |QD#JtQ#C1tdž3ZP"ɔNlp%͌!^ %L tvNC%Xʳ)ϥ! ,>~5&0p`*dذĉ-^DaB k"ƎhDI 9 0/c9cR0'NyTRAO ܰ(L9xD t7obD*L۰AR]Q}N/ c ! ,#! ,F~@ A*T(@.\P Ĉb0E 3…J8(2!Ɇ&`e˗$cH [Xš@a9.YC\*'*IY"[fUԐc $Hr K4\^5P_>ST DHGi-і'cF 3;! ,#! ,#! ,#! ,#! ,#! ,b~ '5H*\Aᅆ;,H D-VX! ,#! ,#! ,#! ,#! ,#! ,b~ :!H  L! Bt0C%HcB ;R82DK˗X! ,#! ,#! ,#! ,#! ,#! ,~m%H*\ȰA #Jఢŋ1NܘǏ7NIIM\yeD0c"t QMhBNJ W?ab P``p34sUQ5‡! J&# 0@"MVxAF"iu2 ـjjiJA_9ev'Pe$6@A&i'Z2蠧rhlBU鸡aM#?0~hN=ݼ]P'8@f[oT0E*{ ;s=`@;Ѕ<@N;cr| ]@@V:֠ @)ʹ0T23xJѼ}wH5Mw[-6HN?;E;41AtĮȭ ʆ K;sذ! 04`H=u@s\ ?Tc]9K,9L.P\sAF3 %ȁD*/8p#( ~A3l) $쑈%A ~+|11f\KG-a 0`xG7юM-HG?qm(F>xq l ,AH7G.`/'H`6f)D!=؇+xq"ptPcZX6h HCb#B,j)c.qk!H E)R1{'61v`cF*F|T6@H/`0̓ _ %%88T۰CLA4^@#CE4@sR( QB 0di8F xP,`B"JP&i#p(<0|A3jG,l0 )6-<"kHfp5v0@`gZSGnTJ J tz#VP `@ ƒ >A^c`C찍"H*Ђ5$%x8Q ~Hb1h~zԃ(GɐT PVg!'nxǨAЅg4[}!؈6 fEF 1ٖ&rpKac t s<P h\vPGFrR6ჇP‘S$t#lHHpQ]@ȶk˃ `AЫCY" |D30H #Ӥ 6y`l#7l%N.lvd|b o@b7A-ٻbc}AdTk(R*QLإͨpG 6-]bU!B?!?C"(&0&rpT{>G> }DZ0h1 \)U`ߜFY5y(6$ p6o]Qǘ勮s`u0`,@ysUɬ59j|,Cow ] 6怏dxG>];Kd8F1 `A nd(a{x$q2-/X$rx}t>HRcpLpBtCTlT88(h/|`]}dHq9 Bz?@!@` w \RuE d 4=F\ - 2MpX; Y$&0 XԀ-e}5heCZG}Opt{G73qT g}}eI"C~~5WfW.`)P@p s0v&`yl!Op "{_pwv K2naPcQr0o{a0 (3|te 08CKnhe!b_ +8`Q"DISH[4Cp IPrr+F ^@Dh FG ^@C*P 討CNC;pK!}k "%pY*b")" p2!srRȓ(3p$I"#Z r)"`+w" a` 2#m +"ЪaP\B*`r eQ,R@"ǖ*.7(}ɡnɪ-40=.䒒 n`3 | QC5s 3bX Q!PE 3T.*wP <)@ 0sݼ I8wYZ..صc  E p( ss<#2s0010 (S;?&" e% 0kpdEpn ]p6Y L y  1]P b7 @c'bC> S>O#@o]!=4AtA0 ="K儆 c0Pnȭ>uN 5>n>i@W`e @A q y1i&P,` `psWӳ]"y@ aح݇Fp AQ4EUTw|H$IpiFFT  ʀYL[VC #b ꜔@$DD N 4\E Tn$PEC;C<|~ : C ,Bue470MPM4$LQHoA. O/E@ @^Vf, 9p7ڞ{'OdO>PNO>QO֐BfHW /Ф֎QQ'Ot$B.@AfO1H>G>p(Q7QwQ18+2`R<] .p `p ?J:7PjY-<. _`S<נupj lVaBX@ ΃US`~j6`EuTIu&+3d 6[?ZYenU?T1Hv/ ;PʛqoSx `G@$ʅ`V`P 3UeP6ө a| ap&We^Cիy_x0Uhoȯo `a(/ mB ٷl`i )a 1)"pe%R0(5!B3BBp`~(5*l)xHѥ@5 @K2Y0v)兎>rQL Kl^h\a! (–Gy9DGKLHoŖ>:}X`)z]yH{mA`H 8mNi#s/4D'}X?(t0`<1 ! nib 랗*`m2 Br0ƢCs&rl j`0H_Kq"IX"H `*4(p(|oqACH@,hֿ } a Lp%>\8(`N@(8(lcEAg1 Q{S x~+GI @iHP<4>,! PX Da艇쑈.@b/4c2.7 =(II?ЃH>0 яT.( T$#<6e 0СB9}#>(6⚷vQ,tA3zM^yp#TP ! H`Yer ծ~BHN]907f}8-?>vQ"#DRAC0vwt reТKbʏ0F>A!G4у"$,1& BC0hb OFю*Q /<ڵh`z1E;шuZ^5; 4Eۆ6A@dCG=fP=2Q51%;Qm!Ah!OJ Tn hbI6zQeTmpؠҀ=TsH^.?hò`'F&<(('!!pV9`n/ҫ5zj DcDЅw4,TGV 9nr]Xo>LzаpCra__w/lEolmB >h {ƽIx3k+`7@  ),H;!\!C1I()H5( #c)/H}{)V\/H'<؇Mhz.G2 ~ȇ\І_ҔdH)},|^І|p`[ȅ\`@ˀ%I" ~H7<:0p(+2ˆ#`5 =sHQQx$}@zk&K89@T0Ͻؾs`[pk jI"WJ<ӡ* ȸZ  }#kp1@KʁF(m, KLhs~{pC304@}PFh%S؁6 @| -CS؃wpX' s ?`$8 *؇8Y!DМ(ГۺKvX#yWhUN!ȍ'r)UT(_0A8[(*O8YNJŢi|0J ˊ`p`@p++"l<[ ӹ4Kk0tm\Eˆ m82H$}(sʦnZ ؃7N y ;]ަI8.~fnnZz/ Me,~V`P9phR.r. 8CHtȅbQ. QH40"0733C?P\/ІLN0thd2~00ㅰPy  WSNYkQH_Npz0"h iZQ^pΑ谞I3o 0 2?H$ ۰X47 FG`V(05erR;?X,ov}QE;Q~S9Ã= #XX8xg ɟ=B9=xK9 P3-^G+a73H=k797pJbs{0:H{ `#:UGPkh0ehڜ2p:HR Wmقk G( <10xm:d<#.Ѥ/eJLNS$L$(\jB`&[)q#ʔ*Wl%̘2gҬi&Μ:w'РBY^Ȕ0CRtp&) ٧c688''ith@~H)$!`й#h0b(.l0Ċkb$L@" 1 "A(i>r6-,sBpx5cxUńh..n8ʗ3GJ?ipy3ak ƇGH v|y*Y!|e͞E57 x tQR?A ?Aaw3Q쁌?Г !P&N75` (#A 9$@@@ u5$*4A B HɁ $$uQP (y%q1zHU$q9'uKMZ9(z!Z(C'J:)s)BPJMAc:*Bfzꦥ**wj:+ں޺+륲:,R ?,J;-A[-j{صz-:u.嚛Ҹ窻n.ƺ/:;*<0p \0 #x ;k03! ,#! , t5H "paB.@ x>G)>HЀƣ|2Q)41f<5ba+ÂDn"ӈ!8|ȡD!Jdl@! ,#! ,#! ,#! ,#! ,#! , 1 8Tῄ !*C flG|I R! ,#! ,#! ,#! ,#! ,#! , 5H*\ȰÇ#Jb@! ,#! , 1 8Tῄ !*C flG|I R! ,5(¿b H`„ |QD#JtQ#C1\Р*D|(BTcb$KMfI{A(#4ģ!ƤKDCPa/:Ȁ*WIYב/4uVDE ^/1 ! ,5HP@ǰaC #&DСC N5{rm "6 yUk_'%kjDXC&EFA 2%dS'O!vH\z>%'ֽ+Q/_ N ϠC]шd+W,HMZq@P+_ L&  N \ @ '.0wOyo'v`{kb(*T萰Y 4"`B H I&P()$@AB`@!B%` W*DA h% 5P O1~l~U~Vx%QLv0]a(6•Ykb)b~\U DI@*0&*yGb~dӗaBk@`e馜vnQpAW#O053!"6`TO?(ଷskVA S6y O=㸁A ̵ʸaԮЬ?AH.O ~ԣ)$`1-T eA ܡ?, Tw aI8F+b! U\hS-6;r\4aЦfLPڽn28.ߜsD^p-gY*ʊ/o1 6`$4tpvx7~l]0BSD ?kT/c~ bkfDsr<@ E+r@#i$Ac ;d9|/H i+[H0"mن :c~!9L1D6^Ѐ ,8#I:DI* $&7:ڱ/4<@;xǟQ1L䈁mv3Xf5MqpmĊZu DSEM@-Z`?Q`&@JYJ&%x8Q ~Hb ApK !m`FlSԦ1w $ !HXJU:!LR$)Ik ,1B>0la 8Cк5eǤ2 kXr"|:U,_%@BV y FI;l& @PZTI ,UvTmP 8Jӌ:="D ͣ0B ג6N @:1`Z1EWtiz pSpvty0!,ۏ 81 X$Y~ԁB /)#8-MQ P@q1c-Rgs;KQ, |Ù+x|T:.x{2iъ\Hu,3 i۠ @@2҂G+% @53@(/ \wR8s֠0o`?Q\`V Q*` -*90ЁM11hp>1P'_,K p-t` 0LP)-W.fh%pⷉ؉B] 7 |#K*a!C2-?qb!CB1#!EM*!'8>2"yRM>2")(D9OHR%N  o8 "+ @r%?RIb؎%2`ib5‹p4-#")Ȏ SB"&1ȋҒ$Y&bV6Hr5ib(G+q[S@oG4)q7 8r㨔ST$+I~'ٕ^,y7A3)C5\P XUQ C)]\|ٗ~2q%X  1] ٘q硘 yJG5(1'/)GyK(*RA|yDf!(!*Q999y99 ٙ9~)GIY7ٛaYu#"ps"_ "&@!!$&R)oA]*G9)mAڞIrj٢.& P/-%s24.g06+",S4V ZV ѡ2*W\i) "+(J*/l[J q @ po / gY\P0 u.f\X NαR. $ 4m8-R=Տ9v$ {p8 Y?0 p*!.,-@ 0- / q`qt @ u١=ڥ}ک;5*Ї#;-H̀ o0 /ֵٝ-a -K>`vA iiq}:.0=O<!H <<>ٜ-'B@ V"z-]7 i0 `L0. 5.7:V~#J HpLa{lJDJP^p21?@ ? \  R ?D@ GOLtnx|Sγ!b9`%XGq95ZHVVgAIд|S0P/ ^E | ) ;/.nnęܞ.bd@ckqVa&^APdA3 >0@ ?a r9DG "Pހ^ |_ͮ>~kڞWr%GaNkk g/pΌ / ,M P"p|:C^DN} X^*4og:ܫXuW YP*_dC#0tkvow r.G D 0_/6ŐM@?Fguy}5kY̍3w-T؟~0  -01ݐ npS)gڂkP} @ `` Ԁ ɽ>25±$*#;2"=|".GC`"` O //A[3$0h@! 5`"VF ~t$H#K*RJ-]SL5męSN=}TPED!DlJ 0eGD'j #'$^2"b2̬zub֭1vrLGX`… FX1/ 0Ars^d! ȓ+/~j֭][lڵ98 )!܈JNܮF\r͝?2M /DPq{^xQuЏl^|Fh/@$?"@{8  jإ`:*rXH+Ы<8F"%\ ĞP$FW FoT DG& \F = .ŚqJyrK$< (0h$ *@hA4`74! $ !`H)/9:ȡ @L0P ,ۃ1TQuJ)KUTä1EEPERCULi\d29x„ZjeG7E]g+TX[lIՔJarЎ ~Q„i'z^VZG=56V1u2aY^׆-6cah)CR8#,9G,:'< W\ !NNy;AfIAhv0׎<O80f 8X i#! v(tk~US/&a"m^{׷ucF-hx$p2)ddb`UhY G\qB|N H0i7Cjd 4#ᆫT<'N`vwg3@cr0N `-~m`I)ŇBgn@0Z h ^׃sGLEU%T30S" ' m`Bh l0LN{)D_jsx[MB`.5xĻKe `uXI^heiЋ_`zT]J4X 8Ii"0 r#rI$̱p a@|xfRD'>2"VJ $tcѕ nq ! i 쑈.@q(T#p D y.{˚5$0 HH; OB30Az<ETB(*⭨'CyǰRKe@ʑp@p,!=Qp\@ :w ](ڀ%6[@'1Xp#CX= #Ҁ.,0:YOx:,=M*%P))U⳨*T9 CBpt@o*Ktl@uj `u.Γ@hNTn`[ԥbMZb:Y2Ą%H.y4yg27h-i%!TUIb˗.V}N"VַK~(1ibCŴֹυnx"5UH]v׻ox\Lx(1aw/~{ LBȫ{ ׽pqHBN6P2`#{?ҐߡdG8fآ# `@XȂ= W8`4G?dLXX9)VN1 0|x'lxO6pH`GW/8[NI㬌`E?1zsGfFs9_9Ltw:\@ T*HOo!gU(`'JģRVummCG ]ٙ[Շ`R QWA=$v: p`͸lF<@-U}VM@U(XC$ E]&PJ"c Ywrݶ4G+6Vfw!VaPvUӄ׫ޮ1><x8Ӄ!hG.@m! 8?X0x{Y)CpkC@->xt(hh |% ?]#Ch_P _h9)O3󇆂0z6[ț >a!؅Xȁ \;h>. q$H}nAO ' 2(~X6"[Ѓ)6إ%}h @a1V1P`3ȁ{x/ P}v `EhwlhMȁtdmP8l^l"BXB:!~ȇ\І_H|^І|p=q`l;{(HP{`iZ}p^8"MCJă06k|dPF `h(ekBxG1]LlqhlFlFHMkW~PPG@4c+Xchv@B|sT@|!o&0FdTFfD1"tJHwԇ8;A0 D`Tp",y A/ @"KbA$8@ hQ0 J8DtQ pfA&8[IX`(fl8ӂYXt`. y8Ll"Lh?0Ihp0)Є{hrK2] XXldv?Ёk,P*{|~H?dH͇@<6Yf؇my7 ?(_7TUUVdD"!mrtRD`_9Y5#@@Hp{=fȯh:p0C:%"dS> ȣTeC-/gB#Gņ.@}X?2ЂՃPUM~C|9CUfI5p=р]ŀt[CH=8dem @_/Xj݈8|-8pExP]Gu\pPc i=ӈXkʃŇe[m-+2葂lІ9dpŽ5628:❀؆C LahYkej{ 73M8(' iRIh4=RO M 4L"Z(6{" ^/ Mfp Ć۽J.9Gh)=JlȄhXȀ0C}P 4P,;" 7{`]혀F.&8M O6{O]@5n7Ύ 1mԀx #Hdb) uhIhs8=)ahl,|a-.kmB!b#\I<_NL|PH :S1'0mmA@}H=K>X,(EŲC}2ZCpN' ~;ՅPŊ /`E[t`[ܽ ya!8O`ONg90O݇ŀ0Dn"f$0Rpz X[]h c~@Y䎹n!H~~h#zipZNC׎E& h"|RށJŵI(NFPN`hUK$ĉev3C@Q46`0|p:f.TnkiGH/&mֆ5DnфE[tP(.8`@)ZpԀd, =EɄwj!1nكvp[À<=:Ȼ]|pj,w@Rc(h@~8; `Chʨ Pzi)J 5A 06+ AoRva>8URSC`H:a 6Lr!.JdW8Kcxxs5 5 SIW{WPONXW Ps6+s+7AJ_v[dt095aJx3t6L6Wu8Tu{_7p5y^'?y`cٍDؒD_nP@fŨ ʠX:-򬻘y[\SH))ܺ zVx=$ʈAy`  (-xzj %x{y͉䪉O]_wWW `k"o|+2||_}r7rGo72}90-)x;/zOL<<6fLH_}F6zKJZRc@ ِ81};s/W r*Ǥ 2l "\lx /M*Wl%̘2gҬi&Μ:wg6j?3C :ˤ/|aR&Co.O M8 Gl !< DW i+Gܾ0^!BqiT;=KN}aÉ;~cȒ7UV@ )R@T@*th Rk "L`:*B`\AB+JapvY]}* IXc"x%z&t~Ja[C`L,tbUx{ $!=2)vO>@Z%8.?UC.ԣ )pAS8ntـ!ӏ2TBH.1=d_51!g\202x,QM= ?qdqK= bWs5!pݼ1J%Xc BK 0>;!WTG]7/ۼH 08`rRQ? ;2\b tA>x >ףĮ>"$aX b=ioIQ@$p L|8>! 0גū.|! ua Q?Pc$XjB'Ş<0\W)`] >7Ї(j8'vH L?P@@ ;``xC ^3>6M(1Lt"&{$b ~G#&PAc"Q8rғ($x9 !  !Lc8% `D,e =La N A<0fAII 0@h@YS椐 A@0XZ `k AAX€1`F@F:ш܁F! ȅ5 Y`hY~/vE}H#_F|d\cNB*rpPRQ l@E - f=nV"l%XD;\v%+mKLXjk!&E)6ȏ[DHPj`G-5@1|,d#+ٗ@`1QVb4| KL12ZPR AcY .!E+rp}S CLQsȍ@ H wq"a쐳 g6|c Xj 8F xP'6 `(<%r!% {$B@*1>0voAݼsƔu,7 ds#B8D 92Nͭ1Omf!!clݕ2l؂:>*h)h`= a&ĥPr$xxl`hXǀ ڡ d #|_0`MLKdl0 )6;YPc@ o5 2^]s;v jpz#( 4R(Rk;?IR!"rc==bc 2\ Cj(< 0#03V'y@< 3zNȐd\ň .f1;C#V.JAqXk)[ҁuE)H<`c .6BbvI< |Q% ?&Ivn#s +M& V<l;!g?tsiNR:` ĽUvظ FSJ'CXVV? A@1@4Wh@?fM\\`_SxՁ\ A̩ٗ, ]αJh DD4]Q!3pQ9D,@5Cd1`d@ @aYU)FS`ll KVe͞^vY60A3hꙁ@ChA ug6C?aC hQ]IM"u$A@D?8 ?yƼ\ !Fŭ!&RG! yDH8#9Bw( %e .ÒG2l%D_, , %~(!h;b@U l!G|=. ;£/!&t#Ah.(-LA;dTA3\n5g0: {&6R(>Bt&%&Ɣ&V @AFM*iBYJ@܃DXy舎[6BR9AahQm6B*5T? b5X(49Y6#D B?IH/U7(@,-4C;i٣Ɠ6C7n=D6@U*@*z5yBT XK* /LFvŶdpԪVbIy [b_'8΋&K;nC hp^ uZASB1jr)rj5;dT?C2馮+.&\C&mTS ܍tC ,I\ADAý/o;C/x.,$\[UB @Fo:LT0~0O{cu0\8JU4,Gq88Ǐxl pz\ls41{ Gt X K׈t\Drxpنe`J?G H(@ GURAu@0tx1h}P0 zG`!t>\@r0g)H  J'kqB$6С {<@+Jcr$.Hx,wI@#W%g2')+/16-y.'PKw $SXs2.+0KȊ]<| .OVB|M$>O]B.q BsBdH.CDEpCc h- Ĵ-F3TGuspN(s~`Cx5E3ԃ& f  Nygf~r-MV[AJV\J5__VBl~Xa?ƐKOHRQW6@u<\d6h6Mh5b'vC6ɐKj?f6lǶl6m@usilDLvovNP@6q'r/7sFj moWwKPp7wL4 ¥px7yiiDk;OuB7f|NTw4{ Dg:DsLxJ TD.^D$vJ3xG7 k8+zv7o[w}y7oT$,cmU 9wK tx!Q4@Ђ L7 A*0lD=M9`\/ 4Hq-[O&Nh@$-HH6#Gt!( ;țk4zxk1D﹣QrCW=1FcCR?-(Mv,:M*|ۊ 1 ;0 p:Ȏ`;r*xF%<@A$=cE2B99;ö6 ˿L\̋ T@RM>ON؆ ֌XȄo{*()@ tm>4O=0l TY Cş6Xޕ߆I8C)ĒJWfuC,@^xL * pG ټS^h>8K\d ׀#Sr%K@ TA ^p>/hC>8[b޼'\hC"C%-=/4 \%n6Et-.y@A(A<t>XvR+ޝG L@c@AlbRcGA9dI'QTeK/aƔRCM7q `+W.% $0@QцhHqB . #dN0<`Wz1ýFbT$A" s>BpL- VH:9 I'J!h :03hf"FeT98|.f eZAÚ!h"FB`# jh&bKg"l!dj΃I` BDk ƀH+d vr5 ZYW5(؀eݎ` 2A 0GnM]Yo[W{Ge.vQXI`|>:aA&#hf>` m!>,L0q2yTAfiʉ]q0]pllc)PB9PAՏhزf`@,@2`+a: H TQ [ `7G<p0| E?%'PqZ2tT`.2O"._x`G=}cLf7}1c5̃KB9 l0\blcNJe 5("#ADthD&@|bc§ (ȅ6D@F;ʧ`E6s[0 '& @|Z,fhhfxv )1!ؠ'\()$ f:-@mPR207lO QCAV$' ?5B D]8xԦr\90E5QyEFe A(MXPn*^pHR xLPN#R@"E?!]r4 xGQ ְ+ Z IM &0?aYF&`D!h]3 `d y0x\7M\#,:8E: tB]5d+3ɜ *xqˆTr.`d-@BҍLx`~?1ܼ CTxÅG4DB:ܐHjx 0ik(A|5թP Pɱ x0P< LЀ@#Wn؋/1%ee) 0(y*P@,U@fO1t,ay@1.^<2 `  TjRz# ]Pd`w=6!F@t.g vY6ʇula{F@H$7 R&H0-MSEi(6`و $b0G [aVBԏ0`ЇOrh"Htt{$ ?1}9tC;Y!юt 7JvB`,/,8 CfCC3tEA8 H6Y`骣QH]I}$'r Şvmwmϝuw}~vRx/3xxEvjQF8  `ZSS L h1{3O% x^Ӏ;wn(#qš|shYD>H .7>@Mrc$jKa)z$,N`NA!+ۢa fC`al`^410o6:v^ `b.Hb \d "))fc lz)40 p ՎAF x@ҐE jq!4AA  "BFhF! vAHP lߘ0p  ;``  RP  =- >An#f` $5`ncIM jƒd#0`B((!f>:(w|lj6 aN1]g:BQڦ &Bʐ4" !&A@8b鑦>`PqCE$.+r#@K*B(j4*I$M qX(T,B-}`p #` }r#9 O(2)5!2%SRDtip@ AK)r+$qu(A઺2-r--6`2.r...+CL//2/ 0+.D^81Q.N/S 2% O2W"2 s3P3N1;n0Ib%M /+232W153aw)frJON/jR@$@#dO4T@|xƐ2B!Ό̦-9Sv|p8%+<3_$<43*׳O Lvo8O>AoX'` KNaR`6A<E +DGCD4F,>.=iD<')mGSGs%FKsvHE@h D #*T' Hp  (( P)4L@ VC6! 4!]FF4@OOGKőtHT G=3GR9I9#l!! Js"3>ncpks0 (MpHYAH-`zN>uY5U}4u'"u03>Sɵ[S5"j 5'p Y0 ǰ VA l#B&pP' xEX lڀP ΃Ec+:cCv_]=0uH35^8eO=7\1eAs]}]{)X b0_Pd!Q)U !61:CQpbU 8dAV_0$7vUxfF4=pRsf{g!U`nSg9(uejU(w I `#bqkXJA a(}ucwvkPpŕ[5oyp[3\}5 INA% A*KQ9 ` iRtMdbyEp(V tO`>jUY uOuPFoǕx 7fMSwvy#Mnqb?2!c"r" `'`2(#>B4Df (2D-kBEqɳx )S7oX>!X]O5M&W,Jo@9,'ETΌξ#N(RoO'q2}$i:]mV6aS8\xwy]qG ِ[!8*P.*+{'N@ 37R6+GY3;PR;]##.`Y}9uƲ,2;ĔI4Ř݅Rwfo9-^y)]vY^⎗UqyGyɹ3n1Sy99}9Q;e%yy5 7%ə!YDZ-^ 'ˇں}Y="lǠ;;`ծT ŀ83,EfXcQo%+>=^M^E n4aH@@D]f&EIt*R>sZOƽrxǜ յh~!aBPt~~U5LDIIyϯܴ ~/;){SӜȶu@ h> " ZYuh1SCuTKս:M;AzQ}@*#Ց"Hc=6 ` PmacM  @}劘` 'F`Ƌ;zhȎ"+X"ǔ,7ʏ3A)ŖW 4СD=4ҥL:} 5ԩF W.S@BO Yޑp 3YZnB<4baɒ;q| ϔ'%7 9gH/frO>:լ[~ h'j+LRvbJTDb"(i"`.b#blCNrK>Eg < ?~N{<~=GtZ` .` (.¬o ,ROE|UdI?a015|P>'x!p)B{4_Y$ I dKIP`Zne^~yT\MS Ma60pG53G!lӍ&`@i'wXçtd'7e'L.IbGZ%9]Ӏa fj& ZAE@ " ;lqJjcE)L4mj~RmHtYj obP&EevО[#{SFy.e/ E*Q26i{Fe,o rȧE^b4)D. s2Ϭ`'t'Ls> tG,tFtJ/L? uROM5NWuZou;ubMvAmvjv`7vߍwJwN@"+T*x?9Wx㳖KwᢏNz|B:#?:=CN˾:؎A街|ʓ}zJDG #\}?smͧ{?? Qybo lWnu(G x׻ A':qw,BT9p,l X A 8aЂܠ=NA(< t,$t(32b@],#|JRY-d@Y-ql$R>F( (.Gjr >Ƀ,%T* e,ʐL Xx{,o\˖/ ^s$f q˒+3`i0l 3Rő 0)U tfKlgǮPQIy |a|J; /5aLҵҁ0-Ud@ Q 4\ 5h<92W؂dD? Ғ\2JGx~ t(I_Q30tDefr -PA?uTROJi*AZudL+%7l3ˌXiut-IJjҕ"x0BWZ-lkL&E`wg0DNc0`a yga7vesWww7]z. O=9`q䓌~+Ixб].pBP yL@iYh^}Lz7^u^*6 P5_Y)v6{wAA|wʷ!' ,R,L9wRYfWK}'~_*x 'V^RU  6dY[[CYe9vȊbR& :l?!ib4dǧJjzER^ |]B|@ @691Y ȩ早ꨡj%NP ̗Jb9&1oFfɴ40=pi@&7Jy zڭԭ;=zpQ0Dkک5j}תJ `r@KcЙ{{IH@NذcR\_׫: S0k0++SCЇ$k(,02@q y@ FT/P(H䴏)N @ "ZڱH8 K&JDI`B@%o s^Kںbb{+^ru}۸c%[pҹ +K!p Ƹ m{ p ; "|ۺKo+˼ ;ԋɋ"۶؛ދ ۻkKBk۾Ak ۾ҋVۿv Rl*,Ll !,#L%l')+-/ 1,3L5l79;=? A,CLElGIKMO3$Ż&DU,mW;Yū_lacLƹfh36wk8pr<6P"l-{Lc@FsVWȆLc v: 2&l"V<*#>$$mFȡ1&{ <F-D˹Ld8<`! ,#! ,#! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,z5H)\"dH!āRdhbČ66({8H(_NjӥH @x !C *(P67_=enư0HC 95Y: C'\XK^0ЋFR˶ˮx\ІYyMZm'˘l/0e`0A_%_!8ps 778@)PӻwR~+L )  ˵s/t@! ,#! ,#! ,"z@ A*TAA.l8AZ?9zLrcG4@B&NJLgzQKd/̌\^Ď2ii 0JҫYI7l/0ՋkϤ%_LPJo^=pRMB0$W z bـ! ,#! ,+zhH࿃BXa‡ :|ā+^$`č:[X $B$HBH1b-[)^BG3F)DEaB;K:ؘ0aR>0P;+1@òl:CsJ -`I:!4,4o D!A:^ lƏ_! ,#! ,#! ,#! ,6zT5(¿b H`„ |QD#JtQ#C1tdž3ZP"ɔNlp%͌!^ %L tvN! ,>z5H!\C :D1bÉ+FĘѢ@ l AB PRcSv1:LLF ! ,tz5H!\C :D1bÉ+FĘѢ@ t萠"T…%rG!0(xr$ji{ [+97xtN ֮pb $,eoX#0;KWO۟-W60 Aszhwqˀ! ,~z5(¿b H`„ |QD1&4 bİ@P|\KCb(P@+)bǜ1tyЊDjfѣ $ LJų/М7YR*]>Saj@A"Ҵ(_Du1! ,#! ,z5HP@ǰaC #&DСC NԲ}`.b#NJ-PxXŪQ!)n0j^77&V$Dt\lXpM4@P`„&Ā!c.{rh@! ,#! ,z5(¿b H`„ |QD#JtQ#C1Pؠ@*T|ÉGWd%xz  '$fV:TZZPO$FPBC HȰ'Iƥ{}qg_ T\gVhpR)Z b׀! ,#! ,#! ,z5H!\C :D1bÉ+FĘѢ@ &h!BC\A%G#|+[bx V9Vn؀wȰzӃNbm ' "Ab@-7'pGbXS5IR`$HHNjs ! ,#! ,#! ,#! ,z5(¿b H`„ |QD#JtQ#C1$hB RbXš)iCH|LLM3Aƾ8?NN+`XÄ 4GҌk4+ SU T#@Oׅhʰfa7Q4$߸! ,#! ,$H*t;Ë}‡r,"^4 Tx:J2(ܽauA]/`;@#P`Eݗ~ٵ{ygh' 0((]hbA e0ÜA, 05X pBCydK6x;bB;Xwm 2| $@ 4rQ @|! HB x5P B!XAۊ:j} %? AvHЈg 4P͂ 7 'yg**b#94xⵣ޴ւ-- m754p8ơ~hN=ݼAih=L);␁MN,ADh>8BZ :,C 4i|c{ 讇zgے[ݎ.qtK XgгI[ԃ\XrH`G=pAF3pNd )olœ_ 1k{7v0;Î˟Hq~x,.AC׮ⷬ+u QtTWbl1l#F\2 pO" i^#E)xx={ R@S:츜A JA΂'?]nW;v| 4ԥF NwS(EU@8[6IxqC;N11 R@pupg|hDE  p syhҔ"Vjek +8S-5`ESQT{Jt2Qqa$ X>ہ"- l7U"nϸ|I ܳfՇoP`lA t#1#xWңUT'Eibi ue^n PG-jSH@䳁 DP(G(+8A> /%Q3L f!lei.c+{[w_^ͱk`%{=A ܰ`08H`G8/X^7N Ald$N9Ѳb&MQ49G; Codr|&c/Ȁa*q[LF;,d!$ "(Ck"ga4GMRkL/TC̦g-ָεw^n4t`Np 68a5\t@ vQ FBn{B?tqL`4.pAh_ VkoQ&I %08W>zF W|U,"D8EIn08<.HhYe1X6>F9л GYUxKRy#4`mb |h|#E^鬙<'I{O)d)<+WPk8D-@ ̀C/R{w}&AzȃhE9%xK'R"3|'GؗHU#S@=P(,`VM'N=P pG0M"Q~R$6xwW` ` Փ / g}0 GCXGx{!=ƀ˰68O, ~928F8Xvmp Bx ] @Io(@)#y s@c2`z^wXk!9gCP)%  PD܅% F1 h ,J <0u-Q= 2)a_)h6 )ub_űpxT )T!Y:X&y0kڡ;'d(94zB*!"csd%M8Y@].YX'FlMBL'Y-!E^Uo!.bє\Y7U0 'gkٕp)_fHchwcymVmO4EC0 o7nN/` < #!1yimQW @6d4I y蘐 >}ifuXwo i@1Pn6^ pwPvל#ˉ0@ 癞ٞIaة4陻d(pV rp5 p wFp_zB!7r%|%_2v,&0{(I)*!3 0 ("+2) 8kPr  * 3`Ѣ/j)2i ( @GI7`kڦoJ D*qA)UzY ꤛbqFh'f U* gO?ڨyJ_'Z]nP#0 0 ][p{00 xPۘ hp 8"Y[u~ UtBl0nN8@Ӛ#~P ` E6o:x `YXp *C*vغ抮:J&@|*@0s[ `-p:q`+HLQ J p) Nh *ys`#ЮN 00 :5Xʕ PN6`0]{  0yS@Op q#PSa%S0OI~pwyy c0 fs8 $2  p ۠,pؐp < o \ ڐNC0̀ jR pLZ \  PK˛s0B+x !kQ p p! n* ذ < #C\Guk7:-Z;0 퐸0 0  C`C:LN e"qP=`73># t23[ц$!ou< ` @8"xy0  }bxN,bQ ̀)`ǀODBpE '`bO@) a`S p L< d0)˱0%aLAC5 P`;P?` M֌1tBihPG(+ $m  ͙0Kp `b\]ICћCADGDC`!5 0dۀ~| Z] PP GBPFqGP Tpxo<J9`^`]p@v؊SPp=w vˠM-7 vlٜBٖo$^ Pm݀@@۶PNH~*>\" {P`C ex-0.q= @p 9 Zf#͍֜lb d>C# _.ă@ݧ|CڒN,m~ .mͰ\0_\P>pyYd0nؠ s6`~N'JPP 0wO+{RlP v``,GJ@$ rP ~Ug]_.GS:x1^0n f̜u_;")58gm1' ؐ /P `-#AiX)NH Ҟh QH跁 O5=P"xCjC ah/-LV̽ߘf?M#-o 0 `)0# Q1ӭ 6P?_8CٮV@9`)06tTeUX%!=p S){_pwpNj0|YPpNC` e0n]oP?A0 Nf 0{FĀHV"l-K(Dž x@ U ˥mܐutl!!}f0` ߾n݊ >p0_tuP`P?Q0L\D~劘8(l" ~kaE|5pP _NvW BzVq@̙5w&lqN 52DLJ@!IhCrL`@2 -nWɃw@ށ)DōG\r͝?]tխ_Ǟ]vݝkP0!8&\"ÅGm*AtVHg4иſq 5C 'SX^vLT95:谷w``|G@<avH F#$P7rP bw3@md~ 釋8+"tQE 6!k<%}2 0w)*SL1n2!K3E+R &|Daa8(Xt &^}6.٨%y  q0F̓J)լrݑ*!I3YS3@7_}_8 LTP & $H!R@il x88!` BHb7X6C)bDPaX@p:UH e8b*6!p8 _)n%:U8l?9P~@hݶm@- e=l%l18]rdlv̍ nBƻTg=@M0?)/:W|*>EX`駧z>>25èJNf ̟} '\0E8t刅Q =|+( F%|2Ca UBЅ-Tw7C Hg1y@?~e(j sA0 dgKlqVъWbEm9^jؽxb2P1∜2\cG>gH?ҐDd"HFc FV8 XHL>@$ RbIRҔ`MreIX=!җģ 9C92pMP3 5c T&d"*7O9 C| ؘ3/NQ Jn;0">8hO& Lh?i.Ӥ'1CbP< 215w#q6NcE1? y%P V&~3ODhw l")@$r0$ED&"^Hw\b(w׿8c-kGQuOAid%; )c)X$ :5&'?֔5M@C "`Q` jG̨"؍2WGl67b-CvYXh&EkZo3nr9֬n(:56mc%rBk V*a0LM0Nd A 6c Of@5H$YNڂ_k'Ɓ4R`RZ,Ow{H lo^f}i>֬y]AkpKp 0n>Ik6~Nn0a@6# pR10{08%qkwRKθ&J?2+1@DғA |@2DZ U<\) Az0H=zPj"Hq xG+5;TʘfhHK)8UGoFtӀ;fAb)5Lp(mmlw` ditGzNJ(Z)dPi L@6XC(p\aXnKi"h>*!l0|0|ԛBՍ?| Rz/DP{B*<}ApO<tk|](>acl(>T|y} DlHM4LRCߐ, N~)A8S a #mXEo S$vc$A Qz]0ءtaO Dav8"WMdArsXш T@hE ?DAK89@T0: H8d0@T@` @rJ [Ѓ)'Ih8Q 8rP?U8q0 }3H<0>v #B ؁5BXB8cd8F(Ef=)AzkO02WK *؉_`i4'_8dmp/H\p)H_pTP 54 1$C3T㰻.v hCLĚu[ȅ\0 YĻBTY[8OQDO52gZ8 5Fb +)0?W؀ Bi;E*H ᠡxA]h ؇8Q" 0<4% |Xk0h=udGPc(^ `GzpcQ0m `& ! IixX{HHC0mʡI*4ATX"}qz|.Q 808XK‹cP;y89G$4h=ȁi`(p(3L ( }~P08lM00z~(?h{`8F|PX&` 0{҂Gȇ5(M`O!vȀ "Ȝʔc\<9Lfl&r؁$#A@Mی30ہ h A9KAx " U(Gt~cW،<M.8mxdV( !h)/ʉQ/~ ŃL hf(tQ5zr@$؇4I ;'E!pPP҈864x 5(ppnƄwb؇_>](I6U@Q0T<]YA%YYяACP[Z @m88Ib]`6ɒdH`P恑*ȃJA(BH Jw45[H2L8!v/8G5;eR18O0hKQ9@8%dB$03eʕ T%ס(W [ͧ$݇&8,\=x4~ױS~M` ${%@:(UbhayjdBdE1\f~t H4]M m؆"Wu=hR8 0A]e#VX6(}v!>X,(6?f@@$;؃/x/[]aex.|і :S1퐅i&ik!"H} 5B5}؄.xvh[(1إ :hL*his"^@h|bE m\Aj&>~҇RȂ.?[B86 /MCL{iܾ丮hhf]097 {8F`pK oh Ⱥ^|PDeГefhe.i :Q芯c{m"ll g2kh()X\LClE/nPvXD*gnChgm~h !D l~p,"Am6z6BۃC#LY (Nxnӆ?H N5)iRH'4MXm` L^ e𶡊p 7d 9.s`sжkXTW AqxP<'_ѪD9z(`aNrz8@{&Е(*H8p po.` a0-ᒉ8_$yO *ij{8|Zۃ !?PFy(w0پ5x1$QrYѓM X?uCK}\ `ML Z\Y7! u)yjWko|腠G @hP`siZ-azL3 _B Z똨!X .v?3j oB:./) xv4z!8Wv1p~H/ Q#sytz7->B3qruq(ւ>3ߌ{okn8 {7Y͹j.;YkWߙ=ˮ9atǞ X` 娪"$UPt1Xd~٤-xkͨ #O ¬#R 7A^ ,HBH(B QÌ7r#Ȑ"G,i$ʔ*MVU'5Фf/cI2 zs&LmA-~i:`tZpBZXTUĪ+ٴjײm-ܸrp!ݼ'/4tШޅR Ċ3n1䁀#Sl2̚7s٠A+״ci G)}5زgӖS^g8\.n8I 4ԪQJݓB 2dHatKzc I3 @@$ A i2fx `s3UZ <]w&PA0,)0 *r!8"5WQ x`x8U&鼣X#QM=ݼX K:( ^|Kt#?0‚LiaډБt@ dAKʸacF ?ȃ8Eg=դ% :(ʱxhu% jA Ty )@@ *Ĵ tSuip5in@h` @~aD-uԓ13UG??rԁt,;̢mVd4  4!L. A ڨ fD& tWxb,zPD=xnu O<*/5S\(9o6eo3PA D֣ ) 8E*paN0E>xB DC[oBA ׌l]+M@-;~X&,J>M\e0 TX2@ ,B:O6(atO),]`q@8-K?x2 09G`)*p#»#/*4>F ?g6tS@  mQp@tQOf1_a 2L! "elS4u a` MFЅ/@EMd @DP@pA_*Pl#/4 |G# 0}XaU(zw<|ȇB)"!x`YIc4Kj ZԁaE8`y5/䑍#px*\%/8&d (#Љ}XaD=06kp@R0NEj@ &q&TPr5` ۰ rS^&8߼d0 0h`A H FkءDh]") DyF`;9|@+h+\XP@C hР5hЂT2 `xŷsia򨒨$`{x`',2QXb!쒙d p d8 7|Z"/ mC 51׎kA2(`a @iEd;ѷ 'hCK"%0 qHG5!YHsюM*B!d`_C&NI ?y af! *Q6p` h*, DВwhG;&@Ǟ ((1 <@Kz0ZLf ~آJCI)p(WV5@V ` 8€#h(m*U 4TE!"+D? T62e`BR:` d4|NA5.:Gh  *]d0r)d!h. x !|+ p~@5H]? ؐBa&PplC`p~$N z& "- DBLzk&݇xc ;'Vc `$P 6c;zc@)LqjLC"A31Qlb XA6`~76ؓ -rPp Ѱw0.E0v1\*[{y;?%+b+X: AUsy$PqW1er PI.TW < nއח!vx$_F5 3ty86g|XBʡ mBp' [:4X7p"ADthDD耏Sa vpa=8L? _a*/P-'\prt/xL]޵>pUi ,C4+C]C;Z%} >,`;5`j8q˝XC0ain6l ;%Cې)BD5H_/!^uPu?h,CUD@3QiD uA'P E$6FLK6, IP@ h5PEd/X?CnM0(6A P B:C.` t`;D1! Ӵ@hl h5#MЋA t >B:@'hT-`M*Ds.m2 )+@@7hjPI@xDC|`#DFOMPh"O@t8MU^\|\(<G'fdž@@ <4@RP ,@tHL gLHX=āXDJMyH@fḣv 4e{}L6ilX!pDȄPy% Dh hʣ|WrwT @%mAȘ Cp@,y8K[fG4х@4%1_H@bOCЇkBl'N*PZaA$+,>C ^o#&D]A'#.n1 h-eyDUt@jČ֨oBLoǃ:_._&NL2 |&6'Y,!C;(hDGU_ >}|Yp)?]~PQ)[\1.)J'YdHjMpxdxbfMO5S ߲ :NB#t0o s 'M#/53C4VM,s~eLk6&+G`v2W0uN"GSNp]Mk3ӺpT2_;2T챪΄ATE.,>iuifF[u/|Ro kk*o/6BFH[C:lov5] r|B7p_wg`t2s[79[xZ oDžxc7zjy7{w{÷{7}St~wfL[sZM5\7CH >}6078 aG \n5t%A+۾LXf IG:A@h }eCt4Ɉ; Nu o8p G @Ky_yxH4Cd=Bmq/ _` ɣ_V:ѵlczhs=m>C@.Ļ=1 +0;0K0[ `kk50 S3X=Xq3@Xs!}XRyjA*v 4;3DӛLq_qo={1q1 q$=0Aڃ`?qA _Np6G3\1C Y$@% ʏ&w'()Q%*w+ò,rj=@ '%±R/,ElmH8J.aĉCh9dI%TdK/5xJ2Q<fO7i2'ϑA[UiSOF:jUWfպkW_rEp)Ht`J?ipy3ak+_Ξ/O4#<>KS!ٓrCHIIxmꢩ_u!3Sgaly\|(3TjNϕ7'(u.f׾{w?^{^%U=Lu6MpQB@Ʉ(Aty?(\b ||;G7R ;?L#n0A`1ΌC򺜒Jn("kc9[RI#$O-/ K8hD!PTJThDX7)@S8(H^bNDғ 6T RH!8uS3 JP!3 RJ\rJvz5*.Hh-(WSa-cJSL  )% -ŃRgzVpV:t.e)Mw\|Xd?ke͗ V6b|'1X9&VUu_BW*XYnaYiqYy矁Z衉.裑NZ饙n駡Zꩩ꫷UVk[c-l&[nE[ݮ6:\5&F<oEJfM\81\s+w \IoKO]խVY]ً]yrC]Sx />-~M╇ǓZGfޱ^6!y9{ѯ|ۗGwߔ..' O؁ 8&PLiv1xYBT"v/vt aCΐ5 qC=; \$ <`N#8) tYPɕ0A #+kJA46 p pRppzhCՠ+*C .p;>nOe1-` Bu1buQ- &@hF >b;`qlB76nԒ(:]Nt&(ЃGX L-Y%$҈yA b#`Da3XI0`'<"D̞<A0 6M͘2* jG @A 8B Qt6N}&P*= PS @7ftkI tPHK UԥI&QFUS+! Аv" !0AlbJ*@&g"obΌ0Ѓ/|I9#-'!1qo|s9| ^>@rړdɽ)8/MtPX+9#=ȂEwsOA<0QKzӚNӼa+liQd,FxDNv O^=`MMo3kr{S*0Z wv^ozfOI 5CB*Pm:ڙWWޛ"WhHʥ/qln{ώ7_w}O_!`1VS)`wl3,J!ny{/Q޷Z>°f<'W.2@+.`pF5l+!()/P+0M+@N9dN8,+$yp8~We\0b>DvpeD=Ȁ g 0 }\ o0 aF հ [ 09] B=S(OPՍP@b c1eL*^ШSpa #.a{FhHeȴ - ?2pPg&(* |l b!!NL(` N`. `[Nb0УP)\  )-"-"523r#=r;:#E+x)~zyGh$-2${'~'~Ƨ]  ȁ hj'*6(HȄl"")P)u(F%* R8.8@''R=Nc:l=/o,ͰV9a %/.p `! 0%<POnn1%/S y 482ȏ2IF Z*~ > yQQh+7vbkPY`6A/n7[)8ǜI! 641-vr`zPJ J/j%bO.#p<s1sy@=ӪRLj%%7p.,Q7t%K!#$J!=x3B_P 4gi5l7S̀X\DypoqDRdEZD5cdF2 ZpPuDѰ&` Ѿ4S&2E%%4DeIbQ*6 ~^aGJ"';g\؛3>'$%OG>KS~Q;|~A9 m{x>L~>~>I^3릞/]Ջ=_\^꿞^׊D>OLﻄ<i<9^DAMQ @KV_*)Jć>cbpLS-ta!;*\8)@pbpY*@6`P, Ȥ6 䃖R B:Р$Hq0!D0 knDPѡC>Kx`8VN¼B >AA)2PqB "(U0MyÅ!L\ 6رd˚=6ڵlۺ} 7ܹtڽ7޽|zK ,WObXO #V8F7T,Be b #Z&MƏ#O5l?毟2%& k_:ȕ?BPidq 0A=Eqed/I.y=\#4{K/k.R )D ZЌ9Raƒ`m0S jI5VcmLM)>H*9@Oê(ցv-7p? D ;` 7X Ȏ7/ ^C16{pK:hBlxG>A (BHAO|ZG34‚ _M Yl\u`sd*-.0Iط_CA t~PwS Nv?*, ]p_AWȆ#?CP8@U@0@$p4 %t+:S𨗍4P<0 XZIX2aJLF}`~1*$x@J@pb`|xAַ Ak( P i1N$h&m `e"E0P P؂ @.gIQ$9Wm`"7-o\8TR$@At"DIp {i#ǁ-`_3?5]|f4Øa_x1zNJe 58@ 69l ,؀8 .HQFA!xP= 7>1u Am ԥO Ԡ BXVBy 0PNR4` uЉy azaYؤ`F ӍLR* V{@?]8L>L15h(:O-A*c r+ USН;X6QQD!\KT*pX Es w Բa(7R.!)@<4@*b@)'" <G@Fy%#TE'1HvĹ8dщ␊]@(D p@1$/{# F-? 2DqB3 w|0 ig8Ɔn thxHb 7]' F 0c `9& }H6*px AJ0zPSkB`\@$@"Be@ _P c0א't '0M z`a2&er& C` x}g6Z shxB~Oi0 P2b ɐ )9Ȓ-K\Gq3 8KZHH6<2-CIq@7k;r]@ v/+c>1-0 /JSA[P 1cd Mp/L@M[p lQ^pR`ej?@ ? \ cR 접@ GZ9yIl.)iK?P ~ >jh+k, !@ s / ȰY*0W/c~EUoUYYXޕ \5|W8Cx/cxX! p0 :\}WX!ёVٻLPG90nP7wp} @ `` ԀŠk,|,/ 8 yP^ `yn9paq7 ƣ AX= `߸D1; |'_$`F|7D'/UVLzԦ c87E}lCp"`*// phvz|ǀÕlzgApF+,JDzĠ S8T)A^[gf)P"{h*3,|,̲֐0)!1}0/Rỏ8l,✨g6n*)CTe1\̾8   M+3M2I=:q-O]`!R N0][-ZeBm^SgL-\aeS r]0o d=y}jlU{mm5Md}w-ЉM؃m~M T-|ِ՜-P]׈}cٙؕH[-c؜cG؍ձڒ]ڧڿmɫ۴-؅=ټmیܝ=گM@ ×ˍ/]Νܸݦ]M*k޻mn=m]}>zn])`۳ܮm ޢmM_ EM߭Β T}ݬvQ؍m޴->A-5~ .4n?>t! ,#! ,#! ,#! ,z q5H "paB.@ x>G)>HЀƣ|2Q)41f<5ba+ÂDn"ӈ!8|ȡD!! ,#! ,#! ,#! ,#! ,#! ,#! ,z(¿b H`„ |QD#JtQ#C14h၄ )L|(2Q1Kd/04a*4.a6_MDJeVա^4`K)ӡu/ϯͫP !ABgjīpL! ,z5H!\C :D1bÉ!a1fHJ?]CPXZh#E!r (cX-ѢeyQI-,W' i9:$h D! ,#! ,6z5H)\"dH!āRdhbČ668!puP俎'L.,C#X K6xP5NF0%R$L6ìSZE vN!|r0 $Hyz3G >`(;S)"5ؠ*? R1s! ,Hz5H)\"dH!āRdhbČ66pƮR< *؀P? @1$ @ A 6!fPX ?毟2%HYBi)5/pXHJR@%/x8TJ HH*-ܸ+n"- +0-ÚgH^ ÃxAkŃl%󦳝=_xiVbh {G/5P@6( y= Wi Cj ! ,#! ,YzhHp & 2t0Ć!Z7Q#Tr܈ kzl`Р" p\s#B.@֮g=|p^H3B`@# \4ӓD Jc$Ɏ vet"|^ d-G44_>Sv1:LLF ! ,#! ,#! ,#! ,#! ,#! ,$H*<.Pbň'C CIɓ(S\ɲ˗0cʜI͛8sD=fSGH*]ʴӧPJJţNW?@ Uc56۷pʝKnZXz5:׽DV0vO68ǐ#KL'^ECj 5v `c,~i @.Sn-m$`@H]=j \_8*6N:viƍpwM:_ͣYX| 6`>W 6Zl5 I?pI\ 6q@ ܁/hx`0 -KȢ6 g!f 'D B( &P B @*P\NB *(TANJ 5B \Ѐn*E $C䀁igm#?(akg I*tB IeiZp34sQ|˜eTJJ&a( 0@(@Fjxu2s]Jzz@Jjk,Zڋjh6@@J*,ԫZ҂)qЯ E:n{X#h=x%]ơ'C@>`Zw-qGt@aM=aZc9 dL0r-ƿ+gX!d Xn뱇;BYӍ.r3D;~ 혃 F79SF4Qp9\s8qq.b |Ap BbA 4<؃O` S$ tͰ0' lG"}4$!Fd/0P0 9@~ mRE;8,6ڱG2 %/h!r&#/R AcF>~@=T`rZc)HARKc!.@lM{@$dD!؀)Q3ʸ51\]d(G9G; DN=}3qpE n5Ȇ~Āxr}4F3BmT_ ZXrf(LgZDG>P0Aa 4yH3lIg0 b3064*QigP 4Ā0f`"Ha& Z ?b(`XHRhX&t0 D5q()OP ldtS! <)e09M h@P*d. p%V@j{\:*;`AiOZsV@@aa m 7 =VЌ}m{KhG+(7#(Fk\6e0~l ߬MGLH"?1\ !@ aE0AOoThcKHJpq"ЌUDGQ"`/lu+G WE+5?<УrF~^3 }A5]>ҐlD^&l# mݡvPDrPg9& 4:@`u 3TcpƖA:om Ȇl@ Q  AwGۂm+tn1~+mj3>qU3ЏPx6AnAހl "n.ݎLoڹ&N^$e!Џ:oBr>cdTmHHR:`  > A3'&W89i^sgR\f ~8*ǟDc o>?Q-]-@λ-IGw !|43 h3`ۮ@RVGΞj=3P l|a oHAlqvm嗫߳-P `h".K`|Ep s XFނ6@ mɀ &w۝ Ͼ*2r@PBq j׀HnCʲDFC3Ge6'tyg|q6e/`F%IfR: ) a|U `X{c\Is T2/';R2' +p`gojyn"7\ŷ TgR)vA! b%V4=K\ - {Mpօ];]:h*0 `]Tp-1_eu_JȄ`tH+ 0 `G lS T ,#jNlÃRw1dSW 60t0c`N <8 0 /'{_pw"!0|YP- B`Pc XxctjF[Q 7xxpHdP<o,P7]f$%ܔJ0p5 x'gQDwD e(b @(u؏&EeB@CM0H@/ P uCYC$H$;^bI _{N[d[ !9%mk+S ;m8s&hqzywcyk;}C7q"U!ǹ A[{+'pB;A%P2лg{Is< S)$qͻr! kv D$r0"%::*(f]"#qF@9  'm-^ 7$T%qwO҆86$# ̽._Wy! {7Ɛ `̃0 05ah1~`;.` 67RX;&$0 ?مt,k_#K{3p 1u|1 Qf L†lk&H.hbsk&f)X):%e"&k&Ⱥ"*Ғ),(JzO yb&@ˌkү!b*v;A/tp P SpAW< Xx, O0d8p _`%*R_aNdJ,)pG-3)'*12' kr."3,P' #** SE6)!(*ɨrӁg 37B&tуz/l4H4LBp33s(`}ʠQQC- 5i823X 00#3%d4p `6[ :7vP07bd-)k5w5 N p6n𪊓4"v^c & E]- t6O50510 r Dm PT&6`hA + i_7h9j-393X<<M::}CG<Ko} h}`SS80;`pOcJ}8>7}3kΜ>?81F=70 ~ C ᒂ)AXSA ,ղDP$ETGs4p ~y JV1]Pf67Eckb7Co8tW vppL0dQU& Y %U~YEa4FePcf%80p 6'prg6u>Bɰ`<D6y EZE^T0 'DV>P &p6 ~DtMB(a`& }\`A`@`;з$JuODOMM!KKCU pQӄjUb `@oҐ `I N p9E ^l$>O٦%HH<  m: H(1_OMDFQRPG;UUuUMIfP!. f)@w ųqxTC{umf>v` xdt^ y Ādo `%VdeV'@)@ö$ƍ(3~vMt?x> ~U"XUP?% @`lm X9gR70oݘ*-oXcg^ep<נ! 1E=03òrJL=&O "DT  0AGQBd!Èɰ-0-\N!^ B{L B K-1G( 6`MrU"Ej };6дY C-_ƜYfΝ=ZhҥMNPtI1!w$[4iA„ʼnowȰ1>9, _?8q aڷshG6̊-߂3kꩧ;|A}Xoc6Yâذ,&Q, p!}( ذCs3OQ1۪.Hp} K &0|aRM;kYICD[o"ݐ]L<-_}_8 H:pN66,Фc2θ <::C:h"Sei ~ dkxó Ob-v:"P y'KB.A1 i|ư1%fD͔^ fJB ZHahæR0 A 1 xtl?N@X0j= -TmbF`FW͘0"0XдO;{ m V ;#oH]}u`y8"p}d ~!vG PV AE懕 (.RԊ|W @~oaE8B_E)^0)bh HpK} Xq A~Yɪs `PA 'c@ .q]/ kC XY y?A? R ! ", A! `l"2+2hrb"H0+ҷI2 rOLzɰ#{ A l%ˉ3v"Bj^3 }QZ @O& q; 083FGL 3hK$@GD `0Jy jV: D\ -X;l(>f`ric?DN@Yb]hmAVHG*/3`cPoCBy4˃&jT:UˑPʆ ${ 8|b2:1u&0:sP ^ ` zIbÊuă>6х6gMZ=!}آ c&0:#_B3 "]b#³aF8t-Aֲ|>J.[L!PP5($Jk^A{X2^z妳 E'\j:CcxyN1" .xО hD<6` Z?PXt,ht|6 z G4у"O%ѧ\ |1"v`BƇA'&w 3 TchG?Ex5+0Kj=hBKH?<nCG z[RݡG&j%;Qm!: z 4 R7dbŌuչ@=d#TXGe A6vkCoh,iMTh?4`ՈRAڞ`/{h=8*v 1\3C[5zb Dq= xȀ. ai>0vʧC=R Ba\PXlBڻ<~a ZЀ;d" 0D>igK D!P1DKC;ǁ _`~?q1bBoWsslF&R ,!q >pw#sl" $HǷ#|&``4=s90F =|@@  <2`ԆӝI"4`'*ޔǟG&1|/('>>c>p>h ? ?Hp y40l D0> TP *Y> `jH{ \@̘;С ._ d>3B؀KAp #k>!Llpى(xT \<4@ X(*dKD&T{; !D2@\:h7|99l?Q? h3[ @9Z#ЀZ_`܌Q Y hĊ i$ `dĊ-_0)Z< h=v\iѸjqc 8~lpgIA0'6Q]M 5S-ː$U "PUU]էPTڪJY1I=ȕ()H5#c)/H@})؆$/N}؄,p;@9mI|m|dH90}Mm'r\5y,%q$~Hds &Zz_ @ jˆ#f=sJQ[Rx$}zk-K89@T0 Xs`NpkL۱DPuJ~PMb 8QWXA HTB;h)b{h;04L8̄`(ҁk,Po0}0B45|h:jh HR?ݘNX6<=x7pp K1)aQfdHc`i}Z/lxX{HDE*݊INNmk0X)`/WX.UZ>>Y&#Hh8N Gv($eKh砜ёrx%m^1D(@7[ePI[mb}Hs 0xpU[Zc:4ROM)vyS(HiAZ _m YH*=N 0{.˄J PcFjB+j]~& A{_t$_`{!əpfnɂ|`f؅e@B 8p|:(Z4g7HiHN l +d,eX!h0"eH(2<_mE褀 ޡqPa0]j-MnZ!!b)P`1"HP"D:&j vC" a . ~覢kzl؅M@0~(2؆!p9W[ ܐ~g b!l*gҀX88«wz1. ;U*d,Ҽ@Bb@dG>Xp 6,LRr4w[lf:^G7m֣ڰ60i v; qIžŒBѐS)\ |A> &'} `=fAH1"C H`HG58`` HoW($keʛa̗8z^b<@M 8F+x`]@4n DpF !݌GM9vx"]˼M-.m =\Ґ\pA\` B`=| ^$pya Y(":|`ְ 9Q.SĨȠCV/m=qLiEd|fVַ'Eu0r 5 =( #,btV9p`xp!ZHzG m!kjTW;NvZJ ,Bظ`+!Օ <XS4ODzbK}kQj-(:W=zg߷+`6e2joGfRqJ l-CGDʹm}"bU8J*b)1FvX)2d,eo5!!`Fcg&<-sKVʑLep)CZ2t.nF,9yBks=j@ІИ*xfpFլ4dQڐt1wAnp-uSj Q&`d0bE)D! v0l kw S8 *ή T@%XJ\]giBp5mfjk~f<'%փM; 4g=ьL $miM?65Y kZM; irmC pwjC OᏠFO3y ؘ |mn[d= ]3?hQd$q$G9  йυnt;]ʽ.vrm@>lAlG,2 #A}9>5{xq,J~~sq pdN xEWo_C@"P t %HA 6 у,MzA^_6|@_זT?w^'g?=/ηhG9`n'< _5EUeui@Q`)YA 4{K0 d>A'%Q  ܧ=n5~9I4)b=()*A*+,-@.R/E$U \{  ^@ 'i֖ _V3!7 !Zd$\1ԃ18`:qTГ=>?@A!B1CATE =H11ďkd5@0ȭ\c]D7 D!84QS ZJZ5E䭥\6$]LFxLCT P8Txx^yz{ $[^|ĴD}v@x$VeZFunE6Z]Q#0F}u؇Vc\cئ ]Xq[J'Rg售)J@.?ȃ?Bҩi)Gi6"ZL*>HjNfbjn**AJNr>*cZ qxu 9qk*檮**+b8S_fJD۴ۂ@LD[ ګ 抖u< b-`&T=b˕M͹f[s8sɧ&絚@B?Zb$_>!,;$A^D]5ClN]-] lI#XC5 f.3(,Ajdѱ)ʱ"]8r #G#W#"w2ѵ2tt(2yk)鮲(Wx- 챜C9@ ?DV.3/.˩03/'߲3 Hb <5[A n3 d6w7@8?5=3<@ڇx@I;A> sr0>p< *ڶ*b BAG4턚@-ϊmDoD Zr4HGkpHt mPݠjvIK4LǴL]@9DϴNY>NpL F.t/0PGulAF/03TKWEz1FԀWUc-Z5hlIfbDN5`7G0^[ 8nW6cC!uLec-56f۩J٠c,"g6iOG ^ivqvhvY\önm!'n ܃ sq?tO7uWu_7vp!]v7ENtyc Bn7{ ;wcL>A"w`w\W%'\\.bNw_ 8~C8`\{Hvf<A_ S?lxLYIq^A(8LI0䀏' 0!(GpJ_9go94K[(@Fxbyš B3qlww{|G\!@-O: 87<Г4=߻pF\fU@/S¬_Dzs봕p $;̡lTxz]{A|ڏznoB.Ev9 }75 uz_STw{6&ȈP- m PM(8Lk'wa!뫁'`%ZL~#?` #n<%f^B)>\l??I,BDӇpxtq6ȺF00?2D%@N5tpȰ ~aB,cGA9dI'QTeK/aƔ9fM7qԹgO Ya–vl`ႂ2HATȰ}X:Ld8(}Um[oƕ;n]u a!}Štͱ2̨Pje,WN!br˞!lgє3Pnkׯaǖ=vm25ț;$``]rÆ-ZL~ fl7]?yNI^֭c^^w{nL`~~}׿6P%/%h8 'ltA%0B )0C /tP QI,QcE ƎFn(kR,#LR%ētTHD (Hn>&R- <'MZ/8? "S9؇R2lfAe[@|H:2jlf!IOA *!䅭Pڀ =D  @}12"_Uem ECI (8Ql f)x@[AL@ &, ]wW}k_G :$*@k@Oxp5 |825ߓQNٮJO_k`]6Y' "r!8c\`&(iUbYnι`MKKbѡd>Ne4[Hnힻnofn(v$&)"Ff< ]US6PqE"\ƍ$QO]ugy@v`kh0VTfg r7ա֛}w/j)pl>>(T+M`lQ(Ih_On62!15hx , 24# R1&P `F} R"\q]aXB%.N;QJ+_ FB .C31cl)ьLth%@DmdXcm>x F`<+ժ#DIIN-R G9as2u% rc!op9xšHTʁ-}$h@(e'H Ke.*f3MiBOk44Mmn6Nqh7z&"lIb=enʙ|b()ړxSj'6Iv' hj.$!@< Rϡ mhIgfD+N`c@ G8 =M]LiM,@_ZxRKHcTFMh0# N}VB *ELQN4`[ \Jֵ"%DaD G ұ.*YTEi)ECk |9g={Њvt@=la  X9韢2ͥI 05 aPX!t]ns2@+@ DHCP"nћ^-I֬ uhbY\8G! DI_ݗ9";+@! *i!|՛`+ՋB)VddZ [B#]A"դm<Ԟ0gұr!o3b7Ll&<@&0o8viN.$dJm:@9eCeÆfbyq. 1u piY(%}3dyS;p;Rb|&zzo iI䛴4~.=iMox1}MN)5JY,p oQcԵIA:ڨ*a~Ҷ6˒6v|QSǶnՌj@~v;j'6bEpiw##k.綷A}0oN?챗68Ӎj~Vvvn˿QPz[96+V~˻oCMQ>͓k|[ ?Nͣq]Wǹ 6ѕc9 y[]T:GtuXMPN{';~?B\-uF'+Oݕv;y:[vGE >TeEu$^Azя5Iz{>6W8׏fglpAH&`Ipp4 al<0RugU$L:qEZI7j1hThAtjjv[Xq)6`Z MuQ` wN@! (M$wuxw pNATPahl!!1A ޠq/!%"%:HUL hqFt9 Sg` 0ain` @hXaހ jOO! CU 8" B3tC`6@4*` nuF|h؆u}oAPUg/JL嘎` b:acb . ԥ2"y+=`3r! 2LR[( &GJ NALSS0ʲ(6& b` tE d[\th$FD#7"CC1&2Mb$:|SzUc* 2 S@HPYB!b+*# !%BV$z"Թ~Cb#)DNrqxdҠvv=ɾ$<" G .jrb;n@ hVC$H /NGhF;" 4aH,Īz'` X "HpdH ^`"%:z[ak x#:@ !> G%߯82 dZ?d0D ″AB`0(A FA >[$N;b[A;@m䱋v$DZ;(<(!(؞Y;{ hĥ%Brr'"WJ,Ađ6%^ ց H baH!@A3=<<ٻ}1%@`$q"$&[kg# @ b!A<~` #:XZa݁`0@əɧ:ʧY zd4jE`%U 0p@*0Jy)`4Tcd|U =R)*"'"DZ7 )|D6Z+9 U#AhR1( ϱ&#\`$ |x(^@$) 4|8a="w=؇Wͧir%3 aA>ֽ4O35D c@019n r0=sӕ* R' [ArR1.( ^xA9@o}$<Vxab@|˽<6@^臾>闾]Y\҇|a  r?A,~HD`@ @y= ր`ҧH7]+h s!C#?- (a `F T>I`X!_ C 䦍v3X2@Hc R̅G!`A݁?a`>by=?>eGfrp4Tjy4Oʅ $H@ƾ50tqa Pk"Ć\lPB&fEs>"{t Y^tX᧠@eZ 88F"dMR$ʖ/7xrxZ=)!!R ,2١ )йSQ:e7~yrմk۾;ݼ{ <ċ?<xcFY$1V+:6~= =nCE  5GU')̚7qvIO3D 葃tHe|aSCFCa.4&$2Jy &$P}JC x)a{$aAB|sC!"D RJIhV7|LVI.䓪1fjfn grΙE$$w;}7u|jC(`*3NN8h1O(DMg{A4ф 6ab!L8p |" Zhҏ1'N N8LaO~!09G,B &/>  PvD("B!ޠCHǺ_j~KgLpphd |Ҟ!a$ c}6@AG lLܠO@L PUX!- &J7SM% Ym ]I83MIlg-\QXG4X(p/A7dq>eЁ)sa$>wOSGHe? 2A\? I'x=@)=4sTn0!;ш40/~P @j]*d a(C!nLlEq`y?э9@>L0&S`\"8=T@ # ㊦hAV4aztJ >!eLE:!$L =C!(h4EfmpғH1D!\4Y zd!(&qA 5t"n8n`|bgy(Ԡ&[P8r͒Hf xs H@*$$`*X((@"$0 PCSȏpU%@TQ j"I&\4T@E!*!YMT h?LP{YT^*0" `&X/WHg`x(bW:ֲs|_J:Ƭ &X(M)".<`Bhΐ%aAD`بp^ 3 n(F!0ɒͲk_ v|5!!.9[]>#L ^Ha魹h Jwԭ.s8,n69D-pkwm{ ofߋwmS x.+x5\d k*0r k6"p7x,~&@pp-*.l]6;3$+yLF]=@d8  hXШkW$όf4xDN憸M ۅ?R@ aG0-ht= yΔ3--bSp޴GPzC  F?p p` <# A\>=b5gzþ j`zn^ܩAQ *$8ȇT ;ٙ.v/}qiv AS6nvE66nd{ o){"4U?7‡MN|9̀u3 bV60ekƖ:IXFrb#|Loy]/{$p;>0PUf=Qt.znvt 9^|)R%/܀.,N?삧eo3|7jPNJz+3ԋ4nE_?y9/'%7|0$# WcW?`#WnF^w7zu@ JHd$ R_xO|,@  @ 2lPȔ^Q&dEWkgz8| n1:< lpH@1K^p`& `QnQp&0i#sgbGTh_H'H  @ QLHaa"(V 6'jfFw8 9& Ăwsg agpx"ֈUCLhበ(vM@ C@UHo>deP7 }HrTXȆ@<A (pc?ei2 b16cF#jxf݈cZPюELP hЇ z%LdR2%d p f_@_31 QAepeY 0UAgBb$ "0S QTJӈ8S*`q A9SHVR(q79Sŧ OAWv -C \e/a6feVN5Fe8mX9)aBu̐#OqbEՕ0|3%3U@)yن%S&0|`RPpAedX؃h9: Hodynh 1 8Q6hN@J(vq{T>{)% 'RZ@P %3h@60(` dGݠ Qhh& ifDY{q5Ze p G` - epP @ *I4x n1Р4PZISZ݀ T` (h@ @ /D m ЃDuj:pr``PD@ a@o ``KjF  `9> #;7l|V(#_x |=@O@k0Д9q ] 0v\@:K񣘮c5 ={ڧP=6kvkkVu J@:vj)!#> @#(br>40=pi@+;A`jXުY !ʭ') Ѱ&iz?`nJ5[ teA00 /@+pZ4] m93 `d8ԶAb,0`d  F tE g9@ v&0+P( 0];uGv@g䨶I+3ئmom]oq;O8)z'@)@P#bp[K@)$aς˶KI@ $J!dX4A3ð 0 @n Lp_BKo p!k`urPqC[{R> .aHfD.S 0q1h9 qm (b  PP*eWKo.wp9a "(0˂BNذc k! )òy &\6oq sbZ >d2˚)ȂiXL'D'! qz go 2ƌ\, PAְ*(((u`** \N`*D@#|ffN85,r"qj\ʧĩĖ͐˻L>NlPcB&iaKokL1uDA<vb>' mܖ@=փ=ڳ!` 0tU 9r/K>|<Ƀހ-N*paUqy) ۅG C@ V6{6.b@R-i ~Ğʨ$ng)x8e RwiF\_Ft 5vdE~< p~ )w utGJs\ jiI&Z `K, `o4FivHonw0Hw~]P>x%)zyP1H)Hj'GEd !GE@DFfwG{dw-PjKPI{nAɾtuP R}NAZ ԰V"`@ C1pJS#z8^my,Pu`q=fΓPqz)/"Q!UOOWPm>t{yS)  yNUOqT\&ROBRS.gm 47i6H R115+j 7)O5 ydTHyD p GI5bڵ#՗WPS`MA}E$]Nwn{JwjE͇zM,/>n> Epy`Ze?g{XsyPPb@ o \6.pdM&&/7GT^BCf|Pf)$XA .dC%NX?3BD !EɃ!A<K!BPG&HOA%Z(LI9e*ӦQA()K nWa&ոT,̧,Ϯe[qΥːFP[uȾi]%\aĉ/zهzM ,!KogС:`f̕Q5իEϦ]mܜI0U߿wnɕ/gcްQKooٻ>ݺ̓g{WwW-p@ \h︅nA4@ +KP=˾1qDKLL8 qCTDcqѠ8!/^G #{?w<tI(,{3R3+i0%K0qު˯&sM6d/ ! ,#! ,#! ,z5H)\"dH!āRdhbČ66(dH:^42% &H@d8OL`xHS$*#"E=7BHЀƣ|2A ŗ+W5R聧YF,l\caAnjV:%DE%* aL9p"ɻ>n1 ! ,z 5H*\ȰÇ#JL! ,#! ,z * 8Tῄ !*C flG|! ,z5H)\"dH!āRdhbČ66(dH:^42 @BD1gPCK/z/!o'EB(18_|0AP=Dc]㧁Y^XKBm0 H>5n޽ ǴOYx-#bx,raX -P|AF3@qpi>a 06Ԁ$`"cL@q')OLEIc5 Nl6l~F+fԳA!TvhH BU0E*RM0E>xB Cnz[gh/#2y)0x0L OE=lp#-F; $ >J0UF|!S zզ>XP?δWG ,AO!`j r%Xc p6;(> Y飏@op]wF@ԡ۪]$Ps>`Av A4uHMjuVk騧n/c15z7BSD ?kh61_x0/LMOP \&Y\hQ \L VP6䓋6 > N?0>VPE p\6>6`` E.B`~*@F0YG"Hซ\ؠE_6\PVKB8  s`H(<h/Os x ؇&PA#V(s9 !,T`` =#\6P ;`/ma>񏀔 Wh0 ,h\) a ; {EV@ xш Ġ 9|BМD )fbf8D;Ѐ}AG#(` PvaG,N*?ցjI)7"NH!.p>C lV o$g `qn5H.D6^@#چ :@eGq#200Q9fjvtb Ȣ*F*:W(O8BJpCR$+h&x!x2Q)!x$~x<,l`z'ud84"Ȁ;PDrZB _pAN _=-Op3?laYi$y r%8A+ *zxm[E{2[b4Q' 9)]ct*r@ah”G 5B> `BR:` dѠ~ՅNA5J;ʕ:Gh  *lz=i\)d!h. x !| ʠL~@5΋4*ؐB`\`&Pple~p~$cA& "H D}(w1b`t݇[jeΗVĒӢ  p}ۗ@, ˠ/` `1TY 0=19S)@'p@?EB03\zxe Ep,`c.- b*01Gڐ QP.`80`,fAQ6` 3p hfS1P)`'8 p &c yApXR}N StP'` XpP/B~3sN8Bu `o[F#Qn7(~p2wskAяl;JX-c-x"* A$` A0"B=pqA($m*@%,!a) 0(C*G`U [A1&PCRf-$0Rb" huP dZ@Q% "er/Yr7 o!9WCw_i@YjAyx\hy-x! sgY!ENB陙W;Z0 ^i턘6C `-`u9(9A ћ +@ AFpZ@ Sq!* 9ri8!HI 9`}d2) I7tFQ C11*#r0{w3)(*9sMwRʣEʢHJLڤNP"R:&-WZ\ڥ^`:$ThL1Ղ-Gg=(ei@^m t@ Ɛ3Vaf86@I&e{|4u¤K쫬N7۴)"<$AURJ ` P)ǖl0Q` p 0jDLk}51?|:[wܾ7zdĵ+Ć)<^W@gzp%,D<᫒1ݺ2[ ӶI qZ@ Z1)Dj6]ՂD^ IM::S]q:8vD-׻֐0Ԧp,F!,k}׎kױ!j؇Bk]؞[ ڢ=ڤZ]rרڬ}`yڰ=۴Gm`x]ػ}q\ A۵]ܴ GeZK 6-eۜ]uFё9 )p*1&ʐ0"s^(H?""{R"סav˃ pk1!vd7 0 %C:.bx.¯;  P 0Sw aIဓN=?1|njc)M,F}>P"!Z');V01;5{9\, E{I*8 y pG'=#0i޲ZNxY ~EN=fNʽlJZww)4P! r8 JP'py}6M$`I dRY$J pNR&u'ξ 1M.nƱXkXwhP&1i +kȫkkUX%@{ ثp]֎`p6ìY0i|',f mZLYymFw"I,a Q+ <|&a|! #$IpL0.5@;< 45(݄j8O:_<qa[`{kpP3ZIK yRQsE:ˁ<8\ُ_}p B %|Ȏ7/Cɚɞ ʢ,lx%~ʬwutu]M]4 3bz Ƈ@Y&\aD<~RH%McG#Y,L4IęL:w TJEETRM>UTU^ŚUVSCp…(gC~7w a%  ^&!(Д߻n:|P0T2ȒlpZ=eoRN5qSخn-OBM{_iITsavŞ]vݽ^|v^"D`=&4P"8xP ܃ P4уOo  R(I>ߛ @D a&k%;Š^Q8Z8ߒn<#D2I%dI'q0 9J >A bt !sx'xG>ygy p$׶ 8P\r8 (0aU}ˆ~g'X(n64Q źzE1_hA9)8↵P &]x&Nτ'p:Тxft!<&,!D80'%?hQ|JD OQF5A@LEa@)JPR  1NmBwE6|@G; CG(82.VҒMA3D#h66c Y0IPD%wpwo~_ ~ 6`_.&),a K>o{`-@KLR@'Nc?f!Wo_1 q @ۈd9W1p@rXN@wq!8Xh'fQ# Hx7J^WFªr-%8UЋ*lvwYi=η2\>rt`~2|o8hְ n&~WrY{lR>@ Qu& ̠Bf L3Q(DTdt7,+ | Gh9(XC0<TAZۭwxx kXe[D9S^{hJPGA~ ҂kf'l{ "x(4C) _8H`BPXX?0I3D 8@ )x@8=Pݹ(0C(ã 1 0+:`xI\AC H#TSV> tX VZ=Zd E\d㛼H`$\فOrlLԹ١(mǙ<q(0G;8hFuT> BAhۻ<@*BxA8A0( @K*CLL٥^2YZH =S4=DL{"RJ 2"$]2(NTM?TmhCOګ8d Ȓ-!\E-پμ ؂v,LI)_,(CP?s >Yԇ@8H{|kA?]ٚq^P uz29a.ǧА-Γ?f雿QƼAƿ cQ9U4bn Hjj*ݜiRkRjFQR˻ԱMƓJ;nd(er̝ ]+Ǩ@G5$VcGwSGz @/?5}f H;CPYMȇ2H_ hUacTeHHHH[PBa~z*dScȿcKT. A?=aI_pɓIx_l۶ ÀlնV5VIe84J8,H 8t틃 sC7r-W֔4-JJ: 8rPz%x`סXi XJF7˹$0ne˒ 5 XKYHԀ|̠@Wx1M7ȁDZYL! VP'븏SHZ 5Өe-ּV M 8XEp)I\T%]O =K[ yB:ų[E5 }Pr?P5} ;;ͱC_^:R pQ @?=9п:c_Su.')(M421HKZe` Ī5K7̄9:H{DaanaDa.g'YƆja ,0G 8Kb6)bia%6ch16z((n$ ayb< ,ɒ:-`[44.!yb4O%VHdII!Idd0ۂtX("DCbD.E&92q:5.a090vh|`Z]^e%$'Xl؃188dV.WF-X~)YFLH.X)5)!Npgr6gtVgvf8x<D0cG|*FTZQo9@74 fhҺWaZ9D|-bolxfBޓ掦.,+ J8e@='t]3b;-fNxpTvU6kKu n(4Xjayw%f>1볎.򋭷kfWkkkWI.La\ql&.La8V n}l"~cFF=}b~dN6 ;N~l7&$m$풘N 5ގžn@%n&jm~>#)n6nhn{ Y6=ّ a^n>c5#ۮn9fn&FpnSp6o&)n oSpSpfA6o(o4ajxpm#q4EAr[_!qG#_$?+/.Y&/j,,-&?ښF5ٺ3;w>@A7tYc74?thEg oztP8aIoDtj4t5 ڗRcO*Gl:VWWk Aj2_uHuiccu FjWYg;vd`mhv vaXkGl8Vwm+wu_tr_wwCx{|wvMnw$*7wI?:(xoxzxzĪE_xxHnx xw/~:wl_jyjxgyH?yW|yz.zm-eآN5/Hɯ|0/aY &|IW| (#tPӧ}PL{`z I&' q`bPtbZжX Pöa0_weT([R(hPyZ& hKWۛbXOXÂ+^Xj)Sbbv2uC\!DA8*W!lZ”T;!JÝjI@+WÉGW;{,a͔ i+).l0Ċ3n1Ȓ'Sl2̚7'2"R Xsjt&O\)eZ5kBh(h;L0* ${vO!?\aAC# ]@B8h`L >q#*,:,DO9ESwkaJjɷ;LM)>+qepsIb4VmAp-hF $zP;` %祁1@l"T<n QC 1P5UC.9e5 ?<WSρ5%ټ<  )=Cy(`"6QÐͯ"K]ݓ0?cP eOJH#1Xh  `VE0P P؂ FfI,$x`QVm`";"%2qF)P=zHD я8 z57WcI :X)l7~?a_x J1zƊe 5p@ w7!LNx/t zP\3 *X6 _ u$&m Fl"4)iR^`Dy HA҉w0&7,!u:1!-0 Jށ, hY?:PI&taȇ)RqZD'b qI[Ldk)g rk\ QD!\iDبJQp M)թRVő^&a@ A@ *jW 4@"@7$2@u*D2ֲ  _%$n]J*>6būZ :TVd`(1Sa ,pŒ a0Pw 39^1%og\ UtX"B,G % )<P‘ 3*Y/,9 AD9&ڸ?2 T NyAA x %2nPT3 hAu4C-[]c 08h.LvTkaAXTI|2jrb^puP AbQ{ 0.:. p;!'9.mld+ƵӭuVU03 i p \t @;L|`bo0q6!`A ZF`B ` ԍ+^k9Amع("xh?K׸65ͭ~S}jMRpo*@Y ^$}* ]ݞ\ 8:+v@XWkE_$^؇ {%~ tAP h@Ƶox' 7|/Ӿ ^@.K|0*~E=19h#Fv Z▄?Ddܜ y I4X;s/T㢌XJ`PFIĆ JKlDi7m8\%$`!@ 78:Ă#pq85\ 5@CtXwi z: !x0Iq/3Ax<=aNȀB|D$XSL p(s =@H@1(|AL " \~,4 83 xyl !Qh, +$aY` dB 0 ;; ~b"/:^,0!7vpaܮ+`'QLP9#:2"J|p n\`05BdAxL6̊9lC#`,TПFlOlA50 ,24AtP@ $\OEz@܃`<:xxH@U@2;(c35P %Q%R*%S7YZ rM*AP;r[.L̀>D#C:T?HBA_cS>A=&C"h2¤4 $@ faC(4Ԥ| _&=0F. 7l\!<s2''T%t:7 s p'Y!rw~g5Z2!].].&ye]NC@>A@*CC(`)2&CЀ@pdJeZSdqgJh2'Hhk.@ᘀ}CR "E_!F5Z B,7U:#4H ƍf@(xf"P Z#gErdy)\Pɀ՝# s eai$ T l!Be~DE΂0LXG~$(H3LĈ9|.) fE5A4$` b`p Q7D `` B <36#.`YkjQ n+ZB(M)$]R@/R0%p8&\<dA9K(R&uT)؁+kCE\\A?HA$AbQ%@C!$!p@!%"Ik5؃4>*P8A<774B@1\e5#7 u --*-v؎-pKu+}2DP&\)ƭ LP=TQC@ŀ&EA`TJHIٕEp0C AF!Hp.$0phCCD>1]@ B>5( |e@bi7P5l-P:×mS/\o./ن* ܝnH^2lx_on%i@{vWhfU#B@i(]` Ve00c^酨HA0@c`(X#@ 3 %pC1 g /'ha1dh16 '1BJ\@A1e#< C!L+vx1 j:he  @09 c11$Gr$G cz$qa`$2)rrw@)ex)2,rfD˲-2..ﲊGSک1]|{@p0ToKY4aKsr8UxcCt[ϴvS3L8c8 wr7SCuq'FW 73uqtKBWs! P%)PB` 0@DױZy4O9 A1P9:|:C( T@p]**Oh%oxS4J:wAHy#yV8z 2 08*p:3ˌ4Tţ7yq ; S[Ax#;a𺯇: 9`< 5%$09tBzx1'2k!=0;yi?4'S{<]ڭ%3JݜZGsZ Lde@!,㶟z<@oc) {u9\z3h|S׿c.<\93C[ 6[E<L< AK =SCɇ;{\e-0iHsFU(¢f$]/ v:U Y@Uϋ%pjI=@ejq6i}3l@ +~˼dV`gNWiRL]>g`^EB HWuΙW2 b۶#Ϧ=MES18DLȄ/(\;Pl4@ゆ ܪo6x_qn:tа#.WۛȕN|7 /q~Do\=pRl&z<Bplx@@Ç`>aW54-I?毟2%& k7_:LޟT㥆\`SPmֵM<4fՐ+WWTQ>|RRٓ9q]VQbpУn޾wC"D[R~ҷxHFJ]=LJh{Ҡ 1j $ ` )T#N #S1]BСFR%L@;s#\s)Hy{W}WKyI%p=rFq H4qWF) >gc9;Ȓl&80x`#viA H@&#8&*h@cFS.Q38`N) ^ZH )&!z98-% s{z؆H@ˎݘ-Y"yK=:Ycolę;gbcnw #SbaH 7aVYexsGG!o/O_h!=-Ӽ'@|qRY A Lh&%fpm0`zptB - p"7] @AB QP-6-Gd UP nM$ p uĺSgC/ 6mD~p"1~ ÔG0إ`uh-, X)x&R@^xJQ]&PJUtKO~PCq?o9_45 !XIh ZfǨC` R 0*f2%* Q!`MPu#` P@"-)$4᧘,@ 1]fq#5 ш+#,GQ&`'^1n]UI&H3<`o 8ǚH[=U iVK-X`Z, 0r_FpX"M,l'  n0)@m@;5RCoI+ܦ AÌe,9OP v О#q=uc a Oy jP<"Ds#=O1y"A+"M8Zˊe 5b/)Nα.0+ >N11 b>lQ1BmET1=w pvHaa_xĄ)a1}l@O(sD @a$T?l".O`thG&P:$6`<BmJ񊝘djN/!t,@)d6s/j,A1y8F޴pL2'83`S/8lqAPmi[uF BLS,`.,%0qFp @ȇ)X&^h @F&T"E?0cM72QtKRЀ;hCG&`e- B &hL!7W6 ,0  \*{_24`6Ch^NE:ޝ y Ҕ iB9DG:~{:tRnYЁ"6oAm PCsÀ~Iė:Tj!ӁA8ч\3HSO7ɗ 2Ҁ@*A4'f=uA((@"^ρ,@9(08\XQ=7zw@N>A!gqf \%P ƿ6 9i/TZEO1P"`C/Ȏя$oT`#VΗ  m#o/Ƭp pAH07f$z': ҃(A8 L@WP 0 0?rpx=/= /' ` ` F/' 37>p:c C*=+lBJ<P:bmC̊(CJH>q D0 ' #|P =A ǰ]aQޡàZ#+*1>|^:=>.ĀLš|bq?0fپp? q= +>  j_α=>^r12!r!^p!C!r")"-"12#5r#91?x'f:$M$Q"E>B#=$S2&er&iRڎFmpR8hhP$PD*2D%@^0I  +)&2,r,h A1$.‰AB>b--c%A8A0 #` r."Ancڒ!0"$213,I4M4q Le >N` eP60  "4a@ `DfĠlf6;eQ3=s=SfA i alab \4 @oF)$"b l 3:A z A XewN>aC_ DٳE]ERB+8 @b@!`aҡ!<v2T lNQI0TC9 (z^(>%ԧ`4MtMQFFLk >A LRCaJR C7Cz*L#jMITMS2>!f>A)X^ `\Kâ ShD C[w@jF>  E˾N[5\*5 05:/a7 @Z622Mb1$n-[$|?6kvk  !'oo P0))@UZej+3Rv.+ˏk7qw>\0B &7fXztA1 %R=:wqYJAwt-qOLte!Datuawv%pkiqw1w}w|xxpYhq $^Qy?Lww{/2@ #+s{Qv}E +w}q>W}w @nw#b1zWuZ,w>f>@8  <"тsS4%ɑKo':7FOXh1. {77׆'_= @A@䇈%imXfI؄>z' )؈߃@R3y= n-=\u_7ŅI%k@p+7V 9XWѰwmJzx!?&=h! Y:!}I h? `E7%Y16޶R%o6'B<|@5FaV2_"GA&_zAX&.@lqihOVR Z6 qmY2bjo:^"n&rlPU@1Hl1m%#j: o+y'!(W/ₐ(mȎ))@4vYkH`'=(;y@H=TÀ)U@:{iDc0=z֥M:) XڶEٸ!P7n癟Pj/po~y|n,^B//B@&0tJ՝!-(2ǎN>sBĵ0%֬d,&R8IE` /7VD| ƀCE;3Y:S;<BMi8 89:(5xGdqCQ!*>L<]d@D oJf8D>Oi~ (^eٿ@)dW~%X0VcC`lA Ҭ\fAiƾ GFhh&I&f `J.&GTTd>eTC@E Y)C=tLrQBlS>ob?Zlb]aT cKw'Dazg|FG#+sixϢfwn6"զS>_5[&?"|X\bc&)0`  (h-׸؈Ќ (EZl.p1DkDm8*+P-*   %U  ( !RVKԡQ.h'G*xP*D*MrN;$Hd޵3 7yXmzýFͻʗ 08É ̒µJ*q!DR/,EEh[8}`&Ȱyܻ{>˛?~0+b"makţB[PiiwhJ/IȁWWԂ_ p=RLYc S?SN!p5n@H+&a@ hχ xJo1F8!~-) 4E>ia#MB}VjХ.傌4y_m_KВ5(z̍/+y[AhO"Z, ;#&$P#S ?$&

!iŒn[H( R+#c*:c9>qG,<0.JE9O5@0 OZ'pNxRR*f3mT/HF P :B`~HhF)LPC,07|”c Fiո4;< R!`tU(0x!)"E値cسPp\Wshs GpV, \.Z!= 7>Q`aZMtAnaS5"c @() i! 8| oȐ'xr\pB@@ `0JrN`![1c50φvIZV;sxB}pv@t Xd0!j?nVdҮhI; Lս. ~"!rԮr0ʘ5t`}= p F0wL;j)/YN#l_(nO;Vi{˹}*@|bcNʘ+غαW>ؓke31eoQYܪyY`^P x@JzҔtwjp:PNRЀ;hCG&`IGPKpE AK4 ^ @F&;XoHiQG4c\h."](ȇ)<= G&X@)#T-l |x)`pta>b{K|k%@ R`Z۞C. d@~G:ajY@m`ZӜ0Pr`F *pnddZ8p'"EV;Cv>uʱ&wtQ !3ֶ&J`901 [t-HѺ?N l 80 dw xs {g ֹZVwhŦ03O?|pckhDp}roa7}=Po/hDٿJV ]H.ck\m@ &kZ$TO @@; ` D0-]ru(ۯ7p~Da*p ~ *-Q# G\: } "! hR׃ (%!(x 29t&#Gqɓ>40kq7 ZO Q)+Q4$ICjCVӕ.3ei[jiL0R0Siw9Q Cx i@Y2R6t+;9cC2 aIi6snX Yq+F)[eS䚱i)iiI)iljyCy )IUI)i Y)驞ɞ 0t )C"Yɖ *iڟI9 *Y j9 YJ)+ʢRY J0z2 ʟ-9;jC/j' &Y?Jz5H!\C :D1bÉ+FĘѢ@ 6h*RQteRRl2 7qbЌIm`ƇGQ ҩN~̺"N@A b/z%aaM+! ,Hz5H)\"dH!āRdhbČ66(dH:^42e*TTA @@%D>FӋ8EBe_,WQkyq8lc߻ns8xp @2gbn B 0;U  Y6')]s@! ,#! ,#! ,#! ,Yz]hHp y*LA`AJlą+jxÊ;fѣƍ%|b[Ndɕeb2%N7y DL=SO>! ,#! ,azhA X#F 0ń >(ŏnG!X#It!eL&)4x@)8lP'Л+V*|RxpZ=esR eYlL%E<6e/֒* Rpԋ z.uaF3=<8phl.@C _@H=6E$}`^ XatClڵmV5L@7Zulѻ͎(`(R6! ,qz@& Jd8bA ):H†;z1dđ$P` D$0!c-Bhp^ 9c =yG[ecDGd=c Fzq^(F*Zl ;b?*a#e77&8pسH( {A&L(l,'= 4 ! ,#! ,#! ,#! ,#! ,~z5(¿b H`„ |QD#JtQ#C1Pؠ@*T|ÉGWd%xz  '$fV:TZZPO$FPBC HȰ'Iƥ{}qg_ T\gVhpR)Z b׀! ,#! ,z5H!\C :D1bÉ+FĘѢ@ &h!BC\A%G#|+[bx V9Vn؀wȰzӃNbm ' "Ab@-7'pGbXS5IR`$HHNjs ! ,#! ,#! ,#! ,z5(¿ H`„t`D(V(QC =BL@  @ LH D:c‡dD">0d#OJ&T@i>^Ѭ6L9 j80iqbH ?]:@@4/2Y=MCr^! ,zR5H!\C :D1bÉ+FĘѢ@ 0qa#GE8X G^ F,=^3g͉7\yɀ! ,#! ,#! ,z9p@ &P 2tHbD)Zƈ~ P1r![iFoNK@u1ϡeE]٬GzR0ko? 9K̉СPSY!'͎-))锘 8(01 xAbS c! ,z@A  C0!Ç#hB%r,E4$$H+DPe @PBC $ !E 5yqn:pZ=esL)p Atxmʢ_!(QRMP@eX "a%Yр!{Gj4Ã6̆G{k:\e0Yܺ$  p`X Ē5V-[p!Oton`D;E,pA(QkFR &{ol?s@! ,#! ,#! ,z @A  C0!Ç#hB%r,E4II*Y,cL @B8(H AB5C 8tNz EP!3۔ES0I!&<c$#±R/,Eה Qdٷ`6hm U}jC2 ,؈H%  aǖ=vm۷qvscgPt5oޘs !^ZhrY ĠM.}! ,#! ,#! ,#! ,#! ,$H&‡#JHŋ3lPǏ CIɓ(S\ɲ˗9œIGlɳϟ@ S&ѝ8nLs!SL*9P)ɨS65ׯ`ÊKղ*:}+E[vxVڶnE˷߿|6 @@ 6aq $Pb0Q$HQ: DVcy ś^[o&̼k ((;sӁ#e0k`mL u pp$ qU7pWq :܂4]VwoF (bUPU@o|Qԃ\#0B*86| $lBSD ?kl& aC`vXT=!fr RV]$.7tiX,xV" $JB1l#H[T`1Tذ2M$pH 4bpaCßkZHpަ\+qi΅y&lZZG;FH|HCa.4&$2pK: &$P#S ?0.6T\mQƥklpsHqv a q q-21p4tB hI?W@*ԁHrs"`z30[̱ [5qp:l昃l]gmjp* n3P3D09b1`.08QXG4\5[gQp0a1dn%gnneHbo@57O!L!`_1%DyjZ̦6nz 8IrL:vX 1{j[F6n_ۏ۾MNL-!8%@:06` n`GXq- €,/`>Vk )LO@p :БHĵ`"OUJE\1[!FQ @Ƥ0E |TXMuDJ \A)3LH/#9J3ee.w}-1?ɏɾQ&Mtwش훿/~ ꩏}Oؗl:7g6¤D+GD}ȱ1~d  "p$()Q'XF$xE,}JX# =8;>BP:ȃIh "M6XVO\;]_ba8ddW؆nXqLx<r8P"y8y8Xx؈<R"$pv)eFXR pR QOyO@g8p @shV$bV_WHW؋oW!S4eyBWO pu@ O^]` |"p[[[oaEW}WŌHpMP @ w!gY k@ J(`_e_5ly E_AG\ƅ\5l 028>=}d"'d9ck@h*ycP6P` `Ws!ps9s=GsTr)9v tv(wIRo7qEG؉h !x (7qY~Yh)\IMI|2|Dٟ:6W:4Y?t0IZ;3 0u`$W!(Z " $z?t.z;*.PK@P(A3Z7G؃<=hE`Ӡ/wBDda&0Q:QzQ-ʤ`19f'pg*@l"jʦfpz(Bw #aڧ;q:P1G$Z:;ZE_꧘ڤZX #p4OVpZhX@'@ʪOB`5;Iګ2K)"(x8 s7pPrr8ʚ" ֊;Jӝr1ቬUzݚIQxxǧ:ZzگWp> {G:iJp WC{S EIԞ PCp (!# \b (2k,+QxG`3۳o#A3F{)0 rP5#@pQG6yP.d6GcHh۲!]e?K<+|[p };α{۸n+xYR[3a^Db۹ R p빤[Z:ga;&@> Jern%Eۻƫq.`g{0/;[32f;?7X{Vq9 ,@ۻ{d T۾ . @-[-Hiٿ<\|<qN'z|fP * ~beװ 0; [P B`0 4\3L^ `P; AD >F `) P, 9@@IKDp@qES00q{tPm;f *4F8{^JjJɍ츞Ɍ(H#Ǣ'8LLp@ʀdf؅;PyP˼\ͯ'kvFlqP :q@clXoopAcf;7_2pss1s?wk ^ ϖ=3Ӊv8w$X<諾LݹZa%p ar!$h ѽ}V \ЬRv[& Gb<&>{%ڬ}kMqKh1qe&HKx++9p 5< ۰ܘ ˰P.w! m ;[ 7K> ,˱=ޱmC 2'>$s&F.*>bmഽ$Aa~ga% 숳:+Xo=-&;ɇV BK Zm,5ky!j*zMp5ANs;{U{HҼVC}Qv0`["m>zcnO^г` 9?zս ST9P"+\w˼ʘz{9Dh .K7"Pr@vp%- dH>=?1#-#ޮNyt[[h?|k҇r9Dxoӗ1ټ7fLGp lCGqC B+R,t< OB|yn1O-` -KeGnlȏ1_ثlwiԏ"B @H1O}?Qz2'|r0zR{'ѡyL[e<6*64hxxbČ#jQH%MDRJ-]SL5męSN=}:dș hҌQc1!yBI5dM7ܩA9R"r0 ʏ$!#N΄PCE4Q(dtNd܈+ӆ242E7SO?UF!zTL:U8A5VYg%9I V]wW_6Xa%XcE6YeeV<6ZiUVRj6[m4ӣg7\qH%7]ueҎ|wxUk7_}S~}&`]ta`V'lH8c7fsԏU`OF9O15Ye_YV]7fořl9gz9T3z砏F:jVi:jj:kkMM#4lF[3>lӆ;&N3Ժ!;o{lܾ{o7mg\' p <-0tyœOgR_lugA']sk#Q|0}'x;/xu'y雏ݧǾг”>|q"q|Ϸ |g}߿iO秿?`8@ЀD`@6Ё`%8A VЂ`5Avl t`NpH! M( 9@I Qi5\t p~b($XC6H b>x&@$Xb P$PIH ` ?jdX H H TЧl`}@AⱍHA 8:qC K5  Rk@)C5F W%hIl@ e )ȇL("/,Te)H\!0y8@ &Hep * w$9ѩt,A nad#0*kdBF:э7Hp'<І>? %`J;ꁏ_H?Pb!=Zq$B*RDC59a-{Q;Q4+m Mp(BVI("·PC:я0=x+jַua";0I*0Z.6CHH>ue3Yx Fh!q] >H/aDKS[dBF108|!>6Mеl8@ i=!n{HdwH(<0.82AՒr/`zA |.}%L 8>4tp,d1Y+G". mMCTHwH@jz  V@>Q#nV"4Mhhq(>v+yh(‡v%jiOs`&$6փ7B@6Zad!6 mF7AW(K]*Ѓ[ZhA Y[z8")@Ќr46fD ]簋qG UNU{ g4NA5v"I@<׈cۼɆʨ#BCi?oW@0@$bzn ƨrYkny_"/hpA`{`Iʫg}<(e,H @/0c febJHCh'A = `S2}@a 43=$xǓ?v 2=))SHD я8t"_ҧ*p kb9l W`GwEr>^a_x/hL02=7P8VX72#}9x;<@x90 `?.@.ꢫ؀ =/; 8_m0(>: Wyp~"wxS&tB(LX| YN!7X?Ȩw`XTxnȄw *B,|8:=@PL'h.(|0wC҆?B RĖT*SB^+LkXt*ё(X=(/*̨zxD)l ~0B*p%a`MJa %8%g=fңXA>D$T:az2n_b&RZ%ը\zt M"F9pod&3 \RLz ` ؒ.ueGNBHX9{H%Epmȋ|[ƍȎt H8ZA: oHIq/fMHpəDJDX` p3< kxUpɖXʦLʱt  ؁'Ȁ$`'hKa%0I0K$K/Cʀ F'UJ,,g8o)(LR%̓H||̌ 'D()b3 ThHg hr pn@dؔMڴM}p}f* `SYUVҴ;sG?M 8vK$9STR=}k I .DNI 5ՀhT/+ uVox ;3(hooHPUonpg^`b=dP4ݚȅ||`[9`Da \`vZか %O=6kö|%8aHxpHjIርՀ|DA,9"n5&@h}P[e{8 MЇDЂeSKUc5HZP,0c%^N{`XohRh(ZygN8_oHN6Kgf}K[R^Se^ q(WHp(Ђy c e_%f=xcd~]fFgfxvc\i牫Rip[%[ZTm_X Ȁ0|#XX(o@D_h>/P 刴^kMM28Fh{Ǟ>e=B`HȲ2N @Bj3ʻ @m81.P(vF#ax &^gs$+<0]8e (8@,xwgoV1Ѕ@߽f]K.Vov<1N-,\hehlPAL Ny%Ȃr- @U<@ |89;pɋ} ⡮ "ihռ!AȾ>HBIB0!@$@W2qWq Q{ >TiQG"gwFHbHg_nHo>//MK8s:sjM`0j@b̈ z{w}iONC 2Wߔ#R2>ρMB6&HGvI_iz<E_U#OSP:SsLD}SbBBX `$!gGx }>x0xn(3۔Qx!q>Wq P0&]Dxa>3xL%^NGpJ3xpb(KD{)LNEvkHQ H$6#>cr8&){[ׇP U` Z!HQZ$ُّPR7G O]] (h *Ԁ!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*WÖ2+6$Xsb͜ -p&РB-j(ҤJ4H@=NB|…Jx6^Qgχhv'ٸrҭk.޼n pU zZ0.鱁ޟBNHmr3ТQR`[ x !C *(@p DA HH`C1$B$0a8knDPWU%~9 ŏn=z ,WO`?ArVC7B ؒr@?pAR (  襷ӌ D5f#A CK"B~9 `9jdI?aAOJ!dSVA:4H D kEA xrVy0_ p !`q2x(ZUchfCJ:)ZZT H>dRD5 p4/0A/Lpzj(DX=tB@Ќ9Ёn"zKDyu;.2ZhSN&p%Uave^PL0 6Ot I-ЦXpLؒ7zbqz"kk.)hQ 6\$o~30A`sU ,&@ @CYx2E?֣q1i6Dp1 0z0 ,0F¨x7 1X, -*6Ec<pL+{ /,^asK76~;Gr*0ZړHqq "nA%|y@"c{|`ox* l?xp TbSȨ1zJЉw0H,!u:1!Lф? ‚ )G&&taȇ)g-d<". 1[0D %)d5@ TEd :)N":,6@;]JNZ.*9jbx2oi} LA_Dd%4"Y9=@Ht̏ IHm e!SJE" ]Q#F4WzJʲɒ l\Ȣ6RP>8%(A RP" X6D`L&8)NU.s$)g|:4p:b2t` |s'|9j X$Ib%.kYh Zф΅ZL ;cx#V 6  @SR vRPD0 % a:]LiJQh<4J 6\" 9:L+*V#`bГNU$>S"l:ِ1o;J@vKL]!n=WM 61}sfZcig099`}!/#Ć q8ƹҁ>c3g9QG=4mxppcnX|(J!?:! 42*}ѐM r a cB]A8:hȅ?[qhx58axհ4cH݊f1,jD(t^tS ;ƶ*܃Z_v$ַ(nh5 @N@2Z j-6Z?7H@P.$ PP) RCD:6Z% }#D=1A\<55(Vo8Ih/0P0 9,\a/> FЅ/@:A' ǃaxK>Q "s{V>I,iԩ\(AӋoy@ ^5xAHoz7boXCG h@{qD=A:0~D%+~K0=ߘəɚGDt@94`~CP9=$ƒl t ,N6@XL x(h0&|0x+`@ OC)|=Ɠ `B+ ̃ B]Lߜ@ ld  81T@1d P@_>a"t K, BPKHaATڑ¡ 6 @a@\<0D<V  T4`"Dt J# J ,`< *0$Uh)#|970(n 8Sda4( -v=@X(ݒB|I, Dkq c@9H,aL(9*B!!h; 6N-*hA |D(RР@>H5>lk!B# KBv4?@ ;71CBdD[,R"IH$ c8Rt"D#=8"`<0$: #$Mr_K MZ L"Gmx]ZO#_#Y@9>:XQ<?bb6IFK@$IBB@'CXC蔆*4[zd?ԁ@l6 c8Al УL"e1e9v@ԠHA aZg~fhAeA&nҀn &aT"@>+- 4,t_AТXFq*%$%Ge03R:$U᥀C7B~@9 ,HC@`d@嘎cg4&C#KBN , <, Ȁ=HY,1! '!ٽJcN,KglN|NQZ(¨.K@T4u$ * @ |Ct,B04A--~ gNMߨi 0.Nc4D=!^ģ)+,pV | z!t>8A>B A>ɘFp4d%!8+NVf2ݿBI%2@ROXhpA378-5 8<Պ@*"B0@uC&~P( )+|2d x  -B:C.C#JN|m؎m&܌6A H6ȃ?x $܄5G7p -8p@0BAmNDMP -H W C۞5bݦB:-UDP*0)}rn4|n7A!DlotR)@`P@2%(YRaPrQEzD7p8PC8+3hfݙUpPK}>6l@H8 xqG GW,@ +@= OoK\ ؅mjo؀t k{IG q_ƙ1YA1X 4׉}/(u( @6@L WcɕyQرD%'?38''33h*R4\NfD\AL=4#rdz0s0WP03PpA03풄2n BP+D'Op8,˻2:Sn:-!C;(z\TF6[B88Z3B\@mmBXA!kCwG{D @A{HtJ'%q,4YEK4tJ%tC uPRO't _G2OQMTSSEVIR5d8YISwuHQ'DVoZAcu숵^Gi4a9tYp"}F%1iarHE%2GYQ,]ǎHHSdH-)jxvjR5oF}G6VDA*Z>0BH8ĥq#(XȉՃ68Ѓ(Q@,i)T8x/Z5rWڥu("Vk[_JònNvmFI Z@1*y m`c퉓-G$̚&0`MMX,7EM!*ၒg&Lε4[gy5kv'3u@OElJ|y™. J((TC A>` \ ,24APf3@m5p >H4Cd=HӤ  z-@/x 8yy6߅P@9oyBpg&C"h2HäONL#C:T?@8.+D =pdNxXJ|ӽ:#Sn9xY/|, ˅L3h܌,;:3A.|4^DԘ@8L=Q.,-$=0AzO0w6ĿW?85ÓGď}RMMݠŜ 4# 3LĈ9|qΖCAVD4L@XDC ltCjU:͐S(.z#uسh#ٷQE4xxت֕xC!$!L%,A"Ʃ5D@㝒w듗Iz# :( ǿ˴>w 7@` h7nw a  ^&!(߻n:p6`<80LYab&<ܱVOٛ!4Sma@FCMW^%̬_z[`رVŢZlڳm׺-VqՀo_v{Â7n_r}׿bo.4pЂ]<@.;>tò(d E BP Al=iqQy,IG!HH#DXDQ$R)J_L4 /[| 1,3LS57S#)h  LO>O L;LLMD R. 8K181Q.;MvY\Br1tz=*r (Ze{g īkg`UxksصV1H 5=7?eP$^yqʑ6] !V4$_u1W~LO2!1Gr=l8hx9fd![ 뭋j9du^:6wiGxa҇dh c8hiZku \X"0f͚A_:!gOyÜ8=HyD&@̋4LB PX 7)P^Dpyݺc#-X@/!v duz@ 9w@%a   KJ7='@p?j- NPF{S">ge6ˠ6Pat%&%4s`s$jFqS r!B8#Qq!}xAP"qL]@p >vlJiR !KZ<4@DԼ2#SR!e(8E=jc5axH @F;zudHiYE@\R ό4LjP?)e6d%{yt IG=a(μ4IM(E (֘ m% QT*Vrmq.9d@C\@.~qP3B*4ZLQ`w³̄9ѩNvrarF̔/WuA\k( ~Qz5RbЕVZ J:?\2b5LLQ<=H0$A<`c=@/I` 9Pf[\u`#Q^ cnwMdA} ? Lx0a&H?/ 3/!@F >bEA tA05 W{]7&?#V  d#X 6qY"JLw,D*aCHH>@(@ r@%`q4A` p,( ~(l0W /y"C8,9x-D/zʳ)L@q%a?LA)`h~ `@ X\#kp:p<|Sy0#< xVl[:C0ّzq8/@Q Krr{9< o@| h& O!76Z+p:(]O L`ZTYa $zB [``dP1 lAxzr g:`j gBA .\*Đ &8 Qba#7. r`'"*.`n4! ZƮ횁 lW+8R 0bְ 6f$oٸ P p+ 8Q fdA: vc-`.4AA 1"qҡ!j6o$%%~dl@> fP[/FAAmXH1o+qZP 6b#j  aJdFD27!#᨜`f`B2@i+i / 1~~:a FPaDKZo ]L8@ \`-@h L`'`MA^Q.N *4q"{@3ڒ -Io1?roR"=*}1 2s0}lQY2eN=6R4534 $->Ag p` Aؒ,56 ,.o&-od+ @nANJ2WyIn [&5hs "sK@;iCJcTë6D$(o79"*`sŽ 8ZD:T`XCh;[+$]3{tN6t シOfC|:~y{[pv^`ۀ=wЧ:sG;]Ҡ3//1t@Acĩ_sQ VT|0Ɠ0&G^$i-B|+H vE}#ſIdE#۽B,>c3WD @b ~C'\AKd/L:1J!*$fL㄄OxL@QNR|@~P6c"}hX9 hLq/R{|#._ƇGHS'|H&ʃO@u1ϡe;Xzh̠ki2uqĈn4=Vnl9aGʜ5lYsΤK=Z4fƧ}:שK7 {kٗw;pܴ?<̛;= x\".pi zcbU* ?<1!wJ$k 9\p *L2(`3DL P S ,`Ws,CU2GwLY#(D5w(a)/h e)"繗h'yffmýٚqgtvwmi։MGhh>`-4 L6d Y馝0`S(4C /LDb+e Dp1D`QA-;`C1W#EOե=M6Da54rBaƬ ly{gpy$&]L|z,)Ϭ(21/s> G3`z j{ Wӧ^U 6G"1{ 낿$,]@``g5\㬩1@OB "W:3x1u5V H4D2Ụ.$=kt4p5`0V7L[i%{5ohom r$slr0˷|ULkޱktOO}p1 q^6/4. ?~cVi ,bC !.k<-JCWړb8,@тC `j [0б! `0P¬2hcBklГ@IJ4` .h>n6)[ǜ;&6QxәH*>4ÙmW'@MZ|Έ48)}|G"~ā!7A%ucp< )5`WkRda^ @T`[Ѕ9g)I,@/H:p;m 9 s .LaX0fdD5)$xEɜM\gקt xUFޭqhӀ& R x)`0Ђ XB>갿tbCP$W9D#@wtcs/4 #*k*^X#HB:`LJ qa"? j=֎3u*^%9Ay|'>q+}zqd'ؐLA*K0΋g?K(h;Pր@)PxL+ hDTG*]ZKrx[. @`#H(m.TWLPqeԮ6HAGf4TxUB@jᫀy. ^c(<*.Vݝ:ɼ~ØY,k-Olٝj*FP9ƮR<$ֽ #Xe @@{gɗ<-phlXl8Z]Xc1H؀{j  & {ehi2GʅL lTl\1H*b P8Q<!k8}!{CP@"T_T $CpP  *xA1Wa!@< A)py .(T@ V!" C(d  5xv|1TOB)w-FyޓD IoQAdwdpX"P}*b1`qMrE;Au#sq bL WUp=excKO!8k p6ET PwAh Q&TгIZvd4 !)!``o ܠ(eL1#G& h0# ӳ!'"S#ƴ$C@H%@ L`Dx>pFL ۰ HArA36d%/9#$ zHA! \@^GQx`K]0ypR -^Cpi( 0) `ȁf!5Q`gJAT0 (8j+G"ڂ ᄥ,k(A ̀ FT0B@L$DJ 0#9 xrL_>@ H$`4xt,! B% , }(8W}a(E%B brTȅZ~x<M7+>׽tr8 Jљ )ffCԢ}.)kY֠|ȁ\%iZ"xCi3Ehf#©`#<?$A}[-EN˃DCxB!^[δXBяꕯ! 6')@{0lFr:4Sg7 1D=E:P 'lp(‘P|`hx>`"-tЏ:@4Blk?p Р 9xY$PTe4{u6 cz-&j/Ztu1p>u!MgrqB\\,8sXfP΂>l,#x!e[% zl}+ H#h]]HyΊe vlp! gя/`[O"e U0g}N# *@7p7 uPY f"bq.{.2ߡ+#m]0/` @*Sh6d `FA1 b , V3Wnq8ux>s %|0S)% ?r-Y?s Pz8*#GQ, 1x5\0b f 8SJVS` J`#+ T0Wq'c8Ր-P8"a N`H6r027{ m  P/ c ,G ҤD! !pa'Q4'Trq$䁍j'"'P8R)G В9P wf$f "p$;rH "84æp-R"+ 27R'>S8B&Ji'nO9$R!9 RԖ @HS ""^bKzb'i~Oi9$E%d2`iPy Vf`vQy$W;Z0  F![Sx>Kxq#v'CpZ@ cY)3I00 f)מ*`  T也LP!! ,#! ,zDPȐ! 1!8/`a@! ,z ,+$*x1F P8@@! ,z 8PȐ! y#{7%݇Y}Xd5^lIUx}& |ї ^qPa|" o^W=]6AmraoȔtݑX5cFֆ#n.F[~Ia -NM'5ycciP&ㄷe՜)[p&fmyY%^"jM [a/fgbjgg)ɥYig*u,EQޫdpr^GT#s.NP@! ,#! ,z 5H*\ȰÇ#JL! ,z\B ,aM޿i<a9Y0D #q8p"v MHJ?]_ XcP)&(U  @P@ dެB.ٴh  0`w kXZxFؠ 7<#/ŐK:hH m*SN q~hCb7o@whC2sPS? QM=D!ƴ U'SNXaG0?ATyPQ@tP,p`O⋖#෪ dEl2TC1L)@&YhOq 0AZ]|Cl;Ɉ)B (YI` 뱫28 &2>@ܰ;EuH 4]z2o/=T619/h)1cE:KD18 !K+VȰOI $pC(m!0@-PA-3\P(<N:#-P)V`5o4 1v"Q)TC3zgE>D cs߁^ p6 MKR` &_#wf*`Ŋ'T@*A$ on;D5 t=  H5F|Bqx!hlC8NA5WADQBN+/s@bwx HLu\&p )@P^9I $ht 4~8hI@|Nچ"$.BE R1! X.1B-@a "HN AHl|z]$D' APIQHذD A &)@|wh%80%CQo%%d8l s93*5`nA61<\Î ?| єl@wP"ALF _ԁ @ۋ@ p n|*1L W22 82]Ƞ)Z`3MLCVRTGUR>L&x)`T'F&H8' db2E Ttk QD!:\ISҖ@i#C  X) v8 D#6uZ rPT'eWrJh*rX(,R Tf ] ;ա/쏒iHe}Pb'fE!Bl#EHMdma S.=@q D+@ 10+`?`̐ T͐;\̍3 ʤ2 ! ,z 8PȐ! i z0!QDq*ICxcTAjشcãJةg& 6-Zeb56̮E7ܟ ъw0ܾY+F7o଄FVPpBc8%;DqC~ѽ'{ ZuV[ g@$^lݗ{_}VBn!}(ц! ,$H&Ç5H" 3jȱǏ CIɓ(S\ɲ˗0cʜIfNJ8Nɓ͟@ JѣH*]4%Ý {JʳիXjʵׯ`BJYaӪ]˶۷p)Χ;V߿ Lpވ XoYÐ#KLeu̹ˠCMtXΨ-^ͺװG~:q۸smXbmmN5̌УKJwسk7[ËOӫ_Ͼ˟O?CF=晀'\ 6 fiot>Uv`bVWSyhV bO((#}*ZX$Uz_3)${5!Y>ؐPF^tЪ&KWKE1,"`624>44 .$R@ $p@< A)PA (E t мޛHn mAt@l)o<0w<2H &05n  !\«x0+ <@s 0 o"P(W (sp!plOB ?(ㆾ+}w{WC7B ؒB` *`M&*ԭM:t}w~l}`H5&,L;׃/tPB.C+@%nBLJ QMsLm1-Z X;<hSmCK" "KC YD? P@ x.X:h#R hh t`(($B`!(AЅ0?p p` <#4`5.| xxЪ "LјW#`AȂ&X6 zp4xa &>1S @$8$CП"s1<"Ȅҳi19- g$12Bu'< +(0ON%0la 8BzptB -ȁĦZիfXP!I@Ќr46XGqRHE:Z`GP]ht8 vWe5vAYrNA5:A=A.@„cSQ#8- KF@@=W aH`zQl6^b$`xgwP X w )@M+&9Ԝ=;1T2Ԃ@%xu` lE RPe XGp["T(lP@_$dLcd-60a9;)twHD я8k~t"_"{spcjV9l W`jaaELtIa_xzTˊe 5S͸@ j7́* /tfD8 *X6 _ u=APl`h-p=Qpq@ :w淿SЀ%Z@'1d~`;<"-^728A.r{`285 wˇ)7wpN8b<%HLf3x wktno6( 6XXHg]=p ygB0@x;R0Ȍ5A:e,Qv*@T3'%@M9X{w`c AJf @MjkX/?Yʂ# c "wrD*>7Bl@Sj+d9 @_<,#$@ŖO5i`<HD C'$ ,~׀'X +,2PEJ$+/ rXpr!a.(8O8N  7J`~Q7p:(,!1g "-"qЄ  G3 UH РPYх_XRx9P< Jx/@  @ @ 0lp8xkp\!Ia0v,B,bv+T 2]A6*/p@ $PP0 u'M0/ "nҤXǘhXGPU .#1.r."-]? B@0 g2@!-//0 y  i*Iw5"K 5P5|A /]+BC4[37 P'z5p5uzg3ehz p62/K3x.0(`!3?밗@ ?v 8gP @ 0Ԩ ܐ exx|闀)I+%~ߓ@=~p 0<', 9P} )u9 /x<]|Î;S> \iwnc G36::c8vf8 @qsY {D@a00>ԐԈ i  ڠځ-! 9ŏ B c0 fD+ : =Gz2pAFe0Q E5D0vPdj+4F\ XI0Foe*c00Et '0M C>DB,[ڥǣ 060@rIVa6B@ ( V2)pÀ J0 2J5 ffccڬNJh0 P?P ? lU8,0-4]JMNQ s0L }Ȁ9!C i ,S!`՘3c'sffsFsr1 7'pJ'u 1tEsGtr1sК:W1cw@CjAs2G4[;gv5f5 `Z\0/ @ ԰ L@ P ex;۳]KU ` yN*0/2#1 `9w4H@4#|uzeZ0'1 `k6z__* y7GهNNE 1"0+V/  }`?@ P gࢪk;̽-&0n$BB]31TĠ ylr;_ȉh/nP K"1a֐0ɠI,>LL@sX#)>iN`!0"C1crg>猎& ʤ<37^)3闾N*"R,R 1+鮞,h.rւ-~/B2/l2W0 0 ~є507R+2 2133o7)8x7|30vs88t<5+09:;S;#w~?@DI@ @3A9|oAaAAd8[CTŨ?DCTDGDK4HIoIIr|7/JdJJL}UGLDLMReQQ0hR5R;YRR.u}DGTFTJ%MS?XX`RYGE_\W4T[p[K[NU_t _d"L_`a=AC&P9 OLpbb.cOq6guf3g~fojfhh!hiR¦džSѪƦjI} ohr7 p'́tyt]՚=s57saC DPB >QD-^ĘQF=~RH%MD . !AB4! 0eҜ@4P"SEM<(P0 r ` RZmݾW\uޕr 185@bƍVpcʕ-_Ɯ9諒 Yho{`1j֭][:q%R("A6l"l`I[r͝?niQ#7tBKG^z^Kd>Ւ/ R؉ǝaY6@˯B /#81D !a9R4 DU:R)0ad!N@a WD2I%I;*r(XqC$aRIK)1!r"@{8 -| k܄لˉP !<+Sы\͐x(@j6uRPjISOE1¤ʂ$`3I@c+RVmƂP,h h\HA V[q-% 6Xc?bMu'HxCR7_}'ꁟ 0b(6aQX)a98&xbR! (8F*0 f R2h!dyh @LH Pض!! ͛f  Dh/N{\' a.!3HZL&lNl:C tćl<Ct ǟqܰmdaz~rk1}ar0LC?n6dtrI}@rIz Nx]gz`4r9 A<=qh 07}.tT!~(C &PQ|^2TѵYo~#L[\l I!тT(tǻ>!_΃^ ?,ypsN T %-J|# ;A uRqю9䀄T  ꁈ1 |`k AX7aC O@ 3 x&p}4Bh q N,!$pjDZ@x\B #wп=g::NޘNX60x8! v,Y̌" ˆ˰-Kjj5mjgF3vi# 9އxlcZXO+93]LW"2 :q:82XfToz]s/f8Bě' 'Fȅ.ȱt% Ђ&azwy R 7+@ DHc07?(b߲7Nlc뜝Zw{P l:tFj]X=wc۾VYQZJr073pt9^PZXumh&r4by Av @i {HB, ASO 1 M00Ai^%1 8Fœ`{P i4`-X|(6]_+`N P - $! fX`:sb'=Ch|`+ceX`b` dI1G01u`dLxČ k $CB4s1xߊó9lڇDSHe2n2cex.1@7s5肠 |89;pb}2@çuk :A7x='j(*Sڇ@7 l7~P :hL*C&dZf–CDC?D&eb8<8҂kpE#xvkRxkFq;$rtH`hL+)Y'.^@LHM{dp%GܞCHtȅ~hlYtTGv(NwІ?HmLʢ("X.0 :.^G|w0uxg EsdD Y K8 ؀ HS ޔ (N8 M ͣY1hA𗒑IDXNLىNꊯ )3 2RP(άO H xq'T鐮3>(3PMETUUEHxHeZ[U\^_`VhRMcEdŶb5VeugV8Z}T +ЩO 쬀!)O Љ PmD}W^ݐ'6(HFO1GP9ZzzІpHHh%`m8j$;,jKxDx٘UמYVPZ(($b/x/hA*C:Vz'(~XXWB+$9D8 #8%}P% ߒZ&<,SE\< x|(Ia+-?k+؂j8,pf|Edh– we)kقkX/ h{bs0B+b+Le@}(8QA9c9(T:)(A0EcBH{`]ލ768 7޺b{&v;:+0z0?'\ax ;uiY0OH K>0-h0=<khÕ:5 WJ=j=c8^X<'fFSNa-7A?~(} 6 A,Dkry%ȂrP?aQ޽5݄.1[Pܽ)=veff. Gڑ;!`(.`J: w?Bⷭpɂ| |h,G)}(fF脖8XAVum@ۄrP,k@ e|ִ؀ U虦i - ]Xh<6ƶnt!?1Aꩦr$(jfkCT+n9c@k(k\ Ȁ0QfYZ@2X¼V2Ž>쒐'5SfuJal`V惠9AڶBf&SԎ, ؔnl(P~xeȱJqܶp,z^`Gkv푐 2hF(b=a)lAWрFaaggh'Ao\ Xpf9QǕx^`fn]@Z)"hteiy~q! Q褈QؙpA l[ )PyxƓ Q`hץ9 0 ᙣ(qA[yR @o{ A]i,2Ost/tP x)@o90XU=''G SߙnakbAuhMV 8)iz Lu֙#9L "P٩dC@:6رYAGtPYm˲wd%_ `bJ$LOeN:8]JLfũۺ[Jm '&(]ڦ}ڕZ{|kC>8e"6zQ>iOЌ-zL1$t|(7#5x#9#=#6R L 1, !K+90RLY#E`B b b aff" XbɁB`@BJ(aCkm.4G!d9(e fI5.1l#H$J6>HJ$ŤPvM=ِE L+ܓ]вZZ`yl[T`1Tذ2M)P ֈd[\Å 4C\0fgއtM޳D $#,Nb& ! x~{؛ΐ jgTkf@ wD>GM! O @d-Pp+t %.s]vs<XЉ]!@(@Zc;/=:"qU$@\{HU!ЀD R]X#4C $\i߈uT6iO1T [xGtØ7 \ q쥊}jG]~Nc%Q3|IȪ#TrM|H,h,NK Р 9 GJFXӂ#h /c+ҶDA`0 05XzkT)2M=-3DVu]]p=œB)m0*f!+)#J)xVdž Zn5 HpA<{TYY׼@! po`T]4({[6I/384(V:LkX0f1\_`u2T0(X]鎆c^ag\0WYuηR9^r4BKE R8_i6 (7$ -\lyf zՔ^"m̹laڒ=> R$昽;NK&p}JAvY$%I,)(A^ڶ>6}%5t X7pPbaqg>C< .6ԡ.|( %όa BB0HqFOa_xt__ 7Y.!1Aln<X3Ɇ*47L&oO<Ӯe!]2s0!">9~ye(sA-H> GU%d(FaAF! x q_PRdBu/ta3::6I8-@|!)0;8A(1}ۓ56R7I+='@ԫx:DdԻD ԢD6dsޗmP CngJЉwC pm $ ^Ȅ !VI2d܁5dA>0@CH?xĆhSʨY)x2`=dbLA:B`tB=)@-5@,?l /T $Ƅ<5IP@ B:0a6` G;C=_ (! A @__`! ,@*C @[7d҆.!8a$p`>pB `x&Cu \@ `f! `VQ X1x@8!? btKXC]T@3a ""*"#"J@: !zD !u10@ H(%M8!c4@:c;B" J"] @N`7B@:-?}GvGI4<7$@H@ @+mh(V^+fn+I+P+++']Aͨ+֫+GR?2kP++,*pgBe`A @Ug6g&Ugre pxV@gWe'MBLGR@Zv%$I@l,mbJI$@d|lle6-$=mZ&^-fbaĀ5>CtXm7-B|lO<J-1 y@FPIU>45[F@3@ `6tKM̌+HB+I3 uY20ba3*Q288*4 hIUVw;6XU *@[,%\)dS =+- B`f3=WG}2 k+ 9e=F)6I+OJ 8ǴLtGC=bh䮑4Z1`2, l?{Guڷ5JD Y5N- ((@Jq DD5 *i&.\~5!@Wˆ4aa)AM*EUUdEA6VhBAlniE )^+\MD^qPp"`T@m[%l/εBJ\8pık 70\3pA s6LPwlȶw7xH)U<8B|#!yw-A>AB,@';w EZI2`Q}@4)CCD6pO,០B!^RF>( L5 Pn<6C_>*qB$ 8FUDhge]$l}Lh<iM_hltg]>hB/(@e$H}br3[6@yc'|FRSy9:RV]ҸqbІN.TȚi"֍zʌbjn: :%::,`r-+׺z:өю):;:'*A+6ŰNgo-ծzz;;h;;'~{+绾3zR:c<󰼾<[B{W_?y_ȏ{lzsMocW~ @W>W>]b~ZV~v{G}<;>3E?ľ׃>Ӿ?>Ӿþ)h~Ы|O8F<gk?s{?jl)3C{>;;$m$ِ6@\ ;PC v bE1fԈQCGhiF'QTeK/aƔ9fM7qԹgO?:hQG&UiSOF:jUWfպkW_~(*YgѦUm[V͆w[wջC4 *thÉ7pP`p 7~!ʗ3R`b۸qy(R#jaǖ=[O4ĈV=mћBpƹiÝjIq@ȕsn<<1a>W=q=mǗ@uh塴E ͺ0d>Yfy1CMФ0FAzl'BP ,#/-JR咑Yx" R!䊂A/,fPbI%"8XbNaAL14hx$p2)E`DžxѬm" lG! f :ӿffb5 {=x6AA52UUYu"lHا"L+QJtٍTi%$,OR 8Z5UlrVYGsݵzraEg3ڃN<8\%L෢!\1XbFPSCO0c` t,e ՑPeq`TTᥙn= (h@tFg0@CPB -p ; DG"n9ERؙ-r$rh HG %l`9 n@j r1#9I$٣a.0q h sL63 ,ìw~Lxʈ #W=fkMiٱ^!3>%>o~o̗Wr}+@4@. t?)j~1H 5A¡l(TQBބD1!VCF% hLcD T7# y'Ґ :dODMz!&7IQ$(3RJ|qR(yIYr&7Qj-{]Ҋa&sYHFA%-M`."!46`&pn~g9pN!Xi&P)M}"@A"Ld)A!JfR)E 9 !AtDbhHU!3o*cKqӥ4CNԙ++QT.MuSUN69|PRn  |!'Imu+v(_(o]ԁ0Ux_4W؂5axAucBeR$@fAZhxhQڨ`\RkamqaLhlnܖpܘVus]NյtmsX a^.G6`wѫ(mW>Z_7/ >G8vd|n#f`\ xyw+ a L Q i KXa&qϘZ.~ ne=2C,౏l'F d`rd)m AXl]vQf.{\r,*\s7*)tvI\B;$D`ZH`w8%s/?|혃yw#?+dGKhMQ(9H$i Zo="wh/鼁 XiQӺ>ڦ˺ןnkdk1gdF )V ^vmtk`ZRf!C]Ip{aw$nsZwvk!\wbC2HŚ".<[o.xjUEf(\ 4@)|7kN%LPC3>rܻ)tMp7ʮB -Ăb3fڟv\-¡>w'];Iw WNmAj+nuyz9}9p-_}Myg ߑvGEXzd|zoM|lwr\)+̟#y;O>5rra]Y/}P#//*cJ<E&"/͢O% %TH$pY\ 2"lM&2Z/D*P:""B%cAČ/7$*Ub~X/p# ^8` f .  [z b,`mG`!4@432h044jjz#@2QOW: 9#*܍ l#8@P/L=qNONOJN,nݦ>̀9,bjK!rdi3xpOpBQA6@DD}H4Bku ʴª8)cy +Ĵ"vJ,lpBb%+^4F=`T<`lG%ʲJ@˸< kJ*)K߯LTMK1{,* bdGO=j#x*N4BY!%ȲxgPŕYcY/%~$#k+/kL8 !j9m^3&@98^a01+y>/yiYיLg2ϱ{:]:fןx!clЙkMp2@N6 :cn&1y?y֞.awtI?œ3 5" Z@Ί`ϝ蛱=Y95wE]r Z.J?B :=ªU$Tjh`z` Z?y0SJ.F:;]_z*z^Zu: ۰AY zk a `@ֺ{r;knbv䜑qWͥ;B ڡarqAy|'`a`Z?wAشOBՍ+2Z b` `W S4 [[;@!^!hAVƺ7+{ c+an=#Z<G0S V\$&5 fO@A`QJܧbn;7{99w1PsxQ\6+`Z E_U/*Lzk[s3³{C`\` \'?Q"Oӵv>\U̓S>)N>U[Kg'pl(~^w#뙝'>ko>^ t ͥ~˅c~&ƾ*gS(>᫾*Ղ)k^*`۳Yi_0YÛ5{ r@?,*}LLѯ&=wY1I?'h:L5Mѿ?/9aub 4`[UMZ֑ͺ6WLJ+YxqFo.NZ9m<1ԫ[ 킝G;Ӳe-+n+\|'>{É/z}]G`x%w%Uƛz)qEv?i!U^q_s$(\]`>8ڃ O8~-6BޑE1i_3^FؓBȔLe grj)`pg؉jf\ovx[]z)'ڧs4|)Ήi2yI%pljhsixhBZsHf٣rJ[Jtm l>*]'y_%j;&񚡛=Z"`v i\*ګe扢®ˮi`栱.{n> _ \XUEz0^pvGnni㶋qp+imê"]3gKrj݌sU+Mt2&&tKRO-2V_='A&'/JuSkvjvn wrMwvߍwzw~ xx/?yg28kc]=*RaV>y馟>Ӗӵp·g:ߎ;֒yZg|ƻΪ~<{l60_}-.E4gkj~oݴܒK" 3ȄlhG9`07(h lpml;yx+ H5pSA)τ*4!+?ͅJt$PjtCdF9 Ґ* IO*! ,#! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,#! ,z5H)\"dH!āRdhbČ66({8H(_NjӥH @x !C *(P67_=enư0HC 95Y: C'\XK^0ЋFR˶ˮx\ІYyMZm'˘l/0e`0A_%_!8ps 778@)PӻwR~+L )  ˵s/t@! ,#! ,#! ,#! ,"z@ A*TAA.l8AZ?9zLrcG4@B&NJLgzQKd/̌\^Ď2ii 0JҫYI7l/0ՋkϤ%_LPJo^=pRMB0$W z bـ! ,+zhHC40!Ç#NXq!ƌ7cC"GvheJ,?FPA!$PL'T %nJqp%;ьQ QvhZ1&ƈB >,HpUd&LV]F|;G-l @òl!"DcTRhɒ,W' Y& D!Ak-xN k@! ,#! ,>z5HP@ǰaC #&DСC NSlP@?82?!߼! ,Pz5H!\C :D1bÉ+FĘѢ@ 6h&LP5)Q… y +/x8TJ HXŠ[4iA&F h̕8Sv1:LLF ! ,#! ,#! ,#! ,$vHt!#JpaÉcb kM!Ɨ0cʜI͛8sɳϟ@ Jѣ; \"҇J:š%#.4'L..]˶۷pʝK]( A$dHaB A_  m*T46a48) #V_Ө]C A q\s#B!Ffa+W'Rh@e6(Hp= gAA=<.aϿy&,Q~,O=ʸQJ* f*Ƞ *,P-#ʄX jN=ݼHH.(@R?( ΑK:. PWm*ХQSe>wTbL Up=exp#8E$RR)*p#6裐F*)~-e `9jdI?aAOҥ@**- #)&,@=ܓƿD0d.@$OA2x0$l(O* N&}B4/0A/tHU&WPuЮ 1+ܓP0ϬLxpH-T` $B3z@h`@JC[; P S ,Ws,`L >qp.הhSN&,j#y3Eذ <65 6@PGHKӛ|s-hF $ z  p6 M+7aԀm<0b|aq @ZIC WQQFW,x^V lٕ+T6Em{8î$St@0c I @@ԉc!@Xu%o:@ʀ׈bz"ჼ(T3QD4  ,̤&7\׶Rl EI{!rLJUv0 Zr% R͌{ZX|yca emJHc|@&A u `shKUE0P P؂ hIt$8Xm`#̨F7M" GJ#]D?Cpdyt1ʦk+a/\Ob2A43}Rc=| ^h&,sa o"+.n>170~y 0jЅ+`#6?` *@|wG"&0WfE+@ p hGKZI{r1z"JЉw0Xmk_,!up:1! Ï, h `- V{@?5]PP>L1!ھECl)1J-A*d* mS;XF2QD!\#b JKq1TL# t @ Tb&h@o8 MdRlPpf!Y{D@T: qbbRZvIG(6vc(6͕|cЦ=ҐZπP9=N RRhzrsi4Fp- 2AԨNIC'"{HQM ! M$֯M@cA. 2P3i$( V!$ɱ eg@ { 2tN`70XJ4F}n٪[d <]Va q7D^g87ppR@C .G<OWn2! R]*Z Ϡ<op7(5yws[YKFh`0\$%+)`A"V a/!@{G3yA deE91:p^u@MD7<xǣO}=Wl,,fA@K] ~ǻ=":5p!X8xxC13!  ZЂ4b`覾?wϿ9dk p9 xDq.a0qqc A_gv Pfp5˷0fT (g(_[QrƁc*u`wT / q``1x wy b pqSXW[؅zjrp&~p 0 9p x8!'z'$ 1$[_%<wph Q2!@" 0| [v@%_"s`=#A2$B[c uM|v @T @D@ ~pyЏ@ Аvi戎(Ƈ# 3|+ c0 f0_ɑY:x12pkAe0Q L"/]mT`\@Z7@Ó1XI/o0P0p Ap#*pr-ٲ-9=% C` Lh<`B@ ؎V@'^|i0 萏9AT )Yu9)IGwJ4 jyQQN$99D H7k@i!pTGuzu' v6c>86p1hpNhk Kp}.S6fhqYCZa.<  )`wp?@ ? \ &/ P@ p*kJʡ!6z2Y5Q9?*QuAA0v P љ`WN5^UyIw[uaJpk0$>QUz _8I`Lw)}0PF*`ޠM8ZzYpFJH:飾 HBQ|PeZ0(Bu@B@AfRKyq: @CP80 E0GK0 0 5MdM<`ɤ0Q` p /0! w" x W(o#{C)Pm} XP1{g65{X Y ;۫=s*Y`5AJX 9Sp K=H-rEWxVm5@p s0v00w/0gS`y |@NUuUYUQכ-a3` 9z D  |>p?P (: !<%|֕@?y `0a s +ƒ h ,J6L@s!&-{}֐1q|,,ӏ C)w]Ԓڪ8:-a}ۺ=2-;O(W-jh^nm2-2}2>c383تRcACS4Gk.G`LcS6g6?nSjNm㚮>nL;C|<u#̇rMw.~^~2 yCĭwT>D;CEtD-y^$F ^ þ^I$JtJJLh3KK-`a(Ҵ#لN f^&_Q!5R+5R*RT2ES69S)S?Nl0`( U]Uol2mOOS?VvZlZe#^ڥ[ [\<8\ݘ!Q0ed4!P$Qbx -d4<9c=&I/|a& Y&>~oo?j9mh)V7!ږmQ6qBai@@ DPB >QD-^ĘQF= pi C,QJ\t3m¬8N=zSPEETRM"g̖4zS:q%R("A6l"lU\uśW^[!/,Fo舅#C*YfΝ=/pE)4H"eө?u WʄxzgUAB ͝?pQD?Vm<.sF?0QF Ozgo$ҁ6*d&Lx+ [\Cg<31Z e4wԢ? u^A) W>·*X;(x)%~I)6E*VQ/: }#D=1A\<@p B!;+|1L`AAiM@ 3 x&p}4]N,(5!$hi^R#A%~ i5a8 P^|.u X]&0A3  $GaEp`86 l=E:Plpxz M,?1\>7-~! nM$P)R, YL1H{.gQ04GŮE`ccߵ`$̣qYfFl ֲKო ^zUSpF4XPCOLU4΍cl,/A?nvYЉc!@)/#wCGk.B 'F un$`ZD?>T[C Dn ~p${ qxEhiʸVLlS⯾e;鱶?ks@}(l>p>Ί8 rTi qh%9^[lh&r4Bu71; AT@n {H@ b@Pj(2~memPg rxE``1vWTBgW&A Ȟl q{U )y1;0hB-`a b?n4 b/X<ʇ!`*ceX @b`d0Gp0u` dLx ,e; A1*3rs1xzc}H;mHU2^>VX7 i@L,j5 :S1',6[(1!1lbP-:A7x='Z4=PAfB"4!9}9xv<@x9 \[2vȄ/9@;4B73rK@z8TZV6D7KEVȃc--P) 8g`oHFj Ph10 kܕH`h Lȷqk.`|0q^@LPPG؀vԀGyԝCHtȅ~hLFnL؜wwІ?HmLvAQzp!X.0ڳ``yT wxSFiLء9t\ʉHȁTHuK+TSH0.P=n *"" H̎z|0ʆ(y8ȞLQXN[9pqnjL|R؀4 i  Qk ؀ 8 H܀`XȁTߺH XlϺ񗩩_a l͎ݴP\͍R0̓y rI*O!O PPNOjLd`/ ,!ԘP@P@J}gpNg8!u^KȬ('LR)2-{h5_-/^LX#S0-1%4]$3ER Lb"R&%B5THTXCxvHQ0@9Ժ7m M1 >Uq='SE* KUT-3>(3P?y^_ֻq @xR`ef wE %%;^ k рVSuokV־c+׎HWv wWhmRojW| zs%eœWDׇ׌hWm׆uX EMX8XXוXXw X%TXjؚ؜X؎}ؔeySV p͓ׄEc =bΏԘ v pPudAzד'5NڮURۉ׫ZS'-[IۑUٶ-xȊ (HF(xek@jqzІpMHoXцSp8K j ɳ ;P\e׵Ue]ݺհ eصZu2ܛ($b/x/y:5j&(~XXW@}E5}PB0#22[MM׀œݴ=u6^>^UTGȇ m5Y8&k*؂j8,p`Edhʔ eڂk96a&!uy;4؇rT <[A }H-Xѳt? xpHjPp !b%ͱ@{.b+ȱE-2Tk#e Y&d>Z>qZfNf=[]ABk<f(<G~H_d h'&,8ȅO:`8(T:) [Z H{`.b07,6ilcc@fe~‘]?Xei}f6Eoik<+0z0?+0_gsxxcӀ kh]m+, д0n \<[c7f =fB]k^ҽFޚ6lC[8!= i2 ABIBP9;y%ȂrPIQ0 e[v C2.@`+'v``>滆f].nlfn^W >N^;Mz9;!`(.(J0 ? a,"p·&;H!GpknS6n^hcf nZ0Zڠ8NcMפ?1OPrY`CnQՀۜOY\a rnnZ9e~ rBgdm%$- ]J. SM]S% D dW8=d=i&M,2'EOYt!NF7}rrJw֝:P'R7SWu rUxVYQhu1Gt \ ]눿Hheob/e8iA~a?6 ;H{q_XnZOT9єYjA(w>`ӁԁPwvwxw'v vp(P~xe1)Ywr뀃_H%9voόKtG/h `wr'CєDaz_&y&Fyp' yg?szy FZ|^`X_Sy@Z)"hTyPzyRaxDQ4љZ:qjqsZiG^]OE`1dahgika|O]d oYyIZQ}ЌG}r޿Y3ȔI GĿτجVA`>ZI'%gx),h0AdB Ё  .Mち` "4A "*pE6PP‰r JhL6U 9 XT!bC b$$HA petҭk.޼z/ n78@ȕ?Bl8Țwl1;sBtР|qNn5k|x ߸6xS9)*L4Cd 7BTctj)Sbǭz4U7B k svUvfPl%@%`Pw8IvݑcMF $uvXb7_}956buQȝw!48r25sxc)8pA3Iv94 @Ždg !W<")l>/ƘNW'} dIW=#T#]]O. ub9q)'50iucw#J;|L6+t Aq !`Cg 3;UP&HL H=1l2#co?!\^.}`.5F)I* <%|cUB -onl0@*8A+ sbC-Q@Y[tbuY lޓwn #V!X@ .PI3!ЀDKsՕF01c(۽^2 ',E^{ؠ |9a29M'X 8> %f75%| G%۶EM;].>! : w?mu!GX-``%C.2PA }/v R*/ u L\C˻ż5HGh@ XP!: {$#Uޞwu}`{hЄh nmtAI3< aHxԎ^Q(6”xq U8 q_NH>S ,@MJVtE}"("9AtWT 2e\·i@ƣ Rlўu NAiW4L|xI\"  .\0Dʃ-0KA L_q7&*+AZL)b ac<#FI0G6v|B CBCF?BL>89F,ACM5t@/z"]3PL>d9NPx>BlP<|ArAO5 I#=d L >8_n䀕UۥØ%ZZ^£^6A`faUKF$[T2@

F)^ǫPʋpaTbihjoi=ƚip)A?\(~.h yvCo |.|nN+YH@Y-U@XD4Ztk pHPh@0UƯb4B\F4EsklEE]KĄ@V|j4BEHXaZD >Ul@HJ諼TH"EO(n4M8¬DP,!%,H` h~,BVNm\,͚4LU,OI$6+24VXHL]@LΒĺD ,Hlђ ,>JL %,j .bTmJ8N@fLJE,؆@E~E&@=cV+/C_FJCAVoJ\ahνU_%`(/V*I>NoA]N|/oJV.\`_^HQo1^Ho^_ԙGN܀/V_0 aAo4K#smp-𵠰 ZC:#װ OK0C=hD 01D6Fd FO+c]ipW gDo1w11㹆` 11_kDZ 1`11q 1W 1g '"i'ZQ1.±J`rAXrJp'2!yr^b2*1+/-q-#o.P`rr%s0,1^)3]$3b]2"Ca7-wm]뿲!MP\-$--SkK+Y3[tlU@CP(<$:д[\EH-:HBWJ&5s3E_3,1LqMcM;3=[Lw2O+({34#PM7uN״7G y< (,S׳D(o" iW Ww*Q ܂-hcdɜxNJڇ*㉞pG`>15"=XnFF`dHF5 #< | z{idJFo`.vT*);KSlCPmmqK+55tovP׶qPKr[?B@`dHϘiX7vs\@30B B4a P񈔞%/@#(05[JjP - L@C >`A.i^Y{3#RIJ]lA:,A8 >i(V>`A,ƹa&)TE1Ŝczr#oguxl9y575qu 95Q6:932C9o0VIZu BK)@2 TA? 8ݕɕ[qc `igyhEeT dڦ[wQQvƂX0Ft x0+pC72,Erq 7* &S|7sL#8ɻ6LJ|RiP@A3 Vw픕;x-`ͅz{_fkL1џ=|;l@C!")Hi1ڣ;[%e=(YBZ|a;K񧏺%|[<4>+>:ʷ#/?'Q<\1 F3k%*R1@%.,"؀ag~7,=@kk H!O ((R荞 _G{hawaPS:$R"1P?8@h84aC >8"B Ըō;dcBAR$1ʏ"4 ˑ.qԹgO?:hQn"\ ~qH5Dnސ:!B˵?603BD`DxS8W`SO}VۋdU!;,%ISMGu%yZ4gكoi-裑NHZVb'LS肫?@j^nЂ_TH' ]2'ryfYpnu/?(=ZܠWA?ƉtMX`MȡtSs͋Zil_/xrX;l;'ǔ1^퍿_|gO_o__<4@z t!A NP(G3™HedUQBPVy4Bx ?,s fA= ;CLg) BD "1?t(-RbDZbF1 _&N^Nuc( & ~PfHE/dYİ doL#%-HMn#UIT CvtepIFSI&gL4!_La3z{#P_Ʋ=b,ɣ-մ5y W݄g8EN3Z\[48x6dIO`F60\Q(U.SLa'+ Q{D16|YCc%xhF9} C!EB`>oD8p^Ҁp\c Ĥ+}dzv`z@Na=A K.Ѕ0ZFIrs]R3#@urARXO Os9Nx(%YcPO8D{h3PFG~1^zX6&" 'Q@ADޑ28U騋o8:r>]MHx-wasϜ5qs$2isEϹGq(I:Go` >u TY&s7F\;X3J!}m;D *wGE2W^v'IeKó4&x#~t1pnD |YHѓF1T=5r!> :|_c*>S>i+?3(n# *j*K@!t!R'HR*PjR,B9T(|j6b:Dyز AME R4,9Y\Ec#.q1GSd(a@l.`Is!tD ˴.FD(@ G5 BQKMtD8˳ޤ(2Qt@5@bS1bؤk ؜QOdva54R1M4` ,8` m;:vr@T8`͂n  Fl2!g7?Vy@W#W۬  !#H4u[1TGM8@ ,Cjr . 1\K3Ƞ,lغ: &UE/ /4#A_MS55rݔ RC2 t* $Oa@!&` 2`L r HS#Z%cRdd!RD%"IV$'??iWV Wv3nt64^w h\@ L+7j-2  o-Joo+Nk0aqh*o0@+8COjdraB17C5B- 5JrdQE92,ET# *r@4hw#bjtV6)}WK) bwV@ RFy|fr@z{0y @ M%V-r|{WP~~[OC Imvz&1 Qb'J B  bJ22 I}&AD ? `ɶg&1m0cx#f9x} Cp[ JІS|T5j @T~pϞQaUk#8.}:@GOc$! XSX,xtF7'IL3?BYMgdg%4#cj jW~8Qgɟ3g@5`6H{z8l@ ,:<:a:G vMŮ3:zo8:F+4* D\'3Yg%>:eV>$ tI7h!̎`o櫽Zlxph`zyr-Z&ɪ5}zա!:nYZjZd;5bY nq a `@;+zPUڮ5#-Z ڡaAY\XM Hc`hl =q~n7€Z#`` `V +;dA&[f&O:":[EJj Dgj$^ jxi5|mej`JB3e#?;e0S@Ve_yS1Yeh弈$Rhf%IZ}:,c8M{xœUfˍ螣Nv| У'$S%$IB9};/}=bZN&"=Ae4QU==@CqMhp=>m] I7z-}؁T*}"Qרg֥ڭڱ=۵}Y0uq۹漝H}܍ܫV,H՝؝s=ދn{wV]@a ɝ>@~d~>~eNm'5^!E=%I>M^Q~W;>e~Jtry}>~艾>~陾>~ꩾ>~빾t8\(& jvllMy@' A>@Xp`ȕ'\1>I`ܥeM Zv!_$%+p bGjJ./`YF_`K`AD5Aq_ycPJl\"IT p?aN^~g;]"[A z \Au?9OBG3F)DEa… +P*D4 0 $*iFL$&2| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 3ATL`1!6<蒫Wb=@&)}X:ičTʸáZTR`@ .Ad+[9͜;{ :tR W 1Xȱ "6Uٴ'Lx]i ӂDBh=ܻ{>ziV Zw{vmiݣsGl>_)Zhҏ1JLcaNHa^!M\aL=AR[] 0Xy0 ,0W%"Yb ,bC .# Z9/$=%b],ӭ) oKo5Wˆ )`P @A *˯)LЀ"pЁT*`(l@;,4$?r. s̵Gk/CPsQ tBM48^FHtN? uLM5AZoA^ v-WMլrjvRm5hovvߍgp}Z x>SSE6 Ny[n?PG5NK妟:ۙotw 띿 Ɏ{{B.S ?y9{?/14~C}w)=L+ٟ߯~߭;W?#琻~M~_? tlx/}㨧=jp[G N}#:r < "T o>/!<pD7ƯqSjr"Jq;b grD\b! ,#! ,$yHt!#JpaÉcb kM!Ɨ0cʜI͛8sɳϟ@ Jѣ; \"҇J:š%#.4'L..]˶۷pʝK]( A$dHaB A_  m*T46a48) #V_Ө]C A q\s#B!Ffa+W'Rh@e6(Hp= gAA=<.aϿy&,Q~,O=ʸQJ* f*Ƞ *,P-#ʄX jN=ݼHH.(@R?( ΑK:. PWm*ХQSe>wTbL Up=exp#8E$RR)*p#6裐F*)~-e `9jdI?aAOҥ@**- #)&,@=ܓƿD0d.@$OA2x0$l(O* N&}B4/0A/tHU&WPuЮ 1+ܓP0ϬLxpH-T` $B3z@h`@JC[; P S ,Ws,`L >qp.הhSN&,j#y3Eذ <65 6@PGHKӛ|s-hF $ z  p6 M+7aԀm<0b|aq @ZIC WQQFW,x^V lٕ+T6Em{8î$St@0c I @@ԉc!@Xu%o:@ʀ׈bz"ჼ(T3QD4  ,̤&7\׶Rl EI{!rLJUv0 Zr% R͌{ZX|yca emJHc|@&A u `shKUE0P P؂ hIt$8Xm`#̨F7M" GJ#]D?Cpdyt1ʦk+a/\Ob2A43}Rc=| ^h&,sa o"+.n>170~y 0jЅ+`#6?` *@|wG"&0WfE+@ p hGKZI{r1z"JЉw0Xmk_,!up:1! Ï, h `- V{@?5]PP>L1!ھECl)1J-A*d* mS;XF2QD!\#b JKq1TL# t @ Tb&h@o8 MdRlPpf!Y{D@T: qbbRZvIG(6vc(6͕|cЦ=ҐZπP9=N RRhzrsi4Fp- 2AԨNIC'"{HQM ! M$֯M@cA. 2P3i$( V!$ɱ eg@ { 2tN`70XJ4F}n٪[d <]Va q7D^g87ppR@C .G<OWn2! R]*Z Ϡ<op7(5yws[YKFh`0\$%+)`A"V a/!@{G3yA deE91:p^u@MD7<xǣO}=Wl,,fA@K] ~ǻ=":5p!X8xxC13!  ZЂ4b`覾?wϿ9dk p9 xDq.a0qqc A_gv Pfp5˷0fT (g(_[QrƁc*u`wT / q``1x wy b pqSXW[؅zjrp&~p 0 9p x8!'z'$ 1$[_%<wph Q2!@" 0| [v@%_"s`=#A2$B[c uM|v @T @D@ ~pyЏ@ Аvi戎(Ƈ# 3|+ c0 f0_ɑY:x12pkAe0Q L"/]mT`\@Z7@Ó1XI/o0P0p Ap#*pr-ٲ-9=% C` Lh<`B@ ؎V@'^|i0 萏9AT )Yu9)IGwJ4 jyQQN$99D H7k@i!pTGuzu' v6c>86p1hpNhk Kp}.S6fhqYCZa.<  )`wp?@ ? \ &/ P@ p*kJʡ!6z2Y5Q9?*QuAA0v P љ`WN5^UyIw[uaJpk0$>QUz _8I`Lw)}0PF*`ޠM8ZzYpFJH:飾 HBQ|PeZ0(Bu@B@AfRKyq: @CP80 E0GK0 0 5MdM<`ɤ0Q` p /0! w" x W(o#{C)Pm} XP1{g65{X Y ;۫=s*Y`5AJX 9Sp K=H-rEWxVm5@p s0v00w/0gS`y |@NUuUYUQכ-a3` 9z D  |>p?P (: !<%|֕@?y `0a s +ƒ h ,J6L@s!&-{}֐1q|,,ӏ C)w]Ԓڪ8:-a}ۺ=2-;O(W-jh^nm2-2}2>c383تRcACS4Gk.G`LcS6g6?nSjNm㚮>nL;C|<u#̇rMw.~^~2 yCĭwT>D;CEtD-y^$F ^ þ^I$JtJJLh3KK-`a(Ҵ#لN f^&_Q!5R+5R*RT2ES69S)S?Nl0`( U]Uol2mOOS?VvZlZe#^ڥ[ [\<8\ݘ!Q0ed4!P$Qbx -d4<9c=&I/|a& Y&>~oo?j9mh)V7!ږmQ6qBai@@ DPB >QD-^ĘQF= pi C,QJ\t3m¬8N=zSPEETRM"g̖4zS:q%R("A6l"lU\uśW^[!/,Fo舅#C*YfΝ=/pE)4H"eө?u WʄxzgUAB ͝?pQD?Vm<.sF?0QF Ozgo$ҁ6*d&Lx+ [\Cg<31Z e4wԢ? u^A) W>·*X;(x)%~I)6E*VQ/: }#D=1A\<@p B!;+|1L`AAiM@ 3 x&p}4]N,(5!$hi^R#A%~ i5a8 P^|.u X]&0A3  $GaEp`86 l=E:Plpxz M,?1\>7-~! nM$P)R, YL1H{.gQ04GŮE`ccߵ`$̣qYfFl ֲKო ^zUSpF4XPCOLU4΍cl,/A?nvYЉc!@)/#wCGk.B 'F un$`ZD?>T[C Dn ~p${ qxEhiʸVLlS⯾e;鱶?ks@}(l>p>Ί8 rTi qh%9^[lh&r4Bu71; AT@n {H@ b@Pj(2~memPg rxE``1vWTBgW&A Ȟl q{U )y1;0hB-`a b?n4 b/X<ʇ!`*ceX @b`d0Gp0u` dLx ,e; A1*3rs1xzc}H;mHU2^>VX7 i@L,j5 :S1',6[(1!1lbP-:A7x='Z4=PAfB"4!9}9xv<@x9 \[2vȄ/9@;4B73rK@z8TZV6D7KEVȃc--P) 8g`oHFj Ph10 kܕH`h Lȷqk.`|0q^@LPPG؀vԀGyԝCHtȅ~hLFnL؜wwІ?HmLvAQzp!X.0ڳ``yT wxSFiLء9t\ʉHȁTHuK+TSH0.P=n *"" H̎z|0ʆ(y8ȞLQXN[9pqnjL|R؀4 i  Qk ؀ 8 H܀`XȁTߺH XlϺ񗩩_a l͎ݴP\͍R0̓y rI*O!O PPNOjLd`/ ,!ԘP@P@J}gpNg8!u^KȬ('LR)2-{h5_-/^LX#S0-1%4]$3ER Lb"R&%B5THTXCxvHQ0@9Ժ7m M1 >Uq='SE* KUT-3>(3P?y^_ֻq @xR`ef wE %%;^ k рVSuokV־c+׎HWv wWhmRojW| zs%eœWDׇ׌hWm׆uX EMX8XXוXXw X%TXjؚ؜X؎}ؔeySV p͓ׄEc =bΏԘ v pPudAzד'5NڮURۉ׫ZS'-[IۑUٶ-xȊ (HF(xek@jqzІpMHoXцSp8K j ɳ ;P\e׵Ue]ݺհ eصZu2ܛ($b/x/y:5j&(~XXW@}E5}PB0#22[MM׀œݴ=u6^>^UTGȇ m5Y8&k*؂j8,p`Edhʔ eڂk96a&!uy;4؇rT <[A }H-Xѳt? xpHjPp !b%ͱ@{.b+ȱE-2Tk#e Y&d>Z>qZfNf=[]ABk<f(<G~H_d h'&,8ȅO:`8(T:) [Z H{`.b07,6ilcc@fe~‘]?Xei}f6Eoik<+0z0?+0_gsxxcӀ kh]m+, д0n \<[c7f =fB]k^ҽFޚ6lC[8!= i2 ABIBP9;y%ȂrPIQ0 e[v C2.@`+'v``>滆f].nlfn^W >N^;Mz9;!`(.(J0 ? a,"p·&;H!GpknS6n^hcf nZ0Zڠ8NcMפ?1OPrY`CnQՀۜOY\a rnnZ9e~ rBgdm%$- ]J. SM]S% D dW8=d=i&M,2'EOYt!NF7}rrJw֝:P'R7SWu rUxVYQhu1Gt \ ]눿Hheob/e8iA~a?6 ;H{q_XnZOT9єYjA(w>`ӁԁPwvwxw'v vp(P~xe1)Ywr뀃_H%9voόKtG/h `wr'CєDaz_&y&Fyp' yg?szy FZ|^`X_Sy@Z)"hTyPzyRaxDQ4љZ:qjqsZiG^]OE`1dahgika|O]d oYyIZQ}ЌG}r޿Y3ȔI GĿτجVA`>ZI'%gx),h0AdB Ё  .Mち` "4A "*pE6PP‰r JhL6U 9 XT!bC b$$HA petҭk.޼z/ n78@ȕ?Bl8Țwl1;sBtР|qNn5k|x ߸6xS9)*L4Cd 7BTctj)Sbǭz4U7B k svUvfPl%@%`Pw8IvݑcMF $uvXb7_}956buQȝw!48r25sxc)8pA3Iv94 @Ždg !W<")l>/ƘNW'} dIW=#T#]]O. ub9q)'50iucw#J;|L6+t Aq !`Cg 3;UP&HL H=1l2#co?!\^.}`.5F)I* <%|cUB -onl0@*8A+ sbC-Q@Y[tbuY lޓwn #V!X@ .PI3!ЀDKsՕF01c(۽^2 ',E^{ؠ |9a29M'X 8> %f75%| G%۶EM;].>! : w?mu!GX-``%C.2PA }/v R*/ u L\C˻ż5HGh@ XP!: {$#Uޞwu}`{hЄh nmtAI3< aHxԎ^Q(6”xq U8 q_NH>S ,@MJVtE}"("9AtWT 2e\·i@ƣ Rlўu NAiW4L|xI\"  .\0Dʃ-0KA L_q7&*+AZL)b ac<#FI0G6v|B CBCF?BL>89F,ACM5t@/z"]3PL>d9NPx>BlP<|ArAO5 I#=d L >8_n䀕UۥØ%ZZ^£^6A`faUKF$[T2@

F)^ǫPʋpaTbihjoi=ƚip)A?\(~.h yvCo |.|nN+YH@Y-U@XD4Ztk pHPh@0UƯb4B\F4EsklEE]KĄ@V|j4BEHXaZD >Ul@HJ諼TH"EO(n4M8¬DP,!%,H` h~,BVNm\,͚4LU,OI$6+24VXHL]@LΒĺD ,Hlђ ,>JL %,j .bTmJ8N@fLJE,؆@E~E&@=cV+/C_FJCAVoJ\ahνU_%`(/V*I>NoA]N|/oJV.\`_^HQo1^Ho^_ԙGN܀/V_0 aAo4K#smp-𵠰 ZC:#װ OK0C=hD 01D6Fd FO+c]ipW gDo1w11㹆` 11_kDZ 1`11q 1W 1g '"i'ZQ1.±J`rAXrJp'2!yr^b2*1+/-q-#o.P`rr%s0,1^)3]$3b]2"Ca7-wm]뿲!MP\-$--SkK+Y3[tlU@CP(<$:д[\EH-:HBWJ&5s3E_3,1LqMcM;3=[Lw2O+({34#PM7uN״7G y< (,S׳D(o" iW Ww*Q ܂-hcdɜxNJڇ*㉞pG`>15"=XnFF`dHF5 #< | z{idJFo`.vT*);KSlCPmmqK+55tovP׶qPKr[?B@`dHϘiX7vs\@30B B4a P񈔞%/@#(05[JjP - L@C >`A.i^Y{3#RIJ]lA:,A8 >i(V>`A,ƹa&)TE1Ŝczr#oguxl9y575qu 95Q6:932C9o0VIZu BK)@2 TA? 8ݕɕ[qc `igyhEeT dڦ[wQQvƂX0Ft x0+pC72,Erq 7* &S|7sL#8ɻ6LJ|RiP@A3 Vw픕;x-`ͅz{_fkL1џ=|;l@C!")Hi1ڣ;[%e=(YBZ|a;K񧏺%|[<4>+>:ʷ#/?'Q<\1 F3k%*R1@%.,"؀ag~7,=@kk H!O ((R荞 _G{hawaPS:$R"1P?8@h84aC >8"B Ըō;dcBAR$1ʏ"4 ˑ.qԹgO?:hQn"\ ~qH5Dnސ:!B˵?603BD`DxS8W`SO}VۋdU!;,%ISMGu%yZ4gكoi-裑NHZVb'LS肫?@j^nЂ_TH' ]2'ryfYpnu/?(=ZܠWA?ƉtMX`MȡtSs͋Zil_/xrX;l;'ǔ1^퍿_|gO_o__<4@z t!A NP(G3™HedUQBPVy4Bx ?,s fA= ;CLg) BD "1?t(-RbDZbF1 _&N^Nuc( & ~PfHE/dYİ doL#%-HMn#UIT CvtepIFSI&gL4!_La3z{#P_Ʋ=b,ɣ-մ5y W݄g8EN3Z\[48x6dIO`F60\Q(U.SLa'+ Q{D16|YCc%xhF9} C!EB`>oD8p^Ҁp\c Ĥ+}dzv`z@Na=A K.Ѕ0ZFIrs]R3#@urARXO Os9Nx(%YcPO8D{h3PFG~1^zX6&" 'Q@ADޑ28U騋o8:r>]MHx-wasϜ5qs$2isEϹGq(I:Go` >u TY&s7F\;X3J!}m;D *wGE2W^v'IeKó4&x#~t1pnD |YHѓF1T=5r!> :|_c*>S>i+?3(n# *j*K@!t!R'HR*PjR,B9T(|j6b:Dyز AME R4,9Y\Ec#.q1GSd(a@l.`Is!tD ˴.FD(@ G5 BQKMtD8˳ޤ(2Qt@5@bS1bؤk ؜QOdva54R1M4` ,8` m;:vr@T8`͂n  Fl2!g7?Vy@W#W۬  !#H4u[1TGM8@ ,Cjr . 1\K3Ƞ,lغ: &UE/ /4#A_MS55rݔ RC2 t* $Oa#&` 2`L r HS#Z%cRd#RD%"IV$'??iWV Wv3np &e r#&(AJAA*֢6A鶢. 4j(6=<@0fJ,tq3q/BkoA(GU|*,RD5B7 Dt7Btʩ,wklu *vgu4! LktglV!u:D݄]h"w ^4 f'`p {Q%6} ӷ &$ 8`P~OgNM #j  t!^ (~&`X#1Dx#J89 T} Cp[  bQUXs'IXA*P#&Yy-ap tn7I VE!6щۧ  |4F@)x€EX9Ջ~7l%o疢'7]@Y$ `'8jWѕG)B_ r7@5{Y>yn>^ubF4ȹyHP4uD WJq*;0!}ўi}蟋ȟa3~g@5`6*{z8l0$:a#zG v7:zo8zF+4* D`Z'sY.g%B:iV>$ xڢI|X7Nh!̒`o&ZĚlxph`zyr1Z& 9Z%mY [j[J{5Y nq a `@;:/z2U5#-` ڡ𡆯F]`[kXM Hc`̢hlIAq~7~Z#`` `V +{ÛdAffS:"z[EJj DIכj^ j"Y&(gZ폿H#@Z4>x9|Aej`JBD3#C{e4SDVe_x˓5ۮYeh弈$Vm|Vf%M:;lsCQ{xܧUf\ˍh&0v <ѥ'$U<)]$IB=;3}=bZN&&}Ce4.UY}=D=W5UI%`d] ֍!]?}1sӡ}&ڱ=۵}۹۽{=H۟8ܯV,ROӽݍ]2}}޿2}q=:?w=v>Fa)i.EgC~^mCQTtX1?'eKw6y}>~艾>~陾>~ꩾ>~빾t8\(& fvllMy@' A>@:p`ȕ'\>bwI`eeM Zt!_$%+pB bGjJ./`Y⦉_`K`AD5q_ycPJl\"IT o?aNb~g;]"[A z \Au?9OBG3F)DEa… +P*D4 0 $*iFL$&2| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 3ATL`1!6<蒫Wb=@&)}X:ičTʸáZTR`@ .Ad+[9͜;{ :tR W 1Xȱ "6Uٴ'Lx]i ӂDBh=ܻ{>ziV Zw{vmiݣsGl>_)Zhҏ1JLcaNHa^!M\aL=AR[] 0Xy0 ,0W%"Yb ,bC .# Z9/$=%b],ӭ) oKo5Wˆ )`P @A *˯)LЀ"pЁT*`(l@;,4$?r. s̵Gk/CPsQ 4vTQF4S8^FL' =Q_uZod`jXSڎzj^״%H^\;ZA6qk4& 01|У \*X8Z<~q0մ UeGhS@YJp [p%7RPG Cu٩לCHH>Ѐ )YYX78`AE.c(aDFRV9SD V!iN< lR!N'u,=@`:Y (\9r,ZfY YD?9Ǒ0} AL: `609=oR0 +G"*m.7ʆS+<0`틀d<W7"pR`;= ?:n7" 7e%@oO(J?H$JͰ VĄ"،0' r0  $Xx_@ #n&@pIG'H"/#Hz$Q2%Ur%YJHi YIA2 CR$ n)Љ0#p  @g$&pEp$xH"&"*.R$257#;#$Ra/uy@yh薑ĔP wa brp)8OIH) &f긎׎8 \0?D{_pwP)#w3|]l#0t0c`NrlPcBD$!3q`'x'q))* Xױcs((2)` 5v0tU@s by)GPci}]'rB'y'y'e2(D1G J j *oɡt -P' =uT0.-p  @L-- !@6W r#Za+ ,w WwQ~Z^A&\́T\, P|AJLL*$j .b.I.@Uآ-/:b-EY2\.{0E@i:9.,a",-NjU,0,xZǒ,,-2) / f ء*7)5J&%I* Mw2Ѵ`1"0L P0'[Kغʭ 0[r2"eML3D d02##MZD(*I-Ԯѭ) D MdQ/$K&`î*ӛ /~eT0ᥴ 01 I311 Y еYpaeK;[tCd/!k WPS`M@ e?~?4h [3c3DacOe;øY[ cIn;+ `0 f&4#b@ ˻>#+_9JDf|Pf);k׋/#</+K{勾! ,#! ,#! ,#! ,z q5H "paB.@ x>G)>HЀƣ|2Q)41f<5ba+ÂDn"ӈ!8|ȡD!! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,#! ,#! ,#! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,#! ,#! ,#! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,#! ,#! ,#! ,z * 8Tῄ !*C flG|! ,#! ,#! ,#! ,#! ,#! ,#! ,z 5H*\ȰÇ#JL! ,#! ,#! ,x L1@H ! ň%^QƊ!~Hrdȑ$=~aE!^(hcÇ/F(F%r8"K6LY1K7Dr%Ö”ɐ&H{&IcOBqFԨϥH4ɒwr +X str: if pipxcmd: cmd = ["pipx", pipxcmd, "--help"] else: cmd = ["pipx", "--help"] helptext = ( subprocess.run(cmd, stdout=subprocess.PIPE, check=True) .stdout.decode() .replace(os.path.expanduser("~"), "~") ) return f""" ``` {" ".join(cmd)} {helptext} ``` """ params = { "usage": get_help(None), "runpip": get_help("runpip"), "install": get_help("install"), "upgrade": get_help("upgrade"), "upgradeall": get_help("upgrade-all"), "inject": get_help("inject"), "uninstall": get_help("uninstall"), "uninstallall": get_help("uninstall-all"), "reinstallall": get_help("reinstall-all"), "list": get_help("list"), "run": get_help("run"), "version": __version__, } warning = textwrap.dedent( """ """ ).lstrip() env = Environment(loader=FileSystemLoader("templates")) with open("docs/docs.md", "w") as f: f.write(warning) f.write(env.get_template("docs.md").render(**params)) f.write("\n") pipx-1.0.0/scripts/generate_man.py000066400000000000000000000016611416500503600171620ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os.path import textwrap from build_manpages.manpage import Manpage # type: ignore from pipx.main import get_command_parser def main(): parser = get_command_parser() parser.man_short_description = parser.description.splitlines()[1] manpage = Manpage(parser) body = str(manpage) # Avoid hardcoding build paths in manpages (and improve readability) body = body.replace(os.path.expanduser("~").replace("-", "\\-"), "~") # Add a credit section body += textwrap.dedent( """ .SH AUTHORS .IR pipx (1) was written by Chad Smith and contributors. The project can be found online at .UR https://pypa.github.io/pipx/ .UE .SH SEE ALSO .IR pip (1), .IR virtualenv (1) """ ) with open("pipx.1", "w") as f: f.write(body) if __name__ == "__main__": main() pipx-1.0.0/scripts/list_test_packages.py000066400000000000000000000131561416500503600204070ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import os import re import subprocess import sys import tempfile from pathlib import Path from typing import Any, Dict, List from test_packages_support import get_platform_list_path def process_command_line(argv: List[str]) -> argparse.Namespace: """Process command line invocation arguments and switches. Args: argv: list of arguments, or `None` from ``sys.argv[1:]``. Returns: argparse.Namespace: named attributes of arguments and switches """ # script_name = argv[0] argv = argv[1:] # initialize the parser object: parser = argparse.ArgumentParser( description="Create list of needed test packages for pipx tests and local pypiserver." ) # specifying nargs= puts outputs of parser in list (even if nargs=1) # required arguments parser.add_argument( "primary_package_list", help="Main packages to examine, getting list of " "matching distribution files and dependencies.", ) parser.add_argument( "package_list_dir", help="Directory to output package distribution lists." ) # switches/options: parser.add_argument( "-v", "--verbose", action="store_true", help="Maximum verbosity, especially for pip operations.", ) args = parser.parse_args(argv) return args def parse_package_list(package_list_file: Path) -> List[Dict[str, Any]]: output_list: List[Dict[str, Any]] = [] try: with package_list_file.open("r") as package_list_fh: for line in package_list_fh: line_parsed = re.sub(r"#.+$", "", line) if not re.search(r"\S", line_parsed): continue line_list = line_parsed.strip().split() if len(line_list) == 1: output_list.append({"spec": line_list[0]}) elif len(line_list) == 2: output_list.append( { "spec": line_list[0], "no-deps": line_list[1].lower() == "true", } ) else: print( f"ERROR: Unable to parse primary package list line:\n {line.strip()}" ) return [] except IOError: print("ERROR: File problem reading primary package list.") return [] return output_list def create_test_packages_list( package_list_dir_path: Path, primary_package_list_path: Path, verbose: bool ) -> int: exit_code = 0 package_list_dir_path.mkdir(exist_ok=True) platform_package_list_path = get_platform_list_path(package_list_dir_path) primary_test_packages = parse_package_list(primary_package_list_path) if not primary_test_packages: print( f"ERROR: Problem reading {primary_package_list_path}. Exiting.", file=sys.stderr, ) return 1 with tempfile.TemporaryDirectory() as download_dir: for test_package in primary_test_packages: test_package_option_string = ( " (no-deps)" if test_package.get("no-deps", False) else "" ) verbose_this_iteration = False cmd_list = ( ["pip", "download"] + (["--no-deps"] if test_package.get("no-deps", False) else []) + [test_package["spec"], "-d", str(download_dir)] ) if verbose: print(f"CMD: {' '.join(cmd_list)}") pip_download_process = subprocess.run( cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) if pip_download_process.returncode == 0: print(f"Examined {test_package['spec']}{test_package_option_string}") else: print( f"ERROR with {test_package['spec']}{test_package_option_string}", file=sys.stderr, ) verbose_this_iteration = True exit_code = 1 if verbose or verbose_this_iteration: print(pip_download_process.stdout) print(pip_download_process.stderr) downloaded_list = os.listdir(download_dir) all_packages = [] for downloaded_filename in downloaded_list: wheel_re = re.search( r"(.+)\-([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)\.whl$", downloaded_filename ) src_re = re.search(r"(.+)\-([^-]+)\.(?:tar.gz|zip)$", downloaded_filename) if wheel_re: package_name = wheel_re.group(1) package_version = wheel_re.group(2) elif src_re: package_name = src_re.group(1) package_version = src_re.group(2) else: print(f"ERROR: cannot parse: {downloaded_filename}", file=sys.stderr) continue all_packages.append(f"{package_name}=={package_version}") with platform_package_list_path.open("w") as package_list_fh: "scripts/list_test_packages.py", for package in sorted(all_packages): print(package, file=package_list_fh) return exit_code def main(argv: List[str]) -> int: args = process_command_line(argv) return create_test_packages_list( Path(args.package_list_dir), Path(args.primary_package_list), args.verbose ) if __name__ == "__main__": try: status = main(sys.argv) except KeyboardInterrupt: print("Stopped by Keyboard Interrupt", file=sys.stderr) status = 130 sys.exit(status) pipx-1.0.0/scripts/migrate_pipsi_to_pipx.py000066400000000000000000000037131416500503600211330ustar00rootroot00000000000000#!/usr/bin/env python3 """ Script to migrate from pipsi to pipx """ import os import subprocess import sys from pathlib import Path from shutil import which def main(): if not which("pipx"): sys.exit("pipx must be installed to migrate from pipsi to pipx") if not sys.stdout.isatty(): sys.exit("Must be run from a terminal, not a script") pipsi_home = os.environ.get("PIPSI_HOME", os.path.expanduser("~/.local/venvs/")) packages = [p.name for p in Path(pipsi_home).iterdir()] if not packages: print("No packages installed with pipsi") sys.exit(0) print("Attempting to migrate the following packages from pipsi to pipx:") for package in packages: print(f" - {package}") answer = None while answer not in ["y", "n"]: answer = input("Continue? [y/n] ") if answer == "n": sys.exit(0) error = False for package in packages: ret = subprocess.run(["pipsi", "uninstall", "--yes", package]) if ret.returncode: error = True print( f"Failed to uninstall {package!r} with pipsi. " "Not attempting to install with pipx." ) else: print( f"uninstalled {package!r} with pipsi. Now attempting to install with pipx." ) ret = subprocess.run(["pipx", "install", package]) if ret.returncode: error = True print(f"Failed to install {package!r} with pipx.") else: print(f"Successfully installed {package} with pipx") print(f"Done migrating {len(packages)} packages!") print( "You may still need to run `pipsi uninstall pipsi` or `pip uninstall pipsi`. " "Refer to pipsi's documentation." ) if error: print( "Note: Finished with errors. Review output to manually complete migration." ) if __name__ == "__main__": main() pipx-1.0.0/scripts/pipx_postrelease.py000066400000000000000000000044661416500503600201310ustar00rootroot00000000000000import re import sys from pathlib import Path from typing import List from pipx_release import copy_file_replace_line, python_mypy_ok def fix_version_py(current_version_list: List[str]) -> bool: version_code_file = Path("src/pipx/version.py") new_version_code_file = Path("src/pipx/version.py.new") # Version with "dev0" suffix precedes version without "dev0" suffix, # so to follow previous version we must add to version number before # appending "dev0". new_version_list = current_version_list + ["1", '"dev0"'] copy_file_replace_line( version_code_file, new_version_code_file, line_re=r"^\s*__version_info__\s*=", new_line=f'__version_info__ = ({", ".join(new_version_list)})', ) if python_mypy_ok(new_version_code_file): new_version_code_file.rename(version_code_file) return True else: print(f"Aborting: syntax error in {new_version_code_file}") return False def fix_changelog() -> bool: changelog_file = Path("docs/changelog.md") new_changelog_file = Path("docs/changelog.new") old_version_fh = changelog_file.open("r") new_version_fh = new_changelog_file.open("w") new_version_fh.write("dev\n\n\n") for line in old_version_fh: new_version_fh.write(line) old_version_fh.close() new_version_fh.close() new_changelog_file.rename(changelog_file) return True def get_current_version() -> List[str]: version_code_file = Path("src/pipx/version.py") version_fh = version_code_file.open("r") version = None for line in version_fh: version_re = re.search(r"^\s*__version_info__\s*=\s*\(([^)]+)\)", line) if version_re: version = version_re.group(1) if version is not None: return version.split(", ") else: return [] def post_release() -> int: current_version_list = get_current_version() if not current_version_list: return 1 if fix_version_py(current_version_list) and fix_changelog(): return 0 else: return 1 def main(argv: List[str]) -> int: return post_release() if __name__ == "__main__": try: status = main(sys.argv) except KeyboardInterrupt: print("Stopped by Keyboard Interrupt", file=sys.stderr) status = 130 sys.exit(status) pipx-1.0.0/scripts/pipx_prerelease.py000066400000000000000000000031521416500503600177210ustar00rootroot00000000000000import sys from pathlib import Path from typing import List from pipx_release import copy_file_replace_line, python_mypy_ok def fix_version_py(new_version: str) -> bool: version_code_file = Path("src/pipx/version.py") new_version_code_file = Path("src/pipx/version.py.new") new_version_list = new_version.split(".") copy_file_replace_line( version_code_file, new_version_code_file, line_re=r"^\s*__version_info__\s*=", new_line=f'__version_info__ = ({", ".join(new_version_list)})', ) if python_mypy_ok(new_version_code_file): new_version_code_file.rename(version_code_file) return True else: print(f"Aborting: syntax error in {new_version_code_file}") return False def fix_changelog(new_version: str) -> bool: changelog_file = Path("docs/changelog.md") new_changelog_file = Path("docs/changelog.new") copy_file_replace_line( changelog_file, new_changelog_file, line_re=r"^\s*dev\s*$", new_line=new_version ) new_changelog_file.rename(changelog_file) return True def pre_release(new_version: str) -> int: if fix_version_py(new_version) and fix_changelog(new_version): return 0 else: return 1 def main(argv: List[str]) -> int: if len(argv) > 1: new_version = argv[1] else: new_version = input("Enter new version: ") return pre_release(new_version) if __name__ == "__main__": try: status = main(sys.argv) except KeyboardInterrupt: print("Stopped by Keyboard Interrupt", file=sys.stderr) status = 130 sys.exit(status) pipx-1.0.0/scripts/pipx_release.py000066400000000000000000000011531416500503600172110ustar00rootroot00000000000000import re import subprocess from pathlib import Path def python_mypy_ok(filepath: Path) -> bool: mypy_proc = subprocess.run(["mypy", filepath]) return True if mypy_proc.returncode == 0 else False def copy_file_replace_line( orig_file: Path, new_file: Path, line_re: str, new_line: str ) -> None: old_version_fh = orig_file.open("r") new_version_fh = new_file.open("w") for line in old_version_fh: if re.search(line_re, line): new_version_fh.write(new_line + "\n") else: new_version_fh.write(line) old_version_fh.close() new_version_fh.close() pipx-1.0.0/scripts/test_packages_support.py000066400000000000000000000013521416500503600211430ustar00rootroot00000000000000import platform import sys from pathlib import Path PYTHON_VERSION_STR = f"{sys.version_info[0]}.{sys.version_info[1]}" # Platform logic if sys.platform == "darwin": FULL_PLATFORM = "macos" + platform.release().split(".")[0] elif sys.platform == "win32": FULL_PLATFORM = "win" else: FULL_PLATFORM = "unix" def get_platform_list_path(package_list_dir_path: Path) -> Path: platform_package_list_path = ( package_list_dir_path / f"{FULL_PLATFORM}-python{PYTHON_VERSION_STR}.txt" ) return platform_package_list_path def get_platform_packages_dir_path(pipx_package_cache_path: Path) -> Path: platform_packages_dir_path = pipx_package_cache_path / f"{PYTHON_VERSION_STR}" return platform_packages_dir_path pipx-1.0.0/scripts/update_package_cache.py000066400000000000000000000146271416500503600206230ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import re import subprocess import sys from pathlib import Path from typing import List from list_test_packages import create_test_packages_list from test_packages_support import get_platform_list_path, get_platform_packages_dir_path def process_command_line(argv: List[str]) -> argparse.Namespace: """Process command line invocation arguments and switches. Args: argv: list of arguments, or `None` from ``sys.argv[1:]``. Returns: argparse.Namespace: named attributes of arguments and switches """ # script_name = argv[0] argv = argv[1:] # initialize the parser object: parser = argparse.ArgumentParser( description="Check and update as needed the pipx tests package cache " "for use with the pipx tests local pypiserver." ) # specifying nargs= puts outputs of parser in list (even if nargs=1) # required arguments parser.add_argument( "package_list_dir", help="Directory where platform- and python-specific package lists are found for pipx tests.", ) parser.add_argument( "pipx_package_cache_dir", help="Directory to store the packages distribution files.", ) # switches/options: parser.add_argument( "-c", "--check-only", action="store_true", help="Only check to see if needed packages are in PACKAGES_DIR, do not " "download or delete files.", ) args = parser.parse_args(argv) return args def update_test_packages_cache( package_list_dir_path: Path, pipx_package_cache_path: Path, check_only: bool ) -> int: exit_code = 0 platform_package_list_path = get_platform_list_path(package_list_dir_path) packages_dir_path = get_platform_packages_dir_path(pipx_package_cache_path) packages_dir_path.mkdir(exist_ok=True) packages_dir_files = list(packages_dir_path.iterdir()) if not platform_package_list_path.exists(): print( f"WARNING. File {str(platform_package_list_path)}\n" " does not exist. Creating now...", file=sys.stderr, ) create_list_returncode = create_test_packages_list( package_list_dir_path, package_list_dir_path / "primary_packages.txt", verbose=False, ) if create_list_returncode == 0: print( f"File {str(platform_package_list_path)}\n" " successfully created. Please check this file in to the" " repository for future use.", file=sys.stderr, ) else: print( f"ERROR. Unable to create {str(platform_package_list_path)}\n" " Cannot continue.\n", file=sys.stderr, ) return 1 try: platform_package_list_fh = platform_package_list_path.open("r") except IOError: print( f"ERROR. File {str(platform_package_list_path)}\n" " is not readable. Cannot continue.\n", file=sys.stderr, ) return 1 else: platform_package_list_fh.close() print("Using the following file to specify needed package files:") print(f" {str(platform_package_list_path)}") print("Ensuring the following directory contains necessary package files:") print(f" {str(packages_dir_path)}") packages_dir_hits = [] packages_dir_missing = [] with platform_package_list_path.open("r") as platform_package_list_fh: for line in platform_package_list_fh: package_spec = line.strip() package_spec_re = re.search(r"^(.+)==(.+)$", package_spec) if not package_spec_re: print(f"ERROR: CANNOT PARSE {package_spec}", file=sys.stderr) exit_code = 1 continue package_name = package_spec_re.group(1) package_ver = package_spec_re.group(2) package_dist_patt = ( re.escape(package_name) + r"-" + re.escape(package_ver) + r"(.tar.gz|.zip|-)" ) matches = [] for output_dir_file in packages_dir_files: if re.search(package_dist_patt, output_dir_file.name): matches.append(output_dir_file) if len(matches) == 1: packages_dir_files.remove(matches[0]) packages_dir_hits.append(matches[0]) continue elif len(matches) > 1: print("ERROR: more than one match for {package_spec}.", file=sys.stderr) print(f" {matches}", file=sys.stderr) exit_code = 1 continue packages_dir_missing.append(package_spec) print(f"MISSING FILES: {len(packages_dir_missing)}") print(f"EXISTING (found) FILES: {len(packages_dir_hits)}") print(f"LEFTOVER (unused) FILES: {len(packages_dir_files)}") if check_only: return 0 if len(packages_dir_missing) == 0 else 1 else: for package_spec in packages_dir_missing: pip_download_process = subprocess.run( [ "pip", "download", "--no-deps", package_spec, "-d", str(packages_dir_path), ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) if pip_download_process.returncode == 0: print(f"Successfully downloaded {package_spec}") else: print(f"ERROR downloading {package_spec}", file=sys.stderr) print(pip_download_process.stdout, file=sys.stderr) print(pip_download_process.stderr, file=sys.stderr) exit_code = 1 for unused_file in packages_dir_files: print(f"Deleting {unused_file}...") unused_file.unlink() return exit_code def main(argv: List[str]) -> int: args = process_command_line(argv) return update_test_packages_cache( Path(args.package_list_dir), Path(args.pipx_package_cache_dir), args.check_only ) if __name__ == "__main__": try: status = main(sys.argv) except KeyboardInterrupt: print("Stopped by Keyboard Interrupt", file=sys.stderr) status = 130 sys.exit(status) pipx-1.0.0/setup.cfg000066400000000000000000000025051416500503600143130ustar00rootroot00000000000000[metadata] name = pipx version = attr: pipx.version.__version__ author = Chad Smith author_email = Chad Smith description = Install and Run Python Applications in Isolated Environments long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/pypa/pipx project_urls = Documentation = https://pypa.github.io/pipx/ Source Code = https://github.com/pypa/pipx Bug Tracker = https://github.com/pypa/pipx/issues keywords = pip, install, cli, workflow, Virtual Environment license = License :: OSI Approved :: MIT License classifiers = Operating System :: OS Independent License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3 :: Only [options] packages = find: package_dir = = src include_package_data = true zip_safe = true python_requires = >=3.6 install_requires = colorama>=0.4.4;sys_platform=="win32" userpath>=1.6.0 argcomplete>=1.9.4 packaging>=20.0 importlib-metadata>=3.3.0; python_version < '3.8' [options.packages.find] where = src [options.entry_points] console_scripts = pipx = pipx.main:cli pipx-1.0.0/setup.py000077500000000000000000000004211416500503600142020ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from setuptools import setup # type: ignore if sys.version_info < (3, 6, 0): sys.exit( "Python 3.6 or later is required. " "See https://github.com/pypa/pipx " "for installation instructions." ) setup() pipx-1.0.0/src/000077500000000000000000000000001416500503600132575ustar00rootroot00000000000000pipx-1.0.0/src/pipx/000077500000000000000000000000001416500503600142375ustar00rootroot00000000000000pipx-1.0.0/src/pipx/__init__.py000066400000000000000000000003021416500503600163430ustar00rootroot00000000000000import sys if sys.version_info < (3, 6, 0): sys.exit( "Python 3.6 or later is required. " "See https://github.com/pypa/pipx " "for installation instructions." ) pipx-1.0.0/src/pipx/__main__.py000066400000000000000000000006071416500503600163340ustar00rootroot00000000000000import os import sys if not __package__: # Running from source. Add pipx's source code to the system # path to allow direct invocation, such as: # python src/pipx --help pipx_package_source_path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, pipx_package_source_path) from pipx.main import cli # noqa if __name__ == "__main__": sys.exit(cli()) pipx-1.0.0/src/pipx/animate.py000066400000000000000000000072261416500503600162360ustar00rootroot00000000000000import shutil import sys from contextlib import contextmanager from threading import Event, Thread from typing import Generator, List from pipx.constants import WINDOWS from pipx.emojis import EMOJI_SUPPORT stderr_is_tty = sys.stderr.isatty() CLEAR_LINE = "\033[K" EMOJI_ANIMATION_FRAMES = ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"] NONEMOJI_ANIMATION_FRAMES = ["", ".", "..", "..."] EMOJI_FRAME_PERIOD = 0.1 NONEMOJI_FRAME_PERIOD = 1 MINIMUM_COLS_ALLOW_ANIMATION = 16 if WINDOWS: import ctypes class _CursorInfo(ctypes.Structure): _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] def _env_supports_animation() -> bool: (term_cols, _) = shutil.get_terminal_size(fallback=(0, 0)) return stderr_is_tty and term_cols > MINIMUM_COLS_ALLOW_ANIMATION @contextmanager def animate( message: str, do_animation: bool, *, delay: float = 0 ) -> Generator[None, None, None]: if not do_animation or not _env_supports_animation(): # No animation, just a single print of message sys.stderr.write(f"{message}...\n") yield return event = Event() if EMOJI_SUPPORT: animate_at_beginning_of_line = True symbols = EMOJI_ANIMATION_FRAMES period = EMOJI_FRAME_PERIOD else: animate_at_beginning_of_line = False symbols = NONEMOJI_ANIMATION_FRAMES period = NONEMOJI_FRAME_PERIOD thread_kwargs = { "message": message, "event": event, "symbols": symbols, "delay": delay, "period": period, "animate_at_beginning_of_line": animate_at_beginning_of_line, } t = Thread(target=print_animation, kwargs=thread_kwargs) t.start() try: yield finally: event.set() clear_line() def print_animation( *, message: str, event: Event, symbols: List[str], delay: float, period: float, animate_at_beginning_of_line: bool, ) -> None: (term_cols, _) = shutil.get_terminal_size(fallback=(9999, 24)) event.wait(delay) while not event.wait(0): for s in symbols: if animate_at_beginning_of_line: max_message_len = term_cols - len(f"{s} ... ") cur_line = f"{s} {message:.{max_message_len}}" if len(message) > max_message_len: cur_line += "..." else: max_message_len = term_cols - len("... ") cur_line = f"{message:.{max_message_len}}{s}" clear_line() sys.stderr.write(cur_line) sys.stderr.flush() if event.wait(period): break # for Windows pre-ANSI-terminal-support (before Windows 10 TH2 (v1511)) # https://stackoverflow.com/a/10455937 def win_cursor(visible: bool) -> None: ci = _CursorInfo() handle = ctypes.windll.kernel32.GetStdHandle(-11) # type: ignore[attr-defined] ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) # type: ignore[attr-defined] ci.visible = visible ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) # type: ignore[attr-defined] def hide_cursor() -> None: if stderr_is_tty: if WINDOWS: win_cursor(visible=False) else: sys.stderr.write("\033[?25l") sys.stderr.flush() def show_cursor() -> None: if stderr_is_tty: if WINDOWS: win_cursor(visible=True) else: sys.stderr.write("\033[?25h") sys.stderr.flush() def clear_line() -> None: """Clears current line and positions cursor at start of line""" sys.stderr.write(f"\r{CLEAR_LINE}") sys.stdout.write(f"\r{CLEAR_LINE}") pipx-1.0.0/src/pipx/colors.py000066400000000000000000000014511416500503600161130ustar00rootroot00000000000000import sys from typing import Callable try: import colorama # type: ignore except ImportError: # Colorama is Windows only package colorama = None PRINT_COLOR = sys.stdout.isatty() if PRINT_COLOR and colorama: colorama.init() class c: header = "\033[95m" blue = "\033[94m" green = "\033[92m" yellow = "\033[93m" red = "\033[91m" bold = "\033[1m" cyan = "\033[96m" underline = "\033[4m" end = "\033[0m" def mkcolorfunc(style: str) -> Callable[[str], str]: def stylize_text(x: str) -> str: if PRINT_COLOR: return f"{style}{x}{c.end}" else: return x return stylize_text bold = mkcolorfunc(c.bold) red = mkcolorfunc(c.red) blue = mkcolorfunc(c.cyan) cyan = mkcolorfunc(c.blue) green = mkcolorfunc(c.green) pipx-1.0.0/src/pipx/commands/000077500000000000000000000000001416500503600160405ustar00rootroot00000000000000pipx-1.0.0/src/pipx/commands/__init__.py000066400000000000000000000012371416500503600201540ustar00rootroot00000000000000from pipx.commands.ensure_path import ensure_pipx_paths from pipx.commands.inject import inject from pipx.commands.install import install from pipx.commands.list_packages import list_packages from pipx.commands.reinstall import reinstall, reinstall_all from pipx.commands.run import run from pipx.commands.run_pip import run_pip from pipx.commands.uninstall import uninstall, uninstall_all from pipx.commands.upgrade import upgrade, upgrade_all __all__ = [ "upgrade", "upgrade_all", "run", "install", "inject", "uninstall", "uninstall_all", "reinstall", "reinstall_all", "list_packages", "run_pip", "ensure_pipx_paths", ] pipx-1.0.0/src/pipx/commands/common.py000066400000000000000000000344751416500503600177170ustar00rootroot00000000000000import logging import shlex import shutil import sys import tempfile import time from pathlib import Path from shutil import which from tempfile import TemporaryDirectory from typing import Dict, List, Optional, Set, Tuple import userpath # type: ignore from packaging.utils import canonicalize_name from pipx import constants from pipx.colors import bold, red from pipx.constants import WINDOWS from pipx.emojis import hazard, stars from pipx.package_specifier import parse_specifier_for_install, valid_pypi_name from pipx.pipx_metadata_file import PackageInfo from pipx.util import PipxError, mkdir, pipx_wrap, rmdir, safe_unlink from pipx.venv import Venv logger = logging.getLogger(__name__) class VenvProblems: def __init__( self, bad_venv_name: bool = False, invalid_interpreter: bool = False, missing_metadata: bool = False, not_installed: bool = False, ) -> None: self.bad_venv_name = bad_venv_name self.invalid_interpreter = invalid_interpreter self.missing_metadata = missing_metadata self.not_installed = not_installed def any_(self) -> bool: return any(self.__dict__.values()) def or_(self, venv_problems: "VenvProblems") -> None: for attribute in self.__dict__: setattr( self, attribute, getattr(self, attribute) or getattr(venv_problems, attribute), ) def expose_apps_globally( local_bin_dir: Path, app_paths: List[Path], *, force: bool, suffix: str = "" ) -> None: if not can_symlink(local_bin_dir): _copy_package_apps(local_bin_dir, app_paths, suffix=suffix) else: _symlink_package_apps(local_bin_dir, app_paths, force=force, suffix=suffix) _can_symlink_cache: Dict[Path, bool] = {} def can_symlink(local_bin_dir: Path) -> bool: if not WINDOWS: # Technically, even on Unix this depends on the filesystem return True if local_bin_dir not in _can_symlink_cache: with TemporaryDirectory(dir=local_bin_dir) as d: p = Path(d) target = p / "a" target.touch() lnk = p / "b" try: lnk.symlink_to(target) _can_symlink_cache[local_bin_dir] = True except (OSError, NotImplementedError): _can_symlink_cache[local_bin_dir] = False return _can_symlink_cache[local_bin_dir] def _copy_package_apps( local_bin_dir: Path, app_paths: List[Path], suffix: str = "" ) -> None: for src_unresolved in app_paths: src = src_unresolved.resolve() app = src.name dest = Path(local_bin_dir / add_suffix(app, suffix)) if not dest.parent.is_dir(): mkdir(dest.parent) if dest.exists(): logger.warning(f"{hazard} Overwriting file {str(dest)} with {str(src)}") safe_unlink(dest) if src.exists(): shutil.copy(src, dest) def _symlink_package_apps( local_bin_dir: Path, app_paths: List[Path], *, force: bool, suffix: str = "" ) -> None: for app_path in app_paths: app_name = app_path.name app_name_suffixed = add_suffix(app_name, suffix) symlink_path = Path(local_bin_dir / app_name_suffixed) if not symlink_path.parent.is_dir(): mkdir(symlink_path.parent) if force: logger.info(f"Force is true. Removing {str(symlink_path)}.") try: symlink_path.unlink() except FileNotFoundError: pass except IsADirectoryError: rmdir(symlink_path) exists = symlink_path.exists() is_symlink = symlink_path.is_symlink() if exists: if symlink_path.samefile(app_path): logger.info(f"Same path {str(symlink_path)} and {str(app_path)}") else: logger.warning( pipx_wrap( f""" {hazard} File exists at {str(symlink_path)} and points to {symlink_path.resolve()}, not {str(app_path)}. Not modifying. """, subsequent_indent=" " * 4, ) ) continue if is_symlink and not exists: logger.info( f"Removing existing symlink {str(symlink_path)} since it " "pointed non-existent location" ) symlink_path.unlink() existing_executable_on_path = which(app_name_suffixed) symlink_path.symlink_to(app_path) if existing_executable_on_path: logger.warning( pipx_wrap( f""" {hazard} Note: {app_name_suffixed} was already on your PATH at {existing_executable_on_path} """, subsequent_indent=" " * 4, ) ) def venv_health_check( venv: Venv, package_name: Optional[str] = None ) -> Tuple[VenvProblems, str]: venv_dir = venv.root python_path = venv.python_path.resolve() if package_name is None: package_name = venv.main_package_name if not python_path.is_file(): return ( VenvProblems(invalid_interpreter=True), f" package {red(bold(venv_dir.name))} has invalid " f"interpreter {str(python_path)}\r{hazard}", ) if not venv.package_metadata: return ( VenvProblems(missing_metadata=True), f" package {red(bold(venv_dir.name))} has missing " f"internal pipx metadata.\r{hazard}", ) if venv_dir.name != canonicalize_name(venv_dir.name): return ( VenvProblems(bad_venv_name=True), f" package {red(bold(venv_dir.name))} needs its " f"internal data updated.\r{hazard}", ) if venv.package_metadata[package_name].package_version == "": return ( VenvProblems(not_installed=True), f" package {red(bold(package_name))} {red('is not installed')} " f"in the venv {venv_dir.name}\r{hazard}", ) return (VenvProblems(), "") def get_venv_summary( venv_dir: Path, *, package_name: Optional[str] = None, new_install: bool = False, include_injected: bool = False, ) -> Tuple[str, VenvProblems]: venv = Venv(venv_dir) if package_name is None: package_name = venv.main_package_name (venv_problems, warning_message) = venv_health_check(venv, package_name) if venv_problems.any_(): return (warning_message, venv_problems) package_metadata = venv.package_metadata[package_name] apps = package_metadata.apps if package_metadata.include_dependencies: apps += package_metadata.apps_of_dependencies exposed_app_paths = get_exposed_app_paths_for_package( venv.bin_path, constants.LOCAL_BIN_DIR, [add_suffix(app, package_metadata.suffix) for app in apps], ) exposed_binary_names = sorted(p.name for p in exposed_app_paths) unavailable_binary_names = sorted( {add_suffix(name, package_metadata.suffix) for name in package_metadata.apps} - set(exposed_binary_names) ) # The following is to satisfy mypy that python_version is str and not # Optional[str] python_version = ( venv.pipx_metadata.python_version if venv.pipx_metadata.python_version is not None else "" ) return ( _get_list_output( python_version, package_metadata.package_version, package_name, new_install, exposed_binary_names, unavailable_binary_names, venv.pipx_metadata.injected_packages if include_injected else None, suffix=package_metadata.suffix, ), venv_problems, ) def get_exposed_app_paths_for_package( venv_bin_path: Path, local_bin_dir: Path, package_binary_names: Optional[List[str]] = None, ) -> Set[Path]: # package_binary_names is used only if local_bin_path cannot use symlinks. # It is necessary for non-symlink systems to return valid app_paths. if package_binary_names is None: package_binary_names = [] bin_symlinks = set() for b in local_bin_dir.iterdir(): try: # sometimes symlinks can resolve to a file of a different name # (in the case of ansible for example) so checking the resolved paths # is not a reliable way to determine if the symlink exists. # We always use the stricter check on non-Windows systems. On # Windows, we use a less strict check if we don't have a symlink. is_same_file = False if can_symlink(local_bin_dir) and b.is_symlink(): is_same_file = b.resolve().parent.samefile(venv_bin_path) elif not can_symlink(local_bin_dir): is_same_file = b.name in package_binary_names if is_same_file: bin_symlinks.add(b) except FileNotFoundError: pass return bin_symlinks def _get_list_output( python_version: str, package_version: str, package_name: str, new_install: bool, exposed_binary_names: List[str], unavailable_binary_names: List[str], injected_packages: Optional[Dict[str, PackageInfo]] = None, suffix: str = "", ) -> str: output = [] suffix = f" ({bold(shlex.quote(package_name + suffix))})" if suffix else "" output.append( f" {'installed' if new_install else ''} package {bold(shlex.quote(package_name))}" f" {bold(package_version)}{suffix}, installed using {python_version}" ) if new_install and exposed_binary_names: output.append(" These apps are now globally available") for name in exposed_binary_names: output.append(f" - {name}") for name in unavailable_binary_names: output.append( f" - {red(name)} (symlink missing or pointing to unexpected location)" ) if injected_packages: output.append(" Injected Packages:") for name in injected_packages: output.append(f" - {name} {injected_packages[name].package_version}") return "\n".join(output) def package_name_from_spec( package_spec: str, python: str, *, pip_args: List[str], verbose: bool ) -> str: start_time = time.time() # shortcut if valid PyPI name pypi_name = valid_pypi_name(package_spec) if pypi_name is not None: # NOTE: if pypi name and installed package name differ, this means pipx # will use the pypi name package_name = pypi_name logger.info(f"Determined package name: {package_name}") logger.info(f"Package name determined in {time.time()-start_time:.1f}s") return package_name # check syntax and clean up spec and pip_args (package_spec, pip_args) = parse_specifier_for_install(package_spec, pip_args) with tempfile.TemporaryDirectory() as temp_venv_dir: venv = Venv(Path(temp_venv_dir), python=python, verbose=verbose) venv.create_venv(venv_args=[], pip_args=[]) package_name = venv.install_package_no_deps( package_or_url=package_spec, pip_args=pip_args ) logger.info(f"Package name determined in {time.time()-start_time:.1f}s") return package_name def run_post_install_actions( venv: Venv, package_name: str, local_bin_dir: Path, venv_dir: Path, include_dependencies: bool, *, force: bool, ) -> None: package_metadata = venv.package_metadata[package_name] display_name = f"{package_name}{package_metadata.suffix}" if not package_metadata.apps: if not package_metadata.apps_of_dependencies: if venv.safe_to_remove(): venv.remove_venv() raise PipxError( f""" No apps associated with package {display_name} or its dependencies. If you are attempting to install a library, pipx should not be used. Consider using pip or a similar tool instead. """ ) if package_metadata.apps_of_dependencies and not include_dependencies: for ( dep, dependent_apps, ) in package_metadata.app_paths_of_dependencies.items(): print( f"Note: Dependent package '{dep}' contains {len(dependent_apps)} apps" ) for app in dependent_apps: print(f" - {app.name}") if venv.safe_to_remove(): venv.remove_venv() raise PipxError( f""" No apps associated with package {display_name}. Try again with '--include-deps' to include apps of dependent packages, which are listed above. If you are attempting to install a library, pipx should not be used. Consider using pip or a similar tool instead." """ ) expose_apps_globally( local_bin_dir, package_metadata.app_paths, force=force, suffix=package_metadata.suffix, ) if include_dependencies: for _, app_paths in package_metadata.app_paths_of_dependencies.items(): expose_apps_globally( local_bin_dir, app_paths, force=force, suffix=package_metadata.suffix ) package_summary, _ = get_venv_summary( venv_dir, package_name=package_name, new_install=True ) print(package_summary) warn_if_not_on_path(local_bin_dir) print(f"done! {stars}", file=sys.stderr) def warn_if_not_on_path(local_bin_dir: Path) -> None: if not userpath.in_current_path(str(local_bin_dir)): logger.warning( pipx_wrap( f""" {hazard} Note: {str(local_bin_dir)!r} is not on your PATH environment variable. These apps will not be globally accessible until your PATH is updated. Run `pipx ensurepath` to automatically add it, or manually modify your PATH in your shell's config file (i.e. ~/.bashrc). """, subsequent_indent=" " * 4, ) ) def add_suffix(name: str, suffix: str) -> str: """Add suffix to app.""" app = Path(name) return f"{app.stem}{suffix}{app.suffix}" pipx-1.0.0/src/pipx/commands/ensure_path.py000066400000000000000000000105031416500503600207260ustar00rootroot00000000000000import logging import site import sys from pathlib import Path from typing import Optional, Tuple import userpath # type: ignore from pipx import constants from pipx.constants import EXIT_CODE_OK, ExitCode from pipx.emojis import hazard, stars from pipx.util import pipx_wrap logger = logging.getLogger(__name__) def get_pipx_user_bin_path() -> Optional[Path]: """Returns None if pipx is not installed using `pip --user` Otherwise returns parent dir of pipx binary """ # NOTE: using this method to detect pip user-installed pipx will return # None if pipx was installed as editable using `pip install --user -e` # https://docs.python.org/3/install/index.html#inst-alt-install-user # Linux + Mac: # scripts in /bin # Windows: # scripts in /Python/Scripts # modules in /Python/site-packages pipx_bin_path = None script_path = Path(__file__).resolve() userbase_path = Path(site.getuserbase()).resolve() try: _ = script_path.relative_to(userbase_path) except ValueError: pip_user_installed = False else: pip_user_installed = True if pip_user_installed: test_paths = ( userbase_path / "bin" / "pipx", Path(site.getusersitepackages()).resolve().parent / "Scripts" / "pipx.exe", ) for test_path in test_paths: if test_path.exists(): pipx_bin_path = test_path.parent break return pipx_bin_path def ensure_path(location: Path, *, force: bool) -> Tuple[bool, bool]: """Ensure location is in user's PATH or add it to PATH. Returns True if location was added to PATH """ location_str = str(location) path_added = False need_shell_restart = userpath.need_shell_restart(location_str) in_current_path = userpath.in_current_path(location_str) if force or (not in_current_path and not need_shell_restart): userpath.append(location_str, "pipx") print( pipx_wrap( f"Success! Added {location_str} to the PATH environment variable.", subsequent_indent=" " * 4, ) ) path_added = True need_shell_restart = userpath.need_shell_restart(location_str) elif not in_current_path and need_shell_restart: print( pipx_wrap( f""" {location_str} has been been added to PATH, but you need to open a new terminal or re-login for this PATH change to take effect. """, subsequent_indent=" " * 4, ) ) else: print( pipx_wrap(f"{location_str} is already in PATH.", subsequent_indent=" " * 4) ) return (path_added, need_shell_restart) def ensure_pipx_paths(force: bool) -> ExitCode: """Returns pipx exit code.""" bin_paths = set([constants.LOCAL_BIN_DIR]) pipx_user_bin_path = get_pipx_user_bin_path() if pipx_user_bin_path is not None: bin_paths.add(pipx_user_bin_path) path_added = False need_shell_restart = False for bin_path in bin_paths: (path_added_current, need_shell_restart_current) = ensure_path( bin_path, force=force ) path_added |= path_added_current need_shell_restart |= need_shell_restart_current print() if path_added: print( pipx_wrap( """ Consider adding shell completions for pipx. Run 'pipx completions' for instructions. """ ) + "\n" ) elif not need_shell_restart: sys.stdout.flush() logger.warning( pipx_wrap( f""" {hazard} All pipx binary directories have been added to PATH. If you are sure you want to proceed, try again with the '--force' flag. """ ) + "\n" ) if need_shell_restart: print( pipx_wrap( """ You will need to open a new terminal or re-login for the PATH changes to take effect. """ ) + "\n" ) print(f"Otherwise pipx is ready to go! {stars}") return EXIT_CODE_OK pipx-1.0.0/src/pipx/commands/inject.py000066400000000000000000000062771416500503600177020ustar00rootroot00000000000000import sys from pathlib import Path from typing import List, Optional from pipx import constants from pipx.colors import bold from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INJECT_ERROR, EXIT_CODE_OK, ExitCode from pipx.emojis import stars from pipx.util import PipxError from pipx.venv import Venv def inject_dep( venv_dir: Path, package_name: Optional[str], package_spec: str, pip_args: List[str], *, verbose: bool, include_apps: bool, include_dependencies: bool, force: bool, ) -> bool: if not venv_dir.exists() or not next(venv_dir.iterdir()): raise PipxError( f""" Can't inject {package_spec!r} into nonexistent Virtual Environment {venv_dir.name!r}. Be sure to install the package first with 'pipx install {venv_dir.name}' before injecting into it. """ ) venv = Venv(venv_dir, verbose=verbose) if not venv.package_metadata: raise PipxError( f""" Can't inject {package_spec!r} into Virtual Environment {venv.name!r}. {venv.name!r} has missing internal pipx metadata. It was likely installed using a pipx version before 0.15.0.0. Please uninstall and install {venv.name!r}, or reinstall-all to fix. """ ) # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. if package_name is None: package_name = package_name_from_spec( package_spec, venv.python, pip_args=pip_args, verbose=verbose ) venv.install_package( package_name=package_name, package_or_url=package_spec, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=False, ) if include_apps: run_post_install_actions( venv, package_name, constants.LOCAL_BIN_DIR, venv_dir, include_dependencies, force=force, ) print(f" injected package {bold(package_name)} into venv {bold(venv.name)}") print(f"done! {stars}", file=sys.stderr) # Any failure to install will raise PipxError, otherwise success return True def inject( venv_dir: Path, package_name: Optional[str], package_specs: List[str], pip_args: List[str], *, verbose: bool, include_apps: bool, include_dependencies: bool, force: bool, ) -> ExitCode: """Returns pipx exit code.""" if not include_apps and include_dependencies: raise PipxError( "Cannot pass --include-deps if --include-apps is not passed as well" ) all_success = True for dep in package_specs: all_success &= inject_dep( venv_dir, None, dep, pip_args, verbose=verbose, include_apps=include_apps, include_dependencies=include_dependencies, force=force, ) # Any failure to install will raise PipxError, otherwise success return EXIT_CODE_OK if all_success else EXIT_CODE_INJECT_ERROR pipx-1.0.0/src/pipx/commands/install.py000066400000000000000000000047321416500503600200660ustar00rootroot00000000000000from pathlib import Path from typing import List, Optional from pipx import constants from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode from pipx.util import pipx_wrap from pipx.venv import Venv, VenvContainer def install( venv_dir: Optional[Path], package_name: Optional[str], package_spec: str, local_bin_dir: Path, python: str, pip_args: List[str], venv_args: List[str], verbose: bool, *, force: bool, include_dependencies: bool, suffix: str = "", ) -> ExitCode: """Returns pipx exit code.""" # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. if package_name is None: package_name = package_name_from_spec( package_spec, python, pip_args=pip_args, verbose=verbose ) if venv_dir is None: venv_container = VenvContainer(constants.PIPX_LOCAL_VENVS) venv_dir = venv_container.get_venv_dir(f"{package_name}{suffix}") try: exists = venv_dir.exists() and bool(next(venv_dir.iterdir())) except StopIteration: exists = False venv = Venv(venv_dir, python=python, verbose=verbose) if exists: if force: print(f"Installing to existing venv {venv.name!r}") else: print( pipx_wrap( f""" {venv.name!r} already seems to be installed. Not modifying existing installation in {str(venv_dir)!r}. Pass '--force' to force installation. """ ) ) return EXIT_CODE_INSTALL_VENV_EXISTS try: venv.create_venv(venv_args, pip_args) venv.install_package( package_name=package_name, package_or_url=package_spec, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=True, is_main_package=True, suffix=suffix, ) run_post_install_actions( venv, package_name, local_bin_dir, venv_dir, include_dependencies, force=force, ) except (Exception, KeyboardInterrupt): print() venv.remove_venv() raise # Any failure to install will raise PipxError, otherwise success return EXIT_CODE_OK pipx-1.0.0/src/pipx/commands/list_packages.py000066400000000000000000000100611416500503600212210ustar00rootroot00000000000000import json import logging import sys from pathlib import Path from typing import Any, Collection, Dict, Tuple from pipx import constants from pipx.colors import bold from pipx.commands.common import VenvProblems, get_venv_summary, venv_health_check from pipx.constants import EXIT_CODE_LIST_PROBLEM, EXIT_CODE_OK, ExitCode from pipx.emojis import sleep from pipx.pipx_metadata_file import JsonEncoderHandlesPath, PipxMetadata from pipx.venv import Venv, VenvContainer logger = logging.getLogger(__name__) PIPX_SPEC_VERSION = "0.1" def get_venv_metadata_summary(venv_dir: Path) -> Tuple[PipxMetadata, VenvProblems, str]: venv = Venv(venv_dir) (venv_problems, warning_message) = venv_health_check(venv) if venv_problems.any_(): return (PipxMetadata(venv_dir, read=False), venv_problems, warning_message) return (venv.pipx_metadata, venv_problems, "") def list_text( venv_dirs: Collection[Path], include_injected: bool, venv_root_dir: str ) -> VenvProblems: print(f"venvs are in {bold(venv_root_dir)}") print(f"apps are exposed on your $PATH at {bold(str(constants.LOCAL_BIN_DIR))}") all_venv_problems = VenvProblems() for venv_dir in venv_dirs: package_summary, venv_problems = get_venv_summary( venv_dir, include_injected=include_injected ) if venv_problems.any_(): logger.warning(package_summary) else: print(package_summary) all_venv_problems.or_(venv_problems) return all_venv_problems def list_json(venv_dirs: Collection[Path]) -> VenvProblems: warning_messages = [] spec_metadata: Dict[str, Any] = { "pipx_spec_version": PIPX_SPEC_VERSION, "venvs": {}, } all_venv_problems = VenvProblems() for venv_dir in venv_dirs: (venv_metadata, venv_problems, warning_str) = get_venv_metadata_summary( venv_dir ) all_venv_problems.or_(venv_problems) if venv_problems.any_(): warning_messages.append(warning_str) continue spec_metadata["venvs"][venv_dir.name] = {} spec_metadata["venvs"][venv_dir.name]["metadata"] = venv_metadata.to_dict() print( json.dumps(spec_metadata, indent=4, sort_keys=True, cls=JsonEncoderHandlesPath) ) for warning_message in warning_messages: logger.warning(warning_message) return all_venv_problems def list_packages( venv_container: VenvContainer, include_injected: bool, json_format: bool ) -> ExitCode: """Returns pipx exit code.""" venv_dirs: Collection[Path] = sorted(venv_container.iter_venv_dirs()) if not venv_dirs: print(f"nothing has been installed with pipx {sleep}", file=sys.stderr) venv_container.verify_shared_libs() if json_format: all_venv_problems = list_json(venv_dirs) else: if not venv_dirs: return EXIT_CODE_OK all_venv_problems = list_text(venv_dirs, include_injected, str(venv_container)) if all_venv_problems.bad_venv_name: logger.warning( "\nOne or more packages contain out-of-date internal data installed from a\n" "previous pipx version and need to be updated.\n" " To fix, execute: pipx reinstall-all" ) if all_venv_problems.invalid_interpreter: logger.warning( "\nOne or more packages have a missing python interpreter.\n" " To fix, execute: pipx reinstall-all" ) if all_venv_problems.missing_metadata: logger.warning( "\nOne or more packages have a missing internal pipx metadata.\n" " They were likely installed using a pipx version before 0.15.0.0.\n" " Please uninstall and install these package(s) to fix." ) if all_venv_problems.not_installed: logger.warning( "\nOne or more packages are not installed properly.\n" " Please uninstall and install these package(s) to fix." ) if all_venv_problems.any_(): print("", file=sys.stderr) return EXIT_CODE_LIST_PROBLEM return EXIT_CODE_OK pipx-1.0.0/src/pipx/commands/reinstall.py000066400000000000000000000076021416500503600204140ustar00rootroot00000000000000import sys from pathlib import Path from typing import List, Sequence from packaging.utils import canonicalize_name import pipx.shared_libs # import instead of from so mockable in tests from pipx.commands.inject import inject_dep from pipx.commands.install import install from pipx.commands.uninstall import uninstall from pipx.constants import ( EXIT_CODE_OK, EXIT_CODE_REINSTALL_INVALID_PYTHON, EXIT_CODE_REINSTALL_VENV_NONEXISTENT, ExitCode, ) from pipx.emojis import error, sleep from pipx.util import PipxError from pipx.venv import Venv, VenvContainer def reinstall( *, venv_dir: Path, local_bin_dir: Path, python: str, verbose: bool ) -> ExitCode: """Returns pipx exit code.""" if not venv_dir.exists(): print(f"Nothing to reinstall for {venv_dir.name} {sleep}") return EXIT_CODE_REINSTALL_VENV_NONEXISTENT try: Path(python).relative_to(venv_dir) except ValueError: pass else: print( f"{error} Error, the python executable would be deleted!", "Change it using the --python option or PIPX_DEFAULT_PYTHON environment variable.", ) return EXIT_CODE_REINSTALL_INVALID_PYTHON venv = Venv(venv_dir, verbose=verbose) if venv.pipx_metadata.main_package.package_or_url is not None: package_or_url = venv.pipx_metadata.main_package.package_or_url else: package_or_url = venv.main_package_name uninstall(venv_dir, local_bin_dir, verbose) # in case legacy original dir name venv_dir = venv_dir.with_name(canonicalize_name(venv_dir.name)) # install main package first install( venv_dir, venv.main_package_name, package_or_url, local_bin_dir, python, venv.pipx_metadata.main_package.pip_args, venv.pipx_metadata.venv_args, verbose, force=True, include_dependencies=venv.pipx_metadata.main_package.include_dependencies, suffix=venv.pipx_metadata.main_package.suffix, ) # now install injected packages for ( injected_name, injected_package, ) in venv.pipx_metadata.injected_packages.items(): if injected_package.package_or_url is None: # This should never happen, but package_or_url is type # Optional[str] so mypy thinks it could be None raise PipxError( f"Internal Error injecting package {injected_package} into {venv.name}" ) inject_dep( venv_dir, injected_name, injected_package.package_or_url, injected_package.pip_args, verbose=verbose, include_apps=injected_package.include_apps, include_dependencies=injected_package.include_dependencies, force=True, ) # Any failure to install will raise PipxError, otherwise success return EXIT_CODE_OK def reinstall_all( venv_container: VenvContainer, local_bin_dir: Path, python: str, verbose: bool, *, skip: Sequence[str], ) -> ExitCode: """Returns pipx exit code.""" pipx.shared_libs.shared_libs.upgrade(verbose=verbose) failed: List[str] = [] for venv_dir in venv_container.iter_venv_dirs(): if venv_dir.name in skip: continue try: package_exit = reinstall( venv_dir=venv_dir, local_bin_dir=local_bin_dir, python=python, verbose=verbose, ) except PipxError as e: print(e, file=sys.stderr) failed.append(venv_dir.name) else: if package_exit != 0: failed.append(venv_dir.name) if len(failed) > 0: raise PipxError( f"The following package(s) failed to reinstall: {', '.join(failed)}" ) # Any failure to install will raise PipxError, otherwise success return EXIT_CODE_OK pipx-1.0.0/src/pipx/commands/run.py000066400000000000000000000160651416500503600172260ustar00rootroot00000000000000import datetime import hashlib import logging import time import urllib.parse import urllib.request from pathlib import Path from shutil import which from typing import List, NoReturn from pipx import constants from pipx.commands.common import package_name_from_spec from pipx.constants import TEMP_VENV_EXPIRATION_THRESHOLD_DAYS, WINDOWS from pipx.emojis import hazard from pipx.util import ( PipxError, exec_app, get_pypackage_bin_path, pipx_wrap, rmdir, run_pypackage_bin, ) from pipx.venv import Venv logger = logging.getLogger(__name__) VENV_EXPIRED_FILENAME = "pipx_expired_venv" APP_NOT_FOUND_ERROR_MESSAGE = """\ '{app}' executable script not found in package '{package_name}'. Available executable scripts: {app_lines}""" def run( app: str, package_or_url: str, app_args: List[str], python: str, pip_args: List[str], venv_args: List[str], pypackages: bool, verbose: bool, use_cache: bool, ) -> NoReturn: """Installs venv to temporary dir (or reuses cache), then runs app from package """ if urllib.parse.urlparse(app).scheme: if not app.endswith(".py"): raise PipxError( """ pipx will only execute apps from the internet directly if they end with '.py'. To run from an SVN, try pipx --spec URL BINARY """ ) logger.info("Detected url. Downloading and executing as a Python file.") content = _http_get_request(app) exec_app([str(python), "-c", content]) elif which(app): logger.warning( pipx_wrap( f""" {hazard} {app} is already on your PATH and installed at {which(app)}. Downloading and running anyway. """, subsequent_indent=" " * 4, ) ) if WINDOWS: app_filename = f"{app}.exe" logger.info(f"Assuming app is {app_filename!r} (Windows only)") else: app_filename = app pypackage_bin_path = get_pypackage_bin_path(app) if pypackage_bin_path.exists(): logger.info( f"Using app in local __pypackages__ directory at {str(pypackage_bin_path)}" ) run_pypackage_bin(pypackage_bin_path, app_args) if pypackages: raise PipxError( f""" '--pypackages' flag was passed, but {str(pypackage_bin_path)!r} was not found. See https://github.com/cs01/pythonloc to learn how to install here, or omit the flag. """ ) venv_dir = _get_temporary_venv_path(package_or_url, python, pip_args, venv_args) venv = Venv(venv_dir) bin_path = venv.bin_path / app_filename _prepare_venv_cache(venv, bin_path, use_cache) if venv.has_app(app, app_filename): logger.info(f"Reusing cached venv {venv_dir}") venv.run_app(app, app_filename, app_args) else: logger.info(f"venv location is {venv_dir}") _download_and_run( Path(venv_dir), package_or_url, app, app_filename, app_args, python, pip_args, venv_args, use_cache, verbose, ) def _download_and_run( venv_dir: Path, package_or_url: str, app: str, app_filename: str, app_args: List[str], python: str, pip_args: List[str], venv_args: List[str], use_cache: bool, verbose: bool, ) -> NoReturn: venv = Venv(venv_dir, python=python, verbose=verbose) venv.create_venv(venv_args, pip_args) if venv.pipx_metadata.main_package.package is not None: package_name = venv.pipx_metadata.main_package.package else: package_name = package_name_from_spec( package_or_url, python, pip_args=pip_args, verbose=verbose ) venv.install_package( package_name=package_name, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=False, include_apps=True, is_main_package=True, ) if not venv.has_app(app, app_filename): apps = venv.pipx_metadata.main_package.apps # If there's a single app inside the package, run that by default if app == package_name and len(apps) == 1: app = apps[0] print(f"NOTE: running app {app!r} from {package_name!r}") if WINDOWS: app_filename = f"{app}.exe" logger.info(f"Assuming app is {app_filename!r} (Windows only)") else: app_filename = app else: all_apps = ( f"{a} - usage: 'pipx run --spec {package_name} {a} [arguments?]'" for a in apps ) raise PipxError( APP_NOT_FOUND_ERROR_MESSAGE.format( app=app, package_name=package_name, app_lines="\n ".join(all_apps), ), wrap_message=False, ) if not use_cache: # Let future _remove_all_expired_venvs know to remove this (venv_dir / VENV_EXPIRED_FILENAME).touch() venv.run_app(app, app_filename, app_args) def _get_temporary_venv_path( package_or_url: str, python: str, pip_args: List[str], venv_args: List[str] ) -> Path: """Computes deterministic path using hashing function on arguments relevant to virtual environment's end state. Arguments used should result in idempotent virtual environment. (i.e. args passed to app aren't relevant, but args passed to venv creation are.) """ m = hashlib.sha256() m.update(package_or_url.encode()) m.update(python.encode()) m.update("".join(pip_args).encode()) m.update("".join(venv_args).encode()) venv_folder_name = m.hexdigest()[0:15] # 15 chosen arbitrarily return Path(constants.PIPX_VENV_CACHEDIR) / venv_folder_name def _is_temporary_venv_expired(venv_dir: Path) -> bool: created_time_sec = venv_dir.stat().st_ctime current_time_sec = time.mktime(datetime.datetime.now().timetuple()) age = current_time_sec - created_time_sec expiration_threshold_sec = 60 * 60 * 24 * TEMP_VENV_EXPIRATION_THRESHOLD_DAYS return age > expiration_threshold_sec or (venv_dir / VENV_EXPIRED_FILENAME).exists() def _prepare_venv_cache(venv: Venv, bin_path: Path, use_cache: bool) -> None: venv_dir = venv.root if not use_cache and bin_path.exists(): logger.info(f"Removing cached venv {str(venv_dir)}") rmdir(venv_dir) _remove_all_expired_venvs() def _remove_all_expired_venvs() -> None: for venv_dir in Path(constants.PIPX_VENV_CACHEDIR).iterdir(): if _is_temporary_venv_expired(venv_dir): logger.info(f"Removing expired venv {str(venv_dir)}") rmdir(venv_dir) def _http_get_request(url: str) -> str: try: res = urllib.request.urlopen(url) charset = res.headers.get_content_charset() or "utf-8" return res.read().decode(charset) except Exception as e: logger.debug("Uncaught Exception:", exc_info=True) raise PipxError(str(e)) pipx-1.0.0/src/pipx/commands/run_pip.py000066400000000000000000000010521416500503600200640ustar00rootroot00000000000000from pathlib import Path from typing import List from pipx.constants import ExitCode from pipx.util import PipxError from pipx.venv import Venv def run_pip( package: str, venv_dir: Path, pip_args: List[str], verbose: bool ) -> ExitCode: """Returns pipx exit code.""" venv = Venv(venv_dir, verbose=verbose) if not venv.python_path.exists(): raise PipxError( f"venv for {package!r} was not found. Was {package!r} installed with pipx?" ) venv.verbose = True return venv.run_pip_get_exit_code(pip_args) pipx-1.0.0/src/pipx/commands/uninstall.py000066400000000000000000000116671416500503600204360ustar00rootroot00000000000000import logging from pathlib import Path from shutil import which from typing import List, Optional, Set from pipx.commands.common import ( add_suffix, can_symlink, get_exposed_app_paths_for_package, ) from pipx.constants import ( EXIT_CODE_OK, EXIT_CODE_UNINSTALL_ERROR, EXIT_CODE_UNINSTALL_VENV_NONEXISTENT, ExitCode, ) from pipx.emojis import hazard, sleep, stars from pipx.pipx_metadata_file import PackageInfo from pipx.util import rmdir, safe_unlink from pipx.venv import Venv, VenvContainer from pipx.venv_inspect import VenvMetadata logger = logging.getLogger(__name__) def _venv_metadata_to_package_info( venv_metadata: VenvMetadata, package_name: str, package_or_url: str = "", pip_args: Optional[List[str]] = None, include_apps: bool = True, include_dependencies: bool = False, suffix: str = "", ) -> PackageInfo: if pip_args is None: pip_args = [] return PackageInfo( package=package_name, package_or_url=package_or_url, pip_args=pip_args, include_apps=include_apps, include_dependencies=include_dependencies, apps=venv_metadata.apps, app_paths=venv_metadata.app_paths, apps_of_dependencies=venv_metadata.apps_of_dependencies, app_paths_of_dependencies=venv_metadata.app_paths_of_dependencies, package_version=venv_metadata.package_version, suffix=suffix, ) def _get_package_bin_dir_app_paths( venv: Venv, package_info: PackageInfo, local_bin_dir: Path ) -> Set[Path]: suffix = package_info.suffix apps = [] if package_info.include_apps: apps += package_info.apps if package_info.include_dependencies: apps += package_info.apps_of_dependencies return get_exposed_app_paths_for_package( venv.bin_path, local_bin_dir, [add_suffix(app, suffix) for app in apps] ) def _get_venv_bin_dir_app_paths(venv: Venv, local_bin_dir: Path) -> Set[Path]: bin_dir_app_paths = set() if venv.pipx_metadata.main_package.package is not None: # Valid metadata for venv for package_info in venv.package_metadata.values(): bin_dir_app_paths |= _get_package_bin_dir_app_paths( venv, package_info, local_bin_dir ) elif venv.python_path.is_file(): # No metadata from pipx_metadata.json, but valid python interpreter. # In pre-metadata-pipx venv.root.name is name of main package # In pre-metadata-pipx there is no suffix # We make the conservative assumptions: no injected packages, # not include_dependencies. Other PackageInfo fields are irrelevant # here. venv_metadata = venv.get_venv_metadata_for_package(venv.root.name, set()) main_package_info = _venv_metadata_to_package_info( venv_metadata, venv.root.name ) bin_dir_app_paths = _get_package_bin_dir_app_paths( venv, main_package_info, local_bin_dir ) else: # No metadata and no valid python interpreter. # We'll take our best guess on what to uninstall here based on symlink # location for symlink-capable systems. # The heuristic here is any symlink in ~/.local/bin pointing to # .local/pipx/venvs/VENV_NAME/{bin,Scripts} should be uninstalled. # For non-symlink systems we give up and return and empty set. if not can_symlink(local_bin_dir): return set() bin_dir_app_paths = get_exposed_app_paths_for_package( venv.bin_path, local_bin_dir ) return bin_dir_app_paths def uninstall(venv_dir: Path, local_bin_dir: Path, verbose: bool) -> ExitCode: """Uninstall entire venv_dir, including main package and all injected packages. Returns pipx exit code. """ if not venv_dir.exists(): print(f"Nothing to uninstall for {venv_dir.name} {sleep}") app = which(venv_dir.name) if app: print( f"{hazard} Note: '{app}' still exists on your system and is on your PATH" ) return EXIT_CODE_UNINSTALL_VENV_NONEXISTENT venv = Venv(venv_dir, verbose=verbose) bin_dir_app_paths = _get_venv_bin_dir_app_paths(venv, local_bin_dir) for bin_dir_app_path in bin_dir_app_paths: try: safe_unlink(bin_dir_app_path) except FileNotFoundError: logger.info(f"tried to remove but couldn't find {bin_dir_app_path}") else: logger.info(f"removed file {bin_dir_app_path}") rmdir(venv_dir) print(f"uninstalled {venv.name}! {stars}") return EXIT_CODE_OK def uninstall_all( venv_container: VenvContainer, local_bin_dir: Path, verbose: bool ) -> ExitCode: """Returns pipx exit code.""" all_success = True for venv_dir in venv_container.iter_venv_dirs(): return_val = uninstall(venv_dir, local_bin_dir, verbose) all_success &= return_val == 0 return EXIT_CODE_OK if all_success else EXIT_CODE_UNINSTALL_ERROR pipx-1.0.0/src/pipx/commands/upgrade.py000066400000000000000000000140001416500503600200340ustar00rootroot00000000000000import logging from pathlib import Path from typing import List, Sequence from pipx import constants from pipx.colors import bold, red from pipx.commands.common import expose_apps_globally from pipx.constants import EXIT_CODE_OK, ExitCode from pipx.emojis import sleep from pipx.package_specifier import parse_specifier_for_upgrade from pipx.util import PipxError, pipx_wrap from pipx.venv import Venv, VenvContainer logger = logging.getLogger(__name__) def _upgrade_package( venv: Venv, package_name: str, pip_args: List[str], is_main_package: bool, force: bool, upgrading_all: bool, ) -> int: """Returns 1 if package version changed, 0 if same version""" package_metadata = venv.package_metadata[package_name] if package_metadata.package_or_url is None: raise PipxError( f"Internal Error: package {package_name} has corrupt pipx metadata." ) package_or_url = parse_specifier_for_upgrade(package_metadata.package_or_url) old_version = package_metadata.package_version venv.upgrade_package( package_name, package_or_url, pip_args, include_dependencies=package_metadata.include_dependencies, include_apps=package_metadata.include_apps, is_main_package=is_main_package, suffix=package_metadata.suffix, ) package_metadata = venv.package_metadata[package_name] display_name = f"{package_metadata.package}{package_metadata.suffix}" new_version = package_metadata.package_version if package_metadata.include_apps: expose_apps_globally( constants.LOCAL_BIN_DIR, package_metadata.app_paths, force=force, suffix=package_metadata.suffix, ) if package_metadata.include_dependencies: for _, app_paths in package_metadata.app_paths_of_dependencies.items(): expose_apps_globally( constants.LOCAL_BIN_DIR, app_paths, force=force, suffix=package_metadata.suffix, ) if old_version == new_version: if upgrading_all: pass else: print( pipx_wrap( f""" {display_name} is already at latest version {old_version} (location: {str(venv.root)}) """ ) ) return 0 else: print( pipx_wrap( f""" upgraded package {display_name} from {old_version} to {new_version} (location: {str(venv.root)}) """ ) ) return 1 def _upgrade_venv( venv_dir: Path, pip_args: List[str], verbose: bool, *, include_injected: bool, upgrading_all: bool, force: bool, ) -> int: """Returns number of packages with changed versions.""" if not venv_dir.is_dir(): raise PipxError( f""" Package is not installed. Expected to find {str(venv_dir)}, but it does not exist. """ ) venv = Venv(venv_dir, verbose=verbose) if not venv.package_metadata: raise PipxError( f"Not upgrading {red(bold(venv_dir.name))}. It has missing internal pipx metadata.\n" f"It was likely installed using a pipx version before 0.15.0.0.\n" f"Please uninstall and install this package to fix.", wrap_message=False, ) # Upgrade shared libraries (pip, setuptools and wheel) venv.upgrade_packaging_libraries(pip_args) versions_updated = 0 package_name = venv.main_package_name versions_updated += _upgrade_package( venv, package_name, pip_args, is_main_package=True, force=force, upgrading_all=upgrading_all, ) if include_injected: for package_name in venv.package_metadata: if package_name == venv.main_package_name: continue versions_updated += _upgrade_package( venv, package_name, pip_args, is_main_package=False, force=force, upgrading_all=upgrading_all, ) return versions_updated def upgrade( venv_dir: Path, pip_args: List[str], verbose: bool, *, include_injected: bool, force: bool, ) -> ExitCode: """Returns pipx exit code.""" _ = _upgrade_venv( venv_dir, pip_args, verbose, include_injected=include_injected, upgrading_all=False, force=force, ) # Any error in upgrade will raise PipxError (e.g. from venv.upgrade_package()) return EXIT_CODE_OK def upgrade_all( venv_container: VenvContainer, verbose: bool, *, include_injected: bool, skip: Sequence[str], force: bool, ) -> ExitCode: """Returns pipx exit code.""" venv_error = False venvs_upgraded = 0 for venv_dir in venv_container.iter_venv_dirs(): venv = Venv(venv_dir, verbose=verbose) if ( venv_dir.name in skip or "--editable" in venv.pipx_metadata.main_package.pip_args ): continue try: venvs_upgraded += _upgrade_venv( venv_dir, venv.pipx_metadata.main_package.pip_args, verbose, include_injected=include_injected, upgrading_all=True, force=force, ) except PipxError as e: venv_error = True logger.error(f"Error encountered when upgrading {venv_dir.name}:") logger.error(f"{e}\n") if venvs_upgraded == 0: print( f"Versions did not change after running 'pipx upgrade' for each package {sleep}" ) if venv_error: raise PipxError( "\nSome packages encountered errors during upgrade.\n" " See specific error messages above.", wrap_message=False, ) return EXIT_CODE_OK pipx-1.0.0/src/pipx/constants.py000066400000000000000000000037331416500503600166330ustar00rootroot00000000000000import os import sys from pathlib import Path from textwrap import dedent from typing import NewType, Optional DEFAULT_PIPX_HOME = Path.home() / ".local/pipx" DEFAULT_PIPX_BIN_DIR = Path.home() / ".local/bin" PIPX_HOME = Path(os.environ.get("PIPX_HOME", DEFAULT_PIPX_HOME)).resolve() PIPX_LOCAL_VENVS = PIPX_HOME / "venvs" PIPX_LOG_DIR = PIPX_HOME / "logs" DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared" PIPX_TRASH_DIR = PIPX_HOME / ".trash" PIPX_SHARED_LIBS = Path( os.environ.get("PIPX_SHARED_LIBS", DEFAULT_PIPX_SHARED_LIBS) ).resolve() PIPX_SHARED_PTH = "pipx_shared.pth" LOCAL_BIN_DIR = Path(os.environ.get("PIPX_BIN_DIR", DEFAULT_PIPX_BIN_DIR)).resolve() PIPX_VENV_CACHEDIR = PIPX_HOME / ".cache" TEMP_VENV_EXPIRATION_THRESHOLD_DAYS = 14 ExitCode = NewType("ExitCode", int) # pipx shell exit codes EXIT_CODE_OK = ExitCode(0) EXIT_CODE_INJECT_ERROR = ExitCode(1) EXIT_CODE_INSTALL_VENV_EXISTS = ExitCode(0) EXIT_CODE_LIST_PROBLEM = ExitCode(1) EXIT_CODE_UNINSTALL_VENV_NONEXISTENT = ExitCode(1) EXIT_CODE_UNINSTALL_ERROR = ExitCode(1) EXIT_CODE_REINSTALL_VENV_NONEXISTENT = ExitCode(1) EXIT_CODE_REINSTALL_INVALID_PYTHON = ExitCode(1) pipx_log_file: Optional[Path] = None def is_windows() -> bool: return sys.platform == "win32" WINDOWS: bool = is_windows() completion_instructions = dedent( """ Add the appropriate command to your shell's config file so that it is run on startup. You will likely have to restart or re-login for the autocompletion to start working. bash: eval "$(register-python-argcomplete pipx)" zsh: To activate completions for zsh you need to have bashcompinit enabled in zsh: autoload -U bashcompinit bashcompinit Afterwards you can enable completion for pipx: eval "$(register-python-argcomplete pipx)" tcsh: eval `register-python-argcomplete --shell tcsh pipx` fish: # Not required to be in the config file, only run once register-python-argcomplete --shell fish pipx >~/.config/fish/completions/pipx.fish """ ) pipx-1.0.0/src/pipx/emojis.py000066400000000000000000000015111416500503600160750ustar00rootroot00000000000000import os import sys def strtobool(val: str) -> bool: val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): return True elif val in ("n", "no", "f", "false", "off", "0"): return False else: return False def use_emojis() -> bool: # All emojis that pipx might possibly use emoji_test_str = "✨🌟⚠️😴⣷⣯⣟⡿⢿⣻⣽⣾" try: emoji_test_str.encode(sys.stderr.encoding) platform_emoji_support = True except UnicodeEncodeError: platform_emoji_support = False return strtobool(str(os.getenv("USE_EMOJI", platform_emoji_support))) EMOJI_SUPPORT = use_emojis() if EMOJI_SUPPORT: stars = "✨ 🌟 ✨" hazard = "⚠️" error = "⛔" sleep = "😴" else: stars = "" hazard = "" error = "" sleep = "" pipx-1.0.0/src/pipx/interpreter.py000066400000000000000000000042521416500503600171570ustar00rootroot00000000000000import os import shutil import subprocess import sys from pipx.constants import WINDOWS from pipx.util import PipxError def has_venv() -> bool: try: import venv # noqa return True except ImportError: return False # The following code was copied from https://github.com/uranusjr/pipx-standalone # which uses the same technique to build a completely standalone pipx # distribution. # # If we are running under the Windows embeddable distribution, # venv isn't available (and we probably don't want to use the # embeddable distribution as our applications' base Python anyway) # so we try to locate the system Python and use that instead. def _find_default_windows_python() -> str: if has_venv(): return sys.executable py = shutil.which("py") if py: return py python = shutil.which("python") if python is None: raise PipxError("No suitable Python found") # If the path contains "WindowsApps", it's the store python if "WindowsApps" not in python: return python # Special treatment to detect Windows Store stub. # https://twitter.com/zooba/status/1212454929379581952 proc = subprocess.run( [python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL ) if proc.returncode != 0: # Cover the 9009 return code pre-emptively. raise PipxError("No suitable Python found") if not proc.stdout.strip(): # A real Python should print version, Windows Store stub won't. raise PipxError("No suitable Python found") return python # This executable seems to work. def _get_sys_executable() -> str: if WINDOWS: return _find_default_windows_python() else: return sys.executable def _get_absolute_python_interpreter(env_python: str) -> str: which_python = shutil.which(env_python) if not which_python: raise PipxError(f"Default python interpreter {repr(env_python)} is invalid.") return which_python env_default_python = os.environ.get("PIPX_DEFAULT_PYTHON") if not env_default_python: DEFAULT_PYTHON = _get_sys_executable() else: DEFAULT_PYTHON = _get_absolute_python_interpreter(env_default_python) pipx-1.0.0/src/pipx/main.py000066400000000000000000000652701416500503600155470ustar00rootroot00000000000000# PYTHON_ARGCOMPLETE_OK """The command line interface to pipx""" import argparse import logging import logging.config import os import re import shlex import sys import textwrap import time import urllib.parse from pathlib import Path from typing import Any, Callable, Dict, List import argcomplete # type: ignore from packaging.requirements import InvalidRequirement, Requirement from packaging.utils import canonicalize_name import pipx.constants from pipx import commands, constants from pipx.animate import hide_cursor, show_cursor from pipx.colors import bold, green from pipx.constants import ExitCode from pipx.emojis import hazard from pipx.interpreter import DEFAULT_PYTHON from pipx.util import PipxError, mkdir, pipx_wrap, rmdir from pipx.venv import VenvContainer from pipx.version import __version__ logger = logging.getLogger(__name__) VenvCompleter = Callable[[str], List[str]] def print_version() -> None: print(__version__) SPEC_HELP = textwrap.dedent( """\ The package name or specific installation source passed to pip. Runs `pip install -U SPEC`. For example `--spec mypackage==2.0.0` or `--spec git+https://github.com/user/repo.git@branch` """ ) PIPX_DESCRIPTION = textwrap.dedent( f""" Install and execute apps from Python packages. Binaries can either be installed globally into isolated Virtual Environments or run directly in a temporary Virtual Environment. Virtual Environment location is {str(constants.PIPX_LOCAL_VENVS)}. Symlinks to apps are placed in {str(constants.LOCAL_BIN_DIR)}. """ ) PIPX_DESCRIPTION += pipx_wrap( """ optional environment variables: PIPX_HOME Overrides default pipx location. Virtual Environments will be installed to $PIPX_HOME/venvs. PIPX_BIN_DIR Overrides location of app installations. Apps are symlinked or copied here. USE_EMOJI Overrides emoji behavior. Default value varies based on platform. PIPX_DEFAULT_PYTHON Overrides default python used for commands. """, subsequent_indent=" " * 24, # match the indent of argparse options keep_newlines=True, ) DOC_DEFAULT_PYTHON = os.getenv("PIPX__DOC_DEFAULT_PYTHON", DEFAULT_PYTHON) INSTALL_DESCRIPTION = textwrap.dedent( f""" The install command is the preferred way to globally install apps from python packages on your system. It creates an isolated virtual environment for the package, then ensures the package's apps are accessible on your $PATH. The result: apps you can run from anywhere, located in packages you can cleanly upgrade or uninstall. Guaranteed to not have dependency version conflicts or interfere with your OS's python packages. 'sudo' is not required to do this. pipx install PACKAGE_NAME pipx install --python PYTHON PACKAGE_NAME pipx install VCS_URL pipx install ./LOCAL_PATH pipx install ZIP_FILE pipx install TAR_GZ_FILE The PACKAGE_SPEC argument is passed directly to `pip install`. The default virtual environment location is {constants.DEFAULT_PIPX_HOME} and can be overridden by setting the environment variable `PIPX_HOME` (Virtual Environments will be installed to `$PIPX_HOME/venvs`). The default app location is {constants.DEFAULT_PIPX_BIN_DIR} and can be overridden by setting the environment variable `PIPX_BIN_DIR`. The default python executable used to install a package is {DOC_DEFAULT_PYTHON} and can be overridden by setting the environment variable `PIPX_DEFAULT_PYTHON`. """ ) class LineWrapRawTextHelpFormatter(argparse.RawDescriptionHelpFormatter): def _split_lines(self, text: str, width: int) -> List[str]: text = self._whitespace_matcher.sub(" ", text).strip() return textwrap.wrap(text, width) class InstalledVenvsCompleter: def __init__(self, venv_container: VenvContainer) -> None: self.packages = [str(p.name) for p in sorted(venv_container.iter_venv_dirs())] def use(self, prefix: str, **kwargs: Any) -> List[str]: return [ f"{prefix}{x[len(prefix):]}" for x in self.packages if x.startswith(canonicalize_name(prefix)) ] def get_pip_args(parsed_args: Dict[str, str]) -> List[str]: pip_args: List[str] = [] if parsed_args.get("index_url"): pip_args += ["--index-url", parsed_args["index_url"]] if parsed_args.get("pip_args"): pip_args += shlex.split(parsed_args.get("pip_args", "")) # make sure --editable is last because it needs to be right before # package specification if parsed_args.get("editable"): pip_args += ["--editable"] return pip_args def get_venv_args(parsed_args: Dict[str, str]) -> List[str]: venv_args: List[str] = [] if parsed_args.get("system_site_packages"): venv_args += ["--system-site-packages"] return venv_args def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 verbose = args.verbose if "verbose" in args else False pip_args = get_pip_args(vars(args)) venv_args = get_venv_args(vars(args)) venv_container = VenvContainer(constants.PIPX_LOCAL_VENVS) if "package" in args: package = args.package if urllib.parse.urlparse(package).scheme: raise PipxError("Package cannot be a url") if "spec" in args and args.spec is not None: if urllib.parse.urlparse(args.spec).scheme: if "#egg=" not in args.spec: args.spec = args.spec + f"#egg={package}" venv_dir = venv_container.get_venv_dir(package) logger.info(f"Virtual Environment location is {venv_dir}") if "skip" in args: skip_list = [canonicalize_name(x) for x in args.skip] if args.command == "run": package_or_url = ( args.spec if ("spec" in args and args.spec is not None) else args.app_with_args[0] ) # For any package, we need to just use the name try: package_name = Requirement(args.app_with_args[0]).name except InvalidRequirement: # Raw URLs to scripts are supported, too, so continue if # we can't parse this as a package package_name = args.app_with_args[0] use_cache = not args.no_cache commands.run( package_name, package_or_url, args.app_with_args[1:], args.python, pip_args, venv_args, args.pypackages, verbose, use_cache, ) # We should never reach here because run() is NoReturn. return ExitCode(1) elif args.command == "install": return commands.install( None, None, args.package_spec, constants.LOCAL_BIN_DIR, args.python, pip_args, venv_args, verbose, force=args.force, include_dependencies=args.include_deps, suffix=args.suffix, ) elif args.command == "inject": return commands.inject( venv_dir, None, args.dependencies, pip_args, verbose=verbose, include_apps=args.include_apps, include_dependencies=args.include_deps, force=args.force, ) elif args.command == "upgrade": return commands.upgrade( venv_dir, pip_args, verbose, include_injected=args.include_injected, force=args.force, ) elif args.command == "upgrade-all": return commands.upgrade_all( venv_container, verbose, include_injected=args.include_injected, skip=skip_list, force=args.force, ) elif args.command == "list": return commands.list_packages(venv_container, args.include_injected, args.json) elif args.command == "uninstall": return commands.uninstall(venv_dir, constants.LOCAL_BIN_DIR, verbose) elif args.command == "uninstall-all": return commands.uninstall_all(venv_container, constants.LOCAL_BIN_DIR, verbose) elif args.command == "reinstall": return commands.reinstall( venv_dir=venv_dir, local_bin_dir=constants.LOCAL_BIN_DIR, python=args.python, verbose=verbose, ) elif args.command == "reinstall-all": return commands.reinstall_all( venv_container, constants.LOCAL_BIN_DIR, args.python, verbose, skip=skip_list, ) elif args.command == "runpip": if not venv_dir: raise PipxError("Developer error: venv_dir is not defined.") return commands.run_pip(package, venv_dir, args.pipargs, args.verbose) elif args.command == "ensurepath": try: return commands.ensure_pipx_paths(force=args.force) except Exception as e: logger.debug("Uncaught Exception:", exc_info=True) raise PipxError(str(e), wrap_message=False) elif args.command == "completions": print(constants.completion_instructions) return ExitCode(0) else: raise PipxError(f"Unknown command {args.command}") def add_pip_venv_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--system-site-packages", action="store_true", help="Give the virtual environment access to the system site-packages dir.", ) parser.add_argument("--index-url", "-i", help="Base URL of Python Package Index") parser.add_argument( "--editable", "-e", help="Install a project in editable mode", action="store_true", ) parser.add_argument( "--pip-args", help="Arbitrary pip arguments to pass directly to pip install/upgrade commands", ) def add_include_dependencies(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--include-deps", help="Include apps of dependent packages", action="store_true" ) def _add_install(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "install", help="Install a package", formatter_class=LineWrapRawTextHelpFormatter, description=INSTALL_DESCRIPTION, ) p.add_argument("package_spec", help="package name or pip installation spec") add_include_dependencies(p) p.add_argument("--verbose", action="store_true") p.add_argument( "--force", "-f", action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR", ) p.add_argument( "--suffix", default="", help=( "Optional suffix for virtual environment and executable names. " "NOTE: The suffix feature is experimental and subject to change." ), ) p.add_argument( "--python", default=DEFAULT_PYTHON, help=( "The Python executable used to create the Virtual Environment and run the " "associated app/apps. Must be v3.6+." ), ) add_pip_venv_args(p) def _add_inject(subparsers, venv_completer: VenvCompleter) -> None: p = subparsers.add_parser( "inject", help="Install packages into an existing Virtual Environment", description="Installs packages to an existing pipx-managed virtual environment.", ) p.add_argument( "package", help="Name of the existing pipx-managed Virtual Environment to inject into", ).completer = venv_completer p.add_argument( "dependencies", nargs="+", help="the packages to inject into the Virtual Environment--either package name or pip package spec", ) p.add_argument( "--include-apps", action="store_true", help="Add apps from the injected packages onto your PATH", ) add_include_dependencies(p) add_pip_venv_args(p) p.add_argument( "--force", "-f", action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR", ) p.add_argument("--verbose", action="store_true") def _add_upgrade(subparsers, venv_completer: VenvCompleter) -> None: p = subparsers.add_parser( "upgrade", help="Upgrade a package", description="Upgrade a package in a pipx-managed Virtual Environment by running 'pip install --upgrade PACKAGE'", ) p.add_argument("package").completer = venv_completer p.add_argument( "--include-injected", action="store_true", help="Also upgrade packages injected into the main app's environment", ) p.add_argument( "--force", "-f", action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR", ) add_pip_venv_args(p) p.add_argument("--verbose", action="store_true") def _add_upgrade_all(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "upgrade-all", help="Upgrade all packages. Runs `pip install -U ` for each package.", description="Upgrades all packages within their virtual environments by running 'pip install --upgrade PACKAGE'", ) p.add_argument( "--include-injected", action="store_true", help="Also upgrade packages injected into the main app's environment", ) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") p.add_argument( "--force", "-f", action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR", ) p.add_argument("--verbose", action="store_true") def _add_uninstall(subparsers, venv_completer: VenvCompleter) -> None: p = subparsers.add_parser( "uninstall", help="Uninstall a package", description="Uninstalls a pipx-managed Virtual Environment by deleting it and any files that point to its apps.", ) p.add_argument("package").completer = venv_completer p.add_argument("--verbose", action="store_true") def _add_uninstall_all(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "uninstall-all", help="Uninstall all packages", description="Uninstall all pipx-managed packages", ) p.add_argument("--verbose", action="store_true") def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None: p = subparsers.add_parser( "reinstall", formatter_class=LineWrapRawTextHelpFormatter, help="Reinstall a package", description=textwrap.dedent( """ Reinstalls a package. Package is uninstalled, then installed with pipx install PACKAGE with the same options used in the original install of PACKAGE. """ ), ) p.add_argument("package").completer = venv_completer p.add_argument( "--python", default=DEFAULT_PYTHON, help=( "The Python executable used to recreate the Virtual Environment " "and run the associated app/apps. Must be v3.6+." ), ) p.add_argument("--verbose", action="store_true") def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "reinstall-all", formatter_class=LineWrapRawTextHelpFormatter, help="Reinstall all packages", description=textwrap.dedent( """ Reinstalls all packages. Packages are uninstalled, then installed with pipx install PACKAGE with the same options used in the original install of PACKAGE. This is useful if you upgraded to a new version of Python and want all your packages to use the latest as well. """ ), ) p.add_argument( "--python", default=DEFAULT_PYTHON, help=( "The Python executable used to recreate the Virtual Environment " "and run the associated app/apps. Must be v3.6+." ), ) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") p.add_argument("--verbose", action="store_true") def _add_list(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "list", help="List installed packages", description="List packages and apps installed with pipx", ) p.add_argument( "--include-injected", action="store_true", help="Show packages injected into the main app's environment", ) p.add_argument( "--json", action="store_true", help="Output rich data in json format." ) p.add_argument("--verbose", action="store_true") def _add_run(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "run", formatter_class=LineWrapRawTextHelpFormatter, help=( "Download the latest version of a package to a temporary virtual environment, " "then run an app from it. Also compatible with local `__pypackages__` " "directory (experimental)." ), description=textwrap.dedent( f""" Download the latest version of a package to a temporary virtual environment, then run an app from it. The environment will be cached and re-used for up to {constants.TEMP_VENV_EXPIRATION_THRESHOLD_DAYS} days. This means subsequent calls to 'run' for the same package will be faster since they can re-use the cached Virtual Environment. In support of PEP 582 'run' will use apps found in a local __pypackages__ directory, if present. Please note that this behavior is experimental, and acts as a companion tool to pythonloc. It may be modified or removed in the future. See https://github.com/cs01/pythonloc. """ ), ) p.add_argument( "--no-cache", action="store_true", help="Do not re-use cached virtual environment if it exists", ) p.add_argument( "app_with_args", metavar="app ...", nargs=argparse.REMAINDER, help="app/package name and any arguments to be passed to it", default=[], ) p.add_argument( "--pypackages", action="store_true", help="Require app to be run from local __pypackages__ directory", ) p.add_argument("--spec", help=SPEC_HELP) p.add_argument("--verbose", action="store_true") p.add_argument( "--python", default=DEFAULT_PYTHON, help="The Python version to run package's CLI app with. Must be v3.6+.", ) add_pip_venv_args(p) p.set_defaults(subparser=p) # modify usage text to show required app argument p.usage = re.sub(r"^usage: ", "", p.format_usage()) # add a double-dash to usage text to show requirement before app p.usage = re.sub(r"\.\.\.", "app ...", p.usage) def _add_runpip(subparsers, venv_completer: VenvCompleter) -> None: p = subparsers.add_parser( "runpip", help="Run pip in an existing pipx-managed Virtual Environment", description="Run pip in an existing pipx-managed Virtual Environment", ) p.add_argument( "package", help="Name of the existing pipx-managed Virtual Environment to run pip in", ).completer = venv_completer p.add_argument( "pipargs", nargs=argparse.REMAINDER, default=[], help="Arguments to forward to pip command", ) p.add_argument("--verbose", action="store_true") def _add_ensurepath(subparsers: argparse._SubParsersAction) -> None: p = subparsers.add_parser( "ensurepath", help=( "Ensure directories necessary for pipx operation are in your " "PATH environment variable." ), description=( "Ensure directory where pipx stores apps is in your " "PATH environment variable. Also if pipx was installed via " "`pip install --user`, ensure pipx itself is in your PATH. " "Note that running this may modify " "your shell's configuration file(s) such as '~/.bashrc'." ), ) p.add_argument( "--force", "-f", action="store_true", help=( "Add text to your shell's config file even if it looks like your " "PATH already contains paths to pipx and pipx-install apps." ), ) def get_command_parser() -> argparse.ArgumentParser: venv_container = VenvContainer(constants.PIPX_LOCAL_VENVS) completer_venvs = InstalledVenvsCompleter(venv_container) parser = argparse.ArgumentParser( prog="pipx", formatter_class=LineWrapRawTextHelpFormatter, description=PIPX_DESCRIPTION, ) parser.man_short_description = PIPX_DESCRIPTION.splitlines()[1] # type: ignore subparsers = parser.add_subparsers( dest="command", description="Get help for commands with pipx COMMAND --help" ) _add_install(subparsers) _add_inject(subparsers, completer_venvs.use) _add_upgrade(subparsers, completer_venvs.use) _add_upgrade_all(subparsers) _add_uninstall(subparsers, completer_venvs.use) _add_uninstall_all(subparsers) _add_reinstall(subparsers, completer_venvs.use) _add_reinstall_all(subparsers) _add_list(subparsers) _add_run(subparsers) _add_runpip(subparsers, completer_venvs.use) _add_ensurepath(subparsers) parser.add_argument("--version", action="store_true", help="Print version and exit") subparsers.add_parser( "completions", help="Print instructions on enabling shell completions for pipx", description="Print instructions on enabling shell completions for pipx", ) return parser def delete_oldest_logs(file_list: List[Path], keep_number: int) -> None: file_list = sorted(file_list) if len(file_list) > keep_number: for existing_file in file_list[:-keep_number]: try: existing_file.unlink() except FileNotFoundError: pass def setup_log_file() -> Path: max_logs = 10 # don't use utils.mkdir, to prevent emission of log message constants.PIPX_LOG_DIR.mkdir(parents=True, exist_ok=True) delete_oldest_logs(list(constants.PIPX_LOG_DIR.glob("cmd_*[0-9].log")), max_logs) delete_oldest_logs( list(constants.PIPX_LOG_DIR.glob("cmd_*_pip_errors.log")), max_logs ) datetime_str = time.strftime("%Y-%m-%d_%H.%M.%S") log_file = constants.PIPX_LOG_DIR / f"cmd_{datetime_str}.log" counter = 1 while log_file.exists() and counter < 10: log_file = constants.PIPX_LOG_DIR / f"cmd_{datetime_str}_{counter}.log" counter += 1 return log_file def setup_logging(verbose: bool) -> None: pipx_str = bold(green("pipx >")) if sys.stdout.isatty() else "pipx >" pipx.constants.pipx_log_file = setup_log_file() # "incremental" is False so previous pytest tests don't accumulate handlers logging_config = { "version": 1, "formatters": { "stream_nonverbose": { "class": "logging.Formatter", "format": "{message}", "style": "{", }, "stream_verbose": { "class": "logging.Formatter", "format": pipx_str + "({funcName}:{lineno}): {message}", "style": "{", }, "file": { "class": "logging.Formatter", "format": "{relativeCreated: >8.1f}ms ({funcName}:{lineno}): {message}", "style": "{", }, }, "handlers": { "stream": { "class": "logging.StreamHandler", "formatter": "stream_verbose" if verbose else "stream_nonverbose", "level": "INFO" if verbose else "WARNING", }, "file": { "class": "logging.FileHandler", "formatter": "file", "filename": str(pipx.constants.pipx_log_file), "encoding": "utf-8", "level": "DEBUG", }, }, "loggers": {"pipx": {"handlers": ["stream", "file"], "level": "DEBUG"}}, "incremental": False, } logging.config.dictConfig(logging_config) def setup(args: argparse.Namespace) -> None: if "version" in args and args.version: print_version() sys.exit(0) setup_logging("verbose" in args and args.verbose) logger.debug(f"{time.strftime('%Y-%m-%d %H:%M:%S')}") logger.debug(f"{' '.join(sys.argv)}") logger.info(f"pipx version is {__version__}") logger.info(f"Default python interpreter is {repr(DEFAULT_PYTHON)}") mkdir(constants.PIPX_LOCAL_VENVS) mkdir(constants.LOCAL_BIN_DIR) mkdir(constants.PIPX_VENV_CACHEDIR) rmdir(constants.PIPX_TRASH_DIR, False) old_pipx_venv_location = constants.PIPX_LOCAL_VENVS / "pipx-app" if old_pipx_venv_location.exists(): logger.warning( pipx_wrap( f""" {hazard} A virtual environment for pipx was detected at {str(old_pipx_venv_location)}. The 'pipx-app' package has been renamed back to 'pipx' (https://github.com/pypa/pipx/issues/82). """, subsequent_indent=" " * 4, ) ) def check_args(parsed_pipx_args: argparse.Namespace) -> None: if parsed_pipx_args.command == "run": # we manually discard a first -- because using nargs=argparse.REMAINDER # will not do it automatically if parsed_pipx_args.app_with_args and parsed_pipx_args.app_with_args[0] == "--": parsed_pipx_args.app_with_args.pop(0) # since we would like app to be required but not in a separate argparse # add_argument, we implement our own missing required arg error if not parsed_pipx_args.app_with_args: parsed_pipx_args.subparser.error( "the following arguments are required: app" ) def cli() -> ExitCode: """Entry point from command line""" try: hide_cursor() parser = get_command_parser() argcomplete.autocomplete(parser) parsed_pipx_args = parser.parse_args() setup(parsed_pipx_args) check_args(parsed_pipx_args) if not parsed_pipx_args.command: parser.print_help() return ExitCode(1) return run_pipx_command(parsed_pipx_args) except PipxError as e: print(str(e), file=sys.stderr) logger.debug(f"PipxError: {e}", exc_info=True) return ExitCode(1) except KeyboardInterrupt: return ExitCode(1) except Exception: logger.debug("Uncaught Exception:", exc_info=True) raise finally: logger.debug("pipx finished.") show_cursor() if __name__ == "__main__": sys.exit(cli()) pipx-1.0.0/src/pipx/package_specifier.py000066400000000000000000000173701416500503600202450ustar00rootroot00000000000000# Valid package specifiers for pipx: # PEP508-compliant # git+ # # # # import logging import re from pathlib import Path from typing import List, NamedTuple, Optional, Set, Tuple from packaging.requirements import InvalidRequirement, Requirement from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name from pipx.emojis import hazard from pipx.util import PipxError, pipx_wrap logger = logging.getLogger(__name__) class ParsedPackage(NamedTuple): valid_pep508: Optional[Requirement] valid_url: Optional[str] valid_local_path: Optional[str] def _split_path_extras(package_spec: str) -> Tuple[str, str]: """Returns (path, extras_string)""" package_spec_extras_re = re.search(r"(.+)(\[.+\])", package_spec) if package_spec_extras_re: return (package_spec_extras_re.group(1), package_spec_extras_re.group(2)) else: return (package_spec, "") def _parse_specifier(package_spec: str) -> ParsedPackage: """Parse package_spec as would be given to pipx""" # If package_spec is valid pypi name, pip will always treat it as a # pypi package, not checking for local path. # We replicate pypi precedence here (only non-valid-pypi names # initiate check for local path, e.g. './package-name') valid_pep508 = None valid_url = None valid_local_path = None try: package_req = Requirement(package_spec) except InvalidRequirement: # not a valid PEP508 package specification pass else: # valid PEP508 package specification valid_pep508 = package_req # packaging currently (2020-07-19) only does basic syntax checks on URL. # Some examples of what it will not catch: # - invalid RCS string (e.g. "gat+https://...") # - non-existent scheme (e.g. "zzzzz://...") if not valid_pep508: try: package_req = Requirement("notapackagename @ " + package_spec) except InvalidRequirement: # not a valid url pass else: valid_url = package_spec if not valid_pep508 and not valid_url: (package_path_str, package_extras_str) = _split_path_extras(package_spec) package_path = Path(package_path_str) try: package_path_exists = package_path.exists() except OSError: package_path_exists = False if package_path_exists: valid_local_path = str(package_path.resolve()) + package_extras_str if not valid_pep508 and not valid_url and not valid_local_path: raise PipxError(f"Unable to parse package spec: {package_spec}") return ParsedPackage( valid_pep508=valid_pep508, valid_url=valid_url, valid_local_path=valid_local_path, ) def package_or_url_from_pep508( requirement: Requirement, remove_version_specifiers: bool = False ) -> str: requirement.marker = None requirement.name = canonicalize_name(requirement.name) if remove_version_specifiers: requirement.specifier = SpecifierSet("") return str(requirement) def _parsed_package_to_package_or_url( parsed_package: ParsedPackage, remove_version_specifiers: bool ) -> str: if parsed_package.valid_pep508 is not None: if parsed_package.valid_pep508.marker is not None: logger.warning( pipx_wrap( f""" {hazard} Ignoring environment markers ({parsed_package.valid_pep508.marker}) in package specification. Use pipx options to specify this type of information. """, subsequent_indent=" " * 4, ) ) package_or_url = package_or_url_from_pep508( parsed_package.valid_pep508, remove_version_specifiers=remove_version_specifiers, ) elif parsed_package.valid_url is not None: package_or_url = parsed_package.valid_url elif parsed_package.valid_local_path is not None: package_or_url = parsed_package.valid_local_path logger.info(f"cleaned package spec: {package_or_url}") return package_or_url def parse_specifier_for_install( package_spec: str, pip_args: List[str] ) -> Tuple[str, List[str]]: """Return package_or_url and pip_args suitable for pip install Specifically: * Strip any markers (e.g. python_version > "3.4") * Ensure --editable is removed for any package_spec not a local path * Convert local paths to absolute paths """ parsed_package = _parse_specifier(package_spec) package_or_url = _parsed_package_to_package_or_url( parsed_package, remove_version_specifiers=False ) if "--editable" in pip_args and not parsed_package.valid_local_path: logger.warning( pipx_wrap( f""" {hazard} Ignoring --editable install option. pipx disallows it for anything but a local path, to avoid having to create a new src/ directory. """, subsequent_indent=" " * 4, ) ) pip_args.remove("--editable") return (package_or_url, pip_args) def parse_specifier_for_metadata(package_spec: str) -> str: """Return package_or_url suitable for pipx metadata Specifically: * Strip any markers (e.g. python_version > 3.4) * Convert local paths to absolute paths """ parsed_package = _parse_specifier(package_spec) package_or_url = _parsed_package_to_package_or_url( parsed_package, remove_version_specifiers=False ) return package_or_url def parse_specifier_for_upgrade(package_spec: str) -> str: """Return package_or_url suitable for pip upgrade Specifically: * Strip any version specifiers (e.g. package == 1.5.4) * Strip any markers (e.g. python_version > 3.4) * Convert local paths to absolute paths """ parsed_package = _parse_specifier(package_spec) package_or_url = _parsed_package_to_package_or_url( parsed_package, remove_version_specifiers=True ) return package_or_url def get_extras(package_spec: str) -> Set[str]: parsed_package = _parse_specifier(package_spec) if parsed_package.valid_pep508 and parsed_package.valid_pep508.extras is not None: return parsed_package.valid_pep508.extras elif parsed_package.valid_local_path: (_, package_extras_str) = _split_path_extras(parsed_package.valid_local_path) return Requirement("notapackage" + package_extras_str).extras return set() def valid_pypi_name(package_spec: str) -> Optional[str]: try: package_req = Requirement(package_spec) except InvalidRequirement: # not a valid PEP508 package specification return None if package_req.url: # package name supplied by user might not match package found in URL, # so force package name determination the long way return None return canonicalize_name(package_req.name) def fix_package_name(package_or_url: str, package_name: str) -> str: try: package_req = Requirement(package_or_url) except InvalidRequirement: # not a valid PEP508 package specification return package_or_url if canonicalize_name(package_req.name) != canonicalize_name(package_name): logger.warning( pipx_wrap( f""" {hazard} Name supplied in package specifier was {package_req.name!r} but package found has name {package_name!r}. Using {package_name!r}. """, subsequent_indent=" " * 4, ) ) package_req.name = package_name return str(package_req) pipx-1.0.0/src/pipx/pipx_metadata_file.py000066400000000000000000000137761416500503600204460ustar00rootroot00000000000000import json import logging from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional, Union from pipx.emojis import hazard from pipx.util import PipxError, pipx_wrap logger = logging.getLogger(__name__) PIPX_INFO_FILENAME = "pipx_metadata.json" class JsonEncoderHandlesPath(json.JSONEncoder): def default(self, obj: Any) -> Any: # only handles what json.JSONEncoder doesn't understand by default if isinstance(obj, Path): return {"__type__": "Path", "__Path__": str(obj)} return super().default(obj) def _json_decoder_object_hook(json_dict: Dict[str, Any]) -> Union[Dict[str, Any], Path]: if json_dict.get("__type__", None) == "Path" and "__Path__" in json_dict: return Path(json_dict["__Path__"]) return json_dict class PackageInfo(NamedTuple): package: Optional[str] package_or_url: Optional[str] pip_args: List[str] include_dependencies: bool include_apps: bool apps: List[str] app_paths: List[Path] apps_of_dependencies: List[str] app_paths_of_dependencies: Dict[str, List[Path]] package_version: str suffix: str = "" class PipxMetadata: # Only change this if file format changes __METADATA_VERSION__: str = "0.2" def __init__(self, venv_dir: Path, read: bool = True): self.venv_dir = venv_dir # We init this instance with reasonable fallback defaults for all # members, EXCEPT for those we cannot know: # self.main_package.package=None # self.main_package.package_or_url=None # self.python_version=None self.main_package = PackageInfo( package=None, package_or_url=None, pip_args=[], include_dependencies=False, include_apps=True, # always True for main_package apps=[], app_paths=[], apps_of_dependencies=[], app_paths_of_dependencies={}, package_version="", ) self.python_version: Optional[str] = None self.venv_args: List[str] = [] self.injected_packages: Dict[str, PackageInfo] = {} if read: self.read() def to_dict(self) -> Dict[str, Any]: return { "main_package": self.main_package._asdict(), "python_version": self.python_version, "venv_args": self.venv_args, "injected_packages": { name: data._asdict() for (name, data) in self.injected_packages.items() }, "pipx_metadata_version": self.__METADATA_VERSION__, } def _convert_legacy_metadata(self, metadata_dict: Dict[str, Any]) -> Dict[str, Any]: if metadata_dict["pipx_metadata_version"] == self.__METADATA_VERSION__: return metadata_dict elif metadata_dict["pipx_metadata_version"] == "0.1": main_package_data = metadata_dict["main_package"] if main_package_data["package"] != self.venv_dir.name: # handle older suffixed packages gracefully main_package_data["suffix"] = self.venv_dir.name.replace( main_package_data["package"], "" ) return metadata_dict else: raise PipxError( f""" {self.venv_dir.name}: Unknown metadata version {metadata_dict['pipx_metadata_version']}. Perhaps it was installed with a later version of pipx. """ ) def from_dict(self, input_dict: Dict[str, Any]) -> None: input_dict = self._convert_legacy_metadata(input_dict) self.main_package = PackageInfo(**input_dict["main_package"]) self.python_version = input_dict["python_version"] self.venv_args = input_dict["venv_args"] self.injected_packages = { f"{name}{data.get('suffix', '')}": PackageInfo(**data) for (name, data) in input_dict["injected_packages"].items() } def _validate_before_write(self) -> None: if ( self.main_package.package is None or self.main_package.package_or_url is None or not self.main_package.include_apps ): logger.debug(f"PipxMetadata corrupt:\n{self.to_dict()}") raise PipxError("Internal Error: PipxMetadata is corrupt, cannot write.") def write(self) -> None: self._validate_before_write() try: with open(self.venv_dir / PIPX_INFO_FILENAME, "w") as pipx_metadata_fh: json.dump( self.to_dict(), pipx_metadata_fh, indent=4, sort_keys=True, cls=JsonEncoderHandlesPath, ) except IOError: logger.warning( pipx_wrap( f""" {hazard} Unable to write {PIPX_INFO_FILENAME} to {self.venv_dir}. This may cause future pipx operations involving {self.venv_dir.name} to fail or behave incorrectly. """, subsequent_indent=" " * 4, ) ) def read(self, verbose: bool = False) -> None: try: with open(self.venv_dir / PIPX_INFO_FILENAME, "r") as pipx_metadata_fh: self.from_dict( json.load(pipx_metadata_fh, object_hook=_json_decoder_object_hook) ) except IOError: # Reset self if problem reading if verbose: logger.warning( pipx_wrap( f""" {hazard} Unable to read {PIPX_INFO_FILENAME} in {self.venv_dir}. This may cause this or future pipx operations involving {self.venv_dir.name} to fail or behave incorrectly. """, subsequent_indent=" " * 4, ) ) return pipx-1.0.0/src/pipx/shared_libs.py000066400000000000000000000076571416500503600171070ustar00rootroot00000000000000import datetime import logging import time from pathlib import Path from typing import List, Optional from pipx import constants from pipx.animate import animate from pipx.constants import WINDOWS from pipx.interpreter import DEFAULT_PYTHON from pipx.util import ( get_site_packages, get_venv_paths, run_subprocess, subprocess_post_check, ) logger = logging.getLogger(__name__) SHARED_LIBS_MAX_AGE_SEC = datetime.timedelta(days=30).total_seconds() class _SharedLibs: def __init__(self) -> None: self.root = constants.PIPX_SHARED_LIBS self.bin_path, self.python_path = get_venv_paths(self.root) self.pip_path = self.bin_path / ("pip" if not WINDOWS else "pip.exe") # i.e. bin_path is ~/.local/pipx/shared/bin # i.e. python_path is ~/.local/pipx/shared/python self._site_packages: Optional[Path] = None self.has_been_updated_this_run = False self.has_been_logged_this_run = False @property def site_packages(self) -> Path: if self._site_packages is None: self._site_packages = get_site_packages(self.python_path) return self._site_packages def create(self, verbose: bool = False) -> None: if not self.is_valid: with animate("creating shared libraries", not verbose): create_process = run_subprocess( [DEFAULT_PYTHON, "-m", "venv", "--clear", self.root] ) subprocess_post_check(create_process) # ignore installed packages to ensure no unexpected patches from the OS vendor # are used self.upgrade(pip_args=["--force-reinstall"], verbose=verbose) @property def is_valid(self) -> bool: return self.python_path.is_file() and self.pip_path.is_file() @property def needs_upgrade(self) -> bool: if self.has_been_updated_this_run: return False if not self.pip_path.is_file(): return True now = time.time() time_since_last_update_sec = now - self.pip_path.stat().st_mtime if not self.has_been_logged_this_run: logger.info( f"Time since last upgrade of shared libs, in seconds: {time_since_last_update_sec:.0f}. " f"Upgrade will be run by pipx if greater than {SHARED_LIBS_MAX_AGE_SEC:.0f}." ) self.has_been_logged_this_run = True return time_since_last_update_sec > SHARED_LIBS_MAX_AGE_SEC def upgrade( self, *, pip_args: Optional[List[str]] = None, verbose: bool = False ) -> None: if not self.is_valid: self.create(verbose=verbose) return # Don't try to upgrade multiple times per run if self.has_been_updated_this_run: logger.info(f"Already upgraded libraries in {self.root}") return if pip_args is None: pip_args = [] logger.info(f"Upgrading shared libraries in {self.root}") ignored_args = ["--editable"] _pip_args = [arg for arg in pip_args if arg not in ignored_args] if not verbose: _pip_args.append("-q") try: with animate("upgrading shared libraries", not verbose): upgrade_process = run_subprocess( [ self.python_path, "-m", "pip", "--disable-pip-version-check", "install", *_pip_args, "--upgrade", "pip", "setuptools", "wheel", ] ) subprocess_post_check(upgrade_process) self.has_been_updated_this_run = True self.pip_path.touch() except Exception: logger.error("Failed to upgrade shared libraries", exc_info=True) shared_libs = _SharedLibs() pipx-1.0.0/src/pipx/util.py000066400000000000000000000340331416500503600155710ustar00rootroot00000000000000import logging import os import random import re import shutil import string import subprocess import sys import textwrap from pathlib import Path from typing import ( Any, Dict, List, NamedTuple, NoReturn, Optional, Pattern, Sequence, Tuple, Union, ) import pipx.constants from pipx.animate import show_cursor from pipx.constants import PIPX_TRASH_DIR, WINDOWS logger = logging.getLogger(__name__) class PipxError(Exception): def __init__(self, message: str, wrap_message: bool = True): if wrap_message: super().__init__(pipx_wrap(message)) else: super().__init__(message) class RelevantSearch(NamedTuple): pattern: Pattern[str] category: str def _get_trash_file(path: Path) -> Path: if not PIPX_TRASH_DIR.is_dir(): PIPX_TRASH_DIR.mkdir() prefix = "".join(random.choices(string.ascii_lowercase, k=8)) return PIPX_TRASH_DIR / f"{prefix}.{path.name}" def rmdir(path: Path, safe_rm: bool = True) -> None: if not path.is_dir(): return logger.info(f"removing directory {path}") try: if WINDOWS: os.system(f'rmdir /S /Q "{str(path)}"') else: shutil.rmtree(path) except FileNotFoundError: pass # move it to be deleted later if it still exists if path.is_dir(): if safe_rm: logger.warning( f"Failed to delete {path}. Will move it to a temp folder to delete later." ) path.rename(_get_trash_file(path)) else: logger.warning( f"Failed to delete {path}. You may need to delete it manually." ) def mkdir(path: Path) -> None: if path.is_dir(): return logger.info(f"creating directory {path}") path.mkdir(parents=True, exist_ok=True) def safe_unlink(file: Path) -> None: # Windows doesn't let us delete or overwrite files that are being run # But it does let us rename/move it. To get around this issue, we can move # the file to a temporary folder (to be deleted at a later time) if not file.is_file(): return try: file.unlink() except PermissionError: file.rename(_get_trash_file(file)) def get_pypackage_bin_path(binary_name: str) -> Path: return ( Path("__pypackages__") / (str(sys.version_info.major) + "." + str(sys.version_info.minor)) / "lib" / "bin" / binary_name ) def run_pypackage_bin(bin_path: Path, args: List[str]) -> NoReturn: exec_app( [str(bin_path.resolve())] + args, extra_python_paths=[".", str(bin_path.parent.parent)], ) if WINDOWS: def get_venv_paths(root: Path) -> Tuple[Path, Path]: bin_path = root / "Scripts" python_path = bin_path / "python.exe" return bin_path, python_path else: def get_venv_paths(root: Path) -> Tuple[Path, Path]: bin_path = root / "bin" python_path = bin_path / "python" return bin_path, python_path def get_site_packages(python: Path) -> Path: output = run_subprocess( [python, "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"], capture_stderr=False, ).stdout path = Path(output.strip()) path.mkdir(parents=True, exist_ok=True) return path def _fix_subprocess_env(env: Dict[str, str]) -> Dict[str, str]: # Remove PYTHONPATH because some platforms (macOS with Homebrew) add pipx # directories to it, and can make it appear to venvs as though pipx # dependencies are in the venv path (#233) # Remove __PYVENV_LAUNCHER__ because it can cause the wrong python binary # to be used (#334) env_blocklist = ["PYTHONPATH", "__PYVENV_LAUNCHER__"] for env_to_remove in env_blocklist: env.pop(env_to_remove, None) env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" # Make sure that Python writes output in UTF-8 env["PYTHONIOENCODING"] = "utf-8" # Make sure we install package to venv, not userbase dir env["PIP_USER"] = "0" return env def run_subprocess( cmd: Sequence[Union[str, Path]], capture_stdout: bool = True, capture_stderr: bool = True, log_cmd_str: Optional[str] = None, log_stdout: bool = True, log_stderr: bool = True, ) -> "subprocess.CompletedProcess[str]": """Run arbitrary command as subprocess, capturing stderr and stout""" env = dict(os.environ) env = _fix_subprocess_env(env) if log_cmd_str is None: log_cmd_str = " ".join(str(c) for c in cmd) logger.info(f"running {log_cmd_str}") # windows cannot take Path objects, only strings cmd_str_list = [str(c) for c in cmd] completed_process = subprocess.run( cmd_str_list, env=env, stdout=subprocess.PIPE if capture_stdout else None, stderr=subprocess.PIPE if capture_stderr else None, encoding="utf-8", universal_newlines=True, ) if capture_stdout and log_stdout: logger.debug(f"stdout: {completed_process.stdout}".rstrip()) if capture_stderr and log_stderr: logger.debug(f"stderr: {completed_process.stderr}".rstrip()) logger.debug(f"returncode: {completed_process.returncode}") return completed_process def subprocess_post_check( completed_process: "subprocess.CompletedProcess[str]", raise_error: bool = True ) -> None: if completed_process.returncode: if completed_process.stdout is not None: print(completed_process.stdout, file=sys.stdout, end="") if completed_process.stderr is not None: print(completed_process.stderr, file=sys.stderr, end="") if raise_error: raise PipxError( f"{' '.join([str(x) for x in completed_process.args])!r} failed" ) else: logger.info(f"{' '.join(completed_process.args)!r} failed") def dedup_ordered(input_list: List[Any]) -> List[Any]: output_list = [] seen = set() for x in input_list: if x[0] not in seen: output_list.append(x) seen.add(x[0]) return output_list def analyze_pip_output(pip_stdout: str, pip_stderr: str) -> None: r"""Extract useful errors from pip output of failed install Print the module that failed to build Print some of the most relevant errors from the pip output Example pip stderr line for each "relevant" type: not_found Package cairo was not found in the pkg-config search path. src/common.h:34:10: fatal error: 'stdio.h' file not found The headers or library files could not be found for zlib, no_such unable to execute 'gcc': No such file or directory build\test1.c(2): fatal error C1083: Cannot open include file: 'cpuid.h': No such file ... exception_error Exception: Unable to find OpenSSL >= 1.0 headers. (Looked here: ... fatal_error LINK : fatal error LNK1104: cannot open file 'kernel32.lib' conflict_ ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/... error_ error: can't copy 'lib\ansible\module_utils\ansible_release.py': doesn't exist ... build\test1.c(4): error C2146: syntax error: missing ';' before identifier 'x' """ max_relevant_errors = 10 failed_build_stdout: List[str] = [] last_collecting_dep: Optional[str] = None # for any useful information in stdout, `pip install` must be run without # the -q option for line in pip_stdout.split("\n"): failed_match = re.search(r"Failed to build\s+(\S.+)$", line) collecting_match = re.search(r"^\s*Collecting\s+(\S+)", line) if failed_match: failed_build_stdout = failed_match.group(1).strip().split() if collecting_match: last_collecting_dep = collecting_match.group(1) # In order of most useful to least useful relevant_searches = [ RelevantSearch(re.compile(r"not (?:be )?found", re.I), "not_found"), RelevantSearch(re.compile(r"no such", re.I), "no_such"), RelevantSearch(re.compile(r"(Exception|Error):\s*\S+"), "exception_error"), RelevantSearch(re.compile(r"fatal error", re.I), "fatal_error"), RelevantSearch(re.compile(r"conflict", re.I), "conflict_"), RelevantSearch( re.compile( r"error:" r"(?!.+Command errored out)" r"(?!.+failed building wheel for)" r"(?!.+could not build wheels? for)" r"(?!.+failed to build one or more wheels)" r".+[^:]$", re.I, ), "error_", ), ] failed_stderr_patt = re.compile(r"Failed to build\s+(?!one or more packages)(\S+)") relevants_saved = [] failed_build_stderr = set() for line in pip_stderr.split("\n"): failed_build_match = failed_stderr_patt.search(line) if failed_build_match: failed_build_stderr.add(failed_build_match.group(1)) for relevant_search in relevant_searches: if relevant_search.pattern.search(line): relevants_saved.append((line.strip(), relevant_search.category)) break if failed_build_stdout: failed_to_build_str = "\n ".join(failed_build_stdout) plural_str = "s" if len(failed_build_stdout) > 1 else "" print("", file=sys.stderr) logger.error( f"pip failed to build package{plural_str}:\n {failed_to_build_str}" ) elif failed_build_stderr: failed_to_build_str = "\n ".join(failed_build_stderr) plural_str = "s" if len(failed_build_stderr) > 1 else "" print("", file=sys.stderr) logger.error( f"pip seemed to fail to build package{plural_str}:\n {failed_to_build_str}" ) elif last_collecting_dep is not None: print("", file=sys.stderr) logger.error(f"pip seemed to fail to build package:\n {last_collecting_dep}") relevants_saved = dedup_ordered(relevants_saved) if relevants_saved: print("\nSome possibly relevant errors from pip install:", file=sys.stderr) print_categories = [x.category for x in relevant_searches] relevants_saved_filtered = relevants_saved.copy() while (len(print_categories) > 1) and ( len(relevants_saved_filtered) > max_relevant_errors ): print_categories.pop(-1) relevants_saved_filtered = [ x for x in relevants_saved if x[1] in print_categories ] for relevant_saved in relevants_saved_filtered: print(f" {relevant_saved[0]}", file=sys.stderr) def subprocess_post_check_handle_pip_error( completed_process: "subprocess.CompletedProcess[str]", ) -> None: if completed_process.returncode: logger.info(f"{' '.join(completed_process.args)!r} failed") # Save STDOUT and STDERR to file in pipx/logs/ if pipx.constants.pipx_log_file is None: raise PipxError("Pipx internal error: No log_file present.") pip_error_file = pipx.constants.pipx_log_file.parent / ( pipx.constants.pipx_log_file.stem + "_pip_errors.log" ) with pip_error_file.open("w") as pip_error_fh: print("PIP STDOUT", file=pip_error_fh) print("----------", file=pip_error_fh) if completed_process.stdout is not None: print(completed_process.stdout, file=pip_error_fh, end="") print("\nPIP STDERR", file=pip_error_fh) print("----------", file=pip_error_fh) if completed_process.stderr is not None: print(completed_process.stderr, file=pip_error_fh, end="") logger.error( "Fatal error from pip prevented installation. Full pip output in file:\n" f" {pip_error_file}" ) analyze_pip_output(completed_process.stdout, completed_process.stderr) def exec_app( cmd: Sequence[Union[str, Path]], env: Optional[Dict[str, str]] = None, extra_python_paths: Optional[List[str]] = None, ) -> NoReturn: """Run command, do not return POSIX: replace current processs with command using os.exec*() Windows: Use subprocess and sys.exit() to run command """ if env is None: env = dict(os.environ) env = _fix_subprocess_env(env) if extra_python_paths is not None: env["PYTHONPATH"] = os.path.pathsep.join( extra_python_paths + ( os.getenv("PYTHONPATH", "").split(os.path.pathsep) if os.getenv("PYTHONPATH") else [] ) ) # make sure we show cursor again before handing over control show_cursor() logger.info("exec_app: " + " ".join([str(c) for c in cmd])) if WINDOWS: sys.exit( subprocess.run( cmd, env=env, stdout=None, stderr=None, encoding="utf-8", universal_newlines=True, ).returncode ) else: os.execvpe(str(cmd[0]), [str(x) for x in cmd], env) def full_package_description(package_name: str, package_spec: str) -> str: if package_name == package_spec: return package_name else: return f"{package_name} from spec {package_spec!r}" def pipx_wrap( text: str, subsequent_indent: str = "", keep_newlines: bool = False ) -> str: """Dedent, strip, wrap to shell width. Don't break on hyphens, only spaces""" minimum_width = 40 width = max(shutil.get_terminal_size((80, 40)).columns, minimum_width) - 2 text = textwrap.dedent(text).strip() if keep_newlines: return "\n".join( [ textwrap.fill( line, width=width, subsequent_indent=subsequent_indent, break_on_hyphens=False, ) for line in text.splitlines() ] ) else: return textwrap.fill( text, width=width, subsequent_indent=subsequent_indent, break_on_hyphens=False, ) pipx-1.0.0/src/pipx/venv.py000066400000000000000000000373701416500503600156010ustar00rootroot00000000000000import json import logging import re import time from pathlib import Path from subprocess import CompletedProcess from typing import Dict, Generator, List, NoReturn, Optional, Set try: from importlib.metadata import Distribution, EntryPoint except ImportError: from importlib_metadata import Distribution, EntryPoint # type: ignore from packaging.utils import canonicalize_name from pipx.animate import animate from pipx.constants import PIPX_SHARED_PTH, ExitCode from pipx.emojis import hazard from pipx.interpreter import DEFAULT_PYTHON from pipx.package_specifier import ( fix_package_name, get_extras, parse_specifier_for_install, parse_specifier_for_metadata, ) from pipx.pipx_metadata_file import PackageInfo, PipxMetadata from pipx.shared_libs import shared_libs from pipx.util import ( PipxError, exec_app, full_package_description, get_site_packages, get_venv_paths, pipx_wrap, rmdir, run_subprocess, subprocess_post_check, subprocess_post_check_handle_pip_error, ) from pipx.venv_inspect import VenvMetadata, inspect_venv logger = logging.getLogger(__name__) _entry_point_value_pattern = re.compile( r""" ^(?P[\w.]+)\s* (:\s*(?P[\w.]+))?\s* (?P\[.*\])?\s*$ """, re.VERBOSE, ) class VenvContainer: """A collection of venvs managed by pipx.""" def __init__(self, root: Path): self._root = root def __repr__(self) -> str: return f"VenvContainer({str(self._root)!r})" def __str__(self) -> str: return str(self._root) def iter_venv_dirs(self) -> Generator[Path, None, None]: """Iterate venv directories in this container.""" if not self._root.is_dir(): return for entry in self._root.iterdir(): if not entry.is_dir(): continue yield entry def get_venv_dir(self, package_name: str) -> Path: """Return the expected venv path for given `package_name`.""" return self._root.joinpath(canonicalize_name(package_name)) def verify_shared_libs(self) -> None: for p in self.iter_venv_dirs(): Venv(p) class Venv: """Abstraction for a virtual environment with various useful methods for pipx""" def __init__( self, path: Path, *, verbose: bool = False, python: str = DEFAULT_PYTHON ) -> None: self.root = path self.python = python self.bin_path, self.python_path = get_venv_paths(self.root) self.pipx_metadata = PipxMetadata(venv_dir=path) self.verbose = verbose self.do_animation = not verbose try: self._existing = self.root.exists() and bool(next(self.root.iterdir())) except StopIteration: self._existing = False if self._existing and self.uses_shared_libs: if shared_libs.is_valid: if shared_libs.needs_upgrade: shared_libs.upgrade(verbose=verbose) else: shared_libs.create(verbose) if not shared_libs.is_valid: raise PipxError( pipx_wrap( f""" Error: pipx's shared venv {shared_libs.root} is invalid and needs re-installation. To fix this, install or reinstall a package. For example: """ ) + f"\n pipx install {self.root.name} --force", wrap_message=False, ) @property def name(self) -> str: if self.pipx_metadata.main_package.package is not None: venv_name = ( f"{self.pipx_metadata.main_package.package}" f"{self.pipx_metadata.main_package.suffix}" ) else: venv_name = self.root.name return venv_name @property def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None else: # always use shared libs when creating a new venv return True @property def package_metadata(self) -> Dict[str, PackageInfo]: return_dict = self.pipx_metadata.injected_packages.copy() if self.pipx_metadata.main_package.package is not None: return_dict[ self.pipx_metadata.main_package.package ] = self.pipx_metadata.main_package return return_dict @property def main_package_name(self) -> str: if self.pipx_metadata.main_package.package is None: # This is OK, because if no metadata, we are pipx < v0.15.0.0 and # venv_name==main_package_name return self.root.name else: return self.pipx_metadata.main_package.package def create_venv(self, venv_args: List[str], pip_args: List[str]) -> None: with animate("creating virtual environment", self.do_animation): cmd = [self.python, "-m", "venv", "--without-pip"] venv_process = run_subprocess(cmd + venv_args + [str(self.root)]) subprocess_post_check(venv_process) shared_libs.create(self.verbose) pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: # ~/.local/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth # example shared_libs.site_packages location: # ~/.local/pipx/shared/lib/python3.6/site-packages # # https://docs.python.org/3/library/site.html # A path configuration file is a file whose name has the form 'name.pth'. # its contents are additional items (one per line) to be added to sys.path pipx_pth.write_text(f"{shared_libs.site_packages}\n", encoding="utf-8") self.pipx_metadata.venv_args = venv_args self.pipx_metadata.python_version = self.get_python_version() def safe_to_remove(self) -> bool: return not self._existing def remove_venv(self) -> None: if self.safe_to_remove(): rmdir(self.root) else: logger.warning( pipx_wrap( f""" {hazard} Not removing existing venv {self.root} because it was not created in this session """, subsequent_indent=" " * 4, ) ) def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: if self.uses_shared_libs: shared_libs.upgrade(verbose=self.verbose) else: # TODO: setuptools and wheel? Original code didn't bother # but shared libs code does. self._upgrade_package_no_metadata("pip", pip_args) def install_package( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: # package_name in package specifier can mismatch URL due to user error package_or_url = fix_package_name(package_or_url, package_name) # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install( package_or_url, pip_args ) with animate( f"installing {full_package_description(package_name, package_or_url)}", self.do_animation, ): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. cmd = ( [str(self.python_path), "-m", "pip", "install"] + pip_args + [package_or_url] ) # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False) subprocess_post_check_handle_pip_error(pip_process) if pip_process.returncode: raise PipxError( f"Error installing {full_package_description(package_name, package_or_url)}." ) self._update_package_metadata( package_name=package_name, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) # Verify package installed ok if self.package_metadata[package_name].package_version is None: raise PipxError( f"Unable to install " f"{full_package_description(package_name, package_or_url)}.\n" f"Check the name or spec for errors, and verify that it can " f"be installed with pip.", wrap_message=False, ) def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> str: with animate( f"determining package name from {package_or_url!r}", self.do_animation ): old_package_set = self.list_installed_packages() cmd = ["install"] + ["--no-dependencies"] + pip_args + [package_or_url] pip_process = self._run_pip(cmd) subprocess_post_check(pip_process, raise_error=False) if pip_process.returncode: raise PipxError( f""" Cannot determine package name from spec {package_or_url!r}. Check package spec for errors. """ ) installed_packages = self.list_installed_packages() - old_package_set if len(installed_packages) == 1: package_name = installed_packages.pop() logger.info(f"Determined package name: {package_name}") else: logger.info(f"old_package_set = {old_package_set}") logger.info(f"install_packages = {installed_packages}") raise PipxError( f""" Cannot determine package name from spec {package_or_url!r}. Check package spec for errors. """ ) return package_name def get_venv_metadata_for_package( self, package_name: str, package_extras: Set[str] ) -> VenvMetadata: data_start = time.time() venv_metadata = inspect_venv( package_name, package_extras, self.bin_path, self.python_path ) logger.info( f"get_venv_metadata_for_package: {1e3*(time.time()-data_start):.0f}ms" ) return venv_metadata def _update_package_metadata( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: venv_package_metadata = self.get_venv_metadata_for_package( package_name, get_extras(package_or_url) ) package_info = PackageInfo( package=package_name, package_or_url=parse_specifier_for_metadata(package_or_url), pip_args=pip_args, include_apps=include_apps, include_dependencies=include_dependencies, apps=venv_package_metadata.apps, app_paths=venv_package_metadata.app_paths, apps_of_dependencies=venv_package_metadata.apps_of_dependencies, app_paths_of_dependencies=venv_package_metadata.app_paths_of_dependencies, package_version=venv_package_metadata.package_version, suffix=suffix, ) if is_main_package: self.pipx_metadata.main_package = package_info else: self.pipx_metadata.injected_packages[package_name] = package_info self.pipx_metadata.write() def get_python_version(self) -> str: return run_subprocess([str(self.python_path), "--version"]).stdout.strip() def list_installed_packages(self) -> Set[str]: cmd_run = run_subprocess( [str(self.python_path), "-m", "pip", "list", "--format=json"] ) pip_list = json.loads(cmd_run.stdout.strip()) return set([x["name"] for x in pip_list]) def _find_entry_point(self, app: str) -> Optional[EntryPoint]: if not self.python_path.exists(): return None dists = Distribution.discover( name=self.main_package_name, path=[str(get_site_packages(self.python_path))] ) for dist in dists: for ep in dist.entry_points: if ep.group == "pipx.run" and ep.name == app: return ep return None def run_app(self, app: str, filename: str, app_args: List[str]) -> NoReturn: entry_point = self._find_entry_point(app) # No [pipx.run] entry point; default to run console script. if entry_point is None: exec_app([str(self.bin_path / filename)] + app_args) # Evaluate and execute the entry point. # TODO: After dropping support for Python < 3.9, use # "entry_point.module" and "entry_point.attr" instead. match = _entry_point_value_pattern.match(entry_point.value) assert match is not None, "invalid entry point" module, attr = match.group("module", "attr") code = ( f"import sys, {module}\n" f"sys.argv[0] = {entry_point.name!r}\n" f"sys.exit({module}.{attr}())\n" ) exec_app([str(self.python_path), "-c", code] + app_args) def has_app(self, app: str, filename: str) -> bool: if self._find_entry_point(app) is not None: return True return (self.bin_path / filename).is_file() def _upgrade_package_no_metadata( self, package_name: str, pip_args: List[str] ) -> None: with animate( f"upgrading {full_package_description(package_name, package_name)}", self.do_animation, ): pip_process = self._run_pip( ["install"] + pip_args + ["--upgrade", package_name] ) subprocess_post_check(pip_process) def upgrade_package( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: with animate( f"upgrading {full_package_description(package_name, package_or_url)}", self.do_animation, ): pip_process = self._run_pip( ["install"] + pip_args + ["--upgrade", package_or_url] ) subprocess_post_check(pip_process) self._update_package_metadata( package_name=package_name, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) def _run_pip(self, cmd: List[str]) -> "CompletedProcess[str]": cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") return run_subprocess(cmd) def run_pip_get_exit_code(self, cmd: List[str]) -> ExitCode: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") returncode = run_subprocess( cmd, capture_stdout=False, capture_stderr=False ).returncode if returncode: cmd_str = " ".join(str(c) for c in cmd) logger.error(f"{cmd_str!r} failed") return ExitCode(returncode) pipx-1.0.0/src/pipx/venv_inspect.py000066400000000000000000000223041416500503600173150ustar00rootroot00000000000000import json import logging import textwrap from pathlib import Path from typing import Dict, List, NamedTuple, Optional, Set, Tuple from packaging.requirements import Requirement from packaging.utils import canonicalize_name try: from importlib import metadata except ImportError: import importlib_metadata as metadata # type: ignore from pipx.constants import WINDOWS from pipx.util import PipxError, run_subprocess logger = logging.getLogger(__name__) class VenvInspectInformation(NamedTuple): distributions: List[metadata.Distribution] env: Dict[str, str] bin_path: Path class VenvMetadata(NamedTuple): apps: List[str] app_paths: List[Path] apps_of_dependencies: List[str] app_paths_of_dependencies: Dict[str, List[Path]] package_version: str python_version: str def get_dist( package: str, distributions: List[metadata.Distribution] ) -> Optional[metadata.Distribution]: """Find matching distribution in the canonicalized sense.""" for dist in distributions: if canonicalize_name(dist.metadata["name"]) == canonicalize_name(package): return dist return None def get_package_dependencies( dist: metadata.Distribution, extras: Set[str], env: Dict[str, str] ) -> List[Requirement]: eval_env = env.copy() # Add an empty extra to enable evaluation of non-extra markers if not extras: extras.add("") dependencies = [] for req in map(Requirement, dist.requires or []): if not req.marker: dependencies.append(req) else: for extra in extras: eval_env["extra"] = extra if req.marker.evaluate(eval_env): dependencies.append(req) break return dependencies def get_apps(dist: metadata.Distribution, bin_path: Path) -> List[str]: apps = set() sections = {"console_scripts", "gui_scripts"} # "entry_points" entry in setup.py are found here for ep in dist.entry_points: if ep.group not in sections: continue if (bin_path / ep.name).exists(): apps.add(ep.name) if WINDOWS and (bin_path / (ep.name + ".exe")).exists(): # WINDOWS adds .exe to entry_point name apps.add(ep.name + ".exe") # search installed files # "scripts" entry in setup.py is found here (test w/ awscli) for path in dist.files or []: # vast speedup by ignoring all paths not above distribution root dir # (venv/bin or venv/Scripts is above distribution root) if Path(path).parts[0] != "..": continue dist_file_path = Path(dist.locate_file(path)) try: if dist_file_path.parent.samefile(bin_path): apps.add(path.name) except FileNotFoundError: pass # not sure what is found here inst_files = dist.read_text("installed-files.txt") or "" for line in inst_files.splitlines(): entry = line.split(",")[0] # noqa: T484 inst_file_path = Path(dist.locate_file(entry)).resolve() try: if inst_file_path.parent.samefile(bin_path): apps.add(inst_file_path.name) except FileNotFoundError: pass return sorted(apps) def _dfs_package_apps( dist: metadata.Distribution, package_req: Requirement, venv_inspect_info: VenvInspectInformation, app_paths_of_dependencies: Dict[str, List[Path]], dep_visited: Optional[Dict[str, bool]] = None, ) -> Dict[str, List[Path]]: if dep_visited is None: # Initialize: we have already visited root dep_visited = {canonicalize_name(package_req.name): True} dependencies = get_package_dependencies( dist, package_req.extras, venv_inspect_info.env ) for dep_req in dependencies: dep_name = canonicalize_name(dep_req.name) if dep_name in dep_visited: # avoid infinite recursion, avoid duplicates in info continue dep_dist = get_dist(dep_req.name, venv_inspect_info.distributions) if dep_dist is None: raise PipxError( "Pipx Internal Error: cannot find package {dep_req.name!r} metadata." ) app_names = get_apps(dep_dist, venv_inspect_info.bin_path) if app_names: app_paths_of_dependencies[dep_name] = [ venv_inspect_info.bin_path / app for app in app_names ] # recursively search for more dep_visited[dep_name] = True app_paths_of_dependencies = _dfs_package_apps( dep_dist, dep_req, venv_inspect_info, app_paths_of_dependencies, dep_visited ) return app_paths_of_dependencies def _windows_extra_app_paths(app_paths: List[Path]) -> List[Path]: # In Windows, editable package have additional files starting with the # same name that are required to be in the same dir to run the app # Add "*-script.py", "*.exe.manifest" only to app_paths to make # execution work; do not add them to apps to ensure they are not listed app_paths_output = app_paths.copy() for app_path in app_paths: win_app_path = app_path.parent / (app_path.stem + "-script.py") if win_app_path.exists(): app_paths_output.append(win_app_path) win_app_path = app_path.parent / (app_path.stem + ".exe.manifest") if win_app_path.exists(): app_paths_output.append(win_app_path) return app_paths_output def fetch_info_in_venv(venv_python_path: Path) -> Tuple[List[str], Dict[str, str], str]: command_str = textwrap.dedent( """ import json import os import platform import sys impl_ver = sys.implementation.version implementation_version = "{0.major}.{0.minor}.{0.micro}".format(impl_ver) if impl_ver.releaselevel != "final": implementation_version = "{}{}{}".format( implementation_version, impl_ver.releaselevel[0], impl_ver.serial, ) sys_path = sys.path try: sys_path.remove("") except ValueError: pass print( json.dumps( { "sys_path": sys_path, "python_version": "{0.major}.{0.minor}.{0.micro}".format(sys.version_info), "environment": { "implementation_name": sys.implementation.name, "implementation_version": implementation_version, "os_name": os.name, "platform_machine": platform.machine(), "platform_release": platform.release(), "platform_system": platform.system(), "platform_version": platform.version(), "python_full_version": platform.python_version(), "platform_python_implementation": platform.python_implementation(), "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, }, } ) ) """ ) venv_info = json.loads( run_subprocess( [venv_python_path, "-c", command_str], capture_stderr=False, log_cmd_str="", ).stdout ) return ( venv_info["sys_path"], venv_info["environment"], f"Python {venv_info['python_version']}", ) def inspect_venv( root_package_name: str, root_package_extras: Set[str], venv_bin_path: Path, venv_python_path: Path, ) -> VenvMetadata: app_paths_of_dependencies: Dict[str, List[Path]] = {} apps_of_dependencies: List[str] = [] root_req = Requirement(root_package_name) root_req.extras = root_package_extras (venv_sys_path, venv_env, venv_python_version) = fetch_info_in_venv( venv_python_path ) venv_inspect_info = VenvInspectInformation( bin_path=venv_bin_path, env=venv_env, distributions=list(metadata.distributions(path=venv_sys_path)), ) root_dist = get_dist(root_req.name, venv_inspect_info.distributions) if root_dist is None: raise PipxError( "Pipx Internal Error: cannot find package {root_req.name!r} metadata." ) app_paths_of_dependencies = _dfs_package_apps( root_dist, root_req, venv_inspect_info, app_paths_of_dependencies ) apps = get_apps(root_dist, venv_bin_path) app_paths = [venv_bin_path / app for app in apps] if WINDOWS: app_paths = _windows_extra_app_paths(app_paths) for dep in app_paths_of_dependencies: apps_of_dependencies += [ dep_path.name for dep_path in app_paths_of_dependencies[dep] ] if WINDOWS: app_paths_of_dependencies[dep] = _windows_extra_app_paths( app_paths_of_dependencies[dep] ) venv_metadata = VenvMetadata( apps=apps, app_paths=app_paths, apps_of_dependencies=apps_of_dependencies, app_paths_of_dependencies=app_paths_of_dependencies, package_version=root_dist.version, python_version=venv_python_version, ) return venv_metadata pipx-1.0.0/src/pipx/version.py000066400000000000000000000001261416500503600162750ustar00rootroot00000000000000__version_info__ = (1, 0, 0) __version__ = ".".join(str(i) for i in __version_info__) pipx-1.0.0/templates/000077500000000000000000000000001416500503600144665ustar00rootroot00000000000000pipx-1.0.0/templates/docs.md000066400000000000000000000005111416500503600157350ustar00rootroot00000000000000{{ usage }} ### pipx install {{ install }} ### pipx run {{run}} ### pipx upgrade {{upgrade}} ### pipx upgrade-all {{upgradeall}} ### pipx inject {{inject}} ### pipx uninstall {{uninstall}} ### pipx uninstall-all {{uninstallall}} ### pipx reinstall-all {{reinstallall}} ### pipx list {{list}} ### pipx runpip {{runpip}} pipx-1.0.0/testdata/000077500000000000000000000000001416500503600143015ustar00rootroot00000000000000pipx-1.0.0/testdata/test_package_specifier/000077500000000000000000000000001416500503600207645ustar00rootroot00000000000000pipx-1.0.0/testdata/test_package_specifier/local_extras/000077500000000000000000000000001416500503600234445ustar00rootroot00000000000000pipx-1.0.0/testdata/test_package_specifier/local_extras/repeatme/000077500000000000000000000000001416500503600252465ustar00rootroot00000000000000pipx-1.0.0/testdata/test_package_specifier/local_extras/repeatme/__init__.py000066400000000000000000000000001416500503600273450ustar00rootroot00000000000000pipx-1.0.0/testdata/test_package_specifier/local_extras/repeatme/main.py000066400000000000000000000004321416500503600265430ustar00rootroot00000000000000import sys try: import pycowsay.main has_pycowsay = True except ImportError: has_pycowsay = False def main(): print(f"You said:\n {' '.join(sys.argv[1:])}") if has_pycowsay: print() print("In cow, you said:") pycowsay.main.main() pipx-1.0.0/testdata/test_package_specifier/local_extras/setup.py000066400000000000000000000004071416500503600251570ustar00rootroot00000000000000from setuptools import setup setup( name="repeatme", version=0.1, description="Repeat arguments.", packages=["repeatme"], extras_require={"cow": ["pycowsay==0.0.0.1"]}, entry_points={"console_scripts": ["repeatme=repeatme.main:main"]}, ) pipx-1.0.0/testdata/tests_packages/000077500000000000000000000000001416500503600173015ustar00rootroot00000000000000pipx-1.0.0/testdata/tests_packages/README.md000066400000000000000000000024041416500503600205600ustar00rootroot00000000000000# Introduction `primary_packages.txt` is the master list, containing all packages installed or injected in the pipx tests `tests`. Platform-specific list files listing both these primary packages and their dependencies are generated from it. These platform-specific list files are used to populate the directory `.pipx_tests/package_cache`. # Generating the platform-specific lists from the master list Using the Github Workflow * Make sure that the file in this directory `primary_packages.txt` is up to date for every package & version that is installed or injected in the tests. * Manually activate the Github workflow: Create tests package lists for offline tests * Download the artifact `lists` and put the files from it into this directory. Or to locally generate these lists, on the target platform execute: * `nox -s create_test_package_list` # Updating / Populating the directory `.pipx_tests/package_cache` before running the tests Pre-populating this directory allows the pipx `tests` to run completely offline. Nox instructions * execute `nox -s refresh_packages_cache` Or manually execute from the top-level pipx repo directory: * `mkdir -p .pipx_tests/package_cache` * `python3 scripts/update_package_cache.py testdata/tests_packages .pipx_tests/package_cache` pipx-1.0.0/testdata/tests_packages/macos19-python3.6.txt000066400000000000000000000151721416500503600230720ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2-cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 dataclasses==0.8 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 importlib_resources==5.1.4 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc-core==7.2 pyobjc-framework-Cocoa==7.2 pyobjc-framework-CoreAudio==7.2 pyobjc-framework-CoreMedia==7.2 pyobjc-framework-CoreText==7.2 pyobjc-framework-FileProvider==7.2 pyobjc-framework-Quartz==7.2 pyobjc-framework-Security==7.2 pyobjc-framework-libdispatch==7.2 pyobjc==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==4.3.3 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos19-python3.7.txt000066400000000000000000000151671416500503600230770ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos19-python3.8.txt000066400000000000000000000151671416500503600231000ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos19-python3.9.txt000066400000000000000000000151671416500503600231010ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos20-python3.6.txt000066400000000000000000000162141416500503600230600ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.0 Jinja2==3.0.0 MarkupSafe==2.0.0 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.0 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2-cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.73 botocore==1.19.8 botocore==1.20.73 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.0 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 dataclasses==0.8 decorator==5.0.8 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.0.1 importlib_resources==5.1.3 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.0 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.9 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.7.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.3.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.1 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc-core==7.2 pyobjc-framework-Cocoa==7.2 pyobjc-framework-CoreAudio==7.2 pyobjc-framework-CoreMedia==7.2 pyobjc-framework-CoreText==7.2 pyobjc-framework-FileProvider==7.2 pyobjc-framework-Quartz==7.2 pyobjc-framework-Security==7.2 pyobjc-framework-libdispatch==7.2 pyobjc==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accessibility==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdServices==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppTrackingTransparency==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CallKit==7.2 pyobjc_framework_ClassKit==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_KernelManagement==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MLCompute==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_MetalPerformanceShadersGraph==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PassKit==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_ReplayKit==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScreenTime==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UniformTypeIdentifiers==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_UserNotificationsUI==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Virtualization==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.0.3 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==56.2.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 slugify==0.0.1 soupsieve==2.2.1 tempora==4.0.2 terminado==0.9.5 testpath==0.4.4 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.60.0 traitlets==4.3.3 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.4 virtualenv==20.4.6 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos20-python3.7.txt000066400000000000000000000162111416500503600230560ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.0 Jinja2==3.0.0 MarkupSafe==2.0.0 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.0 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.73 botocore==1.19.8 botocore==1.20.73 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.0 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.8 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.0.1 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.0 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.9 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.7.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.3.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.1 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accessibility==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdServices==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppTrackingTransparency==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CallKit==7.2 pyobjc_framework_ClassKit==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_KernelManagement==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MLCompute==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_MetalPerformanceShadersGraph==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PassKit==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_ReplayKit==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScreenTime==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UniformTypeIdentifiers==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_UserNotificationsUI==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Virtualization==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.0.3 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==56.2.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 slugify==0.0.1 soupsieve==2.2.1 tempora==4.0.2 terminado==0.9.5 testpath==0.4.4 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.60.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.4 virtualenv==20.4.6 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos20-python3.8.txt000066400000000000000000000162111416500503600230570ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.0 Jinja2==3.0.0 MarkupSafe==2.0.0 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.0 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.73 botocore==1.19.8 botocore==1.20.73 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.0 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.8 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.0.1 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.0 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.9 keyring==23.0.1 keyrings.alt==4.0.2 lazy-object-proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.7.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.3.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.1 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accessibility==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdServices==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppTrackingTransparency==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CallKit==7.2 pyobjc_framework_ClassKit==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_KernelManagement==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MLCompute==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_MetalPerformanceShadersGraph==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PassKit==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_ReplayKit==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScreenTime==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UniformTypeIdentifiers==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_UserNotificationsUI==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Virtualization==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.0.3 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==56.2.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 slugify==0.0.1 soupsieve==2.2.1 tempora==4.0.2 terminado==0.9.5 testpath==0.4.4 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.60.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.4 virtualenv==20.4.6 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/macos20-python3.9.txt000066400000000000000000000162121416500503600230610ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.0 Jinja2==3.0.0 MarkupSafe==2.0.0 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.0 ansible==2.9.13 appdirs==1.4.4 appnope==0.1.2 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.73 botocore==1.19.8 botocore==1.20.73 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.0 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.8 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.0.1 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.0 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.7.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.3.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.1 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyobjc==7.2 pyobjc_core==7.2 pyobjc_framework_AVFoundation==7.2 pyobjc_framework_AVKit==7.2 pyobjc_framework_Accessibility==7.2 pyobjc_framework_Accounts==7.2 pyobjc_framework_AdServices==7.2 pyobjc_framework_AdSupport==7.2 pyobjc_framework_AddressBook==7.2 pyobjc_framework_AppTrackingTransparency==7.2 pyobjc_framework_AppleScriptKit==7.2 pyobjc_framework_AppleScriptObjC==7.2 pyobjc_framework_ApplicationServices==7.2 pyobjc_framework_AuthenticationServices==7.2 pyobjc_framework_AutomaticAssessmentConfiguration==7.2 pyobjc_framework_Automator==7.2 pyobjc_framework_BusinessChat==7.2 pyobjc_framework_CFNetwork==7.2 pyobjc_framework_CalendarStore==7.2 pyobjc_framework_CallKit==7.2 pyobjc_framework_ClassKit==7.2 pyobjc_framework_CloudKit==7.2 pyobjc_framework_Cocoa==7.2 pyobjc_framework_Collaboration==7.2 pyobjc_framework_ColorSync==7.2 pyobjc_framework_Contacts==7.2 pyobjc_framework_ContactsUI==7.2 pyobjc_framework_CoreAudio==7.2 pyobjc_framework_CoreAudioKit==7.2 pyobjc_framework_CoreBluetooth==7.2 pyobjc_framework_CoreData==7.2 pyobjc_framework_CoreHaptics==7.2 pyobjc_framework_CoreLocation==7.2 pyobjc_framework_CoreMIDI==7.2 pyobjc_framework_CoreML==7.2 pyobjc_framework_CoreMedia==7.2 pyobjc_framework_CoreMediaIO==7.2 pyobjc_framework_CoreMotion==7.2 pyobjc_framework_CoreServices==7.2 pyobjc_framework_CoreSpotlight==7.2 pyobjc_framework_CoreText==7.2 pyobjc_framework_CoreWLAN==7.2 pyobjc_framework_CryptoTokenKit==7.2 pyobjc_framework_DVDPlayback==7.2 pyobjc_framework_DeviceCheck==7.2 pyobjc_framework_DictionaryServices==7.2 pyobjc_framework_DiscRecording==7.2 pyobjc_framework_DiscRecordingUI==7.2 pyobjc_framework_DiskArbitration==7.2 pyobjc_framework_EventKit==7.2 pyobjc_framework_ExceptionHandling==7.2 pyobjc_framework_ExecutionPolicy==7.2 pyobjc_framework_ExternalAccessory==7.2 pyobjc_framework_FSEvents==7.2 pyobjc_framework_FileProvider==7.2 pyobjc_framework_FileProviderUI==7.2 pyobjc_framework_FinderSync==7.2 pyobjc_framework_GameCenter==7.2 pyobjc_framework_GameController==7.2 pyobjc_framework_GameKit==7.2 pyobjc_framework_GameplayKit==7.2 pyobjc_framework_IMServicePlugIn==7.2 pyobjc_framework_IOSurface==7.2 pyobjc_framework_ImageCaptureCore==7.2 pyobjc_framework_InputMethodKit==7.2 pyobjc_framework_InstallerPlugins==7.2 pyobjc_framework_InstantMessage==7.2 pyobjc_framework_Intents==7.2 pyobjc_framework_KernelManagement==7.2 pyobjc_framework_LatentSemanticMapping==7.2 pyobjc_framework_LaunchServices==7.2 pyobjc_framework_LinkPresentation==7.2 pyobjc_framework_LocalAuthentication==7.2 pyobjc_framework_MLCompute==7.2 pyobjc_framework_MapKit==7.2 pyobjc_framework_MediaAccessibility==7.2 pyobjc_framework_MediaLibrary==7.2 pyobjc_framework_MediaPlayer==7.2 pyobjc_framework_MediaToolbox==7.2 pyobjc_framework_Metal==7.2 pyobjc_framework_MetalKit==7.2 pyobjc_framework_MetalPerformanceShaders==7.2 pyobjc_framework_MetalPerformanceShadersGraph==7.2 pyobjc_framework_ModelIO==7.2 pyobjc_framework_MultipeerConnectivity==7.2 pyobjc_framework_NaturalLanguage==7.2 pyobjc_framework_NetFS==7.2 pyobjc_framework_Network==7.2 pyobjc_framework_NetworkExtension==7.2 pyobjc_framework_NotificationCenter==7.2 pyobjc_framework_OSAKit==7.2 pyobjc_framework_OSLog==7.2 pyobjc_framework_OpenDirectory==7.2 pyobjc_framework_PassKit==7.2 pyobjc_framework_PencilKit==7.2 pyobjc_framework_Photos==7.2 pyobjc_framework_PhotosUI==7.2 pyobjc_framework_PreferencePanes==7.2 pyobjc_framework_PushKit==7.2 pyobjc_framework_Quartz==7.2 pyobjc_framework_QuickLookThumbnailing==7.2 pyobjc_framework_ReplayKit==7.2 pyobjc_framework_SafariServices==7.2 pyobjc_framework_SceneKit==7.2 pyobjc_framework_ScreenSaver==7.2 pyobjc_framework_ScreenTime==7.2 pyobjc_framework_ScriptingBridge==7.2 pyobjc_framework_SearchKit==7.2 pyobjc_framework_Security==7.2 pyobjc_framework_SecurityFoundation==7.2 pyobjc_framework_SecurityInterface==7.2 pyobjc_framework_ServiceManagement==7.2 pyobjc_framework_Social==7.2 pyobjc_framework_SoundAnalysis==7.2 pyobjc_framework_Speech==7.2 pyobjc_framework_SpriteKit==7.2 pyobjc_framework_StoreKit==7.2 pyobjc_framework_SyncServices==7.2 pyobjc_framework_SystemConfiguration==7.2 pyobjc_framework_SystemExtensions==7.2 pyobjc_framework_UniformTypeIdentifiers==7.2 pyobjc_framework_UserNotifications==7.2 pyobjc_framework_UserNotificationsUI==7.2 pyobjc_framework_VideoSubscriberAccount==7.2 pyobjc_framework_VideoToolbox==7.2 pyobjc_framework_Virtualization==7.2 pyobjc_framework_Vision==7.2 pyobjc_framework_WebKit==7.2 pyobjc_framework_iTunesLibrary==7.2 pyobjc_framework_libdispatch==7.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.0.3 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 richxerox==1.0.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==56.2.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 slugify==0.0.1 soupsieve==2.2.1 tempora==4.0.2 terminado==0.9.5 testpath==0.4.4 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.60.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.4 virtualenv==20.4.6 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/primary_packages.txt000066400000000000000000000012051416500503600233610ustar00rootroot00000000000000# Modify this list if the packages pipx installs in 'tests' changes # Comments ignored after # # Space-separated values on same row. # spec no-deps Cython # in 'setup_requires' of jupyter dep pywinpty on Win ansible==2.9.13 awscli==1.18.168 black==18.9.b0 black==20.8b1 cloudtoken==0.1.707 ipython==7.16.1 isort==5.6.4 jaraco-clipboard==2.0.1 jaraco-financial==2.0.0 jupyter==1.0.0 kaggle==1.5.12 nox==2020.8.22 pbr==5.6.0 pip pycowsay==0.0.0.1 pygdbmi==0.10.0.0 pylint pylint==2.3.1 setuptools-scm setuptools>=41.0 shell-functools==0.3.0 tox tox-ini-fmt==0.5.0 weblate==4.3.1 True # expected fail in tests wheel pipx-1.0.0/testdata/tests_packages/unix-python3.6.txt000066400000000000000000000053351416500503600226010ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 SecretStorage==3.3.1 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 dataclasses==0.8 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 importlib_resources==5.1.4 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jeepney==0.6.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyperclip==1.8.2 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==4.3.3 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/unix-python3.7.txt000066400000000000000000000053321416500503600225770ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 SecretStorage==3.3.1 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jeepney==0.6.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyperclip==1.8.2 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/unix-python3.8.txt000066400000000000000000000053321416500503600226000ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 SecretStorage==3.3.1 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jeepney==0.6.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyperclip==1.8.2 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/unix-python3.9.txt000066400000000000000000000053321416500503600226010ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 SecretStorage==3.3.1 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jedi==0.18.0 jeepney==0.6.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pexpect==4.8.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 ptyprocess==0.7.0 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyperclip==1.8.2 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/win-python3.6.txt000066400000000000000000000056541416500503600224170ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 atomicwrites==1.4.0 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorama==0.4.4 colorlog==4.8.0 cryptography==3.4.7 dataclasses==0.8 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 importlib_resources==5.1.4 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.structures==2.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jaraco.windows==5.5.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pywin32==300 pywin32_ctypes==0.2.0 pywinpty==1.1.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==4.3.3 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/win-python3.7.txt000066400000000000000000000056511416500503600224150ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 atomicwrites==1.4.0 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorama==0.4.4 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.structures==2.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jaraco.windows==5.5.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pywin32==300 pywin32_ctypes==0.2.0 pywinpty==1.1.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/win-python3.8.txt000066400000000000000000000056511416500503600224160ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 atomicwrites==1.4.0 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorama==0.4.4 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.structures==2.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jaraco.windows==5.5.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pywin32==300 pywin32_ctypes==0.2.0 pywinpty==1.1.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/testdata/tests_packages/win-python3.9.txt000066400000000000000000000056511416500503600224170ustar00rootroot00000000000000Cython==0.29.23 Flask==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 PyYAML==5.3.1 PyYAML==5.4.1 Pygments==2.9.0 QtPy==1.9.0 Send2Trash==1.5.0 Weblate==4.3.1 Werkzeug==2.0.1 ansible==2.9.13 appdirs==1.4.4 argcomplete==1.12.3 argon2_cffi==20.1.0 astroid==2.5.6 async_generator==1.10 atomicwrites==1.4.0 attrs==21.2.0 autocommand==2.2.1 awscli==1.18.168 backcall==0.2.0 beautifulsoup4==4.9.3 black==18.9b0 black==20.8b1 bleach==3.3.0 boto3==1.17.81 botocore==1.19.8 botocore==1.20.81 certifi==2020.12.5 cffi==1.14.5 chardet==4.0.0 click==8.0.1 cloudtoken==0.1.707 cloudtoken_plugin.json_exporter==0.1.764 cloudtoken_plugin.saml==0.1.764 cloudtoken_plugin.shell_exporter==0.1.764 colorama==0.4.3 colorama==0.4.4 colorlog==4.8.0 cryptography==3.4.7 decorator==5.0.9 defusedxml==0.7.1 distlib==0.3.1 docutils==0.15.2 entrypoints==0.3 filelock==3.0.12 idna==2.10 importlib_metadata==4.2.0 inflect==5.3.0 iniconfig==1.1.1 ipykernel==5.5.5 ipython==7.16.1 ipython==7.23.1 ipython_genutils==0.2.0 ipywidgets==7.6.3 isort==4.3.21 isort==5.6.4 isort==5.8.0 itsdangerous==2.0.1 jaraco.classes==3.2.1 jaraco.clipboard==2.0.1 jaraco.collections==3.3.0 jaraco.financial==2.0 jaraco.functools==3.3.0 jaraco.itertools==6.0.1 jaraco.logging==3.1.0 jaraco.structures==2.1.0 jaraco.text==3.5.0 jaraco.ui==2.3.0 jaraco.windows==5.5.0 jedi==0.18.0 jmespath==0.10.0 jsonschema==3.2.0 jupyter==1.0.0 jupyter_client==6.1.12 jupyter_console==6.4.0 jupyter_core==4.7.1 jupyterlab_pygments==0.1.2 jupyterlab_widgets==1.0.0 kaggle==1.5.12 keyring==23.0.1 keyrings.alt==4.0.2 lazy_object_proxy==1.6.0 lxml==4.6.3 matplotlib_inline==0.1.2 mccabe==0.6.1 mistune==0.8.4 more_itertools==8.8.0 mypy_extensions==0.4.3 nbclient==0.5.3 nbconvert==6.0.7 nbformat==5.1.3 nest_asyncio==1.5.1 notebook==6.4.0 nox==2020.8.22 ofxparse==0.20 packaging==20.9 pandocfilters==1.4.3 parso==0.8.2 path.py==12.5.0 path==15.1.2 pathspec==0.8.1 pbr==5.6.0 pickleshare==0.7.5 pip==21.1.2 pluggy==0.13.1 prometheus_client==0.10.1 prompt_toolkit==3.0.18 py==1.10.0 pyasn1==0.4.8 pycowsay==0.0.0.1 pycparser==2.20 pygdbmi==0.10.0.0 pylint==2.3.1 pylint==2.8.2 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.2.4 python_dateutil==2.8.1 python_slugify==5.0.2 pytz==2021.1 pywin32==300 pywin32_ctypes==0.2.0 pywinpty==1.1.1 pyzmq==22.1.0 qtconsole==5.1.0 regex==2021.4.4 requests==2.25.1 rsa==4.5 s3transfer==0.3.7 s3transfer==0.4.2 schedule==1.1.0 setuptools==57.0.0 setuptools_scm==6.0.1 shell-functools==0.3.0 six==1.16.0 soupsieve==2.2.1 tempora==4.0.2 terminado==0.10.0 testpath==0.5.0 text_unidecode==1.3 toml==0.10.2 tornado==6.1 tox==3.23.1 tox_ini_fmt==0.5.0 tqdm==4.61.0 traitlets==5.0.5 typed_ast==1.4.3 typing_extensions==3.10.0.0 urllib3==1.25.11 urllib3==1.26.5 virtualenv==20.4.7 wcwidth==0.2.5 webencodings==0.5.1 wheel==0.36.2 widgetsnbextension==3.5.1 wrapt==1.12.1 zipp==3.4.1 pipx-1.0.0/tests/000077500000000000000000000000001416500503600136325ustar00rootroot00000000000000pipx-1.0.0/tests/conftest.py000066400000000000000000000132161416500503600160340ustar00rootroot00000000000000import os import subprocess import sys from pathlib import Path import pytest # type: ignore from helpers import WIN from pipx import commands, constants, shared_libs, venv PIPX_TESTS_DIR = Path(".pipx_tests") PIPX_TESTS_PACKAGE_LIST_DIR = Path("testdata/tests_packages") def pytest_addoption(parser): parser.addoption( "--all-packages", action="store_true", dest="all_packages", default=False, help="Run only the long, slow tests installing the maximum list of packages.", ) parser.addoption( "--net-pypiserver", action="store_true", dest="net_pypiserver", default=False, help="Start local pypi server and use in tests.", ) def pytest_configure(config): markexpr = getattr(config.option, "markexpr", "") if config.option.all_packages: new_markexpr = (f"{markexpr} or " if markexpr else "") + "all_packages" else: new_markexpr = (f"{markexpr} and " if markexpr else "") + "not all_packages" config.option.markexpr = new_markexpr def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request): home_dir = Path(tmp_path) / "subdir" / "pipxhome" bin_dir = Path(tmp_path) / "otherdir" / "pipxbindir" monkeypatch.setattr(constants, "PIPX_SHARED_LIBS", pipx_shared_dir) monkeypatch.setattr(shared_libs, "shared_libs", shared_libs._SharedLibs()) monkeypatch.setattr(venv, "shared_libs", shared_libs.shared_libs) monkeypatch.setattr(constants, "PIPX_HOME", home_dir) monkeypatch.setattr(constants, "LOCAL_BIN_DIR", bin_dir) monkeypatch.setattr(constants, "PIPX_LOCAL_VENVS", home_dir / "venvs") monkeypatch.setattr(constants, "PIPX_VENV_CACHEDIR", home_dir / ".cache") monkeypatch.setattr(constants, "PIPX_LOG_DIR", home_dir / "logs") # macOS needs /usr/bin in PATH to compile certain packages, but # applications in /usr/bin cause test_install.py tests to raise warnings # which make tests fail (e.g. on Github ansible apps exist in /usr/bin) monkeypatch.setenv("PATH_ORIG", str(bin_dir) + os.pathsep + os.getenv("PATH")) monkeypatch.setenv("PATH_TEST", str(bin_dir)) monkeypatch.setenv("PATH", str(bin_dir)) # On Windows, monkeypatch pipx.commands.common._can_symlink_cache to # indicate that constants.LOCAL_BIN_DIR cannot use symlinks, even if # we're running as administrator and symlinks are actually possible. if WIN: monkeypatch.setitem( commands.common._can_symlink_cache, constants.LOCAL_BIN_DIR, False ) if not request.config.option.net_pypiserver: # IMPORTANT: use 127.0.0.1 not localhost # Using localhost on Windows creates enormous slowdowns # (for some reason--perhaps IPV6/IPV4 tries, timeouts?) monkeypatch.setenv("PIP_INDEX_URL", "http://127.0.0.1:8080/simple") @pytest.fixture(scope="session", autouse=True) def pipx_local_pypiserver(request): """Starts local pypiserver once per session unless --net-pypiserver was passed to pytest""" if request.config.option.net_pypiserver: # need both yield and return because other codepath has both yield return pipx_cache_dir = ( request.config.invocation_params.dir / PIPX_TESTS_DIR / "package_cache" ) check_test_packages_cmd = [ "python3", "scripts/update_package_cache.py", "--check-only", str(PIPX_TESTS_PACKAGE_LIST_DIR), str(pipx_cache_dir), ] update_test_packages_cmd = [ "python3", "scripts/update_package_cache.py", str(PIPX_TESTS_PACKAGE_LIST_DIR), str(pipx_cache_dir), ] check_test_packages_process = subprocess.run(check_test_packages_cmd) if check_test_packages_process.returncode != 0: raise Exception( f"Directory {str(pipx_cache_dir)} does not contain all " "package distribution files necessary to run pipx tests. Please " "run the following command to populate it: " f"{' '.join(update_test_packages_cmd)}" ) pypiserver_err_fh = open( request.config.invocation_params.dir / PIPX_TESTS_DIR / "pypiserver.log", "w" ) pypiserver_process = subprocess.Popen( [ "pypi-server", "--authenticate=update", "--disable-fallback", str(pipx_cache_dir / f"{sys.version_info[0]}.{sys.version_info[1]}"), ], universal_newlines=True, stderr=pypiserver_err_fh, ) yield pypiserver_process.terminate() pypiserver_err_fh.close() @pytest.fixture(scope="session") def pipx_session_shared_dir(tmp_path_factory): """Makes a temporary pipx shared libs directory only once per session""" return tmp_path_factory.mktemp("session_shareddir") @pytest.fixture def pipx_temp_env(tmp_path, monkeypatch, pipx_session_shared_dir, request): """Sets up temporary paths for pipx to install into. Shared libs are setup once per session, all other pipx dirs, constants are recreated for every test function. Also adds environment variables as necessary to make pip installations seamless. """ pipx_temp_env_helper(pipx_session_shared_dir, tmp_path, monkeypatch, request) @pytest.fixture def pipx_ultra_temp_env(tmp_path, monkeypatch, request): """Sets up temporary paths for pipx to install into. Fully temporary environment, every test function starts as if pipx has never been run before, including empty shared libs directory. Also adds environment variables as necessary to make pip installations seamless. """ shared_dir = Path(tmp_path) / "shareddir" pipx_temp_env_helper(shared_dir, tmp_path, monkeypatch, request) pipx-1.0.0/tests/helpers.py000066400000000000000000000133251416500503600156520ustar00rootroot00000000000000import json import os import re import sys from pathlib import Path from typing import Any, Dict, List, Optional from unittest import mock from packaging.utils import canonicalize_name from package_info import PKG from pipx import constants, main, pipx_metadata_file, util WIN = sys.platform.startswith("win") MOCK_PIPXMETADATA_0_1: Dict[str, Any] = { "main_package": None, "python_version": None, "venv_args": [], "injected_packages": {}, "pipx_metadata_version": "0.1", } MOCK_PACKAGE_INFO_0_1: Dict[str, Any] = { "package": None, "package_or_url": None, "pip_args": [], "include_dependencies": False, "include_apps": True, "apps": [], "app_paths": [], "apps_of_dependencies": [], "app_paths_of_dependencies": {}, "package_version": "", } def app_name(app: str) -> str: return f"{app}.exe" if WIN else app def run_pipx_cli(pipx_args: List[str]) -> int: with mock.patch.object(sys, "argv", ["pipx"] + pipx_args): return main.cli() def unwrap_log_text(log_text: str): """Remove line-break + indent space from log messages Captured log lines always start with the 'severity' so if a line starts with any spaces assume it is due to an indented pipx wrapped message. """ return re.sub(r"\n\s+", " ", log_text) def _mock_legacy_package_info( modern_package_info: Dict[str, Any], metadata_version: str ) -> Dict[str, Any]: if metadata_version == "0.1": mock_package_info_template = MOCK_PACKAGE_INFO_0_1 else: raise Exception( f"Internal Test Error: Unknown metadata_version={metadata_version}" ) mock_package_info = {} for key in mock_package_info_template: mock_package_info[key] = modern_package_info[key] return mock_package_info def mock_legacy_venv(venv_name: str, metadata_version: Optional[str] = None) -> None: """Convert a venv installed with the most recent pipx to look like one with a previous metadata version. metadata_version=None refers to no metadata file (pipx pre-0.15.0.0) """ venv_dir = Path(constants.PIPX_LOCAL_VENVS) / canonicalize_name(venv_name) if metadata_version == "0.2": # Current metadata version, do nothing return elif metadata_version == "0.1": mock_pipx_metadata_template = MOCK_PIPXMETADATA_0_1 elif metadata_version is None: # No metadata os.remove(venv_dir / "pipx_metadata.json") return else: raise Exception( f"Internal Test Error: Unknown metadata_version={metadata_version}" ) modern_metadata = pipx_metadata_file.PipxMetadata(venv_dir).to_dict() # Convert to mock old metadata mock_pipx_metadata = {} for key in mock_pipx_metadata_template: if key == "main_package": mock_pipx_metadata[key] = _mock_legacy_package_info( modern_metadata[key], metadata_version=metadata_version ) if key == "injected_packages": mock_pipx_metadata[key] = {} for injected in modern_metadata[key]: mock_pipx_metadata[key][injected] = _mock_legacy_package_info( modern_metadata[key][injected], metadata_version=metadata_version ) else: mock_pipx_metadata[key] = modern_metadata[key] mock_pipx_metadata["pipx_metadata_version"] = mock_pipx_metadata_template[ "pipx_metadata_version" ] # replicate pipx_metadata_file.PipxMetadata.write() with open(venv_dir / "pipx_metadata.json", "w") as pipx_metadata_fh: json.dump( mock_pipx_metadata, pipx_metadata_fh, indent=4, sort_keys=True, cls=pipx_metadata_file.JsonEncoderHandlesPath, ) def create_package_info_ref(venv_name, package_name, pipx_venvs_dir, **field_overrides): """Create reference PackageInfo to check against Overridable fields to be used in field_overrides: pip_args (default: []) include_apps (default: True) include_dependencies (default: False) app_paths_of_dependencies (default: {}) """ venv_bin_dir = "Scripts" if constants.WINDOWS else "bin" return pipx_metadata_file.PackageInfo( package=package_name, package_or_url=PKG[package_name]["spec"], pip_args=field_overrides.get("pip_args", []), include_apps=field_overrides.get("include_apps", True), include_dependencies=field_overrides.get("include_dependencies", False), apps=PKG[package_name]["apps"], app_paths=[ pipx_venvs_dir / venv_name / venv_bin_dir / app for app in PKG[package_name]["apps"] ], apps_of_dependencies=PKG[package_name]["apps_of_dependencies"], app_paths_of_dependencies=field_overrides.get("app_paths_of_dependencies", {}), package_version=PKG[package_name]["spec"].split("==")[-1], ) def assert_package_metadata(test_metadata, ref_metadata): # only compare sorted versions of apps, app_paths so order is not important assert test_metadata.package_version != "" assert isinstance(test_metadata.apps, list) assert isinstance(test_metadata.app_paths, list) test_metadata_replaced = test_metadata._replace( apps=sorted(test_metadata.apps), app_paths=sorted(test_metadata.app_paths) ) ref_metadata_replaced = ref_metadata._replace( apps=sorted(ref_metadata.apps), app_paths=sorted(ref_metadata.app_paths) ) assert test_metadata_replaced == ref_metadata_replaced def remove_venv_interpreter(venv_name): _, venv_python_path = util.get_venv_paths(constants.PIPX_LOCAL_VENVS / venv_name) assert venv_python_path.is_file() venv_python_path.unlink() assert not venv_python_path.is_file() pipx-1.0.0/tests/package_info.py000066400000000000000000000634121416500503600166200ustar00rootroot00000000000000import sys from typing import Any, Dict WIN = sys.platform.startswith("win") def _exe_if_win(apps): app_strings = [] app_strings = [f"{app}.exe" if WIN else app for app in apps] return app_strings # Versions of all packages possibly used in our tests # Only apply _exe_if_win to entry_points, NOT scripts PKG: Dict[str, Dict[str, Any]] = { "ansible": { "spec": "ansible==2.9.13", "apps": [ "ansible", "ansible-config", "ansible-connection", "ansible-console", "ansible-doc", "ansible-galaxy", "ansible-inventory", "ansible-playbook", "ansible-pull", "ansible-test", "ansible-vault", ], "apps_of_dependencies": [], }, "awscli": { "spec": "awscli==1.18.168", "apps": [ "aws", "aws.cmd", "aws_bash_completer", "aws_completer", "aws_zsh_completer.sh", ], "apps_of_dependencies": _exe_if_win( [ "pyrsa-decrypt", # rsa EXE "pyrsa-encrypt", # rsa EXE "pyrsa-keygen", # rsa EXE "pyrsa-priv2pub", # rsa EXE "pyrsa-sign", # rsa EXE "pyrsa-verify", # rsa EXE ] ) + [ "jp.py", # jmespath.py NO_EXE "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "b2": { "spec": "b2==2.0.2", "apps": _exe_if_win(["b2"]), "apps_of_dependencies": _exe_if_win(["chardetect", "tqdm"]), }, "beancount": { "spec": "beancount==2.3.3", "apps": _exe_if_win( [ "bean-bake", "bean-check", "bean-doctor", "bean-example", "bean-extract", "bean-file", "bean-format", "bean-identify", "bean-price", "bean-query", "bean-report", "bean-sql", "bean-web", "treeify", "upload-to-sheets", ] ), "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "py.test", # pytest EXE "pyrsa-decrypt", # rsa EXE "pyrsa-encrypt", # rsa EXE "pyrsa-keygen", # rsa EXE "pyrsa-priv2pub", # rsa EXE "pyrsa-sign", # rsa EXE "pyrsa-verify", # rsa EXE "pytest", # pytest EXE ] ) + ["bottle.py"], # bottle NO_EXE }, "beets": { "spec": "beets==1.4.9", "apps": _exe_if_win(["beet"]), "apps_of_dependencies": _exe_if_win( [ "mid3cp", "mid3iconv", "mid3v2", "moggsplit", "mutagen-inspect", "mutagen-pony", "unidecode", # unidecode EXE ] ), }, "black": { "spec": "black==20.8b1", "apps": _exe_if_win(["black", "black-primer", "blackd"]), "apps_of_dependencies": [], }, "cactus": { "spec": "cactus==3.3.3", "apps": _exe_if_win(["cactus"]), "apps_of_dependencies": _exe_if_win(["keyring"]) + [ "asadmin", "bundle_image", "cfadmin", "cq", "cwutil", "django-admin.py", "dynamodb_dump", "dynamodb_load", "elbadmin", "fetch_file", "glacier", "instance_events", "kill_instance", "launch_instance", "list_instances", "lss3", "markdown2", "mturk", "pyami_sendmail", "route53", "s3put", "sdbadmin", "taskadmin", ], }, "chert": { "spec": "chert==19.1.0", "apps": _exe_if_win(["chert"]), "apps_of_dependencies": _exe_if_win(["ashes", "markdown_py"]) + ["ashes.py"], }, "cloudtoken": { "spec": "cloudtoken==0.1.707", "apps": ["cloudtoken", "cloudtoken.app", "cloudtoken_proxy.sh", "awstoken"], "apps_of_dependencies": _exe_if_win(["chardetect", "flask", "keyring"]) + ["jp.py"], }, "coala": { "spec": "coala==0.11.0", "apps": _exe_if_win( ["coala", "coala-ci", "coala-delete-orig", "coala-format", "coala-json"] ), "apps_of_dependencies": _exe_if_win(["chardetect", "pygmentize"]) + ["unidiff"], }, "cookiecutter": { "spec": "cookiecutter==1.7.2", "apps": _exe_if_win(["cookiecutter"]), "apps_of_dependencies": _exe_if_win(["chardetect", "slugify"]), }, "cython": { "spec": "cython==0.29.21", "apps": _exe_if_win(["cygdb", "cython", "cythonize"]), "apps_of_dependencies": [], }, "datasette": { "spec": "datasette==0.50.2", "apps": _exe_if_win(["datasette"]), "apps_of_dependencies": _exe_if_win(["hupper", "uvicorn"]) + ["pint-convert"], }, "diffoscope": { "spec": "diffoscope==154", "apps": _exe_if_win(["diffoscope"]), "apps_of_dependencies": [], }, "doc2dash": { "spec": "doc2dash==2.3.0", "apps": _exe_if_win(["doc2dash"]), "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "pybabel", # babel EXE "pygmentize", # pygments EXE "sphinx-apidoc", # sphinx EXE "sphinx-autogen", # sphinx EXE "sphinx-build", # sphinx EXE "sphinx-quickstart", # sphinx EXE ] ) + [ "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "doitlive": { "spec": "doitlive==4.3.0", "apps": _exe_if_win(["doitlive"]), "apps_of_dependencies": [], }, "gdbgui": { "spec": "gdbgui==0.14.0.1", "apps": _exe_if_win(["gdbgui"]), "apps_of_dependencies": _exe_if_win(["flask", "pygmentize"]), }, "gns3-gui": { "spec": "gns3-gui==2.2.15", "apps": _exe_if_win(["gns3"]), "apps_of_dependencies": _exe_if_win(["distro", "jsonschema"]), }, "grow": { "spec": "grow==1.0.0a10", "apps": ["grow"], "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "gen_protorpc", # EXE "html2text", # html2text EXE "markdown_py", # Markdwon EXE "pybabel", # babel EXE "pygmentize", # pygments EXE "pyrsa-decrypt", # rsa EXE "pyrsa-encrypt", # rsa EXE "pyrsa-keygen", # rsa EXE "pyrsa-priv2pub", # rsa EXE "pyrsa-sign", # rsa EXE "pyrsa-verify", # rsa EXE "slugify", # python_slugify EXE "watchmedo", # watchdog EXE ] ), }, "guake": { "spec": "guake==3.7.0", "apps": _exe_if_win(["guake", "guake-toggle"]), "apps_of_dependencies": _exe_if_win(["pbr"]), }, "gunicorn": { "spec": "gunicorn==20.0.4", "apps": _exe_if_win(["gunicorn"]), "apps_of_dependencies": [], }, "howdoi": { "spec": "howdoi==2.0.7", "apps": _exe_if_win(["howdoi"]), "apps_of_dependencies": _exe_if_win(["chardetect", "keep", "pygmentize"]), }, "httpie": { "spec": "httpie==2.3.0", "apps": _exe_if_win(["http", "https"]), "apps_of_dependencies": _exe_if_win(["chardetect", "pygmentize"]), }, "hyde": { "spec": "hyde==0.8.9", "apps": _exe_if_win(["hyde"]), "apps_of_dependencies": _exe_if_win(["markdown_py", "pygmentize"]) + ["smartypants"], }, "ipython": { "spec": "ipython==7.16.1", "apps": _exe_if_win(["iptest", "iptest3", "ipython", "ipython3"]), "apps_of_dependencies": _exe_if_win(["pygmentize"]), # pygments EXE }, "isort": { "spec": "isort==5.6.4", "apps": _exe_if_win(["isort"]), "apps_of_dependencies": [], }, "jaraco-financial": { "spec": "jaraco.financial==2.0", "apps": _exe_if_win( [ "clean-msmoney-temp", "fix-qif-date-format", "launch-in-money", "ofx", "record-document-hashes", ] ), "apps_of_dependencies": _exe_if_win(["keyring", "chardetect", "calc-prorate"]), }, "jupyter": { "spec": "jupyter==1.0.0", "apps": [], "apps_of_dependencies": _exe_if_win( [ "iptest", # EXE "iptest3", # EXE "ipython", # EXE "ipython3", # EXE "jsonschema", # jsonschema EXE "jupyter", # EXE "jupyter-bundlerextension", # EXE "jupyter-console", # EXE "jupyter-kernel", # EXE "jupyter-kernelspec", # EXE "jupyter-migrate", # EXE "jupyter-nbconvert", # EXE "jupyter-nbextension", # EXE "jupyter-notebook", # EXE "jupyter-qtconsole", # EXE "jupyter-run", # EXE "jupyter-serverextension", # EXE "jupyter-troubleshoot", # EXE "jupyter-trust", # EXE "pygmentize", # pygments EXE ] ), }, "kaggle": { "spec": "kaggle==1.5.12", "apps": _exe_if_win(["kaggle"]), "apps_of_dependencies": list( set(_exe_if_win(["chardetect", "slugify", "tqdm"])) ), }, "kibitzr": { "spec": "kibitzr==6.0.0", "apps": _exe_if_win(["kibitzr"]), "apps_of_dependencies": _exe_if_win(["chardetect", "doesitcache"]), }, "klaus": { "spec": "klaus==1.5.2", "apps": ["klaus"], "apps_of_dependencies": _exe_if_win(["dulwich", "flask", "pygmentize"]) + ["dul-receive-pack", "dul-upload-pack"], }, "kolibri": { "spec": "kolibri==0.14.3", "apps": _exe_if_win(["kolibri"]), "apps_of_dependencies": [], }, "lektor": { "spec": "Lektor==3.2.0", "apps": _exe_if_win(["lektor"]), "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "flask", # flask EXE "pybabel", # babel EXE "slugify", # python_slugify EXE "watchmedo", # watchdog EXE ] ) + ["EXIF.py"], }, "localstack": { "spec": "localstack==0.12.1", "apps": ["localstack", "localstack.bat"], "apps_of_dependencies": _exe_if_win(["chardetect", "dulwich"]) + ["jp.py", "dul-receive-pack", "dul-upload-pack"], }, "mackup": { "spec": "mackup==0.8.29", "apps": _exe_if_win(["mackup"]), "apps_of_dependencies": [], }, # ONLY FOR mac, linux "magic-wormhole": { "spec": "magic-wormhole==0.12.0", "apps": _exe_if_win(["wormhole"]), "apps_of_dependencies": _exe_if_win( [ "automat-visualize", # EXE "cftp", # EXE "ckeygen", # EXE "conch", # EXE "mailmail", # EXE "pyhtmlizer", # EXE "tkconch", # EXE "tqdm", # tqdm EXE "trial", # EXE "twist", # EXE "twistd", # EXE "wamp", # EXE "xbrnetwork", # EXE ] ) + (["pywin32_postinstall.py", "pywin32_testall.py"] if WIN else []), }, "mayan-edms": { "spec": "mayan-edms==3.5.2", "apps": ["mayan-edms.py"], "apps_of_dependencies": _exe_if_win( [ "celery", # EXE "chardetect", # chardet EXE "django-admin", # EXE "gunicorn", # EXE "jsonschema", # jsonschema EXE "sqlformat", # sqlparse EXE "swagger-flex", # EXE "update-tld-names", # # EXE ] ) + ["django-admin.py", "jsonpointer"], }, "mkdocs": { "spec": "mkdocs==1.1.2", "apps": _exe_if_win(["mkdocs"]), "apps_of_dependencies": _exe_if_win( [ "livereload", # EXE "futurize", # future EXE "pasteurize", # future EXE "nltk", # EXE "tqdm", # tqdm EXE "markdown_py", # Markdwon EXE ] ), }, "mycli": { "spec": "mycli==1.22.2", "apps": _exe_if_win(["mycli"]), "apps_of_dependencies": _exe_if_win(["pygmentize", "sqlformat", "tabulate"]), }, "nikola": { "spec": "nikola==8.1.1", "apps": _exe_if_win(["nikola"]), "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "doit", # EXE "mako-render", # mako EXE "markdown_py", # Markdwon EXE "natsort", # EXE "pybabel", # babel EXE "pygmentize", # pygments EXE "unidecode", # unidecode EXE ] ) + [ "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "nox": { "spec": "nox==2020.8.22", "apps": _exe_if_win(["nox", "tox-to-nox"]), "apps_of_dependencies": _exe_if_win(["virtualenv"]) + [ "activate-global-python-argcomplete", "python-argcomplete-check-easy-install-script", "python-argcomplete-tcsh", "register-python-argcomplete", ], # from argcomplete }, "pbr": {"spec": "pbr==5.6.0", "apps": _exe_if_win(["pbr"])}, "pelican": { "spec": "pelican==4.5.0", "apps": _exe_if_win( [ "pelican", "pelican-import", "pelican-plugins", "pelican-quickstart", "pelican-themes", ] ), "apps_of_dependencies": _exe_if_win(["pygmentize", "unidecode"]) + [ "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "platformio": { "spec": "platformio==5.0.1", "apps": _exe_if_win(["pio", "piodebuggdb", "platformio"]), "apps_of_dependencies": _exe_if_win( ["chardetect", "pyserial-miniterm", "pyserial-ports", "tabulate"] ) + ["bottle.py", "readelf.py"], }, "ppci": { "spec": "ppci==0.5.8", "apps": _exe_if_win( [ "ppci-archive", "ppci-asm", "ppci-build", "ppci-c3c", "ppci-cc", "ppci-dbg", "ppci-disasm", "ppci-hexdump", "ppci-hexutil", "ppci-java", "ppci-ld", "ppci-llc", "ppci-mkuimage", "ppci-objcopy", "ppci-objdump", "ppci-ocaml", "ppci-opt", "ppci-pascal", "ppci-pedump", "ppci-pycompile", "ppci-readelf", "ppci-wabt", "ppci-wasm2wat", "ppci-wasmcompile", "ppci-wat2wasm", "ppci-yacc", ] ), "apps_of_dependencies": [], }, "prosopopee": { "spec": "prosopopee==1.1.3", "apps": _exe_if_win(["prosopopee"]), "apps_of_dependencies": _exe_if_win(["futurize", "pasteurize", "pybabel"]), }, "ptpython": { "spec": "ptpython==3.0.7", "apps": _exe_if_win( [ "ptipython", "ptipython3", "ptipython3.8", "ptpython", "ptpython3", "ptpython3.8", ] ), "apps_of_dependencies": _exe_if_win(["pygmentize"]), # pygments EXE }, "pycowsay": { "spec": "pycowsay==0.0.0.1", "apps": _exe_if_win(["pycowsay"]), "apps_of_dependencies": [], }, "pygdbmi": {"spec": "pygdbmi==0.10.0.0", "apps": [], "apps_of_dependencies": []}, "pylint": { "spec": "pylint==2.3.1", "apps": _exe_if_win(["epylint", "pylint", "pyreverse", "symilar"]), "apps_of_dependencies": _exe_if_win(["isort"]), }, "retext": { "spec": "ReText==7.1.0", "apps": _exe_if_win(["retext"]), "apps_of_dependencies": _exe_if_win( [ "chardetect", # chardet EXE "markdown_py", # Markdwon EXE "pygmentize", # pygments EXE "pylupdate5", # EXE "pyrcc5", # EXE "pyuic5", # EXE ] ) + [ "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "robotframework": { "spec": "robotframework==3.2.2", "apps": _exe_if_win(["rebot", "robot"]), "apps_of_dependencies": [], }, "shell-functools": { "spec": "shell-functools==0.3.0", "apps": [ "filter", "foldl", "foldl1", "ft-functions", "map", "sort_by", "take_while", ], "apps_of_dependencies": [], }, "speedtest-cli": { "spec": "speedtest-cli==2.1.2", "apps": _exe_if_win(["speedtest", "speedtest-cli"]), "apps_of_dependencies": [], }, "sphinx": { "spec": "Sphinx==3.2.1", "apps": _exe_if_win( ["sphinx-apidoc", "sphinx-autogen", "sphinx-build", "sphinx-quickstart"] ), "apps_of_dependencies": _exe_if_win(["chardetect", "pybabel", "pygmentize"]) + [ "rst2html.py", # docutils NO_EXE "rst2html4.py", # docutils NO_EXE "rst2html5.py", # docutils NO_EXE "rst2latex.py", # docutils NO_EXE "rst2man.py", # docutils NO_EXE "rst2odt.py", # docutils NO_EXE "rst2odt_prepstyles.py", # docutils NO_EXE "rst2pseudoxml.py", # docutils NO_EXE "rst2s5.py", # docutils NO_EXE "rst2xetex.py", # docutils NO_EXE "rst2xml.py", # docutils NO_EXE "rstpep2html.py", # docutils NO_EXE ], }, "sqlmap": { "spec": "sqlmap==1.4.10", "apps": _exe_if_win(["sqlmap"]), "apps_of_dependencies": [], }, "streamlink": { "spec": "streamlink==1.7.0", "apps": _exe_if_win(["streamlink"] + (["streamlinkw"] if WIN else [])), "apps_of_dependencies": _exe_if_win(["chardetect"]) + ["wsdump.py"], }, "taguette": { "spec": "taguette==0.9.2", "apps": _exe_if_win(["taguette"]), "apps_of_dependencies": _exe_if_win(["alembic", "mako-render"]) + ["vba_extract.py"], }, "term2048": { "spec": "term2048==0.2.7", "apps": _exe_if_win(["term2048"]), "apps_of_dependencies": [], }, "tox-ini-fmt": { "spec": "tox-ini-fmt==0.5.0", "apps": _exe_if_win(["tox-ini-fmt"]), "apps_of_dependencies": _exe_if_win(["py.test", "pytest"]), # pytest EXE }, "visidata": { "spec": "visidata==2.0.1", "apps": _exe_if_win(["visidata"]) + ["vd"], "apps_of_dependencies": [], }, "vulture": { "spec": "vulture==2.1", "apps": _exe_if_win(["vulture"]), "apps_of_dependencies": [], }, "weblate": { "spec": "Weblate==4.3.1", "apps": _exe_if_win(["weblate"]), "apps_of_dependencies": _exe_if_win( # TODO: check if _exe_if_win (can't install) [ "borg", "borgfs", "build_firefox.sh", "build_tmdb", "buildxpi.py", "celery", "chardetect", # chardet EXE "csv2po", "csv2tbx", "cygdb", "cython", "cythonize", "django-admin", "django-admin.py", # NO_EXE "flatxml2po", "get_moz_enUS.py", "html2po", "html2text", # html2text EXE "ical2po", "idml2po", "ini2po", "json2po", "jsonschema", # jsonschema EXE "junitmsgfmt", "misaka", "moz2po", "mozlang2po", "odf2xliff", "oo2po", "oo2xliff", "php2po", "phppo2pypo", "po2csv", "po2flatxml", "po2html", "po2ical", "po2idml", "po2ini", "po2json", "po2moz", "po2mozlang", "po2oo", "po2php", "po2prop", "po2rc", "po2resx", "po2sub", "po2symb", "po2tiki", "po2tmx", "po2ts", "po2txt", "po2web2py", "po2wordfast", "po2xliff", "po2yaml", "poclean", "pocommentclean", "pocompendium", "pocompile", "poconflicts", "pocount", "podebug", "pofilter", "pogrep", "pomerge", "pomigrate2", "popuretext", "poreencode", "porestructure", "posegment", "posplit", "poswap", "pot2po", "poterminology", "pretranslate", "prop2po", "pydiff", "pypo2phppo", "rc2po", "resx2po", "sqlformat", # sqlparse EXE "sub2po", "symb2po", "tbx2po", "tiki2po", "tmserver", "ts2po", "txt2po", "web2py2po", "weblate-discover", "xliff2odf", "xliff2oo", "xliff2po", "yaml2po", ] ), }, "youtube-dl": { "spec": "youtube-dl==2020.9.20", "apps": _exe_if_win(["youtube-dl"]), "apps_of_dependencies": [], }, "zeo": { "spec": "ZEO==5.2.2", "apps": _exe_if_win(["runzeo", "zeo-nagios", "zeoctl", "zeopack"]), "apps_of_dependencies": _exe_if_win( [ "fsdump", "fsoids", "fsrefs", "fstail", "repozo", "zconfig", "zconfig_schema2html", "zdaemon", ] ), }, } pipx-1.0.0/tests/test_animate.py000066400000000000000000000110711416500503600166610ustar00rootroot00000000000000import time import pytest # type: ignore import pipx.animate from pipx.animate import ( CLEAR_LINE, EMOJI_ANIMATION_FRAMES, EMOJI_FRAME_PERIOD, NONEMOJI_ANIMATION_FRAMES, NONEMOJI_FRAME_PERIOD, ) # 40-char test_string counts columns e.g.: "0204060810 ... 363840" TEST_STRING_40_CHAR = "".join([f"{x:02}" for x in range(2, 41, 2)]) def check_animate_output( capsys, test_string, frame_strings, frame_period, frames_to_test, extra_animate_time=0.4, extra_after_thread_time=0.1, ): # github workflow history (2020-07-27): # extra_animate_time <= 0.3 failed on macos # extra_after_thread_time <= 0.0 failed on macos expected_string = "".join(frame_strings) chars_to_test = 1 + len("".join(frame_strings[:frames_to_test])) with pipx.animate.animate(test_string, do_animation=True): time.sleep(frame_period * (frames_to_test - 1) + extra_animate_time) # Wait before capturing stderr to ensure animate thread is finished # and to capture all its characters. time.sleep(extra_after_thread_time) captured = capsys.readouterr() print("check_animate_output() Test Debug Output:") if len(captured.err) < chars_to_test: print( "Not enough captured characters--Likely need to increase extra_animate_time" ) print(f"captured characters: {len(captured.err)}") print(f"chars_to_test: {chars_to_test}") for i in range(0, chars_to_test, 40): i_end = min(i + 40, chars_to_test) print(f"expected_string[{i}:{i_end}]: {repr(expected_string[i:i_end])}") print(f"captured.err[{i}:{i_end}] : {repr(captured.err[i:i_end])}") assert captured.err[:chars_to_test] == expected_string[:chars_to_test] def test_delay_suppresses_output(capsys, monkeypatch): monkeypatch.setattr(pipx.animate, "stderr_is_tty", True) monkeypatch.setenv("COLUMNS", "80") test_string = "asdf" with pipx.animate.animate(test_string, do_animation=True, delay=0.9): time.sleep(0.5) captured = capsys.readouterr() assert test_string not in captured.err @pytest.mark.parametrize( "env_columns,expected_frame_message", [ (45, f"{TEST_STRING_40_CHAR:.{45-6}}..."), (46, f"{TEST_STRING_40_CHAR}"), (47, f"{TEST_STRING_40_CHAR}"), ], ) def test_line_lengths_emoji(capsys, monkeypatch, env_columns, expected_frame_message): # EMOJI_SUPPORT and stderr_is_tty is set only at import animate.py # since we are already after that, we must override both here monkeypatch.setattr(pipx.animate, "stderr_is_tty", True) monkeypatch.setattr(pipx.animate, "EMOJI_SUPPORT", True) monkeypatch.setenv("COLUMNS", str(env_columns)) frames_to_test = 4 frame_strings = [ f"\r{CLEAR_LINE}{x} {expected_frame_message}" for x in EMOJI_ANIMATION_FRAMES ] check_animate_output( capsys, TEST_STRING_40_CHAR, frame_strings, EMOJI_FRAME_PERIOD, frames_to_test ) @pytest.mark.parametrize( "env_columns,expected_frame_message", [ (43, f"{TEST_STRING_40_CHAR:.{43-4}}"), (44, f"{TEST_STRING_40_CHAR}"), (45, f"{TEST_STRING_40_CHAR}"), ], ) def test_line_lengths_no_emoji( capsys, monkeypatch, env_columns, expected_frame_message ): # EMOJI_SUPPORT and stderr_is_tty is set only at import animate.py # since we are already after that, we must override both here monkeypatch.setattr(pipx.animate, "stderr_is_tty", True) monkeypatch.setattr(pipx.animate, "EMOJI_SUPPORT", False) monkeypatch.setenv("COLUMNS", str(env_columns)) frames_to_test = 2 frame_strings = [ f"\r{CLEAR_LINE}{expected_frame_message}{x}" for x in NONEMOJI_ANIMATION_FRAMES ] check_animate_output( capsys, TEST_STRING_40_CHAR, frame_strings, NONEMOJI_FRAME_PERIOD, frames_to_test, ) @pytest.mark.parametrize( "env_columns,stderr_is_tty", [(0, True), (8, True), (16, True), (17, False)] ) def test_env_no_animate(capsys, monkeypatch, env_columns, stderr_is_tty): monkeypatch.setattr(pipx.animate, "stderr_is_tty", stderr_is_tty) monkeypatch.setenv("COLUMNS", str(env_columns)) frames_to_test = 4 expected_string = f"{TEST_STRING_40_CHAR}...\n" extra_animate_time = 0.4 extra_after_thread_time = 0.1 with pipx.animate.animate(TEST_STRING_40_CHAR, do_animation=True): time.sleep(EMOJI_FRAME_PERIOD * (frames_to_test - 1) + extra_animate_time) time.sleep(extra_after_thread_time) captured = capsys.readouterr() assert captured.out == "" assert captured.err == expected_string pipx-1.0.0/tests/test_completions.py000066400000000000000000000003171416500503600176000ustar00rootroot00000000000000from helpers import run_pipx_cli def test_cli(monkeypatch, capsys): assert not run_pipx_cli(["completions"]) captured = capsys.readouterr() assert "Add the appropriate command" in captured.out pipx-1.0.0/tests/test_emojis.py000066400000000000000000000025311416500503600165320ustar00rootroot00000000000000import sys from io import BytesIO, TextIOWrapper from unittest import mock import pytest # type: ignore from pipx.emojis import use_emojis @pytest.mark.parametrize( "USE_EMOJI, encoding, expected", [ # utf-8 (None, "utf-8", True), ("", "utf-8", False), ("0", "utf-8", False), ("1", "utf-8", True), ("true", "utf-8", True), ("tru", "utf-8", False), ("True", "utf-8", True), ("false", "utf-8", False), # latin_1 (alias: iso-8859-1) (None, "latin_1", False), ("", "latin_1", False), ("0", "latin_1", False), ("1", "latin_1", True), ("true", "latin_1", True), ("tru", "latin_1", False), ("True", "latin_1", True), ("false", "latin_1", False), # cp1252 (None, "cp1252", False), ("", "cp1252", False), ("0", "cp1252", False), ("1", "cp1252", True), ("true", "cp1252", True), ("tru", "cp1252", False), ("True", "cp1252", True), ("false", "cp1252", False), ], ) def test_use_emojis(monkeypatch, USE_EMOJI, encoding, expected): with mock.patch.object(sys, "stderr", TextIOWrapper(BytesIO(), encoding=encoding)): if USE_EMOJI is not None: monkeypatch.setenv("USE_EMOJI", USE_EMOJI) assert use_emojis() is expected pipx-1.0.0/tests/test_inject.py000066400000000000000000000041171416500503600165220ustar00rootroot00000000000000import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli from package_info import PKG def test_inject_simple(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_inject_simple_legacy_venv(pipx_temp_env, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) if metadata_version is not None: assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) else: # no metadata in venv should result in PipxError with message assert run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) assert "Please uninstall and install" in capsys.readouterr().err def test_inject_tricky_character(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["inject", "pycowsay", "jaraco.clipboard==2.0.1"]) def test_spec(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["inject", "pycowsay", "pylint==2.3.1"]) @pytest.mark.parametrize("with_suffix,", [(False,), (True,)]) def test_inject_include_apps(pipx_temp_env, capsys, with_suffix): install_args = [] suffix = "" if with_suffix: suffix = "_x" install_args = [f"--suffix={suffix}"] assert not run_pipx_cli(["install", "pycowsay", *install_args]) assert run_pipx_cli( ["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"] ) if suffix: assert run_pipx_cli( [ "inject", "pycowsay", PKG["black"]["spec"], "--include-deps", "--include-apps", ] ) assert not run_pipx_cli( [ "inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps", "--include-apps", ] ) pipx-1.0.0/tests/test_install.py000066400000000000000000000201531416500503600167120ustar00rootroot00000000000000import os import re import sys from pathlib import Path from unittest import mock import pytest # type: ignore from helpers import app_name, run_pipx_cli, unwrap_log_text from package_info import PKG from pipx import constants TEST_DATA_PATH = "./testdata/test_package_specifier" def test_help_text(monkeypatch, capsys): mock_exit = mock.Mock(side_effect=ValueError("raised in test to exit early")) with mock.patch.object(sys, "exit", mock_exit), pytest.raises( ValueError, match="raised in test to exit early" ): run_pipx_cli(["install", "--help"]) captured = capsys.readouterr() assert "apps you can run from anywhere" in captured.out def install_package(capsys, pipx_temp_env, caplog, package, package_name=""): if not package_name: package_name = package run_pipx_cli(["install", package, "--verbose"]) captured = capsys.readouterr() assert f"installed package {package_name}" in captured.out if not sys.platform.startswith("win"): # TODO assert on windows too # https://github.com/pypa/pipx/issues/217 assert "symlink missing or pointing to unexpected location" not in captured.out assert "not modifying" not in captured.out assert "is not on your PATH environment variable" not in captured.out assert "⚠️" not in caplog.text assert "WARNING" not in caplog.text @pytest.mark.parametrize( "package_name, package_spec", [("pycowsay", "pycowsay"), ("black", PKG["black"]["spec"])], ) def test_install_easy_packages( capsys, pipx_temp_env, caplog, package_name, package_spec ): install_package(capsys, pipx_temp_env, caplog, package_spec, package_name) @pytest.mark.parametrize( "package_name, package_spec", [ ("cloudtoken", PKG["cloudtoken"]["spec"]), ("awscli", PKG["awscli"]["spec"]), ("ansible", PKG["ansible"]["spec"]), ("shell-functools", PKG["shell-functools"]["spec"]), ], ) def test_install_tricky_packages( capsys, pipx_temp_env, caplog, package_name, package_spec ): if os.getenv("FAST"): pytest.skip("skipping slow tests") if sys.platform.startswith("win") and package_name == "ansible": pytest.skip("Ansible is not installable on Windows") install_package(capsys, pipx_temp_env, caplog, package_spec, package_name) # TODO: Add git+... spec when git is in binpath of tests (Issue #303) @pytest.mark.parametrize( "package_name, package_spec", [ # ("nox", "git+https://github.com/cs01/nox.git@5ea70723e9e6"), ("pylint", PKG["pylint"]["spec"]), ("black", "https://github.com/ambv/black/archive/18.9b0.zip"), ], ) def test_install_package_specs( capsys, pipx_temp_env, caplog, package_name, package_spec ): install_package(capsys, pipx_temp_env, caplog, package_spec, package_name) def test_force_install(pipx_temp_env, capsys): run_pipx_cli(["install", "pycowsay"]) captured = capsys.readouterr() # print(captured.out) assert "installed package" in captured.out run_pipx_cli(["install", "pycowsay"]) captured = capsys.readouterr() assert "installed package" not in captured.out assert "'pycowsay' already seems to be installed" in captured.out run_pipx_cli(["install", "pycowsay", "--force"]) captured = capsys.readouterr() assert "Installing to existing venv" in captured.out def test_install_no_packages_found(pipx_temp_env, capsys): run_pipx_cli(["install", PKG["pygdbmi"]["spec"]]) captured = capsys.readouterr() assert "No apps associated with package pygdbmi" in captured.err def test_install_same_package_twice_no_force(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["install", "pycowsay"]) captured = capsys.readouterr() assert ( "'pycowsay' already seems to be installed. Not modifying existing installation" in captured.out ) def test_include_deps(pipx_temp_env, capsys): assert run_pipx_cli(["install", PKG["jupyter"]["spec"]]) == 1 assert not run_pipx_cli(["install", PKG["jupyter"]["spec"], "--include-deps"]) @pytest.mark.parametrize( "package_name, package_spec", [ ("jaraco-financial", "jaraco.financial==2.0.0"), ("tox-ini-fmt", PKG["tox-ini-fmt"]["spec"]), ], ) def test_name_tricky_characters( caplog, capsys, pipx_temp_env, package_name, package_spec ): install_package(capsys, pipx_temp_env, caplog, package_spec, package_name) def test_extra(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "nox[tox_to_nox]==2020.8.22", "--include-deps"]) captured = capsys.readouterr() assert f"- {app_name('tox')}\n" in captured.out def test_install_local_extra(pipx_temp_env, capsys): assert not run_pipx_cli( ["install", TEST_DATA_PATH + "/local_extras[cow]", "--include-deps"] ) captured = capsys.readouterr() assert f"- {app_name('pycowsay')}\n" in captured.out def test_path_warning(pipx_temp_env, capsys, monkeypatch, caplog): assert not run_pipx_cli(["install", "pycowsay"]) assert "is not on your PATH environment variable" not in unwrap_log_text( caplog.text ) monkeypatch.setenv("PATH", "") assert not run_pipx_cli(["install", "pycowsay", "--force"]) assert "is not on your PATH environment variable" in unwrap_log_text(caplog.text) def test_existing_symlink_points_to_existing_wrong_location_warning( pipx_temp_env, caplog, capsys ): if sys.platform.startswith("win"): pytest.skip("pipx does not use symlinks on Windows") constants.LOCAL_BIN_DIR.mkdir(exist_ok=True, parents=True) (constants.LOCAL_BIN_DIR / "pycowsay").symlink_to(os.devnull) assert not run_pipx_cli(["install", "pycowsay"]) captured = capsys.readouterr() assert "File exists at" in unwrap_log_text(caplog.text) assert "symlink missing or pointing to unexpected location" in captured.out # bin dir was on path, so the warning should NOT appear (even though the symlink # pointed to the wrong location) assert "is not on your PATH environment variable" not in captured.err def test_existing_symlink_points_to_nothing(pipx_temp_env, capsys): if sys.platform.startswith("win"): pytest.skip("pipx does not use symlinks on Windows") constants.LOCAL_BIN_DIR.mkdir(exist_ok=True, parents=True) (constants.LOCAL_BIN_DIR / "pycowsay").symlink_to("/asdf/jkl") assert not run_pipx_cli(["install", "pycowsay"]) captured = capsys.readouterr() # pipx should realize the symlink points to nothing and replace it, # so no warning should be present assert "symlink missing or pointing to unexpected location" not in captured.out def test_pip_args_forwarded_to_package_name_determination(pipx_temp_env, capsys): assert run_pipx_cli( [ "install", # use a valid spec and invalid pip args "https://github.com/ambv/black/archive/18.9b0.zip", "--verbose", "--pip-args='--asdf'", ] ) captured = capsys.readouterr() assert "Cannot determine package name from spec" in captured.err def test_install_suffix(pipx_temp_env, capsys): name = "pbr" suffix = "_a" assert not run_pipx_cli(["install", PKG[name]["spec"], f"--suffix={suffix}"]) captured = capsys.readouterr() name_a = app_name(f"{name}{suffix}") assert f"- {name_a}" in captured.out suffix = "_b" assert not run_pipx_cli(["install", PKG[name]["spec"], f"--suffix={suffix}"]) captured = capsys.readouterr() name_b = app_name(f"{name}{suffix}") assert f"- {name_b}" in captured.out assert (constants.LOCAL_BIN_DIR / name_a).exists() assert (constants.LOCAL_BIN_DIR / name_b).exists() def test_install_pip_failure(pipx_temp_env, capsys): assert run_pipx_cli(["install", "weblate==4.3.1", "--verbose"]) captured = capsys.readouterr() assert "Fatal error from pip" in captured.err pip_log_file_match = re.search( r"Full pip output in file:\s+(\S.+)$", captured.err, re.MULTILINE ) assert pip_log_file_match assert Path(pip_log_file_match.group(1)).exists() assert re.search(r"pip (failed|seemed to fail) to build package", captured.err) pipx-1.0.0/tests/test_install_all_packages.py000066400000000000000000000426451416500503600214120ustar00rootroot00000000000000""" This module uses the pytest infrastructure to produce reports on a large list of packages. It verifies installation with and without an intact system PATH. It also generates report summaries and error reports files. Test pytest outcomes: PASS - if no pip errors, and no pipx issues, and package apps verified all installed correctly XFAIL - if there is a pip error, i.e. an installation problem out of pipx's control FAIL - if there is no pip error, but there is a problem due to pipx, including a pipx error or warning, incorrect list of installed apps, etc. """ import io import os import re import subprocess import sys import textwrap import time from datetime import datetime, timedelta from pathlib import Path from typing import List, Optional, Tuple import pytest # type: ignore from helpers import run_pipx_cli from package_info import PKG REPORTS_DIR = "./reports" REPORT_FILENAME_ROOT = "all_packages" PACKAGE_NAME_LIST = [ "ansible", "awscli", "b2", "beancount", "beets", "black", "cactus", "chert", "cloudtoken", "coala", "cookiecutter", "cython", "datasette", "diffoscope", "doc2dash", "doitlive", "gdbgui", "gns3-gui", "grow", "guake", "gunicorn", "howdoi", "httpie", "hyde", "ipython", "isort", "jaraco-financial", "kaggle", "kibitzr", "klaus", "kolibri", "lektor", "localstack", "mackup", "magic-wormhole", "mayan-edms", "mkdocs", "mycli", "nikola", "nox", "pelican", "platformio", "ppci", "prosopopee", "ptpython", "pycowsay", "pylint", "retext", "robotframework", "shell-functools", "speedtest-cli", "sphinx", "sqlmap", "streamlink", "taguette", "term2048", "tox-ini-fmt", "visidata", "vulture", "weblate", "youtube-dl", "zeo", ] class PackageData: def __init__(self): self.package_name: str = "" self.package_spec: str = "" self.clear_elapsed_time: Optional[float] = None self.clear_pip_pass: Optional[bool] = None self.clear_pipx_pass: Optional[bool] = None self.sys_elapsed_time: Optional[float] = None self.sys_pip_pass: Optional[bool] = None self.sys_pipx_pass: Optional[bool] = None self.overall_pass: Optional[bool] = None @property def clear_pip_pf_str(self) -> str: return self._get_pass_fail_str("clear_pip_pass") @property def clear_pipx_pf_str(self) -> str: return self._get_pass_fail_str("clear_pipx_pass") @property def sys_pip_pf_str(self) -> str: return self._get_pass_fail_str("sys_pip_pass") @property def sys_pipx_pf_str(self) -> str: return self._get_pass_fail_str("sys_pipx_pass") @property def overall_pf_str(self) -> str: return self._get_pass_fail_str("overall_pass") def _get_pass_fail_str(self, test_attr: str) -> str: if getattr(self, test_attr) is not None: return "PASS" if getattr(self, test_attr) else "FAIL" else: return "" class ModuleGlobalsData: def __init__(self): self.errors_path = Path(".") self.install_data: List[PackageData] = [] self.py_version_display = "Python {0.major}.{0.minor}.{0.micro}".format( sys.version_info ) self.py_version_short = "{0.major}.{0.minor}".format(sys.version_info) self.report_path = Path(".") self.sys_platform = sys.platform self.test_class = "" self.test_start = datetime.now() self.test_end = datetime.now() # default, must be set later def reset(self, test_class: str = "") -> None: self.errors_path = Path(".") self.install_data = [] self.report_path = Path(".") self.test_class = test_class self.test_start = datetime.now() @pytest.fixture(scope="module") def module_globals() -> ModuleGlobalsData: return ModuleGlobalsData() def pip_cache_purge() -> None: subprocess.run([sys.executable, "-m", "pip", "cache", "purge"]) def write_report_legend(report_legend_path: Path) -> None: with report_legend_path.open("w", encoding="utf-8") as report_legend_fh: print( textwrap.dedent( """ LEGEND =========== cleared_PATH = PATH used for pipx tests with only pipx bin dir and nothing else sys_PATH = Normal system PATH with all default directories included overall = PASS or FAIL for complete end-to-end pipx install, PASS if no errors or warnings and all the proper apps were installed and linked pip = PASS or FAIL sub-category based only if pip inside of pipx installs package with/without error pipx = PASS or FAIL sub-category based on the non-pip parts of pipx, including whether any errors or warnings are present, and if all the proper apps were installed and linked """ ).strip(), file=report_legend_fh, ) def format_report_table_header(module_globals: ModuleGlobalsData) -> str: header_string = "\n\n" header_string += "=" * 79 + "\n" header_string += f"{module_globals.sys_platform:16}" header_string += f"{module_globals.py_version_display:16}" header_string += f"{module_globals.test_start.strftime('%Y-%m-%d %H:%M:%S')}\n\n" header_string += f"{'package_spec':24}{'overall':12}{'cleared_PATH':24}" header_string += f"{'system_PATH':24}\n" header_string += f"{'':24}{'':12}{'pip':8}{'pipx':8}{'time':8}" header_string += f"{'pip':8}{'pipx':8}{'time':8}\n" header_string += "-" * 79 return header_string def format_report_table_row(package_data: PackageData) -> str: clear_install_time = f"{package_data.clear_elapsed_time:>3.0f}s" if package_data.sys_elapsed_time is not None: sys_install_time = f"{package_data.sys_elapsed_time:>3.0f}s" else: sys_install_time = "" row_string = ( f"{package_data.package_spec:24}{package_data.overall_pf_str:12}" f"{package_data.clear_pip_pf_str:8}{package_data.clear_pipx_pf_str:8}" f"{clear_install_time:8}" f"{package_data.sys_pip_pf_str:8}{package_data.sys_pipx_pf_str:8}" f"{sys_install_time:8}" ) return row_string def format_report_table_footer(module_globals: ModuleGlobalsData) -> str: fail_list = [] prebuild_list = [] footer_string = "\nSummary\n" footer_string += "-" * 79 + "\n" for package_data in module_globals.install_data: clear_pip_pass = package_data.clear_pip_pass clear_pipx_pass = package_data.clear_pipx_pass sys_pip_pass = package_data.sys_pip_pass sys_pipx_pass = package_data.sys_pipx_pass if clear_pip_pass and clear_pipx_pass: continue elif not clear_pip_pass and sys_pip_pass and sys_pipx_pass: prebuild_list.append(package_data.package_spec) else: fail_list.append(package_data.package_spec) if fail_list: footer_string += "FAILS:\n" for failed_package_spec in sorted(fail_list, key=str.lower): footer_string += f" {failed_package_spec}\n" if prebuild_list: footer_string += "Needs prebuilt wheel:\n" for prebuild_package_spec in sorted(prebuild_list, key=str.lower): footer_string += f" {prebuild_package_spec}\n" dt_string = module_globals.test_end.strftime("%Y-%m-%d %H:%M:%S") el_datetime = module_globals.test_end - module_globals.test_start el_datetime = el_datetime - timedelta(microseconds=el_datetime.microseconds) footer_string += f"\nFinished {dt_string}\n" footer_string += f"Elapsed: {el_datetime}" return footer_string def verify_installed_apps( captured_outerr, package_name: str, test_error_fh: io.StringIO, deps: bool = False ) -> bool: package_apps = PKG[package_name]["apps"].copy() if deps: package_apps += PKG[package_name]["apps_of_dependencies"] reported_apps_re = re.search( r"These apps are now globally available(.+)", captured_outerr.out, re.DOTALL ) if reported_apps_re: reported_apps = [ x.strip()[2:] for x in reported_apps_re.group(1).strip().split("\n") ] if set(reported_apps) != set(package_apps): app_success = False print( "verify_install: REPORTED APPS DO NOT MATCH PACKAGE", file=test_error_fh ) print(f"pipx reported apps: {reported_apps}", file=test_error_fh) print(f" true package apps: {package_apps}", file=test_error_fh) else: app_success = True else: app_success = False print("verify_install: APPS TESTING ERROR", file=test_error_fh) return app_success def verify_post_install( pipx_exit_code: int, captured_outerr, caplog, package_name: str, test_error_fh: io.StringIO, using_clear_path: bool, deps: bool = False, ) -> Tuple[bool, Optional[bool], Optional[Path]]: pip_error_file = None caplog_problem = False install_success = f"installed package {package_name}" in captured_outerr.out for record in caplog.records: if "⚠️" in record.message or "WARNING" in record.message: if using_clear_path or "was already on your PATH" not in record.message: caplog_problem = True print("verify_install: WARNING IN CAPLOG:", file=test_error_fh) print(record.message, file=test_error_fh) if "Fatal error from pip prevented installation" in record.message: pip_error_file_re = re.search( r"pip output in file:\s+(\S.+)$", record.message ) if pip_error_file_re: pip_error_file = Path(pip_error_file_re.group(1)) if install_success and PKG[package_name].get("apps", None) is not None: app_success = verify_installed_apps( captured_outerr, package_name, test_error_fh, deps=deps ) else: app_success = True pip_pass = not ( (pipx_exit_code != 0) and f"Error installing {package_name}" in captured_outerr.err ) pipx_pass: Optional[bool] if pip_pass: pipx_pass = install_success and not caplog_problem and app_success else: pipx_pass = None return pip_pass, pipx_pass, pip_error_file def print_error_report( module_globals: ModuleGlobalsData, command_captured, test_error_fh: io.StringIO, package_spec: str, test_type: str, pip_error_file: Optional[Path], ) -> None: with module_globals.errors_path.open("a", encoding="utf-8") as errors_fh: print("\n\n", file=errors_fh) print("=" * 79, file=errors_fh) print( f"{package_spec:24}{test_type:16}{module_globals.sys_platform:16}" f"{module_globals.py_version_display}", file=errors_fh, ) print("\nSTDOUT:", file=errors_fh) print("-" * 76, file=errors_fh) print(command_captured.out, end="", file=errors_fh) print("\nSTDERR:", file=errors_fh) print("-" * 76, file=errors_fh) print(command_captured.err, end="", file=errors_fh) if pip_error_file is not None: print("\nPIP ERROR LOG FILE:", file=errors_fh) print("-" * 76, file=errors_fh) with pip_error_file.open("r") as pip_error_fh: print(pip_error_fh.read(), end="", file=errors_fh) print("\n\nTEST WARNINGS / ERRORS:", file=errors_fh) print("-" * 76, file=errors_fh) print(test_error_fh.getvalue(), end="", file=errors_fh) def install_and_verify( capsys: pytest.CaptureFixture, caplog, monkeypatch, module_globals: ModuleGlobalsData, using_clear_path: bool, package_data: PackageData, deps: bool, ) -> Tuple[bool, Optional[bool], float]: _ = capsys.readouterr() caplog.clear() test_error_fh = io.StringIO() monkeypatch.setenv( "PATH", os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG") ) start_time = time.time() pipx_exit_code = run_pipx_cli( ["install", package_data.package_spec, "--verbose"] + (["--include-deps"] if deps else []) ) elapsed_time = time.time() - start_time captured = capsys.readouterr() pip_pass, pipx_pass, pip_error_file = verify_post_install( pipx_exit_code, captured, caplog, package_data.package_name, test_error_fh, using_clear_path=using_clear_path, deps=deps, ) if not pip_pass or not pipx_pass: print_error_report( module_globals, captured, test_error_fh, package_data.package_spec, "clear PATH" if using_clear_path else "sys PATH", pip_error_file, ) return pip_pass, pipx_pass, elapsed_time def install_package_both_paths( monkeypatch, capsys: pytest.CaptureFixture, caplog, module_globals: ModuleGlobalsData, pipx_temp_env, package_name: str, deps: bool = False, ) -> bool: package_data = PackageData() module_globals.install_data.append(package_data) package_data.package_name = package_name package_data.package_spec = PKG[package_name]["spec"] ( package_data.clear_pip_pass, package_data.clear_pipx_pass, package_data.clear_elapsed_time, ) = install_and_verify( capsys, caplog, monkeypatch, module_globals, using_clear_path=True, package_data=package_data, deps=deps, ) if not package_data.clear_pip_pass: # if we fail to install due to pip install error, try again with # full system path ( package_data.sys_pip_pass, package_data.sys_pipx_pass, package_data.sys_elapsed_time, ) = install_and_verify( capsys, caplog, monkeypatch, module_globals, using_clear_path=False, package_data=package_data, deps=deps, ) package_data.overall_pass = bool( (package_data.clear_pip_pass and package_data.clear_pipx_pass) or (package_data.sys_pip_pass and package_data.sys_pipx_pass) ) with module_globals.report_path.open("a", encoding="utf-8") as report_fh: print(format_report_table_row(package_data), file=report_fh, flush=True) if not package_data.clear_pip_pass and not package_data.sys_pip_pass: # Use xfail to specify error that is from a pip installation problem pytest.xfail("pip installation error") return package_data.overall_pass # use class scope to start and finish at end of all parametrized tests @pytest.fixture(scope="class") def start_end_test_class(module_globals: ModuleGlobalsData, request): reports_path = Path(REPORTS_DIR) reports_path.mkdir(exist_ok=True, parents=True) module_globals.reset() module_globals.test_class = getattr(request.cls, "test_class", "unknown") report_filename = ( f"{REPORT_FILENAME_ROOT}_" f"{module_globals.test_class}_" f"report_" f"{module_globals.sys_platform}_" f"{module_globals.py_version_short}_" f"{module_globals.test_start.strftime('%Y%m%d')}.txt" ) errors_filename = ( f"{REPORT_FILENAME_ROOT}_" f"{module_globals.test_class}_" f"errors_" f"{module_globals.sys_platform}_" f"{module_globals.py_version_short}_" f"{module_globals.test_start.strftime('%Y%m%d')}.txt" ) module_globals.report_path = reports_path / report_filename module_globals.errors_path = reports_path / errors_filename write_report_legend(reports_path / f"{REPORT_FILENAME_ROOT}_report_legend.txt") with module_globals.report_path.open("a", encoding="utf-8") as report_fh: print(format_report_table_header(module_globals), file=report_fh) yield module_globals.test_end = datetime.now() with module_globals.report_path.open("a", encoding="utf-8") as report_fh: print(format_report_table_footer(module_globals), file=report_fh) class TestAllPackagesNoDeps: test_class = "nodeps" @pytest.mark.parametrize("package_name", PACKAGE_NAME_LIST) @pytest.mark.all_packages def test_all_packages( self, monkeypatch, capsys: pytest.CaptureFixture, caplog, module_globals: ModuleGlobalsData, start_end_test_class, pipx_temp_env, package_name: str, ): pip_cache_purge() assert install_package_both_paths( monkeypatch, capsys, caplog, module_globals, pipx_temp_env, package_name, deps=False, ) class TestAllPackagesDeps: test_class = "deps" @pytest.mark.parametrize("package_name", PACKAGE_NAME_LIST) @pytest.mark.all_packages def test_deps_all_packages( self, monkeypatch, capsys: pytest.CaptureFixture, caplog, module_globals: ModuleGlobalsData, start_end_test_class, pipx_temp_env, package_name: str, ): pip_cache_purge() assert install_package_both_paths( monkeypatch, capsys, caplog, module_globals, pipx_temp_env, package_name, deps=True, ) pipx-1.0.0/tests/test_interpreter.py000066400000000000000000000054161416500503600176140ustar00rootroot00000000000000import shutil import subprocess import sys import pytest # type: ignore import pipx.interpreter from pipx.interpreter import ( _find_default_windows_python, _get_absolute_python_interpreter, ) from pipx.util import PipxError def test_windows_python_venv_present(monkeypatch): monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True) assert _find_default_windows_python() == sys.executable def test_windows_python_no_venv_py_present(monkeypatch): def which(name): if name == "py": return "py" monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) monkeypatch.setattr(shutil, "which", which) assert _find_default_windows_python() == "py" def test_windows_python_no_venv_python_present(monkeypatch): def which(name): if name == "python": return "python" # Note: returns False for "py" monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) monkeypatch.setattr(shutil, "which", which) assert _find_default_windows_python() == "python" def test_windows_python_no_venv_no_python(monkeypatch): def which(name): return None monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) monkeypatch.setattr(shutil, "which", which) with pytest.raises(PipxError): _find_default_windows_python() # Test the checks for the store Python. def test_windows_python_no_venv_store_python(monkeypatch): def which(name): if name == "python": return "WindowsApps" class dummy_runner: def __init__(self, rc, out): self.rc = rc self.out = out def __call__(self, *args, **kw): class Ret: pass ret = Ret() ret.returncode = self.rc ret.stdout = self.out return ret monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False) monkeypatch.setattr(shutil, "which", which) # Store version stub gives return code 9009 monkeypatch.setattr(subprocess, "run", dummy_runner(9009, "")) with pytest.raises(PipxError): _find_default_windows_python() # Even if it doesn't, it returns no output monkeypatch.setattr(subprocess, "run", dummy_runner(0, "")) with pytest.raises(PipxError): _find_default_windows_python() # If it *does* pass the tests, we use it as it's not the stub monkeypatch.setattr(subprocess, "run", dummy_runner(0, "3.8")) assert _find_default_windows_python() == "WindowsApps" def test_bad_env_python(monkeypatch): with pytest.raises(PipxError): _get_absolute_python_interpreter("bad_python") def test_good_env_python(monkeypatch, capsys): good_exec = _get_absolute_python_interpreter(sys.executable) assert good_exec == sys.executable pipx-1.0.0/tests/test_list.py000066400000000000000000000105701416500503600162210ustar00rootroot00000000000000import json import re import pytest # type: ignore from helpers import ( app_name, assert_package_metadata, create_package_info_ref, mock_legacy_venv, remove_venv_interpreter, run_pipx_cli, ) from package_info import PKG from pipx import constants from pipx.pipx_metadata_file import PackageInfo, _json_decoder_object_hook def test_cli(pipx_temp_env, monkeypatch, capsys): assert not run_pipx_cli(["list"]) captured = capsys.readouterr() assert "nothing has been installed with pipx" in captured.err def test_missing_interpreter(pipx_temp_env, monkeypatch, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["list"]) captured = capsys.readouterr() assert "package pycowsay has invalid interpreter" not in captured.err remove_venv_interpreter("pycowsay") assert run_pipx_cli(["list"]) captured = capsys.readouterr() assert "package pycowsay has invalid interpreter" in captured.err def test_list_suffix(pipx_temp_env, monkeypatch, capsys): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) assert not run_pipx_cli(["list"]) captured = capsys.readouterr() assert f"package pycowsay 0.0.0.1 (pycowsay{suffix})," in captured.out @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_list_legacy_venv(pipx_temp_env, monkeypatch, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) if metadata_version is None: assert run_pipx_cli(["list"]) captured = capsys.readouterr() assert "package pycowsay has missing internal pipx metadata" in captured.err else: assert not run_pipx_cli(["list"]) captured = capsys.readouterr() assert "package pycowsay 0.0.0.1," in captured.out @pytest.mark.parametrize("metadata_version", ["0.1"]) def test_list_suffix_legacy_venv(pipx_temp_env, monkeypatch, capsys, metadata_version): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) mock_legacy_venv(f"pycowsay{suffix}", metadata_version=metadata_version) assert not run_pipx_cli(["list"]) captured = capsys.readouterr() assert f"package pycowsay 0.0.0.1 (pycowsay{suffix})," in captured.out def test_list_json(pipx_temp_env, capsys): pipx_venvs_dir = constants.PIPX_HOME / "venvs" venv_bin_dir = "Scripts" if constants.WINDOWS else "bin" assert not run_pipx_cli(["install", PKG["pycowsay"]["spec"]]) assert not run_pipx_cli(["install", PKG["pylint"]["spec"]]) assert not run_pipx_cli(["inject", "pylint", PKG["isort"]["spec"]]) captured = capsys.readouterr() assert not run_pipx_cli(["list", "--json"]) captured = capsys.readouterr() assert not re.search(r"\S", captured.err) json_parsed = json.loads(captured.out, object_hook=_json_decoder_object_hook) # raises error if not valid json assert sorted(json_parsed["venvs"].keys()) == ["pycowsay", "pylint"] # pycowsay venv pycowsay_package_ref = create_package_info_ref( "pycowsay", "pycowsay", pipx_venvs_dir ) assert_package_metadata( PackageInfo(**json_parsed["venvs"]["pycowsay"]["metadata"]["main_package"]), pycowsay_package_ref, ) assert json_parsed["venvs"]["pycowsay"]["metadata"]["injected_packages"] == {} # pylint venv pylint_package_ref = create_package_info_ref( "pylint", "pylint", pipx_venvs_dir, **{ "app_paths_of_dependencies": { "isort": [pipx_venvs_dir / "pylint" / venv_bin_dir / app_name("isort")] } }, ) assert_package_metadata( PackageInfo(**json_parsed["venvs"]["pylint"]["metadata"]["main_package"]), pylint_package_ref, ) assert sorted( json_parsed["venvs"]["pylint"]["metadata"]["injected_packages"].keys() ) == ["isort"] isort_package_ref = create_package_info_ref( "pylint", "isort", pipx_venvs_dir, include_apps=False ) print(isort_package_ref) print( PackageInfo( **json_parsed["venvs"]["pylint"]["metadata"]["injected_packages"]["isort"] ) ) assert_package_metadata( PackageInfo( **json_parsed["venvs"]["pylint"]["metadata"]["injected_packages"]["isort"] ), isort_package_ref, ) pipx-1.0.0/tests/test_main.py000066400000000000000000000016341416500503600161730ustar00rootroot00000000000000import sys from unittest import mock import pytest # type: ignore from helpers import run_pipx_cli from pipx import main def test_help_text(monkeypatch, capsys): mock_exit = mock.Mock(side_effect=ValueError("raised in test to exit early")) with mock.patch.object(sys, "exit", mock_exit), pytest.raises( ValueError, match="raised in test to exit early" ): assert not run_pipx_cli(["--help"]) captured = capsys.readouterr() assert "usage: pipx" in captured.out def test_version(monkeypatch, capsys): mock_exit = mock.Mock(side_effect=ValueError("raised in test to exit early")) with mock.patch.object(sys, "exit", mock_exit), pytest.raises( ValueError, match="raised in test to exit early" ): assert not run_pipx_cli(["--version"]) captured = capsys.readouterr() mock_exit.assert_called_with(0) assert main.__version__ in captured.out.strip() pipx-1.0.0/tests/test_package_specifier.py000066400000000000000000000221641416500503600206740ustar00rootroot00000000000000from pathlib import Path import pytest # type: ignore from pipx.package_specifier import ( fix_package_name, parse_specifier_for_install, parse_specifier_for_metadata, parse_specifier_for_upgrade, valid_pypi_name, ) from pipx.util import PipxError TEST_DATA_PATH = "./testdata/test_package_specifier" @pytest.mark.parametrize( "package_spec_in,package_name_out", [ ("Black", "black"), ("https://github.com/ambv/black/archive/18.9b0.zip", None), ("black @ https://github.com/ambv/black/archive/18.9b0.zip", None), ], ) def test_valid_pypi_name(package_spec_in, package_name_out): assert valid_pypi_name(package_spec_in) == package_name_out @pytest.mark.parametrize( "package_spec_in,package_name,package_spec_out", [ ( "https://github.com/ambv/black/archive/18.9b0.zip", "black", "https://github.com/ambv/black/archive/18.9b0.zip", ), ( "nox@https://github.com/ambv/black/archive/18.9b0.zip", "black", "black@ https://github.com/ambv/black/archive/18.9b0.zip", ), ( "nox[extra]@https://github.com/ambv/black/archive/18.9b0.zip", "black", "black[extra]@ https://github.com/ambv/black/archive/18.9b0.zip", ), ], ) def test_fix_package_name(package_spec_in, package_name, package_spec_out): assert fix_package_name(package_spec_in, package_name) == package_spec_out # TODO: Make sure git+ works with tests, correct in test_install as well @pytest.mark.parametrize( "package_spec_in,package_or_url_correct,valid_spec", [ ("pipx", "pipx", True), ("PiPx_stylized.name", "pipx-stylized-name", True), ("pipx==0.15.0", "pipx==0.15.0", True), ("pipx>=0.15.0", "pipx>=0.15.0", True), ("pipx<=0.15.0", "pipx<=0.15.0", True), ('pipx;python_version>="3.6"', "pipx", True), ('pipx==0.15.0;python_version>="3.6"', "pipx==0.15.0", True), ("pipx[extra1]", "pipx[extra1]", True), ("pipx[extra1, extra2]", "pipx[extra1,extra2]", True), ("src/pipx", str(Path("src/pipx").resolve()), True), ( "git+https://github.com/cs01/nox.git@5ea70723e9e6", "git+https://github.com/cs01/nox.git@5ea70723e9e6", True, ), ( "nox@git+https://github.com/cs01/nox.git@5ea70723e9e6", "nox@ git+https://github.com/cs01/nox.git@5ea70723e9e6", True, ), ( "https://github.com/ambv/black/archive/18.9b0.zip", "https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black@https://github.com/ambv/black/archive/18.9b0.zip", "black@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black @ https://github.com/ambv/black/archive/18.9b0.zip", "black@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black[extra] @ https://github.com/ambv/black/archive/18.9b0.zip", "black[extra]@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"', "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git", True, ), ("path/doesnt/exist", "non-existent-path", False), ( "https:/github.com/ambv/black/archive/18.9b0.zip", "URL-syntax-error-slash", False, ), ], ) def test_parse_specifier_for_metadata( package_spec_in, package_or_url_correct, valid_spec ): if valid_spec: package_or_url = parse_specifier_for_metadata(package_spec_in) assert package_or_url == package_or_url_correct else: # print package_spec_in for info in case no error is raised print(f"package_spec_in = {package_spec_in}") with pytest.raises(PipxError, match=r"^Unable to parse package spec"): package_or_url = parse_specifier_for_metadata(package_spec_in) @pytest.mark.parametrize( "package_spec_in,package_or_url_correct,valid_spec", [ ("pipx", "pipx", True), ("PiPx_stylized.name", "pipx-stylized-name", True), ("pipx==0.15.0", "pipx", True), ("pipx>=0.15.0", "pipx", True), ("pipx<=0.15.0", "pipx", True), ('pipx;python_version>="3.6"', "pipx", True), ('pipx==0.15.0;python_version>="3.6"', "pipx", True), ("pipx[extra1]", "pipx[extra1]", True), ("pipx[extra1, extra2]", "pipx[extra1,extra2]", True), ("src/pipx", str(Path("src/pipx").resolve()), True), ( "git+https://github.com/cs01/nox.git@5ea70723e9e6", "git+https://github.com/cs01/nox.git@5ea70723e9e6", True, ), ( "nox@git+https://github.com/cs01/nox.git@5ea70723e9e6", "nox@ git+https://github.com/cs01/nox.git@5ea70723e9e6", True, ), ( "https://github.com/ambv/black/archive/18.9b0.zip", "https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black@https://github.com/ambv/black/archive/18.9b0.zip", "black@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black @ https://github.com/ambv/black/archive/18.9b0.zip", "black@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( "black[extra] @ https://github.com/ambv/black/archive/18.9b0.zip", "black[extra]@ https://github.com/ambv/black/archive/18.9b0.zip", True, ), ( 'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"', "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git", True, ), ("path/doesnt/exist", "non-existent-path", False), ( "https:/github.com/ambv/black/archive/18.9b0.zip", "URL-syntax-error-slash", False, ), ], ) def test_parse_specifier_for_upgrade( package_spec_in, package_or_url_correct, valid_spec ): if valid_spec: package_or_url = parse_specifier_for_upgrade(package_spec_in) assert package_or_url == package_or_url_correct else: # print package_spec_in for info in case no error is raised print(f"package_spec_in = {package_spec_in}") with pytest.raises(PipxError, match=r"^Unable to parse package spec"): package_or_url = parse_specifier_for_upgrade(package_spec_in) @pytest.mark.parametrize( "package_spec_in,pip_args_in,package_spec_expected,pip_args_expected,warning_str", [ ('pipx==0.15.0;python_version>="3.6"', [], "pipx==0.15.0", [], None), ("pipx==0.15.0", ["--editable"], "pipx==0.15.0", [], "Ignoring --editable"), ( 'pipx==0.15.0;python_version>="3.6"', [], "pipx==0.15.0", [], 'Ignoring environment markers (python_version >= "3.6") in package', ), ( "pipx==0.15.0", ["--no-cache-dir", "--editable"], "pipx==0.15.0", ["--no-cache-dir"], "Ignoring --editable", ), ( "git+https://github.com/cs01/nox.git@5ea70723e9e6", ["--editable"], "git+https://github.com/cs01/nox.git@5ea70723e9e6", [], "Ignoring --editable", ), ( "https://github.com/ambv/black/archive/18.9b0.zip", ["--editable"], "https://github.com/ambv/black/archive/18.9b0.zip", [], "Ignoring --editable", ), ( "src/pipx", ["--editable"], str(Path("src/pipx").resolve()), ["--editable"], None, ), ( TEST_DATA_PATH + "/local_extras", [], str(Path(TEST_DATA_PATH + "/local_extras").resolve), [], None, ), ( TEST_DATA_PATH + "/local_extras[cow]", [], str(Path(TEST_DATA_PATH + "/local_extras").resolve) + "[cow]", [], None, ), ( TEST_DATA_PATH + "/local_extras", ["--editable"], str(Path(TEST_DATA_PATH + "/local_extras").resolve), ["--editable"], None, ), ( TEST_DATA_PATH + "/local_extras[cow]", ["--editable"], str(Path(TEST_DATA_PATH + "/local_extras").resolve) + "[cow]", ["--editable"], None, ), ], ) def test_parse_specifier_for_install( caplog, package_spec_in, pip_args_in, package_spec_expected, pip_args_expected, warning_str, ): [package_or_url_out, pip_args_out] = parse_specifier_for_install( package_spec_in, pip_args_in ) if warning_str is not None: assert warning_str in caplog.text pipx-1.0.0/tests/test_pipx_metadata_file.py000066400000000000000000000066351416500503600210740ustar00rootroot00000000000000from pathlib import Path import pytest # type: ignore import pipx.constants from helpers import assert_package_metadata, create_package_info_ref, run_pipx_cli from package_info import PKG from pipx.pipx_metadata_file import PackageInfo, PipxMetadata from pipx.util import PipxError TEST_PACKAGE1 = PackageInfo( package="test_package", package_or_url="test_package_url", pip_args=[], include_apps=True, include_dependencies=False, apps=["testapp"], app_paths=[Path("/usr/bin")], apps_of_dependencies=["dep1"], app_paths_of_dependencies={"dep1": [Path("bin")]}, package_version="0.1.2", ) TEST_PACKAGE2 = PackageInfo( package="inj_package", package_or_url="inj_package_url", pip_args=["-e"], include_apps=True, include_dependencies=False, apps=["injapp"], app_paths=[Path("/usr/bin")], apps_of_dependencies=["dep2"], app_paths_of_dependencies={"dep2": [Path("bin")]}, package_version="6.7.8", ) def test_pipx_metadata_file_create(tmp_path): venv_dir = tmp_path / TEST_PACKAGE1.package venv_dir.mkdir() pipx_metadata = PipxMetadata(venv_dir) pipx_metadata.main_package = TEST_PACKAGE1 pipx_metadata.python_version = "3.4.5" pipx_metadata.venv_args = ["--system-site-packages"] pipx_metadata.injected_packages = {"injected": TEST_PACKAGE2} pipx_metadata.write() pipx_metadata2 = PipxMetadata(venv_dir) for attribute in [ "venv_dir", "main_package", "python_version", "venv_args", "injected_packages", ]: assert getattr(pipx_metadata, attribute) == getattr(pipx_metadata2, attribute) @pytest.mark.parametrize( "test_package", [ TEST_PACKAGE1._replace(include_apps=False), TEST_PACKAGE1._replace(package=None), TEST_PACKAGE1._replace(package_or_url=None), ], ) def test_pipx_metadata_file_validation(tmp_path, test_package): venv_dir = tmp_path / "venv" venv_dir.mkdir() pipx_metadata = PipxMetadata(venv_dir) pipx_metadata.main_package = test_package pipx_metadata.python_version = "3.4.5" pipx_metadata.venv_args = ["--system-site-packages"] pipx_metadata.injected_packages = {} with pytest.raises(PipxError): pipx_metadata.write() def test_package_install(monkeypatch, tmp_path, pipx_temp_env): pipx_venvs_dir = pipx.constants.PIPX_HOME / "venvs" run_pipx_cli(["install", PKG["pycowsay"]["spec"]]) assert (pipx_venvs_dir / "pycowsay" / "pipx_metadata.json").is_file() pipx_metadata = PipxMetadata(pipx_venvs_dir / "pycowsay") pycowsay_package_ref = create_package_info_ref( "pycowsay", "pycowsay", pipx_venvs_dir ) assert_package_metadata(pipx_metadata.main_package, pycowsay_package_ref) assert pipx_metadata.injected_packages == {} def test_package_inject(monkeypatch, tmp_path, pipx_temp_env): pipx_venvs_dir = pipx.constants.PIPX_HOME / "venvs" run_pipx_cli(["install", PKG["pycowsay"]["spec"]]) run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) assert (pipx_venvs_dir / "pycowsay" / "pipx_metadata.json").is_file() pipx_metadata = PipxMetadata(pipx_venvs_dir / "pycowsay") assert pipx_metadata.injected_packages.keys() == {"black"} black_package_ref = create_package_info_ref( "pycowsay", "black", pipx_venvs_dir, include_apps=False ) assert_package_metadata(pipx_metadata.injected_packages["black"], black_package_ref) pipx-1.0.0/tests/test_reinstall.py000066400000000000000000000035021416500503600172400ustar00rootroot00000000000000import sys import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli def test_reinstall(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pycowsay"]) def test_reinstall_nonexistent(pipx_temp_env, capsys): assert run_pipx_cli(["reinstall", "--python", sys.executable, "nonexistent"]) assert "Nothing to reinstall for nonexistent" in capsys.readouterr().out @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_reinstall_legacy_venv(pipx_temp_env, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pycowsay"]) def test_reinstall_suffix(pipx_temp_env, capsys): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) assert not run_pipx_cli( ["reinstall", "--python", sys.executable, f"pycowsay{suffix}"] ) @pytest.mark.parametrize("metadata_version", ["0.1"]) def test_reinstall_suffix_legacy_venv(pipx_temp_env, capsys, metadata_version): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) mock_legacy_venv(f"pycowsay{suffix}", metadata_version=metadata_version) assert not run_pipx_cli( ["reinstall", "--python", sys.executable, f"pycowsay{suffix}"] ) def test_reinstall_specifier(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pylint==2.3.1"]) # clear capsys before reinstall captured = capsys.readouterr() assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pylint"]) captured = capsys.readouterr() assert "installed package pylint 2.3.1" in captured.out pipx-1.0.0/tests/test_reinstall_all.py000066400000000000000000000023021416500503600200650ustar00rootroot00000000000000import sys import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli def test_reinstall_all(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["reinstall-all", "--python", sys.executable]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_reinstall_all_legacy_venv(pipx_temp_env, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) assert not run_pipx_cli(["reinstall-all", "--python", sys.executable]) def test_reinstall_all_suffix(pipx_temp_env, capsys): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) assert not run_pipx_cli(["reinstall-all", "--python", sys.executable]) @pytest.mark.parametrize("metadata_version", ["0.1"]) def test_reinstall_all_suffix_legacy_venv(pipx_temp_env, capsys, metadata_version): suffix = "_x" assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"]) mock_legacy_venv(f"pycowsay{suffix}", metadata_version=metadata_version) assert not run_pipx_cli(["reinstall-all", "--python", sys.executable]) pipx-1.0.0/tests/test_run.py000066400000000000000000000131601416500503600160500ustar00rootroot00000000000000import logging import os import subprocess import sys from unittest import mock import pytest # type: ignore import pipx.main import pipx.util from helpers import run_pipx_cli from package_info import PKG def test_help_text(pipx_temp_env, monkeypatch, capsys): mock_exit = mock.Mock(side_effect=ValueError("raised in test to exit early")) with mock.patch.object(sys, "exit", mock_exit), pytest.raises( ValueError, match="raised in test to exit early" ): run_pipx_cli(["run", "--help"]) captured = capsys.readouterr() assert "Download the latest version of a package" in captured.out def execvpe_mock(cmd_path, cmd_args, env): return_code = subprocess.run( [str(x) for x in cmd_args], env=env, stdout=None, stderr=None, encoding="utf-8", universal_newlines=True, ).returncode sys.exit(return_code) def run_pipx_cli_exit(pipx_cmd_list, assert_exit=None): with pytest.raises(SystemExit) as sys_exit: run_pipx_cli(pipx_cmd_list) if assert_exit is not None: assert sys_exit.type == SystemExit assert sys_exit.value.code == assert_exit @pytest.mark.parametrize( "package_name", ["pycowsay", "pycowsay==0.0.0.1", "pycowsay>=0.0.0.1"] ) @mock.patch("os.execvpe", new=execvpe_mock) def test_simple_run(pipx_temp_env, monkeypatch, capsys, package_name): run_pipx_cli_exit(["run", package_name, "--help"]) captured = capsys.readouterr() assert "Download the latest version of a package" not in captured.out @mock.patch("os.execvpe", new=execvpe_mock) def test_cache(pipx_temp_env, monkeypatch, capsys, caplog): run_pipx_cli_exit(["run", "pycowsay", "cowsay", "args"]) caplog.set_level(logging.DEBUG) run_pipx_cli_exit(["run", "--verbose", "pycowsay", "cowsay", "args"], assert_exit=0) assert "Reusing cached venv" in caplog.text run_pipx_cli_exit(["run", "--no-cache", "pycowsay", "cowsay", "args"]) assert "Removing cached venv" in caplog.text @mock.patch("os.execvpe", new=execvpe_mock) def test_run_script_from_internet(pipx_temp_env, capsys): run_pipx_cli_exit( [ "run", "https://gist.githubusercontent.com/cs01/" "fa721a17a326e551ede048c5088f9e0f/raw/" "6bdfbb6e9c1132b1c38fdd2f195d4a24c540c324/pipx-demo.py", ], assert_exit=0, ) @pytest.mark.parametrize( "input_run_args,expected_app_with_args", [ (["--", "pycowsay", "--", "hello"], ["pycowsay", "--", "hello"]), (["--", "pycowsay", "--", "--", "hello"], ["pycowsay", "--", "--", "hello"]), (["--", "pycowsay", "hello", "--"], ["pycowsay", "hello", "--"]), (["--", "pycowsay", "hello", "--", "--"], ["pycowsay", "hello", "--", "--"]), (["--", "pycowsay", "--"], ["pycowsay", "--"]), (["--", "pycowsay", "--", "--"], ["pycowsay", "--", "--"]), (["pycowsay", "--", "hello"], ["pycowsay", "--", "hello"]), (["pycowsay", "--", "--", "hello"], ["pycowsay", "--", "--", "hello"]), (["pycowsay", "hello", "--"], ["pycowsay", "hello", "--"]), (["pycowsay", "hello", "--", "--"], ["pycowsay", "hello", "--", "--"]), (["pycowsay", "--"], ["pycowsay", "--"]), (["pycowsay", "--", "--"], ["pycowsay", "--", "--"]), (["--", "--", "pycowsay", "--"], ["--", "pycowsay", "--"]), ], ) def test_appargs_doubledash( pipx_temp_env, capsys, monkeypatch, input_run_args, expected_app_with_args ): parser = pipx.main.get_command_parser() monkeypatch.setattr(sys, "argv", ["pipx", "run"] + input_run_args) parsed_pipx_args = parser.parse_args() pipx.main.check_args(parsed_pipx_args) assert parsed_pipx_args.app_with_args == expected_app_with_args def test_run_ensure_null_pythonpath(): env = os.environ.copy() env["PYTHONPATH"] = "test" assert ( "None" in subprocess.run( [ sys.executable, "-m", "pipx", "run", "ipython", "-c", "import os; print(os.environ.get('PYTHONPATH'))", ], universal_newlines=True, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).stdout ) # packages listed roughly in order of increasing test duration @pytest.mark.parametrize( "package, package_or_url, app_appargs, skip_win", [ ("pycowsay", "pycowsay", ["pycowsay", "hello"], False), ("shell-functools", PKG["shell-functools"]["spec"], ["filter", "--help"], True), ("black", PKG["black"]["spec"], ["black", "--help"], False), ("pylint", PKG["pylint"]["spec"], ["pylint", "--help"], False), ("kaggle", PKG["kaggle"]["spec"], ["kaggle", "--help"], False), ("ipython", PKG["ipython"]["spec"], ["ipython", "--version"], False), ("cloudtoken", PKG["cloudtoken"]["spec"], ["cloudtoken", "--help"], True), ("awscli", PKG["awscli"]["spec"], ["aws", "--help"], True), # ("ansible", PKG["ansible"]["spec"], ["ansible", "--help"]), # takes too long ], ) @mock.patch("os.execvpe", new=execvpe_mock) def test_package_determination( caplog, pipx_temp_env, package, package_or_url, app_appargs, skip_win ): if sys.platform.startswith("win") and skip_win: # Skip packages with 'scripts' in setup.py that don't work on Windows pytest.skip() caplog.set_level(logging.INFO) run_pipx_cli_exit( ["run", "--verbose", "--spec", package_or_url, "--"] + app_appargs ) assert "Cannot determine package name" not in caplog.text assert f"Determined package name: {package}" in caplog.text pipx-1.0.0/tests/test_runpip.py000066400000000000000000000003111416500503600165530ustar00rootroot00000000000000from helpers import run_pipx_cli def test_runpip(pipx_temp_env, monkeypatch, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["runpip", "pycowsay", "list"]) pipx-1.0.0/tests/test_shared_libs.py000066400000000000000000000013231416500503600175210ustar00rootroot00000000000000import os import time import pytest # type: ignore from pipx import shared_libs @pytest.mark.parametrize( "mtime_minus_now,needs_upgrade", [ (-shared_libs.SHARED_LIBS_MAX_AGE_SEC - 5 * 60, True), (-shared_libs.SHARED_LIBS_MAX_AGE_SEC + 5 * 60, False), ], ) def test_auto_update_shared_libs( capsys, pipx_ultra_temp_env, mtime_minus_now, needs_upgrade ): now = time.time() shared_libs.shared_libs.create(verbose=True) shared_libs.shared_libs.has_been_updated_this_run = False access_time = now # this can be anything os.utime(shared_libs.shared_libs.pip_path, (access_time, mtime_minus_now + now)) assert shared_libs.shared_libs.needs_upgrade is needs_upgrade pipx-1.0.0/tests/test_uninstall.py000066400000000000000000000144001416500503600172530ustar00rootroot00000000000000import sys import pytest # type: ignore from helpers import app_name, mock_legacy_venv, remove_venv_interpreter, run_pipx_cli from package_info import PKG from pipx import constants def file_or_symlink(filepath): # Returns True for file or broken symlink or non-broken symlink # Returns False for no file and no symlink # filepath.exists() returns True for regular file or non-broken symlink # filepath.exists() returns False for no regular file or broken symlink # filepath.is_symlink() returns True for broken or non-broken symlink return filepath.exists() or filepath.is_symlink() def test_uninstall(pipx_temp_env): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["uninstall", "pycowsay"]) def test_uninstall_circular_deps(pipx_temp_env): assert not run_pipx_cli(["install", PKG["cloudtoken"]["spec"]]) assert not run_pipx_cli(["uninstall", "cloudtoken"]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_uninstall_legacy_venv(pipx_temp_env, metadata_version): executable_path = constants.LOCAL_BIN_DIR / app_name("pycowsay") assert not run_pipx_cli(["install", "pycowsay"]) assert executable_path.exists() mock_legacy_venv("pycowsay", metadata_version=metadata_version) assert not run_pipx_cli(["uninstall", "pycowsay"]) assert not file_or_symlink(executable_path) def test_uninstall_suffix(pipx_temp_env): name = "pbr" suffix = "_a" executable_path = constants.LOCAL_BIN_DIR / app_name(f"{name}{suffix}") assert not run_pipx_cli(["install", PKG[name]["spec"], f"--suffix={suffix}"]) assert executable_path.exists() assert not run_pipx_cli(["uninstall", f"{name}{suffix}"]) assert not file_or_symlink(executable_path) def test_uninstall_injected(pipx_temp_env): pycowsay_app_paths = [ constants.LOCAL_BIN_DIR / app for app in PKG["pycowsay"]["apps"] ] pylint_app_paths = [constants.LOCAL_BIN_DIR / app for app in PKG["pylint"]["apps"]] app_paths = pycowsay_app_paths + pylint_app_paths assert not run_pipx_cli(["install", PKG["pycowsay"]["spec"]]) assert not run_pipx_cli( ["inject", "--include-apps", "pycowsay", PKG["pylint"]["spec"]] ) for app_path in app_paths: assert app_path.exists() assert not run_pipx_cli(["uninstall", "pycowsay"]) for app_path in app_paths: assert not file_or_symlink(app_path) @pytest.mark.parametrize("metadata_version", ["0.1"]) def test_uninstall_suffix_legacy_venv(pipx_temp_env, metadata_version): name = "pbr" # legacy uninstall on Windows only works with "canonical name characters" # in suffix suffix = "-a" executable_path = constants.LOCAL_BIN_DIR / app_name(f"{name}{suffix}") assert not run_pipx_cli(["install", PKG[name]["spec"], f"--suffix={suffix}"]) mock_legacy_venv(f"{name}{suffix}", metadata_version=metadata_version) assert executable_path.exists() assert not run_pipx_cli(["uninstall", f"{name}{suffix}"]) assert not file_or_symlink(executable_path) @pytest.mark.parametrize("metadata_version", [None, "0.1", "0.2"]) def test_uninstall_with_missing_interpreter(pipx_temp_env, metadata_version): executable_path = constants.LOCAL_BIN_DIR / app_name("pycowsay") assert not run_pipx_cli(["install", "pycowsay"]) assert executable_path.exists() mock_legacy_venv("pycowsay", metadata_version=metadata_version) remove_venv_interpreter("pycowsay") assert not run_pipx_cli(["uninstall", "pycowsay"]) # On Windows we cannot remove app binaries if no metadata and no python if not (sys.platform.startswith("win") and metadata_version is None): assert not file_or_symlink(executable_path) @pytest.mark.parametrize("metadata_version", [None, "0.1", "0.2"]) def test_uninstall_proper_dep_behavior(pipx_temp_env, metadata_version): # isort is a dependency of pylint. Make sure that uninstalling pylint # does not also uninstall isort app in LOCAL_BIN_DIR isort_app_paths = [constants.LOCAL_BIN_DIR / app for app in PKG["isort"]["apps"]] pylint_app_paths = [constants.LOCAL_BIN_DIR / app for app in PKG["pylint"]["apps"]] assert not run_pipx_cli(["install", PKG["pylint"]["spec"]]) assert not run_pipx_cli(["install", PKG["isort"]["spec"]]) mock_legacy_venv("pylint", metadata_version=metadata_version) mock_legacy_venv("isort", metadata_version=metadata_version) for pylint_app_path in pylint_app_paths: assert pylint_app_path.exists() for isort_app_path in isort_app_paths: assert isort_app_path.exists() assert not run_pipx_cli(["uninstall", "pylint"]) for pylint_app_path in pylint_app_paths: assert not file_or_symlink(pylint_app_path) # THIS is what we're making sure is true: for isort_app_path in isort_app_paths: assert isort_app_path.exists() @pytest.mark.parametrize("metadata_version", [None, "0.1", "0.2"]) def test_uninstall_proper_dep_behavior_missing_interpreter( pipx_temp_env, metadata_version ): # isort is a dependency of pylint. Make sure that uninstalling pylint # does not also uninstall isort app in LOCAL_BIN_DIR isort_app_paths = [constants.LOCAL_BIN_DIR / app for app in PKG["isort"]["apps"]] pylint_app_paths = [constants.LOCAL_BIN_DIR / app for app in PKG["pylint"]["apps"]] assert not run_pipx_cli(["install", PKG["pylint"]["spec"]]) assert not run_pipx_cli(["install", PKG["isort"]["spec"]]) mock_legacy_venv("pylint", metadata_version=metadata_version) mock_legacy_venv("isort", metadata_version=metadata_version) remove_venv_interpreter("pylint") remove_venv_interpreter("isort") for pylint_app_path in pylint_app_paths: assert pylint_app_path.exists() for isort_app_path in isort_app_paths: assert isort_app_path.exists() assert not run_pipx_cli(["uninstall", "pylint"]) # Do not check the following on Windows without metadata, we do not # remove bin dir links by design for missing interpreter in that case if not (sys.platform.startswith("win") and metadata_version is None): for pylint_app_path in pylint_app_paths: assert not file_or_symlink(pylint_app_path) # THIS is what we're making sure is true: for isort_app_path in isort_app_paths: assert isort_app_path.exists() pipx-1.0.0/tests/test_uninstall_all.py000066400000000000000000000010321416500503600201000ustar00rootroot00000000000000import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli def test_uninstall_all(pipx_temp_env, capsys): assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["uninstall-all"]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_uninstall_all_legacy_venv(pipx_temp_env, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) assert not run_pipx_cli(["uninstall-all"]) pipx-1.0.0/tests/test_upgrade.py000066400000000000000000000055221416500503600166760ustar00rootroot00000000000000import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli from package_info import PKG def test_upgrade(pipx_temp_env, capsys): assert run_pipx_cli(["upgrade", "pycowsay"]) assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["upgrade", "pycowsay"]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_upgrade_legacy_venv(pipx_temp_env, capsys, metadata_version): assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) captured = capsys.readouterr() if metadata_version is None: assert run_pipx_cli(["upgrade", "pycowsay"]) captured = capsys.readouterr() assert ( "Not upgrading pycowsay. It has missing internal pipx metadata." in captured.err ) else: assert not run_pipx_cli(["upgrade", "pycowsay"]) captured = capsys.readouterr() def test_upgrade_suffix(pipx_temp_env, capsys): name = "pycowsay" suffix = "_a" assert not run_pipx_cli(["install", name, f"--suffix={suffix}"]) assert run_pipx_cli(["upgrade", f"{name}"]) assert not run_pipx_cli(["upgrade", f"{name}{suffix}"]) @pytest.mark.parametrize("metadata_version", ["0.1"]) def test_upgrade_suffix_legacy_venv(pipx_temp_env, capsys, metadata_version): name = "pycowsay" suffix = "_a" assert not run_pipx_cli(["install", name, f"--suffix={suffix}"]) mock_legacy_venv(f"{name}{suffix}", metadata_version=metadata_version) assert run_pipx_cli(["upgrade", f"{name}"]) assert not run_pipx_cli(["upgrade", f"{name}{suffix}"]) def test_upgrade_specifier(pipx_temp_env, capsys): name = "pylint" pkg_spec = PKG[name]["spec"] initial_version = pkg_spec.split("==")[-1] assert not run_pipx_cli(["install", f"{pkg_spec}"]) assert not run_pipx_cli(["upgrade", f"{name}"]) captured = capsys.readouterr() assert f"upgraded package {name} from {initial_version} to" in captured.out def test_upgrade_include_injected(pipx_temp_env, capsys): assert not run_pipx_cli(["install", PKG["pylint"]["spec"]]) assert not run_pipx_cli(["inject", "pylint", "black==18.9.b0"]) captured = capsys.readouterr() assert not run_pipx_cli(["upgrade", "--include-injected", "pylint"]) captured = capsys.readouterr() assert "upgraded package pylint" in captured.out assert "upgraded package black" in captured.out def test_upgrade_no_include_injected(pipx_temp_env, capsys): assert not run_pipx_cli(["install", PKG["pylint"]["spec"]]) assert not run_pipx_cli(["inject", "pylint", "black==18.9.b0"]) captured = capsys.readouterr() assert not run_pipx_cli(["upgrade", "pylint"]) captured = capsys.readouterr() assert "upgraded package pylint" in captured.out assert "upgraded package black" not in captured.out pipx-1.0.0/tests/test_upgrade_all.py000066400000000000000000000014761416500503600175320ustar00rootroot00000000000000import pytest # type: ignore from helpers import mock_legacy_venv, run_pipx_cli def test_upgrade_all(pipx_temp_env, capsys): assert run_pipx_cli(["upgrade", "pycowsay"]) assert not run_pipx_cli(["install", "pycowsay"]) assert not run_pipx_cli(["upgrade-all"]) @pytest.mark.parametrize("metadata_version", [None, "0.1"]) def test_upgrade_all_legacy_venv(pipx_temp_env, capsys, caplog, metadata_version): assert run_pipx_cli(["upgrade", "pycowsay"]) assert not run_pipx_cli(["install", "pycowsay"]) mock_legacy_venv("pycowsay", metadata_version=metadata_version) if metadata_version is None: capsys.readouterr() assert run_pipx_cli(["upgrade-all"]) assert "Error encountered when upgrading pycowsay" in caplog.text else: assert not run_pipx_cli(["upgrade-all"])