pax_global_header00006660000000000000000000000064151524207630014517gustar00rootroot0000000000000052 comment=29013e1f4449fd9539bd8df295ebd3cf39d1d84b sphinx-contrib-typer-8982731/000077500000000000000000000000001515242076300160365ustar00rootroot00000000000000sphinx-contrib-typer-8982731/.github/000077500000000000000000000000001515242076300173765ustar00rootroot00000000000000sphinx-contrib-typer-8982731/.github/CODEOWNERS000066400000000000000000000000411515242076300207640ustar00rootroot00000000000000# Default: everything * @bckohan sphinx-contrib-typer-8982731/.github/dependabot.yml000066400000000000000000000007651515242076300222360ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" sphinx-contrib-typer-8982731/.github/workflows/000077500000000000000000000000001515242076300214335ustar00rootroot00000000000000sphinx-contrib-typer-8982731/.github/workflows/lint.yml000066400000000000000000000035301515242076300231250ustar00rootroot00000000000000name: Lint permissions: contents: read on: push: tags-ignore: - '*' branches: ['main'] pull_request: branches: ['main'] workflow_call: workflow_dispatch: inputs: debug: description: 'Open ssh debug session.' required: true default: false type: boolean jobs: linting: runs-on: ubuntu-latest strategy: matrix: # run static analysis on bleeding and trailing edges python-version: [ '3.10', '3.13', '3.14' ] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 id: sp with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b with: enable-cache: false - name: Install Just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 with: detached: true timeout-minutes: 60 - name: Install Dependencies env: PYTHON_PATH: ${{ steps.sp.outputs.python-path }} run: | just setup "$PYTHON_PATH" - name: Run Static Analysis run: | just check-lint just check-format just check-types just check-package just check-readme just check-docs sphinx-contrib-typer-8982731/.github/workflows/release.yml000066400000000000000000000122761515242076300236060ustar00rootroot00000000000000 name: Publish Release permissions: read-all concurrency: # stop previous release runs if tag is recreated group: release-${{ github.ref }} cancel-in-progress: true on: push: tags: - 'v[0-9]*.[0-9]*.[0-9]*' # only publish on version tags (e.g. v1.0.0) jobs: lint: permissions: contents: read actions: write uses: ./.github/workflows/lint.yml test: permissions: contents: read actions: write uses: ./.github/workflows/test.yml secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} build: name: Build Package runs-on: ubuntu-latest permissions: contents: read actions: write outputs: PACKAGE_NAME: ${{ steps.set-package.outputs.package_name }} RELEASE_VERSION: ${{ steps.set-package.outputs.release_version }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: "3.12" # for tomllib - name: Install uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b with: enable-cache: false restore-cache: false save-cache: false - name: Setup Just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff - name: Verify Tag run: | TAG_NAME=${GITHUB_REF#refs/tags/} echo "Verifying tag $TAG_NAME..." # if a tag was deleted and recreated we may have the old one cached # be sure that we're publishing the current tag! git fetch --force origin refs/tags/$TAG_NAME:refs/tags/$TAG_NAME # verify signature curl -sL "https://github.com/${GITHUB_ACTOR}.gpg" | gpg --import git tag -v "$TAG_NAME" # verify version RELEASE_VERSION=$(just validate_version $TAG_NAME) # export the release version echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV - name: Build the binary wheel and a source tarball run: just build - name: Store the distribution packages uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: python-package-distributions path: dist/ - name: Set Package Name id: set-package run: | PACKAGE_NAME=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT publish-to-testpypi: name: Publish to TestPyPI needs: - build runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/project/${{ needs.build.outputs.PACKAGE_NAME }} permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: python-package-distributions path: dist/ - name: Publish distribution to TestPyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e with: repository-url: https://test.pypi.org/legacy/ skip-existing: true publish-to-pypi: name: Publish to PyPI needs: - lint - test - build - publish-to-testpypi runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/${{ needs.build.outputs.PACKAGE_NAME }} permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e github-release: name: Publish GitHub Release runs-on: ubuntu-latest needs: - lint - test - build permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@a5caf349bc536fbef3668a10ed7f5cd309a4b53d with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_REPOSITORY: ${{ github.repository }} run: >- gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --generate-notes --prerelease - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_REPOSITORY: ${{ github.repository }} run: >- gh release upload "$GITHUB_REF_NAME" dist/** --repo "$GITHUB_REPOSITORY" sphinx-contrib-typer-8982731/.github/workflows/scorecard.yml000066400000000000000000000047631515242076300241350ustar00rootroot00000000000000name: OpenSSF Scorecard on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained push: branches: [ main ] workflow_dispatch: permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: security-events: write id-token: write steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e with: sarif_file: results.sarif sphinx-contrib-typer-8982731/.github/workflows/test.yml000066400000000000000000000075331515242076300231450ustar00rootroot00000000000000name: Test permissions: contents: read on: push: tags-ignore: - '*' branches: ['main'] pull_request: branches: ['main'] workflow_call: secrets: CODECOV_TOKEN: required: true workflow_dispatch: inputs: debug: description: 'Open ssh debug session.' required: true default: false type: boolean schedule: # Every Sunday at 9:00 AM Los Angeles time. # GitHub cron is UTC; 09:00 PT == 17:00 UTC (PST). - cron: '0 17 * * 0' jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] sphinx-version: ['6', '7', '8', '9'] exclude: - python-version: '3.12' sphinx-version: '6' - python-version: '3.13' sphinx-version: '6' - python-version: '3.14' sphinx-version: '6' - python-version: '3.12' sphinx-version: '7' - python-version: '3.13' sphinx-version: '7' - python-version: '3.11' sphinx-version: '9' - python-version: '3.14' sphinx-version: '7' - python-version: '3.10' sphinx-version: '9' env: COVERAGE_FILE: py${{ matrix.python-version }}-sphinx${{ matrix.sphinx-version }}.coverage steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b with: enable-cache: false - name: Upgrade all dependencies for our weekly runs if: github.event_name == 'schedule' run: uv lock --upgrade - name: Install Just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 with: detached: true timeout-minutes: 60 - name: Install System Dependencies run: | sudo apt-get install libopenblas-dev libxml2-dev libxslt1-dev zlib1g-dev - name: Run Unit Tests run: | just test-sphinx ${{ matrix.sphinx-version }} - name: Store coverage files uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} coverage-combine: needs: [test] runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 - name: Install uv uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b with: enable-cache: false - name: Install Just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff - name: Install Dependencies run: | just setup - name: Get coverage files uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: pattern: "*.coverage" merge-multiple: true - run: ls -la *.coverage - run: just coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml verbose: true sphinx-contrib-typer-8982731/.github/workflows/zizmor.yml000066400000000000000000000030511515242076300235070ustar00rootroot00000000000000name: Zizmor on: push: branches: [ main ] pull_request: schedule: # Run weekly - cron: '0 0 * * 0' workflow_dispatch: permissions: contents: read jobs: zizmor-analysis: name: Run Zizmor runs-on: ubuntu-latest permissions: contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Set up Rust uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 - name: Install jq run: | sudo apt-get update sudo apt-get install -y jq - name: Install Zizmor run: | cargo install --locked zizmor - name: Run Zizmor analysis run: | zizmor --format sarif .github/workflows/ > zizmor.sarif - name: Upload analysis results uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: zizmor-results path: zizmor.sarif retention-days: 7 - name: Upload to code-scanning uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e with: sarif_file: zizmor.sarif - name: Fail on Findings run: | count="$( jq '([.runs[]? | (.results // [])[] | select(.level != "note")] | length) // 0' \ zizmor.sarif )" echo "Zizmor findings: $count" test "$count" -eq 0 sphinx-contrib-typer-8982731/.gitignore000066400000000000000000000063051515242076300200320ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .DS_Store doc/source/typer_cache.json poetry.lock tests/**/build tests/click/callback_record.json tests/click/cache.json tests/click/validation/resized_html.png uv.lock CLAUDE.md zizmor.sarif sphinx-contrib-typer-8982731/.pre-commit-config.yaml000066400000000000000000000003741515242076300223230ustar00rootroot00000000000000repos: - repo: local hooks: - id: lint name: Lint entry: just lint language: system pass_filenames: false - id: format name: Format entry: just format language: system pass_filenames: false sphinx-contrib-typer-8982731/.readthedocs.yaml000066400000000000000000000012271515242076300212670ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" jobs: post_install: - pip install uv - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --link-mode=copy # Build documentation in the docs/ directory with Sphinx sphinx: configuration: doc/source/conf.py # Optionally build your docs in additional formats such as PDF and ePub # remove pdf for now - sphinx tabs does not support formats: - pdf sphinx-contrib-typer-8982731/CONTRIBUTING.md000066400000000000000000000112321515242076300202660ustar00rootroot00000000000000 # Contributing Contributions are encouraged! Please use the issue page to submit feature requests or bug reports. Issues with attached PRs will be given priority and have a much higher likelihood of acceptance. Please also open an issue and associate it with any submitted PRs. That said, the aim is to keep this library as lightweight as possible. Only features with broad based use cases will be considered. We are actively seeking additional maintainers. If you're interested, please [contact me](https://github.com/bckohan). ## Installation ### Install Just We provide a platform independent justfile with recipes for all the development tasks. You should [install just](https://just.systems/man/en/installation.html) if it is not on your system already. `sphinxcontrib-typer` uses [uv](https://docs.astral.sh/uv) for environment, package and dependency management: ```bash just install-uv ``` Next, initialize and install the development environment: ```bash just setup just install ``` ## Documentation `sphinxcontrib-typer` documentation is generated using [Sphinx](https://www.sphinx-doc.org/en/master/). Any new feature PRs must provide updated documentation for the features added. To build the docs run: ```bash just docs ``` You can run a live documentation server that will automatically update during editing using: ```bash just docs-live ``` To build the pdf documentation: ```bash just build-docs-pdf ``` ## Static Analysis Before any PR is accepted the following must be run, and static analysis tools should not produce any errors or warnings. Disabling certain errors or warnings where justified is acceptable: ```bash just check ``` ## Running Tests `sphinxcontrib-typer` is setup to use [pytest](https://docs.pytest.org/en/stable/) to run unit tests. All the tests are housed in tests/tests.py. Before a PR is accepted, all tests must be passing and the code coverage must be at as high as it was before. A small number of exempted error handling branches are acceptable. To run the full suite: ```bash just test ``` To run a single test, or group of tests in a class: ```bash just test ::ClassName::FunctionName ``` For instance to run the docs test you would do: ```bash just test tests/tests.py::test_sphinx_html_build ``` ## Just Recipes ```bash build # build src package and wheel build-docs # build the docs build-docs-html # build html documentation build-docs-pdf # build pdf documentation check # run all static checks check-docs # lint the documentation check-docs-links # check the documentation links for broken links check-format # check if the code needs formatting check-lint # lint the code check-package # run package checks check-readme # check that the readme renders check-types # run static type checking clean # remove all non repository artifacts clean-docs # remove doc build artifacts clean-env # remove the virtual environment clean-git-ignored # remove all git ignored files coverage # generate the test coverage report coverage-erase # erase any coverage data docs # build and open the documentation docs-live # serve the documentation, with auto-reload fetch-refs LIB # fetch the intersphinx references for the given package fix # fix formatting, linting issues and import sorting format # format the code and sort imports install *OPTS # update and install development dependencies install-basic # install without extra dependencies install-precommit # install git pre-commit hooks install-uv # install the uv package manager lint # sort the imports and fix linting issues open-docs # open the html documentation precommit # run the pre-commit checks release VERSION # issue a relase for the given semver string (e.g. 2.1.0) run +ARGS # run the command in the virtual environment setup python="python" # setup the venv and pre-commit hooks sort-imports # sort the python imports test *TESTS # run tests test-lock +PACKAGES # lock to specific python and versions of given dependencies validate_version VERSION # validate the given version string against the lib version ```sphinx-contrib-typer-8982731/LICENSE000066400000000000000000000020611515242076300170420ustar00rootroot00000000000000MIT License Copyright (c) 2023-2026 Brian Kohan 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. sphinx-contrib-typer-8982731/README.md000066400000000000000000000130301515242076300173120ustar00rootroot00000000000000# sphinxcontrib-typer [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![PyPI version](https://badge.fury.io/py/sphinxcontrib-typer.svg)](https://pypi.python.org/pypi/sphinxcontrib-typer/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/sphinxcontrib-typer.svg)](https://pypi.python.org/pypi/sphinxcontrib-typer/) [![PyPI status](https://img.shields.io/pypi/status/sphinxcontrib-typer.svg)](https://pypi.python.org/pypi/sphinxcontrib-typer) [![Documentation Status](https://readthedocs.org/projects/sphinxcontrib-typer/badge/?version=latest)](http://sphinxcontrib-typer.readthedocs.io/?badge=latest/) [![Code Cov](https://codecov.io/gh/sphinx-contrib/typer/branch/main/graph/badge.svg?token=0IZOKN2DYL)](https://app.codecov.io/gh/sphinx-contrib/typer) [![Test Status](https://github.com/sphinx-contrib/typer/workflows/Test/badge.svg)](https://github.com/sphinx-contrib/typer/actions/workflows/test.yml) [![Lint Status](https://github.com/sphinx-contrib/typer/workflows/Lint/badge.svg)](https://github.com/sphinx-contrib/typer/actions/workflows/lint.yml) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sphinx-contrib/typer/badge)](https://securityscorecards.dev/viewer/?uri=github.com/sphinx-contrib/typer) A Sphinx directive for auto generating docs for [Typer](https://typer.tiangolo.com/) (and [Click](https://click.palletsprojects.com/) commands!) using the rich console formatting available in [Typer](https://typer.tiangolo.com/). This package generates concise command documentation in text, html or svg formats out of the box, but if your goal is to greatly customize the generated documentation [sphinx-click](https://sphinx-click.readthedocs.io/en/latest/) may be more appropriate and will also work for [Typer](https://typer.tiangolo.com/) commands. ## Installation Install with pip:: pip install sphinxcontrib-typer Add ``sphinxcontrib.typer`` to your ``conf.py`` file: ```python # be sure that the commands you want to document are importable # from the python path when building the docs import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / '../path/to/your/commands')) extensions = [ ... 'sphinxcontrib.typer', ... ] ``` ## Usage Say you have a command in the file ``examples/example.py`` that looks like this: ```python import typer import typing as t app = typer.Typer(add_completion=False) @app.callback() def callback( flag1: bool = typer.Option(False, help="Flag 1."), flag2: bool = typer.Option(False, help="Flag 2.") ): """This is the callback function.""" pass @app.command() def foo( name: str = typer.Option(..., help="The name of the item to foo.") ): """This is the foo command.""" pass @app.command() def bar( names: t.List[str] = typer.Option(..., help="The names of the items to bar."), ): """This is the bar command.""" pass if __name__ == "__main__": app() ``` You can generate documentation for this command using the ``typer`` directive like so: ```rst .. typer:: examples.example.app :prog: example1 :width: 70 :preferred: html ``` This would generate html that looks like this: ![Example PNG](https://raw.githubusercontent.com/sphinx-contrib/typer/main/example.html.png) You could change ``:preferred:`` to svg, to generate svg instead: ![Example SVG](https://raw.githubusercontent.com/sphinx-contrib/typer/main/example.svg) | Or to text Usage: example [OPTIONS] COMMAND [ARGS]... This is the callback function. ╭─ Options ──────────────────────────────────────────────────────────╮ │ --flag1 --no-flag1 Flag 1. [default: no-flag1] │ │ --flag2 --no-flag2 Flag 2. [default: no-flag2] │ │ --help Show this message and exit. │ ╰────────────────────────────────────────────────────────────────────╯ ╭─ Commands ─────────────────────────────────────────────────────────╮ │ bar This is the bar command. │ │ foo This is the foo command. │ ╰────────────────────────────────────────────────────────────────────╯ The ``typer`` directive has options for generating docs for all subcommands as well and optionally generating independent sections for each. There are also mechanisms for passing options to the underlying console and svg generation functions. See the official documentation for more information. sphinx-contrib-typer-8982731/doc/000077500000000000000000000000001515242076300166035ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/examples/000077500000000000000000000000001515242076300204215ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/examples/__init__.py000066400000000000000000000000001515242076300225200ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/examples/example.py000066400000000000000000000017661515242076300224400ustar00rootroot00000000000000import typer import typing as t try: from enum import StrEnum except ImportError: from enum import Enum class StrEnum(str, Enum): pass from typing_extensions import Annotated app = typer.Typer(add_completion=False) class Kind(StrEnum): ONE = "one" TWO = "two" @app.callback() def callback( arg: Annotated[Kind, typer.Argument(help="An argument.")], flag: Annotated[bool, typer.Option(help="Flagged.")] = False, switch: Annotated[ bool, typer.Option("--switch", "-s", help="Switch.") ] = False ): """This is the callback function.""" pass @app.command() def foo( name: Annotated[ str, typer.Option(..., help="The name of the item to foo.") ] ): """This is the foo command.""" pass @app.command() def bar( names: Annotated[ t.List[str], typer.Option(..., help="The names of the items to bar.") ], ): """This is the bar command.""" pass if __name__ == "__main__": app() sphinx-contrib-typer-8982731/doc/examples/themes.py000066400000000000000000000010131515242076300222530ustar00rootroot00000000000000from rich.terminal_theme import TerminalTheme red_sands = { 'theme': TerminalTheme( (132, 42, 38), # background (210, 193, 159), # text [ (210, 193, 159), # ( 0, 0, 0), # required ( 77, 218, 77), # option on short name (227, 189, 57), # Usage/metavar (210, 193, 159), # ( 0, 18, 140), # option off ( 75, 214, 225), # option on/command names (210, 193, 159), # ] ) } sphinx-contrib-typer-8982731/doc/source/000077500000000000000000000000001515242076300201035ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/source/_static/000077500000000000000000000000001515242076300215315ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/source/_static/style.css000066400000000000000000000022361515242076300234060ustar00rootroot00000000000000.wy-table-responsive table td, .wy-table-responsive table th { white-space: inherit; } section#reference div.highlight pre { color: #b30000; display: block; /* ensures it's treated as a block */ margin-left: auto; /* auto margins center block elements */ margin-right: auto; width: fit-content; } body[data-theme="light"] section#reference div.highlight, body[data-theme="light"] section#reference div.highlight pre { background-color: #f8f8f8; } body[data-theme="dark"] section#reference div.highlight, body[data-theme="dark"] section#reference div.highlight pre { background-color: #202020; } /* AUTO → system prefers DARK (acts like dark unless user forced light) */ @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) #reference .highlight, body:not([data-theme="light"]) #reference .highlight pre { background-color: #202020; } } /* AUTO → system prefers LIGHT (acts like light unless user forced dark) */ @media (prefers-color-scheme: light) { body:not([data-theme="dark"]) #reference .highlight, body:not([data-theme="dark"]) #reference .highlight pre { background-color: #f8f8f8; } } sphinx-contrib-typer-8982731/doc/source/changelog.rst000066400000000000000000000157151515242076300225750ustar00rootroot00000000000000.. include:: ./refs.rst ========== Change Log ========== v0.8.1 (2026-03-05) =================== * Tighten up CI * Add typed classifier and badges * Fixed `When typer is used in docstrings image paths can break in pdf builds. `_ v0.8.0 (2026-02-12) =================== * Report version to sphinx loader appropriately. * Change dependency to :pypi:`typer` instead of :pypi:`typer-slim`. This was done to track `upstream packaging changes `_. v0.7.2 (2025-12-23) =================== * `Fix compatibility with typer 0.20.1+ `_ v0.7.1 (2025-12-22) =================== * `AttributeError: module 'typer.rich_utils' has no attribute 'MarkupMode' with typer 0.20.1 `_ v0.7.0 (2025-11-22) =================== * `Drop python 3.9 support `_ v0.6.2 (2025-09-23) =================== * Fix changelog. v0.6.1 (2025-09-23) =================== * Added project urls to pypi package. v0.6.0 (2025-09-22) =================== * Implemented `Use intersphinx for better integration with the sphinx ecosystem `_ * Implemented `Support python 3.14. `_ * Implemented `Switch from poetry to uv. `_ v0.5.1 (2024-10-14) =================== * Fixed `Spaces in image filenames can cause problems with pdf builds. `_ v0.5.0 (2024-08-24) =================== * Implemented `Support Sphinx 8 `_ * Implemented `Support Python 3.13 `_ * Fixed `typer 0.12.5+ breaks click compatibility `_ v0.4.2 (2024-08-22) ==================== * Fixed `:typer: role default link text has colon where space expected `_ * Fixed `:typer: role does not allow link text `_ v0.4.1 (2024-08-22) ==================== * Fixed `:typer: role does not work if processed before the definition. `_ * Fixed `:typer: role link text does not reflect the actual command invocation. `_ v0.4.0 (2024-08-19) ==================== * Implemented `Use builtin sphinx cache to cache iframe heights instead of custom file. `_ * Implemented `Allow cross-referencing `_ v0.3.5 (2024-08-17) ==================== * Fixed `Lazily loaded commands throw an exception. `_ * Fixed `Changes are not detected across sphinx-build `_ v0.3.4 (2024-08-15) ==================== * Fixed `list_commands order should be honored when generated nested sections for subcommands. `_ v0.3.3 (2024-07-15) ==================== * Removed errant deepcopy introduced in last release. v0.3.2 (yanked) =============== * Implemented `Add blue waves theme. `_ * Implemented `Add red sands theme. `_ v0.3.1 (2024-06-11) ==================== * Fixed `ruff dependency should be dev dependency only. `_ v0.3.0 (2024-06-01) ==================== * Implemented `Allow function hooks to be specified as import strings. `_ * Fixed `pdf builds are broken. `_ v0.2.5 (2024-05-29) ==================== * Fixed `Proxied Typer object check is broken. `_ v0.2.4 (2024-05-29) ==================== * Fixed `Support more flexible duck typing for detecting Typer objects `_ v0.2.3 (2024-05-22) ==================== * Fixed `when :prog: is supplied and a subcommand help is generated the usage string includes incorrect prefix to prog `_ v0.2.2 (2024-05-14) ==================== * Fixed `Move to ruff for tooling `_ * Fixed `Fix WARNING: cannot cache unpickable configuration value `_ v0.2.1 (2024-04-11) ==================== * Implemented `Convert README and CONTRIBUTING to markdown. `_ * Fixed `typer-slim[all] no longer works to bring in rich `_ v0.2.0 (2024-04-03) ==================== * Fixed `typer 0.12.0 package split breaks upgrades. `_ v0.1.12 (2024-03-05) ===================== * Fixed `Typer with sphinx-autobuild going on infinite loop `_ v0.1.11 (2024-02-22) ===================== * Fixed `Typer dependency erroneously downgraded. `_ v0.1.10 (2024-02-21) ===================== * Fixed `Pillow version not specified for png optional dependency. `_ v0.1.9 (2024-02-21) ==================== * Fixed `Some dependencies have unnecessarily recent version requirements. `_ v0.1.8 (2024-02-20) ==================== * Fixed `When using convert-png sometimes the pngs images are cutoff too short. `_ v0.1.7 (2024-01-31) ==================== * Fixed reopened issue: `nested class attribute import paths for typer apps are broken. `_ v0.1.6 (2024-01-31) ==================== * Fixed `nested class attribute import paths for typer apps are broken. `_ v0.1.5 (2024-01-30) ==================== * Fixed `When the sphinx app is an attribute on a class the import fails. `_ v0.1.4 (2023-12-21) ==================== * Meta data updated reflecting repository move into the sphinx-contrib organization. v0.1.3 (2023-12-19) ==================== * Fixed repository location in package meta data. v0.1.2 (2023-12-19) ==================== * Try big 4 web browser managers before giving up when :pypi:`selenium` features are used. * Fixed pypi.org rendering of the readme, and rtd documentation build. v0.1.1 (2023-12-19) ==================== * Fixed pypi.org rendering of the readme. v0.1.0 (2023-12-19) ==================== * Initial Release sphinx-contrib-typer-8982731/doc/source/conf.py000066400000000000000000000062521515242076300214070ustar00rootroot00000000000000from datetime import datetime import sys from pathlib import Path from sphinxcontrib import typer as sphinxcontrib_typer import shutil # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- sys.path.append(str(Path(__file__).parent.parent)) sys.path.append(str(Path(__file__).parent / 'typer_doc_ext')) # -- Project information ----------------------------------------------------- project = sphinxcontrib_typer.__title__ copyright = sphinxcontrib_typer.__copyright__ author = sphinxcontrib_typer.__author__ release = sphinxcontrib_typer.__version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinxcontrib.typer', "sphinx.ext.viewcode", 'sphinx.ext.intersphinx' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'furo' html_theme_options = { "source_repository": "https://github.com/sphinx-contrib/typer/", "source_branch": "main", "source_directory": "doc/source", } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] html_css_files = ['style.css'] html_js_files = [] todo_include_todos = True latex_engine = 'xelatex' typer_get_web_driver = 'web_driver.typer_get_web_driver' autodoc_typehints = "description" # or signature autodoc_typehints_format = "short" intersphinx_mapping = { "click": ("https://click.palletsprojects.com/en/stable", None), "rich": ("https://rich.readthedocs.io/en/stable", None), "python": ('https://docs.python.org/3', None), "sphinx-rtd-theme": ("https://sphinx-rtd-theme.readthedocs.io/en/stable", None), "sphinx-click": ("https://sphinx-click.readthedocs.io/en/stable", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None) } def pypi_role(name, rawtext, text, lineno, inliner, options={}, content=[]): from docutils import nodes url = f"https://pypi.org/project/{text}/" node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] def setup(app): from docutils.parsers.rst import roles roles.register_local_role("pypi", pypi_role) if Path(app.doctreedir).exists(): shutil.rmtree(app.doctreedir) return app sphinx-contrib-typer-8982731/doc/source/howto.rst000066400000000000000000000152521515242076300220020ustar00rootroot00000000000000.. include:: ./refs.rst How To ====== The examples below all reference this example Typer_ application: .. literalinclude:: ../examples/example.py :language: python :linenos: :caption: examples/example.py | Build to Multiple Formats ------------------------- :doc:`sphinx:index` caches directive output and reuses the results when building the documentation to different formats (e.g. html, pdf or text). This causes problems with the way the typer directive dynamically determines which render target to use based on the active builder. This can mean that if you run :doc:`sphinx:man/sphinx-build` for html and :class:`latexpdf ` at the same time the pdf may not render all typer helps as expected. To work around this you can do one of four things 1. Run :doc:`sphinx:man/sphinx-build` for each format separately. 2. Use the :rst:dir:`sphinx:only` directive in combination with :rst:dir:`typer:preferred` to specify builder specific content. 3. Use the :option:`--fresh-env ` option to force sphinx to rebuild the directive output for each builder. 4. Add the following code to your conf.py to remove the doctree between builders: .. code-block:: python def setup(app): import shutil from pathlib import Path if Path(app.doctreedir).exists(): shutil.rmtree(app.doctreedir) Change the Width ---------------- The :rst:dir:`typer:width` parameter defines the console character length :doc:`rich ` uses when it generates the console output. If your image is too wide, you can reduce the width by setting the :rst:dir:`typer:width` parameter to a smaller value. For example, for :doc:`sphinx-rtd-theme ` theme a width parameter of 65 works well: .. code-block:: rst .. typer:: examples.example:app :width: 65 .. typer:: examples.example:app :width: 100 .. typer:: examples.example:app :width: 65 :convert-png: latex .. typer:: examples.example:app :width: 100 :convert-png: latex | .. _render_structure: Render Subcommand Structure --------------------------- Add the :rst:dir:`typer:show-nested` and :rst:dir:`typer:make-sections` options to the typer directive. This will render all subcommands as sections. .. code-block:: rst .. typer:: examples.example:app :width: 65 :show-nested: :make-sections: .. typer:: examples.example:app :width: 65 :show-nested: :make-sections: :convert-png: latex .. tip:: See :ref:`cross_references` for information on how to cross reference sections. | Render a Single Subcommand -------------------------- Subcommands can be rendered individually: .. code-block:: rst .. typer:: examples.example:app:bar :width: 65 :show-nested: :make-sections: .. typer:: examples.example:app:bar :width: 65 :show-nested: :make-sections: :convert-png: latex | Render as HTML -------------- By default for html builders, svg output is generated. HTML output is also supported, but requires rendering the html output into an iframe to isolate the generated css. The iframe heights can be given directly using the :rst:dir:`typer:iframe-height` option - or dynamically calculated using :pypi:`selenium` and a web driver. To use the dynamic height calculation, you must install the html dependency set: .. code-block:: bash pip install sphinxcontrib-typer[html] Otherwise provide the :rst:dir:`typer:iframe-height` option. Use :rst:dir:`typer:preferred` html to render the html output .. code-block:: rst .. typer:: examples.example:app :preferred: html :width: 65 :iframe-height: 300 .. typer:: examples.example:app :preferred: html :width: 65 :iframe-height: 300 :convert-png: latex | Generate Nice PDFs ------------------ By default the latex builder will convert the preferred rendering output to pdf. This may not render predictably if the necessary fonts are not installed. You will likely need to install `FiraCode `_. You will also need to install the pdf dependency set: .. code-block:: bash pip install sphinxcontrib-typer[pdf] Alternatively you can convert the rendered helps to png format using the :rst:dir:`typer:convert-png` option and passing it the builders you want to render pngs. You will also need to install the png dependency set: .. code-block:: bash pip install sphinxcontrib-typer[png] Any format can be converted to png - even text! .. code-block:: rst .. typer:: examples.example:app :preferred: text :width: 90 .. typer:: examples.example:app :preferred: text :width: 90 :convert-png: latex|html .. typer:: examples.example:app :preferred: text :width: 75 .. typer:: examples.example:app :preferred: text :width: 90 :convert-png: latex|html | :class:`latexpdf ` often has issues with unicode characters. You may get better results using the xeLaTeX engine instead, especially when rendering text. In your conf.py add: .. code-block:: python latex_engine = "xelatex" Customize the Rendered Output ----------------------------- The initialization parameters for the :doc:`rich console ` and export functions can be overridden to provide more fine grained control over the rendered output. For example, to render a console that looks like Red Sands on OSX we can use the :rst:dir:`typer:svg-kwargs` option, and pass an import string to a dictionary of kwargs to pass to :meth:`rich.console.export_svg`. .. literalinclude:: ../examples/themes.py :language: python :linenos: :caption: examples/themes.py .. code-block:: rst .. typer:: examples.example:app :width: 60 :preferred: svg :svg-kwargs: examples.themes.red_sands .. typer:: examples.example:app :width: 60 :preferred: svg :svg-kwargs: examples.themes.red_sands :convert-png: latex The preset Console parameters can also be overridden using the :rst:dir:`typer:console-kwargs` option. Refer to the :doc:`rich ` documentation for more information on the available options. .. _cross_references: Cross-Reference with :make-sections: ------------------------------------ When using the :rst:dir:`typer:make-sections` option, a section will be generated for each subcommand. You can cross reference these sections using the ``:typer:`` role. For example, to reference the :typer:`example-bar` subcommand from the :ref:`render_structure` section above: .. code-block:: rst :typer:`example-bar` The format for the reference is ``prog(-subcommand)`` sphinx-contrib-typer-8982731/doc/source/index.rst000066400000000000000000000063501515242076300217500ustar00rootroot00000000000000.. include:: ./refs.rst =================== sphinxcontrib-typer =================== .. only:: html .. image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://opensource.org/licenses/MIT :alt: License: MIT .. image:: https://badge.fury.io/py/sphinxcontrib-typer.svg :target: https://pypi.python.org/pypi/sphinxcontrib-typer/ :alt: Package Version .. image:: https://img.shields.io/pypi/pyversions/sphinxcontrib-typer.svg :target: https://pypi.python.org/pypi/sphinxcontrib-typer/ :alt: Python Versions .. image:: https://img.shields.io/pypi/status/sphinxcontrib-typer.svg :target: https://pypi.python.org/pypi/sphinxcontrib-typer :alt: Development Status .. image:: https://readthedocs.org/projects/sphinxcontrib-typer/badge/?version=latest :target: http://sphinxcontrib-typer.readthedocs.io/?badge=latest/ :alt: Documentation Status .. image:: https://codecov.io/gh/sphinx-contrib/typer/branch/main/graph/badge.svg?token=0IZOKN2DYL :target: https://app.codecov.io/gh/sphinx-contrib/typer :alt: Code Cov .. image:: https://github.com/sphinx-contrib/typer/workflows/Test/badge.svg :target: https://github.com/sphinx-contrib/typer/actions/workflows/test.yml :alt: Test Status .. image:: https://github.com/sphinx-contrib/typer/workflows/Lint/badge.svg :target: https://github.com/sphinx-contrib/typer/actions/workflows/lint.yml :alt: Lint Status .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://api.securityscorecards.dev/projects/github.com/sphinx-contrib/typer/badge :target: https://securityscorecards.dev/viewer/?uri=github.com/sphinx-contrib/typer :alt: OSSF Scorecard A Sphinx directive for auto generating docs for Typer_ (and :doc:`Click ` commands!) using the rich console formatting available in Typer_. This package generates concise command documentation in text, html or svg formats out of the box, but if your goal is to greatly customize the generated documentation :doc:`sphinx-click:index` may be more appropriate and will also work for Typer_ commands. See the `github `_ repository for issue tracking and source code and install from :pypi:`sphinxcontrib-typer` with ``pip install sphinxcontrib-typer``. For example, commands and subcommands are renderable separately in four different formats: * svg * html * text * png .. typer:: examples.example.app :convert-png: latex :preferred: html | .. typer:: examples.example.app:foo :width: 70 :preferred: html :convert-png: latex | .. typer:: examples.example.app:bar :width: 92 :preferred: text :convert-png: latex The ``typer`` directive has options for generating docs for all subcommands as well and optionally generating independent sections for each. There are also mechanisms for passing options to the underlying console and svg generation functions. See table of contents for more information. .. toctree:: :maxdepth: 3 :caption: Contents: installation howto themes reference/index changelog sphinx-contrib-typer-8982731/doc/source/installation.rst000066400000000000000000000022761515242076300233450ustar00rootroot00000000000000.. include:: ./refs.rst Installation ============ The basic library can be installed with pip: .. code-block:: bash ?> pip install sphinxcontrib-typer There are several optional dependency sets that are involved in more advanced automated rendering. If you want to use :pypi:`selenium` to automatically determine the heights of the iframes when rendering in html you should install the html extras: .. code-block:: bash ?> pip install sphinxcontrib-typer[html] If you wish to convert rendered docs to png images you'll need the png dependency set: .. code-block:: bash ?> pip install sphinxcontrib-typer[png] If you wish to convert rendered docs to pdf format you'll need the pdf dependency set: .. code-block:: bash ?> pip install sphinxcontrib-typer[pdf] Once installed you need to add ``sphinxcontrib.typer`` to your ``conf.py`` file: .. code-block:: python # be sure that the commands you want to document are importable # from the python path when building the docs import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / 'path/to/your/commands')) extensions = [ ... 'sphinxcontrib.typer', ... ] sphinx-contrib-typer-8982731/doc/source/reference/000077500000000000000000000000001515242076300220415ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/source/reference/configuration.rst000066400000000000000000000077371515242076300254600ustar00rootroot00000000000000.. include:: ../refs.rst .. _configuration: .. role:: code-py(code) :language: Python ============= Configuration ============= The following extension scoped configuration parameters are available. These should be added to the :doc:`Sphinx configuration file `. For example, to override the default :func:`~sphinxcontrib.typer.typer_render_html` function our :doc:`conf.py ` might look like: .. code-block:: python import html extensions = [ 'sphinxcontrib.typer', ] # change the default iframe padding typer_iframe_height_padding = 20 # redfine the default render_html function def typer_render_html( directive: TyperDirective, normal_cmd: str, html_page: str ) -> str: return f'' Configuration Attributes ------------------------ .. confval:: typer_iframe_height_padding :type: :code-py:`int` :default: :code-py:`30` A number of pixels to use for padding html iframes. .. confval:: typer_render_html :type: :code-py:`str | Callable[[TyperDirective, str, str], str]` :default: :code-py:`"sphinxcontrib.typer.typer_render_html"` A callable function (or import path to a callable function) that returns the html to embed in an html page. Only used if the target format is html. See default implementation :func:`~sphinxcontrib.typer.typer_render_html`. .. confval:: typer_get_iframe_height :type: :code-py:`str | Callable[[TyperDirective, str, str], int]` :default: :code-py:`"sphinxcontrib.typer.typer_get_iframe_height"` A callable function that determines height of the iframe when rendering html format onto an html page. The function must return an integer containing the iframe height. See the default implementation :func:`~sphinxcontrib.typer.typer_get_iframe_height`. .. confval:: typer_svg2pdf :type: :code-py:`str | Callable[[TyperDirective, str, str], None]` :default: :code-py:`"sphinxcontrib.typer.typer_svg2pdf"` A callable function to convert svg to pdf. The function must write the converted pdf format to the given path. This is only used for latex/pdf builders. See the default implementation :func:`~sphinxcontrib.typer.typer_svg2pdf`. .. confval:: typer_convert_png :type: :code-py:`str | Callable[[TyperDirective, str, str | Path, int, int], None]` :default: :code-py:`"sphinxcontrib.typer.typer_convert_png"` A callable function to convert the given format to png. The function must write the converted png format to the given path. This function is used when the builder is listed in the :rst:dir:`typer:convert-png:` parameter. See the default implementation :func:`~sphinxcontrib.typer.typer_convert_png`. .. confval:: typer_get_web_driver :type: :code-py:`str | Callable[[TyperDirective, int, int], ContextManager[WebDriver]]` :default: :code-py:`"sphinxcontrib.typer.typer_get_web_driver"` A callable function to get a :pypi:`selenium` web driver. This function must be a context manager and it must yield a :pypi:`selenium` web driver. It is used by other workflows that need access to a webdriver. See the default implementation :func:`~sphinxcontrib.typer.typer_get_web_driver`. Function Hooks -------------- These functions may all be redefined in :doc:`conf.py ` to override default behaviors. Your override functions must conform to these function signatures. .. warning:: Sphinx will warn that these functions are not pickleable. This messes up sphinx's caching but that wont break the doc build. You can either suppress the warning or specify these configuration values as import strings instead. .. autofunction:: sphinxcontrib.typer.typer_render_html .. autofunction:: sphinxcontrib.typer.typer_get_iframe_height .. autofunction:: sphinxcontrib.typer.typer_svg2pdf .. autofunction:: sphinxcontrib.typer.typer_convert_png .. autofunction:: sphinxcontrib.typer.typer_get_web_driver sphinx-contrib-typer-8982731/doc/source/reference/directive.rst000066400000000000000000000132671515242076300245620ustar00rootroot00000000000000.. include:: ../refs.rst .. _directives: ========== Directives ========== .. rst:directive:: typer .. code-block:: rst .. typer:: import.path.to.module:main :prog: script_name :make-sections: :show-nested: :markup-mode: markdown :width: 65 :preferred: html :builders: html=html,svg,text:latex=svg,text:text=text :iframe-height: 600 :convert-png: html|latex :theme: light :console-kwargs: import.path.to.console_kwargs :svg-kwargs: import.path.to.svg_kwargs :html-kwargs: import.path.to.html_kwargs :text-kwargs: import.path.to.text_kwargs The only required parameter is the first argument. This is an import path to the Typer_ or :doc:`Click ` application to render. It may also include nested subcommands and may be delimited by either ``.``, ``:`` or ``::`` characters. For example, to render a subcommand called `print` from another subcommand called `add` in a Typer app named `app` in a module called `command` in a package called `mypackage`: .. code-block:: rst .. typer:: mypackage.command.app:add:print .. rst:directive:option:: prog: program cli name :type: text The script invocation name to use in the rendered help text. This parameter is optional, the directive will attempt to infer the name but this is not always possible to do reliably solely from the source code. If the name cannot be inferred, this parameter should be supplied. .. rst:directive:option:: make-sections :type: flag This is a flag parameter. If set, the directive will generate hierarchical sections for each command. .. rst:directive:option:: show-nested :type: flag This is a flag parameter. If set, the directive will include all subcommands in the command tree. .. rst:directive:option:: markup-mode :type: text Override the Typer rich_markup_mode command configuration value. Supports either ``markdown`` or ``rich``. See the `Typer docs `_. .. rst:directive:option:: width :type: integer **default**: ``65`` The width of the terminal window to use when rendering the help through rich. 65 is a good value for text or html renderings on the read the docs theme .. rst:directive:option:: preferred :type: text The preferred render format. Either ``html``, ``svg`` or ``text``. If not supplied the render format will be inferred from the builder's priority supported format list. You can replace the default priority lists with the **builders** parameter. The default format for the html and latex builders is *svg*. .. rst:directive:option:: builders :type: text - a colon delimited list of mappings from builder name to priority ordered csv of supported formats. **default**: html=html,svg,text:latex=svg,text:text=text Override the default builder priority render format lists. For example the preset is equivalent to:: html=html,svg,text:latex=svg,text:text=text This parameter can be helpful if you're rendering your docs with multiple builders and do not want the preset formats. .. rst:directive:option:: iframe-height :type: integer **default**: ``600`` The height of the iframe to use when rendering to *html*. When *html* rendering is embedded in an html page an iframe is used. The height of the iframe can be set with this parameter. Alternatively, the height of the iframe can be dynamically determined if :pypi:`selenium` is installed. See also iframe height cache. .. rst:directive:option:: convert-png :type: text - a delimited list of builders Convert the rendered help to a png file for this delimited list of builders. The delimiter can be any character. For example: .. code-block:: rst .. typer:: import.path.to.module:main :convert-png: html|latex All formats, *html*, *text* and *svg* can be converted to png. For some builders, namely pdf the *html* and *svg* formats may require non standard fonts to be installed or otherwise render unpredictably. The png format is a good alternative for these builders. .. rst:directive:option:: theme :type: text **default**: ``light`` A named rich terminal theme to use when rendering the help in either html or svg formats. Supported themes: * light * dark * monokai * dimmed_monokai * night_owlish * red_sands * blue_waves .. rst:directive:option:: console-kwargs :type: text A python import path to a dictionary or callable returning a dictionary containing parameters to pass to the rich console before output rendering. The defaults are those defined by the Typer library. See :class:`rich.console.Console`. .. rst:directive:option:: svg-kwargs :type: text A python import path to a dictionary or callable returning a dictionary containing parameters to pass to the rich console export_svg function. See :meth:`rich.console.Console.export_svg`. .. rst:directive:option:: html-kwargs :type: text A python import path to a dictionary or callable returning a dictionary containing parameters to pass to the rich console export_html function. See :meth:`rich.console.Console.export_html`. .. rst:directive:option:: text-kwargs :type: text A python import path to a dictionary or callable returning a dictionary containing parameters to pass to the rich console export_text function. See :meth:`rich.console.Console.export_text`. sphinx-contrib-typer-8982731/doc/source/reference/index.rst000066400000000000000000000003151515242076300237010ustar00rootroot00000000000000.. include:: ../refs.rst .. _reference: ========= Reference ========= .. automodule:: sphinxcontrib.typer | .. toctree:: :maxdepth: 2 :caption: Contents: directive configuration roles sphinx-contrib-typer-8982731/doc/source/reference/roles.rst000066400000000000000000000012021515242076300237120ustar00rootroot00000000000000.. include:: ../refs.rst .. _directive_roles: ===== Roles ===== The ``typer`` role allows you to cross reference a Typer command or subcommand in your documentation. The syntax is: .. code-block:: rst :typer:`progname-subcommand1-subcomand2` You can also use a string identical to the :prog: setting to make the reference. For example if ``:prog:`` is ``python -m progname.py subcommand1 subcommand2`` this will also work: .. code-block:: rst :typer:`python -m progname.py subcommand1 subcommand2` .. note:: This is only works when you've made sections for your commands using the :rst:dir:`typer:make-sections` option. sphinx-contrib-typer-8982731/doc/source/refs.rst000066400000000000000000000000471515242076300215750ustar00rootroot00000000000000.. _Typer: https://typer.tiangolo.com/ sphinx-contrib-typer-8982731/doc/source/themes.rst000066400000000000000000000030021515242076300221150ustar00rootroot00000000000000.. include:: ./refs.rst Themes ====== You can always create and use custom themes with the :rst:dir:`typer:svg-kwargs`, :rst:dir:`typer:html-kwargs`, and :rst:dir:`typer:console-kwargs` options but there are also predefined named themes available that can be swapped in using the :rst:dir:`typer:theme` option. .. code-block:: rst .. typer:: examples.example:app :theme: dark | Light (default) --------------- .. code-block:: rst :theme: light .. typer:: examples.example:app :theme: light :width: 63 :convert-png: latex | Dark ---- .. code-block:: rst :theme: dark .. typer:: examples.example:app :theme: dark :width: 64 :convert-png: latex | Monokai ------- .. code-block:: rst :theme: monokai .. typer:: examples.example:app :theme: monokai :width: 65 :convert-png: latex | Dimmed Monokai -------------- .. code-block:: rst :theme: dimmed_monokai .. typer:: examples.example:app :theme: dimmed_monokai :width: 66 :convert-png: latex | Night Owlish ------------ .. code-block:: rst :theme: night_owlish .. typer:: examples.example:app :theme: night_owlish :width: 67 :convert-png: latex | Red Sands --------- .. code-block:: rst :theme: red_sands .. typer:: examples.example:app :theme: red_sands :width: 68 :convert-png: latex | Blue Waves ---------- .. code-block:: rst :theme: blue_waves .. typer:: examples.example:app :theme: blue_waves :width: 69 :convert-png: latex sphinx-contrib-typer-8982731/doc/source/typer_doc_ext/000077500000000000000000000000001515242076300227535ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/source/typer_doc_ext/__init__.py000066400000000000000000000000001515242076300250520ustar00rootroot00000000000000sphinx-contrib-typer-8982731/doc/source/typer_doc_ext/web_driver.py000066400000000000000000000022641515242076300254610ustar00rootroot00000000000000from contextlib import contextmanager from sphinxcontrib import typer as sphinxcontrib_typer @contextmanager def typer_get_web_driver(directive): from pathlib import Path import os if not Path('~/.rtd.build').expanduser().is_file(): with sphinxcontrib_typer.typer_get_web_driver(directive) as driver: yield driver return from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # Set up headless browser options options=Options() os.environ['PATH'] += os.pathsep + os.path.expanduser("~/chrome/opt/google/chrome") options.binary_location = os.path.expanduser("~/chrome/opt/google/chrome/google-chrome") options.add_argument("--headless") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920x1080") driver = webdriver.Chrome( service=Service(ChromeDriverManager().install()), options=options ) yield driver driver.quit() sphinx-contrib-typer-8982731/example.html.png000066400000000000000000002443271515242076300211560ustar00rootroot00000000000000PNG  IHDRDU&.6iCCPICC Profilex}KP?KA.C.j[?jѱUni֐Ft&Ipqqx )Hy>-jdԏO&05SMg:ujFDDD(x@46v\ (k$ >HPXٶŌ; ZkDV-116m2Zo„ \@Fݻw8y$Ο?@=ХK?z5k%rxzzm۶E-ZWg)666X~=ԩ}6,X`(1[[[lذ`"lƍضm˖-Ĵie.%>w/^\~]S.Ѻuk#GD.] Pm5P(= ={ҥKp41b;۷FQQɓ'5m{ӧ $ylxx8>3QSO=%zގ?.`~g0qD_Zzm`̙W_}w?Qy W1qDQf͚={Z ln?Ӗ-[eqpwwc=;UV.^h}or@4::oN:Xn5kw^}pww'5j=z ((Ȭ@frr2?x"5BCCѫW/+7؎%AO6x%"""Ҧ>-__o?l-aEd9`o߾}U۾}mJRFӧ:33So; ¤o̘1;wٳYHjNqqq7|SGT7..Τƍ3) ۷>jZvZmTիWx%RW> }%Κ5KVթz?11Q͛M_mF|t &Y^ _p݌3We3g 2䶾{턇|6*4qDmL8ѤGDDDd-8eF'>|ӦMÚ5k{a<طo5jT~nذvµkиq WU*]|RT?~gJ{#oʂ0~x<pqqAdd$"""?mw1 >>>$۾5jrrri&K.ɓM1n8hy޽j666qFCܿr?XdQ!B /޽{_~ﯪIv噵ѱcGݻ?DuRRRm6QMJwQBB~m۶‘#G >/t """"je@T*AFF,{=kҥK ջr& pBɶZj%X,V^-xIiٲhuٳgYp!{9mU9+Wr9;OOOMY۶m1j( &K,,3g+"33֮]+~zQ\.Ǚ3g4ѣGo߾:8{,:wl۷oJL&+wm)))vڶm[} ?z(J%rYFEE|N 裏0m4"<<'NJ%6mڄ^zBUEz)7nڕQ122Rt;ׯl"z۷<󌨾]jZ9?Fjmu\]߿+ x΢Ņzu\م{Σd;662xz'U' +6~>7ر௿ƭ[G y 56 d2[bѸgU;8~苜B@'u֢ccbbD_/~l׮]#r>cAرcػw2r 9sХK޽{nݺW^3g&ӽ{wl۶ .]¸q_,X?X_|~IPrJxzz>r4h/_^Au_~'DBCC1vX̞=G=ZF믿d{BBBƍfU۷&V5lP_ӦM+׏)##||ĉWܰf3...}Aqdꩧ0flذAPoʕ&J%._viGm۶Z-M6 ?ɓ0`]2ٳ'Ο?]~ YbE G%|!5j狦K>FbΝyr ^ZPnM """ݵ=[7`0?[ֻso6bbN ZBjml2@oXz( 6i(oP𗟟)ޙ3k|y$&^Cѱjص]TzmŊV?6~`%ٶmZn5kHN-+o#&&Fg FxD\\\4#?۷//^l0R͛ѵkW@5~zL2E%ԩSxꩧ0rHȔO>,))I7ob)J6l@=1XRzz:FKC||CϞ=q*闥\pAT&ݻ,.._^ZZU3ы{.]2xk GDDHϚ5qO? BNN5W^2ŋO;ÇqE2((ڿ(m:uJT6uT(@SF1J[cqwwǜ9sDrD8s挠gϞ޽{+|_~~~TvVVi̙ k׮kt" z6opa#<+*JͳV[T{&_}>5؞-غ ꖔL Zʕ+1|p͛7O4( J}}75g/+赴ٳg+Ɍ3 m_nvnCXXdJ)gΜAXX+gs۵kWQY-$s_&''~srru\MPWZbT*ӮV~'f͚r-TSƊʂ%S1"\rjJT.['qbE|hg?~\U/QY vertW߶mǚjԨQ׮]}U&:u=z"""}}-(*k޼`[kxȑfߟ5]jZ=}zֶ6le'66?;;ih 89K۹QҋU0/998}Zz%`W׆_1d2[Ѿk"5YU222$G $ YpfzuJػk.9rxtRmh_1p@̞=G%NqX=qCΜ9ctaZdd9((FQ~aRJJJ$hԨd&Mʻ R4}Μ/kcbbLecc!CʕJ%&MBwy֤&0Dz "pj֬lllD 6Fk2}Mۊ!C>Ř1?c֬?19t2u Z1c~;DWc„a̘cڴf|evAA6_? jn]7,\aLumhdfs7Πa6ccE##{쉄߿;wDLL &N(:V*K_u|Gׯ^y}z(n6<ʴgvϞ=:ꚳΝ;l2|gxѤIџyHgSY5Q\662<|(ί G~ W$@ﯿ~ SlA˖$4lnnKJ ߺuLrjP^H!}N]>)*?~ =qV))chqq1kQu_.l2QMj+ׯ,<<~L.cժUzWa AFFVX!9(J;/..&!!EPN{qưO>)Znj]<(ͩGPֽ{w)ʻsU7S;Ǣ6WWWQ)Qwwwؠ~СcY@T9 [fyG۵k'ρУG :V/sGC麆H=Փkk+N-bj @YPgϞ&/5ֱcG>}?FI%jN[SIĮM;wf=wNNNQλvZ6 6`x?~0?DNֺy Z5Qj~eV7 /O7x*_ΞJ=`Xi|cj22ģk22% a6`h=JKO9vtt\@^^:ґR=pZBfi°0#;;BVVEJH#LUVzjvnnnHMMŲe+`Сر#7nghT٨)S[`_a8mƣGS*7Vy鎢CTO`0JeiRsC3r½ H[ۺ sƍ8pѺ;v@>}L]HPnG4smz&55^PiӦ;CߨnݺtԪ=Qκ9Xnjr޽{ L+wJJJ,zI*  %%"'N[na}yyy2RLL RRR$p """"jŒM<[/#>2hnt U+''6 qqW~~#prr+))RTVP/lrߒoFj8q ! ѢK'&&F2hF~z\@c edd୷ޒiiLjj`k٨)/AÇQѺtDyCZTRSޟ}Y K6J%T*d(++KTViʺf̘EUPUlw?or?[jݻw#""˗//駟J5k&n[|RA ƕZ,Nꚭe㥂R$o%$$txTADDDD5C&Ntv̙Qi+Ğ: ^,*?yg#5mmFWtB(pشISzGS~3x/гgR_T@jeM fCeM(|뛺(|6eΝ;q \riiiPT_|a5EU3x۫W68)äIpehe.]*ߏiAu#`YJ-T_5jL bJ/m&CTj8 ^`Qf͚ GۼyłkEEEϱ%FW˗/Kccǎ&}|'v%o{֭[ۃbؽ{ -9kQMg!G.*=R&l%G֯ٳB||{5; Ϝ: z{{'M?H{WХD8:;oRxw&{,#Cx }R(ZlldWUL==-?%ҤmrFIJ~&Mi֭(fzˇ.BOme7Kj1&CJ45]hh`(PԪR||xI5k&y.z-LأGԯ_bA4A-[rqq˱uVcTRA={ 99٬QR8KP{=w$]s5OH4# u۷\L ~fGTp:tӒ{xxTVj:g6666m^uꗔhժz4hh:sEGEW-?+mϙ3GqyyyprD}SNǏ܏&??+gϞnj4hrE_OjZ2B5#DtMq!@iٳgcҥw}Ç-|]Lm!=]8Ʀ\`j5RRRVj'==]42...fEDDD֩;FxT%8}z~&*su5,{{gt0ӧo˔[mC}:4h^?//Ggضm7˗ER+6\\\Y&9rfȕ+W0}tQc+Wۍ΃Wow}'ϭk֬A۶mѧOJ[,ʉT*1|p߿NԩSq ѱ%[RS'jǮ֭CӦM[oӚj5\sJufOu O>>G͛7e7 6uTQ٩S/ɓؿ?F!:V.FTRSN? <պ&NXޮn݊ygBڵk%M0BSU~{g$E?x9sFob}|}}%su.\P:fƌ'Mz Fll,:7/Y[AR oe}IK{Ǝo7#<|c|RG(a48:Qv'k5j'h"ZT)=֮1(*űc!33ޔ^faE/oMj̨Qb a`/??Vgg@&Effl4o>,[LT|r͗>e» GKKG۷o j , `4..N'5O?5?˖-͛km0^9O?Uy/1fלkעN:_{kmͪk-%%E4ZPگ_?QѣGͺ_}+[„ [oUZ{nQYn֗Y[h1cƈ}(ٳggϞf߇kQMU)Q=z1mUtſ c$*oѢ'_bt[^|?9`&]#-eذ/ж@f ,G}T6sω+>ۨQ#ɑ/_hUhh] K R(شid2o~[HQc ;r_[4jΝLK.ɓx'g5WU:u`FI-f :8b-z©S$gJ];&Nc$(GC>*JPĚ5kjM℄† `LѻR|M1|QYTTl"(sssáCμֳgOL2Em """~~66F&œOŬYUڵ$4lƤ 7LKhF^~ ҿ;9cƌice /jaĈo0n*>'3N1LErT_CBFg+z^*}]DEE>[{/,X NjѤٳg_cvر<|wgӝ.,O˩c}}p!ɾܹs&O=nРN8˗cĈzFٳX|`\.Lo35EÆ q L0AoPO>8sՎV-d)RSگHt59헾%˵뻺BTb#t3׮]\D6qwwΝ;W_ޅغusIJƍ%'@{LٶkXV֖Ԃ}Vz?~\o?D}!r9|}}ѥK̛7۶mCllI/j\O= P:B&&&Ϸ_ppd⯾JTVN,Y6l0xǦMp1ik5H9{%?8ad6sHMǨ[a SuBZdg'!7d2;^_ I 11IIM{S4o޽\S ))yO>i/>oIx>>zR ~ױi&ͶB@rrE_^EEEy&_hӦIʁvdd$nܸL;iӦq>Ӻ8;;ͭ{Uyk׮hժ/X-844T0ĉXzWEG׺uWw"mo""B؇ JNX@ ⥐*kR@H}^i2do^u"&Mš5kg@HÙ*(> bbNԩ_7/ p97~╸u .KEDDDDDDDDD*֦W'"x'%(Ål5X!oƍwU&}fϞwDDDDֆ rvvGaxs]r|||,~?ݻou^=9Μ9%K4i>{d}c5`ߤ0fO?G ޤjhL_~NNN+ÊsNܸq1119r$FQ{_عs' 88SLgрh||<̙Svzj3gi&WDU*/^7n#òe4 +::Z: `@ĉz'$$Vvv6mjr[NҼ6ـhjjq%Kjt@4""/_.? Jv9g%""""Z`˖-iK; Jd=z ##~zTs^^^  v,j+66J`0lllG5ۦMGw{DDDDDDT}jŔy"S:uJs;++{B1l0 6LPֱcGV&Mp5A_TT3l߾] 1bxyyi5ytkUKMM՜/j Qh@Eؽ{/ftVoKsppdLbkk7вe*CU " ,XaÆI&JI :99az{yyiVG5/6;ADT!111:tX@Vw7Rgffʕ+hذ!5k[[jÇxjx{{CPյZeiyyyERR֭yu֭UB\|hݺuG1͛A`` ׯoVL^^nݺl4n32$$%%ZBz*KRC۶m5EΙZFBBQ^=ո[EdffjnT_G Sqq1ܹx4h~~~TJ|DEE-ZkRm"""""zʀhII >ڵ ϟ{w lܸQu֡UV&)<<-Ž;$cĈ6m4h`r5o<+m;::&Rk.,ZHoNǾ}bȐ!6mԩS>>u;!^x~aݻ{N+3zh+ٳhZgb߾}%Eǎ?2\%$$^4a:tc+jZ WFDDh)̜9ǏP+W?rrr ǎ4{ٳeگk׮7w\+ŰѣG-ZVL8o񆨎uhɘ?~??ƞ={D4̬Y0tPA/99ݺuMMM>q7o.y֭G(**׻wo|b|X~}~/4x~mi;wuq*7nM:Us̙33glK 6h6Ν;gr;mi_v-_~cѣM~wޭGUJua_XXd{ׯ Poڴ`;kFӤJRXvIQfUVP+Wc~7u 7n1cƨ >?\o[uݻg^׷o_6oATQ/rBP_~ݴYNMs.\(jݻ:zwĉzf͒SOjZ}IgO>c4o߾}e؁j:11QݧOU={Vo;q&""""G_!|r`o8z|۶m8}Q.~~~޽fgΜ1?/_@2d&Ztt4.^(9*kFq@bbbO?a͚m\#FJQQQ8ƞ={BRioݺeu_R_~Q O<qYk5..;wF\\||| k6YZbh!_ 3Ƭ 7uo9F.###%8qB=j(ݻꗥ8qB8M9K.lQBM={482i?#Gu( 5ubbb `} ZbZR [NP?41Ǎ7+{S/\P @} ю&LP9sF޽+x( uQQ}i#DtGn FN>|(j2ً/(eee;wNӧd;F=zT4zTن RMeSAAhd.))ݰaZ.?c{ҥzi>cGرC4477WPk&ٖDDDDDdje@tٲef̘!:gDfgu(O@Çc6o\ɽ,?KDMmݸq^||7_@uJJY.3::`+Wf*xue?~Ā%ݻWPo˖-,yt/vԧN2AAAꄄ2ԀhE?KjZv:ju T*A~s'c6Yj\w>]6ݮi\)ŋ U_,߿`;))IT۷f^z r\]طof{޼y7xL۶m.} {Lm4yd}BPhw-YϒlՂ{`u18][׮]ӷCCCq1tOK, ѣ ާIi@j8T_~Y|3fl9rhnu9D"O R*C֭5ñxbL>@V;;;hٳ' M֡Cr777vZZΥK4믿VѰaCM˗/[Rtt`{̙k;-- Grr `ݺu5Qw$((-[ggg{˖-VEFFjnO<٢~M šCD*Uϧi̙]ccccC[744sb)nu&MW^=vnnnD0w\̝;}O<|Pߒ,##C~33g`ѢEzYE?tGo7nܸBDQ\u~=?ƁLUFD.z)OXfd;v`ǎ8<{챪c׮]߱rJ8p@TGTbҥؿ?Ν;WGŖnlҥѣ{zzZK%î]4 /2ѰaChQFUYnʤ7XJII`[7U%ϙkϧ'8|0࣏>B޽ѻwoߗ糺U_{~UMt.]_~q߿۶m{qYTSOF¨Q˗/ĉصk`[TT>S|gꣻ@B@HHH5FEׯ_Z.뱠}fW^طo%gff}5QbbYYY mK3B\%^{5|HNNFV4ի4U=ڽyGkP*(**Gtknn|Mggg`L6 [nEtt44ϟ?o4R8;;[n?>N<#GIB;hM Prrrڹp `b =ݼUѩځ߂r}>uue֭[knkCwy2 6ƍ5)))4iޅj2󝓓nNʦhH7 "S͚5>IQ*;v,^xGDDDDa@cݺuwVOg*Q^xbvTThpUDfddT^x)AիWUuGiJ`*JRRR %$$pرCs[7SM6k֬P Yڳg.]Z)Ur0"AO?Ym@,֮t^opņ i&DDDDDTy9żpqVd-Y_>|0/ņ5kP~tdff-..Ƃ [}hڴSDJS믒gϞp~i8gÇV*Xdc;*?GPPf^åKVuSy8vd_Z7nqQa 땾x>~~~lzZJݥKJYD+駟믿7V\\wyGPֶm۪3ܼyӦMCppf{رؽ{d]Z#G`т`SeT*ڵ+^*YǎhժΟ?/( l/X*JP1cTh1%ݔ/.T _yΛo#G0~xͶBs='SlРA0`fLJ~(xMZZƏ1cƘԶ.'''y9rd+Bg9ssέn(]O{ r uh#3-ZdG`gg>@9s[l~=zCDDDDDT^PE#V^-դ`OX -ݻw_ݻcǎ@z+VlϘ1Ctez뭷DtϞ=[t\NGCDEEa޼y7o 6mڠe˖d? L:ղD'N͛Zn.]J:{;;;,_?8ґ B>}dgg͛8u5,]tĉf)ڵkQF}ppp{pYQ L1p@ٳrJDDD`ܸqpvvիWeA1bpuܹsM6Qcƌ/l-???̜9˗/l޼7oFhh(֭ hժV\i>}`ܸq Brr26l `b1u/^,=`x'ѴiSʕ+ؿ?J%cVڂl2̚5 @iٳg;Buxqa׉:`ĉhР\M6U[ܰ|rL0AӿÇO>ԩs>tP",,L~ϱzjtza( ?S{AK~3F0sܸq4ogΜA!pe6*MDDDDDVF]ƍ3Sj5kzoNLL7dA=Sr S+<h3gά(eذaF)ׯ 0x BS[o݂|}}+͑7o};qℨx\.7zAݻwlf̙39sLn+99Y 1ƍ۷K/>|(yuN:%ӧd^jjd=J8pu댜yҾfC1cw}=|RKUL~o~&=nWo߾}džݿ~I=S_ׯ*㺭V!""""ڥJk7iqoWWwԖFi{1d) ,_Gs<]N:zᣏ>B ;wVB+;֭[!Cr6S_gm;p@ܸq'O6؞?}*] _}N<))+((?dFʕ+6l裏uV]@3sθz*fϞ-B_ N:> }OV}׾^=c8y$Dr9|z_uκu+W`޼y'O7|#O:mcc+W gܸqUA)vvv8s ('1ydZJZw|JIp}/r|&M.]x7,:/s>Mi3$$cǚԶۆ{"""""F.g2>(**7AP͚56jiIIIլиqc4mڴZjqqqy&V}MBPYfhѢI$xxxcǎGIBBΞ=bmf*%$$͛4iYGZqUdff"(( Ubܽ{7oăP~}4iZz$>[Ҳ\~%%%GPP\\\kDDDDDb@W'"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""(Y Dj0 JDDDDDDDDDVQ""""""""" `@DDDDDDDDDd5%"""""""""aW)T*rss!˫+8rN:2dH5Xu@499wvvvA&MХK899Us/-+33W^l3 JDDDDDDDDjwuRŸwݻK.aʔ)W^/%%k׮W_mQb9Doܸk׊2t(JHKK \dggCVWy5V7BTR= \.GJJ <[n w^?:kq8p p)Y FEEXcO>mB?3111ͅs\\\V """"""""V7eܹsjFQ 7~rrrp-dff@R!%%111}uF^^Xdeeb}$"""""""˪F#&&Fwڸ/d2& гgOAd^@iѹs8>/(**і4h[іesNDDDHWTHLLO?K.z~طodcdeeW^zw}<Ν;g@ȊYU@T{&xyy%QZXX˂2 7F:up}M/++ +Vܹsacc#8ٚ\AA > yyyzڵK mР\]]*1~ԭ[۷74Q~}Ǐk׮z͛P{{{lP*xRRRL~DDDDDDDD貪hʺu_T @1uT899(9aܾ}[ѦMq?`;66+WlO2vv?]ClGGGcݺuf˗/k1exzzj"##oi/R1aT{ZJ н{wѱyyywf]v>|~sssqYDGGQdU*utt4X_wPt1c+9rĔlOK3LU'_93ϙ,yΊ`kkksV'_93_m}ЧO(J֭[cbbpUm}ܹ3ZhaT*-~JIIBH[pqqA:u*V^^j)xsV>׆xsf>K3K^jk|x@sݽ=!:uꤹ]TT7yvXXѶ#""=w涓YßK&M4cbbD]ܸqCЈvi_JiDDDDDDDDTsxxxhnk4 MHH`{͂Q?ٳz MRa())8p@X~'Nwtt8\} }l^Z0zuVͶ \Y]e˖… zW}{.n߾QԎgJojgggҥKJ? p}ȧz޽%KUVdw4 -־}{M?SRR_kT۶m+ ٱc!E 4&jzz:.]rۂ{eؽ{7݋M (**;wDL=DDDDDDDD豺( <2L`TJJ#F]v&۴iSܽ{8h&L`}ŵk4#U*90D˂\zUT+9|pHLL ""B:k׮FT*bbbaOzR 4n۷7SLAhhzDDDDDDDDhQjC/^,[o7X 66J^^^񁽽bccrJ `oob"55nnn}$QHOOGbb"憆 B.Wkl4DDDDDDDDhڵkf6ѣUN@gggf͚Yfk&`^zW^uwj02ODDDDDDDDD։Q""""""""" `@E4U*UvqqqA˖-.4dkk[=""""""""">ٚ999 jnɩ]{ԯ_cǎnY Lu9eрh:u4(%"""""""""R... :::jn3 JDDDDDDDDD5v eLʒͼDDfRոwjh7n\ "GDDDDDf:pn޼)oȐ! nXf>L Vmƀh%7,jlls!t{=c= ;L[@Ⱥw># eŅŚ}n{Ok->dyB7zWZ5{w п*Y )) ЩS'V[_JJJp$''xf_ppp \yyy\\\0iҤ*tɈBZZJf Ǝ [[[1|lYb7}zEEhdB.޹cG* ǥ{p>n'' FdX>a7BaNz5`@ Su*-(hP5wwwk Q\\ll۶ ot[K. RYHR8rMd2tA(UD-l鿫 5޹hP"kn:"J砣RBDZ +0l0 6LPֱcGWSjt|/T*;da`h׮]ѡChk2sLիWWhף:_r 7֞ %YMR\\/رczT ZE XS.ܞxO h/2uJbAD2 t im~$ ̸,r;KT/0nN*7!00P4"e9LU |CEͫG5o[ID.*koYO?-Q_.._TdQ͓~/CTax;<ᩗ 厖QԸqc 8aUY0eo߾zDT6lX=!z4hPM=tS੧9Jj<D-DTӳz"V99HÒ99 7̼<\B.G %_wwtj-Z I^S#>"uՅdv2*@T:; v TG{놪X|"7=2;:Egw'ˌB4xRZlآ^#W4nMѴRRQuPTHKKCRRѸqc4mTrS 22h߾=W_EÆ 9uLyyyHLLDzij5Ґ ///g"Tdj|II bxzz^znj<$$$ww f-P(**Ҽd2QnrDݽ\m4mO=|}}"VрZsáCm7ԧ= nΕַ<C׮;z١gVٷ/c,9p@p0Lg$kh+aPW'BlDpac8[#KOھH'B7z f')ヸ}<Y $ޭO:u`Y;#@oGpzYAYޘ~l-3. 77pPz 9Vqc8 rnx5Dϗ3X>g1;`e xi<4*cΝزed0 4o&!111={6#(ٳ'.\///L:@iCZT%K`z9rϟLɵC˖-ElMqY={:3d27naÆ 4]zwiDϞ=˗/ڵ+x QyJJ ]k׮!-M|͖dDΝѱcG_~Aff&`Ȑ!/h݊(dx,s9=zTP.{"""}qhc=gyƤ˗qQH̲ td? x<+12 s5^^jwquEP(Я_?4nؤv> .A6loߎ8@^$ر#oor>:"Àhd"G}I&Ǎ+*.ơpz$Xt֓ga^|>x 3*('-W4}%KLϟ? ~?s ~iї'N@߾}h"?}7ǤIؼy3?<#22YYY&F-[ݻU*ݻKbĈhӦd|A@Oɺׯ6ۯڵ wСCRSSQ xٯVֳ/#ZF7]U4%;;]&Y… HNNɓW\\;wjT*O?&P^Qי<kSTT8\C׮]-))oTtU*m۶5, Dw`9L?td0TY??3<)|?=d mWaf'z oj^[FȒxvQѾ?8, vtJQ!%%O|=q:t;wI@BBmPPv (… +Pm߾]d񁣣#bccQPP***²e;;;HNNPٲe #j~hԨ,))I챷o0L777xzz YYYwW"++`))w@;*ˡP(%Kk.Q0ApuuEjj`Qn*K/  4|鈏?p$GnΝ;EFA.#66yyyw^>" +G2A)=ܑWT 1wsJv6v_C*Ki&7sQ]c.7|4m\Xcx{22uhN.E pdu#v*ѣz_5:w\ cNJ%`Ŋ1c6l)SD,YD7lGGGW^Tseggիmggg̘1ryineZ];]GC c߻w7nۺu+fΜ)ߢE hBPb *$$ 0 ݻwG۶mEAlݺUERRRY2Wm2tPtA} lڴI}IɀhVV #L"GFF~l_6{s#ʕ+5SLԩƴh=z@ӦMEӱvZMĉѣ}-8NNN2e wݻq…yDD50gOV hMJ8n.{;;  ]G"AG۶ Nܼ)߽Z+ym[,62~9rԤDPqyU?-Z-c1?lZq'3~9FV5?xR.0<r;_={O=^~Ã!6&z.,ԩSFH3{~{[[[L>ݸq( S мysl۶ aaa~diǎlO:U JG9F_8}f;00ÇkҤ ƌUV(}M]vҧKzqttȑ#_h\|DY޽Phݺ5Y=))IÇ-&O,Ij%+J\|Yt 7~rwwѣ/(~Qw_|Qҳ>DS"GQ9zV&IԱɪ$:{7z->`( zhdNzhV&Ì' LϔpoR}  lآ=Eu2b3Ee~[T#ѪO-]&yb#N&ձשsˇcY2!L2 u.Vu8'U=2=ld6w ?DF7QMb}߾}5O.c=`3{IU-R*___Elll +/vСz Fݹsheʪl?jL7v~Zen7WyF%YCkG)'99}}||$:DDթF/*›7AUx{ ;w!e66EDrGG$ Č (HFÇzK<$y[Wє118UP~ߤ>S}adq Cld6x(տJyAm@2EHaNht:ά~LK j' md')$%K(Owψl۶63}|r^hh(~GZn]Waa!㑐8AܖZ j#FлH U?Z-x۵kn۶m5aIIIܖd^j1!Z-XG~ 33Fڋ22Jnݺ^#?VV%777puYPPNN`(/h:uвeKM5S;+$ӧ+$999Bff&J=/4u5Џh5QSyEEŤho@)MSmF&$ӝhq9:'&J|9 %_SV 6Eq鏊}Qnﻁ)F0l' D5cgԢ m` 5 OˋHZYMw} AWe_fU*шQx4}CċzUW6e˖O?5 +l P͢ lHT*bSTEÇcZwm(&Rb,**<} tK7 [& Vq5>|XrQogggDDT(ϝTLeJ,,tqՆ 1>e)1%~ĨUCS{"5ZPWo#MEb :(Hs[#گ\֭WYʱcЫW үU/݀uE親 MM{(LNNN>V=;T"T5AII VZen_,^0QÀ#} C99AIl%{mRӳTV U*غ~ Wz;8ͧsD~Vul.^6QA #G/ǏǬYq篶QNc'Cϧ-Z'0+{k׮hѢ\]]A#Gەt~}hgu놀 I&:ٳG Chh(5jz^k`ʯ74?SQWulm+<[kwzgڧ`og"a_p6:ZT^5][|T"ԯ,L, G<*j[;Yq/]دƶȊrm(~ىGdƋGA6(2_ Mis}QUhd~ޭEo[ΞOY PD'zlڴI0zo޽0`d  /䦽N\\w^c# \.׼k_`^z%xxxHֵAZJ%6lo;;;A Ah@QCAiOOOL>uH**Ojl9GѣFD]őwX5ӧY D>ͅU"htUj:FWDb-^xko҆X-Kh"@^D0 ~?9/Oޭz>gU_Uw)} Ei lll$"AP{)w!񗛻cy:4NI8 `3EbN5~hBs{߾}&=uШ-[SO F.]TritJ=U?ӭ[/={h[ڵVT8rI}0t4M&%%!11Ѥt뤂ęC;h\6H?蝶} YiQy^g#/Je)44Ts;==]3JR̙3F""8B:şL{:zP+9:9_?<rJ1}{|YP_T%-oq?uHT֭eK4.x^h]'T*$^"{1ʮZTӭD EEf ZHy66q.ѪOK4 j :ǷOs[ػOnCe;[SLXXxbkmF~2e ڴi#gLf<}]̟?c̘1}&ٳg1zhT*AF7JQիW/\՚n:L8Q0Jرc&?E6mp><лwo󛔔ӧOի9s(8c)QhD֭訩S4MBϑWm?qrY0zw޽hذH; ˗/}ףo߾(=eA =11Q0z:u~Vμuڵ /3(??6m2iTrnp t5k`„ @uII 6nhR*2j`^w8tjmMBDDk'۶7!C"~J%f>ӭ+~>|Dt̆ӧi2>8iR&pp`{\ h@.8Lz(Ѣn>v`LO-n36663i*%iF HιsG6m4A1c__j۶ms]vHII3gmӧ믿FJJ?3~gtFGV6777sKi'N;UQ T%K 00&m3K0Rʞ]1BzŅ pYM0T.ӦMLիWq ?~U ޽{ Fƾ}pyMh=777Qh>}7n@qq1гgr9$$: "==]gu޽{ڈEtt4-.X]8u:k׮JQѣeeeѣ8p"""4PF52yyyHOOל=z\ M Sp&" ӂl%V}*3c͚a0S{{mN' ɰ0Gwmw/drS._ǣFaTNCSObоRsT|R&?›Ff'^g'zm3\*RE)TN鈹g.)RluηϽNcy ?hKaYh AZ۔xᇑxW.k ^EVa*%h*kUΝqU̞=[0ȥ?W+ӷo_DDD੧27ӧ[oK.`̛7Fݺu5徾&k !ƞ+}k(2eAL}@OOOL6 ...2ϊ2 4YгgOU&!00F{C}ӧD.-[ī*苡5jx L"X׊ ƌWrS|'0vXQ1Ҏq<d@7U...:u*u릷-;;;1C<3777̜9SZ#t_|Qp 0\FBӦM5d24nO?4z)~둮\Rb$$$ %%NNNhڴiWArr2222PRR777˫Z999HIIAzz:lmm OOrrrrLԯ_M4)׊T3j#11yyypssCÆ C5HOOGJJ ???|888| ЬY ߏ1EEE4#Ie2.\X} m׮]x"3Yg+"wtDƍN//*6bhǎ&H4JC|S؊O%%'*{'{418jJx:MF?8!ac;·x2%>/شTDBBBbve.\ݲeKÇ&cgg???zhVK"rL <<J%rssaggBBVZYt7"cрhaΰ{k憆ܪDTKlhEKPBTQ^^^# eŅŚ}n{Ok->dyB7zWZ(P 2;Hg|z/X>jiiipvv :p:uB```W\Arr24y$9}73~?9ޤ>̖Rkڵkf"88XP'N=&!!y`߾}شiDEEiM65j&LHW(ڵf߿mFEE&MT[@4>>6l@nn~OOZJܧ/ Y'Zݻwŋz디HDuV/"..999,͛w&H1tPMtR>"^dcFǡTHBR~xwԑ'Q}nMk4X'^e MxR`!hWE=vA:u^|EQ@ AAAׯW+V`waQ\k_`V JQQQP+h1(KlI4EML31zc)5$,(*"Mwel{gʙgus3w.222$"66˱fUp++BvAAc =yyyP*z0ݻq-@nеk2Iʕ+z}VU`WܢNR!99ɸtoKKKiFXa@ fϿ* UZ~i_>\y4=Iƍ9P){Vգ^K i1bF!־}{\|ZT]|Yg0TSxx8 4(疑]tA6m lٳE׮]Ν;\Ǐ;&,r 2Bw X[[K[^YUcPM* H"€e"_߄n]j rayS=0?נP'=Mb@` nQ{Xk -z-<`iUƺG!7#WXOM ˷D^f6L݊Wͪ=tRvԩ[S;cƌ?7n\r:BCC;v,Μ9D0www 0[CT D9@ƍWb.ggg~BÇ8rn߾-۳ġDTmԌbնƜ%J\՞L&ʭĠ6mлEzx9w(;zT0D鉔|7Ek[ZXC/Dr[s6Jj]_- ,eHz Cu샰8$MkC!I1hРnFYfaܹhڴhL&C׮]qYtaqyDFFEd2QISW^%⥤yxxTRK&kkkxxxO>ZQ&N;vU*_ݻWts0 jFBm *%bEEEHL$gfNvvpCR>...p1aBVZU+V $066T$"2ʏTcUǏ R#wZw:Nֶlq#ǯ_;wtGУy3 |8t5 _>\R2z7#F`?!Q-_%:jՙڳiB+ "}]rǾ:%6gP]+z Ѿ>s{ߛO 'd 䳣uV̽^e[KG Y]z 7N(׶Ʀ!XE xrرCLǎ1d;F !ƫ|֭ӧ(uw\;Je ~rb'ODhhhHL&CӦMr… p5F,YZZ#Fhv8 ڦp^u\.]S]RR_ׯѣGZ---Ν;}zFjj*`ذay!!!WWWL:UoeQeii_]g7N:%ヱcǖѥ ۶m~ÈʕڡC -[LKKK̝;\Ӏ!&&  Q8q/^ È#g!x٫WROgggkkk!hbbb!"27D %; j_|t*/ћԜuN6X._kq*??uYw4~8~B^Czv?~V"jl%lm }e>>]i%/?Tn:o;'?12 I8?ýl'aieiJVd~&xO@|x.m/{W;\־a}Hu-J>NKLCCCGb۶mz?̙3ׯ-Z$J4 6TJ۷oGT'R7n --٨cHWT{.V\#G읛# Iڟ)YvƍHOOWҮ$߿wu>|(Sx%/**YJ{s7$33S(iii¹~SwE$&&_YRľ} &Jxxg Ln.)))z' *߳,rrr~ze5k`ҥβضmϠkFF .$a*JyDTU0 JB ً=s߀FPuԒe[5eΞ{7]~d bFN|x *1\xxN~}=PuV1K;TJnWn"ck~~~ARRO3gЦMܹsvv~K.B@ >?RpAsΕԒaϞ=@%<==akk#77qqqFՕoFd򂃃>|(RTرc驝mKHH=֒C NNNpssp]k׮!--Mo)(u@=*ˡP(&|}DGGWkC=<q]711Q4nԨ&"* D@V9%4M. ]]w9)=._D4 5GXwZ;H;@UCq&/c\AN{P!/:I/]#dUt_K3|:x^NHMEԉZuO:nӃD<=mlt-_֪ӥ35Ç᫯^{5L0A+_baa!V^Yf(^yfL6MZd[۷ѫW*._sOOOOǵkׄu{{{̚5 ryqn墢"ܹSg;MNC1x`pwb˖-B]vaZ#o4i&M^ZڶmpB@nвeKQnn.v%f߿j3qMy\dhӦ[ gϞ R bڴi7m6aرc ,b߿5kӦM+ס4iݻwe 3gΠ{ZtôiDC8/ݗ.S U4D`R+}ZQzvt6 T Me-ahv% >ٽGLd( z9&f-ѷeK|0b8,-SF 7om٪#;u*sT=44T*la?}Oa/n{~O7@?rAzϣ1`aؿ0DTfЇcVW6 ˊyLnӃ@ʵ'8_m1o=n(y.>g߲4]/ٓxDw9e… xb[YYa̙8~ct˖-ZǏO>] ƍcرϢ֯_/{պw\Yӧ P٨Qh>MMMş)*נA? {hٲeYNŠ^zID/5j.]* zj dO?( ͛7?n( y/VOZ 4HȗWjfMagg }?`}sVoSjM4x`NjzVBB(!C2bXXXR'cReC't- @' aNݸUFL+KKﻣ$gpsޣӰU-+nUׯ,M3x0sLz:t@[Y1eaj%P^%9ɑ%4>\gYQ;w>>>cTXZZ驞V3i*_>֭+YM6К>1'OOO59~===%:0Kּ~?DVZ,۲eK!7.III²𷗚LH=LT"==jOʔgޔ*Y׮]釕Ig;?ͅ~ylS< ZV-4mTjF -swF^dff"-- ---I3HC6)**֭[E <بDDJDd$! z:")zf4M! z#.GT|Iej̞/,PI _&] ÊCZۇo{I''oeL!C"d~>Ho@Ա^iiwk]ayPTT _.PB>=j5#?fUJV^mK7d ߈pp<7{5Er{֭[oŋ*/5M6{/%;;Cx=p5kV|_ͼlllD )QJ% /6FEX8qF~/ٺ[[[Q0\BG~~4{hf@e霒RmEEE~:N8!SI}(@d?ٳgӾ}{tADDJDt<|i?YQ7m Gvim(*L1%z]0HDw#LCn` Kul~v_׵hBl|q~fOu?z*s=3 x .T;V7tڵ4hP%j xSvmQ*MRtcUDѣFUs&;;RQI}~~> kdB/&|?i^RE=zuQMM6M1 O!PG;;mmaia!9a:+gyK|Njx!Wbb0vO9旡ґ ZXJT)kVU ݫs <Q9iz{ZҾ%5*Taphh fYʈÐ!z ¨QWWWGӧ1guhW]E(GRa颡ދ/ɓ+QUNC4i=US3e_~]+&MN:ɓ'q֭rmU xC5]ϯlT\Ce2ڵkjS6mڤ34`4矢~1vX:5U*eeUj%4MZ&CN8`S|\}[k{mkk|< ]4s+TEztvFҘqv0}na_'F?r`%,$6\|aSc6[zM^u<'ZڟM>:$x[n1p@ɲJQ'66Voهro`yoWbz^jaa\.gUjz@D&ᥗ^d54J TYkaj*8_u!fEk/|>f4g/'ul7Vߔϛ.srm%B,{)xroʙ':U5'E*LX=Z+mTYdȓ7O?׍j|rTk,|p<*\'O  ݨQ#a9""*Jg *|+W /"Roœ_>4OR4{nݺB@C_]qqqz~tC陰5ժUK{nG{1UTZ-+++@3V}Ai{ʎ3F=w$f}\ ׯ_Ǿ}:u`ڴiޫDDUMd:uJL +65v`;r*&vnRٳLgkg Yv Q3-.QnyC/k_]{ZYkw,$K1KUf  =hl?7 9#V ȜqrgEBB*oBȣN}HgFFN:޽{kSTذaQ3 |>zhe \޽[4)SkVFz]EFFddfm}֭+LV2w4z%''Tϧ/f{Tz$Y@1-?!^zLV^%ApJ7o 뚽g֭lxh=/_ݻK 3~R[naǎºO^%>dS??[SSC7zǯ{b%gfb3e o^k[J?-~Q7vvvx`޽{1k,|wB… =zVV۶mӅ ᇱ><;^mr;,,,0g_L>9r$sŇ~-Z?Ə_~Ytz̖-[bܹhժpym{k̙ꫯT㧟~O? eZ^+:l0ٳZUW4믿?+Mڐg}7ngϞ˗ gggXXXÇCͥM68s,,[ ;v=z(ӹsg_Ν;q#99n*s 9rJcf̘! j ıcհ{n9r...Bcǎ=<2Qy*5='y} R<9Z3˫[8b8{oR}F hQeml0uDt:3c:bxddPM^c/Aby\[E&_~B-wFFΟ?sܹsZCx;w51e,X@  /^D@8`6olU]Ս9Rt?eeeŋp F߳nj3Dl\v gΜӧqƍ O?-T*_!$$B{o>'''roƟ(J١Gns۶m1iVV:u駟֪}6n߾mmusωfEXXΝ;(QM6ҥKe4\>=zh24:u FXX 4Ws޽FFrrpww24"'yĬRیաQ#yuu% XZZb/abn:M/dtM.Ɨ_g >>ܸ1^g֭%{:=,˵P{a2CsVxSXXZ+YzM7Ͽ5RE-ڪVuz=枞nӂ!R4ww9::M 0~,ƭ%Qt{ZǼs01+4Gi1n(kw:zg/eiό@=T^=:wk׮W_ /!s>ڵkuQׯзo_?o6큁7oN8ڵk ۽,J;{1']e%AL]@777̘16+Jxxx`Μ9ѣkkii kV}mz3gD.M6k&j9BiD֘zXXX`:t($ocٳ'&L???J1Hg4A*ӧk׮:d9r$^ië5oϟgNNN={(?:[[[aԩ}޽{#88 67} C>:(2{ŊSܷ~[0z2rs!<|e!+YNLĄW)ys:Dȇx K+ 7S׵L'U,<wS`emzpuy/k j>prrBz8)HJJBzz:ckgI&QFe~C_꽅---{zۿ?.]*kbZ#55JEcc-޾=z[PiOYNJm=bnppG>#|'WkIvoRyUvvvh۶-ڶmkz%.^(,7mԬGp/((0LVdoRs5䘪/ rx\\\eB4S>|ARÔ%J\s'{dh/HJO׵fkkߨﶞpq5cRpnyty\ˣGsLLh_aT>|}}up""LJJ8qBX(O2 ~~~ĪDD($_Dht4B73.xDTsć' ><`Nڣ^Gyg3f`ذa􄅅 qi B@;[[3t;wf$** ۷o:v(J;ܸqC(3TXfLWk1 JbfSJDO.7{ x׸ej0̞=[燨(r˗/IHzJҚ[;F +KKԶE^Sۆ hbD5Z:qA^f}Ba+=94~~~Xvޙ4Kk۶-XaC*#Lr[[|]ħJ]ĥ 5+)Yp3]] ֭Qаs,~W㐑lepv¯.+DU_Ӹu␖MEѣ|gΜtڕQ ;wĉ' 6d@x2ODDDVZZ^yn6lC ##/_ŋSO!88Zۺu+Zh-Z`ƌ""@7oh߾=Jee7H/%""jSLݻyyy VDj"<<xO""zeff k2D06/3-;;<UVڵŋѻwod2!../_. 7nX[[Wrk bԩP.)KD0)PI!""*?k֬A< xzz 絆S &Tv3^zUv*(޿G;;֭ ud!*!EEEU(dl)>5?Sve7jd\|YXEP)AAA 2T*"""8::\P<[ мysFEtt44iU`Rlܿ ]6ݡP(*o+W B/22hժL.7nƦLmAA ׬N:&ץRXdeenݺP(puuIu"!!pqqlllLnS{ݻhٲ%K]O|֏W C`` Ν;7{Nrrxpqlܸ =sp1[Sj4ڟnt< ڗ`\s[%0J¸q}v=GbhРA_W,J]sa ^ D%޷oy{(oM'N`ժUz{x~xWKXXиqc BCCѩS'u%%%oAnn.~'2'Np߿CΝuMHHСC%pǹsm1ƍә#66_~%>;w Mf=+ɰm6;VݻqUEL^^^~e/Z?2RĒ%K,s9t K,anXb0YѨQpU/_&O, ?c}6>0b7n @bb*f/ܾx)?_k܁MngYdea%V<ټyjU.v튺u뚥>}r9w\SNK-¡C$ su <3rcK.q/_FVVn_%,?SFg, 4HƍKdb/^* {2eT ^KR~GQ0T.cȑAFF*s"( ?Oz?@dc={ĦMٳG2 vZѵ۶mSN ͛7CTb۶mmzV0tذaB۷oҥKF=rAAAڵ+r9bbb^ෟѸqcdggIII ŋ%K*  ݗ };v IIIx뭷j˵k״`{dx C޽ir*5S%CХKԩSB8::?3,X Y[o% rBM6uTlذAmhܹ֬'N`ԩSfo&o1g]VT.##º[msttDnDԃxgEӤ${p)Lz"€h%z<>1B-5;=&YjYYVbV/`LԴ24DDT.Э[7\xx87nz CE```_cشi0\3++ 'Nf?|0nܸ!9,Ps>@Xaaaaػw/FfPիF 3{%:uJX78Sm|0GZM}E"|xwlقӧcҥgϢG&E=#G@.c̒O{EPP233Q~}!ȢP(F!ߏ^TGJJhn* a…/ǝ5k~i6Jr9o4^xY\NNN:ɓ`EnªU6r9̙ӧaÆZO<3u\|96mڤU.$$D 1b6o [[[{Nk(.s^SNEݸq#&N,me˖>[ʋw}-[sya1|a\RXɓ'* `y9{(گ_?^>>>¶L}p _r?mھ}`mmmt}DDZ`ڷǹ?C //1im[y0yWW64 ̘Co.b_5O`!m%Ev% "'f@'## .D֭1c`֭F ֤P(ork.vֱqqqX|>ydv ???a d2 0@gJϷ%ڴicUΝ;`(XXX`kkX9)J;wdcƌӧ 4;(ݻЃ PH+**駟r3QQF4!IIIì#GDYZZbʕ>{zaaB`` ' o.9##˖-8q"n޼ ^YnoڵN""ƀնƂ!aei Z0?2I? U*Ur˟wllli(cbxlkZbHğQ7+mDDT3Ȕ 6$%%a7n1yd34h@&..ND=/kd2zxx8޽+d,\ȑ#ӧŋ:4E~$'z+Y'۪JD$ܥ%mmmE,I1J(G=xeɇmҼMƣGڝ %ɌH׫fNŸ8y͙3Grxf̘aOg^M3EsIs]3)^([RQF܌xl.c֭e2 m;[=1-DDD5ϐ!C}Π;w5dZ7&oooǮ5 މ`_]XEFkH޾}-&&i*g>T@d2=z聁jMR4'5R'rppٌ hlڴIi ƨk׮i͚7o.x|2VX3g*ߥz`8矣~&ץ xヒ˗/ܹs?%0h ܽ{W4^TOJ\24t=Iݻ',wAXNJJB\\ׯ"߿_[n =aLՉDʡ?N2!3੧B۶mѩS' Fy 0jֵOFCqm1cDCΝs_~ٳ'ڵk]Q:-{\.oׯѱcG<ر#7겳a|zgVVecŁC+Ν3i\y05$ (N3M฽)o%CKL2Ť߿___}_ /h۹sI戈Hk?alPͼ?RH5Ճ2a,kLGFDDT2 :uo={ ++K (QduŐ;wˆzY[[.增%b]3رSSSEΝ; *uE;wVˌ رsA^{‚.%4ꓘhS ˚Si gO=z>}Y899au}vXt)N: ]ѬY3̚5| \]]֭6Uvvv7\L@> ]cooR"##t|nn'NXhNGDDU5QH ќ(!88{FF@UHHhs+((5gERvmmJz& Ő!CpqSNǬYDnRsbؿ?v܉5kH+WСCᅪXU߳@qEcrF5ᅬ}Mn޽ػw/BCCE5=3UvZtׯ~GɼEEE6l(ӧQFpwwʙZJS6 )yW\ݻ=Fdkfv3t@njFܼyS3hx7JZVVzC}R"wGGj|(M̭M+KK45aRO=)kie+PrڹQ@׊w xfrJUnKr'Q*,ÐK:t0rHN+ wޥ? baa`#++ W^ř3g~樨(,^\9R 0EPP?#<<ϟǡC{nQ_~.\;A/xbܹs.]ѣGuVQPr̙2dVċ/#.>32OaFF 9|8:::9rD^z`?(,M4~|>|X?1((T/-?׮lzŠ+(QΪNsȻT |}}a6Q$}V9۶mf@tǎ:ܹSo]x={,_QyJ(Ƣz;sYݲe ~aoիE'"<<,g*[ɄS0nhh(nݪ/!!$"?Dk ~IQ:>`V j9~=sVoѳQQ?WqC2%n==9|GAMX~>QQyԩh}ڵ:~²BO ,PïK,zA骫<s}F3_lyd(otP;//ԯyʒQ. ]Xq|7z_C ֿ{^>\eر' NU,f6x`ayݺuHKK,g7N:Q)J<%"##1~xa}Ȑ!x?(hm *>Ԟp-.%/W~60'~zzkߨEwNjg 6{}=>ǯ]_mՆ &M`ʔ):sݿǏٳgYf!K.՚TCRa޼y|- HMR'Էo4L4{.ZHh֬/_3GDC{7fU_׵ųoڴ ]t*Y?>nܸ!_Tmkٲd>ƍéSt>oECMhѢ.9Sdd(ЦOI^bx{>}DHRR3f 00PX0aEEE8y$Fsk&Z0ar44ooUd`ggNX?w&Odl24jHqvv6{9a]P_~>cm6axx8fϞm1\.R "2V!Z4vwNߡZ=~{ezVNkG&?Gm&DOVu7zTQi[֭Zn-Z@Tػwl@@ϟ_mzwqFa}ܸqؾ};ڶm JG&Q(4id]RC  ,OTG?;vK,ڵkѾ}{, ,YoKUsHT0SNvAA0o<̛7C A-дiSXZZƍXjw_4hO?>!-[ѣѢE 8;;#!!7ncDŽmժd}wѣGW_[nh߾=cբf9Cvjj0d}􁟟<==#ڵ NJ/wB!NL|I 0h ySg>-[`Z.$""X!8I.c֭:.-[`ܸq¶ݻwkͬ ߱cV~~~ WrhX E?ptbS1%V\)z$ʍ?B%?;vX߿_oe٢ɂ `kk+zh}2{9M;vIJeˌ+::ZD5rLYugm߾]ՃرSNW,`]=z0uT!y !V㚙ʕ+'\pͭ!;wܹs^_Lcƍ|^zI]`?.\)SCZ= BCCcoM6 eddVA|D*2&*%3`+/KI=z Uԩsfcf񁵎M2+stXtnXrߤ=Yp0Q,""~_|h׮rrK,A||l 2T֮رcwrҥKzR4vذaZP!G)AAAuz-Ouͬ Ĺ"zmch-ptt'|nݺ=]vطoV\Y5yk^}еd7oѻwom֭ ;6l: [:u :cZ3kz7͛,3p@={VPvaΝx뭷DukkƦMbv///8~8Zn-:UbB BDD^|Ee)~Ldkdg3BBB0k,z±cD=i5͖ggg]| Q>ޕD"""(DGGr9V\I&URHh~~Rʵ1TY[Tjsoy8Wb ӧ"!!_>ÇӳJDDKOO3335aggWQuS_M_ """"rгgONDDDT˩zrR%"""""""""1 Dkժ%,dL9JDDDDDDDDDUloò(U5²o8dj D`@j D`@j D`@j D`@j D`@j D0RY!"""""""""2zBoY:ueh L)...rTTTEDDDDDDDDDd^w쬷|QDDDDDDDDDDwJ8::N12C6oʂR/kkkԮ][ 233`acc++2ו.s]5, {tf539hXZU' 񚙮]"`}EEEE ݻw֭"DDDDDDDDDDUƎ+$^QQHLLĶmېlL&C3a0! Z"?? 333 &-5VRR YJIIjժU溲QTT{{2ץT*_3R<\3' 񚙎tf '''UUϓxLkf:s_3s~Ua:^3Μ,;;j2תz7LkfxE(Quepy"""""""""'DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5DDDDDDDDDTc0 JDDDDDDDDD5PըT*dff?)q!@Z0~JnUUTp]>kkkԯ_;wFf pΝnUƍ'???111Aƍ1q n!S c'ɠT*E4׉Ѽ$&&z3z֭[bccf 0]tY6%%Eꦤ $$DS*ػw/ЫW/EGGcƍZhx1on߹ׯGLLrHLLD˖-bTDDDDDDDDTL]ͥ +S}aaaZPGGGC} )ЪU+ԩSGؖR:u 00PTOOOadȻ\~ݨlժn8|0- qZ)N>LW^y666 ƍnYWnЯ_?C}/|||/d^M4AѰaC}X~}УGa=..W=*ŋ²3fΜ [[[QRW?dpQ=D5s_,W`8p( nnn6mh۠Ah$ooo؈=>$j֬ܤIaRikk 777au;;;,X?d0(9zha]TΝ;zϳĄ `(XXX_~޻qqqZeffիz-DP_>ƍgT;T*{ ۷ǜ9s|QŪQQfƍ²rͪy05srrBÆ ѪU+QJH} b֭yRCK\饮KRzR iOII 9AO͛Q}]WQJ[Waa!~>N{:u F[Ke___|+WuDEE!**JFi@Gʕ+딖&%Y999%  n߾۷o5lP2><fҢE t:r2$kѢf͚ooodc͚59 Æ èQѥK̘1C+'' IR>ښu0{lx{{Kkkk L:Ut tI}޾}{ UYZZ_~ɴt< ʭSFQF-GDDDDDDDDUETL5+V@ZZ߆m42Dڵwww]GFF㑚 ;;;xxx3$>>>Aʒ{!??ׇG}NNp999gwvv~DDDDDDDDOҥKG;,[cKqrrSU*@hn...{PV&GGGjժԮ] 40C*C扈fa@j D`@j |aYRkcL.,gff-k0 ',Ga955UoY'""""""""`@VZ²L&+AX[`@VXf@WWWaYoY'""""""""Q"""""""""1ON+EDT 4_|Q.j*ܸq0rHٳ\^2a޼yP* ӳ[U5T}VN>UV6mEUr={`۶mΝ;_QSSp67Q"M6 egٲeܾ}[jΝ8qHÆ ȀJŠ+'2 ?uU[n ٮ];Dt +999 ɪ|@c(s=%2Vݑظq#ڶm[-""""""Q""*wΝ*%DDDDDDT1 JTCڵ yyy/_. Im3d…:u*C [lAaa!DDDDDDDd~ }չB@TP`РA,իP!Ǝ[M """"""zUJµXȬIgQF-k׆u钞dde\oWWԶ6KDUU^^^ 4o666J$"++ uօB+,,,*mTPP8>yyyhܸ1իg6fggݻHLLDݺuѠA8885LT*qmu *;>KWPXn8|rV^=ѾQ#mYvw m[\|ػw/4>ý0n8|Ǩ]Qm2|رcbcc2e }]XZZ<֭[8x كSNiر#f̘a{ҥK=z4W^E||<>s|Z'M>F_VYYYpA)t sϯ_7|gϞ՚.(( ,% ǐ!C}􁋋 ,Y"jÁ`/End1eM^^^ػw/|||0d?^8.886mBZD…  Ν;u^^^h߾=>Cڵ~EDGGcҥ ]@lܸZY_cǎa…ի<Ȑj-P*uvb"&#??7-6omj5 U*\+b0^gnbZ2ssESSٞH1y[8#Qzl۶Mݻwի s)==:?xSNz$%%oAnn.~'.=t+-04n&L0҈CăZA&puu5~sg7nd bcc_عsgtĉ}v}Xp!°n: nj˗/| 8q'Ns_a!FGGgϞ#00s[9;w/{ܹs8w>s;e~WL4IgϟLj#o`Z,++KhT[Ν;"::III}o&ѿa=:j(QX 6 Boc4m}h{jj*tJꍍ޽{/`ʔ)rssEx >\@;wND?ॗ^ڞʕ+zSZDOHfPWR_7[o޾̯) nNNF}e~]$**JRyyyK.ũS矱`8::[nm3+//m B׮]!pA#:BQo(c]zU+:l0n߾K. [6mvV/5???t2 /^Kaaaݻ7"##K5ܺÅ{^^^۷/p!nj.=1}],ZV{<3P\.F͵ʝ={+W vJ_/,ڦ*DYe4{+^SNqƍ8q"Ӳeͥ/#%% ׯ;$wޘ3guedd75kt) K裏D1/_7|siі,Ykزe fΜ)YJ… EBBB3wA߾} +Vy驳}{EPP233Q~}!PP(#FSO=ؿ?{ɺ,Yz п=zTv!##CwAZuM4 /:wݻEaƌZ 86mVdeeaĉؽ{7øqЦDQQ(x+q ۶n݊ql!: ɨQxYV>mcZ#uÄVGBÇ*Qnҿn*Uahvhk 1b+اg#Qu5bH5,MJJBbb"ׯ_mQ>+ &M૯*׶ .'N޽;w^Q*We' "mٲEr??/dұcGO3~wQ;VXoF'OLK.cʕؿ~{gR; cvpq(J37|#o ƍC~Dׯ_/ ٱcDh}9#Psk׮B/޽{ NEEE%%\.r},/ :;;#--MѠA^[P~:L_. Si7maj_7<&1JJKڌVpã t щIHLOtdM$5tͨz6mHn]I???矗{Ԋּys˗b ̜9B]VgW\i&$P^=aիWI3aÆaÆ {Vg e@W厍}ѶTq45;&LM:z Bcr\xcر#~itAAA*1cƈKϝ;sE~гgOk]vc%|reff1%3$?iE7<_sR8]=DՃRfffjգRŋP1JF_.Ւ[0 JDDDDeR-1D9RC NOFXRW&gggɡġXt)N:رcB֭>Gᅬg}NNN>|8n2rURL=didggxSxyy靌H3X2K.E6m5Uy}h),,z˫oK5jmmszP>ǫ׫O3T*1|p <ؤyyy˔e6Wӓӟ*^!*5I\$9|p]23TM9L#"Þy`ժUXv0P?#f̘Q ,}ŏ?uIٻw/݋PtСbXEh,,r$:ԀܿZON z㏥K=k.ö BkѣGuﱊ>)ՁtQ}1Μ9cRݡغu?4%"""e@>޵4'?A% 'kB 1ocJ9ɓB78q|Q:uJoyRd|dt?~,}5ŕGs"]=@J%}݊h_~rll,r[n&Ds1x?_/pW mf^m"1kX,ܹnxġXDT|7nN:3G~+ tܹg6uJ%{=Ѷ-[VDD5j$,_Qf̘@a}„ 8pd٢"?P-XСCrѵkW0a(ԩSʤbĈmNY_EcZZƏ_!):tIz矘={vLrR%uqz^>߭h}|.8%485v"XgΜ; [npBd*°H___~~~ģGk.QO2___tUvZ\rEMsŋkMEݽ{GW_}nݺ}󃣣#߿իW*Κ5K4tE\kkk|G6m{[n=z4{OG A C׬Y0L8vvapR ?66;wѣ先[EDDDDOj燨$际t޳"Q:aI#"7-*ș5k٥N"qAXZVL{b޽zHpZQuΝùstر#-[f|Ch1bΟ?/_ٱcr@=pL:U!| 8q_X^^^"᧟~<^ݻ!))Ip <8q" 1uTa[i&M͛7d5kH0`ϫ觟~Bͅ^ú>S d)6ydܸq_}o޽{XU~ȼD?L/ ,-Yp0u }?Y,1\4^ 2ګ iYV̡9%Տ3T{NWY\<688'Oʙ7Ddd(eyc- 8BoN}^v܉ 6`ذa] 4xB&`Roozce'O'N輿e2:s8\.ɓqUx{{KP L2~裏0e2 _|8O?%_?L٧R~'|]v ˚ukXl~Q;vL՘S]WUg""""*E45Xiii~0cfe((,D'|օ JDDD ::YYY¾QFaȑ:gl۶ йsg """"vm@tDރ\e%iӦ!==,[ >>>۷oc֭??r Tܹ'N4l T*XBX8q"SQYUq!<<\rT 999 ɪe@4='؉""öoߎ2;Q"cu)))7m۶ܢAT Eaa U#Ν*%˞={D> #GDݺu@]e5Z[IZ;;cZ^hXM&Wd]v!//Or˅!pm,\SNtС_"XYYa˖-(,,4mڴ[DUz0_~xw+5DDDDDDOjֶuómZWBk}wA! P(0hРjQzUMcǎ&P-,iӦBDDDDDтRHQT*yIiihT.<: !5w>`iiY<\z666h޼9lll*=* IIIEVV֭ BWW'*cAA uԩ즕 sgyyyDff&l斖NjjPuQFJ5 %%n݂L&CQv2ϜJ%ܹ>>>J/`k+W B/22hժQS*Փ<11QQQhѢ\\\j[vv6Lf͚AR!""/Uyyyvd2ZhQH]f9֥f <][6`z?Flrh%cN~kT/ݹDT}^..٩#ubT]Dٶm۰vZ>|X}ĈXf@Sxx8&Osy0fe˖fpWP(0fL2B'ٲe6 ЬY3|2-Z{J ȑ#1c xxx^S|GذaCEݺ:tm۶e 779݋0>///7q @n0g >\o,,<<C ...Xd @DDE/End1eM^^^ػw/|||0d?^8.886mBZD…  Ν;%'B\LLLD׮]/"::K.EHHbƍhժJ;v .\.G^DDDDDThRz2JDKӥgN;۶JLB QW^* =maq#_ '{{CTm۶Mݻwի s)==:?xSNz$%%oAnn.~o{UU #u!Pi^* %/]HMK0Q /i͌.Qy)͕yKqWFMEqҨL6P\}:.\h2H^{M>׿yftĝ^zz:|M}t,XgV->+//ڠ֖t :yyy;#` %,믿*#F@Ϟ=.\@ff@-=䓊<CBBTeAAAfsv)(?\Fxx8rss}5wE9rD2'U۳g"`zں_|j]/ЧOTVVBp2d2xI6#DgΝ; ز=z49"YСC۶mCTT_CZUC5J2ʱ^ӌS̵Z-Ȏ;˗}s$[bڴi6m8OҥKPVC3fH׭[ &HڸyfpZd ͛נ63` 0@r,""BH?6`ܹc2dE 8P/[ 3g΄KJ?#I[***0zhHNNFddEmJEmŘ5k" uVL#a׮]kC[nXt)ϟmj ?ct`m6Ė!ٞ={$A[bĉƎ˗ ~!~7[FJIIL޸q$^rlɒ% % Ǎ̶m;v@@@p,..52Z+V޺u+}]ѩSi> pQ,Tpa2u#0ZVYkիW%uxzzHLLT /֭['[4]oussSK>uܴ4K/ͶȜmժEVvQ+]IŨMwWW?N=(־5QQ8WT$#O%oۺ&Wmצ bTXkjz:LHyPH^j,^^_l `}ܷD y# d4)L:U2r833ڦ6Ta[*rOÈ#;v(9rD4ddpNNjw}|vHHFi#<}V'((Huuu>_nd&|Ѭ\;O\N-qRS%Ͽ%?ӧ.B닐aPQF>.(Ͽ~u7u=d+'k1*, e}B=䓪ӴT,XΝ;7)00Px ,[ 'On|M!YVV]QQ-[W[[N: SWEеx?%Luwdj8L.T¾^_}n"wޕ,4}z-eՑuBRaWWWa[C\mׯ_Gaa!\k׮IFiFxJtҥ0Tm!0y. s/q8V@7] U$p}Ied#uvWDV5@t-<<ZVƍqF^@hh(miLoVX!Ϙ13fAs!$$}Gv򜕕I1u+[.?iKp9a[-=PXX(9ҥK¶xTbY&/Ba>#VALS!*K-SISSST,\%LROW .]DDDD -bʼ䫌6v(zQ%7om'QKjxSĶm322g!** ]tA޽9[~?VOKKC\\^~eo8|жLGEEEηjɽvdyTm|d_t47i qɾ@1:Q...F_8U/&>_\5y `0 ""Æ Jeh y=h#jڪT?,[]]#z]y9ʪ@OD5d\t _56lؠ:M3## ի1iҤ&heaXz5U$''#99xf޽{˗#غIBYFN>`0Hf.@:Z^‡5lgܹs%:u:M`tI24**.#o?CDulo2Re-_чN* Ω\g1"j""""z9W~*N]֫N(=)rk5k˘8q̱cǚuhI&aΝpd \z [t<==%ӹF ?(u&ly_4icDAEe5W?䋠u s66qp\KQ`̦M !"""qZ3JqLeTN?7U/تr ?xxx`Ւ\u|lڴIrLHnݲW߽{w;w49y߾}¶NS~c yyy8sѺ8 ٯoNOKKëWw],|9{JJ<7nѣG;##IIIiJDDD* 8Vt6>ٝ,䯪Ĵčrnn;MQnDX*_Pi ٗg!ӧOuF%l@JV,_.ǠEà[W+_/O`ω2OL'"ۛ7oqa*u$>}ثy63x`̚5h`0>hIP.ZvoL4 1cZȑ#¾3f$ pIIDFFJ͞=[QW=0vXa>Sjjj0sLddd%C\\d򫯾ȝ[ɓ۷/VZx͖}\S?g۷ocvYLIW_ҍUf~ۮ""""C-_ Ǟ'P% {U*#F S'}0rHx{{̙3HLL{pQ+6L6*O^71c}~ ];|1v ڵ\cD/`Ŋ&dddHuLbtu鼼ؾ}zCLL 1aX}~j͚5 W2;uy)6n8>}K.p?( )jѿ!MDDDDT-r|kY><XۿC\4|y(Vľ m7c0땗b$/sVS񷿢[ǎ^SR|:\\\̖纴ܨ(7N3OwAnn$ec5n={6Fa|n*eǎشiFs1/~W^3g0qD#..`~h4,Z)))&| g2j(9saaaFˌ= ǣAb?~3Sϫ5kڹܹ3N<_1v)s,6n,YVAᅲ{5E>e""""jj4e-[۷o}Z\u =ۣGNxD?▸QV3(UmۢWgxyz-'"K(**½{ ]vMD™3gPTTBh4t:>Ł ^Gnn.^ WWWxyy>>>M<)..FNNz=<==__z.++CNN쌠 t޽~[3 trssq xzz=z`>k pYCիܹsΆ3zRW}ݻ`v-QfM@@DDDDDDDDDdDa0 JDDDDDDDDDQ"""""""""rfUUUvMMM6ZwL5Q"""""""""֭[vIIɲ2ODDDDDDDDDl@u¶FiY]vss3Yl@Uf@:۞&r<9 Da0 JDDDDDDDDDQ"""""""""r `@DDDDDDDDD0%"""""""""(9 Da0 JDDDDDDDDDQ"""""""""rfZ`hYKtrr2Yl@]vviiiEDDDDDDDDDd{oL5f?}4ȶݻadyрaѣʉJuu5vڅڶmkJr _...hӦ╕f/h2ۤ - 6\φgki}V[[;wHϩ\$&& V"""""""""$ F,Ƣ(\v ?qM44 z!C]a" Z EEEKۗMZjbt:u-u psskp]| TVVڤπ{|6>z3())AmRWsO>cY}f=[-5a=gֳeUTTu6\φgk}!YVDZ*=(%"""""""""(9 Da0 JDDDDDDDDDQ"""""""""r `@DDDDDDDDD0%"""""""""(9 Da0 JDDDDDDDDDQ"""""""""rvIENDB`sphinx-contrib-typer-8982731/example.svg000066400000000000000000000312061515242076300202140ustar00rootroot00000000000000 example Usage: example [OPTIONS] COMMAND [ARGS]... This is the callback function. ╭─ Options ──────────────────────────────────────────────────────────╮ --flag1--no-flag1Flag 1.[default: no-flag1] --flag2--no-flag2Flag 2.[default: no-flag2] --helpShow this message and exit. ╰────────────────────────────────────────────────────────────────────╯ ╭─ Commands ─────────────────────────────────────────────────────────╮ bar      This is the bar command.                                foo      This is the foo command.                                ╰────────────────────────────────────────────────────────────────────╯ sphinx-contrib-typer-8982731/justfile000066400000000000000000000165511515242076300176160ustar00rootroot00000000000000set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set unstable := true set script-interpreter := ['uv', 'run', '--script'] export PYTHONPATH := source_directory() [private] default: @just --list --list-submodules # install the uv package manager [linux] [macos] install-uv: curl -LsSf https://astral.sh/uv/install.sh | sh # install the uv package manager [windows] install-uv: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # setup the venv and pre-commit hooks setup python="python": uv venv -p {{ python }} @just install-precommit # install git pre-commit hooks install-precommit: @just run --no-default-groups --group precommit --exact --isolated pre-commit install # update and install development dependencies install *OPTS="--all-extras": uv sync {{ OPTS }} @just install-precommit # install without extra dependencies install-basic: uv sync # install documentation dependencies _install-docs: uv sync --no-default-groups --group docs --all-extras # run static type checking check-types: # @just run mypy # @just run pyright # run package checks check-package: uv pip check # remove doc build artifacts [script] clean-docs: import shutil shutil.rmtree('./doc/build', ignore_errors=True) # remove the virtual environment [script] clean-env: import shutil import sys shutil.rmtree(".venv", ignore_errors=True) # remove all git ignored files clean-git-ignored: git clean -fdX # remove all non repository artifacts clean: clean-docs clean-git-ignored clean-env # build html documentation build-docs-html: @just run --group docs --extra png --exact --no-default-groups --isolated sphinx-build --fresh-env --builder html --doctree-dir ./doc/build/doctrees ./doc/source ./doc/build/html [script] _open-pdf-docs: import webbrowser from pathlib import Path webbrowser.open(f"file://{Path('./doc/build/pdf/sphinxcontribtyper.pdf').absolute()}") # build pdf documentation build-docs-pdf: @just run --group docs --extra pdf --exact --no-default-groups --isolated sphinx-build --fresh-env --builder latex --doctree-dir ./doc/build/doctrees ./doc/source ./doc/build/pdf make -C ./doc/build/pdf @just _open-pdf-docs # build the docs build-docs: build-docs-html # build src package and wheel build: uv build # open the html documentation [script] open-docs: import os import webbrowser webbrowser.open(f'file://{os.getcwd()}/doc/build/html/index.html') # build and open the documentation docs: build-docs-html open-docs # serve the documentation, with auto-reload docs-live: @just run --group docs --extra png --exact --no-default-groups --isolated sphinx-autobuild doc/source doc/build --open-browser --watch src --port 0 --delay 1 _link_check: -@just run --no-default-groups --group docs sphinx-build -b linkcheck -Q -D linkcheck_timeout=10 ./doc/source ./doc/build # check the documentation links for broken links [script] check-docs-links: _link_check import os import sys import json from pathlib import Path # The json output isn't valid, so we have to fix it before we can process. data = json.loads(f"[{','.join((Path(os.getcwd()) / 'doc/build/output.json').read_text().splitlines())}]") broken_links = [link for link in data if link["status"] not in {"working", "redirected", "unchecked", "ignored"}] if broken_links: for link in broken_links: print(f"[{link['status']}] {link['filename']}:{link['lineno']} -> {link['uri']}", file=sys.stderr) sys.exit(1) # lint the documentation check-docs: @just run --no-default-groups --group docs doc8 --ignore-path ./doc/build --max-line-length 100 -q ./doc # fetch the intersphinx references for the given package [script] fetch-refs LIB: _install-docs import os from pathlib import Path import logging as _logging import sys import runpy from sphinx.ext.intersphinx import inspect_main _logging.basicConfig() libs = runpy.run_path(Path(os.getcwd()) / "doc/source/conf.py").get("intersphinx_mapping") url = libs.get("{{ LIB }}", None) if not url: sys.exit(f"Unrecognized {{ LIB }}, must be one of: {', '.join(libs.keys())}") if url[1] is None: url = f"{url[0].rstrip('/')}/objects.inv" else: url = url[1] raise SystemExit(inspect_main([url])) # lint the code check-lint: @just run --no-default-groups --group lint ruff check --select I @just run --no-default-groups --group lint ruff check # check if the code needs formatting check-format: @just run --no-default-groups --group lint ruff format --check # check that the readme renders check-readme: @just run --no-default-groups --group lint python -m readme_renderer ./README.md -o /tmp/README.html # sort the python imports sort-imports: @just run --no-default-groups --group lint ruff check --fix --select I # format the code and sort imports format: sort-imports just --fmt --unstable @just run --no-default-groups --group lint ruff format # sort the imports and fix linting issues lint: sort-imports @just run --no-default-groups --group lint ruff check --fix # fix formatting, linting issues and import sorting fix: lint format # run all static checks check: check-lint check-format check-types check-package check-docs check-readme # run all checks including documentation link checking (slow) check-all: check check-docs-links # run zizmor security analysis of CI zizmor: cargo install --locked zizmor zizmor --format sarif .github/workflows/ > zizmor.sarif # run tests test *TESTS: @just run --no-default-groups --exact --all-extras --group test --isolated pytest --cov-append {{ TESTS }} # run tests against a specific sphinx major version (for CI matrix) test-sphinx SPHINX_MAJOR *TESTS: @just run --no-default-groups --exact --all-extras --group test --group sphinx-{{ SPHINX_MAJOR }} --isolated pytest --cov-append {{ TESTS }} # debug a test debug-test *TESTS: @just run pytest \ -o addopts='-ra -q' \ -s --trace --pdbcls=IPython.terminal.debugger:Pdb \ {{ TESTS }} # run the pre-commit checks precommit: @just run pre-commit # erase any coverage data coverage-erase: @just run --no-default-groups --group coverage coverage erase # generate the test coverage report coverage: @just run --no-default-groups --group coverage coverage combine --keep *.coverage @just run --no-default-groups --group coverage coverage report @just run --no-default-groups --group coverage coverage xml # run the command in the virtual environment run +ARGS: uv run {{ ARGS }} # validate the given version string against the lib version [script] validate_version VERSION: import re import tomllib from sphinxcontrib import typer from packaging.version import Version raw_version = "{{ VERSION }}".lstrip("v") version_obj = Version(raw_version) # the version should be normalized assert str(version_obj) == raw_version # make sure all places the version appears agree assert raw_version == tomllib.load(open('pyproject.toml', 'rb'))['project']['version'] assert raw_version == typer.__version__ print(raw_version) # issue a release for the given semver string (e.g. 2.1.0) release VERSION: install check-all @just validate_version v{{ VERSION }} git tag -s v{{ VERSION }} -m "{{ VERSION }} Release" git push origin v{{ VERSION }} sphinx-contrib-typer-8982731/pyproject.toml000066400000000000000000000067511515242076300207630ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "sphinxcontrib-typer" version = "0.8.1" requires-python = ">=3.10,<4.0" authors = [ {name = "Brian Kohan", email = "bckohan@gmail.com"}, ] license = "MIT" license-files = [ "LICENSE" ] description = "Auto generate docs for typer commands." readme = "README.md" repository = "https://github.com/sphinx-contrib/typer" homepage = "https://sphinxcontrib-typer.readthedocs.io" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Web Environment", "Framework :: Sphinx", "Framework :: Sphinx :: Extension", "Topic :: Documentation :: Sphinx", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Documentation", "Topic :: Utilities", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ "sphinx>=6.0", "typer>=0.22.0,<1.0.0" ] [project.urls] "Homepage" = "https://sphinxcontrib-typer.readthedocs.io" "Documentation" = "https://sphinxcontrib-typer.readthedocs.io" "Repository" = "https://github.com/sphinx-contrib/typer" "Issues" = "https://github.com/sphinx-contrib/typer/issues" "Changelog" = "https://sphinxcontrib-typer.readthedocs.io/en/stable/changelog.html" [tool.uv] package = true conflicts = [ [ { group = "sphinx-6" }, { group = "sphinx-7" }, { group = "sphinx-8" }, { group = "sphinx-9" }, ], ] [tool.hatch.build.targets.wheel] packages = ["src/sphinxcontrib"] [project.optional-dependencies] html = [ "selenium>=4.0.0,<5.0.0", "webdriver-manager>=3.0.0,<5.0.0", ] pdf = [ "cairosvg>=2.7.0,<3.0.0", "lxml>=4.2.0,<7.0.0" ] png = [ "selenium>=4.0.0,<5.0.0", "webdriver-manager>=3.0.0,<5.0.0", "pillow>=8.0.0" ] [dependency-groups] themes = [ "furo>=2025.7.19", "sphinx-rtd-theme>=3.0.2", ] docs = [ "doc8>=1.1.2", "furo>=2025.7.19", "sphinx-autobuild>=2024.10.3", { include-group = "themes" }, ] lint = [ "ruff>=0.13.1", "readme-renderer[md]>=40.0", ] coverage = [ "coverage>=7.6.0", ] test = [ { include-group = "coverage" }, { include-group = "themes" }, "beautifulsoup4>=4.13.5", "pytest>=8.4.2", "pytest-cov>=7.0.0", "scikit-learn>=1.6.1", "pypdf>=6.1.0", "scikit-image>=0.24.0", "numpy>=2.0.2", "scipy>=1.13.1", ] sphinx-6 = ["sphinx>=6.0,<7.0"] sphinx-7 = ["sphinx>=7.0,<8.0"] sphinx-8 = ["sphinx>=8.0,<9.0"] sphinx-9 = ["sphinx>=9.0,<10.0; python_version >= '3.12'"] precommit = [ "pre-commit>=4.3.0", ] dev = [ { include-group = "docs" }, { include-group = "lint" }, { include-group = "test" }, { include-group = "precommit" }, "ipdb>=0.13.13", ] [tool.ruff] line-length = 88 exclude = [ "doc", "dist" ] [tool.ruff.lint] exclude = [ "tests/**/*" ] [tool.pytest.ini_options] testpaths = [ "tests/tests.py", ] addopts = [ "--doctest-modules", "--cov=sphinxcontrib.typer", "--cov-report=html", "--cov-report=xml", "--cov-fail-under=80", "--cov-report=term-missing:skip-covered", "--no-cov-on-fail", #"-x", #"--pdb", #"--flake8", ] [tool.doc8] max-line-length = 100 sphinx-contrib-typer-8982731/src/000077500000000000000000000000001515242076300166255ustar00rootroot00000000000000sphinx-contrib-typer-8982731/src/sphinxcontrib/000077500000000000000000000000001515242076300215175ustar00rootroot00000000000000sphinx-contrib-typer-8982731/src/sphinxcontrib/typer/000077500000000000000000000000001515242076300226625ustar00rootroot00000000000000sphinx-contrib-typer-8982731/src/sphinxcontrib/typer/__init__.py000066400000000000000000001147461515242076300250100ustar00rootroot00000000000000r""" :: ███████╗██████╗ ██╗ ██╗██╗███╗ ██╗██╗ ██╗ ██╔════╝██╔══██╗██║ ██║██║████╗ ██║╚██╗██╔╝ ███████╗██████╔╝███████║██║██╔██╗ ██║ ╚███╔╝ ╚════██║██╔═══╝ ██╔══██║██║██║╚██╗██║ ██╔██╗ ███████║██║ ██║ ██║██║██║ ╚████║██╔╝ ██╗ ╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ████████╗██╗ ██╗██████╗ ███████╗██████╗ ╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗ ██║ ╚████╔╝ ██████╔╝█████╗ ██████╔╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝ ██╔══██╗ ██║ ██║ ██║ ███████╗██║ ██║ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ """ import base64 import contextlib import hashlib import inspect import io import os import re import traceback import typing as t from contextlib import contextmanager from enum import Enum from html import escape as html_escape from importlib import import_module from importlib.util import find_spec from pathlib import Path from pprint import pformat import click from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from rich import terminal_theme as rich_theme from rich.console import Console from rich.theme import Theme from sphinx import application from sphinx.addnodes import pending_xref from sphinx.util import logging from sphinx.util.nodes import make_refnode from typer import rich_utils as typer_rich_utils from typer.core import MarkupMode, TyperGroup from typer.main import Typer from typer.main import get_command as get_typer_command from typer.models import Context as TyperContext from typer.models import TyperInfo VERSION = (0, 8, 1) __title__ = "SphinxContrib Typer" __version__ = ".".join(str(i) for i in VERSION) __author__ = "Brian Kohan" __license__ = "MIT" __copyright__ = "Copyright 2023-2026 Brian Kohan" SELENIUM_DEFAULT_WINDOW_WIDTH = 1920 SELENIUM_DEFAULT_WINDOW_HEIGHT = 2048 def get_function(function: t.Union[str, t.Callable[..., t.Any]]): if callable(function): return function if isinstance(function, str): parts = function.split(".") return getattr(import_module(".".join(parts[0:-1])), parts[-1]) def _filter_commands(ctx: click.Context, cmd_filter: t.List[str]): return [ctx.command.get_command(ctx, cmd_name) for cmd_name in cmd_filter] def _add_dependency(env, command): cb = getattr(command, "callback", None) cb = getattr(cb, "__wrapped__", cb) if cb: env.note_dependency(inspect.getfile(cb)) def _command_path(ctx: t.Optional[click.Context]): parts = [] while ctx: parts.append(ctx.info_name) ctx = ctx.parent return ":".join(reversed(parts)) class RenderTarget(str, Enum): HTML = "html" SVG = "svg" TEXT = "text" def __str__(self) -> str: return self.value class RenderTheme(str, Enum): LIGHT = "light" MONOKAI = "monokai" DIMMED_MONOKAI = "dimmed_monokai" NIGHT_OWLISH = "night_owlish" DARK = "dark" RED_SANDS = "red_sands" BLUE_WAVES = "blue_waves" def __str__(self) -> str: return self.value @property def terminal_theme(self) -> rich_theme.TerminalTheme: return { RenderTheme.LIGHT: rich_theme.DEFAULT_TERMINAL_THEME, RenderTheme.MONOKAI: rich_theme.MONOKAI, RenderTheme.DIMMED_MONOKAI: rich_theme.DIMMED_MONOKAI, RenderTheme.NIGHT_OWLISH: rich_theme.NIGHT_OWLISH, RenderTheme.DARK: rich_theme.SVG_EXPORT_THEME, RenderTheme.RED_SANDS: rich_theme.TerminalTheme( (132, 42, 38), # background (210, 193, 159), # text [ (210, 193, 159), # (0, 0, 0), # required (77, 218, 77), # option on short name (227, 189, 57), # Usage/metavar (210, 193, 159), # (0, 18, 140), # option off (75, 214, 225), # option on/command names (210, 193, 159), # ], ), RenderTheme.BLUE_WAVES: rich_theme.TerminalTheme( (20, 118, 247), # background (250, 240, 250), # text [ (250, 240, 250), # (0, 0, 0), # required (0, 255, 0), # option on short name (227, 189, 57), # Usage/metavar (250, 240, 250), # (2, 2, 214), # option off (146, 226, 252), # option on/command names (250, 240, 250), # ], ), }[self] Command = t.Union[click.Command, click.Group] """ Callbacks that return a dict of kwargs to pass to various renderer functions must all have the RenderCallback function signature: """ RenderCallback = t.Callable[ [ "TyperDirective", # directive - the TyperDirective instance str, # name - the name of the command Command, # command - the command instance click.Context, # ctx - the click.Context instance t.Optional[click.Context], # parent - the parent click.Context instance ], t.Dict[str, t.Any], ] """ Custom render options can be provided at a python path that resolves to the following type. Either a dictionary of kwargs to pass to the relevant function or a callable that returns a dictionary of kwargs to pass to the relevant function """ RenderOptions = t.Union[t.Dict[str, t.Any], RenderCallback] class TyperDirective(rst.Directive): """ A directive that renders a Typer app or Click command help text as either an html, text literal or svg image node depending on the builder and configuraton. Ex usage. .. code-block:: rst .. typer:: import.path.to.typer.app:subcommand :prog: script_name """ logger = logging.getLogger("sphinxcontrib.typer") has_content = False required_arguments = 1 option_spec = { "prog": directives.unchanged_required, "make-sections": directives.flag, "show-nested": directives.flag, "markup-mode": directives.unchanged, "width": directives.nonnegative_int, "theme": RenderTheme, "svg-kwargs": directives.unchanged, "text-kwargs": directives.unchanged, "html-kwargs": directives.unchanged, "console-kwargs": directives.unchanged, "preferred": RenderTarget, "builders": directives.unchanged, "iframe-height": directives.nonnegative_int, "convert-png": directives.unchanged, } # resolved options prog_name: str nested: bool make_sections: bool width: int iframe_height: t.Optional[int] = None typer_convert_png: bool = False console: Console parent: click.Context theme: RenderTheme = RenderTheme.LIGHT preferred: t.Optional[RenderTarget] = None markup_mode: MarkupMode # the console_kwargs option can be a dict or a callable that returns a dict, the callable # must conform to the RenderOptions signature console_kwargs: RenderOptions html_kwargs: RenderOptions svg_kwargs: RenderOptions text_kwargs: RenderOptions target: RenderTarget builder_targets = { **{ builder: [RenderTarget.SVG, RenderTarget.HTML, RenderTarget.TEXT] for builder in [ "html", "dirhtml", "singlehtml", "htmlhelp", "qthelp", "devhelp", ] }, "epub": [RenderTarget.HTML, RenderTarget.SVG, RenderTarget.TEXT], **{ builder: [RenderTarget.SVG, RenderTarget.TEXT] for builder in ["latex", "latexpdf", "texinfo"] }, **{builder: [RenderTarget.TEXT] for builder in ["text", "gettext"]}, } @property def builder(self) -> str: return self.env.app.builder.name def uuid(self, normal_cmd: str) -> str: """ Get a repeatable unique hash id for a given directive instance and command. This is used to generate repeatable unique filenames for any build artifacts like svg -> pdf conversions. :param normal_cmd: The normalized command name """ # Contextual information source = self.state_machine.get_source_and_line()[0] line_number = self.state_machine.get_source_and_line()[1] source = os.path.relpath(source, self.env.app.builder.srcdir) return hashlib.sha256( f"{source}.{line_number}[{normal_cmd}]".encode("utf-8") ).hexdigest()[:8] def import_object( self, obj_path: t.Optional[str], accessor: t.Callable[[t.Any, str, t.Any], t.Any] = lambda obj, attr, _: getattr( obj, attr ), ) -> t.Any: """ Imports an arbitrary object from a python string path. Delimiters can be '.', '::' or ':'. :param obj_path: The python path to the object, if False, returns None """ if not obj_path: return None parts = re.split(r"::|[.:]", obj_path) tries = 1 try: while True: # walk up the import path until we find something importable # then walk down the path fetching all the attributes # this allows import strings to reach into nested class # attributes try: tries += 1 try_path = ".".join(parts[0 : -(tries - 1)]) obj = import_module(try_path) file_spec = getattr(find_spec(try_path), "origin", None) if file_spec: self.env.note_dependency(file_spec) for attr in parts[-(tries - 1) :]: obj = accessor(obj, attr, try_path) break except (ImportError, ModuleNotFoundError): if tries >= len(parts): raise except (Exception, SystemExit) as exc: err_msg = f'Failed to import "{obj_path}"' if isinstance(exc, SystemExit): err_msg += "The module appeared to call sys.exit()." else: err_msg += "The following exception was raised:\n{}".format( traceback.format_exc() ) raise self.severe(err_msg) return obj def load_root_command(self, typer_path: str) -> t.Union[click.Command, click.Group]: """ Load the module. :param typer_path: The python path to the Typer app instance. """ def resolve_root_command(obj): if isinstance(obj, (click.Command, click.Group)): return obj # use lenient duck typing check incase obj is a proxy for a Typer instance if isinstance(obj, Typer) or isinstance( getattr(obj, "info", None), TyperInfo ): return get_typer_command(obj) if callable(obj): ret = obj() if isinstance(ret, Typer) or isinstance( getattr(obj, "info", None), TyperInfo ): return get_typer_command(obj) if isinstance(ret, (click.Command, click.Group)): return ret raise self.error( f'"{typer_path}" of type {type(obj)} is not Typer, click.Command or ' "click.Group." ) def access_command( obj, attr, imprt_path ) -> t.Union[click.Command, click.Group]: attr_obj = None try: attr_obj = getattr(obj, attr) return resolve_root_command(attr_obj) except Exception: try: self.parent = TyperContext( resolve_root_command(obj), # we can't trust the name attribute for the first # command - but it is probably the best bet for # subsequent commands - so if this is a nested # import pull out the name attribute if it exists # otherwise we use the last successful import path # part because it is probably the module with main info_name=( ( getattr(obj, "name", "") if getattr(self, "parent", None) else "" ) or imprt_path.split(".")[-1] ), parent=getattr(self, "parent", None), ) cmds = _filter_commands(self.parent, [attr]) if cmds: return cmds[0] except (IndexError, rst.DirectiveError): if attr_obj: return attr_obj raise return resolve_root_command( self.import_object(typer_path, accessor=access_command) ) def get_html(self, **options): return self.console.export_html( **{"theme": self.theme.terminal_theme, **options, "clear": False} ) def get_svg(self, **options): return self.console.export_svg( **{"theme": self.theme.terminal_theme, **options, "clear": False} ) def get_text(self, **options): return self.console.export_text(**{**options, "clear": False}) def generate_nodes( self, name: str, command: click.Command, parent: t.Optional[click.Context], ) -> t.List[nodes.section]: """ Generate the relevant Sphinx nodes. Generate node help for `click.Group` or `click.Command`. :param command: Instance of `click.Group` or `click.Command` :param parent: Instance of `typer.models.Context`, or None :returns: A list of nested docutil nodes """ ctx = TyperContext( command, info_name=name, parent=parent, terminal_width=self.width, max_content_width=self.width, ) _add_dependency(self.env, command) if command.hidden: return [] normal_cmd = section_title = _command_path(ctx).replace(":", " ") section_id = nodes.make_id(section_title) if not getattr(self, "parent", None): section_title = section_title.split(" ")[-1] section = ( nodes.section( "", nodes.title(text=section_title), ids=[section_id], names=[nodes.fully_normalize_name(section_title)], ) if self.make_sections else nodes.container() ) self.env.domaindata["std"].setdefault("typer", {})[section_id] = ( self.env.docname, section_id, normal_cmd, ) # Summary def resolve_options( options: RenderOptions, parameter: str ) -> t.Dict[str, t.Any]: if callable(options): options = options(self, name, command, ctx, parent) if isinstance(options, dict): return options raise self.severe( f"Invalid {parameter}, must be a dict or callable, got {type(options)}" ) def get_console(stderr: bool = False) -> Console: self.console = Console( **{ "theme": Theme( { "option": typer_rich_utils.STYLE_OPTION, "switch": typer_rich_utils.STYLE_SWITCH, "negative_option": typer_rich_utils.STYLE_NEGATIVE_OPTION, "negative_switch": typer_rich_utils.STYLE_NEGATIVE_SWITCH, "metavar": typer_rich_utils.STYLE_METAVAR, "metavar_sep": typer_rich_utils.STYLE_METAVAR_SEPARATOR, "usage": typer_rich_utils.STYLE_USAGE, }, ), "highlighter": typer_rich_utils.highlighter, "color_system": None if self.target is RenderTarget.TEXT else typer_rich_utils.COLOR_SYSTEM, "force_terminal": typer_rich_utils.FORCE_TERMINAL, "width": self.width or typer_rich_utils.MAX_WIDTH, "stderr": stderr, # overrides any defaults above **resolve_options(self.console_kwargs, "console-kwargs"), "record": True, } ) return self.console # todo # typer provides no official way to alter the console that prints out the help # command so we have to monkey patch it - revisit in future if this changes! # we also monkey patch get_help incase its a click command orig_getter = typer_rich_utils._get_rich_console orig_format_help = command.format_help command.rich_markup_mode = getattr( self, "markup_mode", getattr(command, "rich_markup_mode", "markdown") ) command.format_help = TyperGroup.format_help.__get__(command, command.__class__) typer_rich_utils._get_rich_console = get_console with contextlib.redirect_stdout(io.StringIO()): command.get_help(ctx) typer_rich_utils._get_rich_console = orig_getter command.format_help = orig_format_help ############################################################################## export_options = resolve_options( getattr(self, f"{self.target}_kwargs", {}), f"{self.target}-kwargs" ) rendered = getattr(self, f"get_{self.target}")( **({"title": section_title} if self.target is RenderTarget.SVG else {}), **export_options, ) def to_path(name: str, ext: str) -> Path: return ( Path(self.env.app.builder.outdir) / f"{name.replace(':', '_').replace(' ', '_')}_{self.uuid(name)}.{ext}" ) # Image URIs must be relative to the document's directory, not srcdir, # so that Sphinx can locate the file when the directive appears in a # document nested inside a subdirectory (e.g. via autodoc). # See https://github.com/sphinx-contrib/typer/issues/58 doc_dir = Path(self.env.srcdir) / Path(self.env.docname).parent if self.typer_convert_png: png_path = to_path(normal_cmd, "png") get_function(self.env.app.config.typer_convert_png)( self, rendered, png_path ) section += nodes.image( uri=os.path.relpath(png_path, doc_dir), alt=section_title, ) elif self.target == RenderTarget.HTML: section += nodes.raw( "", get_function(self.env.app.config.typer_render_html)( self, normal_cmd, rendered ), format="html", ) elif self.target == RenderTarget.SVG: if "html" in self.builder: section += nodes.raw("", rendered, format="html") else: svg_path = to_path(normal_cmd, "svg") pdf_path = to_path(normal_cmd, "pdf") svg_path.write_text(rendered) get_function(self.env.app.config.typer_svg2pdf)( self, rendered, pdf_path ) section += nodes.image( uri=os.path.relpath(pdf_path, doc_dir), alt=section_title, ) elif self.target == RenderTarget.TEXT: section += nodes.literal_block("", rendered) else: raise self.severe(f"Invalid typer render target: {self.target}") # recurse through subcommands if we should if isinstance(command, click.MultiCommand): commands = _filter_commands(ctx, command.list_commands(ctx)) for command in commands: if self.nested: section.extend( self.generate_nodes(command.name, command, parent=ctx) ) else: _add_dependency(self.env, command) return [section] def run(self) -> t.Iterable[nodes.section]: self.env = self.state.document.settings.env command = self.load_root_command(self.arguments[0]) self.make_sections = "make-sections" in self.options self.nested = "show-nested" in self.options self.prog_name = self.options.get("prog", "") if "markup-mode" in self.options: self.markup_mode = self.options["markup-mode"] if not self.prog_name: try: self.prog_name = ( command.callback.__module__.split(".")[-1] if hasattr(command, "callback") and not hasattr(self, "parent") else re.split(r"::|[.:]", self.arguments[0])[-1] ) except Exception as err: raise self.severe( "Unable to determine program name, please specify using :prog:" ) from err self.prog_name = self.prog_name.strip() self.width = self.options.get("width", 65) self.iframe_height = self.options.get("iframe-height", None) # if no builders supplied but convert-png is set, # force png for all builders, otherwise require the builder # to be in the list of typer_convert_png builders self.typer_convert_png = "convert-png" in self.options if self.typer_convert_png: builders = self.options["convert-png"].strip() self.typer_convert_png = self.builder in builders if builders else True for trg in ["console", *list(RenderTarget)]: setattr( self, f"{trg}_kwargs", self.import_object(self.options.get(f"{trg}-kwargs", None)) or {}, ) self.preferred = self.options.get("preferred", None) self.theme = self.options.get("theme", self.theme) builder_targets = {} for builder_target in self.options.get("builders", "").split(":"): if builder_target.strip(): builder, targets = builder_target.split("=")[0:2] builder_targets[builder.strip()] = [ RenderTarget(target.strip()) for target in targets.split(",") ] builder_targets = {**self.builder_targets, **builder_targets} if self.typer_convert_png: self.target = ( self.preferred or (builder_targets.get(self.builder, []) or [RenderTarget.SVG])[0] ) elif self.builder not in builder_targets: self.target = self.preferred or RenderTarget.TEXT self.logger.debug( "Unable to resolve render target for builder: %s - using: %s", self.builder, self.target, ) else: supported = builder_targets[self.builder] self.target = ( self.preferred if self.preferred in supported else supported[0] ) parent = getattr(self, "parent", None) if parent and self.options.get("prog", None): # we unset this because we're not at the root command and this gets # messed up for whatever reason # https://github.com/sphinx-contrib/typer/issues/24 parent.info_name = "" return self.generate_nodes(self.prog_name, command, parent) def typer_get_iframe_height( directive: TyperDirective, normal_cmd: str, html_page: str ) -> int: """ The default iframe height calculation function. The iframe height resolution proceeds as follows: 1) Return the global iframe-height parameter if one was supplied as a parameter on the directive. 2) Check for a cached height value. 3) Attempt to use Selenium to dynamically determine the height of the iframe. Padding will be added from the config.typer_iframe_height_padding configuration value. The resulting height is then cached if that path is not None. If the attempt to use Selenium fails (it is not installed) a warning is issued and a default height of 600 is returned. :param directive: The TyperDirective instance :param normal_cmd: The normalized name of the command. (Subcommands are delimited by :) :param html_page: The full html document that will be rendered in the iframe """ if directive.iframe_height is not None: return directive.iframe_height if not hasattr(directive.env, "iframe_heights"): directive.env.iframe_heights = {} if height := directive.env.iframe_heights.get(normal_cmd, None): return height with get_function(directive.env.app.config.typer_get_web_driver)( directive ) as driver: # use base64 to avoid issues with special characters driver.get( f"data:text/html;base64," f"{base64.b64encode(html_page.encode('utf-8')).decode()}" ) height = ( int( driver.execute_script( "return document.documentElement.getBoundingClientRect().height" ) ) + directive.env.app.config.typer_iframe_height_padding ) directive.env.iframe_heights[normal_cmd] = height return height def typer_render_html( directive: TyperDirective, normal_cmd: str, html_page: str ) -> str: """ The default html rendering function. This function returns the html console output wrapped in an iframe. The height of the iframe is dynamically determined by calling the configured typer_get_iframe_height function. :param directive: The TyperDirective instance :param normal_cmd: The normalized name of the command. (Subcommands are delimited by :) :param html_page: The html page rendered by console.export_html """ height = get_function(directive.env.app.config.typer_get_iframe_height)( directive, normal_cmd, html_page ) return ( f'' ) def typer_svg2pdf(directive: TyperDirective, svg_contents: str, pdf_path: str): """ The default typer_svg2pdf function. This function uses the cairosvg package to convert svg to pdf. .. note:: You will likely need to install fonts locally on your machine for the output of these conversions to look correct. The default font used by the svg export from rich is `FiraCode `_. :param directive: The TyperDirective instance :param svg_contents: The svg contents to convert to pdf :param pdf_path: The path to write the pdf to """ try: import cairosvg cairosvg.svg2pdf(bytestring=svg_contents, write_to=str(pdf_path)) except ImportError: directive.severe("cairosvg must be installed to render SVG in pdfs") @contextmanager def typer_get_web_driver( directive: TyperDirective, width: int = SELENIUM_DEFAULT_WINDOW_WIDTH, height: int = SELENIUM_DEFAULT_WINDOW_HEIGHT, ) -> t.Any: """ The default get_web_driver function. This function yields a selenium web driver instance. It requires selenium to be installed. To override this function with a custom function see the ``typer_get_web_driver`` configuration parameter. .. note:: This must be implemented as a context manager that yields the webdriver instance and cleans it up on exit! :param directive: The TyperDirective instance """ import platform try: from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions except ImportError: raise directive.severe( "This feature requires selenium and webdriver-manager to be installed." ) # Set up headless browser options def opts(options=ChromeOptions()): options.add_argument("--headless") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") options.add_argument(f"--window-size={width}x{height}") return options def chrome(): from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager try: return webdriver.Chrome(options=opts()) except Exception: return webdriver.Chrome( service=Service(ChromeDriverManager().install()), options=opts() ) def chromium(): from selenium.webdriver.chrome.service import Service as ChromiumService from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.core.os_manager import ChromeType return webdriver.Chrome( service=ChromiumService( ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() ), options=opts(), ) def firefox(): from selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service as FirefoxService from webdriver_manager.firefox import GeckoDriverManager return webdriver.Firefox( service=FirefoxService(GeckoDriverManager().install()), options=opts(Options()), ) def edge(): from selenium.webdriver.edge.options import Options from selenium.webdriver.edge.service import Service as EdgeService from webdriver_manager.microsoft import EdgeChromiumDriverManager options = Options() options.use_chromium = True return webdriver.Edge( service=EdgeService(EdgeChromiumDriverManager().install()), options=opts(options), ) services = [ chrome, edge if platform.system().lower() == "windows" else chromium, firefox, ] driver = None for service in services: try: driver = service() break # use the first one that works! except Exception as err: directive.debug(f"Unable to initialize webdriver {service.__name__}: {err}") if driver: yield driver driver.quit() else: raise directive.severe("Unable to initialize any webdriver.") def typer_convert_png( directive: TyperDirective, rendered: str, png_path: t.Union[str, Path], selenium_width: int = SELENIUM_DEFAULT_WINDOW_WIDTH, selenium_height: int = SELENIUM_DEFAULT_WINDOW_HEIGHT, ): """ The default typer_convert_png function. This function writes a png file to the given path by taking a selenium screen shot. It requires selenium to be installed. To override this function with a custom function see the ``typer_convert_png`` configuration parameter. :param directive: The TyperDirective instance :param rendered: The rendered command help. May be html, svg, or text. :param png_path: The path to write the png to :param selenium_width: The width of the selenium window - must be larger than the png to avoid cropping, default auto determine :param selenium_height: The height of the selenium window - must be larger than the png to avoid cropping, default auto determine """ import tempfile from io import BytesIO from PIL import Image from selenium.webdriver.common.by import By tag = "code" with get_function(directive.env.app.config.typer_get_web_driver)( directive ) as driver: with tempfile.NamedTemporaryFile(suffix=".html") as tmp: if directive.target is RenderTarget.TEXT: tag = "pre" rendered = f"
{rendered}
" elif directive.target is RenderTarget.SVG: tag = "svg" rendered = f"{rendered}" tmp.write(rendered.encode("utf-8")) tmp.flush() driver.get(f"file://{tmp.name}") png = driver.get_screenshot_as_png() # Find the element you want a screenshot of element = driver.find_element(By.CSS_SELECTOR, tag) pixel_ratio = driver.execute_script("return window.devicePixelRatio") # Get the element's location and size location = element.location size = element.size if size["width"] > selenium_width or size["height"] > selenium_height: # if our window is too small, resize it with some padding and try again return typer_convert_png( directive, rendered, png_path, size["width"] + 100, size["height"] + 100, ) # Open the screenshot and crop it to the element im = Image.open(BytesIO(png)) left = location["x"] * pixel_ratio top = location["y"] * pixel_ratio if directive.target is RenderTarget.TEXT: # getting the width of the text is actually a bit tricky script = """ const pre = arguments[0]; const textContent = pre.textContent || pre.innerText; const temporarySpan = document.createElement('span'); document.body.appendChild(temporarySpan); // Copy styles to match formatting const preStyle = window.getComputedStyle(pre); temporarySpan.style.fontFamily = preStyle.fontFamily; temporarySpan.style.fontSize = preStyle.fontSize; temporarySpan.style.whiteSpace = 'pre'; temporarySpan.textContent = textContent; return temporarySpan.offsetWidth; """ width = driver.execute_script(script, element) right = left + width * pixel_ratio else: right = left + size["width"] * pixel_ratio bottom = top + size["height"] * pixel_ratio im = im.crop((left, top, right, bottom)) # Defines crop points im.save(str(png_path)) # Saves the screenshot _link_regex = re.compile(r"([^<]+)(?:<(.+?)>)?") def _link_and_text(text): return _link_regex.search(text).groups() def resolve_typer_reference(app, env, node, contnode): if node["reftype"] != "typer": return target_id = node["reftarget"] if target_id in env.domaindata["std"].get("typer", {}): docname, labelid, sectionname = env.domaindata["std"]["typer"][target_id] refnode = make_refnode( env.app.builder, node["refdoc"], docname, labelid, nodes.Text(node["reftitle"] or sectionname.strip()), target_id, ) return refnode else: lineno = node.line or getattr(node.parent, "line", 0) error_message = env.get_doctree(node["refdoc"]).reporter.error( f"Unresolved :typer: reference: '{target_id}' in document '{node['refdoc']}'. " f"Expected one of: {pformat(list(env.domaindata['std'].get('typer', {}).keys()), indent=2)}", line=lineno, ) msgid = node.document.set_id(error_message, node.parent) problematic = nodes.problematic(node.rawsource, node.rawsource, refid=msgid) prbid = node.document.set_id(problematic) error_message.add_backref(prbid) return problematic def typer_ref_role(name, rawtext, text, lineno, inliner, options={}, content=[]): env = inliner.document.settings.env title, link = _link_and_text(text) title = title.strip() if link: link = link.strip() target_id = nodes.make_id(link or title) if target_id in env.domaindata["std"].get("typer", {}): docname, labelid, sectionname = env.domaindata["std"]["typer"][target_id] refnode = make_refnode( env.app.builder, env.docname, docname, labelid, nodes.Text(sectionname.strip() if not link else title), target_id, ) return [refnode], [] else: pending = pending_xref( rawtext, refdomain="std", reftype="typer", reftarget=target_id, modname=None, classname=None, refexplicit=True, refwarn=True, reftitle=title if link else None, refdoc=env.docname, ) pending += nodes.Text(text) return [pending], [] def setup(app: application.Sphinx) -> t.Dict[str, t.Any]: # Need autodoc to support mocking modules app.add_directive("typer", TyperDirective) app.add_role("typer", typer_ref_role) app.connect("missing-reference", resolve_typer_reference) app.add_config_value( "typer_render_html", "sphinxcontrib.typer.typer_render_html", "env" ) app.add_config_value( "typer_get_iframe_height", "sphinxcontrib.typer.typer_get_iframe_height", "env" ) app.add_config_value("typer_svg2pdf", "sphinxcontrib.typer.typer_svg2pdf", "env") app.add_config_value("typer_iframe_height_padding", 30, "env") app.add_config_value( "typer_convert_png", "sphinxcontrib.typer.typer_convert_png", "env" ) app.add_config_value( "typer_get_web_driver", "sphinxcontrib.typer.typer_get_web_driver", "env" ) return { "version": __version__, "parallel_read_safe": True, "parallel_write_safe": True, } sphinx-contrib-typer-8982731/tests/000077500000000000000000000000001515242076300172005ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/000077500000000000000000000000001515242076300202655ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/LICENSE.rst000066400000000000000000000027031515242076300221030ustar00rootroot00000000000000Copyright 2014 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sphinx-contrib-typer-8982731/tests/click/aliases/000077500000000000000000000000001515242076300217065ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/aliases/aliases.ini000066400000000000000000000000241515242076300240240ustar00rootroot00000000000000[aliases] ci=commit sphinx-contrib-typer-8982731/tests/click/aliases/aliases.py000066400000000000000000000101431515242076300237000ustar00rootroot00000000000000import configparser import os import click class Config: """The config in this example only holds aliases.""" def __init__(self): self.path = os.getcwd() self.aliases = {} def add_alias(self, alias, cmd): self.aliases.update({alias: cmd}) def read_config(self, filename): parser = configparser.RawConfigParser() parser.read([filename]) try: self.aliases.update(parser.items("aliases")) except configparser.NoSectionError: pass def write_config(self, filename): parser = configparser.RawConfigParser() parser.add_section("aliases") for key, value in self.aliases.items(): parser.set("aliases", key, value) with open(filename, "wb") as file: parser.write(file) pass_config = click.make_pass_decorator(Config, ensure=True) class AliasedGroup(click.Group): """This subclass of a group supports looking up aliases in a config file and with a bit of magic. """ def list_commands(self, ctx): return reversed(sorted(super().list_commands(ctx))) def get_command(self, ctx, cmd_name): # Step one: bulitin commands as normal rv = click.Group.get_command(self, ctx, cmd_name) if rv is not None: return rv # Step two: find the config object and ensure it's there. This # will create the config object is missing. cfg = ctx.ensure_object(Config) # Step three: look up an explicit command alias in the config if cmd_name in cfg.aliases: actual_cmd = cfg.aliases[cmd_name] return click.Group.get_command(self, ctx, actual_cmd) # Alternative option: if we did not find an explicit alias we # allow automatic abbreviation of the command. "status" for # instance will match "st". We only allow that however if # there is only one command. matches = [ x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower()) ] if not matches: return None elif len(matches) == 1: return click.Group.get_command(self, ctx, matches[0]) ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") def resolve_command(self, ctx, args): # always return the command's name, not the alias _, cmd, args = super().resolve_command(ctx, args) return cmd.name, cmd, args def read_config(ctx, param, value): """Callback that is used whenever --config is passed. We use this to always load the correct config. This means that the config is loaded even if the group itself never executes so our aliases stay always available. """ cfg = ctx.ensure_object(Config) if value is None: value = os.path.join(os.path.dirname(__file__), "aliases.ini") cfg.read_config(value) return value @click.command(cls=AliasedGroup) @click.option( "--config", type=click.Path(exists=True, dir_okay=False), callback=read_config, expose_value=False, help="The config file to use instead of the default.", ) def cli(): """An example application that supports aliases.""" @cli.command() def push(): """Pushes changes.""" click.echo("Push") @cli.command() def pull(): """Pulls changes.""" click.echo("Pull") @cli.command() def clone(): """Clones a repository.""" click.echo("Clone") @cli.command() def commit(): """Commits pending changes.""" click.echo("Commit") @cli.command() @pass_config def status(config): """Shows the status.""" click.echo(f"Status for {config.path}") @cli.command() @pass_config @click.argument("alias_", metavar="ALIAS", type=click.STRING) @click.argument("cmd", type=click.STRING) @click.option( "--config_file", type=click.Path(exists=True, dir_okay=False), default="aliases.ini" ) def alias(config, alias_, cmd, config_file): """Adds an alias to the specified configuration file.""" config.add_alias(alias_, cmd) config.write_config(config_file) click.echo(f"Added '{alias_}' as alias for '{cmd}'") if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/aliases/index.rst000066400000000000000000000001151515242076300235440ustar00rootroot00000000000000.. typer:: aliases:cli :show-nested: :preferred: text :width: 65 sphinx-contrib-typer-8982731/tests/click/callbacks.py000066400000000000000000000020661515242076300225620ustar00rootroot00000000000000from sphinxcontrib import typer from pathlib import Path import json import os TEST_CALLBACKS = Path(__file__).parent / "callback_record.json" test_callbacks = {} def record_callback(callback): """crude but it works""" if TEST_CALLBACKS.is_file(): os.remove(TEST_CALLBACKS) test_callbacks[callback] = True TEST_CALLBACKS.write_text(json.dumps(test_callbacks)) def typer_get_web_driver(*args, **kwargs): record_callback("typer_get_web_driver") return typer.typer_get_web_driver(*args, **kwargs) def typer_render_html(*args, **kwargs): record_callback("typer_render_html") return typer.typer_render_html(*args, **kwargs) def typer_get_iframe_height(*args, **kwargs): record_callback("typer_get_iframe_height") return typer.typer_get_iframe_height(*args, **kwargs) def typer_svg2pdf(*args, **kwargs): record_callback("typer_svg2pdf") return typer.typer_svg2pdf(*args, **kwargs) def typer_convert_png(*args, **kwargs): record_callback("typer_convert_png") return typer.typer_convert_png(*args, **kwargs) sphinx-contrib-typer-8982731/tests/click/completion/000077500000000000000000000000001515242076300224365ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/completion/completion.py000066400000000000000000000030421515242076300251600ustar00rootroot00000000000000import os import click from click.shell_completion import CompletionItem class AlphOrderedGroup(click.Group): def list_commands(self, ctx): return sorted(super().list_commands(ctx)) @click.group(cls=AlphOrderedGroup) def cli(): pass @cli.command() @click.option("--dir", type=click.Path(file_okay=False)) def ls(dir): click.echo("\n".join(os.listdir(dir))) def get_env_vars(ctx, param, incomplete): # Returning a list of values is a shortcut to returning a list of # CompletionItem(value). return [k for k in os.environ if incomplete in k] @cli.command(help="A command to print environment variables") @click.argument("envvar", shell_complete=get_env_vars) def show_env(envvar): click.echo(f"Environment variable: {envvar}") click.echo(f"Value: {os.environ[envvar]}") @cli.group(help="A group that holds a subcommand") def group(): pass def list_users(ctx, param, incomplete): # You can generate completions with help strings by returning a list # of CompletionItem. You can match on whatever you want, including # the help. items = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")] out = [] for value, help in items: if incomplete in value or incomplete in help: out.append(CompletionItem(value, help=help)) return out @group.command(help="Choose a user") @click.argument("user", shell_complete=list_users) def select_user(user): click.echo(f"Chosen user is {user}") cli.add_command(group) if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/completion/index.rst000066400000000000000000000001201515242076300242700ustar00rootroot00000000000000.. typer:: completion:cli :show-nested: :preferred: text :width: 70 sphinx-contrib-typer-8982731/tests/click/complex/000077500000000000000000000000001515242076300217345ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/complex/complex/000077500000000000000000000000001515242076300234035ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/complex/complex/__init__.py000066400000000000000000000000001515242076300255020ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/complex/complex/cli.py000066400000000000000000000031661515242076300245320ustar00rootroot00000000000000import os import sys import click CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX") class Environment: def __init__(self): self.verbose = False self.home = os.getcwd() def log(self, msg, *args): """Logs a message to stderr.""" if args: msg %= args click.echo(msg, file=sys.stderr) def vlog(self, msg, *args): """Logs a message to stderr only if verbose is enabled.""" if self.verbose: self.log(msg, *args) pass_environment = click.make_pass_decorator(Environment, ensure=True) cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands")) class ComplexCLI(click.MultiCommand): def list_commands(self, ctx): rv = [] for filename in os.listdir(cmd_folder): if filename.endswith(".py") and filename.startswith("cmd_"): rv.append(filename[4:-3]) rv.sort() return rv def get_command(self, ctx, name): try: mod = __import__(f"complex.commands.cmd_{name}", None, None, ["cli"]) except ImportError: return return mod.cli @click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS) @click.option( "--home", type=click.Path(exists=True, file_okay=False, resolve_path=True), help="Changes the folder to operate on.", ) @click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.") @pass_environment def cli(ctx, verbose, home): """A complex command line interface.""" ctx.verbose = verbose if home is not None: ctx.home = home if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/complex/complex/commands/000077500000000000000000000000001515242076300252045ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/complex/complex/commands/__init__.py000066400000000000000000000000001515242076300273030ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/complex/complex/commands/cmd_init.py000066400000000000000000000006501515242076300273450ustar00rootroot00000000000000from complex.cli import pass_environment import click @click.command("init", short_help="Initializes a repo.") @click.argument("path", required=False, type=click.Path(resolve_path=True)) @pass_environment def cli(ctx, path): """Initializes a repository.""" if path is None: path = ctx.home ctx.log(f"Initialized the repository in {click.format_filename(path)}") if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/complex/complex/commands/cmd_status.py000066400000000000000000000004361515242076300277270ustar00rootroot00000000000000from complex.cli import pass_environment import click @click.command("status", short_help="Shows file changes.") @pass_environment def cli(ctx): """Shows file changes in the current working directory.""" ctx.log("Changed files: none") ctx.vlog("bla bla bla, debug info") sphinx-contrib-typer-8982731/tests/click/complex/index.rst000066400000000000000000000001701515242076300235730ustar00rootroot00000000000000.. typer:: complex.cli:cli :show-nested: :prog: complex :preferred: text :make-sections: :width: 65 sphinx-contrib-typer-8982731/tests/click/conf.py000066400000000000000000000052041515242076300215650ustar00rootroot00000000000000from datetime import datetime import sys from pathlib import Path from sphinxcontrib import typer as sphinxcontrib_typer import json import os # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # get all sub directories from here and add them to the path sys.path.append(str(Path(__file__).parent)) for path in Path(__file__).parent.iterdir(): if path.is_dir(): sys.path.append(str(path)) # -- Project information ----------------------------------------------------- project = "SphinxContrib Typer Tests" copyright = f"2023-{datetime.now().year}, Brian Kohan" author = "Brian Kohan" # The full version, including alpha/beta/rc tags release = sphinxcontrib_typer.__version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx_rtd_theme", "sphinxcontrib.typer"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] todo_include_todos = True ########################################################### # Test our typer configuration parameter function overrides typer_render_html = "callbacks.typer_render_html" typer_get_iframe_height = "callbacks.typer_get_iframe_height" typer_svg2pdf = "callbacks.typer_svg2pdf" typer_convert_png = "callbacks.typer_convert_png" typer_get_web_driver = "callbacks.typer_get_web_driver" typer_iframe_height_padding = 40 def setup(app): app.connect("builder-inited", iframe_cache) def iframe_cache(app): if not hasattr(app.env, "iframe_heights"): app.env.iframe_heights = {} app.env.iframe_heights["validation"] = 347 sphinx-contrib-typer-8982731/tests/click/imagepipe/000077500000000000000000000000001515242076300222255ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/imagepipe/imagepipe.py000066400000000000000000000203711515242076300245420ustar00rootroot00000000000000from functools import update_wrapper from PIL import Image from PIL import ImageEnhance from PIL import ImageFilter import click class AlphOrderedGroup(click.Group): def list_commands(self, ctx): return sorted(super().list_commands(ctx)) @click.group(cls=AlphOrderedGroup, chain=True) def cli(): """This script processes a bunch of images through pillow in a unix pipe. One commands feeds into the next. Example: \b imagepipe open -i example01.jpg resize -w 128 display imagepipe open -i example02.jpg blur save """ @cli.result_callback() def process_commands(processors): """This result callback is invoked with an iterable of all the chained subcommands. As in this example each subcommand returns a function we can chain them together to feed one into the other, similar to how a pipe on unix works. """ # Start with an empty iterable. stream = () # Pipe it through all stream processors. for processor in processors: stream = processor(stream) # Evaluate the stream and throw away the items. for _ in stream: pass def processor(f): """Helper decorator to rewrite a function so that it returns another function from it. """ def new_func(*args, **kwargs): def processor(stream): return f(stream, *args, **kwargs) return processor return update_wrapper(new_func, f) def generator(f): """Similar to the :func:`processor` but passes through old values unchanged and does not pass through the values as parameter. """ @processor def new_func(stream, *args, **kwargs): yield from stream yield from f(*args, **kwargs) return update_wrapper(new_func, f) def copy_filename(new, old): new.filename = old.filename return new @cli.command("open") @click.option( "-i", "--image", "images", type=click.Path(), multiple=True, help="The image file to open.", ) @generator def open_cmd(images): """Loads one or multiple images for processing. The input parameter can be specified multiple times to load more than one image. """ for image in images: try: click.echo(f"Opening '{image}'") if image == "-": img = Image.open(click.get_binary_stdin()) img.filename = "-" else: img = Image.open(image) yield img except Exception as e: click.echo(f"Could not open image '{image}': {e}", err=True) @cli.command("save") @click.option( "--filename", default="processed-{:04}.png", type=click.Path(), help="The format for the filename.", show_default=True, ) @processor def save_cmd(images, filename): """Saves all processed images to a series of files.""" for idx, image in enumerate(images): try: fn = filename.format(idx + 1) click.echo(f"Saving '{image.filename}' as '{fn}'") yield image.save(fn) except Exception as e: click.echo(f"Could not save image '{image.filename}': {e}", err=True) @cli.command("display") @processor def display_cmd(images): """Opens all images in an image viewer.""" for image in images: click.echo(f"Displaying '{image.filename}'") image.show() yield image @cli.command("resize") @click.option("-w", "--width", type=int, help="The new width of the image.") @click.option("-h", "--height", type=int, help="The new height of the image.") @processor def resize_cmd(images, width, height): """Resizes an image by fitting it into the box without changing the aspect ratio. """ for image in images: w, h = (width or image.size[0], height or image.size[1]) click.echo(f"Resizing '{image.filename}' to {w}x{h}") image.thumbnail((w, h)) yield image @cli.command("crop") @click.option( "-b", "--border", type=int, help="Crop the image from all sides by this amount." ) @processor def crop_cmd(images, border): """Crops an image from all edges.""" for image in images: box = [0, 0, image.size[0], image.size[1]] if border is not None: for idx, val in enumerate(box): box[idx] = max(0, val - border) click.echo(f"Cropping '{image.filename}' by {border}px") yield copy_filename(image.crop(box), image) else: yield image def convert_rotation(ctx, param, value): if value is None: return value = value.lower() if value in ("90", "r", "right"): return (Image.ROTATE_90, 90) if value in ("180", "-180"): return (Image.ROTATE_180, 180) if value in ("-90", "270", "l", "left"): return (Image.ROTATE_270, 270) raise click.BadParameter(f"invalid rotation '{value}'") def convert_flip(ctx, param, value): if value is None: return value = value.lower() if value in ("lr", "leftright"): return (Image.FLIP_LEFT_RIGHT, "left to right") if value in ("tb", "topbottom", "upsidedown", "ud"): return (Image.FLIP_LEFT_RIGHT, "top to bottom") raise click.BadParameter(f"invalid flip '{value}'") @cli.command("transpose") @click.option( "-r", "--rotate", callback=convert_rotation, help="Rotates the image (in degrees)" ) @click.option("-f", "--flip", callback=convert_flip, help="Flips the image [LR / TB]") @processor def transpose_cmd(images, rotate, flip): """Transposes an image by either rotating or flipping it.""" for image in images: if rotate is not None: mode, degrees = rotate click.echo(f"Rotate '{image.filename}' by {degrees}deg") image = copy_filename(image.transpose(mode), image) if flip is not None: mode, direction = flip click.echo(f"Flip '{image.filename}' {direction}") image = copy_filename(image.transpose(mode), image) yield image @cli.command("blur") @click.option("-r", "--radius", default=2, show_default=True, help="The blur radius.") @processor def blur_cmd(images, radius): """Applies gaussian blur.""" blur = ImageFilter.GaussianBlur(radius) for image in images: click.echo(f"Blurring '{image.filename}' by {radius}px") yield copy_filename(image.filter(blur), image) @cli.command("smoothen") @click.option( "-i", "--iterations", default=1, show_default=True, help="How many iterations of the smoothen filter to run.", ) @processor def smoothen_cmd(images, iterations): """Applies a smoothening filter.""" for image in images: click.echo( f"Smoothening {image.filename!r} {iterations}" f" time{'s' if iterations != 1 else ''}" ) for _ in range(iterations): image = copy_filename(image.filter(ImageFilter.BLUR), image) yield image @cli.command("emboss") @processor def emboss_cmd(images): """Embosses an image.""" for image in images: click.echo(f"Embossing '{image.filename}'") yield copy_filename(image.filter(ImageFilter.EMBOSS), image) @cli.command("sharpen") @click.option( "-f", "--factor", default=2.0, help="Sharpens the image.", show_default=True ) @processor def sharpen_cmd(images, factor): """Sharpens an image.""" for image in images: click.echo(f"Sharpen '{image.filename}' by {factor}") enhancer = ImageEnhance.Sharpness(image) yield copy_filename(enhancer.enhance(max(1.0, factor)), image) @cli.command("paste") @click.option("-l", "--left", default=0, help="Offset from left.") @click.option("-r", "--right", default=0, help="Offset from right.") @processor def paste_cmd(images, left, right): """Pastes the second image on the first image and leaves the rest unchanged. """ imageiter = iter(images) image = next(imageiter, None) to_paste = next(imageiter, None) if to_paste is None: if image is not None: yield image return click.echo(f"Paste '{to_paste.filename}' on '{image.filename}'") mask = None if to_paste.mode == "RGBA" or "transparency" in to_paste.info: mask = to_paste image.paste(to_paste, (left, right), mask) image.filename += f"+{to_paste.filename}" yield image yield from imageiter if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/imagepipe/index.rst000066400000000000000000000015101515242076300240630ustar00rootroot00000000000000.. typer:: imagepipe.cli :builders: latex=text,html,svg:html=text,svg :show-nested: :make-sections: :width: 65 .. typer:: imagepipe.cli:sharpen :make-sections: :preferred: svg References ---------- Cross references: * Check 0: :typer:`imagepipe`. * Check 1: :typer:`imagepipe-blur`. * Check 2: :typer:`imagepipe-crop`. * Check 3: :typer:`imagepipe-display`. * Check 4: :typer:`imagepipe-emboss`. * Check 5: :typer:`imagepipe-open`. * Check 6: :typer:`imagepipe-paste`. * Check 7: :typer:`imagepipe-resize`. * Check 8: :typer:`imagepipe-save`. * Check 9: :typer:`imagepipe-smoothen`. * Check 10: :typer:`imagepipe-transpose`. * Check 11: :typer:`sharpen `. Contents -------- .. toctree:: :maxdepth: 1 :caption: Contents: references sphinx-contrib-typer-8982731/tests/click/imagepipe/references.rst000066400000000000000000000012071515242076300251000ustar00rootroot00000000000000Typer Role Test References ========================== References ---------- Cross references: * Check 0: :typer:`imagepipe`. * Check 1: :typer:`imagepipe-blur`. * Check 2: :typer:`imagepipe-crop`. * Check 3: :typer:`imagepipe-display`. * Check 4: :typer:`imagepipe-emboss`. * Check 5: :typer:`imagepipe-open`. * Check 6: :typer:`imagepipe-paste`. * Check 7: :typer:`imagepipe-resize`. * Check 8: :typer:`imagepipe-save`. * Check 9: :typer:`imagepipe-smoothen`. * Check 10: :typer:`imagepipe-transpose`. * Check 11: :typer:`sharpen `. This one is bad: :typer:`bad-reference` sphinx-contrib-typer-8982731/tests/click/inout/000077500000000000000000000000001515242076300214235ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/inout/index.rst000066400000000000000000000001301515242076300232560ustar00rootroot00000000000000.. typer:: inout.cli :builders: html=html :markup-mode: markdown :width: 65 sphinx-contrib-typer-8982731/tests/click/inout/inout.py000066400000000000000000000014401515242076300231320ustar00rootroot00000000000000import click @click.command() @click.argument("input", type=click.File("rb"), nargs=-1) @click.argument("output", type=click.File("wb")) def cli(input, output): """This script works similar to the Unix `cat` command but it writes into a specific file (which could be the standard output as denoted by the ``-`` sign). Copy stdin to stdout: ```bash inout - - ``` Copy foo.txt and bar.txt to stdout: ```bash inout foo.txt bar.txt - ``` Write stdin into the file foo.txt ```bash inout - foo.txt ``` """ for f in input: while True: chunk = f.read(1024) if not chunk: break output.write(chunk) output.flush() if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/naval/000077500000000000000000000000001515242076300213665ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/naval/index.rst000066400000000000000000000001751515242076300232320ustar00rootroot00000000000000.. typer:: naval.cli :show-nested: :make-sections: :width: 80 .. typer:: naval.cli:ship:new :make-sections: sphinx-contrib-typer-8982731/tests/click/naval/naval.py000066400000000000000000000035531515242076300230470ustar00rootroot00000000000000import click class AlphOrderedGroup(click.Group): def list_commands(self, ctx): return sorted(super().list_commands(ctx)) @click.group(cls=AlphOrderedGroup) @click.version_option() def cli(): """Naval Fate. This is the docopt example adopted to Click but with some actual commands implemented and not just the empty parsing which really is not all that interesting. """ @cli.group(cls=AlphOrderedGroup) def ship(): """Manages ships.""" @ship.command("new") @click.argument("name") def ship_new(name): """Creates a new ship.""" click.echo(f"Created ship {name}") @ship.command("move") @click.argument("ship") @click.argument("x", type=float) @click.argument("y", type=float) @click.option("--speed", metavar="KN", default=10, help="Speed in knots.") def ship_move(ship, x, y, speed): """Moves SHIP to the new location X,Y.""" click.echo(f"Moving ship {ship} to {x},{y} with speed {speed}") @ship.command("shoot") @click.argument("ship") @click.argument("x", type=float) @click.argument("y", type=float) def ship_shoot(ship, x, y): """Makes SHIP fire to X,Y.""" click.echo(f"Ship {ship} fires to {x},{y}") @cli.group("mine") def mine(): """Manages mines.""" @mine.command("set") @click.argument("x", type=float) @click.argument("y", type=float) @click.option( "ty", "--moored", flag_value="moored", default=True, help="Moored (anchored) mine. Default.", ) @click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.") def mine_set(x, y, ty): """Sets a mine at a specific coordinate.""" click.echo(f"Set {ty} mine at {x},{y}") @mine.command("remove") @click.argument("x", type=float) @click.argument("y", type=float) def mine_remove(x, y): """Removes a mine at a specific coordinate.""" click.echo(f"Removed mine at {x},{y}") if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/repo/000077500000000000000000000000001515242076300212325ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/repo/index.rst000066400000000000000000000001121515242076300230650ustar00rootroot00000000000000.. typer:: repo.cli :preferred: html :show-nested: :width: 65 sphinx-contrib-typer-8982731/tests/click/repo/repo.py000066400000000000000000000114431515242076300225540ustar00rootroot00000000000000import os import posixpath import sys import click class AlphOrderedGroup(click.Group): def list_commands(self, ctx): return sorted(super().list_commands(ctx)) class Repo: def __init__(self, home): self.home = home self.config = {} self.verbose = False def set_config(self, key, value): self.config[key] = value if self.verbose: click.echo(f" config[{key}] = {value}", file=sys.stderr) def __repr__(self): return f"" pass_repo = click.make_pass_decorator(Repo) @click.group(cls=AlphOrderedGroup) @click.option( "--repo-home", envvar="REPO_HOME", default=".repo", metavar="PATH", help="Changes the repository folder location.", ) @click.option( "--config", nargs=2, multiple=True, metavar="KEY VALUE", help="Overrides a config key/value pair.", ) @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.") @click.version_option("1.0") @click.pass_context def cli(ctx, repo_home, config, verbose): """Repo is a command line tool that showcases how to build complex command line interfaces with Click. This tool is supposed to look like a distributed version control system to show how something like this can be structured. """ # Create a repo object and remember it as as the context object. From # this point onwards other commands can refer to it by using the # @pass_repo decorator. ctx.obj = Repo(os.path.abspath(repo_home)) ctx.obj.verbose = verbose for key, value in config: ctx.obj.set_config(key, value) @cli.command() @click.argument("src") @click.argument("dest", required=False) @click.option( "--shallow/--deep", default=False, help="Makes a checkout shallow or deep. Deep by default.", ) @click.option( "--rev", "-r", default="HEAD", help="Clone a specific revision instead of HEAD." ) @pass_repo def clone(repo, src, dest, shallow, rev): """Clones a repository. This will clone the repository at SRC into the folder DEST. If DEST is not provided this will automatically use the last path component of SRC and create that folder. """ if dest is None: dest = posixpath.split(src)[-1] or "." click.echo(f"Cloning repo {src} to {os.path.basename(dest)}") repo.home = dest if shallow: click.echo("Making shallow checkout") click.echo(f"Checking out revision {rev}") @cli.command() @click.confirmation_option() @pass_repo def delete(repo): """Deletes a repository. This will throw away the current repository. """ click.echo(f"Destroying repo {repo.home}") click.echo("Deleted!") @cli.command() @click.option("--username", prompt=True, help="The developer's shown username.") @click.option("--email", prompt="E-Mail", help="The developer's email address") @click.password_option(help="The login password.") @pass_repo def setuser(repo, username, email, password): """Sets the user credentials. This will override the current user config. """ repo.set_config("username", username) repo.set_config("email", email) repo.set_config("password", "*" * len(password)) click.echo("Changed credentials.") @cli.command() @click.option( "--message", "-m", multiple=True, help="The commit message. If provided multiple times each" " argument gets converted into a new line.", ) @click.argument("files", nargs=-1, type=click.Path()) @pass_repo def commit(repo, files, message): """Commits outstanding changes. Commit changes to the given files into the repository. You will need to "repo push" to push up your changes to other repositories. If a list of files is omitted, all changes reported by "repo status" will be committed. """ if not message: marker = "# Files to be committed:" hint = ["", "", marker, "#"] for file in files: hint.append(f"# U {file}") message = click.edit("\n".join(hint)) if message is None: click.echo("Aborted!") return msg = message.split(marker)[0].rstrip() if not msg: click.echo("Aborted! Empty commit message") return else: msg = "\n".join(message) click.echo(f"Files to be committed: {files}") click.echo(f"Commit message:\n{msg}") @cli.command(short_help="Copies files.") @click.option( "--force", is_flag=True, help="forcibly copy over an existing managed file" ) @click.argument("src", nargs=-1, type=click.Path()) @click.argument("dst", type=click.Path()) @pass_repo def copy(repo, src, dst, force): """Copies one or multiple files to a new location. This copies all files from SRC to DST. """ for fn in src: click.echo(f"Copy from {fn} -> {dst}") if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/termui/000077500000000000000000000000001515242076300215725ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/termui/index.rst000066400000000000000000000003151515242076300234320ustar00rootroot00000000000000.. typer:: termui.cli :preferred: html :width: 65 :make-sections: :show-nested: .. typer:: termui.cli:menu :preferred: html :width: 100 :convert-png: latex :make-sections: sphinx-contrib-typer-8982731/tests/click/termui/termui.py000077500000000000000000000103541515242076300234570ustar00rootroot00000000000000import math import random import time import click class AlphOrderedGroup(click.Group): def list_commands(self, ctx): return sorted(super().list_commands(ctx)) @click.group(cls=AlphOrderedGroup) def cli(): """This script showcases different terminal UI helpers in Click.""" pass @cli.command() def colordemo(): """Demonstrates ANSI color support.""" for color in "red", "green", "blue": click.echo(click.style(f"I am colored {color}", fg=color)) click.echo(click.style(f"I am background colored {color}", bg=color)) @cli.command() def pager(): """Demonstrates using the pager.""" lines = [] for x in range(200): lines.append(f"{click.style(str(x), fg='green')}. Hello World!") click.echo_via_pager("\n".join(lines)) @cli.command() @click.option( "--count", default=8000, type=click.IntRange(1, 100000), help="The number of items to process.", ) def progress(count): """Demonstrates the progress bar.""" items = range(count) def process_slowly(item): time.sleep(0.002 * random.random()) def filter(items): for item in items: if random.random() > 0.3: yield item with click.progressbar( items, label="Processing accounts", fill_char=click.style("#", fg="green") ) as bar: for item in bar: process_slowly(item) def show_item(item): if item is not None: return f"Item #{item}" with click.progressbar( filter(items), label="Committing transaction", fill_char=click.style("#", fg="yellow"), item_show_func=show_item, ) as bar: for item in bar: process_slowly(item) with click.progressbar( length=count, label="Counting", bar_template="%(label)s %(bar)s | %(info)s", fill_char=click.style("█", fg="cyan"), empty_char=" ", ) as bar: for item in bar: process_slowly(item) with click.progressbar( length=count, width=0, show_percent=False, show_eta=False, fill_char=click.style("#", fg="magenta"), ) as bar: for item in bar: process_slowly(item) # 'Non-linear progress bar' steps = [math.exp(x * 1.0 / 20) - 1 for x in range(20)] count = int(sum(steps)) with click.progressbar( length=count, show_percent=False, label="Slowing progress bar", fill_char=click.style("█", fg="green"), ) as bar: for item in steps: time.sleep(item) bar.update(item) @cli.command() @click.argument("url") def open(url): """Opens a file or URL In the default application.""" click.launch(url) @cli.command() @click.argument("url") def locate(url): """Opens a file or URL In the default application.""" click.launch(url, locate=True) @cli.command() def edit(): """Opens an editor with some text in it.""" MARKER = "# Everything below is ignored\n" message = click.edit(f"\n\n{MARKER}") if message is not None: msg = message.split(MARKER, 1)[0].rstrip("\n") if not msg: click.echo("Empty message!") else: click.echo(f"Message:\n{msg}") else: click.echo("You did not enter anything!") @cli.command() def clear(): """Clears the entire screen.""" click.clear() @cli.command() def pause(): """Waits for the user to press a button.""" click.pause() @cli.command() def menu(): """Shows a simple menu.""" menu = "main" while True: if menu == "main": click.echo("Main menu:") click.echo(" d: debug menu") click.echo(" q: quit") char = click.getchar() if char == "d": menu = "debug" elif char == "q": menu = "quit" else: click.echo("Invalid input") elif menu == "debug": click.echo("Debug menu") click.echo(" b: back") char = click.getchar() if char == "b": menu = "main" else: click.echo("Invalid input") elif menu == "quit": return if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/click/validation/000077500000000000000000000000001515242076300224175ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/click/validation/html.png000066400000000000000000003032741515242076300241020ustar00rootroot00000000000000PNG  IHDR:%y6iCCPICC Profilex}KP?KA.C.j[?jѱUni֐Ft&Ipqqx )Hy>-jdԏO&05SMg 55´ O?*cuuuXjtgbڵygf@9s+ W[[ ǎÇ2dFcbԩ/,,ij> aaa2d ___ٺz>(lْ>}0uV|Wgx7uV۷ƩS=zQ *~OoKEFF?jxbɼC}I{8<.]$ DBBFٳg#)) *e=jk۶-rDDDDDvpzK@aa3ξ (g0#jfRSS1k,ɼɓ'wmQCbؽ{l٩Sp)x[o)pt:֯_+@a`Gvv$ w%EEErFoɖ!-- 9֮]7"88X?'| ̫r/iCYY{ѣGѵkWy""(++Ծݻ7QjdѢEjKbʔ)HNN߿dOmۆGBn>;w`Xruۇ{۷oGLL]/t ,\?磴:{dڞMDDDDDr穇E|||pEIM6Ij8C޽e{PZ5N`` o~cW0dժU{O4$y2N[oNٟ;YơL/_z m۶u(]}C8{-ZH=x _!""""j#B}?Sb.\pgaʔ)NoKc>8̙3Xv-$?rx饗PSS\:uJVodDFFBRaܹn JKKsNٶJ}<[2ݽ{w9$L3;"""""jf3())G``bZlKJFYYhNDD􂷷cͰjk+pO]9K< }TFZ(((;wо}{h45uF˨}cfj ___ZGܼyYYYHHH@hhSnW  ;w.Ə/Y8q fuݻwǟ'ah4/P 楦"22jZ1hɼx|V3f3$z) \a7ے3g7m߾&MHHH2+6 +Vp( WUU~I2/00@XX0o߾3g~O>oigQ4?aǎ? :AAɓ߆Ol^_7…ȸlq>///E^CϞkܾ}= |& |={Wm3g zMS_q5l %T*BB4xC/ܛo (/c͒y}&O,LkZ޽Md&;w.~_|vg}Vիx$bΜ9x=Q8똙>u6oެAcw{Tݻ7Ow󳸏={JX۷aI0wXnkw=h?$q0||גVXa3 g ?8-[ǏK*D+V͛;wXqmtd󢣣%8vW_ŋѮ];>l%\TލZXy7n͛m0((ڵ/㮻c"/_׿&kRŅ$ UU(kׇ;.../ĠA0eʻP>˗OFVZ7HS͛r˗/̃9^y>_~%oߎ#G68֭[6V2YrQL4Iv}ʕ+v;??ڗ'˓ DϞ=e.HԜNkM-##C6OtM8n@azv_^\v퇈\Y]&N8p@XE:Nq KwI6@ ڵó>+L_~+m^^^>}l~YY{1DDD7@jj]#"""""k"%a]>#̟ ^x^$ff3.?F ^|8~17կboeYYr:\tD_???{ߧbƌ?ɖ[i qxx饓X~㈎?}svNSS ٳG6OiӔ܋dddܹsoo?ĕ+Wo~\eƍsgô^?$''۶msx8fk֬|cǎɶ:udߚ5kcbc0 Q<7o8\zyyyPArsrj4u;J`@AAۇiӦ)nTX *54U鸋-\P24 %-.+++[o^zaҤI8rD~/#"""""j9p玼ɍo&N| )]W'w{DG@HH{Y 5zub%K 8~taȇ9\v^6Uhmdl)2رC2]^^. Cz oGtQ3|Aټ[n9QTPՒ?GݸqC9֭[6m`ꫯާSt jbmOK7| j*L4 ^^^~+<Ӳ/_ngoCnIII5ǵ V\RFk/Z^^^FDD>ŀkDD Pο=ۦ lz$azʕjv_~XVֵΝ;1j(<k4A|||P[[+LF,[60`yډG\Tjtq𿑟"TTI\)ZRbyѝ,lԨp7V_aal^۶mB;3"((:VXh4B'w-y?rJJJо}}ò͛?SKoq-dgg###\:ɑ߿? "7k,@VV,YҠ}4KȊ JO9fAAAx'SNyy}Z->s+zwl6?&""""sz@GCkGTҊ{xmh^qsG|}Z>՚jh۶-m>\{{˛svm7[4s.]l6mPOYZ  7|j=z`ȑ&4rRsjzCp̔k#*SאC3Rsir`&qqq8t94t:F;w/枞[yKJJW:Ԑ\2_ص`rk2||ϛ׽EHJzmֿ5eeh:Ky%%#*VV*ט0HdCn'u=9sfșHOOW?NiJZJ6?..Vw-azŎ-Q n5E&66V6O`9bc)RMC۷]cbb&ODAA%׭R-Cs5jT遺t",,Xɷ =z/+֯_O@@>#lܸQbsiNDDDDԜ9= t+//`` yyd*$;g׏X[[K6ĉ͕>T NF j=GF6rM: Ty0)DE iKYUU%Ji㇀jh8ԩSH}pɼ@1B+K=Z2VC%J}j5d? ͫÉ'S(5;ud5nܸ!1ػwo5Z"K9rD?xlݛ gQ y5558q#6uزuW_ټb\VX[Jի;9Ics;Wӧ,puVYqSlRhitB):J)v Jعs'233%~'Y-s>fǎs1k߾)aff&v%[W_>kSTxe}]I^-yS)}˗KO^8HO?Y۽nii)pBdd(61"#5Ku{Gز嗸y^݉?7oN~:˖Mիj.y}III!6lѺvXoǎiӳ8~c?cQP`!P_SMy_<۷+ns1Xq? ؿ6?͟?_2eY͌3f(nkCQ2]TTM)vuOBUUk]*S5|gVV-Zds_>fJ'cٲerh"\rE?5k+汞z)ټ#GÇc̚5Kv*6+Tϟg}&SjrJ:JL6Mv}a̙رc9zJ64,^ءt#QkժUܹ3~_ѣBswш ._VL[BBCCDDDDDszU3Ell~rv$'[O6 Ņжm[tGdd"eɑtɼB|  ֖<=ls{q^a2gX|d^UU>GHHT*ohyp{#W:u*X]Ǽ)Iv/w*++1qDܸq6=BCC1h YYo&|MIW\v0h ző*0w\;mӿ9G_jb߃&/ͮ<Ɏ;_IEcǎɓ%|||K/7SN2dHu'x7+jwP///X6""ݺus31.!7p#)k8MOŪq;w'7)w{yIq7o^c?CBHrK z%).@V ܾ ϛ7OV/,WVV}:xmLXzu~\}~xLS/"yǾ<%00۶mkn)F#G(66׿8qBqT$&&'ODyøYmڴ+Oj$1iҤmKDDDDDqY@cxc'* ݷ/p 8;$%K6CsWb„XڵkވHy8ooymܹ`РIBxx Y£n@EV]p|ma֬exɯSJkbzM y#Fɓ?~@<8x bbbQsX{C`رcqq9Rq΅ *2^'Nn#%%bR"""""r>/УNgDAA*n> 궈6+(«(-FEET6 D]څٵ99琛{hѵXA[o 5i|p{Zm@pp,; <[2#==ٳ'__ߦNSF\v ?3ڴiAj-̤.]µk>}{-y/ޓk)?qeBRaܹRrݼygΜ`@BBzդٲ2dffχhDLL 8B% 4]hDDDDD$妀\˵j,DGȑK'|38~d~Tw%Z ӱi&%Z{ +W9"""""4T&+*_#GFc:ܼymڷm'`@Ή Q\\@yG3?pOj#vؽ~>0{ ZZ\hЩS'7ZLX\87yb@>pg/xwREDDDDDDDD9'Gz\@EE!*+xA錰nO@.w˻JDDDDDDDDM9"""""""""7R5uZ䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7R7u_Wܹk"&&Q/_̚5 {o'i&[0l0tھҞ={?'|SDDDDDDDD|N F,\555Ç㥗^k'|7pf$.^CY\^VVذaܹSrϟڵkUUU-& wI|)S0 GDDDDDDD-Sr^^^HOO+ w XBڵ3%ӥK[HKKsg$*ox饗8#"""""""r6m~~>\bsH'OdI[HIILDOzz:.]K.!;;CDDDDDDDԬ8I&᷿0O?gϞVٽ{`` ,~p]w5qj3fSN;6qj\ ""mK,`-[ɓ'rѣG7u^zW^M """"""""rz@NRa̙ صkz=j叺xdS V\AѠGu 555x"j5cڣ3~4֢[nh߾}YSSs -9= r3pY 2DqKǍ'[ öm۰aX޽1|p s·ߥKh"_yu~G,]T2_ʶS8{,ڵkg1FBmm1cmz;Xz5.]$Y'wޱ>}~u_`Νug̘+V8%GDDDDDDDKr5o1 '?nȐ!o><V?4-[?`ذa'JKKq)˳_<3eeeزe Ξ=4cj%8%Yȑ#EDDXV,77 ,}gffbŊعs'; :Tq=wUXn͛sԩSh4v% L8QUgk? ӧOk=z@^еkWTVV… Bp'??III8}4oc[pp0F)g-dƍeѣG#!!gΜS+W:;bʔ)yaSThӦ4}0 իWkYc00qD?^{m۶ʼn'>07l0dff"66~҄`\\\jOw~:-[_ݡ4Q+gt>@sA:OV֭[7n(sc``۝bI:RSS֒c ?is}`4hM``رcuI:O?Wӧ Ok{;eټyxL2Ev/_Rq_WF^/suc=DDDDDDDD\3oVy1:"""'"//_ѹsguF~[-Ovލda?GRRdy7ߴkFw{7SO ]͈g̘uӟ|5kQ+㲀\Ϟ=/LOuo.?uTTQcƌL;w[S۲e``f !Yff>kO?-6LDɫ8ҤI\""""""""2qIr&3f?Om?YXj%SfI]]rss,hZF#7+͍7g͚___:t耤$ɱ۷oK|A4qqqddd燄H mĥɓ' d!44|ЃcZWee%VZ/BPի:tnǎ ȵcv-tLLufw@.00PU,""B4=\3y1c`޽c"88~rrr0tPJ&ͩ?4H@}['u3q@.00f]qvu8"""""""" \k۶-f͚o>! ? M6>F#O. ,O=tHonilLGMz-'_K?fdN<8Ý;w""""""""4 <۶m>CdffJjM8OƩS_+WjB=cRZZju}[c75)++CmmhT"B<;< 7~xׯ͛㑐`qݻw I0gOGJk5Eͮ] f`g=q.],]O0`~_-DDDDDDD}tX_>x #LO>,6ܺuk$k~SN[nE]]zEEE8t} 8viqtILq:W_kJāS"""""""j=\3g ޽۶m'MduHk߸q~a% pqML8p@qkܗ8xxyw;"..Ncq?\2tY㖀իW#??_nۧOK.aʕu\xQilo[ ʴi$͛7%=z?}ræMR(L'''㥗^5 ]~= sE^ܕL"""""""" !wuQVV&?}tYvԨQx=oߎqΝ;HNNƊ+_]Rjш`Ĉ:'TaڵwCxx8o[<k 6 sE\\RSSkJ\}̦O~Z1cz={" @}|} xe} :u K.3|IG}}?~&MJ¹sAEL~9&-2-_ܮmm&V/dɓqVi&6|]iSKIIK_y'O,LO>Iه~hsߥ&''7}駊kիW[NWѣ!a(eرV#""""""-MV͛0a]N<F޽46lk&V;V'@rr2}QRڟCam[ooo@سgF%S=8px +`9fJKLLDrr2y 2/_Ƃ ڷ >ֵM6V#""""""h4:HOOիWйsg$&&JFl) ^C ``tΝ;HKKåKPWWx[hJKDDDDDDDԚU@s[U"""""""""b@ȭ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#䈈܈9"""""""""7b@ȍ#""""""""r#uS'ǎÕ+Wz°aÜF7n`?!!!Nj=.^[\~}wnLQRRRիW[\޳gOL0QQTT}ڴi|Q3 Xf ӦMFqT\p#F4qcǎ 6 z{[g:\pyyy(**BmmlHLLthu QYY)̏ɓV" //۷o`M"rZ-6nGo޼7o]3\`С QhZY\ܘ֧kghZ]vM`@Z !-..n5W Ν;;s1_ v***7 33Sqyii)rDTrr2~g51N'@rj@nr.KY*̒ʎUUP Ǘ_~ Ƌ/oo&N9sF~SD~uuuVqڶm`8K7o={\rz^XC N8'NH4O\\fΜܗ_~l@߾}1uT|'5;l01GJJ RRRwPT ERRRhݸq#222j v﫶6mBZZǠApڵg3ojy풒0filL$f {Ϟ=b@߾}1rHpׯ_t#Я_?lْܸ'Z%h4͛t.]Q1i$yءCpa@||<z!ܺu ;vaJC۶moiZlݺojcȑG,--,w"oSy:+߮û+E!<<lj'$y aĉ޽l_x=|<ȤK.vh?Aa.;:qIIKV[nfY8]x[l_K'OĹsdۍ1Bvj5 ZM'^O@ @~̖.]"ݻoߖS0|(˕HII?^:}YY>#agm(`ʕ?Ν;[MN'|"LO>{W[[Gòsʤs4ho3>;Kaa!6n܈\ᗹΝ%'|Y^^.Y^zaʔ)vZ-݋ .(.Wٳ'.۶$2 wI:;s OZGII@&&~gQ[,)//ɓ'&nv-S\\ #Gĸqd G W^_~Y[$ǡ[n`ٳf:'N@zz:.\vK9l޼YWF۶men¶md UXX+W*VUU"77su[o- nݺ>fB>}\HܸqpYVز%YzA/  e\~3fpkyyyV {t:|C΃u̴Z-jkk-¤g7621Xr$??C߾}\I 8zjXnƍQF)JϒZdeeaŊ8q"n3eeeBL_^q]SNo߾~\<569<6mdq劊 ڵ W^ܹseKzsrkתm( ޔaU\… 7FHHt8ȩR(,sF9(22uuu5wq3gHyg@GI3%7oDvv6rN{3]~WV"11Ѯ}zl޼b`0 77~)&M$5kvv9\v .p_|a3)))rf N'y#ׯ[no^R4 V dffʂq‰YTT$Tt5kڵkyQQQGqq1rrrLȑ#h۶-n>>>xG@}~z<#fas̱+AAABurdee 7"|xlFPZZ*Emm-j5zꅌ }E5L݁f:t@uuKZZ6oތYf=|~AǎQ[[L!(xIR2X{ZEȃL>,ySTGQ]]B8lr@}"ك jFh[TT$ѶsN]2M6 ^J<)>vv)h*dff 7nڵkcI5...\ߖ* ܑoرC1=VTTXX\\l>}0{lNYY>S… 8qGٿSNd~믅6n܈em#"",dgϞ3B!S-1j(WV㮺7nDZZ\u=c kd7[.^( ukee%>3^8͛7O2}mXB~ꩧ<f$$$O?I%PW;wGmѺML" }M|W޽{uQF)/..Ɨ_~)\w¨QnN(w}:tW ?۷oWJ޽[C=] z=l"4@ZZz!%KHO?᧟~P0_n;mS@`ܹjرcbƠܹǎk~tR\xQO?-<Flذbm>Kq̺u&{Yl2;k_ξaѢE `0`޽:tm6Ӓ!&M>}xp=((U*F%ij AϞ=ŅIJ%Eo۶$s &O=d=k}:V0k,ٛN:atQQzGHKK<֭[Rf < {9 0@1жm[̞=[RՌӘ@؂ $ߵ]vXxc<`0H{G<kK </4:t[Ν;cڴi´NSl֮];xG,61=(z#|I}ݒJkɚxjcI'<Ō3$5enj5m|k׮x%_`޼yx%7əc̙4&. R,{lw XSy/3fM6I;]~~gϞ-zjH ݺu.׮5[3 YVoL%tɓ'%浴:v(y{:Mjjz⚤~~~;촧d{kxN'9 XS]vv9ceW^O΃ LHͥW.]srr$yN?X֔/??_4/C:$ 0we2x`!%L55M,ݛufW+WGGG#<<\qJʎ{x`ƈ}ݒ:kjj5ivMVCBB,op:SxxI.--ž}0jԨFW6PC)..iNC]]ba4ާ~ 7DCB:Xkþ ^f+ g3~ XI{T*gZ,,AAAj$''˚ IJjjQZZ*!LUg0oj0`%r̜y{{Oxx9wv>}x0~Քj KJJl\$O6v=Oɷp) 999.k|;!!QLRv4DZ}F}Q~t:8p@Rs[mv7k73?-`0{[q*wuPK͛k׮I-UW6޽{ngO g-?sfiyPbb"= >6lƍ]"..;w/5WլOgiD8ш۷o -m /^trAArʷuaMsSʎMzWxooˋI/nݺ% ȉ 5[ŵĜUR8ppKNNĉegDGG} BDDPtXb|}}ѥKt;vDLL]iA,`r[/kzv|ۚZqqCy$=z{!88]tANйsgPkr z4T׮]$6i|a~ll,|AHɼvģH*6l؁P^*Ufkֈ-EGRj.;73; }4[NIITq#h{nIui{~z:󀜵yرcq I,nޞT*<_(UJ5b rd$ņ@vcGuk w 6ə}S>^jq Ɲ;w$#^aɒ%Z!Oͷkk4Ŏpgݘ_O-y-Z[M +++]f'O̓<8j%/nU*~XO4uU9())Ih*IFW0`]____;cǎEYYn߾WҥK7nŋfi߾PFiYRVVf8V%u贼V`6mp-\rW^/ك~>s2ooo"66FBaa!VZ%t(--u)hg߶lܸQ9C-&-- 'Oع) [nV,5lc ZgݐZ ֪ ~ZϞ=%}\pIII7111S]]㭾9nhx₝+Yb^P/--Doc̃L|}}xL0iiiXvpz\@Nnݺ%)LӒcbQm&9`pxIg3`VvU-T |999\VW^trr2jkk%瞵ͿSTTumޠ,AbT2bsLy0ȑ摮ʃѣƍ'LWUUabh4 Hy_E磵Fw%Oͷ,9po[ SKR$_Y^AZO\9Sc& Ez<ʕ+y󦰮yq,H^>:uJR;NX<{T*s=eOŸyg[ӝI\*Sr \zUVHA<͛'MY!0 [sΒ{۶mvmg0,$P_üo#Kb) l8qBq~UU$HzqZ0FYL3ڹk׮PZx...v{Pߖx.1^/i\JózxkL2E...ի->(aϞ=x&S՘?P8ǼyX~g&]pA6ŋ~wLOywU$ƌc>7N}"..NnGw}zKuE]Jj<޽[`0`ǎ+^~[o^^^=z0]\\۷K֩իm\}ڶm+Iر`r޽{-4%iM@ؿ?]&W\\N(n [l UUUXrk6xrJ-[fvmm-N:6lo۷o{bOηW_C۷ݘ*ד+ gj=k@}!SF6QT.Zbh6OII~ӧO[,k޼yS_ش&Fz^4;SHH|_Λt%_|hNN6n(LjŠȑ#%Ԇ p5HKKwyGq[bʕHKK{I@5^cǎ[0{nY&x@2 8w4 bbbmۢɒb 6G"uD=zTڵ+"##V+1Ubٰa=w\H}>|+==Ǐ (y駟AYY222l6O?ʕ+СKK.VKLLľ}yA>|AAABAi{$|p1/T}.((@NN_yyyaoz5W5ILLġCu}] 2!!!(,,DZZZFA>}jx뭷$ ł dn޼YV2ԩ7h ޽[׉'pMt FW\9ـ~ AAAy߾}6gAUUU8t:DFF"""^^^%,VBnN+WH s 0P_Gbb"|}}+W4]Oη5 D:~):uHkeeeCvvp@DEE!88XΞ= . ,,LRuܸq({jדNS"N(wut9CXXddFBBPӳpϽ\9[c. >2ETTC_ ;wJm۶mmm-uVl۶ :uBxx8P[[7n^S^nɶonPLH慇瞳ӝi̘1n ֭['pekEEEI)=z ((HOO\oGV> V^ >wCpp0QPPL!a)QUU%4EΝŋ&l[Vx"){PTXqq7q1ccm{ 7n޼ڎۏrII!C};?~<Ӆ;vcǎ;wFbb 2b5^WE6mP^^ŷ5kCUT3gV^-dAүR_Xx1֮]+[UUq 4HT*ɛ Kƌ &>>>opMI&!33SR@,5(7~_/΁}஻jg4DDD a0w^߾}1|pz!|BIjV &M6CPlh~/zY+ޛKISii׉n6YfaʕgΜ:5b[1Uy5,^^^С!ꛭZK. 8P2dC7M&냷qd$\{k5LQQQ:u*l"̻v,@۩S'3gbwXmˡ@}*gƦM甂7W\?Jc=*k㻵ɪ#[kXZmwʚS3f {9]vxG0{lY]7..2I$FI&Y܇J‚ $Ii0i$ODD/^,y[k狽璿?,Ybq={gGΝ/o߾U-QQQXt)FeuuJ̙3}EGGˎI>}T*,YbqdJnݺ_T;wO@,]#F@DD}4G^XJHH?l]}ڵk_~#GDhhygmldҶm[$%%Y$((fٳ*:u£>ϫT*;g϶xCBB#..Nq/OHK'* w~i55 ^L8Ѯtu=omgIScصVtFљLe KKXX,Y"ZJ3u۷%x=sFd-3א|tgjg8vu9OkIп:'{dIgS=IC1|ӝiXpb2JKZLYKW@@/^#FX\GVc֬Y1co߾x饗,V1#(.OLLDnݬ@}_l@)͕!hCii)t:T*Ѿ}{<ݡFhf05PhR[[>111q\rDDDDDDDDDnĀ1 GDDDDDDDDF  .bcc1bw'c}w(++3:uj.7n۷ӛ8EÎ;pU.\`uE߿{k_ׯGEE`񈍍kŋ8}4@`ڴiMرcr W^6lXuaMDDRRRիW[\޳gOL0-_UUU;FOYY6n///)77EEEWWWjVNו+WZj@.''G8o 61͛7π1&""gZsh0 )Ӓ*++q aZ3 G9gPSS7o:t)w۷~SDzF2 BYY<(,,lyJhоJ_BiiiCGdmSˎPZm& 4 ~_Imڴ ?sK/^ 644yD= 7`h4&nEEE!**KDDDDDDDDe֨|[PRRV h4ص`@aa!ˡh45`@nn.jkk6m8ezPՈZ9NEE#((^^^vo_YYRΝ;ʂRއY]]jFӤF@`` :ZJdgg#((aaa :FFvZpZ5F(--EEEڷoP8b̃rPii) СCɱ6 OQ[[,!22QNCAAǼ(--^GXXX3adӧG]x۷oPߧOݻw ݺuÜ9sjػw/.\\VgϞHJJjV#6 عs'RSSe# <SL!oո}6`ر6lΞ={___{;\͛/:$$cƌq%\xnR\'88 0aݒ.sɃ駟h4 >m۶!%%@}ǏA,;6t a$Jӄ̛7URX6s/b˖-yɓ'q9v* /$(j/ uVa^K yQSS#iĬYfƍ7k.GFxY󑒒~MU*BCC۵OO~P=m_VV>H~geA;w_z”)S8&ZyfÇqIIj;w̙3_999Xr0sV?GO>O޽{7:2< 'Dל=CGJ  rWw>뭦 6_]]e˖)yft:=~FⲲ2l޼b߾}B3-e~)&M$i*,,Dmm-#hVTT˭nt ZJq"]Æ sxx3g &tRjkkePΝ;s?--fHKKᅬGyӥ[7//}Νu\u̮]oj^R\\-['NO%Ǐǎ;P<fdd/?^f5*m3g //O>i)//ǶmpIi޵kb@ĉBLVFR>}Š/mٳfG:'N@zz:.\jltɃ y+ʎ( {T|gΖ{͚5xWh1QUU:ܹnMM .\`رc6[sٳ'|G}]tyv-S\\ #GĸqRgV2 (((-[pu̘1:Oo~~>*++vma{J%rrrfŗ @}ܹsv.\º5i5ǩSӕ?=,:vqe3gHN:5 P\hh(F0mkXY!~XvuF#{Z\\,,!0sNF#//Or9x ___ӽcItSBhdd$/)ر~~~6k\8Vŗ_~)gT^^.jO8~:SF^\YSS#G`„ N&F-< 8:"4 BCCQ[[|3z=V\;vtwq7Cqxx8BCC-+kgAdd9]|֭SՈFAaa! /:t{###O(!Xjd^HH:v___ pu- 22:N933ׯ_wEJJ;w>@?^ %~)fg͚ew5b&!!!(//GVV¬~!^{5pWgA(;N:q6111˗쮭?q1ʶDXX8qju:$9l0i"""hd//-6%^f ]&p#''GGA۶mqw[MT* 44ڵNí[t@Y|IhT*!nݲTIFFddesvv6>3~~~(//ꫯג3kL8JmV譨g}_]T* N 6ݔT*$&&bĈׯcx/~o4T*-Z$ ڷoŷv}lk׮Iq7o$}XfbHLLtK75j+ vVWWcƍHKKP'77W O ș? &`kɓB>V[[={8t>D-<%((/coύd 2Dݰa$/ ϟ?K3nݺ%iI`{`>>MRq񃢽}YzG>s!hXm____zU2}MIs3fX,O{_n3meO0G`nO}HN?1+v#G#"" %ק$-]K>,ڵbKRv Ç/((P O=fdY\.oJ cǠ G3#w|l^CM6޽PpUiLjub)7Z"~#yo RhZhZIXCy^zYl ???*p4ݹsG2ԨQNYw~zy5.7mڄS*$$bS q ?Xjp$qo5>> ~[nIrbiiiB%qVq-KbvGTkR3K5甞j5(ϓ1qDY_&My`@CS!}2hJܝ5-l5m:ljh0WƬu7+53u13/f}YӮΓ1{l|:?Hpp0&OP̭y-,go%5zl#Ȗ, ךWb͚5M wAHM.]C… Mt:֮]qaԨQNO?I9oö? |[őƀXcnK;<ȓA%MΝ;'/Wiw13 \:p4|ΝvZ$k֬O?mWh0}tL6 EEEu\WJ ,{A~<|A)l7m**|ޙ3g$ʜ9s,ŵ?y% 1Oʃ!!!`PZZ*cIlsf}4Ʌ0ZbBB<ݻסyyy!,,  ?_W]f׾L8osTeqѭ[7oʝÙys!:3j(WZq$FBWK{zdd#AbWCNN0=~xiO%dee #..NWRSS%/,4lJ\ml3W񾚢Z3%Jɴ\%?LIchD&2 '/$~`iNC[cvW9 ^R4D`0 --Mq=ш/ZݗhjL| Z̼'[g[oC/ñ9Vkc+;fsz$t(???̛7O~{?曒68-m@z[ _tI_V+|1ILL 7G:cc򠆖]Qj.yxDqGuYr:uJR$11ѡ+((kĘidTTuuTcRSSO^%o\7o *+Ƭ]!|L\&3 n95g._lqyYZ#yfi4Ǖ\ ȉ=Y⭨hv["q޸qTw4tf \JHH}Z2-p1m~/ 4YvZ_7ʼ{{B-o_^o Z^^.ɇ-5aqh_rrrrV-yȲtn>}i{CƔ]j.kqSNICuh_}21 m"i۶m3]]|P kƮ] C|]_@}d@}K\v3-&vpŅYqqqrbqZyfZĉ̙=w0h~F,rj'N𘠜y4}zdJK lܸQV3$q˂Y-N' [.Lgeeaݺuc~Mlذ<]HHF)LbHuuuصk ~o***pqyyy/^CٳgUرNþ}IڵkeWJܹ&c6qDIdڵ(T河[NGDEEa„ tMMl$,[bʕHKKx8 )*o2DReӦMBI&<7a .(xgsf$N8!p ٳǮt2jLB<(((*e/RSS%Et])Sb^b` {o- ޒ@ݻe vawSJhob('#G 6… k4?鍍L_/%#o۶Q]85g +Y0v޽w} bcc-'..mP)Fo ~z:wutbqC] ?AHH^aaa>>B<`WD۷O-<Ç#((Ha8s?FϞ=|Ymcʶs1k׮&N۷6mڄ#G ..(++CNNNk%#p%7nNpMܼyB2$$xa׷iZѣ^ѭ[7iiiSNW2vLbb$x駟AYY222$ڻ3#F`~> AAAɑ49ŕyPcʎ.5S{ڵ ۷ ^^^hJѵkWY ZVUU?D^<پ@yQ]vEdd$j/ ܺzD:t@ w}C AHH P_zOJ\/ =:tP2> V^ ^wCpp0QPPLឧ5%}ZAddd୷BPPpއbvoĉ|,PxbY0Z3%xoߖ0STvIIIIG.ݨrY@f-^NttՀ\>}p1IaVJڹ= |_~0O nVv0sL^Z.555@ 0@R\HHd-sOqAY+>,VX!y >>>0a0"NsVd|둞nw(ڵÌ3$ ~ŒkC`„ |mm-UU*̙իW A ՖtnݲI$L\ŭ . 5o|8.]*kaCZLb@@Ν+n:tbb"ufjxDVXUHLLk{WkLmoa۸#1{liRR,Y"cK mL6 &M,^XG8+?PTѣ,X 9-WA&);:$g~ 5xp{`iڵdڴOG9ƍSL}Q\ڵkGyg϶r(..?pZRdatݺuË/(i*TScljqqq:k5o߾x饗dy#8@,]#F@DDDD1m4*\yݷ+???}ݺukXbU258FAee%BBBmaNIyy9jѾ}{tɡjJL:sA۷@}gigxJ塸uuuh4 :\`@~~> PUUP5p\F#++ رC !//h׮4 "##yLFj(--E]]ڵk#44mOt7nFd &ZڵCTTBCC.3vLmm-򐟟DGG7ًCKF ;;UUUBlll^:;r6gZk׮aժU>* Y?Q޼yHHHhDnn.ѦMtѮ򏒚`?BBB$r磸ވ9һ%~=9)/χNC6mY]k3gNo$|||k=H m/FkfR@=zT5ҥKiꋬHZs1=;V6Q뒑! :ɛ5DUUPu8j(>Ql2aȐ!߿?BBBtlذAX7 bHD|BsDvzK̷Z: gϞErrdaÚ(eDDtsRSSqqsO;E(//xF 8BD~dz„ w5 D-ו+W*.&;; ]w]^̜9{v-s΁O_zz:Q~}VA+WXXxT*%'' 4IUչVR ___oEvv6:uT=ECu:Ϫ^ZFrr2 ZjY= ["IIIw|}}+}6//)))P(BڵP(,ZDzz:*+##iiiPT7DDD`ˍ7aay޼yF8]t .HDGG ???9nnnz5ILLf]}5իWglvvv?nR:frISpy~rG]=JJJd5iĤwڅ .:uAa޽zdΝ;cF/ uVĈ+t邇~?)STYמDƍVZ8{gFrr2 zʾ>KϏ:u`Μ98s N:dQYL8Qo`3''˗/Gii) ͛7X{na:`РA2ĥKpmz<<<вeK jyqFzsssb yϬ,$$$ݻw6eY:r^^ !<އǵkז ,GBB\\ .@En(^STprr;wDS{ų>RO_>4 $C[Ug^^^.B3_pAo%ǎk޽{ dwÇѣGj͙Ș*lsB(33iii-$[;vDϞ=e@Xnp?`ڴiy&ŋ/(Z?"55:JKKg@:u*4i";}18f͚5CfD~gܹs@YwG}6iw^?~Bui=z4:t ,_zUԥȑ#˗/~cڴiBW"Z콪0yd4iυRs='''`ݺuʺHɩnഴ4ZJ~:u m...ѣv*IرcO>={Vy,%88XL((wѲz3F|u]Vo߾N|?y۷o-<=zUv)޽@g]tSYY/z)|aa!֮]xe7NHt)Q0M61++ [lA\\:^]tA.]eFEUGii)'Z7~xnZXNKK?,/^GyFwrrӅW[AիY&"""Jy梋1ti_$vjժx 1B6xn 7nܨT]zUt|Ĉ`PԬ߿(-[D˖-夤$}3@E3D8::b֬Y6 @Yǭ2eY@՞1bƲS8NUiذaxзo_лwoQ柡Ϫ(((Htm޼ytUʼ . %%EX1bz)8[nSjqR3gƲtrr媝yK/fyxx`֬YC2ڙs...FyfiywjknNNN?>>^ 24nh=9C @7{:t |X޺"""rprr 9{$ !<~衇 +**©SpaFf\NlU0) w3]fWTTd0 g.ݴR)SWUvvvJR$,:tHYcHMڷoOOOx|111lLС>_S, ʺ@*ÓG;_~hڴF–UBcMtDDDDP9___Ԯ][gٳB@N{66mGۢ;w#FȖMNNTӭJPEȒ3 x14ݮ1ci/\7\mgΜVǍw۷o[mₖ-[իʂpF||PNߤ'tѝ=]vpLj'~Q>AP ~w~W9삿<ŋ9r$RSSEAN:ݿ('[?x][! gG8f݀\FFdr5yF|nNdb(flL٬Y3T{FeǞgPppӑ+WەJdvN?gndckj+7vUSTŒwmڵPcMt?;yܣ> Jevi>:tjܹs:'''3iiQ1%ҥKjn&UAill \yʔc=-.rn@N;S[XTQf) 4hF̡STj`qK|5nXt Evd]CkLӞ~}ΫjߝIIIjnݺも\~buW,&&̈́HR48Vݾ}7oޔDDDTUy@Ct1""Bر6VB777a9**BTF˖-EEEvl9Sfc9UM:hk6͢~({{{Q\LLlӧOH~?Π-""BRf.gMaϠݻ EnݺU}d:-}o^xjr:+KwF7nȖ;|5#F 55_]γ@ZBni 5rx˗/ uW 6'NH$''cՕjógV"D矒LXv "55U4AGM6h ZƲeDwsrr|r[4b~(`ɓ'%Ioܸh=/"11QRҥKؽ{w*z޽R,ojQUl'O,XҫW/Qw˖-xlRDGG_~ŋDgIwcǎ U%OOOKXNKKʕ+eo`߾}M_]γ]2ݶoߎQ|w-уǐʂMsr6m R)ܵ߳g\͛X4hGGGc=& euo]pAB}С]n;;;<طo,e˖E@JJތ!]U}̂DAm۶a߾} &L efNmce͛7KtM4p iҤ ׯ/oW_6liz?UTϞ=ҥKѪU+Ԯ]yItQ(^|9РA֭[*jȐ!رc/ ͙3G!*M6tUv\HH+nƢ< fu nPv/[ >}||0lذ*m=Fu(ݻT A,kv#GGGiFMMMŷ~VZ7oDjj׿>}Z8w7|ƍCRAP 99qqqF'VF?/@tqq=T*ѯ_?;]Vcʕh֭֬lDGG^[@@V ȹ @XN_6Zja̘1[nI517, irVڗ%=wޢn'HNN jZRT*U}j׮ ueݻjZ6 mɽv,ër0c _^M06l6T%k**((h5>^LԽ{w wFhٛ;OlaݮU?/;í7oɓ'wȑ:t* s̑dBI&aȑ.24q|jڴh=j }W}VX̚5 >(doJkWUζmbh޼йsgL2`R[{Y7JKK~~~:EEEHNNFJJ ߷3^~k׮N* у},888IJj 11  6%d񑌇WS 11U|}}QNnN=jT@:8q;믿ng͚5...xx!LDDDDDTԼ*5 :$,7j"2ɭ[DIv֍8"""""j*:7.\ۣsI`„@,DRRRٳwn! P8s Μ9lbh4Qnݺ.QM6n6ljժe)#2Plgg3-DDDDDD'u RTT[n!&&C~~>\]]RP^=o jD8p@Y&g&MM"""""j9"""""""""+,DDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdE YrDDDDDDDDDVĀ1 GDDDDDDDDdEJ[7h4˃R޽0Fm=`\rr2Μ9[n!99T*ѠAGU[iY|̀u=7n`ڵh4jo۷qYܹh!++ %%%k#Qe>}$''_~P6+cǎԩS)J;v4ڮ";v (R[ƍѩS'^zI۷v@[vmT*Ϲj*899I&hܸ15j mKe>}GH]~?<͑'''W򿫱)xcV1F 5Uu}<7cf>3YY:N13,ẙ`ooocV_' Y) ԫW7FLn5) WRR?CS0*--h@N=3NCu`dU|YkroǏ?,+,,իWqUeG}9-##â?`*--k ދ13$0Wu<7cf>3YuZ\u|A97,Yx̪:ynZ,33QQQ8~8fϞm4veu֭&;݀ٽ{ =Ⱥv fVWY^nRI,X}oV!""""""Ovv6/_.1!˗/ ͚5C$3 m κKwLrrssFAup];3.999Qui3R ^ѕ`A\\]HQ܍7p%mm޽;5kfkSrr2իgJו{K<8::ZUs|8fũSr.\08?`B@@L26Wk׮sz_~]xh4;,;;[XeZDj1MIѬ hݺ5Zn#FEXXʕ+U]^1M4uUgNU񘙏||:Ǭe˖'N03V>X>ٳr-1oooQ$66Voٌ QUYR\rEhѲ %uC7M;[RFkׄtC߾}(KMMa~ӵkWqzzFrw{{{WYGn݄EEE ?w?}.^wlEl"sf+āQk ߸qCWի`sttxgԱcGM,Zw9QCoWTmk׮EIIh={D~, "ٳGlm -^uغuT*M bV˗d>}Z,a7oMJҸq*o=XrqquuEp9eAe˖nݺPTurssE 8oݺ-ZB[D?Rip:m۷ٳguꫯnmJBBB$A ͛%i CQvm"&&F4H~,2h1EEE{.v܉ݻw# uօp XL>DDDDDDDDU Q`gga]jjbcǎEvL7 nBvv6%ەJ%Mf̣ ˗FAZZ҄2zr<==Q~}۴]YYYHKKCvv6 oooDDDDDDDDfN 첪 ZX}JM4A&M,VguP(PNԩSM0:{-=]Vl9"""""""""+b@Ȋ#""""""""":(YTYf;;;۸EDDDDDDDDd Ќ ͬY\\l?0e[7l̜YрX4PGDDDDDDDD1'f4 lreDDDDDDDDD"sbhJDDDDDDDDdE Ydeݽ>m")--+Z xgV￱c@PPfϞm=X/_F &ضADD 䈈jqZmcrrp2ڴy\²Oc: ߽)ϓ'-ܺ txfTtn/Z7 qCns'N"==]XoggF2Y ѩJѺBxq/Y;{{OFl"ZG.59 W#;w|| tċ/5kC''wr6mn}E2{=ojbĈz@RRz%,oem xqaLJպuk,Xf2|U~Ê+pQzwww :|ɓqD̈́I& s2x`\~cݻO>!!!cĉ `kС}]۷OwPTOm%ڵ /XܦMر7nիw^Q1c`ժUz)))4hj5J%_%*c…p7pMFDT5NayD`8y;N֭z?8LA2EŒ@WrV\L,)#CR.GRV!*(yE8]jҺ]ý{Yzg+I$|}ʹfB:zdյfNhayhѢ݅ P̙3*]~g$RRRl2>}!!!_:Kq"##oXnѣG1aFrqqqB6l߻wOrKvcufrvv?k֬1۾}AAAv؁)S"##1{laٲe]絔 ̝;Wv>, ǘ1c^[`SJJzo߾P&,Z=vj^"Ā, 0`DEE<; 8(ocƌpss3X%EGG 8Sll,~gOU*&MÇ ǻީS'<߅ w ׯ_h_uT*L>]~?Iqpss&"*)>+`0NTB̜", cMs6{?KQ*Bmj.GbL?*Ddɓ[ӧ7U\H+pu54c.Y(--lҤڥhfhe\]'VUbd}u\۶m;Vx>cQPx'r?<:$ $7kL4ʦMeJKKvZayƌ`ʕ+1sL[SNPy_'bǎhӦЦ> ooz -ɍ70tP!_c…Ǭ* 88hРTT8t0f!_aҥF_0c aٳxᇅ?#L2EbvmҥxꩧZ6SII v؁ |( ""*ݐ뻵mw;3;.\ʖ@l`h(.)°];wwT4؀ָqٶ ?JƦ?-_$Ѻog@F1d ծ*U$rssp TY/SSdLNN( vMlsMM_8ڵM123 .ͫP;m)55Gnfi_0@Txǭ6m*RUV!<<\._Hr 믅ӧK j–-[ЦMBwQ$˗E˯y H!///7Nb_^o@N{Cwww 4HR?uЬY3Q'''| .ę3♰-Ν;B (ˢ>u6mB.e9rwd0N>]ȭZ K, >}Z|'%ev!ʘ?drN2B dz>F .S(x믿 '[dB3D4i?'NPֵ299jPwtssCϞ=  djwیGii`믿. @ǎqF!3-66۷ogӹsо}{:{{{3#F$8DdspTBna!چt=!'_YI={H{dGaxPpc˖r?^R7ȶe?׮=%e2$D:eu!HQK,]i@,Cϡ+⣏Zk3s=>"" 6l0. %vmvY`?XRSN7ڜ9sd9g<(fxiצT*1oHܺuKRNٳFt <M&yQV\|$x?GDD/2_~lΝ1~xaYn>v@MvfQf"/ AW ({hw6t4hsى2LuV}" :TdnZ <Y4>c^^zM0h Q=Y|?͟?_`U(ϮC騲 فxAͥP(lDki2vvvxu#zPaօd߻'ɎÓ:`N>xVgfĻwM~ 2\g쑇J/ϷVs MΝu4(TSI/0t r1PXxOv-xyy[) K2#n݊+WZ,qJ%/m6lBCCeNRRR" bfI>>ұ6KKKQT GGggg7ް̸E (޺YL\=8thy2e;wz@V]nhL46N(K*,_]bŊB)00ǼyL~;rD̜9S]&?޿rUDŽ9-ڵKT{Nu]̕onFmu\tnkwݦΦe3MjJJ Iwa"JPdUd&Pq &'QEݽ6rrmIMMʬSnyr%4lX!=bĉHON"90sNE?fHsWPn+bt2ԝܟ/$˜ u&5ϔz\\\&M… Xb~Wccc /MnO>[ ;={V%@2vѣG͚ٸqf4,iӦf4F_&;L"Oa! WTT$ bώK}ݻm1,Y4`vwTk􈈪js][vPlsTi]QR f@C) CnqraٖAQA=pB@ihڴlϘki7os16TFu4Ull,._,U^=xup]ǀsQբ}5[󑒒x;oG!%2T**̙3Tr/:wmՀ\rr2ڶm+MwVU݀Kr iST:t(?+Vs7Ǟ={}u)))" m۶rGuޱcO. b1Bon///f%1.@X|ܨەa3-YkH14Io!Bk֬DDT}P diEEXy}ϵѺ:jIaw[hY !/7餜1uxUq۠%D;jVH}E478x(ӠΝ۠5{nr\\5֭[ֽ{&Z<\[Ullg.S(J̛7O4`֠Pkt͛7~͛q Qhܸqz"{R-aaaȰH6m~z_M<`ڳJyC,3V5P6Zebk۶mذaڵkNDT7STHֹf(T k% ^g۷óvmH|KV85[|u+eLm.Ӓv<֡hLƁF3g0KhJKNH%Iwˉܰ5dn8kɍ?ubNfyYջ y *2IPءTwUpw_$s u4y)4]c\r {= aa?]Cr%ԫ'Uhg&0#G"""p $&&Zm~ }X[+W7o^ZҵtRJԘcc;ov0122g3f7HJ/K_~gtvvƸqㄙy̝;utn),,4:re5 >loV8q"~j*vcٗ&L#e"m_988<ay޽ZRNNz)[N4?!]PfhB40_|;fh4xW.[hdC ŧ~yaԨQ>NPI&!88X]իrGz?;#m۶?~<ڴi///$%%ʕ+ᅤ]vF뮬?k֬^ʕ+1d@Pڵk8s(klLP;;;̜9S=i$R*зo_ec1 @׮]lDEEѣ¤$r999СO___\x6lq 3`L:[Frr2֯_/ [ofV1 gONE,% ѲvvvxsXlXv3}zcɓz9 o[p_6|DoP^^/r0m6'8|x( 8pKU*<8qb}׮70kVG{\!wwwS(J 6LSoJqd}|u&a{EEEؿG-ݮeR +WĜ9sd gϞEǎ 1a\z`hҤI8sh ]:u‚ D;v,Ν;gpuk IW_VS 9ƺ>u&&ЭW^Add$ `l^ @Ue˖8r/^ J;Νݻw4e6m$κ5klj}Wb֬Yw;9ko}Ys{>S^Rĉ'dc֬Y_D{ݺ 1u[.]~믿ҥK x?0:8*YH7|70탮P\INBAq1k'ZSՌyEEiS(ЮZ*.)AdR2.'%^͚Bed"O)RR"q@: /&wٳgEA<ԪU 8{,bbb燎;&'77=ZnzXBBnܸ$5B@@dr : YYY7n,k iiiB\\???4mԦ">>QQQHLL3op<Qii)n߾K.!33[FPPPU!!!'OZF۶mѼy :eN 9"""z ʜYrDDDDDDDDDVĀ1 GDDDDDDDDdEFZ***P6OIII7֭ӧ͞Ȝр\aa!@P#"""^zj[7j0sbhJDDDDDDDDdEFrchBǜрɕ=̉*1 GDDDDDDDDdEZESSq * IYHF޽{6B&+AAT=jlڴI>%%ΝcY\Vn:eٸDDDD€ݸ.Y舷 Ak~ָqG1亝׆hB0P(i- ->Μ AQcw0|]RO,٣n]_ITQ=dgKԓ[7hvN:we]ẁY\:u0yd[79K*.)SթS:3 g'ʲ3WAq1$%NDT-=*|ŢuO=;||g6{?;;<ػغU|8z-,GDD .. 6vo/"BCCwΝ;˖-òe Y}믿 e###1ba?@n >BB#,:t\~~>VZ?)))uM0'Nȑ#%vڅ_|Q.66Vxc͚5J%Ξ=+ w>… m8q">C.Ϝ9py$&&O>? )?m4{ fľ+8r8;;a۷o'NֻwXx1ڴiw 4jJ\h6n>}:^}U&$$/7|#]R'Č3yG}@s\+V* cǎŗ_~iJKKÞ={c$0v#F74)0VѦMa ͚5w}/Rׯ>CWXWZZ'O"44[lAddyѹsgz%?տb =zTC7|78`ڵz9r$֭+YߥK'!!o6>,,֭[c5k:OXx1`Ȑ!Xt)"""௿uwwpBs!"!*!=?EFɕ)k_/nFݞ""p*:M}>odR2^XV+W\T+ׯǫc`LP;TO<?4V֭u몤lSmQ5hayhѢ݅P̙3ے&\ ˒?ܹ#A~"\6oի6n(+=z 6 effJ.zu^"sӱ\dd$&N( ĕŋw^lٲ=6~׬Y ,X@Bee7Ahh(BCCzjL>]\AA8y !\xxs,%%K,{b e-gۥ5IDAT!??_7ȶkٲe8}4BBBP~}/^/s#<<ƍUNшO)))Yu\?ڷo/z/]v77o޽{CTӈ\p @TTkɢ`JB@@^b\PPrnL&&& Jy7%!C/_e͘1=zcB{9AѥKRЯ_?ԫWW^}Q?AAAz:t صkr֭[}}W̙3Ezꅎ;"33΍CVͶ' M""[`@vv^g߼", cv=`Ӝ%?Li~޻c۷'55k֌6X&07Wn7뷬GP(l3ζmۄcI9d!  T8x Oю5 ۷ow8p@t1SO 9 ĉe̙3nnr3nz/2+իqAӧk,=!!!~ZnSf\3gU֭[S(/z`׳>>*7"#""ꫯ fUᅭc`LBBka900uݷo ",9rodi(Xi&7NR.&&˖-i`޽[被V1{l g,-,K/aɒτ\sP-~z̞=zꅕ+WEy>N6 >,w.v-7N.Zsg|w`OƓO>){q!i޼y7on_}'Zdddॗ^aKXWXXw}W肺i&̝;79nK.SO=WWWeʎ;pj3"=}Uwe~̑ 7cG,ݵ[ԕL ^jlM.jqdիd#F?[nʕ+vTޭŶ A欬,I=>h]^V.44T)RޭSWӦME~!4h`2  ^egoVyO}j۟vB3&L. ZZE .'| Tsxzq' q$$$ >>¶s ޗ̝;WLJKKCBBܹdwpAW݅T4iv֥ݼySdsCåKгgOkj#5U\t% NB~^^du |}"3DGl,cre6j(Yho άeSE<=x)h~"m[5xӝ 5..NRСCfMPKVͤjB ӛ:tڵJojtg%"fѝU܏O5ЏrQkGDd;sGhf gΝ;E˺79d̚Eׯ05kvhv9M4 b 믢Y^P\\,ZEcrYoooZ" eBYСCqM,[ We. ǠA|rbJn*!!#F>|8x N:B /P6 ywE8Jy!((ǍgRQviDc,32t?7=*3q&lc""k9G{{8T{ָ ^kK*&KɆK10nv4reW^xQ#HvX4EEE8p#  "[h4زehӦMj=3B@_ŷ~ a1l2G~︺b?>RRRb͢cҤIU:S,*cU nZoy.*ʤ>t g}O?7n3g~lذAT3<#FHVTԫWOvnN{kؽ{LpUz=r~w#UAP ((HתUڼ7u'pƜ$"2Wyo/h?>c,i]i,!%RɌޕm*S(憼\QT.)-Ds7iyɺh| AMy@5Ǚ3gD;ڳѽ#Gںu+:u$p6mT* :C+VٳJr^^^%u? )+QwC=$ٮ"B( "00ƍŋ+f1n8뮌dcΪӞvzqu&(wigʕ+t\jm۶>cRRRm6o! k֬DDT}P ԩUKr'#Hrdo:2@~9zLn( nggI&"ܹuHOOS(3GgJKK}{źQͱ{nr\\5֭[\Uh|UVaڵr^*=iRļyDGEEf=o8oE] VRR":Ymrrl]o <<<|rk~z5޽{n -546XIIU?'q!k3B-aaafѾbNݺ]Num۶aÆ k׮J7t"zp0 WMt1֭ݴm.L*c\Cgb˹Ք|,m8=ӓo;FEEPՅؽ3I}]?ƌJ=&ܩxw#6m$<ӧ< L`e;q(&>}ѣc9C{.SJƥ3d„ 㰰0iо:2dxrMѧOIQpLwrٳ;~O?\Rxܼys8@wmϱyfDFFVsN_Z[oYǏ)F%<~k~7[l)}d'N`&ĉqA;?(XԽ{w궤CYF8/_{5\C A@@ ]3gΈ>͌eg֭[1@zUgP˖-q,^9sb&ze]F8J "##1`ӫW/|BF:W:{,IQj$/oAVV}dyyEEiS(ЮZ̋Pk4 w)-EShנ>\M*.''n@ qWWW4jm񈊊Bbb"4jͳRܺu /_FFFЦMA1zn߾K.!33[FPPbbb$ 5B&MD5={V4Kr^^jժB={111CǎirBtt4JKKѪU+ibAʸ}6:usfU--- QQQCqq1燦M#66W^EVV<<<ƍf""sbh U+W駟o߾-=~/ GDDt3'.DDDDU(;;os%)-p`k`63 ٸvr `ǏuzAS$eҢqlѺzO׭d粷Wި[9ڴۗy >Uǧ1|7у'773g{Wн{w㥗^Bbb"G3 {[&!!ATߛoɀQ 1䈈lY{7MnǎuׯGii Zc}GE>}n:[7rMcOo.'$بEDt*--oM/BaoP Vi&;w;vA,KR?PINNFLL ~G!#N`4kVM%""""+b@>SD긻#^Oy""}5jZ>>Q_z%̜96l}Xj,e)Tq%)apvl$"O%|7L Nc̘EZetعsxذa?~uV[4j\\\tR N>m51Cn݆SW}3u*:7߽Wʴϓ'ֹe|3x]8x,4h{= %Ɍ#"'? 33C>1Q~2 "cʻrEdCÆ m4B޽ p`QUc\%$geP/PTXR&9#CO@F~|V6^'Μ 9%oY'YnPMhayh׮(c6 """""c@hSEkVʷ&tꄢGW0&J-޽{E?XRSNرF"##ѹsga9>>)))rVBpp5FDDDD6TryEExv(--ptH!;^ MWG QQ&MyתeуM>5j \lvh&ޫW/xyy1Bmݺ+WRYTZL2}7nlU_yEEqRuIMB~^^d\?j;;[9 i`UH~~>6o,,>\RfРA:z1jm<( [7:<=m"z999^;C"==-CD֭hyΝU& vU č7p \r˗/Lj#۶m[%ODDDDr5Drt\Wu;;͞ouC$}ǎj=tkݺuB l uuvvFƍѸqcls;?eGYDDDDTMT뀜=+ϫvm>ڵ+]l} +}+ٕQBXTDd[k7w/[{Yu&=о4֭HMbR=Dhe˗EqssVwݷ{9Qw/n2""""r..8[ϹX.P-'e_ [s96h YwɺٲII$<==CFWtBQQhΝcƌz";shQwwwsrڽ{7ڴiSem5ARi۝+8]v ˋ-ªU*T,:C$Yw+h(k5Gub+7YV:$oI2JJpލz>;;<۷RKtؽ{h9..zuI(u*m8))`Y"P 9hӦ O.,/ZgUw <=qFg]Hi#"qM[Oɺr<o;FT]|X z"==}.j :젝;_̙"rGvnU6mFF'WIڵk'UTԭ[WR\YR.\_UXϰr GDDDD5rK.';C>{^3Pڵ ܶ _nfSjU;Z ƕ+'DKJJK,ջoZЪHN=&5qq1HNz_FrXDFF 3h |޽{Ee4zhsNVc̙8znɕz6m`̘1ضm`ժUx7ѴiS(JٱCQu_h0];jP..*eI'iذO+V9Y{3wzu҃!44TbʕmP5tM||6nrDDDd!!!Xz5.^XaJB۶m裏bȐ!hӦ  dׯcÆ ͛3 geX~W_1 GFmٲaaaƍ3 wILL>0 '#>>K,#zԁȒ#`  %%aaaXp!ڵk~F--\mڴA6mxLDTs 9""" 8mXiDFFl""2DZxwx5a񃈈fygDwjܼy/>#\.fΜ ҥ[Cd]Zºu6n rT- oM "*͛7 3gĪUDeJ%5kf͚aʔ)ضmT""ulN:>h֬4h`׮]ݻwѡCԲ%TȒ_22DV̚T| ݻ'ZQOf6;YYxo_B6R ?__?bZ3Ƥ_"/~9k%;!dǎm?yhj.͚avSG'@;;;c߂h+(.G{p2* ymhְ!a08 qCns'N"==]XoggF=NDD$ˢeK]\j۸q#V^{֏3V2xW"''G-88:F-oСvjEfɓh"7f׮]xE봻kS*8{IgΜ_|Pѱ ºuЮ];oaŊ8zh;o*ɓ'ĉI&ᣏ;jz#Z1*Fť`;.uyݕ`}VRmVKUVERĵ(RR\TeQf~3I|̝M&d_gx!`Μ9XP?#,, olkjhqƌ_>␙ N2ydZ 666meeen޼)| #0k,Vf$>>SLQcǎUŋqY^JL0/V8_f >}:|}}~z#66SvŊXz<ɓ'믿"**],G5kvct+Woƛz裏ѣG?>8緋 -[ujJu*Saa!zW^C|*'Kx gϞ틠 >>>Vn]\xQ26!P@Z##+.")Q0 w7#/8L ƅmaaJ^%%%+- v(x'p{7I}^ƙ;wyIX}>l/yZ51Ǭ޽^ꥨXsg=y8"$<|sOOxJ/^+m+[VVm!kBb4iDg?|0ƍ'-44qqqEƍ㧟~ɓnr <==pBlذA̅ŋȝ8qٿiӦZ\9m/--U{x9۷oC*"&&U|p2 !!!8s >  HRx{{s֗?VrСCamm˗/yA.t?PkqyJL[effbJgr^nݻ_~EzVرcA922͛MHH`*55˖-|/1zhcܹJpww<___u/Q/z>թS/ĉT?z7{lN[?~\iݻPGۄEJҩb}ʞONϙx2M:F~Q ˼<|gpwwǭ[`䦵;'Jѹsgrz* !Jaggsssɓ4I4O>A\\'|rL:Up8sZZ}Jݻ7>|Ȝ7nk ]O&L 3f̀:t)k.N.Ԡ ^6T*LVmBH\%}Ю-9=22PXR: ڣ ?`o9h Zζ3Wbj/'X17uưj-Z > =@$]jpoչ3kOH$x^^wBGqU`Ȑ!:l21=zwww$''-Z +W;}4ݙGԶmԛDAQQFŞBXUκ#G",, 0sLZJ0C1i$Ξ=o9 .ĺu8ׯ_رcrs##dvm۶-zK94 W^^ S S앶k.L6~gL4 @E0d˖-Z7a| ֬Y#8n޼?\oA0ؾ}܌tg"#U.Q_s׮]i?,߾}aaaǎÇ~,?gon\~0w\ :7TV&a…L(44Blllpq&xT^^ ˖-c}!̜97|Yޱc;wO>*T|sgʔ)*dM8˗.]}bmBHQ}ۈ(UG$B{=,#=3N$WVu_9jnn_|맟Y8ڶ=JD"ff+px|Sbn1<ܱv`4e2.ҏ>BƍQ[EYW?@Wk`@~l>w^Y6z4.\PoLy] !-enn;wr%''cٰfϞ'NXr/9A֭[cڵrvv69x>e0b8`iqfԖv޵vo-2W;P᪩2 6Dz̬6(`!p&(++Lp!GT:Udl [DVMo_`cWo jܺ&pgk@1]8wIk4R3s]SyVBzX|9;wzd2]N±cвeKV@La휜^EQT=z43\(-- LwU̚5O  ɾf_:WC鶖-[2y%~/b \U|]=t)SZZʙpԩǺu󸠠P;.3 0..\Vfҥj)DeO=\\\tTH$\rl#GĂ 崴4nZiyT*3 -{ L&Czz:ӑ"f˗/Kw u A:D"*Ə>|8q".\={͞={бcG]B[5J:+hs5) U< WNX[Z/V\˗~ _Sfllqʿ0xҸ^siz~u@,ML:K.P> :b۷/v튞={4YcHu*Hm¦\o0?? 8lڴWNJf^; .jJ瓿?\T|( Bhj ̟+//ϚUmlx3r>xLUFL `lQa!"o@=וWxyA5@obc'e4!LMMѿ^/^ē'OyfNfgT̊B 葯IR!޼e2BBB0w\8;;7XvU511 Y577ZtlӦMҥ8s4o6T=S}>rzʱBHUro:`In2 IIU@@w)0[eh<}VE B޻ gw%43ByJg!8-gf .8IhfL67*fffJLr7oL*WWWܹsбcGN"}77J7Uyy9'3yd̟?_ b'Ndr3899*RסI&mܸeLLLGb8{,L&?Μ9k׮=n7u&;Ic5ӧYWWWXXXpj[n CO!laaa0aC!耜IZ]Y6jРR)։)I$x2}CJ%iҔjqɪf?Y0U FqMLW?x[ijÒQq<[ ?l !3 $gRuHKKKfبФl)ssscr׮]33G}M"$$N‚ pef75\UqS:? ȅhܸ1Ο? (*N!kNU>dddѣGcG\\ 6૯4ORp 9p1ZYY^}vD"իWѪU++ayلCꋾϧ`ر~ĉ֭ڷ_Bjt@w|ΎwkΎ6P ϝgKJJp;= ][lck'pΞ[_ݚ3|₲r\;!R`ˏR362i!IONaRr;Fۦ#m۶L@N3]<< )?Mqbޤ޽{c۶m+W kVA!!!@^^' ğmбcG& 'X&pssX,foǏǔ)Sp̰aд)F+P1l!ejj޽{wETTμP!u96}SrŜaЛ7oV9/݆ߐ>ϧ$|̲aFBllʼoL///^sXdI5Պ Ӂa:}sՕ 6J5LRYTJJ|T6)6l=zEx%| 1񁏏 :]v066FBBv 59]pp0?~+rJN.e˖1j=9]  '$ gxh"C"۶m>ӧ9mgHޜ@ՠA0i$888 ;;<83$0y?䌍gtԉy~!n޼F*dp)~A}={rЊj6!j( #IPۇA+vZ8e2o0Puͩ5V/hƩ}`"B!#99Y  {/qFC!r B@@c)<==Qn]f{bU} Z@@@'R||<9~a׸qc\v SNerd|T1crbX^...8qM\GFF"22Rupfv`!s T#fY,c޽@EСC>KRNDsdܸq*r5{R54dU\333ɨDXᎽ}*SNQƍ _Ak  cQ_;[ L`mi'On46.Ǧ㛩St+:vZj(;!N:!** .DOkfkk#G 66V"HE"_}N8~֭ɓ'U$¨QeWWW?C3g~wL>666z SlGU{蠪r۷Gtt46oެ}b1p),֡C09>}V# DL>]e9XB4uM4l_|飲#?ywܸqHLLTٓtqQ|uA$̙3JbL2qqqK<֪ʍ;111b19"}_pYF͕_~Xbo>mMK*ͦ~Sz_`TmېvMVZV/^d$aոʜt4? '?O_{WN]03Cƍn[IYdNF:ѦIStj.L!(믿td2ԯ_VVVhٲ%5kddgg#>>iiihԨ`mm]z )) (..F-ТE iFC)//GZZuEUI>/++Z2HUU^zxVZppp@v3={$Dڵ+lll222p C*}5{&O614 B!B!REjƭ B!B!B#B!B!Ā( G!B!BQ@B!B!R+**P1cMii+D!B!BțFڀ\aa!:!B!B!-h*!B!B!6 WvmH$ke!B!ByiCS[#B!B!mM B!B!b@#B!B!Āh j{!uӜAT# ! ֟>ܢѹEsԈBQϟ?9'*W\~ W_}*/^9sPVVcccܹbXs9?~ J1c ?!f)--ԩS 7mڄ-[Vsjr-ZTs!ڠ\Ǐ9Vk@.K^R@BH CPPܹfD"Aǎ1x` 4:to"88`kkK9{%<,z ]v !CP@@ii),/^r ʰm6fyҤI# CCV !bPIIIpssȑ# @vv6"##h"t }]5oѢEС:t]vUwu!Ppp0?`֬Y]B:CB! iiim666~U{+=xj !Cz)?ܼkCۇr1ZŽ394iR:.'BQD* &=ws _ ZjsyyXu<SSQTX& E3]Q?ʘuMȬo8|fi|ܭF!r=ᆱ8|0pYzOOO޽[vK.A&q999#G1d}&L???Wɓ7og{HիgDu'`̍7i&>}vR?3:u(**`ƍj!WPPݻwcݺu,3n8|>|ǭ~ k׮̛7O|NcرLbW{@p֋bc۶m???ܼyS̀0b̚5 kVy7oaaaۥR)FYfBLNNΜ9ǏszٲÆ ҥK5 `޼y8y$g VZMbi`b1Ο?xX|9.^joooL>]mtEmffӧׯGxx8bcc9eWXիW} ѣsӧOǜ9s4}zQiƌpYϞ=4\^^ɓ'3S~}\rE0{!ά;;;۟3Ǻ}6n߾6m`„ e6oތM6}R\rEW~}Uv#Y3"mDjCOκqNEfժl^ϻ3Wbj/'X?;!0tY޿?^  2D'y-[???&ѣG\xnݺ-9*++ʕ+9N> wwwfѣG߿?sm60fbbbPTTqۜeXUŅnȑP™3gjP`cҤI ={ *'ix"gyX|Vxgނ _XaT/ &@JWm&رcϣr'O&M={0C\sssUIWty>M8QQQLOSɉ9 89Rmmm:7nxiiV90q κ& o;!D7-#bV…a%pqkܗg򆩚o'8:{.Fmy:[gU8t  WHDb%hBo277Ν;9뒓1{lXZZgƉ'P\\===_rm[fvO<{ NPl߾}`XqomٲyD{u~:_~:5hhǎ355<Ҧ]*K1_ @FǏNlLgϞmfu7\zz:-[,{yya֭NݻwzƩSxu*))cǎŋ|Q#&M ȑ#*x\ O'UfϞ p:tWٳga3gM6LYuq^p:u`ӦM1bnѢE*tfB88<pؾ 汃8A%333>}Zz蒮'"s+֭ƌ,k2ƍj˨WS <8Dyق޽ar%%%!ׯ缯{7o{N.]"Xz)7tP}ge533 ASz8#n߾ ___ 00Y;Vꄐ7X_TOByyyaٸ16Hһ48~h; Sw߳1G|8˩2 6DzoYyH(++ bduD")$̭Bj&ccc,_CΝ;y=d2֮]SNرchٲc+S +#QTQ=z433-- Lz*f͚ k۶-gx|F>oLv=zPe˖̌-ř׮]͛k׮znM(a'g5֭}8"""b <fff9r$M6EHHN: ̾oj .… 7o"** !!!^5###N^z:ˏgddѣGcG\\I^ 6૯TXqrr~Gʕ+8s o&O?W^M d:u ϥlfO9v=L\yHqkkkö ImKߕJY_y>) ƍ[?o߾Z %耜; z25tKx&֮I޺8:{@iWU!TR۶m{K~vŋ޽{c۶m*fd09h "$$䨷H$֭C`` >3f3gޘtؑ kZSSS{/0|p&6 '' J!J1k,`ܸqLXdff򆒲O2Ei` N"uD) ǞBb@.;;tfV:a#󩠠3# ^ӓ&MBn`oo.K,]6 B*P ?CZFzBY2>^ߟX_0ovHkݺu1zhfyŊ46ӥ͛Y޵k`"> dn݊nݺaÆ8v>>>`y?+$mEbbΝCv83vğۦ-f-\\Mu:4 IJ 333S+ B,YVVVcf Ut5`Մ 8=M;wp2A5K OcpEFF_5x۷/΀xUNpM׹sg̙3Y͛LHH76m*yuVrrr83OPbѢEHHH^RR˗suؑWoaf'f;y$fR^zXb///N]z3P=<<Ty 6%ccc,[tD111x1ydꚮϧDN\D={ĦM={(D ;5:ۤ}3 LkrϞ=Rb =;vĵw1s:.Sձ!d2V^իW]t-?;w Z緧'Y3fPKO>3f lmm001c;#_'7p@^:Ǎ1^^^̲'`gg { |dbccǏ_AA XaÆ666GLL oHPnڵطoXx1vڅAFFF>nܸWWXqq1 6 :t@v`llر(9sҺ__>}[nEÆ ]vq'///ԫWwQFq_;v7:ul\r7A*g_l䄗/_jc֭c|ر#ƌ:QFBBBΝ;Ǵ]N4~uҘ1cdU޽{'N͛7n: 0j(fY" ((כϟgzdO:=z@xu ­[8wÆ afffSѣGlܸAAA֭ ֋R5AVBZpYWii R)Z.=xL0K*?!d3:::"(( 'Ocƍ*w,;h (=bpӓ{̅X, %{6*Be:v4)pA=r'qƸvNLTxx83;9shČV[l{aɒ%`֭[ˮ^k׮UY pww|ٓXZZbӦM?~cubѢE̺#GOtfTvm͛7:p5k+gll{֖_ƍ 0|^ X,V366?zŬf&?~<Z"rahHU٧95nu yjR0G0þMm3ZcBy;t QQQXp!$ʲ8rbccN_;H$W_}'NߺupIe9gD|Z2g>}:lll8؎ރڵk-WׯxLGGGx{{V~FŸ/ڀݾmI͛7LbxyyԩS6l_|qq,tRl}8(/$ۂ9l/_ƒ%K8\T DFFr&kQ5يH$ѯ_?ۧO|7a6VT[n) /gǪOOYd6ѣGdZeo(N:$ <ŋ!J?3Ǫ*C3QPum۶!aK,%UZV/^d$aո+ٯ^afz\9Ua!"?@~?h+$B_!++ d_>вeK4k֬Ju!;;HKKCFkkjNFF=z,d2 ְ&''IIIHMMEqq1Zh-ZM6]T˗/,:@~njJ 좢"$&&"++ DH$B֭QNUPP$$''СƯKyAvbLNAiB2tB!Qǜ/cB!}#B!5Ν;cHKKcf,--Ņ ”Jo Bj> B!B*oƜ9sвeK"CZZSn֭6 !6 B!Bjɜe[[[ͭjD!NEEE*fC+--{Hbjbځ=eX#B!B*ɓ?pEB!?F Y%B!B!Āj׮<v#B!B!䭣M Mm@nݺB!B!614J!B!BQ@B!B!!B!B!D9B!B!B rB!B!!B!B1 B!B!b@#B!B!Ā( G!B!BQ@B!B!!B!B!D9B!B!B Hm@Ȉy\RRB!B!&*--eiB4hLXcN:UVUTT011ޖ6+--EiiN >ڣ6tfkʪ>ڣ6tf>ɯ>ڣ6tfEEEUή_kφ f%%%۷+c޽L׻7Myydzmiuʹ:uL{f{[myDm=j3Qe賡hAW^ji'Oxy+ghfo%j3Qimiuʹ:uL{fڣ6#gC{lwyJ8 'WTT,p#??5pJ=y͚5ɱ?XڵkWXnU>Vqq1^zob̀:鳡=j3QiOm033ɱjφʹGm=]ً/tv:鳡=j3QiOm]I $mu@B!B!RyjgY%B!B!!B!B1 B!B!b@#B!B!Ā( G!B!BQ@B!B!!B!B!D9B!B!B rB!B!!B!B1 B!B!b@#B!B!Ā( G!B!BQ@B!B!UZIENDB`sphinx-contrib-typer-8982731/tests/click/validation/index.rst000066400000000000000000000003231515242076300242560ustar00rootroot00000000000000.. typer:: validation.cli :preferred: html :width: 65 :convert-png: latex .. typer:: validation.cli :preferred: svg :width: 65 .. typer:: validation.cli :preferred: text :width: 65 sphinx-contrib-typer-8982731/tests/click/validation/validation.py000066400000000000000000000026461515242076300251330ustar00rootroot00000000000000from urllib import parse as urlparse import click def validate_count(ctx, param, value): if value < 0 or value % 2 != 0: raise click.BadParameter("Should be a positive, even integer.") return value class URL(click.ParamType): name = "url" def convert(self, value, param, ctx): if not isinstance(value, tuple): value = urlparse.urlparse(value) if value.scheme not in ("http", "https"): self.fail( f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed", param, ctx, ) return value @click.command() @click.option( "--count", default=2, callback=validate_count, help="A positive even number." ) @click.option("--foo", help="A mysterious parameter.") @click.option("--url", help="A URL", type=URL()) @click.version_option() def cli(count, foo, url): """Validation. This example validates parameters in different ways. It does it through callbacks, through a custom type as well as by validating manually in the function. """ if foo is not None and foo != "wat": raise click.BadParameter( 'If a value is provided it needs to be the value "wat".', param_hint=["--foo"], ) click.echo(f"count: {count}") click.echo(f"foo: {foo}") click.echo(f"url: {url!r}") if __name__ == "__main__": cli() sphinx-contrib-typer-8982731/tests/tests.py000066400000000000000000000715261515242076300207270ustar00rootroot00000000000000import pytest import re from sphinx.application import Sphinx from sphinx import version_info as sphinx_version from typer import __version__ as typer_version import typing as t import os from pathlib import Path import shutil import subprocess from bs4 import BeautifulSoup as bs from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from skimage import io from skimage.transform import resize from pypdf import PdfReader import numpy as np import json TYPER_VERISON = tuple(int(v) for v in typer_version.split(".")) test_callbacks = {} DOC_DIR = Path(__file__).parent.parent / "doc" SRC_DIR = DOC_DIR / "source" BUILD_DIR = DOC_DIR / "build" CLICK_EXAMPLES = Path(__file__).parent / "click" TYPER_EXAMPLES = Path(__file__).parent / "typer" TEST_CALLBACKS = CLICK_EXAMPLES / "callback_record.json" def check_callback(callback): if not TEST_CALLBACKS.is_file(): return False return json.loads(TEST_CALLBACKS.read_text()).get(callback, False) def clear_callbacks(): if TEST_CALLBACKS.is_file(): os.remove(TEST_CALLBACKS) def similarity(text1, text2): """ Compute the cosine similarity between two texts. https://en.wikipedia.org/wiki/Cosine_similarity We use this to lazily evaluate the output of --help to our renderings. """ vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform([text1, text2]) return cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0] def pdf_text(pdf_path) -> t.List[str]: """ Returns a list of page strings. """ with open(pdf_path, "rb") as file: return [page.extract_text() for page in PdfReader(file).pages] def img_similarity(expected, to_compare): """ Calculate the Mean Squared Error between two images. MSE is a non-negative value, where 0 indicates perfect similarity. Higher values indicate less similarity. """ img_a, img_b = resize_image_to_match(expected, to_compare) io.imsave(str(expected.parent / f"resized_{expected.name}"), img_a) err = np.sum((img_a.astype("float") - img_b.astype("float")) ** 2) err /= float(img_a.shape[0] * img_a.shape[1]) return err def resize_image_to_match(source_image_path, target_image_path): target = io.imread(target_image_path)[:, :, :3] source = io.imread(source_image_path)[:, :, :3] resized = resize(source, target.shape[0:2], anti_aliasing=True) return np.clip(resized * 255, 0, 255).astype(np.uint8), target def replace_in_file(file_path: str, search_string: str, replacement_string: str): with open(file_path, "r") as file: file_contents = file.read() with open(file_path, "w") as file: file.write(file_contents.replace(search_string, replacement_string)) @pytest.mark.skipif(sphinx_version[0] < 6, reason="Sphinx >=6.0 required to build docs") def test_sphinx_html_build(): """ The documentation is extensive and exercises most of the features of the extension so we just check to see that our documentation builds! """ shutil.rmtree(BUILD_DIR / "html", ignore_errors=True) # Create a Sphinx application instance app = Sphinx( SRC_DIR, SRC_DIR, BUILD_DIR / "html", BUILD_DIR / "doctrees", buildername="html" ) assert app.config.typer_iframe_height_padding == 30 # Build the documentation app.build() # Test passes if no Sphinx errors occurred during build assert not app.statuscode, "Sphinx documentation build failed" def test_sphinx_text_build(): shutil.rmtree(BUILD_DIR / "text", ignore_errors=True) # Create a Sphinx application instance app = Sphinx( SRC_DIR, SRC_DIR, BUILD_DIR / "text", BUILD_DIR / "doctrees", buildername="text" ) # Build the documentation app.build() assert not app.statuscode, "Sphinx documentation build failed" def test_sphinx_latex_build(): shutil.rmtree(BUILD_DIR / "latex", ignore_errors=True) # Create a Sphinx application instance app = Sphinx( SRC_DIR, SRC_DIR, BUILD_DIR / "latex", BUILD_DIR / "doctrees", buildername="latex", ) # Build the documentation app.build() assert not app.statuscode, "Sphinx documentation build failed" def build_example( name, builder, example_dir=CLICK_EXAMPLES, clean_first=True, subprocess=False, project=None, ): cwd = os.getcwd() ex_dir = example_dir / name bld_dir = ex_dir / "build" if clean_first and bld_dir.exists(): shutil.rmtree(bld_dir) os.chdir(example_dir / name) if not subprocess: app = Sphinx( ex_dir, example_dir, bld_dir / builder, bld_dir / "doctrees", buildername=builder, ) assert app.config.typer_iframe_height_padding == 40 # Build the documentation app.build() else: assert ( os.system( f"uv run sphinx-build {ex_dir} {bld_dir / builder} -c {ex_dir.parent}" ) == 0 ) os.chdir(cwd) if builder == "html": result = (bld_dir / builder / "index.html").read_text() elif builder == "text": result = (bld_dir / builder / "index.txt").read_text() elif builder == "latex": if not project: from conf import project result = ( bld_dir / builder / f"{project.lower().replace(' ', '')}.tex" ).read_text() return bld_dir / builder, result def scrub(output: str) -> str: """Scrub control code characters and ansi escape sequences for terminal colors from output""" return re.sub(r"\[\d+(?:;\d+)*m", "", output).replace("\t", "") def get_ex_help(name, *subcommands, example_dir, command_file=None): ret = subprocess.run( [ "uv", "run", "python", example_dir / name / f"{command_file or name}.py", *subcommands, "--help", ], capture_output=True, env={ **os.environ, "PYTHONPATH": f"{os.environ.get('PYTHONPATH', '$PYTHONPATH')}:{example_dir / name}", "TERMINAL_WIDTH": str(os.environ.get("TERMINAL_WIDTH", 80)), }, ) return ret.stdout.decode() or ret.stderr.decode() def get_click_ex_help(name, *subcommands): return get_ex_help(name, *subcommands, example_dir=CLICK_EXAMPLES) def get_typer_ex_help(name, *subcommands, command_file=None): return scrub( get_ex_help( name, *subcommands, example_dir=TYPER_EXAMPLES, command_file=command_file ) ) def check_html(html, help_txt, iframe_number=0, threshold=0.85): soup = bs(html, "html.parser") iframes = soup.find_all("iframe") iframe = iframes[iframe_number] assert iframe is not None iframe_src = bs(iframe.attrs["srcdoc"], "html.parser") assert iframe_src is not None code = iframe_src.find("code") assert code is not None assert similarity(code.text, help_txt) > threshold return code.text def check_svg(html, help_txt, svg_number=0, threshold=0.75): soup = bs(html, "html.parser") svg = soup.find_all("svg")[svg_number] assert svg is not None txt = svg.text.strip().replace("\xa0", " ") assert similarity(txt, help_txt) > threshold return txt def check_text(html, help_txt, txt_number=0, threshold=0.95): soup = bs(html, "html.parser") txt = soup.find_all("pre")[txt_number] txt = txt.text.strip() for element in ["
", "", "", "
"]: txt = txt.strip(element) assert txt is not None sim = similarity(txt, help_txt) assert sim > threshold, f"{sim} is below threshold {threshold}" return txt def test_click_ex_validation(): clear_callbacks() bld_dir, html = build_example("validation", "html") help_txt = get_click_ex_help("validation") check_html(html, help_txt) assert check_callback("typer_render_html") assert check_callback("typer_get_iframe_height") assert not check_callback("typer_get_web_driver") check_svg(html, help_txt, threshold=0.7) check_text(html, help_txt) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_termui(): """ tests :make-sections: and :show-nested: options """ clear_callbacks() bld_dir, html = build_example("termui", "html") help_txt = get_click_ex_help("termui") clear_help = get_click_ex_help("termui", "clear") colordemo_help = get_click_ex_help("termui", "colordemo") edit_help = get_click_ex_help("termui", "edit") locate_help = get_click_ex_help("termui", "locate") menu_help = get_click_ex_help("termui", "menu") open_help = get_click_ex_help("termui", "open") pager_help = get_click_ex_help("termui", "pager") pause_help = get_click_ex_help("termui", "pause") progress_help = get_click_ex_help("termui", "progress") # verifies :show-nested: check_html(html, help_txt) check_html(html, clear_help, 1) check_html(html, colordemo_help, 2) check_html(html, edit_help, 3) check_html(html, locate_help, 4) check_html(html, menu_help, 5) check_html(html, open_help, 6) check_html(html, pager_help, 7) check_html(html, pause_help, 8) check_html(html, progress_help, 9) check_html(html, menu_help, 10) # verify :make-sections: soup = bs(html, "html.parser") assert soup.find("section").find("h1").text.startswith("termui") assert soup.find_all("section")[1].find("h2").text.startswith("clear") assert soup.find_all("section")[2].find("h2").text.startswith("colordemo") assert soup.find_all("section")[3].find("h2").text.startswith("edit") assert soup.find_all("section")[4].find("h2").text.startswith("locate") assert soup.find_all("section")[5].find("h2").text.startswith("menu") assert soup.find_all("section")[6].find("h2").text.startswith("open") assert soup.find_all("section")[7].find("h2").text.startswith("pager") assert soup.find_all("section")[8].find("h2").text.startswith("pause") assert soup.find_all("section")[9].find("h2").text.startswith("progress") assert soup.find_all("section")[10].find("h1").text.startswith("termui menu") # verify correct name of subcommand assert ( "Usage: termui menu [OPTIONS]" in bs(soup.find_all("iframe")[10].attrs["srcdoc"], "html.parser") .find("code") .text ) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_repo(): """ tests :make-sections: and :show-nested: options """ clear_callbacks() bld_dir, html = build_example("repo", "html") help_txt = get_click_ex_help("repo") clone_help = get_click_ex_help("repo", "clone") commit_help = get_click_ex_help("repo", "commit") copy_help = get_click_ex_help("repo", "copy") delete_help = get_click_ex_help("repo", "delete") setuser_help = get_click_ex_help("repo", "setuser") # verifies :show-nested: check_html(html, help_txt) check_html(html, clone_help, 1) check_html(html, commit_help, 2) check_html(html, copy_help, 3) check_html(html, delete_help, 4) check_html(html, setuser_help, 5) # verify :make-sections: soup = bs(html, "html.parser") assert len(soup.find_all("section")) == 0, "Should not have rendered any sections" if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_naval(): """ tests :make-sections: and :show-nested: options for multi level hierarchies """ clear_callbacks() bld_dir, html = build_example("naval", "html") help_txt = get_click_ex_help("naval") mine_help = get_click_ex_help("naval", "mine") mine_remove_help = get_click_ex_help("naval", "mine", "remove") mine_set_help = get_click_ex_help("naval", "mine", "set") ship_help = get_click_ex_help("naval", "ship") ship_move_help = get_click_ex_help("naval", "ship", "move") ship_new_help = get_click_ex_help("naval", "ship", "new") ship_shoot_help = get_click_ex_help("naval", "ship", "shoot") # verifies :show-nested: check_svg(html, help_txt) check_svg(html, mine_help, 1) check_svg(html, mine_remove_help, 2, threshold=0.65) check_svg(html, mine_set_help, 3, threshold=0.60) check_svg(html, ship_help, 4) check_svg(html, ship_move_help, 5, threshold=0.52) check_svg(html, ship_new_help, 6) check_svg(html, ship_shoot_help, 7, threshold=0.57) check_svg(html, ship_new_help, 8) # verify :make-sections: soup = bs(html, "html.parser") assert len(soup.find_all("section")) == 9, "Should have rendered 8 sections" soup = bs(html, "html.parser") assert soup.find("section").find("h1").text.startswith("naval") assert soup.find_all("section")[1].find("h2").text.startswith("mine") assert soup.find_all("section")[2].find("h3").text.startswith("remove") assert soup.find_all("section")[3].find("h3").text.startswith("set") assert soup.find_all("section")[4].find("h2").text.startswith("ship") assert soup.find_all("section")[5].find("h3").text.startswith("move") assert soup.find_all("section")[6].find("h3").text.startswith("new") assert soup.find_all("section")[7].find("h3").text.startswith("shoot") assert soup.find_all("section")[8].find("h1").text.startswith("naval ship new") if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_inout(): """ tests :make-sections: and :show-nested: options for multi level hierarchies """ clear_callbacks() bld_dir, html = build_example("inout", "html") help_txt = get_click_ex_help("inout") # verifies :show-nested: check_html(html, help_txt) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_complex(): """ tests :make-sections: and :show-nested: options for multi level hierarchies """ clear_callbacks() bld_dir, html = build_example("complex", "html") check_text( html, """ Usage: complex [OPTIONS] COMMAND [ARGS]... A complex command line interface. ╭─ Options ─────────────────────────────────────────────────────╮ │ --home DIRECTORY Changes the folder to operate │ │ on. │ │ --verbose -v Enables verbose mode. │ │ --help Show this message and exit. │ ╰───────────────────────────────────────────────────────────────╯ ╭─ Commands ────────────────────────────────────────────────────╮ │ init Initializes a repo. │ │ status Shows file changes. │ ╰───────────────────────────────────────────────────────────────╯ """, threshold=0.8, ) check_text( html, """ Usage: complex init [OPTIONS] [PATH] Initializes a repository. ╭─ Arguments ───────────────────────────────────────────────────╮ │ path [PATH] │ ╰───────────────────────────────────────────────────────────────╯ ╭─ Options ─────────────────────────────────────────────────────╮ │ --help Show this message and exit. │ ╰───────────────────────────────────────────────────────────────╯ """, 1, ) check_text( html, """ Usage: complex status [OPTIONS] Shows file changes in the current working directory. ╭─ Options ─────────────────────────────────────────────────────╮ │ --help Show this message and exit. │ ╰───────────────────────────────────────────────────────────────╯ """, 2, ) soup = bs(html, "html.parser") assert soup.find("section").find("h1").text.startswith("complex") for idx, cmd in enumerate(["init", "status"]): assert soup.find_all("section")[idx + 1].find("h2").text.startswith(cmd) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_completion(): clear_callbacks() bld_dir, html = build_example("completion", "html") subcommands = ["group", "group select-user", "ls", "show-env"] helps = [ get_click_ex_help("completion"), *[get_click_ex_help("completion", *cmd.split()) for cmd in subcommands], ] for idx, help in enumerate(helps): check_text(html, help, idx, threshold=0.82) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_aliases(): clear_callbacks() bld_dir, html = build_example("aliases", "html") # we test that list_commands order is honored subcommands = reversed(["alias", "clone", "commit", "pull", "push", "status"]) helps = [ get_click_ex_help("aliases"), *[get_click_ex_help("aliases", *cmd.split()) for cmd in subcommands], ] for idx, help in enumerate(helps): check_text(html, help, idx, threshold=0.82) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_ex_imagepipe(): """ tests a chained command """ clear_callbacks() bld_dir, html = build_example("imagepipe", "html") subcommands = [ "blur", "crop", "display", "emboss", "open", "paste", "resize", "save", "sharpen", "smoothen", "transpose", ] helps = [ get_click_ex_help("imagepipe"), *[get_click_ex_help("imagepipe", cmd) for cmd in subcommands], ] for idx, help in enumerate(helps): check_text(html, help, idx, threshold=0.87) check_svg(html, helps[-3], threshold=0.7) soup = bs(html, "html.parser") assert len(soup.find_all("section")) == 15, "Should have rendered 13 sections" assert soup.find("section").find("h1").text.startswith("imagepipe") for idx, cmd in enumerate(subcommands): assert soup.find_all("section")[idx + 1].find("h2").text.startswith(cmd) assert ( soup.find_all("section")[len(subcommands) + 1] .find("h1") .text.startswith("imagepipe sharpen") ) # check the cross references: assert soup.find_all("section")[-2].find("h1").text.startswith("References") def check_refs(section, local): for li, anchor in zip( section.find_all("li"), [ "imagepipe", "imagepipe-blur", "imagepipe-crop", "imagepipe-display", "imagepipe-emboss", "imagepipe-open", "imagepipe-paste", "imagepipe-resize", "imagepipe-save", "imagepipe-smoothen", "imagepipe-transpose", "imagepipe-sharpen", ], ): if local: assert li.find("a").attrs["href"] == f"#{anchor}" else: assert li.find("a").attrs["href"] == f"index.html#{anchor}" if "sharpen" not in anchor: assert li.find("a").text == " ".join(anchor.split("-")) else: # test special link text assert li.find("a").text == "sharpen" check_refs(soup.find_all("section")[-2], local=True) # check references page refs = bs((bld_dir / "references.html").read_text(), "html.parser") check_refs(refs.find_all("section")[1], local=False) assert ( refs.find_all("section")[1].find_all("a")[-1].text == ":typer:`bad-reference`" ) if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_typer_ex_reference(): clear_callbacks() html_dir, index_html = build_example( "reference", "html", example_dir=TYPER_EXAMPLES ) doc_help = check_svg( (html_dir / "reference.html").read_text(), get_typer_ex_help("reference", command_file="cli-ref"), 0, threshold=0.82, ) assert "python -m cli-ref.py" in doc_help index = bs(index_html, "html.parser") ref1, ref2, ref3 = tuple( index.find_all("section")[0].find_all("p")[0].find_all("a") ) for ref in (ref1, ref2): assert ref.text == "python -m cli-ref.py" assert ref.attrs["href"] == "reference.html#python-m-cli-ref-py" assert ref3.text == "command" assert ref3.attrs["href"] == "reference.html#python-m-cli-ref-py" def test_typer_ex_composite(): EX_DIR = TYPER_EXAMPLES / "composite/composite" cli_py = EX_DIR / "cli.py" group_py = EX_DIR / "group.py" echo_py = EX_DIR / "echo.py" try: clear_callbacks() def test_build(first=False): _, html = build_example( "composite", "html", example_dir=TYPER_EXAMPLES, clean_first=first, subprocess=True, ) # we test that list_commands order is honored subcommands = ["subgroup", "subgroup multiply", "subgroup echo", "repeat"] helps = [ get_typer_ex_help("composite", command_file="composite/cli"), *[ get_typer_ex_help( "composite", *cmd.split(), command_file="composite/cli" ) for cmd in subcommands ], ] doc_helps = [] for idx, help in enumerate(helps): doc_helps.append(check_text(html, help, idx, threshold=0.88)) return doc_helps index_html = TYPER_EXAMPLES / "composite/build/html/index.html" composite_html = TYPER_EXAMPLES / "composite/build/html/composite.html" echo_html = TYPER_EXAMPLES / "composite/build/html/echo.html" multiply_html = TYPER_EXAMPLES / "composite/build/html/multiply.html" repeat_html = TYPER_EXAMPLES / "composite/build/html/repeat.html" subgroup_html = TYPER_EXAMPLES / "composite/build/html/subgroup.html" files = [ index_html, composite_html, echo_html, multiply_html, repeat_html, subgroup_html, ] test_build(first=True) times = [pth.stat().st_mtime for pth in files] test_build() times2 = [pth.stat().st_mtime for pth in files] assert times == times2, "Rebuild was not cached!" # test that replace_in_file( cli_py, "Lets do stuff with strings.", "XX Lets do stuff with strings. XX" ) txts = test_build() times3 = [pth.stat().st_mtime for pth in files] for idx, (t3, t2) in enumerate(zip(times3, times2)): assert t3 > t2, f"file {files[idx]} not regenerated." assert "XX Lets do stuff with strings. XX" in txts[0] replace_in_file( group_py, "Subcommands are here.", "XX Subcommands are here. XX" ) helps = test_build() assert "XX Subcommands are here. XX" in helps[0] assert "XX Subcommands are here. XX" in helps[1] times4 = [pth.stat().st_mtime for pth in files] for idx, (t4, t3) in enumerate(zip(times4, times3)): if files[idx].name in ["echo.html", "multiply.html", "repeat.html"]: continue assert t4 > t3, f"file {files[idx]} not regenerated." replace_in_file( echo_py, "def echo(name: str):", "def echo(name: str, name2: str):" ) helps = test_build() assert "name2" in helps[3] times5 = [pth.stat().st_mtime for pth in files] for idx, (t5, t4) in enumerate(zip(times5, times4)): if files[idx].name in ["composite.html", "multiply.html", "repeat.html"]: continue assert t5 > t4, f"file {files[idx]} not regenerated." # check navbar navitems = list( bs(index_html.read_text()).find("div", class_="sphinxsidebar").find_all("a") ) assert navitems[1].text == "composite" assert navitems[2].text.strip() == "python -m cli.py repeat" assert navitems[3].text == "cli subgroup" assert navitems[4].text == "cli subgroup echo" assert navitems[5].text == "cli subgroup multiply" finally: os.system(f"git checkout {cli_py}") os.system(f"git checkout {group_py}") os.system(f"git checkout {echo_py}") def test_click_text_build_works(): bld_dir, text = build_example("validation", "text") help_txt = get_click_ex_help("validation") assert similarity(text, help_txt) > 0.95 assert text.count("Usage:") == 3, "Should have rendered the help 3 times as text" if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_click_latex_build_works(): """ also tests the convert-png option and typer_svg2pdf and typer_convert_png callbacks. """ bld_dir, latex = build_example("validation", "latex") help_txt = get_click_ex_help("validation") assert check_callback("typer_svg2pdf") assert check_callback("typer_convert_png") # make sure the expected text rendered at least once assert latex.count("Usage: validation [OPTIONS]") == 1 # get all pdf files from the build directory pdf = bld_dir / "validation_2a8082c3.pdf" assert (bld_dir / "validation_2a8082c3.svg").is_file() assert len(list(bld_dir.glob("**/*.pdf"))) == 1, ( "Should have rendered the help 1 time as pdf" ) assert pdf.name.split(".")[0] in latex pdf_txt = pdf_text(pdf)[0] assert similarity(pdf_txt, help_txt) > 0.95 assert len(list(bld_dir.glob("**/*.png"))) == 1, ( "Should have rendered the help 1 time as png" ) html_png = bld_dir / "validation_4697b61f.png" assert img_similarity(CLICK_EXAMPLES / "validation" / "html.png", html_png) < 9000 if bld_dir.exists(): shutil.rmtree(bld_dir.parent) def test_typer_ex_subdocdir_latex(): """ Regression test for https://github.com/sphinx-contrib/typer/issues/58 When a typer directive is in a document located in a subdirectory of the source root (e.g. via autodoc from a nested module), the image URI must be computed relative to the document's directory, not srcdir. The buggy code used ``os.path.relpath(path, self.env.srcdir)`` which resolves to the wrong location when ``self.env.docname`` contains a path separator. """ import io ex_dir = TYPER_EXAMPLES / "subdocdir" bld_dir = ex_dir / "build" shutil.rmtree(bld_dir, ignore_errors=True) warnings_io = io.StringIO() app = Sphinx( ex_dir, TYPER_EXAMPLES, bld_dir / "latex", bld_dir / "doctrees", buildername="latex", warning=warnings_io, ) app.build() assert not app.statuscode, "Sphinx build failed" # With the buggy URI computation (relative to srcdir instead of the # document's directory), Sphinx cannot find the generated PDF and emits an # "image file not readable" warning. A clean build must produce no such # warning. warning_text = warnings_io.getvalue() assert "image.not_readable" not in warning_text, ( "Image path was not resolved correctly for a directive in a " f"subdirectory document (see issue #58).\nWarnings:\n{warning_text}" ) if bld_dir.exists(): shutil.rmtree(bld_dir) def test_enums(): from sphinxcontrib.typer import RenderTarget, RenderTheme for target in RenderTarget: assert target.value == str(target) for theme in RenderTheme: assert theme.value == str(theme) sphinx-contrib-typer-8982731/tests/typer/000077500000000000000000000000001515242076300203435ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/composite/000077500000000000000000000000001515242076300223455ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/composite/composite.rst000066400000000000000000000002021515242076300250730ustar00rootroot00000000000000.. typer:: composite.cli.app :prog: composite :width: 65 :convert-png: latex :make-sections: :preferred: text sphinx-contrib-typer-8982731/tests/typer/composite/composite/000077500000000000000000000000001515242076300243475ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/composite/composite/__init__.py000066400000000000000000000000001515242076300264460ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/composite/composite/cli.py000077500000000000000000000005511515242076300254740ustar00rootroot00000000000000import typer from composite.group import app as subgroup, AlphOrder app = typer.Typer(help="Lets do stuff with strings.", cls=AlphOrder) app.add_typer(subgroup, name="subgroup") def repeat(string: str, count: int): typer.echo(string * count) app.command(help="Repeat the string a given number of times.")(repeat) if __name__ == "__main__": app() sphinx-contrib-typer-8982731/tests/typer/composite/composite/echo.py000066400000000000000000000000711515242076300256350ustar00rootroot00000000000000import typer def echo(name: str): typer.echo(name) sphinx-contrib-typer-8982731/tests/typer/composite/composite/group.py000066400000000000000000000007371515242076300260640ustar00rootroot00000000000000import typer from composite.echo import echo from composite.multiply import multiply from typer.core import TyperGroup class AlphOrder(TyperGroup): def list_commands(self, ctx): return reversed(sorted(super().list_commands(ctx))) def subgroup(): pass app = typer.Typer(help="Subcommands are here.", cls=AlphOrder, callback=subgroup) app.command(name="echo", help="Echo the string.")(echo) app.command(name="multiply", help="Multiply 2 numbers.")(multiply) sphinx-contrib-typer-8982731/tests/typer/composite/composite/multiply.py000066400000000000000000000001301515242076300265720ustar00rootroot00000000000000import typer def multiply(arg1: float, arg2: float): typer.echo(f"{arg1 * arg2}") sphinx-contrib-typer-8982731/tests/typer/composite/echo.rst000066400000000000000000000001731515242076300240160ustar00rootroot00000000000000.. typer:: composite.cli.app:subgroup:echo :width: 65 :convert-png: latex :make-sections: :preferred: text sphinx-contrib-typer-8982731/tests/typer/composite/index.rst000066400000000000000000000003521515242076300242060ustar00rootroot00000000000000.. typer:: composite.cli.app :prog: composite :preferred: text :width: 65 :make-sections: :show-nested: .. toctree:: :maxdepth: 1 :caption: Contents: composite repeat subgroup echo multiply sphinx-contrib-typer-8982731/tests/typer/composite/multiply.rst000066400000000000000000000001771515242076300247630ustar00rootroot00000000000000.. typer:: composite.cli.app:subgroup:multiply :width: 65 :convert-png: latex :make-sections: :preferred: text sphinx-contrib-typer-8982731/tests/typer/composite/repeat.rst000066400000000000000000000002271515242076300243600ustar00rootroot00000000000000.. typer:: composite.cli.app:repeat :prog: python -m cli.py repeat :width: 65 :convert-png: latex :make-sections: :preferred: text sphinx-contrib-typer-8982731/tests/typer/composite/subgroup.rst000066400000000000000000000001661515242076300247500ustar00rootroot00000000000000.. typer:: composite.cli.app:subgroup :width: 65 :convert-png: latex :make-sections: :preferred: text sphinx-contrib-typer-8982731/tests/typer/conf.py000066400000000000000000000045361515242076300216520ustar00rootroot00000000000000from datetime import datetime import sys from pathlib import Path from sphinxcontrib import typer as sphinxcontrib_typer import json import os # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # get all sub directories from here and add them to the path sys.path.append(str(Path(__file__).parent)) for path in Path(__file__).parent.iterdir(): if path.is_dir(): sys.path.append(str(path)) TEST_CALLBACKS = Path(__file__).parent / "callback_record.json" test_callbacks = {} def record_callback(callback): """crude but it works""" if TEST_CALLBACKS.is_file(): os.remove(TEST_CALLBACKS) test_callbacks[callback] = True TEST_CALLBACKS.write_text(json.dumps(test_callbacks)) # -- Project information ----------------------------------------------------- project = "SphinxContrib Typer Tests" copyright = f"2023-{datetime.now().year}, Brian Kohan" author = "Brian Kohan" # The full version, including alpha/beta/rc tags release = sphinxcontrib_typer.__version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx_rtd_theme", "sphinxcontrib.typer"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] todo_include_todos = True typer_iframe_height_padding = 40 sphinx-contrib-typer-8982731/tests/typer/reference/000077500000000000000000000000001515242076300223015ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/reference/cli-ref.py000066400000000000000000000002511515242076300241720ustar00rootroot00000000000000import typer app = typer.Typer() def reference(name: str): typer.echo(name) app.command(help="CLI ref tests.")(reference) if __name__ == "__main__": app() sphinx-contrib-typer-8982731/tests/typer/reference/index.rst000066400000000000000000000005071515242076300241440ustar00rootroot00000000000000Reference Tests --------------- This tests that references to commands like :typer:`python -m cli-ref.py` work. You can also use a section id style reference: :typer:`python-m-cli-ref-py`. You can also use link text: :typer:`command `. .. toctree:: :maxdepth: 1 :caption: Contents: reference sphinx-contrib-typer-8982731/tests/typer/reference/reference.rst000066400000000000000000000002071515242076300247700ustar00rootroot00000000000000Reference ========= .. typer:: cli-ref.app :prog: python -m cli-ref.py :width: 65 :convert-png: latex :make-sections: sphinx-contrib-typer-8982731/tests/typer/subdocdir/000077500000000000000000000000001515242076300223215ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/subdocdir/cli.py000066400000000000000000000002111515242076300234340ustar00rootroot00000000000000import typer app = typer.Typer() @app.command() def hello(name: str = "World"): """Say hello.""" typer.echo(f"Hello {name}!") sphinx-contrib-typer-8982731/tests/typer/subdocdir/index.rst000066400000000000000000000001201515242076300241530ustar00rootroot00000000000000Subdocdir Test ============== .. toctree:: :maxdepth: 1 subdir/commands sphinx-contrib-typer-8982731/tests/typer/subdocdir/subdir/000077500000000000000000000000001515242076300236115ustar00rootroot00000000000000sphinx-contrib-typer-8982731/tests/typer/subdocdir/subdir/commands.rst000066400000000000000000000001321515242076300261400ustar00rootroot00000000000000Commands ======== .. typer:: cli.app :prog: hello :preferred: svg :width: 65