pax_global_header00006660000000000000000000000064146133110750014513gustar00rootroot0000000000000052 comment=f87e6d30d7d8ea313a6b05b89f99c6baca40bb3f sphinx-gallery-0.16.0/000077500000000000000000000000001461331107500145455ustar00rootroot00000000000000sphinx-gallery-0.16.0/.circleci/000077500000000000000000000000001461331107500164005ustar00rootroot00000000000000sphinx-gallery-0.16.0/.circleci/config.yml000066400000000000000000000115641461331107500203770ustar00rootroot00000000000000version: 2.1 # Aliases to reuse _imageconfig: &imageconfig docker: - image: cimg/base:2022.10-22.04 commands: bash_env: steps: - run: name: Set BASH_ENV command: | ./.circleci/setup_bash.sh jobs: setup_env: <<: *imageconfig steps: # Get our data and merge with upstream - checkout - run: name: Merge with upstream command: | echo $(git log -1 --pretty=%B) | tee gitlog.txt echo ${CI_PULL_REQUEST//*pull\//} | tee merge.txt if [[ $(cat merge.txt) != "" ]] && [[ $(cat gitlog.txt) != *"[circle nomerge]"* ]]; then echo "Merging $(cat merge.txt)"; git pull --ff-only origin "refs/pull/$(cat merge.txt)/merge"; fi - bash_env - restore_cache: keys: - cache-pip-1 - run: name: Pip # for VTK: https://gitlab.kitware.com/vtk/vtk/-/packages/102 command: | pip install --upgrade --only-binary ":all:" pip setuptools pip install --upgrade --only-binary ":all:" \ numpy matplotlib seaborn statsmodels pillow joblib "sphinx!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6" pytest traits pyvista memory_profiler "ipython!=8.7.0" plotly graphviz "docutils>=0.18" imageio pydata-sphinx-theme \ "jupyterlite-sphinx>=0.8.0,<0.9.0" "jupyterlite-pyodide-kernel<0.1.0" libarchive-c pip uninstall -yq vtk # pyvista installs vtk above pip install --upgrade --only-binary ":all" --extra-index-url https://wheels.vtk.org vtk-osmesa - save_cache: key: cache-pip-1 paths: - ~/.cache/pip - run: name: Test libs command: | python -c "import pyvista;print(pyvista.Report())" - run: name: Install command: | pip install -e . - persist_to_workspace: root: ~/ paths: - project - python_env build_docs: <<: *imageconfig steps: - attach_workspace: at: ~/ - bash_env - run: sphinx-build doc doc/_build/html -nW --keep-going -b html 2>&1 | tee sphinx_log.txt - run: name: Check sphinx log for warnings (which are treated as errors) when: always command: | ! grep "^.* WARNING: .*$" sphinx_log.txt - store_artifacts: path: doc/_build/html/ destination: html - store_test_results: path: doc/_build/html/ - persist_to_workspace: root: doc/_build/html paths: . build_tinyhtml: <<: *imageconfig steps: - attach_workspace: at: ~/ - bash_env - run: make -C sphinx_gallery/tests/tinybuild/doc clean html - store_artifacts: path: sphinx_gallery/tests/tinybuild/doc/_build/html destination: tiny_html build_latexpdf: <<: *imageconfig steps: - attach_workspace: at: ~/ - bash_env - run: name: Get LaTeX tools command: | sudo apt install texlive texlive-latex-extra latexmk tex-gyre - run: name: latexpdf command: | make -C sphinx_gallery/tests/tinybuild/doc clean latexpdf - store_artifacts: path: sphinx_gallery/tests/tinybuild/doc/_build/latex/ destination: latex deploy_dev: docker: - image: cimg/base:current steps: - checkout - add_ssh_keys: fingerprints: - "87:18:18:25:10:8c:29:0f:25:bd:28:b1:4f:cd:af:96" - attach_workspace: at: html - run: ./.circleci/docs_deploy.sh html dev deploy_stable: docker: - image: cimg/base:current steps: - checkout - add_ssh_keys: fingerprints: - "87:18:18:25:10:8c:29:0f:25:bd:28:b1:4f:cd:af:96" - attach_workspace: at: html - run: ./.circleci/docs_deploy.sh html stable workflows: version: 2 default: jobs: # https://circleci.com/docs/2.0/workflows/#executing-workflows-for-a-git-tag # Run for all branches and tags - setup_env: &filter_tags filters: tags: only: /.*/ - build_docs: requires: - setup_env <<: *filter_tags - build_tinyhtml: requires: - setup_env <<: *filter_tags - build_latexpdf: requires: - setup_env <<: *filter_tags # Run for master branch - deploy_dev: requires: - build_docs filters: branches: only: master # Run for tags named vX.Y.Z - deploy_stable: requires: - build_docs filters: branches: ignore: /.*/ tags: only: /^v\d+\.\d+\.\d+$/ sphinx-gallery-0.16.0/.circleci/docs_deploy.sh000077500000000000000000000025221461331107500212440ustar00rootroot00000000000000#!/bin/sh # ideas used from https://gist.github.com/motemen/8595451 # abort the script if there is a non-zero error set -e # show where we are on the machine pwd ls ${PWD} siteSource="$1" destDir="$2" if [ ! -d "$siteSource" ] || [ -z "$destDir" ]; then echo "Usage: $0 , got: " echo " $@" exit 1 fi # now lets setup the docs repo so we can update them with the current build git config --global user.email "Circle CI" > /dev/null 2>&1 git config --global user.name "bot@try.out" > /dev/null 2>&1 git clone git@github.com:sphinx-gallery/sphinx-gallery.github.io.git siteSource="${PWD}/${siteSource}" # Stuff that we persist to workspace that we don't want rm -Rf "${siteSource}/project" rm -Rf "${siteSource}/python_env" cd sphinx-gallery.github.io/ mkdir -p ${destDir} destDir="${PWD}/${destDir}" echo "Copying ${siteSource} to ${destDir}" # copy over or recompile the new site git rm -rf ${destDir} cp -a ${siteSource} ${destDir} cp dev/binder/requirements.txt binder/requirements.txt # stage any changes and new files git add -A # now commit git commit --allow-empty -m "Update the docs: ${CIRCLE_BUILD_URL}" # and push, but send any output to /dev/null to hide anything sensitive git push --force --quiet origin master > /dev/null 2>&1 # go back to where we started cd .. echo "Finished Deployment!" sphinx-gallery-0.16.0/.circleci/setup_bash.sh000077500000000000000000000005201461331107500210710ustar00rootroot00000000000000#!/bin/bash set -eo pipefail echo "set -eo pipefail" >> "$BASH_ENV" sudo apt update sudo apt --no-install-recommends install -yq optipng graphviz python3-venv python3 -m venv ~/python_env source ~/python_env/bin/activate echo "source ~/python_env/bin/activate" >> "$BASH_ENV" echo "Python: $(which python)" echo "pip: $(which pip)" sphinx-gallery-0.16.0/.git-blame-ignore-revs000066400000000000000000000001631461331107500206450ustar00rootroot000000000000009f0d05c5af5c5d88fc2381d2a4ea01a2085a1880 # black 1ca0ecf676f880e1f0353ad7aa132d893a175896 # yamllint indentation sphinx-gallery-0.16.0/.github/000077500000000000000000000000001461331107500161055ustar00rootroot00000000000000sphinx-gallery-0.16.0/.github/install.sh000077500000000000000000000036011461331107500201120ustar00rootroot00000000000000#!/bin/bash # # License: 3-clause BSD set -eo pipefail python -m pip install --upgrade pip setuptools wheel PLATFORM=$(python -c "import platform; print(platform.system())") if [ "$DISTRIB" == "mamba" ]; then conda config --set solver libmamba # memory_profiler is unreliable on macOS and Windows (lots of zombie processes) if [ "$PLATFORM" != "Linux" ]; then conda remove -y memory_profiler fi PIP_DEPENDENCIES="jupyterlite-sphinx>=0.8.0,<0.9.0 jupyterlite-pyodide-kernel<0.1.0 libarchive-c numpy" elif [ "$DISTRIB" == "minimal" ]; then PIP_DEPENDENCIES="" elif [ "$DISTRIB" == "pip" ]; then PIP_DEPENDENCIES="-r dev-requirements.txt pillow pyqt6" # No VTK on Python 3.12 pip yet if [[ "$(python -c "import sys; print(sys.version)")" != "3.12"* ]]; then PIP_DEPENDENCIES="$PIP_DEPENDENCIES vtk" fi else echo "invalid value for DISTRIB environment variable: $DISTRIB" exit 1 fi # Sphinx version if [ "$SPHINX_VERSION" == "dev" ]; then PIP_DEPENDENCIES="--upgrade --pre https://api.github.com/repos/sphinx-doc/sphinx/zipball/master --default-timeout=60 --extra-index-url 'https://pypi.anaconda.org/scientific-python-nightly-wheels/simple' $PIP_DEPENDENCIES" elif [ "$SPHINX_VERSION" != "default" ]; then PIP_DEPENDENCIES="sphinx==${SPHINX_VERSION}.* $PIP_DEPENDENCIES" else PIP_DEPENDENCIES="sphinx!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6 $PIP_DEPENDENCIES" fi set -x pip install $EXTRA_ARGS $PIP_DEPENDENCIES pytest pytest-cov coverage pydata-sphinx-theme lxml -e . set +x # "pip install pygraphviz" does not guarantee graphviz binaries exist if [[ "$DISTRIB" != "mamba" ]]; then if [[ "$PLATFORM" == "Linux" ]]; then sudo apt install graphviz else # could use brew on macOS pip but it'll take time to install echo "Removing pygraphviz on $PLATFORM when DISTRIB=$DISTRIB" pip uninstall -y graphviz fi fi sphinx-gallery-0.16.0/.github/workflows/000077500000000000000000000000001461331107500201425ustar00rootroot00000000000000sphinx-gallery-0.16.0/.github/workflows/circle_artifacts.yml000066400000000000000000000007251461331107500241720ustar00rootroot00000000000000on: [status] # yamllint disable-line rule:truthy jobs: circleci_artifacts_redirector_job: runs-on: ubuntu-latest name: Run CircleCI artifacts redirector steps: - name: GitHub Action step uses: larsoner/circleci-artifacts-redirector-action@master with: repo-token: ${{ secrets.GITHUB_TOKEN }} api-token: ${{ secrets.CIRCLECI_TOKEN }} artifact-path: 0/html/index.html circleci-jobs: build_docs sphinx-gallery-0.16.0/.github/workflows/label-check.yml000066400000000000000000000014251461331107500230210ustar00rootroot00000000000000name: Labels on: # yamllint disable-line rule:truthy pull_request: types: - opened - reopened - labeled - unlabeled - synchronize env: LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }} jobs: check-type-label: name: Please wait for a maintainer to add a label runs-on: ubuntu-latest # skip for draft PRs if: github.event.pull_request.draft == false steps: - run: | echo "Labels: $LABELS" if [[ "$LABELS" != *"bug"* ]] && [[ "$LABELS" != *"enhancement"* ]] && [[ "$LABELS" != *"api"* ]] && [[ "$LABELS" != *"maintenance"* ]] && [[ "$LABELS" != *"documentation"* ]]; then echo "A maintainer needs to add an appropriate label before merge." exit 1 fi sphinx-gallery-0.16.0/.github/workflows/release.yml000066400000000000000000000025271461331107500223130ustar00rootroot00000000000000# Upload a Python Package using Twine when a release is created # Adapted from mne-bids-pipeline name: Build on: # yamllint disable-line rule:truthy release: types: [published] push: branches: - master pull_request: branches: - master permissions: contents: read jobs: package: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build --sdist --wheel - name: Check package run: twine check --strict dist/* - name: Check env vars run: | echo "Triggered by: ${{ github.event_name }}" - uses: actions/upload-artifact@v3 with: name: dist path: dist # PyPI on release pypi: needs: package runs-on: ubuntu-latest if: github.event_name == 'release' steps: - uses: actions/download-artifact@v3 with: name: dist path: dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} sphinx-gallery-0.16.0/.github/workflows/tests.yml000066400000000000000000000071631461331107500220360ustar00rootroot00000000000000name: 'Tests' concurrency: group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} cancel-in-progress: true on: # yamllint disable-line rule:truthy push: branches: ['master'] pull_request: branches: ['master'] permissions: contents: read jobs: pytest: name: '${{ matrix.os }} / ${{ matrix.python }} / ${{ matrix.distrib }} / ${{ matrix.sphinx_version }}' timeout-minutes: 30 continue-on-error: true runs-on: ${{ matrix.os }} defaults: run: shell: bash -el {0} env: PYTHON_VERSION: '${{ matrix.python }}' SPHINX_VERSION: '${{ matrix.sphinx_version }}' DISTRIB: '${{ matrix.distrib }}' strategy: matrix: include: - os: ubuntu-latest # newest possible python: '3.12' sphinx_version: dev distrib: pip locale: C - os: ubuntu-latest # oldest supported Python and Sphinx python: '3.8' sphinx_version: '4' distrib: mamba - os: ubuntu-latest python: '3.11' sphinx_version: '5' distrib: pip - os: ubuntu-latest python: '3.11' sphinx_version: '6' distrib: mamba - os: ubuntu-latest python: '3.8' sphinx_version: '7' distrib: minimal - os: macos-latest python: '3.11' sphinx_version: 'default' distrib: mamba # only use mamba on macOS to avoid Python shell issues - os: windows-latest python: '3.11' sphinx_version: 'default' distrib: pip steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Xvfb/OpenGL - uses: pyvista/setup-headless-display-action@main with: qt: true pyvista: false # Python (if pip) - uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} if: matrix.distrib != 'mamba' # Python (if conda) - uses: mamba-org/setup-micromamba@v1 with: environment-name: test init-shell: bash create-args: 'python=${{ env.PYTHON_VERSION }} pip numpy setuptools matplotlib pillow pytest pytest-cov coverage seaborn statsmodels plotly joblib wheel libiconv pygraphviz memory_profiler ipython pypandoc lxml conda-libmamba-solver mamba' if: matrix.distrib == 'mamba' # Make sure that things work even if the locale is set to C (which # effectively means ASCII). Some of the input rst files have unicode # characters and we need to deal with this gracefully. - name: Set env vars shell: bash -e {0} run: | echo "LC_ALL=C" >> $GITHUB_ENV echo "LANG=C" >> $GITHUB_ENV echo "LC_CTYPE=C" >> $GITHUB_ENV if: matrix.locale == 'C' - run: .github/install.sh - run: pytest sphinx_gallery -v --tb=short - name: Remove incompatible doc config entries run: | if [[ "$(uname)" == "Darwin" ]]; then CMD="sed -i ''" else CMD="sed -i" fi $CMD "/show_memory/d" doc/conf.py $CMD "/compress_images/d" doc/conf.py if: ${{ !startsWith(matrix.os, 'ubuntu') }} # pydata-sphinx-theme is not compatible with Sphinx 4 - run: make -C doc SPHINXOPTS= html-noplot if: matrix.distrib != 'minimal' && matrix.sphinx_version != '4' - run: make -C doc SPHINXOPTS= html -j 2 if: matrix.distrib != 'minimal' && matrix.sphinx_version != '4' - uses: codecov/codecov-action@v3 sphinx-gallery-0.16.0/.github_changelog_generator000066400000000000000000000005171461331107500221100ustar00rootroot00000000000000project=sphinx-gallery user=sphinx-gallery add-sections={"api":{"prefix":"**API changes**","labels":["api"]},"documentation":{"prefix":"**Documentation**","labels":["documentation"]},"maintenance":{"prefix": "**Project maintenance**","labels":["maintenance"]}} max-issues=2 issues=no compare-link=no cache-file=.github_changelog_cache sphinx-gallery-0.16.0/.gitignore000066400000000000000000000025171461331107500165420ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] .coverage* .vscode # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg .eggs # 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/ .coverage .cache nosetests.xml coverage.xml *.orig .pytest_cache CHANGELOG.md CHANGELOG.rst junit-results.xml .github_changelog_generator/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation doc/_build/ doc/auto_examples doc/auto_plotly_examples doc/auto_pyvista_examples doc/tutorials/ doc/gen_modules/ doc/sg_execution_times.rst # Test builds sphinx_gallery/tests/tinybuild/doc/gen_modules/ sphinx_gallery/tests/tinybuild/doc/auto_*/ sphinx_gallery/tests/tinybuild/doc/_build/ sphinx_gallery/tests/tinybuild/doc/sg_execution_times.rst sphinx_gallery/tests/tinybuild/tiny_html/ junit-results.xml # PyBuilder target/ # General temporary files to ignore *.swp *.swo # VisualStudioCode .history # Jupyter notebooks .ipynb_checkpoints # Default JupyterLite content jupyterlite_contents sphinx-gallery-0.16.0/.pre-commit-config.yaml000066400000000000000000000022121461331107500210230ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.1 hooks: - id: ruff-format exclude: plot_syntaxerror - id: ruff args: ["--fix"] - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell additional_dependencies: - tomli files: ^sphinx_gallery/|^doc/|^.*_examples/|^tutorials/ types_or: [python, bib, rst, inc] - repo: https://github.com/mgedmin/check-manifest rev: "0.49" hooks: - id: check-manifest args: [--no-build-isolation] additional_dependencies: [setuptools-scm] - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint args: [--strict, -c, .yamllint.yml] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v0.9.1 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt rev: 1.8.0 hooks: - id: pyproject-fmt additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject rev: v0.16 hooks: - id: validate-pyproject sphinx-gallery-0.16.0/.yamllint.yml000066400000000000000000000001751461331107500172020ustar00rootroot00000000000000extends: default ignore: | .github/workflows/codeql-analysis.yml rules: line-length: disable document-start: disable sphinx-gallery-0.16.0/CHANGES.rst000066400000000000000000003261261461331107500163610ustar00rootroot00000000000000Changelog ========= v0.16.0 ------- Sphinx 7.3.0 and above changed caching and serialization checks. Now instead of passing instantiated classes like ``ResetArgv()``, classes like ``FileNameSortKey``, or callables like ``notebook_modification_function`` in ``sphinx_gallery_conf``, you should pass fully qualified name strings to classes or callables. If you change to using name strings, you can simply use a function as the use of classes to ensure a stable ``__repr__`` would be redundant. See :ref:`importing_callables` for details. **Implemented enhancements:** - ENH: Allow plain list as subsection_order and support a wildcard `#1295 `__ (`timhoffm `__) - [ENH] Minigallery can take arbitrary files/glob patterns as input `#1226 `__ (`story645 `__) **Fixed bugs:** - BUG: Fix serialization with Sphinx 7.3 `#1289 `__ (`larsoner `__) - ENH: minigallery_sort_order on full path `#1253 `__ (`story645 `__) - BUG: ``UnicodeDecodeError`` in recommender `#1244 `__ (`Charlie-XIAO `__) **Documentation** - DOC Update FFMpeg note in conf animation docs `#1292 `__ (`lucyleeow `__) - readme: adding quickstart section `#1291 `__ (`Borda `__) - readme: add link to docs `#1288 `__ (`Borda `__) - DOC Clarify sub level example gallery `#1281 `__ (`lucyleeow `__) - DOC Mention ``image_srcset`` config in scraper section in ``advanced.rst`` `#1280 `__ (`lucyleeow `__) - BUG: Fix errors in example usage of ignore_repr_types and reset_argv `#1275 `__ (`speth `__) - DOC Use ‘nested_sections’ ``True`` for docs `#1263 `__ (`lucyleeow `__) - fix: Missing full stop in download message `#1255 `__ (`AlejandroFernandezLuces `__) - Add HyperSpy and kikuchipy to ‘who uses’ `#1247 `__ (`jlaehne `__) - DOC: Fix formatting in contribute.rst `#1237 `__ (`StefRe `__) **Project maintenance** - [pre-commit.ci] pre-commit autoupdate `#1294 `__ (`pre-commit-ci[bot] `__) - Fix typo in ``test_fileno`` `#1287 `__ (`lucyleeow `__) - [pre-commit.ci] pre-commit autoupdate `#1284 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1279 `__ (`pre-commit-ci[bot] `__) - Remove leftover config checking of ``image_srcset`` `#1278 `__ (`lucyleeow `__) - [pre-commit.ci] pre-commit autoupdate `#1277 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1273 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1272 `__ (`pre-commit-ci[bot] `__) - More informative title for ‘check label’ CI workflow `#1271 `__ (`lucyleeow `__) - pyproject: cleaning pytest config `#1269 `__ (`Borda `__) - allow call script as pkg entry `#1268 `__ (`Borda `__) - refactor: migrate to ``pyproject.toml`` `#1267 `__ (`Borda `__) - lint: enable ``sphinx-lint`` for Sphinx extension `#1266 `__ (`Borda `__) - ci: associate ``install.sh`` with used job `#1265 `__ (`Borda `__) - lint: switch from Black to Ruff’s “Black” `#1264 `__ (`Borda `__) - [pre-commit.ci] pre-commit autoupdate `#1260 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1257 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1256 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1252 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1251 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1249 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1248 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1246 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1245 `__ (`pre-commit-ci[bot] `__) - Fix AST deprecation warnings `#1242 `__ (`QuLogic `__) - Simplify Matplotlib scraper `#1241 `__ (`QuLogic `__) - [pre-commit.ci] pre-commit autoupdate `#1239 `__ (`pre-commit-ci[bot] `__) - MAINT: Fix deployment `#1236 `__ (`larsoner `__) - MAINT Bump version and update ``maintainers.rst`` `#1234 `__ (`lucyleeow `__) v0.15.0 ------- Support for Python 3.7 dropped in this release. Requirement is now Python >=3.8. Pillow added as a dependency. **Implemented enhancements:** - ENH: Improve logging visibility of errors and filenames `#1225 `__ (`larsoner `__) - ENH: Improve API usage graph `#1203 `__ (`larsoner `__) - ENH: Always write sg_execution_times and make DataTable `#1198 `__ (`larsoner `__) - ENH: Write all computation times `#1197 `__ (`larsoner `__) - ENH: Support source files in any language `#1192 `__ (`speth `__) - FEA Add examples recommender system `#1125 `__ (`ArturoAmorQ `__) **Fixed bugs:** - FIX Copy JupyterLite contents early so it runs before jupyterlite_sphinx build-finished `#1213 `__ (`lesteve `__) - BUG: Fix bug with orphan sg_api_usage `#1207 `__ (`larsoner `__) - MAINT Fix check for mismatched “ignore” blocks `#1193 `__ (`speth `__) - Avoid importing new modules in backrefs `#1177 `__ (`aganders3 `__) **Documentation** - DOC Put configuration list under headings `#1230 `__ (`lucyleeow `__) - DOC: contributing guide `#1223 `__ (`story645 `__) - DOC Note support for python 3.7 dropped in release notes `#1199 `__ (`lucyleeow `__) **Project maintenance** - [pre-commit.ci] pre-commit autoupdate `#1231 `__ (`pre-commit-ci[bot] `__) - MAINT Add ``extras_require`` in ``setup.py`` `#1229 `__ (`lucyleeow `__) - [pre-commit.ci] pre-commit autoupdate `#1227 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1224 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1219 `__ (`pre-commit-ci[bot] `__) - MAINT: pydata-sphinx-theme `#1218 `__ (`larsoner `__) - MAINT: Improve CircleCI time `#1216 `__ (`larsoner `__) - [pre-commit.ci] pre-commit autoupdate `#1215 `__ (`pre-commit-ci[bot] `__) - MAINT: Move to GHA `#1214 `__ (`larsoner `__) - [pre-commit.ci] pre-commit autoupdate `#1206 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1201 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1196 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1194 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1191 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1189 `__ (`pre-commit-ci[bot] `__) - [pre-commit.ci] pre-commit autoupdate `#1187 `__ (`pre-commit-ci[bot] `__) - MAINT: Bump ver `#1185 `__ (`larsoner `__) v0.14.0 ------- **Implemented enhancements:** - MAINT Update backreferences docs and add tests `#1154 `__ (`lucyleeow `__) - Remove extra spaces in reported running time `#1147 `__ (`stefanv `__) **Fixed bugs:** - MAINT: Fix for Sphinx 7.2 `#1176 `__ (`larsoner `__) - updated mpl gui warning catcher to new error message `#1160 `__ (`story645 `__) - Ensure consistent encoding for md5sum generation `#1159 `__ (`sdhiscocks `__) - Maint: Fix ``app.builder.outdir`` as Sphinx now using pathlib `#1155 `__ (`lucyleeow `__) - Make \_LoggingTee compatible with TextIO `#1151 `__ (`o-laurent `__) - MAINT: Replace build_sphinx with sphinx-build `#1139 `__ (`oscargus `__) - Set table.dataframe width to auto in CSS file close #1128 `#1137 `__ (`photoniker `__) **Documentation** - DOC Fix typo in ``_get_docstring_and_rest`` docstring `#1182 `__ (`lucyleeow `__) - DOC Fix Jupyterlite config example in ``configuration.rst`` `#1181 `__ (`lucyleeow `__) - DOC Update basics gallery name `#1153 `__ (`lucyleeow `__) - DOC: Add link to sphinxcontrib-svg2pdfconverter `#1145 `__ (`oscargus `__) - MNT: Add a few badges `#1143 `__ (`oscargus `__) - MAINT: Fix Zenodo reference `#1140 `__ (`oscargus `__) - Add OpenTURNS to “who uses” list `#1133 `__ (`jschueller `__) - Correctly hide download buttons `#1131 `__ (`timhoffm `__) **Project maintenance** - MAINT: Force PRs to be labeled properly `#1183 `__ (`larsoner `__) - MAINT Add to ``test_identify_names`` so class property tested `#1180 `__ (`lucyleeow `__) - MAINT Lint - fix ast node type in docstrings `#1179 `__ (`lucyleeow `__) - MAINT Move ``figure_rst`` path testing to own unit test `#1173 `__ (`lucyleeow `__) - MAINT Remove unused parametrize in ``test_figure_rst_srcset`` `#1172 `__ (`lucyleeow `__) - MAINT Parametrize notebook first/last cell test `#1171 `__ (`lucyleeow `__) - MAINT Lint api usage `#1170 `__ (`lucyleeow `__) - MAINT Fix lint, clean and expand docstrings `#1169 `__ (`lucyleeow `__) - [pre-commit.ci] pre-commit autoupdate `#1167 `__ (`pre-commit-ci[bot] `__) - MAINT: yamllint `#1166 `__ (`larsoner `__) - ENH: Use pre-commit `#1165 `__ (`larsoner `__) - MAINT: black . `#1164 `__ (`larsoner `__) - MAINT: Make outdated check better `#1161 `__ (`larsoner `__) - Use pathlib for url ``_embed_code_links`` `#1157 `__ (`lucyleeow `__) - MAINT: Speed up conda solving `#1156 `__ (`larsoner `__) - MNT: Change % formatting to f-strings `#1135 `__ (`StefRe `__) - MAINT: Update deps and intersphinx links `#1132 `__ (`larsoner `__) v0.13.0 ------- **Implemented enhancements:** - ENH: Create backreferences for default roles `#1122 `__ (`StefRe `__) - ENH raise error in check_jupyterlite_conf with unknown key `#1119 `__ (`lesteve `__) - ENH Add functionality to modify Jupyterlite notebooks based on their content `#1113 `__ (`lesteve `__) - ENH: Add support for WebP `#1111 `__ (`StefRe `__) **Fixed bugs:** - ENH Clean-up code by early initialization of sphinx_gallery_conf `#1120 `__ (`lesteve `__) - FIX JupyterLite button links `#1115 `__ (`lesteve `__) - Fix thumbnail text formatting `#1108 `__ (`StefRe `__) - Fix JupyterLite URL with nested gallery folders `#1105 `__ (`lesteve `__) - Avoid potentially changing the matplotlib backend when scraping `#1102 `__ (`ayshih `__) - Remove default ‘%matplotlib inline’ line `#1099 `__ (`ArturoAmorQ `__) - FIX: Only ANSI sanitize non-HTML output `#1097 `__ (`sdhiscocks `__) - BUG: Fix bug with show_api_usage `#1095 `__ (`larsoner `__) - FIX: Add blank line at end of table of contents block `#1094 `__ (`sdhiscocks `__) **API changes** - API: Remove deprecated mayavi support `#1090 `__ (`larsoner `__) **Documentation** - Add reference to qtgallery (Qt scraper) `#1126 `__ (`aganders3 `__) - DOC: Unify abbreviations of reStructuredText `#1118 `__ (`StefRe `__) - Add PyGMT to list “Who uses Sphinx-Gallery” `#1114 `__ (`yvonnefroehlich `__) - DOC Update JupyterLite doc after JupyterLite 0.1.0b19 release `#1106 `__ (`lesteve `__) - Fix project list `#1101 `__ (`StefRe `__) - DOC: Document changes `#1098 `__ (`larsoner `__) - DOC: Document point release changes `#1096 `__ (`larsoner `__) **Project maintenance** - MAINT: Use non-aliased status_iterator `#1124 `__ (`larsoner `__) - CLN Clean up naming of early config validation `#1123 `__ (`lesteve `__) - MNT: Remove Python 2 leftovers `#1116 `__ (`StefRe `__) - MNT: Sync minimum sphinx version with README.rst `#1110 `__ (`StefRe `__) - CI Install jupyterlite-pyodide-kernel in CI `#1107 `__ (`lesteve `__) - Add test for setting a non-agg Matplotlib backend `#1104 `__ (`ayshih `__) - MAINT: Bump version to dev `#1089 `__ (`larsoner `__) v0.12.2 ------- **Fixed bugs:** - FIX: Only ANSI sanitize non-HTML output `#1097 `__ (`sdhiscocks `__) v0.12.1 ------- **Fixed bugs:** - BUG: Fix bug with show_api_usage `#1095 `__ (`larsoner `__) - FIX: Add blank line at end of table of contents block `#1094 `__ (`sdhiscocks `__) v0.12.0 ------- Support for Sphinx < 4 dropped in this release. Requirement is Sphinx >= 4. **Implemented enhancements:** - ENH: allow rst files to pass through `#1071 `__ (`jklymak `__) - Update advanced usage examples `#1045 `__ (`HealthyPear `__) - Use descriptive link text for example page header `#1040 `__ (`betatim `__) - Expose ``sphinx_gallery_conf`` in ``python_to_jupyter_cli`` `#1027 `__ (`OverLordGoldDragon `__) - DOC: fix ‘Who uses Sphinx-Gallery’ list `#1015 `__ (`StefRe `__) - [MAINT, MRG] A few small leftovers from API usage `#997 `__ (`alexrockhill `__) - [ENH, MRG] Make orphan of unused API entries `#983 `__ (`alexrockhill `__) - Jupyterlite integration `#977 `__ (`amueller `__) **Fixed bugs:** - MNT: fix subfolder README detection `#1086 `__ (`jklymak `__) - API: Deprecate mayavi scraper `#1083 `__ (`larsoner `__) - FIX: indentation fix `#1077 `__ (`jklymak `__) - Adds ``plot_gallery`` as a string by default `#1062 `__ (`melissawm `__) - Fix broken links when using dirhtml builder `#1060 `__ (`mgoulao `__) - BUG: Remove ignore blocks when remove_config_comments=True `#1059 `__ (`guberti `__) - Fixed a bug where backslashes in paths could show up in reST files `#1047 `__ (`ayshih `__) - Allow 2 decimal places in srcset `#1039 `__ (`OverLordGoldDragon `__) - Fix “``subsection_index_toctree`` referenced before assignment” `#1035 `__ (`OverLordGoldDragon `__) - [BUG, MRG] fix issue with api usage dict `#1033 `__ (`alexrockhill `__) - MAINT: Remove lingering ref `#1022 `__ (`larsoner `__) - MNT: Fix erroneous commit c6ed4e `#1021 `__ (`StefRe `__) - MNT: make “clean” behave the same on Windows as on Linux `#1020 `__ (`StefRe `__) - DOC Fix typo in scraper doc `#1018 `__ (`lucyleeow `__) - Fix outdated import `#1016 `__ (`OverLordGoldDragon `__) - FIX: role names `#1012 `__ (`StefRe `__) - Bugfix thumbnail text formatting `#1005 `__ (`alexisthual `__) - [MAINT, MRG] Add unused option for API usage, set as default `#1001 `__ (`alexrockhill `__) - FIX: No orphan `#1000 `__ (`larsoner `__) - BUG: Short circuit when disabled `#999 `__ (`larsoner `__) **Documentation** - DOC: Add note for html-noplot to suppress config warning. `#1084 `__ (`rossbar `__) - Reorder paragraphs in the minigallery documentation `#1048 `__ (`ayshih `__) - DOC: Switch to pydata-sphinx-theme `#1013 `__ (`larsoner `__) - Fix sphinx link typo in CHANGES `#996 `__ (`alexisthual `__) **Project maintenance** - MAINT: Fix CIs `#1074 `__ (`larsoner `__) - TST: gallery inventory/re-structure tinybuild `#1072 `__ (`jklymak `__) - MAINT: Rotate CircleCI key `#1064 `__ (`larsoner `__) - MAINT: Update CIs `#1061 `__ (`larsoner `__) - BUG: Fix full check `#1053 `__ (`larsoner `__) - MAINT: Work around IPython lexer bug `#1052 `__ (`larsoner `__) - MAINT: Fix CIs `#1046 `__ (`larsoner `__) - MAINT: Check CI status `#1028 `__ (`larsoner `__) - MNT: Fix required sphinx version `#1019 `__ (`StefRe `__) - BUG: Update for matplotlib `#1010 `__ (`larsoner `__) - MAINT: Bump to dev `#995 `__ (`larsoner `__) v0.11.1 ------- Support for Sphinx < 3 dropped in this release. Requirement is Sphinx >= 3. **Fixed bugs:** - BUG: Fix single column example `#993 `__ (`larsoner `__) **Implemented enhancements:** - Use Mock more in tests `#986 `__ (`QuLogic `__) - Remove old sphinx compatibility code `#985 `__ (`QuLogic `__) v0.11.0 ------- In this version, the "Out:" prefix applied to code outputs is now created from CSS pseudo-elements instead of additional real text. For more details, see `#896 `__. **Implemented enhancements:** Nesting gallery sections (i.e. gallery subfolders) was implemented in `#904 `__. This feature can be disabled (see config option ``nested_sections`` in the documentation) if the previous behaviour is prefered (`alexisthual `__) Tooltips now overlay gallery items `commit 36166cd `__. Custom CSS might need to be adapted (`alexisthual `__). - Problem in section and example title level in subgalleries `#935 `__ - Add ability to write nested ``index.rst`` `#855 `__ - Optional usage of ``module`` instead of ``module_short`` when doing backreferencing `#950 `__ (`ExtremOPS `__) - ENH: Better dark mode support `#948 `__ (`larsoner `__) - Store API reference examples thumbnails in common div `#946 `__ (`alexisthual `__) - Add flag to ignore code blocks in Python source parser `#941 `__ (`guberti `__) - Improve Jupyter notebook converter’s handling of code blocks `#940 `__ (`guberti `__) - [MRG] Changelog regarding nested sections `#926 `__ (`alexisthual `__) - Possibility to exclude implicit backreferences `#908 `__ (`StefRe `__) - [MRG] Handle nested structures `#904 `__ (`alexisthual `__) - Use pseudo-elements for ‘Out:’ prefixing `#896 `__ (`QuLogic `__) - FIX: Fix for latest pytest `#894 `__ (`larsoner `__) - Config capture_repr on file-by-file basis `#891 `__ (`StefRe `__) **Fixed bugs:** We now display gallery items using CSS grid instead of ``float`` property `#906 `__, see `migration guide `__ to adapt custom CSS for thumbnails (`alexisthual `__) - BUG: Hotfix for docopts_url `#980 `__ (`larsoner `__) - BUG: Fix bug with clicking examples `#973 `__ (`larsoner `__) - Remove test examples for seaborn warning `#971 `__ (`lesteve `__) - Fix typo `#970 `__ (`tkoyama010 `__) - Avoid matplotlib warnings in seaborn reset_module `#969 `__ (`lesteve `__) - Fix Tensorflow/Abseil compatibility `#961 `__ (`guberti `__) - syntax error fix in sphinx_gallery.downloads `#951 `__ (`photoniker `__) - Merge toctrees containing subcategories indices and examples without … `#944 `__ (`alexisthual `__) - Fix rendering of embedded URIs in Python notebooks `#943 `__ (`guberti `__) - FIX: Fix for dep `#938 `__ (`larsoner `__) - Fix typos `#934 `__ (`kianmeng `__) - MAINT: Fix CIs `#932 `__ (`larsoner `__) - MAINT: Use -nWT –keep-going on Azure `#924 `__ (`larsoner `__) - Ensures right builder conifg `#922 `__ (`ExtremOPS `__) - MAINT: Fix CIs `#920 `__ (`larsoner `__) - MAINT: Clean up namespace `#917 `__ (`larsoner `__) - FIX: Azure `#915 `__ (`larsoner `__) - [WIP] Bugfix missing parent div for mini gallery `#914 `__ (`alexisthual `__) - Honor show_signature `#909 `__ (`jschueller `__) - Css grid for thumbnails `#906 `__ (`alexisthual `__) - Fix matplotlib intersphinx url `#902 `__ (`StefRe `__) - FIX: Pin pyvista `#901 `__ (`larsoner `__) - Fix matplotlib resetter \_reset_matplotlib `#890 `__ (`StefRe `__) - Fix “Out” layout for pydata-sphinx-theme `#886 `__ (`timhoffm `__) **Documentation updates** - added RADIS in Who uses Sphinx-gallery ? `#979 `__ (`erwanp `__) - add Tonic to list of sphinx-gallery users `#972 `__ (`biphasic `__) - Add Apache TVM to user projects list `#942 `__ (`guberti `__) - DOC: fix rst link syntax in changelog `#925 `__ (`GaelVaroquaux `__) - add GitHub URL for PyPi `#923 `__ (`andriyor `__) - Add Biotite to list of user projects `#919 `__ (`padix-key `__) - MAINT: Remove LooseVersion `#916 `__ (`larsoner `__) - DOC Fix example “Identifying function names in a script” `#903 `__ (`StefRe `__) - DOC Update docs for Adding mini-galleries for API documentation `#899 `__ (`StefRe `__) - Add PyVista examples! `#888 `__ (`banesullivan `__) - Fix a few links in project lists `#883 `__ (`ixjlyons `__) v0.10.1 ------- Support for Python 3.6 dropped in this release. Requirement is Python >=3.7. **Implemented enhancements:** - Feature Request: ``reset_modules`` to be applied after each or all examples `#866 `__ - Enable ``reset_modules`` to run either before or after examples, or both `#870 `__ (`MatthewFlamm `__) **Fixed bugs:** - embed_code_links throwing `#879 `__ - ``0.10.0`` breaks ``sphinx_gallery.load_style`` `#878 `__ - Add imagesg directive in load style `#880 `__ (`lucyleeow `__) - Use bools for ‘plot_gallery’ in sphinx_gallery_conf `#863 `__ (`timhoffm `__) **Merged pull requests:** - DOC Add reference to sphinx-codeautolink `#874 `__ (`felix-hilden `__) - Add Neuraxle to “Who uses Sphinx-Gallery” `#873 `__ (`guillaume-chevalier `__) - DOC Fix typo in dummy images doc `#871 `__ (`lucyleeow `__) - CI: Fix CircleCI `#865 `__ (`larsoner `__) v0.10.0 ------- In this version, the default Sphinx-Gallery `.css` files have been updated so their names are all prepended with 'sg\_'. For more details see `#845 `_. **Implemented enhancements:** - Generalising image_scrapers facility for non-images `#833 `__ - Add a mode that fails only for rst warnings and does not run examples `#751 `__ - Add a “template”, to make it easy to get started `#555 `__ - ENH Add config that generates dummy images to prevent missing image warnings `#828 `__ (`lucyleeow `__) - ENH: add hidpi option to matplotlib_scraper and directive `#808 `__ (`jklymak `__) **Fixed bugs:** - BUG URL quote branch names and filepaths in Binder URLs `#844 `__ (`sdhiscocks `__) - Sanitize ANSI characters from generated reST: Remove `ANSI characters `_ from HTML output `#838 `__ (`agramfort `__) - Bug Pin markupsafe version in Python nightly `#831 `__ (`lucyleeow `__) - BUG Fix test_minigallery_directive failing on Windows `#830 `__ (`lucyleeow `__) - BUG Fix LaTeX Error: File \`tgtermes.sty’ not found in CI `#829 `__ (`lucyleeow `__) **Merged pull requests:** - DOC Update reset_modules documentation `#861 `__ (`lucyleeow `__) - Remove trailing whitespace `#859 `__ (`lucyleeow `__) - Add info on enabling animation support to example `#858 `__ (`dstansby `__) - Update css file names, fix documentation `#857 `__ (`lucyleeow `__) - MAINT: Fix mayavi build hang circleci `#850 `__ (`lucyleeow `__) - MAINT: Fix mayavi build hang azure CI `#848 `__ (`lucyleeow `__) - Refactor execute_code_block in gen_rst.py `#842 `__ (`lucyleeow `__) - [Maint] Remove travis `#840 `__ (`agramfort `__) - DOC Add gif to supported image extensions `#836 `__ (`lucyleeow `__) - DOC Clarifications and fixes to image_scrapers doc `#834 `__ (`jnothman `__) - DOC Update projects list in readme.rst `#826 `__ (`lucyleeow `__) - DOC Fix zenodo badge link `#825 `__ (`lucyleeow `__) - DOC Add TorchIO to users list `#824 `__ (`fepegar `__) v0.9.0 ------ Support for Python 3.5 dropped in this release. Requirement is Python >=3.6. **Implemented enhancements:** - Add a mode which “skips” an example if it fails `#789 `__ - Can sphinx_gallery_thumbnail_number support negative indexes? `#785 `__ - Configure thumbnail style `#780 `__ - ENH: Check for invalid sphinx_gallery_conf keys `#774 `__ - DOC Document how to hide download link note `#760 `__ - DOC use intersphinx references in projects_list.rst `#755 `__ - Delay output capturing to a further code block `#363 `__ - ENH: Only add minigallery if there’s something to show `#813 `__ (`NicolasHug `__) - Optional flag to defer figure scraping to the next code block `#801 `__ (`ayshih `__) - ENH: PyQt5 `#794 `__ (`larsoner `__) - Add a configuration to warn on error not fail `#792 `__ (`Cadair `__) - Let sphinx_gallery_thumbnail_number support negative indexes `#786 `__ (`seisman `__) - Make any borders introduced when rescaling images to thumbnails transparent `#781 `__ (`rossbar `__) - MAINT: Move travis CI jobs to Azure `#779 `__ (`lucyleeow `__) - ENH, DEP: Check for invalid keys, remove ancient key `#775 `__ (`larsoner `__) **Fixed bugs:** - Custom CSS for space above title target conflicts with pydata-sphinx-theme `#815 `__ - Minigalleries are generated even for objects without examples `#812 `__ - Python nightly failing due to Jinja2 import from collections.abc `#790 `__ - test_rebuild and test_error_messages failing on travis `#777 `__ - Animation not show on Read the Docs `#772 `__ - BUG: Empty code block output `#765 `__ - BUG: Fix CSS selector `#816 `__ (`larsoner `__) - MAINT: Fix test for links `#811 `__ (`larsoner `__) - Fix SVG default thumbnail support `#810 `__ (`jacobolofsson `__) - Clarify clean docs for custom gallery_dirs `#798 `__ (`timhoffm `__) - MAINT Specify Jinja2 version in azure Python nightly `#793 `__ (`lucyleeow `__) - BUG Remove if final block empty `#791 `__ (`lucyleeow `__) - Replace Travis CI badge with Azure Badge in README `#783 `__ (`sdhiscocks `__) - Point to up-to-date re documentation `#778 `__ (`dstansby `__) **Merged pull requests:** - DOC Add section on altering CSS `#820 `__ (`lucyleeow `__) - DOC Use intersphinx references in projects_list.rst `#819 `__ (`lucyleeow `__) - DOC Update CI badge `#818 `__ (`lucyleeow `__) - DOC Include SOURCEDIR in Makefile `#814 `__ (`NicolasHug `__) - DOC: add 2 projects using sphinx gallery `#807 `__ (`mfeurer `__) - DOC: clarify advanced doc wrt referencing examples `#806 `__ (`mfeurer `__) - MAINT: Add link `#800 `__ (`larsoner `__) - Add Optuna to “Who uses Optuna” `#796 `__ (`crcrpar `__) - DOC Add segment on CSS styling `#788 `__ (`lucyleeow `__) - DOC minor doc typo fixes `#787 `__ (`lucyleeow `__) - DOC Update CI links in index.rst `#784 `__ (`lucyleeow `__) v0.8.2 ------ Enables HTML animations to be rendered on readthedocs. **Implemented enhancements:** - DOC Expand on sphinx_gallery_thumbnail_path `#764 `__ (`lucyleeow `__) - ENH: Add run_stale_examples config var `#759 `__ (`larsoner `__) - Option to disable note in example header `#757 `__ - Add show_signature option `#756 `__ (`jschueller `__) - ENH: Style HTML output like jupyter `#752 `__ (`larsoner `__) - ENH: Add reST comments, read-only `#750 `__ (`larsoner `__) - Relate warnings and errors on generated rst file back to source Python file / prevent accidental writing of generated files `#725 `__ **Fixed bugs:** - Example gallery is down `#753 `__ - DOC Amend run_stale_examples command in configuration.rst `#763 `__ (`lucyleeow `__) - DOC update link in projects_list `#754 `__ (`lucyleeow `__) - Enable animations HTML to be rendered on readthedocs `#748 `__ (`sdhiscocks `__) **Merged pull requests:** - FIX: Restore whitespace `#768 `__ (`larsoner `__) - CI: Remove AppVeyor, work on Azure `#767 `__ (`larsoner `__) - DOC Standardise capitalisation of Sphinx-Gallery `#762 `__ (`lucyleeow `__) v0.8.1 ------ Fix Binder logo image file for Windows paths. **Fixed bugs:** - sphinx_gallery/tests/test_full.py::test_binder_logo_exists fails (path is clearly wrong) `#746 `__ - BUG Windows relative path error with \_static Binder logo `#744 `__ - BUG Copy Binder logo to avoid Window drive rel path error `#745 `__ (`lucyleeow `__) **Merged pull requests:** - DOC Add link to cross referencing example `#743 `__ (`lucyleeow `__) v0.8.0 ------ The default for configuration `thumbnail_size` will change from `(400, 280)` (2.5x maximum size specified by CSS) to `(320, 224)` (2x maximum size specified by CSS) in version 0.9.0. **Implemented enhancements:** - Pass command line arguments to examples `#731 `__ - Limited rst to md support in notebooks `#219 `__ - Enable ffmpeg for animations for newer matplotlib `#733 `__ (`dopplershift `__) - Implement option to pass command line args to example scripts `#732 `__ (`mschmidt87 `__) - ENH: Dont allow input `#729 `__ (`larsoner `__) - Add support for image links and data URIs for notebooks `#724 `__ (`sdhiscocks `__) - Support headings in reST to MD `#723 `__ (`sdhiscocks `__) - ENH Support pypandoc to convert rst to md for ipynb `#705 `__ (`lucyleeow `__) - ENH: Use broader def of Animation `#693 `__ (`larsoner `__) **Fixed bugs:** - \_repr_html\_ not shown on RTD `#736 `__ - Binder icon is hardcoded, which causes a loading failure with on some browsers `#735 `__ - How to scrape for images without executing example scripts `#728 `__ - sphinx-gallery/0.7.0: TypeError: ‘str’ object is not callable when building its documentation `#727 `__ - Thumbnail oversampling `#717 `__ - Working with pre-built galleries `#704 `__ - Calling “plt.show()” raises an ugly warning `#694 `__ - Searching in docs v0.6.2 stable does not work `#689 `__ - Fix logger message pypandoc `#741 `__ (`lucyleeow `__) - Use local binder logo svg `#738 `__ (`lucyleeow `__) - BUG: Fix handling of scraper error `#737 `__ (`larsoner `__) - Improve documentation of example for custom image scraper `#730 `__ (`mschmidt87 `__) - Make md5 hash independent of platform line endings `#722 `__ (`sdhiscocks `__) - MAINT: Deal with mayavi `#720 `__ (`larsoner `__) - DOC Clarify thumbnail_size and note change in default `#719 `__ (`lucyleeow `__) - BUG: Always do linking `#714 `__ (`larsoner `__) - DOC: Correctly document option `#711 `__ (`larsoner `__) - BUG Check ‘capture_repr’ and ‘ignore_repr_types’ `#709 `__ (`lucyleeow `__) - DOC Update Sphinx url `#708 `__ (`lucyleeow `__) - BUG: Use relative paths for zip downloads `#706 `__ (`pmeier `__) - FIX: Build on nightly using master `#703 `__ (`larsoner `__) - MAINT: Fix CircleCI `#701 `__ (`larsoner `__) - Enable html to be rendered on readthedocs `#700 `__ (`sdhiscocks `__) - Remove matplotlib agg warning `#696 `__ (`lucyleeow `__) **Merged pull requests:** - DOC add section on interpreting error/warnings `#740 `__ (`lucyleeow `__) - DOC Add citation details to readme `#739 `__ (`lucyleeow `__) - Plotly example for the gallery `#718 `__ (`emmanuelle `__) - DOC Specify matplotlib in animation example `#716 `__ (`lucyleeow `__) - MAINT: Bump pytest versions in Travis runs `#712 `__ (`larsoner `__) - DOC Update warning section in configuration.rst `#702 `__ (`lucyleeow `__) - DOC remove mention of other builder types `#698 `__ (`lucyleeow `__) - Bumpversion `#692 `__ (`lucyleeow `__) v0.7.0 ------ Developer changes ''''''''''''''''' - Use Sphinx errors rather than built-in errors. **Implemented enhancements:** - ENH: Use Sphinx errors `#690 `__ (`larsoner `__) - ENH: Add support for FuncAnimation `#687 `__ (`larsoner `__) - Sphinx directive to insert mini-galleries `#685 `__ (`ayshih `__) - Provide a Sphinx directive to insert a mini-gallery `#683 `__ - ENH Add cross ref label to template module.rst `#680 `__ (`lucyleeow `__) - ENH: Add show_memory extension API `#677 `__ (`larsoner `__) - Support for GPU memory logging `#671 `__ - ENH Add alt attribute for thumbnails `#668 `__ (`lucyleeow `__) - ENH More informative ‘alt’ attribute for thumbnails in index `#664 `__ - ENH More informative ‘alt’ attribute for images `#663 `__ (`lucyleeow `__) - ENH: Use optipng when requested `#656 `__ (`larsoner `__) - thumbnails cause heavy gallery pages and long loading time `#655 `__ - MAINT: Better error messages `#600 `__ - More informative “alt” attribute for image tags `#538 `__ - ENH: easy linking to “examples using my_function” `#496 `__ - sub-galleries should be generated with a separate “gallery rst” file `#413 `__ - matplotlib animations support `#150 `__ **Fixed bugs:** - Add backref label for classes in module.rst `#688 `__ (`lucyleeow `__) - Fixed backreference inspection to account for tilde use `#684 `__ (`ayshih `__) - Fix regex for numpy RandomState in test_full `#682 `__ (`lucyleeow `__) - fix tests regex to search for numpy data in html `#681 `__ - FIX: Fix sys.stdout patching `#678 `__ (`larsoner `__) - check-manifest causing master to fail `#675 `__ - Output of logger is not captured if the logger is created in a different cell `#672 `__ - FIX: Remove newlines from title `#669 `__ (`larsoner `__) - BUG Tinybuild autosummary links fail with Sphinx dev `#659 `__ **Documentation:** - DOC Update label to raw string in plot_0_sin.py `#674 `__ (`lucyleeow `__) - DOC Update Sphinx url to https `#673 `__ (`lucyleeow `__) - DOC Clarify syntax.rst `#670 `__ (`lucyleeow `__) - DOC Note config comment removal in code only `#667 `__ (`lucyleeow `__) - DOC Update links in syntax.rst `#666 `__ (`lucyleeow `__) - DOC Fix typos, clarify `#662 `__ (`lucyleeow `__) - DOC Update html-noplot `#658 `__ (`lucyleeow `__) - DOC: Fix PNGScraper example `#653 `__ (`denkii `__) - DOC: Fix typos in documentation files. `#652 `__ (`TomDonoghue `__) - Inconsistency with applying & removing sphinx gallery configs `#665 `__ - ``make html-noplot`` instructions outdated `#606 `__ **Merged pull requests:** - Fix lint in gen_gallery.py `#686 `__ (`lucyleeow `__) - Better alt thumbnail test for punctuation in title `#679 `__ (`lucyleeow `__) - Update manifest for changes to check-manifest `#676 `__ (`lucyleeow `__) - MAINT: Update CircleCI `#657 `__ (`larsoner `__) - Bump version to 0.7.0.dev0 `#651 `__ (`lucyleeow `__) v0.6.2 ------ - Patch release due to missing CSS files in v0.6.1. Manifest check added to CI. **Implemented enhancements:** - How do I best cite sphinx-gallery? `#639 `__ - MRG, ENH: Add Zenodo badge `#641 `__ (`larsoner `__) **Fixed bugs:** - BUG Wrong pandas intersphinx URL `#646 `__ - css not included in wheels? `#644 `__ - BUG: Fix CSS includes and add manifest check in CI `#648 `__ (`larsoner `__) - Update pandas intersphinx url `#647 `__ (`lucyleeow `__) **Merged pull requests:** - Update maintainers url in RELEASES.md `#649 `__ (`lucyleeow `__) - DOC Amend maintainers `#643 `__ (`lucyleeow `__) - Change version back to 0.7.0.dev0 `#642 `__ (`lucyleeow `__) v0.6.1 ------ Developer changes ''''''''''''''''' - Added Zenodo integration. This release is for Zenodo to pick it up. **Implemented enhancements:** - Allow pathlib.Path to backreferences_dir option `#635 `__ - ENH Allow backrefences_dir to be pathlib object `#638 `__ (`lucyleeow `__) **Fixed bugs:** - TypeError when creating links from gallery to documentation `#634 `__ - BUG Checks if filenames have space `#636 `__ (`lucyleeow `__) - Fix missing space in error message. `#632 `__ (`anntzer `__) - BUG: Spaces in example filenames break image linking `#440 `__ **Merged pull requests:** - DOC minor update to release guide `#633 `__ (`lucyleeow `__) - Bump release version `#631 `__ (`lucyleeow `__) v0.6.0 ------ Developer changes ''''''''''''''''' - Reduced number of hard dependencies and added `dev-requirements.txt`. - AppVeyor bumped from Python 3.6 to 3.7. - Split CSS and create sub-extension that loads CSS. **Implemented enhancements:** - ENH Add last cell config `#625 `__ (`lucyleeow `__) - ENH: Add sub-classes for download links `#623 `__ (`larsoner `__) - ENH: New file-based conf-parameter thumbnail_path `#609 `__ (`prisae `__) - MRG, ENH: Provide sub-extension sphinx_gallery.load_style `#601 `__ (`mgeier `__) - [DOC] Minor amendments to CSS config part `#594 `__ (`lucyleeow `__) - [MRG] [ENH] Add css for pandas df `#590 `__ (`lucyleeow `__) - ENH: Add CSS classes for backrefs `#581 `__ (`larsoner `__) - Add ability to ignore repr of specific types `#577 `__ (`banesullivan `__) **Fixed bugs:** - BUG: Longer timeout on macOS `#629 `__ (`larsoner `__) - BUG Fix test for new sphinx `#619 `__ (`lucyleeow `__) - MRG, FIX: Allow pickling `#604 `__ (`larsoner `__) - CSS: Restrict thumbnail height to 112 px `#595 `__ (`mgeier `__) - MRG, FIX: Link to RandomState properly `#593 `__ (`larsoner `__) - MRG, FIX: Fix backref styling `#591 `__ (`larsoner `__) - MAINT: Update checks for PIL/JPEG `#586 `__ (`larsoner `__) - DOC: Fix code block language `#585 `__ (`larsoner `__) - [MRG] Fix backreferences for functions not directly imported `#584 `__ (`lucyleeow `__) - BUG: Fix repr None `#578 `__ (`larsoner `__) - [MRG] Add ignore pattern to check dups `#574 `__ (`lucyleeow `__) - [MRG] Check backreferences_dir config `#571 `__ (`lucyleeow `__) - URLError `#569 `__ (`EtienneCmb `__) - MRG Remove last/first_notebook_cell redundancy `#626 `__ (`lucyleeow `__) - Remove duplicate doc_solver entry in the API reference structure `#589 `__ (`kanderso-nrel `__) **Merged pull requests:** - DOC use # %% `#624 `__ (`lucyleeow `__) - DOC capture repr in scraper section `#616 `__ (`lucyleeow `__) - [MRG+1] DOC Improve doc of splitters and use in IDE `#615 `__ (`lucyleeow `__) - DOC mention template `#613 `__ (`lucyleeow `__) - recommend consistent use of one block splitter `#610 `__ (`mikofski `__) - MRG, MAINT: Split CSS and add control `#599 `__ (`larsoner `__) - MRG, MAINT: Update deps `#598 `__ (`larsoner `__) - MRG, ENH: Link to methods and properties properly `#596 `__ (`larsoner `__) - MAINT: Try to get nightly working `#592 `__ (`larsoner `__) - mention literalinclude in the doc `#582 `__ (`emmanuelle `__) - MAINT: Bump AppVeyor to 3.7 `#575 `__ (`larsoner `__) v0.5.0 ------ Developer changes ''''''''''''''''' - Separated 'dev' documentation, which tracks master and 'stable' documentation, which follows releases. - Added official jpeg support. Incompatible changes '''''''''''''''''''' - Dropped support for Sphinx < 1.8.3. - Dropped support for Python < 3.5. - Added ``capture_repr`` configuration with the default setting ``('_repr_html_', __repr__')``. This may result the capturing of unwanted output in existing projects. Set ``capture_repr: ()`` to return to behaviour prior to this release. **Implemented enhancements:** - Explain the inputs of the image scrapers `#472 `__ - Capture HTML output as in Jupyter `#396 `__ - Feature request: Add an option for different cell separations `#370 `__ - Mark sphinx extension as parallel-safe for writing `#561 `__ (`astrofrog `__) - ENH: Support linking to builtin modules `#558 `__ (`larsoner `__) - ENH: Add official JPG support and better tests `#557 `__ (`larsoner `__) - [MRG] ENH: Capture ’repr’s of last expression `#541 `__ (`lucyleeow `__) - look for both ‘README’ and ‘readme’ `#535 `__ (`revesansparole `__) - ENH: Speed up builds `#526 `__ (`larsoner `__) - ENH: Add live object refs and methods `#525 `__ (`larsoner `__) - ENH: Show memory usage, too `#523 `__ (`larsoner `__) - [MRG] EHN support #%% cell separators `#518 `__ (`lucyleeow `__) - MAINT: Remove support for old Python and Sphinx `#513 `__ (`larsoner `__) **Fixed bugs:** - Documentation is ahead of current release `#559 `__ - Fix JPEG thumbnail generation `#556 `__ (`rgov `__) - [MRG] Fix terminal rst block last word `#548 `__ (`lucyleeow `__) - [MRG][FIX] Remove output box from print(__doc__) `#529 `__ (`lucyleeow `__) - BUG: Fix kwargs modification in loop `#527 `__ (`larsoner `__) - MAINT: Fix AppVeyor `#524 `__ (`larsoner `__) **Merged pull requests:** - [MRG] DOC: Add warning filter note in doc `#564 `__ (`lucyleeow `__) - [MRG] DOC: Explain each example `#563 `__ (`lucyleeow `__) - ENH: Add dev/stable distinction `#562 `__ (`larsoner `__) - DOC update example capture_repr `#552 `__ (`lucyleeow `__) - BUG: Fix index check `#551 `__ (`larsoner `__) - FIX: Fix spurious failures `#550 `__ (`larsoner `__) - MAINT: Update CIs `#549 `__ (`larsoner `__) - list of projects using sphinx-gallery `#547 `__ (`emmanuelle `__) - [MRG] DOC typos and clarifications `#545 `__ (`lucyleeow `__) - add class to clear tag `#543 `__ (`dorafc `__) - MAINT: Fix for 3.8 `#542 `__ (`larsoner `__) - [MRG] DOC: Explain image scraper inputs `#539 `__ (`lucyleeow `__) - [MRG] Allow punctuation marks in title `#537 `__ (`lucyleeow `__) - Improve documentation `#533 `__ (`lucyleeow `__) - ENH: Add direct link to artifact `#532 `__ (`larsoner `__) - [MRG] Remove matplotlib agg backend + plt.show warnings from doc `#521 `__ (`lesteve `__) - MAINT: Fixes for latest pytest `#516 `__ (`larsoner `__) - Add FURY to the sphinx-gallery users list `#515 `__ (`skoudoro `__) v0.4.0 ------ Developer changes ''''''''''''''''' - Added a private API contract for external scrapers to have string-based support, see: https://github.com/sphinx-gallery/sphinx-gallery/pull/494 - Standard error is now caught and displayed alongside standard output. - Some sphinx markup is now removed from image thumbnail tooltips. Incompatible changes '''''''''''''''''''' - v0.4.0 will be the last release to support Python <= 3.4. - Moving forward, we will support only the latest two stable Sphinx releases at the time of each sphinx-gallery release. **Implemented enhancements:** - ENH: Remove some Sphinx markup from text `#511 `__ (`larsoner `__) - ENH: Allow README.rst ext `#510 `__ (`larsoner `__) - binder requirements with Dockerfile? `#476 `__ - ENH: Update docs `#509 `__ (`larsoner `__) - Add documentation note on RTD-Binder incompatibility `#505 `__ (`StanczakDominik `__) - Add PlasmaPy to list of sphinx-gallery users `#504 `__ (`StanczakDominik `__) - ENH: Expose example globals `#502 `__ (`larsoner `__) - DOC: Update docs `#501 `__ (`larsoner `__) - add link to view sourcecode in docs `#499 `__ (`sappelhoff `__) - MRG, ENH: Catch and write warnings `#495 `__ (`larsoner `__) - MRG, ENH: Add private API for external scrapers `#494 `__ (`larsoner `__) - Add list of external image scrapers `#492 `__ (`banesullivan `__) - Add more examples of projects using sphinx-gallery `#489 `__ (`banesullivan `__) - Add option to remove sphinx_gallery config comments `#487 `__ (`timhoffm `__) - FIX: allow Dockerfile `#477 `__ (`jasmainak `__) - MRG: Add SVG support `#471 `__ (`larsoner `__) - MAINT: Simplify CircleCI build `#462 `__ (`larsoner `__) - Release v0.3.0 `#456 `__ (`choldgraf `__) - adding contributing guide for releases `#455 `__ (`choldgraf `__) **Fixed bugs:** - fix wrong keyword in docs for “binder” `#500 `__ (`sappelhoff `__) - Fix ‘Out:’ label position in html output block `#484 `__ (`timhoffm `__) - Mention pytest-coverage dependency `#482 `__ (`timhoffm `__) - Fix reST block after docstring `#480 `__ (`timhoffm `__) - MAINT: Tolerate Windows mtime `#478 `__ (`larsoner `__) - FIX: Output from code execution is not stripped `#475 `__ (`padix-key `__) - FIX: Link `#470 `__ (`larsoner `__) - FIX: Minor fixes for memory profiling `#468 `__ (`larsoner `__) - Add output figure numbering breaking change in release notes. `#466 `__ (`lesteve `__) - Remove links to read the docs `#461 `__ (`GaelVaroquaux `__) - [MRG+1] Add requirements.txt to manifest `#458 `__ (`ksunden `__) v0.3.1 ------ Bugfix release: add missing file that prevented "pip installing" the package. **Fixed bugs:** - Version 0.3.0 release is broken on pypi `#459 `__ v0.3.0 ------ Incompatible changes '''''''''''''''''''' * the output figure numbering is always 1, 2, ..., ``number_of_figures`` whereas in 0.2.0 it would follow the matplotlib figure numbers. If you include explicitly some figures generated by sphinx-gallery with the ``.. figure`` directive in your ``.rst`` documentation you may need to adjust their paths if your example uses non-default matplotlib figure numbers (e.g. if you use ``plt.figure(0)``). See `#464 ` for more details. Developer changes ''''''''''''''''' * Dropped support for Sphinx <= 1.4. * Refactor for independent rst file construction. Function ``sphinx_gallery.gen_rst.generate_file_rst`` does not anymore compose the rst file while it is executing each block of the source code. Currently executing the example script ``execute_script`` is an independent function and returns structured in a list the rst representation of the output of each source block. ``generate_file_rst`` calls for execution of the script when needed, then from the rst output it composes an rst document which includes the prose, code & output of the example which is the directly saved to file including the annotations of binder badges, download buttons and timing statistics. * Binder link config changes. The configuration value for the BinderHub has been changed from ``url`` to ``binderhub_url`` in order to make it more explicit. The old configuration key (``url``) will be deprecated in version v0.4.0) * Support for generating JUnit XML summary files via the ``'junit'`` configuration value, which can be useful for building on CI services such as CircleCI. See the related `CircleCI doc `__ and `blog post `__. **Fixed bugs:** - First gallery plot uses .matplotlibrc rather than the matplotlib defaults `#316 `__ **Merged pull requests:** - [MRG+1]: Output JUnit XML file `#454 `__ (`larsoner `__) - MRG: Use highlight_language `#453 `__ (`larsoner `__) - BUG: Fix execution time writing `#451 `__ (`larsoner `__) - MRG: Adjust lineno for 3.8 `#450 `__ (`larsoner `__) - MRG: Only rebuild necessary parts `#448 `__ (`larsoner `__) - MAINT: Drop 3.4, add mayavi to one `#447 `__ (`larsoner `__) - MAINT: Modernize requirements `#445 `__ (`larsoner `__) - Activating travis on pre-release of python `#443 `__ (`NelleV `__) - [MRG] updating binder instructions `#439 `__ (`choldgraf `__) - FIX: Fix for latest sphinx-dev `#437 `__ (`larsoner `__) - adding notes for filename `#436 `__ (`choldgraf `__) - FIX: correct sorting docstring for the FileNameSortKey class `#433 `__ (`mrakitin `__) - MRG: Fix for latest pytest `#432 `__ (`larsoner `__) - FIX: Bump version `#431 `__ (`larsoner `__) - MRG: Fix for newer sphinx `#430 `__ (`larsoner `__) - DOC: Missing perenthisis in PNGScraper `#428 `__ (`ksunden `__) - Fix #425 `#426 `__ (`Titan-C `__) - Scraper documentation and an image file path scraper `#417 `__ (`choldgraf `__) - MRG: Remove outdated cron job `#416 `__ (`larsoner `__) - ENH: Profile memory `#415 `__ (`larsoner `__) - fix typo `#414 `__ (`zasdfgbnm `__) - FIX: Travis `#410 `__ (`larsoner `__) - documentation index page and getting_started updates `#403 `__ (`choldgraf `__) - adding ability to customize first cell of notebooks `#401 `__ (`choldgraf `__) - spelling fix `#398 `__ (`amueller `__) - [MRG] Fix Circle v2 `#393 `__ (`lesteve `__) - MRG: Move to CircleCI V2 `#392 `__ (`larsoner `__) - MRG: Fix for 1.8.0 dev `#391 `__ (`larsoner `__) - Drop “Total running time” when generating the documentation `#390 `__ (`lamby `__) - Add dedicated class for timing related block `#359 `__ (`ThomasG77 `__) - MRG: Add timing information `#348 `__ (`larsoner `__) - MRG: Add refs from docstring to backrefs `#347 `__ (`larsoner `__) - API: Refactor image scraping `#313 `__ (`larsoner `__) - [MRG] FIX import local modules in examples `#305 `__ (`NelleV `__) - [MRG] Separate rst notebook generation from execution of the script `#239 `__ (`Titan-C `__) v0.2.0 ------ New features '''''''''''' * Added experimental support to auto-generate Binder links for examples via ``binder`` config. Note that this API may change in the future. `#244 `_ and `#371 `_. * Added ``ignore_pattern`` configurable to allow not adding some python files into the gallery. See `#346 `_ for more details. * Support for custom default thumbnails in 'RGBA' space `#375 `_ * Allow title only -\> use title as first paragraph `#345 `_ Bug Fixes ''''''''' * Fix name string_replace trips on projects with ".py" in path. See `#322 `_ and `#331 `_ for more details. * Fix __future__ imports across cells. See `#308 `_ for more details. * Fix encoding related issues when locale is not UTF-8. See `#311 `_ for more details. * In verbose mode, example output is printed to the console during execution of the example, rather than only at the end. See `#301 `_ for a use case where it matters. * Fix SphinxDocLinkResolver error with sphinx 1.7. See `#352 `_ for more details. * Fix unexpected interaction between ``file_pattern`` and ``expected_failing_examples``. See `#379 `_ and `#335 `_ * FIX: Use unstyled pygments for output `#384 `_ * Fix: Gallery name for paths ending with '/' `#372 `_ * Fix title detection logic. `#356 `_ * FIX: Use ``docutils_namespace`` to avoid warning in sphinx 1.8dev `#387 `_ Incompatible Changes '''''''''''''''''''' * Removed optipng feature that was triggered when the ``SKLEARN_DOC_OPTIPNG`` variable was set. See `#349 `_ for more details. * ``Backreferences_dir`` is now mandatory `#307 `_ Developer changes ''''''''''''''''' * Dropped support for Sphinx <= 1.4. * Add SphinxAppWrapper class in ``test_gen_gallery.py`` `#386 `_ * Notes on how to do a release `#360 `_ * Add codecov support `#328 `_ v0.1.13 ------- New features '''''''''''' * Added ``min_reported_time`` configurable. For examples that run faster than that threshold (in seconds), the execution time is not reported. * Add thumbnail_size option `#283 `_ * Use intersphinx for all function reference resolution `#296 `_ * Sphinx only directive for downloads `#298 `_ * Allow sorting subsection files `#281 `_ * We recommend using a string for ``plot_gallery`` rather than Python booleans, e.g. ``'True'`` instead of ``True``, as it avoids a warning about unicode when controlling this value via the command line switches of ``sphinx-build`` Bug Fixes ''''''''' * Crasher in doc_resolv, in js_index.loads `#287 `_ * Fix gzip/BytesIO error `#293 `_ * Deactivate virtualenv provided by Travis `#294 `_ Developer changes ''''''''''''''''' * Push the docs from Circle CI into github `#268 `_ * Report version to sphinx. `#292 `_ * Minor changes to log format. `#285 `_ and `#291 `_ v0.1.12 ------- New features '''''''''''' * Implement a explicit order sortkey to specify the subsection's order within a gallery. Refer to discussion in `#37 `_, `#233 `_ and `#234 `_ * Cleanup console output during build `#250 `_ * New configuration Test `#225 `_ Bug Fixes ''''''''' * Reset ``sys.argv`` before running each example. See `#252 `_ for more details. * Correctly re-raise errors in doc resolver. See `#264 `_. * Allow and use https links where possible `#258 `_. * Escape tooltips for any HTML special characters. `#249 `_ Documentation ''''''''''''''' * Update link to numpy to point to latest `#271 `_ * Added documentation dependencies. `#267 `_ v0.1.11 ------- Documentation ''''''''''''''' * Frequently Asked Questions added to Documentation. Why `__file__` is not defined? Bug Fixed ''''''''' * Changed attribute name of Sphinx `app` object in `#242 `_ v0.1.10 ------- Bug Fixed ''''''''' * Fix image path handling bug introduced in #218 v0.1.9 ------ Incompatible Changes '''''''''''''''''''' * Sphinx Gallery's example back-references are deactivated by default. Now it is users responsibility to turn them on and set the directory where to store the files. See discussion in `#126 `_ and pull request `#151 `_. Bug Fixed ''''''''' * Fix download zip files path in windows builds. See `#218 `_ * Fix embedded missing link. See `#214 `_ Developer changes ''''''''''''''''' * Move testing to py.test * Include link to github repository in documentation v0.1.8 ------ New features '''''''''''' * Drop styling in codelinks tooltip. Replaced for title attribute which is managed by the browser. * Gallery output is shorter when embedding links * Circle CI testing Bug Fixes ''''''''' * Sphinx-Gallery build even if examples have Syntax errors. See `#177 `_ * Sphinx-Gallery can now build by directly calling sphinx-build from any path, no explicit need to run the Makefile from the sources directory. See `#190 `_ for more details. v0.1.7 ------ Bug Fixes ''''''''' * Released Sphinx 1.5 has new naming convention for auto generated files and breaks Sphinx-Gallery documentation scanner. Fixed in `#178 `_, work for linking to documentation generated with Sphinx<1.5 and for new docs post 1.5 * Code links tooltip are now left aligned with code New features '''''''''''' * Development support of Sphinx-Gallery on Windows `#179 `_ & `#182 `_ v0.1.6 ---------- New features '''''''''''' * Executable script to convert Python scripts into Jupyter Notebooks `#148 `_ Bug Fixes ''''''''' * Sphinx-Gallery now raises an exception if the matplotlib backend can not be set to ``'agg'``. This can happen for example if matplotlib.pyplot is imported in conf.py. See `#157 `_ for more details. * Fix ``backreferences.identify_names`` when module is used without attribute `#173 `_. Closes `#172 `_ and `#149 `_ * Raise FileNotFoundError when README.txt is not present in the main directory of the examples gallery(`#164 `_). Also include extra empty lines after reading README.txt to obtain the correct rendering of the html file.(`#165 `_) * Ship a License file in PyPI release v0.1.5 ------ New features '''''''''''' * CSS. Now a tooltip is displayed on the source code blocks to make the doc-resolv functionality more discorverable. Function calls in the source code blocks are hyperlinks to their online documentation. * Download buttons have a nicer look across all themes offered by Sphinx Developer changes ''''''''''''''''' * Support on the fly theme change for local builds of the Sphinx-Gallery docs. Passing to the make target the variable `theme` builds the docs with the new theme. All sphinx themes are available plus read the docs online theme under the value `rtd` as shown in this usage example. .. code-block:: console $ make html theme=rtd * Test Sphinx Gallery support on Ubuntu 14 packages, drop Ubuntu 12 support. Drop support for Python 2.6 in the conda environment v0.1.4 ------ New features '''''''''''' * Enhanced CSS for download buttons * Download buttons at the end of the gallery to download all python scripts or Jupyter notebooks together in a zip file. New config variable `download_all_examples` to toggle this effect. Activated by default * Downloadable zip file with all examples as Python scripts and notebooks for each gallery * Improved conversion of rst directives to markdown for the Jupyter notebook text blocks Bug Fixes ''''''''' * When seaborn is imported in a example the plot style preferences are transferred to plots executed afterwards. The CI is set up such that users can follow how to get the compatible versions of mayavi-pandas-seaborn and nomkl in a conda environment to have all the features available. * Fix math conversion from example rst to Jupyter notebook text for inline math and multi-line equations v0.1.3 ------ New features '''''''''''' * Summary of failing examples with traceback at the end of the sphinx build. By default the build exits with a 1 exit code if an example has failed. A list of examples that are expected to fail can be defined in `conf.py` and exit the build with 0 exit code. Alternatively it is possible to exit the build as soon as one example has failed. * Print aggregated and sorted list of computation times of all examples in the console during the build. * For examples that create multiple figures, set the thumbnail image. * The ``plot_gallery`` and ``abort_on_example_error`` options can now be specified in ``sphinx_gallery_conf``. The build option (``-D`` flag passed to ``sphinx-build``) takes precedence over the ``sphinx_gallery_conf`` option. Bug Fixes ''''''''' * Failing examples are retried on every build v0.1.2 ------ Bug Fixes ''''''''' * Examples that use ``if __name__ == '__main__'`` guards are now run * Added vertical space between code output and code source in non notebook examples v0.1.1 ------ Bug Fixes ''''''''' * Restore the html-noplot functionality * Gallery CSS now implicitly enforces thumbnails width v0.1.0 ------ Highlights '''''''''' Example scripts are now available for download as IPython Notebooks `#75 `_ New features '''''''''''' * Configurable filename pattern to select which example scripts are executed while building the Gallery * Examples script update check are now by md5sum check and not date * Broken Examples now display a Broken thumbnail in the gallery view, inside the rendered example traceback is printed. User can also set build process to abort as soon as an example fails. * Sorting examples by script size * Improve examples style v0.0.11 ------- Highlights '''''''''' This release incorporates the Notebook styled examples for the gallery with PR `#36 `_ Incompatible Changes '''''''''''''''''''' Sphinx-Gallery renames its python module name to sphinx\_gallery this follows the discussion raised in `#47 `_ and resolved with `#66 `_ The gallery configuration dictionary also changes its name to ``sphinx_gallery_conf`` From PR `#36 `_ it is decided into a new namespace convention for images, thumbnails and references. See `comment `_ v0.0.10 ------- Highlights '''''''''' This release allows to use the Back references. This features incorporates fine grained examples galleries listing examples using a particular function. `#26 `_ New features '''''''''''' * Shell script to place a local copy of Sphinx-Gallery in your project * Support Mayavi plots in the gallery sphinx-gallery-0.16.0/LICENSE000066400000000000000000000027161461331107500155600ustar00rootroot00000000000000Copyright (c) 2015, Óscar Nájera All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. * Neither the name of sphinx-gallery 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-gallery-0.16.0/MANIFEST.in000066400000000000000000000031661461331107500163110ustar00rootroot00000000000000include CHANGES.rst include LICENSE include ignore_words.txt include requirements.txt include sphinx_gallery/_static/sg_gallery*.css include sphinx_gallery/_static/no_image.png include sphinx_gallery/_static/broken_example.png include sphinx_gallery/_static/binder_badge_logo.svg include sphinx_gallery/_static/jupyterlite_badge_logo.svg recursive-include examples *.py recursive-include examples *.txt recursive-include tutorials *.py recursive-include tutorials *.txt recursive-include plotly_examples *.py recursive-include plotly_examples *.rst recursive-include pyvista_examples *.py recursive-include pyvista_examples *.rst recursive-include sphinx_gallery *.py recursive-include sphinx_gallery/tests/testconfs * recursive-exclude sphinx_gallery/tests/tinybuild/doc/_build * recursive-exclude sphinx_gallery/tests/tinybuild/doc/jupyterlite_contents * recursive-exclude sphinx_gallery/tests/tinybuild/doc/gen_modules * recursive-exclude sphinx_gallery/tests/tinybuild/doc/auto_* * exclude sphinx_gallery/tests/tinybuild/doc/sg_execution_times.rst recursive-include sphinx_gallery/tests/tinybuild * include sphinx_gallery/tests/reference_parse.txt exclude sphinx_gallery/_static/broken_stamp.svg exclude RELEASES.md recursive-exclude continuous_integration * exclude continuous_integration recursive-exclude doc * exclude doc exclude *.yml global-exclude *.pyc exclude dev-requirements.txt recursive-exclude .circleci * exclude .circleci prune **/__pycache__ exclude __pycache__ recursive-exclude */gen_modules * exclude gen_modules exclude .DS_store exclude .github_changelog_generator exclude .git-blame-ignore-revs exclude .pre-commit-config.yaml sphinx-gallery-0.16.0/README.rst000066400000000000000000000154431461331107500162430ustar00rootroot00000000000000============== Sphinx-Gallery ============== .. image:: https://img.shields.io/pypi/v/sphinx-gallery :target: https://pypi.org/project/sphinx-gallery/ :alt: PyPI .. image:: https://img.shields.io/conda/vn/conda-forge/sphinx-gallery :target: https://anaconda.org/conda-forge/sphinx-gallery :alt: Conda-forge .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3741780.svg :target: https://doi.org/10.5281/zenodo.3741780 :alt: Zenodo DOI .. image:: https://dev.azure.com/sphinx-gallery/sphinx-gallery/_apis/build/status/sphinx-gallery.sphinx-gallery?branchName=master :target: https://dev.azure.com/sphinx-gallery/sphinx-gallery/_build/latest?definitionId=1&branchName=master :alt: Azure CI status .. image:: https://circleci.com/gh/sphinx-gallery/sphinx-gallery.svg?style=shield :target: https://circleci.com/gh/sphinx-gallery/sphinx-gallery :alt: CircleCI status .. image:: https://codecov.io/github/sphinx-gallery/sphinx-gallery/badge.svg?branch=master&service=github( :target: https://app.codecov.io/github/sphinx-gallery/sphinx-gallery :alt: Code coverage .. tagline-begin-content A `Sphinx `_ extension that builds an HTML gallery of examples from any set of Python scripts. Checkout the `documentation `_ for introductions on how to use it and more... .. tagline-end-content .. image:: doc/_static/demo.png :width: 80% :alt: A demo of a gallery generated by Sphinx-Gallery Quickstart ========== Sphinx-Gallery can be used to generate an example gallery from ``.py`` files, for a library, as well as a stand-alone web page showcasing examples of a particular Python package, module, or class. Let's get started with a simple example or checkout the `documentation `_ for introductions on how to use it and more... Install via ``pip`` ------------------- .. installation-begin-content You can do a direct install via ``pip`` by using: .. code-block:: bash $ pip install sphinx-gallery "sphinx>=4.0" pillow .. important:: Sphinx-Gallery will not manage its dependencies when installing, thus you are required to install them manually. Our minimal dependencies are **Sphinx >= 4** and Pillow, which we use for scaling images. .. tip:: Sphinx-Gallery also has support for scraping images from Matplotlib and Matplotlib-based packages such as Seaborn. We recommend installing system ``optipng`` binaries to reduce the file sizes of the generated PNG files. .. installation-end-content Add examples to your docs ------------------------- Let's assume simple scenario, you have a Python package with a directory structure like this: .. code-block:: ├── doc │ ├── conf.py │ ├── index.rst | ├── make.bat │ └── Makefile ├── my_python_module │ ├── __init__.py │ └── mod.py └── examples ├── plot_example.py └── README.txt (or .rst) Enable Sphinx-Gallery by adding the following to your ``doc/conf.py``: .. code-block:: python extensions = [ ... 'sphinx_gallery.gen_gallery', ] # path to the examples scripts sphinx_gallery_conf = { 'examples_dirs': '../examples', # path to your example scripts 'gallery_dirs': 'auto_examples', # path to where to save gallery generated output } Finally just compile your docs as usual. Sphinx-Gallery will generate reST files, adding execution outputs, and save them in ``auto_examples/``. Add a link to ``auto_examples/index.rst`` to include the gallery in your documentation. Who uses Sphinx-Gallery ======================= An incomplete list: .. projects_list_start * `Apache TVM `_ * `Astropy `_ * `auto-sklearn `_ * `Biotite `_ * `Cartopy `_ * `FURY `_ * `pyGIMLi `_ * `HyperSpy `_ * `kikuchipy `_ * `Matplotlib `_ * `MNE-Python `_ * `Nestle `_ * `NetworkX `_ * `Neuraxle `_ * `Nilearn `_ * `OpenML `_ * `OpenTURNS `_ * `Optuna `_ * `PlasmaPy `_ * `PyGMT `_ * `pyRiemann `_ * `PyStruct `_ * `PySurfer `_ * `PyTorch tutorials `_ * `PyVista `_ * `pyxem `_ * `RADIS `_ * `scikit-image `_ * `scikit-learn `_ * `SimPEG `_ * `Sphinx-Gallery `_ * `SunPy `_ * `Tonic `_ * `TorchIO `_ .. projects_list_end Contributing ============ You can get the latest development source from our `Github repository `_. You need ``setuptools`` installed in your system to install Sphinx-Gallery. For example, you can do: .. code-block:: console $ git clone https://github.com/sphinx-gallery/sphinx-gallery $ cd sphinx-gallery $ pip install -r requirements.txt -r dev-requirements.txt $ conda install graphviz # if using conda, you can get graphviz this way $ pip install -e . Check that you are all set by running: .. code-block:: console $ pytest sphinx_gallery How to cite =========== .. citation-begin-content If you would like to cite Sphinx-Gallery you can do so using our `Zenodo deposit `_. .. citation-end-content sphinx-gallery-0.16.0/RELEASES.md000066400000000000000000000003011461331107500162640ustar00rootroot00000000000000# Making a Sphinx Gallery release Information on how to create a new release for Sphinx Gallery can be found in the [Maintainer's guide](https://sphinx-gallery.github.io/dev/maintainers.html).sphinx-gallery-0.16.0/codecov.yml000066400000000000000000000010211461331107500167040ustar00rootroot00000000000000comment: false github_checks: # too noisy, even though "a" interactively disables them annotations: false codecov: notify: require_ci_to_pass: false coverage: status: patch: default: informational: true target: 95% if_no_uploads: error if_not_found: success if_ci_failed: failure project: default: false library: informational: true target: 90% if_no_uploads: error if_not_found: success if_ci_failed: failure sphinx-gallery-0.16.0/dev-requirements.txt000066400000000000000000000004031461331107500206020ustar00rootroot00000000000000# see https://github.com/sphinx-doc/sphinx/issues/12299 sphinx>=4,!= 5.2.0,!=7.3.2,!=7.3.3,!=7.3.4,!=7.3.5,!=7.3.6 pydata-sphinx-theme pytest pytest-coverage numpy matplotlib seaborn statsmodels joblib plotly absl-py graphviz packaging jupyterlite-sphinx lxmlsphinx-gallery-0.16.0/doc/000077500000000000000000000000001461331107500153125ustar00rootroot00000000000000sphinx-gallery-0.16.0/doc/Makefile000066400000000000000000000163431461331107500167610ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -nWT --keep-going SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* rm -rf auto_examples/ rm -rf auto_plotly_examples/ rm -rf auto_pyvista_examples/ rm -rf tutorials/ rm -rf gen_modules/ html-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." html_abort_on_example_error: $(SPHINXBUILD) -D abort_on_example_error=1 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Sphinx-Gallery.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Sphinx-Gallery.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Sphinx-Gallery" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Sphinx-Gallery" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." show: @python -c "import webbrowser; webbrowser.open_new_tab('file://$(PWD)/$(BUILDDIR)/html/index.html')" sphinx-gallery-0.16.0/doc/_static/000077500000000000000000000000001461331107500167405ustar00rootroot00000000000000sphinx-gallery-0.16.0/doc/_static/demo.png000066400000000000000000007016011461331107500203770ustar00rootroot00000000000000PNG  IHDRrFsRGBgAMA a pHYseIDATx^XUpsq)4;FPTJ:[ x }k}8; (HHH`aa7M,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,QarJ VVkV4ܳI_V'^ը4ό,}.MGU}0 Pץ}Ѩ8na&%78@DDDDy/$ʴIX&mVNeըUP*RE.\k8 #˦'>6wriąAV+Y QTb% 21nܸD^3W}ƭ׾:J7<.14JE"k8uN\1P@0 0y*""""3}Y_TS*;wp5;{\ _<Gl:aBT#,_l/~in#c=vck/kYHQ3W|c7Ggqf ZhT֛{1-D}ktA9s[~/)DF_Si枨 ),cˈ8,[ؠ#ʘ;T}{4G(f8Gb-*蒽1u{qũ]FcT$PōQD(i3n;80 0y,""""3}Y~4Щ-l_NP s@)sg{y %MPV;1ӷ_ͧqןKaq D2> OEͺPJ=UZF>Sap1^tP"o5).󜺣Xw(bo2fs6 Ł^X`a&""""3}Y~(Zh#}z7vj&P\N#Jء*[z#7| ϊ vK$j9s;= oGAa=r+,Q&#axC KWrCUQCQ!>8}|ۇK/Tno*0m,30 ü7M,QakG1nX W{7ri 3q:r..\Y6`U1m1Q-V܊V\ {HE}PEWm=7߇*9 z i*j *xX`n`h l=!- 0 $78@DDDDy/˯ h;n(k ]Q*QĨ $!5YDdBpj ;Oy ,W[tE9c=S{^W͌'$qIFJ_[$c^3,0 0{ƾ,:JtJD=1^(Y10`ꈯcxAjTk8O<{qw wLQeuCy7Ÿ{Ɗð;FgdX`a&""""3}Y~eTZbaq-6pCs[T!"AD@(U$&ygr^XxT"JV-Qu/tT",0 0{ƾ,J0:aM,QaKRC珃  .W?t[ͷc׫D!9IU?.>ի7bU5WdZLYveP(岺L%4$$x2vm݆i Vck0MXRGR2JqѾ>kF׏W_EgFDu,^+WnmR,#ֿx:{aҌ5g9,:cD[N)E'SnY{p>x{idũk1{~X6r2$阹+0d }[QW0|Ĭ5<|夻unqq@6>7Nø`h3VbRgyMzIڃ e>wEX"kIr29!~.a-0XmŠMqCIIԾ}P;w ,9pA8U*1u\=M7`\z_ ĥXMTLPi8g,^m؏1Wc}8t"Tzrr"bm:k%F7_p ufL3wcDk j-RR5H~s1YZ ,m<ֵ w{nǬE~SĔW~2 0L~Jnbq c__9fnB. uEEX"OR He]݇MDKnJm"E١;~j-{MF۱V(t9O)x^1|5t1Opl* ?.^P8M8Lkms/w+m'ȫ%t)l2[~kT%A*͝g^#g sTo-{sv\IZ-k22M;rFճEf}. } y8_ڢ-A8pOlc1ܽVSgKp@0qQ0fys3G2E1qC_UPjG慪frA샽aRuPbLPv 0bXa2&feޫjZ5 ձEqo9Cv>F1 f(kbz^&'צn>n1Fr ύQLoI3&PJ\Yuah?RfnOhB1!^8PmD|=jvέ׊#,IIxq]1\=pblo`Rb%M=`5v7.>U"9[_)T: +7 `.Q#WFBeBX Fò㰘lk93M[QAUp/VJ ]r<.Aծq!>v(~0Ca1 עe!J7fV`X8j$cK_Ư-;2bR0{"Yv8xCK+ (c?^}c{|Y3};z (ǚX%MlQY_8ۂAO+p0&MPJ~wHk1.S6r엨gmي3JhlO1c_dq`WrJ}0OF\< !P|-a=Mnbq c_s݉6D9f劲ۡVF;kZTJ}/`K;|n&aM[c#XqNFm]P3Jֳםcۭh܆/)tF?ճѤ+JINs{3qidr7ĬY2(#l};Tl3=x W_lf`AZ5XVnX=J(}qXPA=79pF!JhX fL:x#˾n u*ĘrAzҼUlߏE+Vިa&mS%̽`=<e{<_oCX Vmُ0P'j[;~bal;vOm\Q -FqssL1`cHqB !:쏪uD;T^:B3]BRR .r,L=G4B8<9cDǍzaLY 6"}"!9ʚ٣|.btٮ2Z(m,_/4芪6(l WcXq;> -[;szFl L; 'ߚc]X XzŷڡaR6Ntb֫+lBzaJxk1nj#~r@YyV ڣM%GioUc\D}ҥ3*b,ܴW8e+0h`˔4c\~G4"*88G ݆_Ob֜9prJ[Ӧ0d}_v3 0LInbq c_sR 2g7͇ym9i&Nh;~'B6wT񑸵oZ5Q5_=DBFޛ['q|tb*6pBCB J&iD=w4kOޅ~pYc#pXv=PYlw+GlqT*b?ErZlo=hZջxX9]MrQն}nF%^ރڣ3׷C5Xw=Jߚd=*Z *y.1[ϼJ@6;OBm-oդ"2~b=7tE)qL]Cj$eWTj5n BNCOqŸ֯ʉfʚ9 ^cK'H ؼd,ۡPa< Almy82&|:<~# cJ+SǸq6, +'KOL^xܷ+*;x"^;ah@7ދA(/r;cAS~EŐ(i(h8u? rӰ<}.>On]y"ȵ.0 &(0e9Ǩ4H%QC8 ĿH联d)@p }! :$4ШTh~/E.>VVN| X}q/6'j!ahPXu l7J?Z`_05g}0woNqƿXpE9 gh=3>EhgZOZۨ夯FvƷZGe9A+S=t:<\??6Oۦ m{Ee_/*$Bqv"X53Lr5q@KV$:헓J j(<}&'FQa .FeY|J# y3[d;1%Un ˩8 ǜ* .@ӖPE;~7BHJL) -PEZLĜ3c: K7D^ h3=zKن(k;=6@y3g_ RZY--7:ѿsSwTJoJ;iE<UC-O*hı Ƀ1- rBqh?"Sϯd^0GX{5zƘJ[J-Eu?έnmm`q"獊*-`qex(^mŹ yEAZgy&j|u:^MBrvNC-Qx1dg>aM,QarQiFzh(xqaQ8 ' c/DϑawM|!aZ'4z$\ÂPPaV\VZP{LO(c儢 kE+9 gEFP EzcQ}y*ʫ6l؞8uBM"Ðw3tcHE[pZ9mJd}]la"Y'8l e|,g/9@>Ɗny{ҢC;ŚDqƆ)#𫩝/{1vChzp|'ڢ#n#8s^,rr 딡#J!'/ˈ~eFSPIF+T"{RjS@E"faM,Qar_r2%¨>IȹTc}Łj&bk*?6)p0"k`-GL6V}n'1MWh^X+GMJY %3aY8X~BeQ-TCzݚOz@vcS>%MLv1\q#vf=22“S EW~~2 0L>Onbq c_sR'8| 4&fql_ֶA lax9΋ZIj^¦+0hD6ͻ G0`j6=QHL{Yq@l^ [YNdʉz.puղՓnH7irBMOr(mZn+qImG/Dz>0e4~5G3BYo#'//x]0|2M"HWyg'2my9(2 `ݕeorrLp俳8 ּ"vN/kѭg%v@qs1atv,6/ŴYb6_ʤl&Mū=1b}<}iq zg.L y 1Vh5j'x]nVrCVqt 8-x~hiJ+kȦʋ6mTQ6z4H9EIVm2#vx#>)k|Ho؟`煪E8D t~btXI G{؎- pqxXu Xm(f(ajcIDW%+g8Ł3Z~[v7(uI:آy1l(6~_\碡}, y@ ykYv(,W{rǷmz4L)WU2FG0 05""""3}Y~Yz\؇ hnS1x Uy((UJ<%}P e+~v1+`|._{7cp]ŁY>P-דobuڡw[HPASKACyh+Atj9&)/M-QP,)Y3G`FqXツ-X*1 їKw1eeL]vy<WXkuTNy,ɱrؼ<ŵkq!̙: :hP8C+߂s28 QT1/q،;L&El3вI;&?#ˋ~Ņ*ͺ}Ca&?%78@DDDDy//REK=3 :CM\ǃ$h)(UPD^d8#̝HL{OB#g4eJ 8hԱC鿩8Wڹ0PJNNZH}7`b=WwSGZfͧ h  -Q{8ZœX?^2 _MP!"R^c'*~cB{o+Cr-,7ȳ r -cLjP<%/DfcI^AGw81ı8<<=&m&dxl x qi'tGq31v|{56_ @PPye9TJD!oY w{7W0\plbaarDDDDg&vaa=ʶeBmyJJ€3~pע2ibY'UЧ9hw Ḳ}ԵE(Y3:.8$*ok`øQMWY+{o2kAaenhf1"ⲞQ>Ry'=eY<67T>XNn˫IoŽqq@wI^6-1ov; -Pz6 A_9\:<;*7BǍ0\V`X>mB>A'Ł'8-X:s J{G1w'0C.+NaYcE8DPʥ (xFo8ʫsa=Onbq c__%:%ىnvijR]`=m/i_`⌼8*.~{g蓷i\FVȈ 0["Z\؃^.i)cj Qq_DMe&8 '";Ď\wX86Wl8MǎGFQpo%Eۗi;uG&=!ǛFᷖVGbX:Ҥ3ߞ_Y!ꕸpmU(kjӑ[j,w?kʝ(D$< \Who:m3,/'RȃUF4vO^ly‼%%M8u@ u!Y?yv*5.u1YgXwor0 üM,Qa+ 16jrKNY9Ewx-cԯw{!k/)Ip\Z:͜ gJe;cOD[qvśvけ‼eMPNOalowYHPi~ CU3;nN=QUnnƞd|oh0=޳j%X]&-4wS|cr+Y}_* ?1~@W/odb,9/V˗3wD푇q7$3t~eL~7ބNӱ1}{\Qq{YH|..#Øٕs\|Y3Hِ󲠤[ѩh/KW^|f{*^2a?M,QaD  tA1ÃQ儚 Xt,Yɸ[[;if5VPD(.'E-Pf  GjN~>}~瀲/+[[ v Łv-C?*U rB>yl`6 C2MN> :M8mZf["5u@[p5nq"m;siq@nkrN-b#uH_fV.(m[ּImrs$PWޞ%uAE7^㭅^mVbeAIlkpi,^^TZ$GP sxFEReL_c8]VBP<2OkaQhO$C * -J6|_JwMOMFZXD җŁ0;_}!`=*oPLZṣ́-mycQyB ^}ϋmYa&rDDDDgzQBǽVގ&mB+gTm;h*P(E|<_;=F 2/VH 3+QTNUgX܉J틓* _ǒ1]DiN(^o uȉנmY ^r4YoQcR؈hRr~kf\@.#/*=vl' J94SN *>}nr"3ֱJ;R~rD e8ū{Q4 ^Sp r!Fz|ZٶGY1+oET\Ƙɴ\XzOu"g@o^ L3d<>-EsݼPw*~.Q|Y k^rCSؕhj)E6J8ȵ8 ?8`a"fc@(o Sk煁|lW_#U G]bxm2^A \^ء(l<L9& @> :2iO\Fch0_u+lbSszM>u1S3va&&78@DDDDy/˯`YMo\VM g?\ˈ 7co4ufHȲ PJ\P>U| 3<PZNɉWw,>t"4 Р\n&KءGp&Kw8 qh*x *e*)c,|5˯ YюarLuUX}&<;`pn*7| T*DxcY?'-|y-ǰup? r>5* cc1e`nc1j1'abqlX< vm= RA| 4(?#bxw xpwv#1z)\ BWRa ?g% &Qg|譡#R=0l"T#1>'VFX~nh7Yq=R,d?c;-oqMゲfak &B@ ↫N~tAץQ.)KcP8临tDg@t":9MʿXAW6 &B%3'3g mĕ03Y(|>>o C)H^V/mj`@ 3߶G,v̚&iS?.Lm'|2]QT!YۛeLJEYi.ps|10 È&(0em]s&uGTkӴ_3&&YUGM.o ֝ @Be|"Q*>~;6vʹN9:kzهpdnTw_=/{\>{hCdn96ZFW8*/S$80%;WQyw4X{Łs73y88TԬgg.U#7uWiuזi!#xaGW]ۑ>isNqB bFN(k%iK{n>#v@$BO/&MP k(ϮtkAqs'-G8 ڡ r쉺qgȴ\u.p[xFn%H£n*bVmX&8p {qYDǓAh3#ҨҸ=mr=2K@LM8?}X,d.,ujb۪oxPol"  0 ƾ,u$V ]L(܏'LDN}P]7h?킺ж _?BB F kL<*Gpu遟t6`4]F3>шu)靓X4~ͦm~u`H AO[z᧶^ql6=aot cR8]}h=d(x\UkVQk>}d#,Q z-腑ǃbp&uEٮԭ$β}w"TLڟq%tIa;6^x0&EK2~17ԴA//9=Pm Γf>| qM8$7bYй/j٤s5]zB,v !jhԯ( dlEd0ބҦ# _9>W0<,/uܐ,ۢ6ת/.+Yk3{_9q=_>f2920sҋϖ1V} XͲrLŸzñY']ǮiKY>d|6md~J.1 0L^Lnbq c_5R`aU>ןd3/)x;&aa]rDDDDgg$ønJsCL  0 üFrDDDDg(oq"_&#J[ءx9v/+^080 0k$78@DDDDy/L~pveB<Þɣ{سp"̛;3JwS U 0 üFrDDDDg̼Q!N?sMP ]2󎀂W 0y),0 0 M,Qa2G}jϙ:߷DA{4sDIS{Tj =aPĩ4|0 0ƾ,3{TЧi J5_mi1EWCe ,C=Yv(Y5>}X`ayrDDDDg̼QB{c={#컁Gj$3IbR8'a \Nq0 0LZrDDDDg̼Q(HE $fދ(j$%%O71eu0,0 03""""3}Yfaay_X """ &`6&LC(ibü,9p 7!NCJRCSU ~Ǐbƈ |a P[7{aa&(0eQBÓ?w39y4 |'BQeTB 1Q'85 )@jZdzj(sU(uz|*F,?Wm'&.5%=*II!$'fJM:TJr}g/̯WHIɼD{/y_PP%&"Ulϳm"]C6LyK~f,6[ȼu6JKݢ)J6~R2WXWRi+qgO0MUFe5P@!9,m+s[2G>1$6:9_zaaBrDDDDg'Š QJ7x+:kLNկ5hcX`ΣfE .IRB­C{0D|wʧ8u=OfsL.=^N"gYO('&"el]'3 ]97BZ INf}RuYLۢ#8(Ncâ%=f6< G#FNN>"9klt:~&߉"FA$W*h0y&Xn+7g^ZN`ڰ>a8~YhqSgBS2y?=D*Y򱽘BCȢ21oZb>*5 r/}L1NaM|Qujd$)pNL*-lNޏ4^8_ۏ#fdB cXǘ9%oKxDq_۷a[qS8N^|?YD"!NnYme\ ;"Ln׫#aa@rDDDDgs <> [CQsk9BF3,lx0l,ZP5 ֲF:ڬ+wS6bh$3WA g&(F HlP+Ju}Y:uu8L98*5g4v-P eڣ`X^}''͕ggƀ.rl7v?+{m#Ep$"Jc\DCG/|-mHz֨3%p-:U3s8 #)W7/G>Caٶ#KM_4;q RθzCX 3+ -ҳV6{ 1X1n01G?8v) W@%P*~C}:FN^&sԕ:n}F,:CdBA^I <񯟜b< ơUܡ3O5쀚΃`;i.!~9 ;\Nbͺs {zyDB܉~ljBڢ`;:z,Ī~cga柜ƾ, 8v~kRmKϥzO݋gfUT\ڂ{Z};cРxt0Ξ mRV0k>.YɥsШ 5#ós)Ե6G(ej6nvGY'6UЄCP׳A:q(: Ca_ִAΨn;C6^A:؆ݣګ3ձCs0v\o/s :oMa*D+V)N(S_lt?PNl,p6sIPDRPBS e vܬLDc kEaGTh.SZT"k =Q8LSP?B\X>Mxvh0j;AqՀr!6xl=>>埵3L,c(= ńoA#Pa;n腖&c%LPL۠ ]Qfo}&Ww|mafNP0<f5ҳE(e QeW`<Ь}Gڡ3 wB}}q܉XכЩCqxhj-ƒlQI^oPi/8MލKK1-ZOZs L٣-F1$hY`a柝ƾ,y{Mptɺ^przhM\/QAw=QZv1z|ѣ'xOQ-Jl*{BlLŁ:v(mfm &m?ޏQD?1o0l2Ψ"K(I\8q^Pu T \Ff>-ƀx/GΞq#Sv(anGa&d^=Vʊui6__Op_-^p{D_%E??&8JEeXw>ĭ[ױ{lx؃#>qЉ>̼ 0 0&(0e9=\QŭHΘ|1 9YPL.do:{wCeyfuYp jըVB%,ڡtA-#kq=~"VNJui-{z5 [JخĹ[֤m o Pjt^"vjKWF?sР-ň l(d)wFv<N>B+ctb4WP$hcvPD٨iꌲ}NFߞ]=통Y9TIX~ "m[B$P?xLZo5'ļM7UkC}oU<C%LUsӁBžp*3}Bmdt(3Eu6N?~ JYvCpi<4Z^.^׽׵;ʛ9\0UתuqڴlJV.(p6?{v;g3'k H1u-ƖEu<?ԶGqS'Xt2HDC_8ދvQ^WIXvRFNx*i]'`=5Zoc7n#!%)ለaaaM,QarNŁHﳘEMPH |J#5DAR]8 m; A,=; Dm>&PyF&( ViX0l5E#1Lj։g^.A-}\g{LJjy'cfa%78@DDDDy/9%8pS[Z-PX"5 &x@6(RO2N(YBXLzq@UPGUb뒑d45J{ksG$:KqP.")u5c{O|m( ሕJmh FmKMd|{R8d[z4@8`b3Lhg둜lb/_ؓ1H֦O‼Bq ;3YLp݋,@R\?%ؓMrp]7ͬڢX/tZG:6:&Lo2ۇڣl}<I퓥8PŜ'bȱ>QiK P,<' ҿ jVNy*@ѧ-eQk| K&CAq3+",IkD1 v5xG_!V0w3~{f(S?v㑬ռP)ؽ1cލRDОɨPn+*5FŐ0w鍚m:haT<:u7#ĶmvOFޑ=i"\;C}a"2Ψn#ENq@^Vm:hOr1m&viӥwE~({ zmM?v@rfPe& JGNу=y"Dmp}zgOv$2D >cq`x,8Cq@m2u-IRm{cx*$&Ȋ+u;e1Sfq@>Zj#Yg4q2nxfޮ$_쌚n1vk^u0 0?0""""3}Y1p_l3E9T=g# 5t84q~1VPViŁʍ=i}v:[w??ZN&g}R878P;Y-{}01Шźު8F3N+7v3X8C͆WMS>JyȡYaһL,~#ؼO uG m oJX8d=w8-<{ڗ9=s8k }AΨ)G>#!Eq@W9Jr3`a#}c,iufH8AEL^?v(.*5{q9 aa柕ƾ,9٬ `^P\, y%IQ8Sq@UҊ]BG\%>^> Jm8uC7/&qP(m*m"N@ݖ.(k^`B#>Ʀi+T$^ۀ_m3Dd bħG4P=jnlG&^+WSƬ<}`ȴg1-5g7r &lQlZ.±hĊqioXՄx :VuS&%kaa)M,QaˢjrE13g4/'-_:!ȱj5܆N6g | bTn@lJe baSTtA9 aҊ)"b{Qa3/(m :N֝PE^9`1=~+Qgd,ogcVPe;B lr[cKWTgśb,ipj-)CPT^v!ZwQPj|yF`o,⳿[&Ucɨoe/i;s ׂ ڴ;&K-Έ~ZPϺ#[:j42/,>2hWKwW \Pm=V[Vé=>p?,S+9olɇdUh;ajD[3Pd_>Qa؛jO`vqC(5}?:vE *^}YRG  jŬ%m i 0 0ErDDDDg*\=Q-P~a ZU={;+g^D\HyPGX/7*GԴ.@z\yIϷ*ڊ:8 '7FT܋ˉ }}Y*آhΰ|wUWDN?4}$rl4V]UQiՈy#M~ԧ0ePT1uDI7-?(Œ,IFqm#EɾO}ž`e?Wt>9w41q@I>c \mW]^㘬ǞRE*4oy,+,u L53Y`aƾ,2rYK@Fv(c劊 \P f`с BwyQut3w1,'+_7sE&]a?% >>_Eo`<(3 .} O/!(r8q1z&qVQ^&o cᅡG q|p3zh/+\˞h*MpEq\`q(b7>_N`ggTmVJY CD {1`L8E0ol95٣Ig8O?i;uB3GCC"xm`c u#>Q/bVh=|Rӟw[GY *EF'7jH$d9 j=v݇*) hNaˍ )3/,JCWr`3߱1{ ĸ?c2'%\܏a]Ϛkx!GzF[Flux[!aaM,QakETae)l<_PY#I_RCwT@: GFnbUn=c?0n\xuꂯ8s␢˘|Ł.A!:Bi 1xPi抲(٬?ܖ]Elٙo@bn_h_'a;m><:ؖ'*7pAy <[oA=wsyX}'gkM,ö otwyPmsA  /Т,Z>yhp\ݵ me\mwϯxqfZtBs{m=k!N6,&ϞnFb˾q;$fa柝ƾ,~hxzu?qFq`HOOm/^]0)A{Vn_Ɠsl+2t<; rwCr](Zk {P*L! @Ut6gUk5"ZDb[ڡ,@MwS Q )cPJnlm-aTe`YP}(T:Nw&6(Tk濬80P-0ΨP'-үlHr|6b c JvBN(^7Ɵxp1o诨X5oa\w6sUR76jVn,dZە|{ 0 0&(0e#'GX3k:{F=.ug|gM=UX~F&@Vgzp(U*bĺo?D1nş RA3ú;*6~/qNŁd?,:uZvBfcVċyƼ"V[k/` :Aשqj0"o(x}PU^o-ܑYq -joPx?Sa0s?EBg!4f  ~ueTkMG( [ oSQ&ף|bhO[G}֞3bTwӮp7c3rRCi7?7MieJֲ_ 0 0&(0eawcŁlWx0 0 0ƾ,3 0 0?#""""3}Yf݇aaFrDDDDg0̻ 0 0&(0eawCq\kPs)aa&(0eawe|.nEnѶx-aa8""""3}Yf&wQ&j$(/0 0 ^rDDDDg0 0 0 &78@DDDDy/ 0 0 0krDDDDgT*aaa| 0 0 0&""""3aaaMrDDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDD/2Q AHH"cP된/ H*KNM0A$i GTXZ$/U)zEG 2^%;g)5ɉ:F*ƞaHЧo$U<"Bߖ0Gk1'=?zSXѨšU0T=A(%ID"",*)6Ro=QtH;:we?/';] A.LzeP|(jHTQ XcsXr{ڗ7+͑^G/Иvr)8l~EjdrKJ"|a疥83%'("M>5Dbo-[l&At ;wlƁ&ĞRɠ#yc?^ڔ†Xᗔ>3}eLoYTI?|.߁ g?cQIyD8@DDDDTh.a۷P7Vu1{s.](x_O̐z󬪡@8+g}>t@x?F7-;DTL2.) 8cXiѧdRm닾ٮePgkN(M:.j>CNd6>7wp5}l eܸu2h[~ߙ$eۨAB?\O!Xx߹x{-*nycL*41O}+n" 2ٻz$u\r 7<@`F+CPG?{7pUϠ(p7d迸l'Ĕd$D>p-< E &6 _7l`耋_8Ey>B!/)A 7D"RϳHU!Dy.^ DNmnߺ0u*qWDWD{u릊25ޠ8$Ek2Yqw Cĭ[78FBs;m} cu诛8X&E໷H|$%;>AR;xxa_-P<mpM}y;p6R)Ex?MEx|Wq=< "&o3FLݒ.<IMNRqc\c6+>(.GFx1e]-kXBSǑ;!?Ɠ8d뱗KR#2gh>oMID#)Z=ĶݹotҊ>@KPɿlCTbA|t$"XwԢ-ŸxKE@Dֿ@ܻywc}$!8l]\X{a0Ic0ϨlǍ z.|xS賾RՈ ÝiqOlC8gC_ĨԈ o܄w`p8^̕?DCg ,eyeg6XzCzIEBXF=b`hXX% }O>_Tmq<{›LDCLOX~S~!JU-,@}8Oi} !&FaE_ѳ1i@T(XWu11עҋ8֮?U4:ᔢDm_+E[~y5r|iT]<|O-[ST,%>s) Lc?g~D7a%jTbjy,v\ u3R V w@oglz-1c3 z4.qaL?G~"5ðU|k$LVG+_)<ZŁX(UD'O`#Us,:pǻjI|254n n=& *ǽ;Tܙi;W-PI;]/1jUBBb?徳B q-(!D^!l_+T5ſ%PŁMeTA/da=$D֨S8OlG5`<o&XcoC>OQǡ?΋O O`>hU* B(#ibSu>)v3~\58-܏ ]qTld%m J㾈@,rSs/JVC}1X#eLjo`z-G=yJ\W#4}Ѹ l[T.!+>>^WㄌOh_-u=W+O>]@`/N1Gb1q/\{>>m=]-b)QkXzIL9Rwf9j/P̯hk:^*gq).lNV?藟 d_ѬPBwfW߿?|I>)O(0y,VAq,">F>_7V0}e(3qcӭ'忣k+q?jh^^ ڠFOAQʬ @NTIpSl>1[e/~*kCТZa4 MgP8?-Z ǚˁdz8z(OkV$>mIJYW>YHƤ"QK&k,,>?+"kuY8|'8<&|>Kqb2n#| +[źOi98ö0Q|_)3 ][́qϞ9TU =Sf5J/Fb%d̗GP^J:?1R}^aթsvo~!UA%L[UZZIDqwͭ+U>cPa,<~*׶KݯP. [f!'ƶjae߬>,9~2l ׅW-6g[cq('aUIrʙ$!p>)g l3?G 1a_г3wG#ZXFI{jeSŭg|4{0Yh{V܋}~{j(e5_}>(~n:mG`цXbB&4-@AyQZ?C~Cɏsp.&ߴ8p}>CGԭnSV`߮՘ѫ*76=G,;(_K3;3>3i?: Aɪa{ nZVڈ|0Î]t+^ v>MT,}nV?48,-zmx vz!}SEv˜[ ~/~쀵7'=bQ(\ ^aX7+l-/ʴ[љ&I&,Ƙtx?veRmffܔ::?Fe(lۊ-sOQ࿵1l3u0f"ܵ+&Lq|ѿ(nFď!mPJUaը-ZĨ;ei^GG,$b_ h7` w?`mFbX>u*D.Sbkv]1y۲c:Q4ɨhtl6WWk3cߞг1/aFEϋkOx}|z~S?݃@b?]ذ'6 _೪XXdS\4DꦘN\o X}X5m`]رi>F:7VTĜ$z+_jubҊX7HģmQXm=iœ ) ~ر*>~8ndeG?S1|Xg M{c[&Cƅ)@~8/`2LiQu=d,hR`ܲ8{fH u }~4yf+v- _Ŀ@KFhm֍ЧQ @Q.]>BS[Wԫcc׮X0ՋPT+$iX4*W7Cq+NF?ҷY丹Q~h7sÛp3T|n'#g="ޱ?iQ WNuakl\=&%Aq{+.-NJm;}pw{!1]z1|sMT ,Žk1{cGxVF~Fg&8^@qO~W&I(<<=Z*Dq-+Gah(R*ձغ n 8Mv7h_lǮسb \UA_/-T )77o>N1onڠjPTHߨ3,a]K4U;b]aV#(E<;s_>*7t7{ m%zFBڢG]<(\Iռ*Z6uCt;mĴA֨ٿPд?v≯:PsZrbh5,B:t{///!@| ߣi(NJ^Ue A DƇOvIQ{[ Ʀ9a[8˚M?d^83mPo0oc\(n9g,8;~ v}bŊXUO"pb5J2ǨYN,Ƥ.ӴF)Bu?"EDw%[A@۲da8 j8֥x ZVzLq~EJvڟF46osoa k`R[NNrF,\<u^E4ϧs.O 2(;_IBɡJNj=J\^`ŨM5PB;1i4'T*{.|@J,N }d}pԱqvȉIM},T9+45>F@ph TEw''4'ѧb!V?Jq @ab2$A %?B)?;KQR_Pl[LAq Sk3gC3֒;@=.ϫLcrb VĿzJLǨp3 ?('N 鸑ib:x_/X,۞?{gDօ᰸V]CPb[ wN/uIܙiByv2ܙLs>#م!אxA͚_RB>]6|r$Ҡy^b]3LIub..Cmю#\'4DFM j?aQ YGx7~OmD*pq,ot/"'RāXѩ$rFίAQ[Xϋ:_zI5(Z?\72.[_qe@N UG,ѳqc), 97 %wch%v I_4my\xt,lpUh%]܃ EXwWyāKBS'V4Q^AbQlv<$6iwc}}3|80$Xݴ՛]r9YEAGd*bdHYgC]/Ф.EQĈ`OgL7{BQxDrzO=ZWΪAꫡv$gX;~QvociH\c<^~AAqY1ʙ- V?mrE#ྪ-r5s1'Jസ=WO{կ, x֯<m[&K :i|衋]Sg`{p~|gwpv H"vS>D_қ Ф`2ԟό%"\}>p%O_{ɻK{ K$E-q.h-ËV&#ʍڇǾnO<>!UXN;TA*_8~&xjt8" !w7V&M>٣42x/Ml, ;,yt;_]Z@\7mVlm"CRcú;O&Y:?`,B_IJB)|8? iPq<07{﭅iraGN#8RBv=}L?A =8'=2R/>qmhN_f]+ՑC Uq:ˑhɘu n.?ﱫJAUѠahi^ĤԘr,7`,DQZdOt&LiW I@~*%SJz96fxb{oexy*l9cq,o\Oxz}ֆzbSW Rd r`мX!W8u^4~6z sD pp[#B\g-4ɐv[l&xئّ8(D\Y6h9"='8FeK]<}|= Y_?B溛x.hKtˣy 4^yT+\\%0f=%kn(LiPh[d{S^v®Ջp FR#MҬ(]Hp*{h4ʛR3(KpHȚtħE\ڏ84Zd){+x8pV[Ǣ&)2&{p_m -F,xo)^5_]э }b (mbݟض~^!.qڪ^d. ѯWGr4,A;P^P)_jd O}coHF4EuFU&Ydh8dѕ/wE4t]q h*KjhJyfoѸ\~y}pq[0ZK$>W@vC1n>{.oЮrVc9<%^TGPjXtدEw;J E,vq`!:ʂZ=Dv6wAd0fmtq>@NqMiƂ=kg];4ɐ&/rfޑ7"Nb5Xh3GB6PL.a6qW<xyDdFkzUP 鋴^ gu9`Io?w'1zfn9}hBpݶ2~->3`_0 0 #2ASf\ ޞC5Mr۵C;h]{2+TJ-$1āP G:ZZiEtCࣔIWŁO>nìv{ּ(U5nzb4_r%չMoI;PŁiM "bc/ eS?3MŁ@w*8 ϛ!8%)z pF@w"L%q`!nL(<硩&j/4ک`=ZQ(IQdlJJFMЪ}Ӯ]{t=ž{aqօ]Ԫrfύ2j~hQ0uE.*80]1dBd8@Y([8$y/ S& āfG&kC`8Ԝ8qxd,mqz홈IXb}|'TqXOā'\ /$b!6q es2̲CNu-2C+s"3 0u&t>4/]-:gj,")1һ? ^ђa;1T1.OLn^z{4~CQv#4m{$ h#ՍŁȖ&ަI2j "8ā{k^cHtqw8OhADNާ?L[Q-v#з}SԪX 93ҽ03j[/B>8,E?Ws^ z3 ?ѰF@PqxE T 6ʁ/&iLh=Mrg=)t_q*dϜ MۘgŐ#0E> d94E͊S\D9Q{ x駴!qRVԜQ4kˊbH%ψr@'bq'R#LȳlKasQ\ {08i d/}Xy+MG 93oqF[upRd+H ~I5Z<;vAC0z>n @eoyxMXqS,a),0 0 0L4ԸH)~[qsXK`I3M naxyx&Łau-ʔ 9ɾ1x GO}_JP[;6(ROyW{X-$+-q}K631v 07D@?̉N¢tbd%+wh'ŁKi 8P4krvo6O~R$1 CtJd2LLSC} nZq ~x32.8sLem14Q8Czt^w?Va)+ X^(h`ǀ$Q.`H\e q c1+ʗ/ǽksʁ6Ȕ1ƞ3IM}R8(8 eMLxuAV8%6xt-{P9PB\6$_Dn>/D?!bD$(cydLVGHdN;d"x}q`H\6~EiP+!xnAIwʫh]U0/\;r/nX:}yǷ-6ő)Y^t?uCِxߛ^ ~8;;siEh OrCȎ}G!]&)¬ =bCG @ݡ#0K`V9 2涃v$pj ٫*_[*#_,*[e&Eu2U$xqP|NE9fBLO0 aaƄ;0f*L4s/3:=2)$woly=py@dk8}U ?dXx$^.Q2!ʁ.H_+Y xA7cPT?nh@,Y)STq Em \q#Q6i T$"H(쉞 xn! }ÑJLM$z)|OA1z K8hxsΏD9cMq!mXy( MrKT\'{HIR땓4oMUy"Np2&̠$O!kS^q~nnEiB{l42K Izk4&gمA^Ȭ LE; iS|=2=Vqw룳Q5{R$ʐA̓ۯh\GĚH94+CTNߚ:ױwV<TOQ"/&qG451{曉i|m$/&|XR+34Y[aWJSlC/c#Ii kLQ/ 4+OrRj3} =LZv_:Ә 2}#j֠DbC酑:euXF3=;uX竇n=Q=e:yD ƽ}Q e&|s@ȗH1Gb|>?=42@S܆m(w:lzY8,(Na aa&^Y^s!s?kw[}g]v`ئɂMM0CpkPe oX$޹naۻ2% -GY0ϦA}0ا۸}(j v٤w]9&6 ʃiN| byڶHz`/ S¨s>qSкD {"'b4U_kpj l,(xS('#2ʆ -ga3y7أ_owBq H%NڿOsAd`P.vx}VsC[ Y ^'oJeD]䊚8gr✭:pV\qh54-lD(3`Y!1k "Qreǯ}X5wZ/‰*PfySLc3v&.nEȜ4z'L=aDY+a_p>F„Y{ nc̚M|>4%P=ϣ{ Q.SVm0wⲸ]=VBԿ\q P2cJ TTh[u-pI2ej1k_} WOjˁ07ˁ5Y} 7'dϜSF6|náV.4a#5sMPz m?K upM;"^ˡ۔fHk~4]-v] yڰ;1X@yߐfou{b|SϐuFmŁ/~K\q㙷4x!zʼݦ^ ǿ\pA܋n?S"&{tYd@K븸'_ڡrj9_*7nZ0R%O5a3޾;Wc^^FU'D%1pm(os?\Z)4eխثчõT0RW۝qu9sB=P*sd\7 Wn=ifn_.i#۰ p\ğة4c5WiMw Cc"U o2 0?aa=|1Qx *YʔA2f"#gڤ'ۊ3J9}kZ;F%`V Ϗ%=P^_n)84h3R΍]sлN/RK@!d-X )2KQNAE&MtA>E4鐻*UZZ4X ܩKʬfuU M<=Bt鐫X%TXu7ƀqSa?*(_.ad067e א-x|?O,@zE;_,eeKr=4d)lDln5i 牍$s 9e 9< ۟DNn2LQn:ڵ=0>މ)]jp_ʖ*E|X|=z-Nf "g✕ErUq̝:9#ݵuؾQI wLEx{CREQbcXdžA[J4%S>4jSrB\'& /eK wi֜}'Si qdJu:<rLZ?1c=A/qv4^D)qm*9VE)q[ &G[qhhM[(>j?m"|pҮ IEQN4n g/4gWyW8mRAg)N B_ds[K \~%!9 V(S:xBfվJ{k-FEݏ?<~hR +)ݥQ4on,k! ]yԨo-OS WpwK?$C~np$&C؞\u{J(\JW{oigԋ!\B™!s-]kFa|>K hkEV$֔թ$7rŃ\(fVfJ O0ۯ"'-Z"e l+p8g%'0_ 0 0 I>صi)fϚ3cMz4FcûDZk=,π 8}7rƤG.d >7bN\|Qo^"if\G.#Z[P4K6+ONbJ;XΘ U}(B=as[my,_'=P}heױd| G? +,0]Cx0J6_cbW ]C.2 y{p3;.pmc sqmkopu۟rH2Oğֳo!6~=̃==kffZXc ׍#ظ 3,l`2_\Ӗ%3s/݅j`|[-IH|s ΜEqGRvL8߃FaĮ՘o)6XF?v>rq kh6S75~%W ۥzNWcX5*3OC7a|qߜ9 gtz[2).+Eqy p_KSϣwt^8r!\AEW>@w^则,k^&wW}9k8Dݣh @PlCa9!󶋷VTקEI)J[;iJ,y]+qM7kǚs' %uuC=<`9 W|c2G<ܫxo Yc51Kf \=v/,ad;|qCʿ#ߠոㅫ{WZlvv+7j|/1>DDְuX cxz]žŋss4{v\nқ=-ü9qk`7 g<ݙ3-a$x~HTV2zwxxo,q1tj/=os9n`uCp|̵9Ƶ#۰akv#[<7OM:5rH{\xea 0 0 |]TqfUjs*0?7z]7ÎϴiQŁo"8~9T&!A@ߑ\7|3 {-`(bȃºi^Z~➤]`Z,AKM<&0 aqaaaK8.05a~"\p,=5A=㇑8IDcq ?01͎:#I\:{.;aR۲ȕz:^A8pz̎aU |X`aarJnKڛ o4(1a~tbz 4ҠDGG\Tr&w#T@S^Eɑ}OᯯcU ̎ wb(W &-[>^}rH*ɋݑ`ea,0 0 0 uyw۶mũHsg868F('@ ޵ Utwlۃ0D`n=fW۰׸{ڵy?|c'l۲ 0 0 _` …8&HDΊϾ78'~R}-~Ӿ>BuB:!@/C] ؛KbI>TZ1W{쁓\\\88J[>>>U0 k֬[o>xyyk0r"4>u(Qb(&b FuFhm2}GЯDk= }NAvз $L6{FsгoE B!2LpktiNEF0&٠djD1mf1),EXYQ\):\ :zu뷝ns}Cqq~wuq<݆X㳔ڣ%zkzw>]g)"Ƨob̦_)j lqx1ryn$[ذf#3?aqaaaoyÏ8CWLSZ# ϡ(x#* { @KA}:/ QU=!"nA sHo/js/̙38wL~Ď;d1M βrA1kLD дD_Zf6=j&Z:Vn'jgYvAk(jZFf1QrM@ PT1 LG3P|&̚D閳PQl6{F(8[ 0 )br1&F"Fi}m,d-B9jXNATC<^^/x\l7 S(Ez([\wP,i)Lm4MGPTT<OD2ŸEcP(; rFlQ;K_[kJU3wͤPC?:'5sDyM=XO+#'AO.bu ^B&p l(` ;QQv{̷|!0 0 |kd;$ $RW! `Wa DP5GpU ȷy)cD;/`n!b;b3ŶvMqDbƉ}#P z9U;ءz|j6 n1VhPeVf3Ш4 B5= y!0<Q>r.EW4LYy\%j氝9Ł ~NHѠؐe?!Z!?qɋn ؉Z%Ct1,0 0 |Kuzx;@0`h OQH@,+uO<Da}{ZOh@U_Sr- ̗A{`mjSF))Iĝ I'4L wUāT=(m/g q` l8w "`^b*fa% 4nzQ jۢfyJ:〲āQb#80Y;lGpDA+QUq * & aL]O vId((^HUŁľvD80HHܨأJG;Tkc-fyV#jZA9R4iXv"Rh"I %g/G}Ÿ0y(Q|дL;~zq@fԥdvm?D]ğ`xžwbt+3m>4ΏU*Y"dj︂aa曡" N>8B~T-8Bn'X/rx鋠ج><x}Z"Ԅ'ք7nI輎AKfdX㞿?Ky8ھ8)jo$ PՀ0n?3 8k+I(3 BG@mkԩoZmQ6Fl(=L`qC]IGvruEZV_mTfqS`^d" G /1FIGYT5࠴H^J ("" P,1B*kW7TPiPYw=H֚|K߁: *jXU}BEjk!C j1D)YԴaq;'5d?N>8!""y@@B#XE 80DgrcZzYmx(BX^O  @J{.^nXPbM rO5haā6>[klx8^(n¡VB]o]aaA߉i68 H`$3@,߯ waY1|u@S[XP~b2?X`q[hh_F(Ʒ$ P:0Oi'e0#T#bO-j@~ KjZ ۢ'gO"JB^z H#bk ZՀ* Hbca P,5 2)IC{!KULU 2Icdk!SQmB-5m-T~S5o[ bxS{!ғ@H"E7LԎŁWֵdAQV]:?|HVFcޘz׾=I)텽c^wWTrr(P<ٻNߐQn"/i\/B]YujA% CkIo> .bհ'`YLg,nS)/V,O|DYP[:™~!FSq@۵5R1gn2\H{} lL_i:e Tmv.^]\_ű&~BaakSpT-I 8yg{ǐ8T_xп9a%.+LIz@D_Й^ۑw{EGK!&3 2vS ǀb@ a ^dDl(;S6jT5PuڢV[i[vGFURHG_8b (`kh1d 0d{!՘dk،Z=Pk;Y=PؠnkԫaU,d륆f3an\= {`dT{!w)U*:OzVO+8gf 5Dz[CP4Sb+rʨdV9%F *Nڋ *EJ:_M 5Ɽ-ଷuxxR,bffXbR*۬y)^}/uoȬ8C|%P@&hRfF`*f-REs xcZ_m5VgL)I%gFj¹S Q"?`O#q  BblSg69hl%F)dlk+GЅH$1d94]yhGSun(IXQ}&税kNO~yG*DQO_BԦj41H= ilSeB-I;m\JVAjV(]ո|?jyx *IRksK!aaoophVw[5`$a@Gd^=U bkBO+t X0? $ܿ'N@`` BCC"}xdz8ė1}&Q^Qz Nh45'3S7y TPj*UE&bFFT5@FĆ(c0"6T 8`\5@&T5@€hm ~"HXŐ  I sb{z`>j6TԟU=@T=P+"KJ{Hc@m1D \U;yWv`q;ta]\Mbp M ;FoFT#z\\Q<9ś f*t5H嵨v".sZ84ӣ{Bcq =ڮbYED 9|))BoG:3qW%$Fxa s' 85\ ~_q@⃍ӊuFm&c5Tt ckvK!V-buێ|aa&D/d@lmH0 Xj)0 "R[ EVP{!kzD (9N 2=0@S@oz'C^h͐R #+4hi f9sijwO)05\V[F˝hZDy+aeu O!Fcu1A1^$$BѮN6Ps<-C4 :.SŁ\hǡd&p_3o 6Śgr"nEbb&@{rr)m]V%覺0+?ߎV櫊9Wԅ&=YLbIN|(LT-pa}UmŌquIܰ8ė1Q @ h T-`, D L!gKa ղFz6NrQ U (wė ɪ&&vB3`$j bT00klc1h}ՐA J)8U텨QJU=g{< }/;#T1 ?8@*DO_%<]kO^U>z0_?}׽!XQ| 2fTT P$q@)hD€ 3а3S;!2!ZGU vBvBHpl'Tb#&ąj"J05x }0$NsbXf,Ҭب hA^91R*w2'* u0Q;& 1Ҥ:tN /lX8}w;yR{eqෳ|^ 'fL%Y҂(w.ܥ8 dͤ$cdbSqSiO’&~+Nj۾8ŁvbY"\fJ˵GT#ld c?Xā/_οп~^u_"(Pzۇǟ#+dAϕcik0 0 | µ8H-bI(AїʂJȒTb(LIfLD;gYJc7ܺҶSY=uR>QWՅ߶B,Q6_y:}nl9bŭsв|VU(ȋ+ęuO΢AJ#q!1 0 |+#tg!@8Up}"[ W?:ՇAZj%D3`9Qh?HL|=6{ߏJs8ė1üev: j NJHV Fj3` ԷZvBըPPTՀPl'4YR3`+ $ U*E " B& k=h|L)@O? hB$tdNL{^HUh le%jDUrjg9$( |H/YI@1u~ 9,|/|cq@ơNUf(ָA胳k\YD+[x3tȑIWßwωFh Qβ}:ҿ; 3%&YZL텋*Ma{G]+ЅO_V *Dx(`YI%f I!ow-m<Ocˤ&H-^;kZ]8h~A;B]0 0 |}>EਗbFlp'NA8 īDIZ< `%#(z@O9µXy1.?{}<+X` yxx^3f  P @ i>j2[ȞT1+U ytj'$ |KQ Qckc Zŀ]D'`ȷJF(,Pz>I(02" @@TA@TA`,?@qdN d\ED P+H -*)&4Ѫ>y}5h] ifx}C'_zcCh>2RT̛Vcֱe`"rYqAx-{cJWmWV*ցqh|ʁBh$#a LdC*ڐSVVh**B뻋c[Շ?0Gz ,b2bgn0 0 {ޅjQ&p N «xVhC'tn?8@W'r-ŜLܰ8|k?`gxaq1s\p{.U2fL4*1VDt0/V (`6P0`P* 4EJŀb@l' +@o{) P?2Cq(1QIS +K`R hbzjD/WKRbMdU5̃E fU/=|R+E*MmU:o+z+XUF1?NA?/Zngb4/[uysj{1yIKMџ(E*d“X:A?NjF% A%>|-[ѣXjd#Gyw4(=^^V ( X(5-Qu0P\jfԊvҩ* rI_fNho(.[ 00_m%D€%iVBH[UQR/{6힅b{(nEv+"Aj5"W}"+ &Ų@1PA0N4(vT s -ZΗu͕TA 0J*P% й*3L5 ܐ8_&éNB6#O*ϰW9$פFwuc'k)=zWs67l0n.Xov:aNwVN۠v<o.ޗbބڽ;wmqBž8aިǻ`7Z[S=bz^1e nUzhmR蠁t[ #M?cٺâ7n1tE{mc􊍸Hl'Օ娔L,a+u10 0 q'২pB8*ccA݇z@9{`> L\FsxT`c0@CI ((H}3tVg+QfdU栾YRhx P ) TohF=%Υ005J=VBy dNiV BmD$ ($ ; %D}Qrt#{gyw͖Sp;UX"V+E@ }@H P$X*E-&$b#Uv@ Cƶ@o=g+ԯe"ȰXjs5 O,|'|sq@ i4 ?%Lt8VO'$#NO&Mܨaao%Х8@-ybIhA″^.a{yC{:j)DU?E"r ,0qq{l/  AdDLa~3fl42 *S;$@uutE-sR͇ċՊJ";f J`%@iD?4OF#Q$TQi"e^كSJS5H`$8KG @Cǒ҇ͥ(X?Zǻ,0J0b \vM ޽Ν;qH>~_/D]aq/#AF`S@~4Q@ EG(;Hi%TzJxbG%a<,+Ta`\b@ XJa-Pj )PP)4 ՝2@G(4>1MN G#Ec=H;6u]E$ u*C$HH|Ȩxlud,W+qHd8^ 26) UWj$E1޲`XLBz~Os[T=Ło/##ø ="B,|aaZku8?8@#'|rt}Qo d9.e$@/\ *ٙ+H´pV|,0e9lw/Chhz tjJbۣU HaA OPact0`j>,[ E Qv(mJd(L{R`O S$槆huzZ6gGFkc-D4599B>j5A5 қ)ҏfhlŨxu@@jGCj!@` 2k Ư*NxӸG|ȖCl*ַ\84ɑ2aaa~z+G1q0\TqYg8B:/@{G ^nyҁ*$xHFpp=ZV  S|,0e9R&e1QB&QBo{9=R T  F4>c`Z1`0&a`# Q4k^B}8sǨB`TJ;3DsID#Z^< )$B5]I(wp"(gbTVB "P)( kRb*ƉqTA@G( GH@w <yZGy9 Gg)ذ80 0 0?!?_ζzl.8cA6I~оUÏ_T*!^¾LI8;wF>0 =uɗ_ODVV :Y*%I 3]^vb[zO@q0`0W5ν|x# 휍bgr>rt:VV D֧HAӹr?Mq7\%RzǻH,(OzTI@v;G#ełA ;Ȩ PUVcZ2y!PARq&S)>NJ2C@@mȷ|oeDΉH@Y3Kر80 0 0 p-HߟMp WPz z]tq-Dxu:/gا<,0\b _H*Bv} -z+= CPfR1 $ !Aq@~;(a1) U<*JSN0P4:>Jd~3eu@W)ER/ /w Pt?oZ_bܩ@? $熄ΗYK80 0 00Sl0\|qD$q#88]oAO$A~2Lܰ8^B3{V]`q/'XJghb@Г(4AʬwJnSeۜQ`>lG'{ⱕ@ލse) "(w:_dTw:nhY-0=ׅ{Q\aW`yF]"ƨ?G](CtHPy %QӃ04:AcAգQdiTL PTq*Ȥ^ R T`"H`\YVG:{祢8G\(֊ŁaaWm{'y,/M+н9&C劈{y) N퍍!\u Y`˰IV& C cQ`6$ LQx0`h%X Ԃg\䕭a0@j.Qm,'KgA:tЫ]1Zg)nm1FL1EIANjNJH< !?P z\+ ՐRE0BzvjN$Li(op dZ XkTDyIbHK((,|/8G_2ԬRn߿їpc?U9oG[aaoW R=`ܼd,ׅB&t>d"t>n>Яaq aCIDATkz_MB6JZD 0X9pZ`ᐿR"Qx# 0`% Pb<+Ɋ) lFc؞YCSPD cc9pC.A=1JWcwm1f+̺sn7wMmRCL< H\s ]&= _#93OS}ơTTE {<ŤEj1>J b LҨxҚ*1Jc=JowjDΉ<7" X'X㕨@cmɬ~{L|: D2mW@ LU³lh€b>,V;k|XIa@= @CSQH'Gʶ>4{}^aWbxLS%fX wbX(⏻^],Vcݺby=86b.F _XH\xˎ?B`o@†D7oի8<;OԩSؾ};|}}5SPK*D Q`$U Å0 gJ02PVBTaPt87Pwg;ƽ 4{u ~ {P Ta%#c{XaPL1uz>r&O^`ǸmZ8ܹsG7nӧuP<F0 Y% -tHJ*lTX6_ISsLzjSpLS+:F#d+JH a` (^yV= Q@>cǣ QF,v<.m+bJyH$W[ rkD So0j'Ybot9m F3ՃLLLC3P(c@y֩`dRH @q! @p|j+XBB|8u5軐>.]dNGpc|ϗ/pvsruCN8s1>R6\\/P@?AЛzE':9Ǐ'#/>wpN\AO ݋6u{ |L)Gq>.{!dz{cv$.> {V1 0 o>/+@-= ʁ݄㰜yk{zBsg\?~\=?ŁEpX\b}dt8M6aڵعsڵ ,Ç`hkB2VE"Q"a@Oƺ؎0S"c` lAdœ%ΔtJWs:cLa~um0VKXi"g/WO&MU lyXI ŞeIixRP%}KcXoXDR$vCY0ffK)4WLO Cc@Pi"*⸊+,6G+%1ȃKY!} fŪH@X((&"'XBl dNfJ&/iˡus{8 ,Ÿ9>%mKٞFf`(/{`~QL$IcZD), gh$%Sžj:1lXD㌝(m.Lm(-f&NhXw$U3Qm2܏M xclJH>GD%~7}z_m쏒4>z<8[A(2DHOـkgX |<_!8wŒ^'cA{O2 0 @9gGI =݆p⧷iпmTS%V h=}}VNz\ 8s ֯_'Oȿdf8p+qV8/puy!իWx왼EUϲBqs#вJP)@6B/0Q1V+Z1@ vȽrb@Ll5 lD-2DIazS^ꅡWaNt fn!Ѭ~ X~>+* 4>)#OIQ8?-*i!Ga%G?mR48d\d? vϔ퓨 V)ڹʸpt>,&Ŋ@'4*LM)B 7 Y^OāNcf겟ELdAdP̙3 GY>J@p瓓8#wD$ɍVbY9#ZTFnJ̦̏e3gcά e^zY4 Iְ=3gR4 k,9RyWA{1N~|NWڍgaYؿ*͊h>ZvȕZg<8g%Y<@3lb ln3xmt<ߊAu%i2f#ѡdRqI$GpghN/O2Slw6L YX&6x?drs ɓ%GNCvds#SÃt|4+a C>TBq 0 ֻL$j`VPkci]d=$w5 hRb>˗/۷o#,,LV޽ŁPl}Oc`>LB$_oC3hO+1@U ( Q0@cb 2#a`R1JΠ/k6)g֗?!>zan}&l'wKa`:Z=*`r'=+ Գ8-|8+~ߴp{ZNb#Oax>UOհ~MfNC#=F^AWzEE h}f&T::eMAIiPLly@ }VD yIƓL UиG yDZ8=U)3U@!0FB=2H4.!!ՃQ"x,Q :>^힄U1v+E|x;)L.VB:ϥ9;k~s1FQpl< ieW\򓑼{*FY 6qx:x:xpS_L/7CfjXm}:Dm[āOEh2yn>7cI~/Ǎ?̮`z"H,2^Eө-8v/_k̲<Gbt[cq`V1z^N4Kkg_*%42;2aa2& \BeL -:q|t^$8tޮz@ !~r ; % µ:z ߿%,0*O$) R( ZbT1@3Wϋ4 Gq12 ?]&+PhX= @;Ka`Vqo* o `ˣ=.%+8=ˇ <<7.?υ+OX~^<~VwROUOГȫ@OU VFu0&yt]rOW'OBcquƠxT<: eNE}dP<[ J.٠B-G^1~""C8VCJD,|O'@Dj2zXQDe1rE6IM$Oq :fw.+_̨x[yw*$i88 ҈NjvuOq@lsF26LjmƖ;ym.?cMEsaԚ7g];&eɃFKA'1RU`S 2D1hbaiISe碎SZk0 TxS&A_+c}u0 0 *űW8x> #y;^?Ty$W#@1f!1ؐ8p#l܃ǯ7q-.X`V(6un cLJKQZ\eVUaJ*H0T lT Ua(|I2uFH<= D }0rot[avwͥ0~ l$0y}RL?,<υk/r!⦈[Ox\R,8/?#wYAYu@B|TN $P\Ocw~.B }@8= Dm1Gt-q?q8pg+TTis.mn[6qZ:ecʤëGqG&v'FɄ:%ֻ]苾zb{jKfܫ?UףbgpFV d֋pw_fýQߴ] מ%U$vCԒSTAKג&Ŷbfj7`䵎|+z_Ahrj8duDCS=3Qx - P[ 8 $W;UH#+ Ԙ\Sm`Łs 9r4XdmbM\YۢqnNyコTQ{6n*8 ~?ۊ4X+7qf}WV @)::pT_q5mی$fۜ;یV}U "5ndHWdS5G g0o@ۗ?%~Ǔwxߧ?>ú>9MZV㱺s d~aIH}?XK8p}ħ]輎@+[ Ş'9. /@DmOmkŁ0'B 8)̰. P !UU 0|3fr7 = ͐G'xNg}^!⎑Ĕ50a <)! ?- jDUȎ/HcO<~SZ@=m izf|=.)I Xܫ ; `3n1jgYbt<7@VA4>1uAu xd2LSf9J{-iPlΓmo"{S 4&"E,|G'@gMr4g R0w%D`:HN@ux/W4)ߏ+i.Z5eYv(vb?HlXAzbw "H}+y׫OvD}^0.q@lOUo e$T_&"ah}IR$/ϚL(WZ<@?bՀҴ:ϠxP8 ɪ/w@t$>OLaa›|_+.JOؒ ⨧? HuTfxW rܤqU9zɪ8u}l~ ,0e+e%T Ha0@Dπq;,QpLS}jS$TsncvBC쀨vBW;cv2nd@|6<-+b#3) 80 anȎ{(@Bs/Da7-&X PH`(^X +=aÃXylo$ Uq;`蕮꡻l/4N CPm4]OfdLABU +Ɨ9C`$й" L'XB>+Dß,nxO٣Y!J8[߼ž)Il3xX%z;ַl+:EvKT]9ޝy08s%_8Nޝ&m^x6}ryo1WGNLZ R% קOo`'}Vpǒv5hp oMPVW۞J"{!pyg'}:30?U x_Oc^Nj'r?7±S{ _ͱBa (Aj{_0@3 đ"fP Q"(:N qpY5|?K}LϹMI#`g4 &`8`d w^d'QD/>+QߴKOslpן甾88<.-PYV1,[wBnŨkrr3Cr=1<LBكSe%^m$(b,I X] KQHc58Vh!2xy+s,< xzDŽeҶDMsbL:,)܅z:Z HD ֔tKl.0e1J.tƨzY:3{ؖEU,DQlEn+QnRIW}1jt-Y[Jt?ы(ВXŁ Щ(]G,׭ά,bFѣxZhR );1 0 Dpu0z*&$ >* > $݇t_j:)i=27>܆>r9|=Xy m>Z#"Ł96Dj#a "a gf˪QL(olS$TW D aUհQ~\Z&)aO4ß-Ad* ZxkR%U@pϨ<L Xr4fv5^뀡WҜF ULBÓe%^hh4'&%@Vy ΍CaOmo2+bQqwLKt!s^7֡ohΊZ3ps:0{ϝǹiz+dz†k?9(I@Ex>=oIf}X?mnymn/"^՛ut&#H!{s撢puEuf㾟q2_׳:2g!n]uxmz):5(\)4;qV0 0 DG'Mkq]z {S@m}j&H8*͒?,wX h{u z_3ie:oh_ /Ā6 cŁ],0E60@3 vSbvBU o)Cy Cccp!0ZGLsnEU Uebj@L {JS0T 0@oD̎^9'W oEH@U<an>ρs˶EBT=!UT{T=P6Jm1Z' ={顲uP"Qґɲz^zȓ!/UlFiN,ƒ TbHB=GtDd_(:)X`!l-ѡv h4`l:Z}./n|pt\EK0}%NI1,p"9r?`NSQls-ƵWĈd9Kb cPГU'L$'.Q ,lp.xk~tq4IU;a8>ނ? jΉheBgMt`/ǖslq*)/F?N]^1۶ƶH LB96Em6Ք sTm q DԨP qb!9mM8W}wz Lo* PE_ĕ@f^Ƣ4jlu;mkPF"ZzF}Fl<ډذ^lb0 0̧ λ`YE^8* {C&F>Gvp}"b'+о(e! ؓ_%m6bmؗs^_8}idOOOZ\ӟI8s6 և[2 lAP-@1 T5V ȷ-k^kdTs:nceKAv/]aW`~4u*b2jiaNd"L^Чar_DW.z厌b=N Ti@3ZY@ƆBOJJσ+u1N#D1;`ծRrڜ,ExkCP)DzlWP-1;E "B YZcbXB8ՐirFY+7YEva;vuunZD] w]VP,,1PQ${{ED|ܹޙqsDŅzQ @ml6&%Kl%.+ ,J&IR9%ecvB^;BJ@lyOj >yg m?`޻`d-:my!8..UUʸ Q@DaU  (@ ʧ}ʳ,*X$. /Z qb;- VWPk/-xjaƲG0ALgAߚ!Ys+j- OHLT=P_l,(: Ђ%MBp pzЫ7f<Nː7nZбyДR!)=c<ca$R#2Οq]4̈́IQxfw3uq=:=sw2Ny15ck{3{e;x+RK[=#Gc?tϨ j#{Ngx%j` u0y{>tBB7ף&(G!!!!!!!wR  jkiypyF79 r!o (ڞϚ3Xgq; cѨR{J{H}/%B @mBn$AKl>~1hSA8%[> (  /^@pp0:9K ss 8 ^q8$ $iT MUf([ Y4zxb<>+o)4C&`hLvZ -rdD<[YG\ڼjL=W%Jj'D {$(@0 ֧"8JH|pĨ_t<#{t|:~*F#|C:>QR+=Ľa_mmϟSN vIݴi=z$@6WT pÙG>Peo%%-Z0"$$(ڝ)J`@4k]i)DpR% ,ES+2#Lއ͉/x~o׿\6#3~:~'\e jCFRZ^IU)v( z̉K'"p(q}/`s+̝ qw^0aؓ)x8WRPEEr8`}6E,d@ @֒hBIV}"qw Ԕj/zbxqgU? darފfysK7W΋#>C(!xmxx8x"p7^vFY? 8e]+Μ9;v?BLt4FX)R5 (a5 4p͉q{,&cރ~X2y[peuغWx o@6"&8+oT`BU(cdzcǥF )v8V2%vp` 풒4_+'8@OH ) p kI ) w'YT^j՜s+u~.J?2}[A oXk7F`|~IGr(9lݺ'NWR6)[t^3Dǫl%3_7oø{.\ݻwÇs4T2],p`~ppp-p PfT 8pŭ;^)p҂fd% 8 $$$$$$$$$$QH#{U8RJ.AATy:/Hٹ۶mΝ;q޽d-mȾzgW$H/[>DZQ Pk3f9eH9@IiJNg!T90 np V !qT/kr8xGN2$*% |%3bܴ]-CiwOfHslHXx 8 $$$$$$$$$$aTR2?!(?B_@ 'hz<"Y{Ih^|i] mw"$h@@VP:U!q*8YH B$41x!(D'^Gg8yw8nDu0(8$Ob8:Pf@y ^'hߕp {?"6θ_^$Pz (@P6p`)OnW:UFV Ly_͓nLPqGpaOS[`=ogsic pe-\vYͮc0AvtΗg<4^Fgey|Uj) h}ЮPUIZ&54aЄ:Tޗ4^fq>EWU2 y=i]m,4C~<&#~x=^wL<\co= ypYgX 'M˕hS,{pؽ,??;^`B(Ku߿'''ɏ薀Oaq 'xKJ&:T4G ] Uk!3.˔T:`̓ݔotv1[/D[y|e6NG1n |u#TVP{j$co+W2Thϰ㜀&>4؈ R'Jjt+G=q+VR߇Ѹ"V~ѩiKobq?mO9^8E8|#",Q<2"jqi|2GtK8}]79̪p@(p-̥H ҪApL%zdj_:'%<)|t<FWg)pkF1`+6s߁v4ɗuqޭWG([8/_K4_ $Hw$pNH 0V,̈o%=oGyki`ݑv{,ݚצ,4oj^o`%oDUa -#bw]`9J8$g`h"=t,T̫8 ,dCM.7 -}W=/vمC,ҫIkQ-x9M@z@i-$Tj-T;ekK?,4' HZ u୅4Iyj-D7J?dLLI}O%Ƀ rJJTy@kGlܪ:8ҐW3l~Ɉ H-be? u9ljdFlȌXj (4yK^  `F 46p I" 4j5,DP!!!!!!!!/E5MTT! ') >&-8]]b4xR%y@ er?CiGDO8DIh|ǐC j|- /ͺp@("8Pm)*m@EI@ܠ8 YRġ8V9ՎsBKJn-ty@S0nVkXkt'#tn]/kC[*%W<R{!2' yKABA;[mig>銕RA?̼?WAPK&u\^5jYR1A$BgvB)t(J1_*pK;]Ozy !.wp Lr^hTGTt d"!!!!!!!!"u5|BM- P^ $ *;KP:\ ë3/c0L7=a1?zڸ.yЬ~ @@*(#O(AJT-@'[ߍ7!fvEeyq:< @ES%W ?5UK{czo(z{~m:A;B* Frޖ)e@6B PRJpKP[,W*E`:p ޏaQ k*$$$$$$$$$qixƗ5i>5TI͋AO:m= $_t 5H<γg" 2_dm%8_u_[De% 8 ^q8,PB,LufH &% Z {{@XZWV:1@ICaQ0b٘Zlmy!F8Z'qǣ{xTA@-HEIAxƒO/8(Gle`Q/%z si[`7"ևٓ.#aþ3FͫS U \ˮ򪁺''H^J;!qT [%v^U^g2Jp P7?6MPHOG[1L^,$$$$$$$$$)禿*߫x>?9]*i+q׀OOꊁ [߭#F y/";;;Tj N?`+ݐ3+IjcXaʦ<ϔ@"H%@.% 3EkQU{24ZT=pI7'r_ zt[wέESqIj/D~n p/JRԸm4jp W7zz%ЧFuTP{Y$$$4 z_z`1Ok['hfmtk 뜺`ӳ-v8ľMqԵN5pŽ qϳ,=ɫRxYCJ(ApfQST1pݽ.I`˺8!:7v=̟spQ/{3 Ƅ#0X^5)MJ^PkQ~dBcmgFmԲ (`6UR7<{UfYI|v}J8kBQ==ԟd'sPvZM8kZ|l ~n%hȘXhXxYj ep/(N`BК.$Nqp{œ'OB|UWxdC0@p@(5C5 Okz2X~YfbX0O|) s`yDo_ m6e>Φ}9.ĺ򖙨׸LÌ3b5E6-ø 1,|"u;K0q,:G?S0c9\h߱x|V_3qܐ6LKǴvQ%옶)y=15xmw4foX3n,Fm9w e5ib j[.A:Y&9}/J1$WL8$g#xzJg x}PzE@ɓؿ?#GI,o3(p@(CJRZi:$PbTů XXrLx%_Z'Whb/GKs,צBB `٣X6<5/O;gm˹ C4jFUpǽzIe8xY,'MF=[R8G`h[gY'9azs}PNh_hB XjGWrPRa 'PxN ḃ2lܫRA'{EQ~YIdPnzq^_@ϮM|=%~Q&6% ے1p?cBɏU" m,u(.yi{2瑸{&jG7-# [^ V9ˡ([[qʃ%dG uTt̲M;w}c[aYzl5Pd|V +P^b+^ƣr "9~p47)JyKǏ#6Vj%<"pvrv)%Pz5u՗X!'eH+ vC_s6=;\:[}œMU8ps53Jk}޺EaJ6G; ۰mm7$}&\|{;[{a܇io9B#u888qqeQfGy.o(UÞ6,JY-l+?B"8WXPb)|7Nn}+ t(R=>0g?cFCiY~ګ}ϱmxso.DzehMWsmޗ-zr+d+ydגͼ +XPD#2AFPj5Sѭ[ [8`71++C%T-PP* P[(-h;jj& o+PR>^i/e6Nmyۍhј0  `ţ`+̝ y[܊{P"k}r!VWP 2&΂ YZM. pEcvnFq ,u셹c3@ FSv:턬rg5tPj'Dl|8%V ǀ9*j (@W,*p + GJQBTQKM 3S@jwb>۬Qz .D aڅzeGcZኇc(ι8 <(\7kǧbY8+GllYzB)gn>UǷ/E 0IחG)fs80g{t\ϕ&u\Uo_F?D8ip*uPDzcBSdt9m(r\Ѱs:~)RPWl04wpיϢhQp972[1-A%>j e@|~D/߽O|h4=Ey_! IA˵$i cԙ5YJ(N *KT%H@UrJTE`:_Ck@" 8-fx)dPL}9 0y )^8 ξ 5y5yظI?)h9#Juq̥haU%lކ{ lbǡ  Ƥ#30]lN-KP@]d@,Ċ9G2VA>=BWfxYE i<ț/}dl?'`?#U[Xѯ*O?͜:ѵ2_WETϱ}b+ҦÚK W7kR]~Kk*ZCu&W̙\s#p.T^{9ӿ_`uJYpk+d}k?2x 1yӓcoQ׶"{^%o  e iTY}.Lgp<^IRk4B%8͂ h71t@(pu]qc_żA6_޼yܾ}8~8\]]q5;8S4u Z-Qbrz4C]$H"Ő4<STAcT>V8OK:\ +3ӵiS6(TAp*b٣^X?y A&+*NMk=e_4YGh;j ?fɀx ۵п<3 Y+&P7 V*ęR1@`2Uc5}Ab ?><]O9PD`}Ap,3SwD~==^~sٲ[h1+<;^x|:8nojG ^^Z kƨll},oB7M.I*3EeA;u7w݆l?}cޕ1]c񸴤#JBxtW=m7ÈDGf)$$$$$$זu{ +PO-#_$2#v r ;b?/Dv` zO 8tlq@DpV\\ݻӧOcǎx"gΜ쌄`;uZ6,IgJD'BUf( @-(O-A1Pa _nhpf jAu  =cƿ8 X̜/:x _4]C]kCOpGC{|& `d ذi'^@mV= K0p0&9@b@,<T98%Z4~dL[8J-  v~Uj !qn8gA ==8o53+}p) ˰uJ!rw*PuDUh|htRBU:ؔf EӟHL8=p !ҾYsF8~\:U1t{c-`V͕dh>e+go?)!!!!!!/L7P_ёt&Ajjk MTr8aP NpF;鞍/s,nĦzR3>0?>a&lҥK{.كsޞKKKPpP F^zSzRٖ3GJ>+C& )UЮbb̃ P*@`J֢P_gj9ؗ[0\A@ ycc|܃ZdMOlj5D8K`ss~ь;_@@PoX'901@`<$00BXZ'V$ƨ@ PzЯpET-@R@  `M})L,*p z7pŎx@||lLa@p`p T:) Oߤ&:ߋH&25x-Ud3dΝ9}/>bMws 9O| o%G/^ <\J x N6Ei?[ ڠ@U:UaAFf,{(&@@wCxИql\+Y-D *CwFap>Q/~w'#X>5IO;`Y[  :'B'ݰq,}ԛW)fǙ@``8F LT1ЊoR b*#R=&Z`] VBl'qB 0@jqƛM]^ԛ)vbQ} L,*p z'稧:u:ze3b95^r MP=^w}ĶB/C+vk_/+Xb-ɏsoy@ݑ[qWG%Fz@č(*q4-”|ǶoU`s_YqL#P T6aW.rR'DC6T>ٻjKPm-cUlgF*[yGškX,o*$ 8+qO1NK|٢ll,o/D>C턴5m1M0E)RLω` h 6"6CC -b:n[1]Whtv [/e3T5W #0P"X'>NNύ 7-d&l5z`9b?Nuo㌻;# sco%dxe 0j!8j_)-R?zbߺQJH Z98s0ƝC{x&[hȠ@-MaWt=^/vKu)lL5)5xtT"=81 u c̮H?&G١`$/5寄&PS]ظo0;+}.++EҞJAu@^O\ Kڱ6bW>F[Q5Τ{v D["DW(}p+>A 7X?{0bsX 8 ^M[dcLpNc|$ XhA^E@m@[ c(G @PWA 4f:\=z\7'aЭ qg,ڏ xR* '@nX+̞tO=N+2^X/ o팆 `uc cW ,`@1o\1@*L-8YHVU P7UrݓCEd% 8Aq8?=5k^AHlſ1Qi -K]]d#Sڮ["(zyp)8G(z4p9{W Z-USwR: w̫r&8":6{-qy/䝠Wn)p%vOAoǟ$;Sb@2ъ'SqsR(P_]6ῧ cSCMH op8uLɏbKZמ(// Uh٘5iZʒRw * |ֽ΄{v }p'6ٿz5+B铀Wa1+Cs. 8 ^h:ZH1x9=QJJ'BRj5 )]eUF2 a ƨrd j%hv~.If1oMmF}&; Ō{0* ">X؛Jzb[=Ff gj!t SُHn<<oN@v:/A{J1>#Sk*Hz|pB׹m1]16Nh0SW t/=ݧc,P{ L*p pG&MR_ܱz_qU$*vLDoC#}4QJmjѠ;FdTA8?~(U5w4ͪB(z>|NYԿ_2Ӷ#߇~ХuWpa:Q%PuGtcofbEQ{Vc\Y!Tbq q(CԸ8ϮK//;;!Z7ys#WNu igOCt|W!ORÁw3f|1q Qh; DG!!!!!!!lX܂bHW*#\:yGud<|-Z [|ѫ[ 8႓c10IjBc4n#t9@R1TE@^f)M3y AyTA[ ɀ` 7 VdRpq. .B׫31n1܋@0~0s9,X/XIrJ`Apxao?B=$d;]@KfZZ/D㳋р{ 1@ %2cNl[ժ 0B,Qc xӸ}{ƒ~Xh d 8A%M|yrNXhΑ ~#`3JRG!Toj ܺqZ4@ee|y>F/sȷ'bAhCA>?Z!5"=`*t,d?ikInoEZ: Ԙ#. WoK Pp DFAXp!.>@ 8 ^MoMbќb`dR5ACnKԝ),y"u,,P~FnΛXALPZT=[[P&V9 h{aO^nT孆&bAsg4$o?1فbI^|rr+mr+!K* nLЄ g$7Xrؒqq'Xb|O'#_ _"p JFd%;_K?@ ޕc;÷ȥn3yP|hԾγWh?Z%1h Yp? BTpJi&\v qqq\zU Os U'P5s{m#%[ʠ x%Qr!#6C2 :Z TN 0G-r6 EX 8BLzr *nV</φՙǵiܓ (OTU@@;ܚ~nN=Xt=/! sASQ 0F=(GsW*dm0@O`,K%I` *eZ6=7Ƞr6P!.㊭-lWq>χbWy _A4qxZJv*wD+I!ρhxMQ9w_4'=($_hW!BCCy 0$ΘG' +O=!WxKŦHj5hۍERX6?Ig P s4bÒlV ɀ (1^{H-vT$@{J&`@S$snVL&/ͅ t6W>ÂAfpC{ 6#sPRԡ hW J]RZ*xƩd6fr!W Z`P І`'tox}M;:3ȠLEbz9'g8n Z鏎Nߏ"wcUu dQA&spz~Z%Yhb0{+ e#%DvB* 572>mRJG7R!pWp b{ O +.rBB$Pz5s7\vFfRtaUJ>K`=@*j34MFK`bT,%٤VSB]W!ʡ5vtdT|rOS!jC^ (O3;]+ fJ0H3Y0dYjA6nu~-@m.A=v'WB#k$0vܿIZ XƂ6.3VB/0do^-&H:4o fd 8A<:-+&cki$̧1?1sÈpFDiqglL]{>M~ xg׬DU=ŵiIj֬5y:t6^%H@jn?Ul &õ|-x)O-~ eIqfSj3TW"h,{!Ay+Zu4' h-HfíΓ-b\.s Tg` 0r+!^1 x @ 6-nD+6JP  (ed! 8 $$$$$$$$$$aib$ϥIW uhbtO!Sbjcc_ yEkBooWJ ߰hX=ٹg8!QqMS^AHJf\NW0AGQJ>S"Ҕn+CnA|bTL3NgT,d՗[*@?% PA+Prԓ!%i?'Od^L*OS5 , ,R!jgDmz! i{ I1@`&iU 홨 W @Jh@'t`Uv&h|9 8U$gʕ+ e?q8@}.Bw 4(qNy@$lR$ٳkP1[AH(- 8[jxG3q¡볣JfXέXm(ѩ-d@ t[=6 "h|iU :9 HfRG dK z!*jGW6C܋Cz ]礖CdX2Ҡb^ub4J 0AUda `]Z %)̇ d:\[*qҸ4!2wLlfmoC" ct& @0Z~o[Hɠjf["p@HHHHHHH3JJH ݤ  P>5[&_ij9G)cU!/THHAGUP4>}qk)pV||R 1orVnF|xk5$SbnB З 4 8ᎅGaqC%-___:t/_ƩS`ffgycA ::/A00hBJ@7Y4S]ZK톨=po >ZCxE b3,Qg%j͓T_bjA d&KTA:TcUkyba# H@U+jT)8GK-$oFH 0@m+0+VIȌc U@h@}uAR`T!@P@Õ0lBGt-CjafY,">҂JH OʁP{&/2-iM}r1qb@P DSmx­0zsp#XwgVK\p_{͛#߸~GC\Z"E0'A&!A+_vFOъU RZ xȤXJ漅O2@w&'?HŒqRHyu,To̶7Ar"a (Iڭj,;hB``Hж 3tDЂl9G(}903p H Ul \qJܵbr U0^{7~^ލQgw+D s u>댃}ܽO8|{v;hȋ-b^'ܽ{W~݅ buRC)$$!zPB@VEnE/ƛ(X{G(8 *ޥ/DƩ'a8pN>ƿ7*0ipV`` Μ9B/^`cc?Zð\U_(EE0ɢ[^g&Hh$HmLwC j@]A0_K-Qm TCfQn1d@@FvC(!Z'PaA9 _jj#Dm8 Hl%T1@`<lPJ`]h W $L`@i:@5KWl6l@PҽQSh_nnp HO.5޸:u0Ƌ77E'W B>q6>.JͥuP[eW%1g}p; ?W5Ğ)9QQxgtE`OVy`CB %/ G3ݱƳ@y{Hy ;amǓI+hl#.XX5FJcv(),$- $$1^8BYMZPB]g=;#_Ụ{x.D7:۷@C: $fl]m`ggc@l ~a1xs}aq9[?g&ojYE|ƥK8pttիW8;;ޞ)3i9D?è<UaQu>O>d4%,as icuVjc'`)TWs&.QR~6T#E%,pHYAU|;xW*j9 h6MKu@I?s%@P2Kަ T^m*P%RqP8*ԂrA+H`UH4o.K`Ɖ|:3A'26BT-@}R) Ct8W?tXt(1Y,$zy} Ҧ(1L^ dB4dw=r ؘC~vunP\33l\`[xWPخ3@]_kW>{BS(]wbXoxtU&_D4/aR+/rDq7 _`1*Ǭc[!!WF}v%DC* (G^ˮ)ߚ~-"p7&V(}u 8 $}v })[ÁxFg0;a;6X?ǮlyЧB՜ KaT~*.e)f%Js4cZP!f 4o1D bףu/ ܈fC:`lPLv<5Xb T]iɓ𔌧< (/W$S"ا?琀 %AzS APX 1mbgg@6 n.ֽ6r0@ q[ʭrO={ŢC0[m)@]#QTOu 鄭BO/7m5)W~"llO 'M _;zp ?+-Xe@KQ5C%H {V~샢42)R~nݺ@Z7pM\u~g~~x&LJ]  0n|.1E&Hdb z-d2(N^Z j/4VeOj(?@9 bH& >fZ ȡ- +cq0 W T[i;7X$3WNW R5j24CGg@ h@I` U  w%=nt^oUd}R8$/ 82 ̔_q珡(}Q˝'ͱ}0ayc&~\,rtVqARjMZOq^iENCZMc}lIpk ?Q<.ZCIh ;M:n7oEx^Rd˶MVXةrƅG|Pyi^ Ο۰b?;.:7T -N"7a{ ٺE+ןOutK+'R+zs̃!+k'x-ž'3Na(MaO- ui:UA!M<ΙðVϓsi˼y~`1.~($q|XʋbV+?v'Dx sګ9ǂJH IIiYk!,-у2.),gq+B&t#q#^z [>= =qE|u7l䂃cJ(Nl!ҫ9cQ0*6FߌQq ( H]E@ kP W69ڮ$7`s4SȜfמcZ;UT_ngӬ}̒W$J$ئEePʿZmǶZ 0@DπV;NlK6OC|9"zmDT5Nn'd"jd@`. P@ -PoK 8@~7"aCT4)A_sA/w+a,xz/卦mO.Ы|_O&.O`Pe) CcOFB|

hq[#~~.px^^e5nɞCp?;י^vX})"0^9,Z)gM&8[oxzb u΁ [^;43~b>:*.m@ӻꡄޮ @} %}.7lP=@Be ; &sɜqH % ~٢Ő4?>ш̈́B9^ijϛ!$+7]pݰۗ=߰hD}| 8 ^ߎa'j 3P;!*ðު)Uŀ6 XCX#ݫG]A0[e.@'1N:~]>+Lƕ㇟Jl~Q' "pnmooRGmE8F1vvIȐX ۇA5;*byFʡjQ իVAFXw%\80-_,.[z}S[8۶sq}gK}!qB#c8{cr` =ȣe]y) @q'a ԥk>W^~㄄X!*5,;ѹ)'Jؒ!q腕hÐTWVf0Ρ?4#Is9q4\`iA_SOK3dpDCL;q3.\=-}jm;) `|[nx#Mօ>>PB@#?&AOvHgjK|oM,}ȼY 9Yp#bv~=J=AHH> U1 8x 8~ ='ٿ)Go#7᱈KV)+]%Pz5g~3 G<,A,u s,w?PðJ'VvOЦFjV@#dS,QGz֢텪iꁔ-ȀTI" W v _efqxjT@~ځ 5`c-t 3GAhO6!UQ?j/(UNb^An^(P_ p azJhֹ{Ru@mЦRO8}.8Vɦ +7u._V:GQ {g0[欣xj')s/H&SNlt~vQ+KRy jf¬ >קjϟ%4&{@[Nޒ6/<} L)}+kM Q.9B 8X O$n?Vœ=tWH}} %܇T>36B*P%룞YSi,,|"p;hkv,y_P!w+p@d p.?ɇ8dx@{`_0rJ^âC`X` O ~,yPQUUԗ {5yssvfN{ZP@S-zSꁺ^H-S9yTXHT@ (PV ^1 ^5ǫVsvBdzT5Η h v=uQuUI3U PT5`}j- jRaיBèԤVB*н{T+msيd}B |x)/ p=#Tׇ!/EٗF +M>8iԦ/=CTu%EvpT/q*"@#,Ff5lw 8@ݢ51hgyBaQD4FDp@兕5A=/[[NcUYd29ڦ6Mp n)}p@#uBsp]g?4@M,^m6=o^UbAkqLls8@o`ASb 5FH ϲ!70k5>-oLcl7bl|nh?Kp4[^(XDq8Ѓ]S}C^\v;Rt鮉J`ۗLx<> 6zM|+p}.8B' 4 1P?pY@\f o%cUj'VYyg?Q"*n P@T 8Ʈ[ J{'>MŧwEƉ/LWGχ׃`o e1  $tQ͐A?Tpsżw~9q5r :]zuh9`#n&:UԓhV>X(W,gUzXJs8@QJ!B9X@!/@A2d΂퇷dB̎[VՀ5pT5ИPUy+mzHUdܩ):Y ad%0U8` Bӹǃ\1@Ġ 7t{W[d!}RC'򲜠D8`d;7;e_'^v"}pߺtIţАYmW}pZA;_KUUGOu?<e֚;_}:G^ܳHU-پ ִD8man*}p@ۺL.o:WuNNxe%?%(ZJ SÁ\_C)Vpj+R'2M%ۏ#ÁعuXBc-;Uz/®̰qǡزBgr }ip@  2Kg իd |pzV#"G=q;}! V>gdf"ȀPJ+Kɓ|q#ca gP$7!!ҫYC!@08 tm@@fJLUK-dxLyk=y|9'VHIwJoҌ9jKTj/T$r yv81  b sUqc&{ PJ 8@URFV С:й7_ge0Ȅ@ S#j'D&P|< I` ?/t~AhS,$2wÁ1oIQw1Ns2zS4>QƎf8a%Wa!0:{<hHv YeyXC>{V^k9t=+aV pB9©@M,V'^˭bo7fK"=7®c^rllxn©>cU@f^PlC #{3%f\_~S te8B!M\8A&J&@.q0: [z,?Lûsht ljWgSAʤ_ E{Dp@KP]{⸮4nYɼ:<耴ܺ)pg(X+nKY%k!X+{Jf Y]za`CC{bn? u|W C#p}}i M^BB﫬@ 砖<Eq+T-apRh=vY@F%8 ,k`TAUN;o'$W $vrG0[A,"2D8uρx<ٷu8(6@ۣg&p J_SB_N⏭n&tp}iV1LnX@J\yp1e9b]3Pm]ߵ~H]0RmZrtYО }{BW~b}ZGz@pxm8j~+:Oӟ8Zr#t%x2AlbpgPٌzAnzmҺ$8PpʊE8¤FTv3z;7eC~(<.[°[nԛlzO8=Z8S.3!D߆@&,#pu8l ݼy..._BB/ 8 .q8Pb s A> F(~=FaTt)9]AaTe!j,A^&kۚcG376;Ѧ9Z@AԣMGZMh8~Oބz6L?oFQcfT_bfT]bfT1لʦPɌņM&Td6[9~Il=cW4g`~*Uaj,J5nFͅQ{fԝbfԟ & GoBh>-Xuosml@{ ۘ@Z4^ +`X{T_ ѥRfJLwS`$>Ά_aQ0?݇=X7QHp`d 8Aq8P,B|`AQ(q00k;rteeS/_ #'@iy }ZJ`?k)j(̓cD,p;- .F}ȏ_i2w W|R֌P_zI{/On=!@BߣV*˕ ֋ '8S<+ ~l? g3l{HofՁzO!`ycϩ"&c W]W]F˖DC@ 쳉uWyYss`P,ܗR HjoJ7v,na/q?D_I-|+ V .z,|l/@7ǎ\@um߿666|rb\r.]⡽<3cIi]}h(L1t46ο <<3P 8OG!,DQ,FDqrLJXLSgŴ(?Ō(?SYR9 egLM3}:*i[wtcVgLT:ULW;gv{@u4`E׸SѴ4c߼D?CZcв(dcֺ0.Ư@6ٸEqnUn=(-BםjX-֭ZȌx~ eȠ@oPtY-[eʔA1dR敩ľ`,ͷQ.[%UCu&>YʲcZ=K7d &7j%K4Jy=yLT3lY FQC ^<ʖ.bM}*!8$) ŁߣH͎z]^\F~4 +-R~Dm0ix#%{VaRƶ rmr_^IRß߶2~xsw(x2:4xw JbY ;w@OUwקk(>_E;1M78fF{wm뜂cOS3~ K%G3q!B=%AS7A^?gٲQڨa$1*/:']/2vƝ?6o?fMv3i7Fc[;]^+Sqx};.&JٓP2qPȇWѵkŠSG?$F4*'E Vl߯*KR$T3>}6ty$f{@%KN Z2l P{}x Dxc쓞:{\jMXV"c( y @WG}4DΛ8EB0իW|7U\v-Çq=xyy\'#A{.oOOO|Hܸq>L5V8}4oAcǎ}عs'=z1F~B_=g/9yܾrWp–n_#9&q*ێmwvu[+gq7>Mq>acuxrj`)x{Gb;{{hG>p>v8kw&;k!_N;Vvt|n>mvpxIN8idAJ t>˷.e 8A%ztE瀐V714Ra[/*}""_t%E@轥NbWZAj_ $*r2`U '%9o'݇~F~p _"4 }28Nߴ*Mi1̯dĚ7%)Y1ŋ2B_|||kn w:{"$ӫ FzF$@,ΞBBBBBNIu1zKyTCͰxt%E|9! {KSؗ ~@/4Q>,􆚂7 ;Ƽi0Od5Ū4V>_t|,^G&'_y=2>獍P%cccg&$$ **ެ'%5 Գ~Ϩh[ZY37>>!!!6hLiΙMYtt});3MU훎!OA7zgT#=>#Eڎ$ڟ|~ Ky {'5tii)ڞAWHHp-p p@HHH(k@4^_+wB"@(/K&%CYF?y|I$y`$'$D-nN<ɓep?~~~z*%pg8AE-޽鱲mz>Dt'N~i>nyRkXXXɓ˺PRf_G>It3C9=#%@@9p!;a=x'G(׬ tb>^: $ 18~Dz9xp7n2n1CUҔD/7$2DI&JQܹs%(NI|JSb+})o?|'-;wx%(o>Hۺu+OxkeL/ :_J`~32;Y[4i2JlRb?IDm6>%:/#G峗\i𾳙Օ'3 { F)!OFF7;{?7 A5zm^|_V@W}Lkz=O]s2CB-}sP2}׮]|>,1166":/ŋ}Qb=,$%)JpƝ3A(g^6l؀@y_._%AHz$6O^~GOzPB=wz,oQrz/#̌PUTLI X ]?e 8Aq|4R!6q}^˸| >lPVR\o\Õ˺wZq.5$BSU_x^óh85%{8%i?ԪOI0J}ϓl4VƒtT@NQ2ZAƃƕ??4sCDIITJR;J4>B_h;=Bw|Jv{t>Z#@?}^D#e?}ѹ{ϥ>K]~@cD82/2E"D  r(1EA'%2D _ROof?%>"SŇ*徕3CUΌs2KdFG)Eʾ+F;H97Z_9'%r.y(kW({K!!!!!!LDiBBBBB_D"MHHHH(I!!!!!!L  r׀Z9h#$$$$$PHWWw7|M6}pd!!!!!(onn΍3]HHHHHcI!!!!!!LįiBBBBB_8ȑ#8|py eJ2RDqqq텄 &!Z5&&0Qb*:p AJ8kl!c50V3^UpAЗ8݅uK?[oĊ>BBoDJ( $$$$$$$$$$$$'o\/zś8y! C\pcJ8f(yq /~Es]coP>x{*ğ VW|s܋FH(SS9ʚp3(>Og`},}6XӠ(;V渗㾣k|l-&wBY[p zF5XmO9m l"s3y/KjgQ\G[*$$R&E|!P֖Y@~p W6 kc8ocQ;7REl;l\n5 PWHHmҕ 兀BBBBBBBBBBBBC|vqwhԢGe(f_k}p@ #Q3Cpc 8KȐx{"8"ynJrB@!!!!!!!!!!!%2(uB "# jpCBd hcMǒxqu"_c)켁(e~"?ZHD&'eD *6uUG<h:cTl Qb$"Ҩ%m%&d;P cd57G$G:OE*IEDX8"IlزyшOH{Hcmc}IzͰc&ϞQ>Eҽ|Ib;fFa@_kBt\:^:&{q<*",]O'F#2, !Q)B9]"r~( $$$$$$$$$$$=%@qt*C?8.ժ_ RCx}%}+}@ BPż o!SV㮇r׭h?;"_Q^qC23&,7cR-DI3! F\P`3tӑttp@ϓ|rMJ.rm_tKR oWW "<g6F۲e틢FHy`./{ 18CCoG i4cҨ`іCpn֭УE,VN}̈{4-__~ë$%)F)n^ƸǞׯoQ$o>yb̺Ck|x$T D.C`~ RT:&nI}`75jP-aLv |>I7ڃW]E X3jɐ#oREt̖X=Qz ||̚h2ih5=u3h??mkp J|"B@!!!!!!!!!!!O@a)ʬ%Qq=0;f6Wsd:hٲ97(Azonhc!+5yhѼ W(z WIcк%Z6olBߕC&ТEs͚qNcyWXEM[iJp^)ۊ׺ oN+6wo5k ?&ͮ3Ύꥼ~ZDp .N@"z4٢a ɟz٢SLI*q(q]*#ObV9ZjKJ@J_'oƳ{{]Jkl~ /NfX1ɫp6P^Bc{_ e%8? /{JuмW bymE {j Xf3j}WJVCfl|[Go(ר9m?R`T(;T.`mҴ*(>X^[0n4b}^39ux YY,PYL|}ԑo[OR1l)zU FF>*Jk5,zE^(f=IŹ[8k\ oQƄx*?SumfHP_2Ew/Á*v l84`~gJnb+cCnb7lgia2aGׯ(η}gJz̎YY}ڒ=y)W@bٸT1P#a,/áA|C^eLǑG>rr=O챂e!i=q(+7bB#,۽0؁{.Ҵusv=Vγ4O*:ênUk/wT@p5(޴Z=C%\T˞E~ē5$$(drvs//oG ٵCV|I16z VYHה5hO3q%sWH(kщY9Y$p`x._2?_>4M~l`(V&1v=axi_w}g[6-rt?ms pim-\kRE;6yU[I#;fnmavoa}:8Ǯ8㝔Wyg7YͰHp]m?x| w1 {Xe۩"`F+j٤~|ha^3=mp@㎭Z'RvʷVQIp /7[Ʀ8A$8Y☀y>~?kMQRLU^8;=Kޡʙ$p`()?oņ Y fid|߱/ស^O`1R~yM'p=1ma?{ptOT?%x VT(]j7*MiFy \|G!=)jrl̋UlrǺo]oNX&N_NXwAFhys[}3g<+B*x߰D<5{`;~ ύ鯇GgjGcXsϰqb Z-%f%8P-/r(.1F fXF^JrÖ^9L`,,LG19Uy/Zdigu=Álzo0f u>//0q8ЋYu3*εH#k5hǶsZ=FW%μȔ!/AY( $$$$$$$$$$$< |xW$J-ՕtǩE)g*iY7GN$6Eqe,jї+8tp ٭ϓ _U퇵|IR_į ^n:s a!;)PaCL!أ)l]l}$oJ^g<8?=7ܛܛn7j{F]D{i{b{."("HM1>Ȟݝٝog7܃',=J,Waʊ[ZcܧX'iiy LQ k1nǸUIL|IzL3}?|ZtOA .<;->zS}Lܬ*%;mf] foLOC?wHL .Kyzw݉i]oqsXs>'ZG<ȏN{'bR+nT-(kKCl8xOuWfݣi~Е )N_^`~4^?߃u7*#lDL~v[H$Z) H$D"H$߃E4j=SS%ta҉Ԏwtףww=.t8X_"ѓ⸶vD% 3v-(y7w:㟎#Jճ >@x4yy{+kky};U,۾s<@J q@9rMȎ g O;|I1ym>{ w'\nwcɶW|ޙԊJx ,S韛Zh꭪#\ؽS%4.;͢Y|[b+c:211P3ja"I$;g( H$D"H$/8pQP2wL;u<[݉Ɋ8yH੤Z+b}?3!շŁ|yu^{u^8tO:O`۹緸ˌ(lq\ٲcbȥ5k5gqMFGkā[8?ES:~{gR?]9pm+S,{8Cn]w~X}Lgh&<8q@UF8Z>*45dqfTv֯Scݶ;q>_y_BbVKtV+DrWQ$D"H$8\wtRfMs Q+5;VZa [0ttU*%4'wQI3W^5U2rR72'ymP:O{p V̷%i~H$w-HQ@"H$D"H$ RO϶8.[(Ξ;ϙGcE'c.jcU%y&s\: |8[NfD;Ĭj@ٹ l |7Dp9.9| g$3=%l.* kECaD?Y3Zo~e` y%1clx"ʽ+eZ}VC{,z5G8 fLWޠA'*ă ͭV,meĶ#9U[U~*boʹgqr-?=7q%63DF_%P[d(q)<{mGu0J*-|5]-h 0y;޾xJ=a)GY/wƟc: [P=צ4 ߾M5a>v2Vܹ0Nl +w߿8˂)\}&9%/yUzw[vz.^Y@&URǞ6nhփwǕR' 'oT!T{fz9yPطԃ/?'~{机('Ӥ _t{D{{RH$D"H$6RO[uw0_EI; +~{V&ewkO?Qǟw:0mqn}eUk_㉗HY>gkn n.p38Cۉ{|Z??`ƏF}_cO38fs䟿ϖ6un=םy!5FF琰ђ>x9 ye5ܬC1ߧ8@ &а2f6ݼFL)a2WF%1O[=؎n5(sxwtNW #UD6CǼz5. %V3yJgvsk*Elnsjo Dm΍msӥ?Uc!ǟw;1{W4ro282X@e逥 -TA|ud_)Ł^Z|k> Vr>^Y0pP3M!z>N]Kn>az i5BԗhM )5d(J:CFrS#s,k:FL4˃ &0tR^ j3lөӢ1+rn@(g}zX/\#slGa"J;p5!uنߤ@^{8-θ =*yekO(=4U ׏YGk{ySY$Xx*ȋ݆?"ȮêJHY(L2t8&XX7jE*%=jF}0y5=w<<[7>\]?Hf/Txu1DbߡLZ{fAIu,UݡF2w1;Oi//az$xfouN/a{4+ڷAz\Á^᣶7彵dٛhU9;&=ǯ|3 c$HnRjD"H$D"Hn{$![T=G?^WrE=QOE[}{ 'S˖; H4 f}Pxftx6 K$) H$D"H$n}R%t&![*c@%$NhvפÔjzvv(ߌ ۘԪ">|=\"N8 ]zTt::^OֹD"8 ED"H$D" RO8pT2Hx/1o/is ɏ?E/xN[t}qoȋވ}yRw_Hfq44xE\Ѫ^$D"H$Dr?Hq>yŁBoyÆ4 ܈^ق1{M/Q#q}&43f!~8WW1IcQ_oC7mwVݜήwx_~1r/+r_ 띆ݧP/7p#iRH~+HQ@"H$D"H$)' kU9y-u8"䊥LsR.};#oi!7VA4pה]  D>SrN_/sm[:w,vԺŖ^ājttnFD$̽RH$D"H$Ƀ@|K+\ u+T@T*J(,>bQnRwTUca w)ϳgVB"V H$( H$D"H$$D"H@$HQ@"H$D"H$)H$D"'HQ@"H$D"H$)H$D"(w淑WJ4;ks/OpH F8fL1p\CM>ߊ0eLx/bsj-/)Ԛxo!P~8ܨXr(Grȫ4c*C$;nR6n{0N8! PLSk/5g61y!hv1#WėoDyN!`F0íg3+YGo {.Am\%c?L e@7FqC3{H)H$D"TRxNd +85;kujG1ha8IaBhHtj]zkU>ߊk`hg~%GZIkk1f#\c7cp~FpbH.i]Ѡ)be1uc†KǙV4˦Tɟ GE _9yod܀S6]!w΢NtiE .sC'Yz=ص{pY(82) G[8?Ε ҳs"%v. czGx{%#mh>2q!878W*H)H$D"H$EH$jq}s=nhI!ti9nߪ!\; zˢHE&Hh7vc%V)8W#.6VXI(ɹ7sp*u)eql uowƑOHqwNq@N[ZNK$~wTo)MѼukZWnh1W`H$D"H~8|sfȍ.9u6NH/&4LRrgI ٨Iqnt7P.(όcϒ9~;3tMA賗wP_~w[?ʡX^y[*ʹ}[l6\y~~JI vYȊku:>ꦀ ciSI tZLnst dpbOodDgaX*"s@ozNtd\wsa1⇼gC<Ia{"|ᶇz;E)30[_?<~ìi9b.G5Rb<Aԁvs$^^+px F4fIK'8`E^:gIȕ{[%8z1;uktI'iXvYy%). yQ^{c 7/l.hӦHnԹD9'i'zsY;Uil'&ǗCW#]&y籎mqu)Ia<>iʩ'56[CeVLh+X=c;0Ͻ%d8ŝhkG l8B̈\ٌ߫4x7~4emT}8PLzYu/rj7V&X v&aWbhIt3ժ$X+w_zdyϺ3)Nf/ˎ)ShH˔d2-ЛIcv2za0˹JzyݹJsgZNܬø͋b%&{1}%O&i~W('#.Y'ID}&ީ_1@LBq^{]NYlwC [qgVìgI2Nā\_?)vmq$W=r¶MIF^L m4(6ƾx aBzMv'~o$]dCg"g-# 8"喀v͋ux:Cx.l\ehh(%t5bf͑K,ha"1X;Sᥔ[]Hqw :h- ٩)^Cѷ\;GW/z0K@fIaFHhE5{@ɑ[k`7nj?elMӁ{(*Rg'?XtfsYy: tw=m ˺TY'6SVKikҵ莴W9Mb丩|C%)1,`Ka?#1NL1V4\KLYrgc0ޢ L+iS\DzX:y0s>VV)lBi'7çJA̽์+c<0H%vt_w5=(!=65 pO\l]8Ȝ&~Jr9ŏO<효*-!?/ܢҪw1\]]FS?Xb3c!.l̔%M)oāάدgķV|h:Ο)c/JDXJk #Zi?6Qh[&n dYoNqޘ]Eo3ޏynWҤj`@ux<콧3lݙW&c_ " ڎǚ ST\LaA>y?kFц#V.@9{t&9E Y`ggΖQXt]Ki21d*{3缝؁& (P:O :/:rrO<ƒ{> rv(7v 䱕cIEk+J; vz{",ӳ`dT䩨MJ;e{*rog!'NME(^JƴZ 9ߔ^<#_bȄ1lK]gG,`j-RgZ.vC I\ϯzn%_ǽMUyH q~(ɣAUX#ܨ=ܔ0+%߱kȻy&5 bYt6yu(?7]KӾfk"NSUy%1025a0b))+S=ڄ)ۯr3_/y z MogmG:\RR.ٜY%oCˡnL\IF\S`!YamUY q[QU1dQKbEJԗ|RbakAd0%|;e. fjI$Cw]\wʼny;XIk]wfL!]co rd!l-Qk4О^3W5F]ij̰7._xB-,(!)Za75I ;aE h‚SX%쳃M}/ĵ ĵ >8$>ٚt˫'ٽd&]ywrE9ŲӗvgQ&TBGG}s~\x\œ*ȋVh)5bRͥW*WKe dVU ޿C( ͜8*|WHkTW0s'dQw%߸J&(痓_[b4ZwW3ٔ&DEHy&ؙ8n*ϱT Yy^VLXzʼDlAAftǹQc"QVx瘟9( +F9]Ihޑc?j*?mvI9x#gUڢXVU_/'Tq{mdEeh%J|4֋ ҵG$WUX{Yd.U\fXJ;=E!oivͣCsISs# 7c[Z"JNE}lIXآz^B3qiM=LlQ\W2_Oڨdu+X͉^|;܊+/,LErgeq`,&t8|ߨ,MVQI )89Sq ~yAyKE4N|(/%V"H$6-$͇oBV5sydЭP]vf'Wrٿn#0=Z12/6Gc#B*DG̜Ae{Z]‰(J((*H~:mfL_죘S7*)i3oT+JDG3֨ǺH']F,DDT 89. ܮXQ96/ ^ĵn 3(lcX[9%GX{|r(Ł*rQ|:!3͏ì]ɫx^={ Qٹ[NsjRFzL6mb}Ŋ)D"]9 d J,rva)5޿">dS8;Jž+i6p'=FZ3zP(%ft>@;Ĉo쯃Q8P64H@YiCvb+NFIz"\j(Z4W}<N.vtKDZVFΦ;-ɹĪ@7׷QC&[ZK>w 7PTWXKkzE(/Hd5.hjQm\ K2X?]m!_%o-a&N|k:Ł䃌9PڊuQQ6W^\H;+= BQ䁜S[= qES@Yv.qbW+ݯ8`Ϣ,.Hq>_6$y4}Y xFt3GXڝܣ8P|9L=^~NI"=7hئ'E^~zLs'^s3 96ݿHC+ϼg4GWd^Ǵjf&"7̼?xřѬ3KViPu_hG=+n_|wnbC:;<N.-Vb'a1;R{զI$DPtc@l+04 -/Lq*ĝ<~򦃧2eof"rF8`sEY,KFPeeU-ʼJ萶iAQ&rL|rCc}FN#TUNNZ!7NW=s̚3`9G)P(+c_BEk_8uQ%V CYa ?-8CQ^8n$A]3"h\9Pzc~Gq@9IlЛRӾ 5wNn 1gNMd*zߦ*zxH%_c75ԣ5'(J:AO=0r4eAjZs:}i>~ k!+ƑϝvÝ@e}V ;8axrkݟJ|JIYs!gfdLqo>M̍ JpmIyWUIg7ˏK Y_1s`{=3Ρ eYBtp`;I?Ź#]XSed68ڙ2j' ;3{[lm+Zq83zeid E^4_ka;; yGqJY&m=Y&y۹0\%Qcv1yz8'2ɷY[ߚEXHOq f́)R|if<[WR杨듗ajgWn,eyms:Oz,>qj  {h^O-1Jk:<TqR|so:fIKtVG\HM:fU?i5~ŁUß 200Bu@#toc:ǐp=t]"+ (ޚy~ƨ`NA&{Zt]XmO@>yBXެY0*.JSZrXJ$D+:F44ak i_By5^M_?gn K(Q,}bzQIM M ? i˞XJJDcL4#g6#{ޤ| KD^V\O`;cqಬ$;v'=|z8N洳]ˮzn'@[z%[c5y)) n4e5ч_GQXYr3E ߞ*hsLbB)Gn_eߊcӷ#L5߬3D] 5e+Ly~8@MB;ỡ̄~~dȇk,\M߱VI[ػ4a!d鬊rRlMU/IXl<[{mmXNnL;y̢ ]\\]3i3̚NTͶVrpGyabq0G_{8FjU43<5c#Asٿ8v°f7 b\EU~>|U Z rc%u*R7,( 9#A/55ȋ\AO=k:zrnq{}=f|5.wE[H9$]+"wDKauw_v5-E88C>*[Tw5|h5Pح#WBCy6F#న_)@aQkܝh?iEuS"l7 W5m l Sڻ^|K.T4&Ԣaw^ )픤CA2-0ݗNfLNɌ5!8:yl8JN/pюigAe1iBcflWX񼊋qvFMi5AYd[E8Q;,KVW8ЈIBkO; P Y5R;]}=y;u9Eǫ'cL?:4|587Pl7}@ygf^jO|T:ӻΓ/>@qNDc9tzoNkUpKhF]EWőx"{ ~ ~6q ' ;>i՟/CSt}.3~{@[s|ђٵ mOw:TUsm9_=x晴Qz]G"H$&|nf8k7ía3i{7`oFcFyo&Nu(ǡK¹R7,:_4ݩy6G%-FybX)ci;Ƌ1vKJ)V,8iFQΌ^7};$ 5bBɇ?Fuj;̕;o{{h.+tYoN XXO/F*36S4mFEevŊ{(iˈ8\q|mE=bns3yRF|oYW%7yD}z[G7Npb FGKm7D{GEow".p.2?t "2wQ jqUtq"V+}e14Vlk5;Uܥ8p~[(k|Ӹjf_xGݓ5k%1'E:o=C Q́;l _:HS4[r)3N=u5'~Gq`c{(?ϴMŻ51Uk`%3o!6EOMCY=%)u%ia/q#D"<ʔdų{ƙz8W:LpLw%;TUrKddW:u4օPmx:}=h&o)y DĠ;j%^tO Du*3] g3d蠹a0(Wv9; O"[ZY'qeNo]uV(!9w]m?G/S9QM9f8ybgWʱ5eJq.t6})D/a. 8et WF[2ҢCq|o+D\h3wƓ^NOŶz)mLvRq 7̢r6]Q95ӇqZ+r0mh9;]ʋ 8:ߙ,WZm@Y9#Y>#δ<50J7O#m* MطIn|o`⨪u4JϯdOqB:fٚk]5黉:F`37sz=d-NnڃNc"FK/~0 d?؎,ٔ٩8=gkF1.:gy/9r ,lN#없hiRQuSgz-pцřũ7zsh91E,2NnŇ'~+_=܈J6owODf_* 3ky޼9 ߥ]b6kKF'pғOUpNgŏqrNN;]9HM)Ѭ7a :.dyhyؼd]]7;"3gvbb^?3?1JsO7&{m37 z}z[~"/axwE{)Ә"kyU@ {/zi;>k['EA.důs6C""V{|v&To‚$.QE=qt'=+4Q2-0#31W&_ȊSרfNsE[Nڏqcr֟I5Ψ;3SagE9u5t!bɊ=?6FUt>L^zF=8:jJapw'o7W뎮*&h6(Y$Oz׵)M bÔugi/TnD*_&@|wrk>sU[1AUi%/Q#u^-Roo⼧^KQcu9Mp+iKŁgߡm>Eh"uƭ>_1!=E^^7+)Hq ;47M7:0Be:E@W{%D".J/U1A'7;;kO?\j}K5$QIZ^wI -Sy&( W`7&H6ݚ EDZ@9EHg_E4jowQyxY8ςqyFF=.tVFX95*"kZ;Rsh%᥄шuNw +:gT%r).t^^+yqŁ,|@@@z)"H]3OYo@wK!/~E)Hsq?Lvå'M0YQܔD"H$?+36&fGn^._<Ju eEd 915Au54quli.g搝OnN6)spĥHpT"C8{v.gSaw23na!1xbwhBH$Huq@~CEIQ>tx>hD{#Ł@i4 Z⾗ujN8ŏctz,j땤n-ZTcB|؟CTd6 ḿN^\c@ʁiOkŁwVH7 )UGYV!H }?8V;]>]OP妓嫩ݦy7@Q?-WǬ33:<ҍw٥Ϸ6`åc$D"k9GZDWzo:# h?dn'd'<̔6z&45 J Myj ˂=VϚ|1Xz%wݣ&zXGF 3tfCKw|̍g/H0]8pKg@)Ch6ĔfFY~o#[aIȪI [:E5rC0t7iuwy7R@ gpd.?MCxy~4]NUJݢ4ʺ EHд!9qK4T֭x{Rj^*CrdL7Qcg32*rSQ/NaѶ٣J w͗[H@>QůfY2zE,‚utNJ8V쳮x쌦!`Jrbc<m̗R5#;ˍg"⧦8˂ :_2fe*ԫ(M?:O]>WQ98{އFgG}Z_̥3̲5|a9J ݡl3wR}_1vwB}cP>χ$ʳO6HCYѝT}D H懚p1O<4PE<;'W"H$D"H$D"}AO3a2@hp"=? x[q#xm5FP<8s_h)x4\1ӆOSׅԻJA&]~6 sG Oi,I[)I|i3 8gS϶|3F".3O+<]9&jqgwLvu??ij|cMI::/~BK<:xH$D"H$D"H$8VclV_LF(aw< vM]E4{ r{vcvz1W.h}'^s}~;J|pe{: %0:O^XXtwpKs70q%V2;eVBw杸9CAn-3Oߺ/hY{X>潊B5T؀/c{X7Nges=6E$^Q?_l_)4f"vgSF:>'5imںiFsPCqXE#EPk0/t{9gŋn@o{q>;5㜲$hftֱ8D"H$D"H$D"yHq>QJ\upk׬aժU_c^_lDU))gwb*V^]j:^gCsP9Vj: DkRFNNmzU4Wrv{FUCٺb%+V,g(PFg1׋γo7FS"H$D"H$D"Hq>xPGɝHdZG)U^b5{ VO4&P;ȒD"H$D"H$D"}e%PPp[1e])?q@EYIE%8͊ 3u$D"H$D"H$?)'W?ty-ȧ5'%js;"4$Vq`f|_/jUƵp0u '$H$߇BCX/ DreGX{)ž~HB~V/jOGb!'i*(<|%1RO0pbr5 9ك~ /s#MC0ZE:'R^A_fHdXGL)H(&H$D"y$Pnܘ//fD"[ o=ǛH[n g.l,` %Um|U\1mɚ=D{QLBnQiʋI?476`fD"FD"H$䡠RޚV'H8X8d,d9(SI`8k4Bl(fˎK=D{f|=d)g2 Ù 4;%6RH$D"H$)H$ 9qr4{<$ȑ;#H$,yiH$;#D"H$$st3 7'dU ҕ;OJ/D<߂^HOݳEq_{Gg\Ƽ]t}Z %;Xu,P%%ŞdɪM,]\.Q_A)NaIpEp+s|Vx'qlouVfm;ETJGmJK<Ϧ\ %"ma\HXQ'9mh2%9px<.u}YelGyimc({T],9,̾=:t'p= H`x:|ݠ2Yɰ64܍l@f UiW`sl` e9\ ;ȼmٌ}=D*/f!_ͦ< [){qG-D )/&1\^ choo8Rdg68T 2g=W'wK9fn %.8Φ5pm{vhAI:eV{(Lj8wˏ&NlAf-m;Xz2c$Ti - 'r [eKBQ؂˂Ϯשٺ(~5krJ ܹq+v6 .IHq@"H$D"y蔓˲3e981̉cqfDs=okk*YAn b-Gn.xYq5MhBW]#04}ҬdVr\0#{tMa |bi0KW~aFˡv Yٴ\qrm3o &Yk+ikK5l|ˡP8 :G '1؉XjV%r+HIOʙXa}W#6CH"y(Y8pkgzjI*]B˪EWo4#Wޣ"7 t4ɑftp7LV4ohKG;z w]`#L8;]Y|7JUe3&Yq=Uv +fg833̅G~B .O_+zFLte\fc3Mi;ČOIݔs_w"`-:U#;:[m[ެf7r.Eh?=D=:I'z%5Kk]h1c*Nh\劅߶Si7؊:Ɋ6c>BkQ9yY8^Z5+8qܹ 1lOc7~gBkFjM96;/~vIׁ<еeѢ<m>Z)>SuMs0x bіݦ.jM:N_{hdHE%{FD"H$C$'ms᪓>w ?t)^JjVƏ1Nf]9tuӉA΢g_p|>FtFqb/_&<4E [iM6'~qZҐǞ ݋=Mtu_D(N'pl̰T(<΁p^{c.wgT$"%XbgO3}vGv qR!0bMqKz)(HSǥA|5Ȃ_-X^dߦU1vB+j*˦v/N}+mv k{:NæܪA }<šoD"4 L60d m׳>4NLaJ%ؕ#1Z}=tR\:qK#oVYK+z,eH"p6PkH24kWgc+l\a'DJ:2CH 0kk#?K\%""C'ryR4Hv6ESa{6Ɉ9lK D"H$DP)&^&[3`a8ڃtaj4zg̢0[L6fst uje bgOטQjuJKBi4#a8mף}q9WA:fѬGT E|x=Dr{ gqzy=2`iͽ"t7;0LZ%s#tx;b+jC%7#2j#cUX/pItz\LΝ?(IQ<hsUu-F89紃{r& p`Hr]97ְYA-2aTDJd$T)Sx [vԈQz=O sɊaS#I|︑}!r [DN҈y\7oIZuTN!8M#jkRzq;X0hY +8QϘ!kk|oц3X+)H8)x$Sy3j7mtPi 77^YC"ϗ'j= G-pjF0frΫȹv'}s:nL/v /k X:M,;b;'vk[bΠ@GE䯈.+Vӓ+I\J$D"yfrfg09?8{#&S-he}zYYNd^ j/IJ8~5$ΰg6]i>c~EXeHklޥ!4r:!@*>1c1M8`EE~n̍uiAq(*-q F8V|= 8|B9Ǝ-8ʌ9'.^~ [Z `Zf?K7j+<Gg@CC\ɭU8$SZL5"> 3yq$J0LGVZ;v>:SW(Rg%E47JVܑ[@ fS4GhQA؆ D0)|0Xj]R.lYH\@"Qf#<;T-~ ,17,U6Vӹ&v_MTMNpZLN+j\Fo2+*ںGY.M[E\;7),"b{ 97Ю L1!-HR %yzp{kPN?"W=sIUq2Űqz taxEyh$L?aI z w B.RQ?tty1OMdk8 H$䡡&zBtN-ٰf>vùT+W1dkB0WtGy,M_A]i1ؓdWoGfF5g(*NVݣ8|&]84j9OfP@q>h(Nʀ8/+'Q7Leb HL8 _Z `QM\hR8 qhA8,?o=+T8WIuq@ Q~V&t 8ٚ˰\tK9J^TS_82r"WaW\8@[D yFq)kHq@"oJ+p&.jVt81quherb I‰U^#hmcA7c:g o>{a^8֮ÆagXIӱKu"‚<˯hsKR-@1773#jEٱȇOG,H,ɣp 1=ā4 >@58 H$QNf|(c&JT܎9BI4KamfBGǭH)$7?;ڍǮBqE,;rk3BdE3g\ԝ)1p.Wkryi;7JNZ‪s=wu:Y]K.LX{AˈQ :j,BvtEj9H%G:Ł2"zknjs2v΢U?:އ8l w\E9sNԸ,5vM\Fuq@Eqy'h TPF-NF"kKq@"-( >gkP$"h5y. *smXCںn%䦶=`:j ,_8Ssj+/N8S丝ke*rB=9X(em13z ) + Һh#9GB3тoWidC RNT1„X_ݎFb7sL$:R=Ł _F=D"H$Ttzw- 5͢mlٹY36̔v/XVL+fAZ (!~iE;\U*#etZ {xYȯl>#軥btރfSߊ#mj.36b%tYKL]!WU-q@t YM>V=lڼ?/JdV 8חXu }f2Ȕǹ+nG:;z%=f4l`-x;X[}=a(Rp(4 ص%0lT?uHsLaLYGUPD I YίMi3%9ذcA3i`E1 RH~ y\i?XwV^%M:*l8AnƷSp_M?Cr5ǮTMH/LXl!&وzϜ|QEYDXL&a+C[t5廩+i)‏톙h ^͂Y<®}3+(<+ udܼlXdU zr=Cͤvivf ( h+ɣUxb 9° OqƘ:n"4nL}*GD&N ƘLͦᵌ*0qFGһ'<ëahc ')ۄ 34eGE3ARȾL6///6Cۙ>MɿqS0Yݒ".fV"6]Ku+"<{{k N9Y|%}-j7^<4/գ”\bFڳ->O4U\8|W7ƎxjvRRq$x<'ZJؚ֕1n8]z fUqiz#Vu` &4HSգq5c٢Q+E!6~ptqC=MY"H$G e{ >tԝJ|3Bt`&T|'+$d\2K"R^Fƌn ݺf4bA ]|" όNrFZT.ۊߋNS9e'8t(0 V/dXKZc;Lu)2#Gq$-u?eğ܅OvĚNks~4x*rSpeL'T[΍oU}^E"+"Ł4|A',nO~D="_r{U0Q wܛ8PvyL;='y#:Jgn5cSiw퉗A1KyZa~Y9ilԣ{xyO<ǫ6 l]X5^}J-h|Ҭb5ߢJ9n҄}54~\q _w:GDM\gdBq>NJi6a~6{_oR=wriQH.R~՝qD}wGJ!BD"eΤU}jڑJ=S&J]lg(V4?K|3-U6օRPsvն &kn MpF.†(i^ݡ>.[-pM=BMUvI![W-t2!-/"ySDY֎:4x>{IzTx6;ҳjg38tq f yBq-΋za<8a䛉D S8JHhCw2Ӱ3ω$TX!!qqLUVC]z8'OrRi{q:Pct>/$#C1K(,fR ٽ'^wޠq?[:LԜ4lȜEHq}\HsjSRӒY L\SCOZS BtILH$DR)26]~%D"P%xprS)n UP$Ƀw63H zwo|n;#ِRbG.$jFhUۃV~=uLŁYz|8ʊk\SE&GH3G͙:BHզ=YQk|Y)̨6<% +Q=L.'M>3էz3Jv_Z?8:ϽʊŁ73jN:}ʶ47 ooh7ZNQuyA4Z1Łq%M5+\a&~{RTSā>>/ۂ O?hY̌{DԂ:7h7ZjyQO=Zxٯ3_*2t~e퍚/]>?~CjD".ƕH$DH}Gbyx@k9)[̾R{:S1lzvvrMJ>n`BBf]= Rzi簓Z87@=aD`)K#z(sgo)ǼJ_fqǰ(W F,q ϣK|W-q;sbo~Gu7ՏIEQ}o;3J1Ͽߐڱ42n2k}U3fʌ=Zn^~s5Ug>_+c=[?47UKnLPfBK %@ 굧>D"Hl4}|Y~QD"i(#~t>7QXWV9$a!O+:t&NO%^ޥ6uw@IB~mv^Lv"M퉪'ƴjnYFs#UYܥ6RvpZ"z B-wCJ-\w9d$ U͹׸ϡK|X8 q-7;:mlw~:*y}ϾY6e; Y[Јjkhws[@Oq?e:2l":  u1pQi_A q/ϽӪMX(ax[ ;ZS2I pV>ܩ,D"H%Ԏ.H$DEyY E%rkQNiq1ťT.H8RO,dE/agivā=.tVv}W35{k9, Gt[?\PȎ߇ExWxVqujb֝FUv1nOeĝZט4yeD)l47GX5LYkUNL}:vyI ŵ{΁QGL(^G_|-.(؟Є*Of{uXg lH$D"H$D"H*Hq>8Ǽߩg|4z ƾ;q i q,?[R¦sA͍7kTr K|< z}zl"U~nhKzs\b{Fk?ێ+޽}QxJ|"ѐԧG`K\-6s :&^3,ncavXp qܜXqD"H$D"H$D"]}rGq C>GG1>JfwmN(>9_ctَzF'0orGZkT9zޜād}`zyS47D#qJYWn1$Tϓo}q$kff/q/}뫯&i`x2ߡi%Y#H$D"H$D"HROān$dv&__Xtk\/yh)yK]-ۋes}XzֲgՋ.uS[8>UE:0F^K.ijsE]?P{뽊k:aʳQc D"H$D"H$BZxM&c8x-AE\L@ZZ-'#g+%7?.i8NV?Q5L[\h8!֓PV)-fW7Dp> vvS99{_?ـ^S5KKJΖ,ɽU^:u[ PJK E wwx{&d2Yo}$%̝73W9f(a.sCA쎒c;P!4ZvI3zB3 Ij:2H>؍\_1 P|Ա vcBq8DzGqÁM7gl5l8p7>`{w@m>u=sY9UCkEs?|į>;l.q^-"""""""""dÁ/ø?W+^|'w.Ax;J t#i탸pW⪫/G}r\yʿηpDs 6a߫7Ἇ۞K|!|54c]Y|?RW\~9.zܿ9yam3ƸO|?qEW+yU5Yg}7AӻvvU{__dq܏ y~Kq%Ətag5[ߧ?v .W].-  >_1Ч+sk.,~v2DUTcAuyǝ~W}q^k|s29w޹( >>q/>ɆK/%\yUW?9!bqG]x"""""""""r(8A;yd;/'^MC^gϻ"˩o>ݸݽ ڛӯ۞a/½"UhHOe2vL.9a<\p筆4'ށ?_y|%_eEAm-Ǻ ?ޑ {z"VǕ'AOSֿv~uyϞ;_]jDO=Ɲs.~>:s>?W߈ґדXEӞ>5_2QcVs;֍t6`?cܶ>yxSq?ޱ=%5߄!]H8_26c !w0 iKEDDDDDDDDR8p7$|4o!Qp8pOqǯ7s1.oZ5 """"""""r(8AÁk*w8Oqc9_z0/Y>q!rpv80|"($!x FpPj{]FF P0DXWp>2n_Aɀpm 8tJNf j:˰8*;ďq8?E^p܊-șp5cԚ04O3Q0g/_؄,¡Nl|OW@Wg /ł(P .@ဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c4T@;6ww U;oN]>twmAN.ԋҘ9a/r[K~נ@oywm@jt`_H6ZzWUh=6nO)C6Yi{' AHCsSs柶9 NPw \cܸq>#\W=;և cß:;r,1x[uVyB_o0֟ps/cp4reH~YbHbW?8[עMP2\߹_s!λ4}C9H8ûu"o9瞦91 NPO\øq?M/O[3g`ƌ0}_YŘ螇O"GU8wsfMGmoY"أVGȋ?!qܯoŎosIl??Is1{t̘9 W}p{"͸wlAB{&Vyb0f|Q|KQzTi_y6q8s0.8o8/'t'Ops*N :\ %x{E Mq>£k  ^xt|Xт.579H8_?Gεa~t7_o^8chD6>/}qؕa 0bs)ULyn@;s?gZUwq{0x"2jL80XS?O~|s!yj.1~G?~{\osoDrS󊣔aW%\^T4m|/9Nk8 p*oGW#p~:p;_ܳ™8q٭<,ߘps`q:_/wJwᆯ~ y䠽}KGh;nop`|99ϧ8鐾n/~+G't~Mm/Dž9?O}s))8_9ugzkE/G8+(u͍hG>o9~K#6؁72vj½?=_\P78EĖO~^v!'~69O_%CC/|y buA[m3ɍ\^nN _=[qC!Gv=XU17xqGW zsV#`9vϼ""""""""""g)ֳ8:k#1,O܎-~s{{ 6mHf{Ο{|?l]i;(Wu>{xxW!vU|ï0{#na;޹p91qG~*̙,4˸qA8onjH,""""""""")z1p.Ÿ7qO.]~.q=#"~~U}q ˎQ;3>oG=p7ј w~c{Qvpl|ϯ!\Y,G8P2q?O]q ᚷ]Q탻/"})`Z@p{5W=2DđplHP{kHGESq6Іqxk 4~}fn×p79p/>:|v\tT8,ͨ}W8OqC^8ij~1s ܲ<\Ҏi#{s}֕N_Iܾ2CwC*CMޫև![{+>aGz?a9p^=\v:OSx 8SDDDDDDDDDtS8p/@S2];|;QvT  >3>w-f!1% Gm /c|d58 p s '?q看Jp2(9>;G^0?6e<{G')H\_iWqCv$n~6C Y8/tT 0ȅ x~YHx'oG&s~))"""""""""r(8A`nq߸[R=p"@vx-Y,BZ8ābMfݯcmhŢo~!|Yq "lyAL1=KNj8P_K%a^*bJ*~0G"fSv!>g_X_=Ԁx3q9q_] JfyǦ!}!DDDDDDDDDDF''z.!wFc ޼Q7t*^w9}և|۷s>1-qCExOZnQ2:؟~0L7dw%`!6>sOeQ}I_ tئ}/~oG9C!o <۱>?{J׽~:|,8OL?I;2F|T\=t靃cF 9wr/;,󜿝缡șp&m}]p-.8S̅/!l}?j#g/}_ַo~|o|鯱 qt7<}GDže gqރQُ7&u}O0g^{[Ӱ𱏞6'yX_sriÁ~Ԅz9Ο 주5گ~?el/p^A!id<ǟG_:5+;W-Jv _/ /~h<քb?z9}!V+󜉻&?<9>^|9EDDDDDDDDD$"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDd;ti;}ok|^ץ7Mk 1jjjmNʖ ;ijZ[[k6-++ ]]]w~TUUٿ7߈cڴOY91 DDDDDư6a EHHOvO"""zjkk|hv_̛7%%%wF߿{AXX1ڴn;{nttt8ira---Giiǽ4u%%%M]Sdd$UFN*___9Wpjkk ~eHDw~HlOOѳX$!!d)a`p<U놾u(hjjr((Kb ENp`x'&&"//!::vULL ***m/sy]gg=p ~t }hmCKO Zhv7l=W[mo͝oߚc=rMလ \85vV?js͛' p`s`v7l؀ufoˆn+]]DG{n]}8zÖ-[l-a~8c6`Iooo4K 9 HMF\E²v"(u-7! {sv\˳mO6N؈k6bbC:l`/ߗS#u3X ۑ"_;#bq.L> /ڄn4X4A@Bsk lv:VxV,޾vߏ?ξ~4Ktkޚ E9@[@#@ȿ?~ پ.364q> Oဈa o>;Ǝ,%{ֲgWWW bY!H)wYޢ pEWpE{  ':'O+ ֯_o XHdž8PpMq@ a#P[]mEx^vWbKLܓ4# m9 \|ؙo`lcas zԿܱP8>(@* u}?77<7 > U {>P+ /R|/çp@DDDDp|:?=XaX|9?>?+7,qp|>u [ X UD,%@7bSt Ɣ  kxXN8k+=xl\%`86fcP00ipu)p!C+:ͿU(d1C#`M/+I/u@sUxp`a 8𛙙f9;0i1jt7{#ZY!ͭCwۃ3Ik_};G@EϭK΢3 kÐC6cXӏX%86{ {ZR801 k5td@@m]Eg7@[1N=çp@DDDDd ;+N6084,I^S]}H"Վ \ 8f@C lؤqo9`9~{XZq4]4 ׷=h~-(p] FFc 1 诎`ǧ=_qQɯ9 }@S*xCp`a FVsCd/Hơ^XQN#A,)kgB\Y@eұ,@^^֭[ݻw6,͵aArr2k޴igoAEVVٱNBf8/u(mNJ$sp}p@][f,68Âzs9~Gs^W.s Aԕ2gN[:h<6֣14mQm31/m@A]*k݆#sŶmې.TTT={`Νî]pAtvv{_i2.5@bi桡F"e Wf2շ`5K;Pl~ys"0c?&rs}43ۻ̹9dKԶ!2^ipO*_F%Z9y-}ow gh EppEH W 8{tA=آzTX}su)8Z*G`ˁ=_v| Pz|S8 """"b8[~,g~_VVvx@h>>W1Gii 6twDz(-4':/5b[L)띗l \|9_؁f8JphDic'@qs zۺ?v!64镅XRcx8H΀ yL+ǞR Î"֡{{6lfkv(<vK'ֵ! ˱)ׄF2 hBKW Q8,ԘdL}q.K Yd>p`[3~ g_gs`uC?\h8D(8ʎ*L =e 0;ۖ`  8H́ܚV$6!oA]0VtLscּ 64h4,3h]rp5YV( AH.2 +, Łr;++j[y DqE쌖=\|JQRߎ,چ``[t1F۠ ن&\]\2_GR8p [;v| ]@ycעp`g_~`" Fbb"222B;Ǡ=DP8pl˶qpwB9bϣ=1tX^ތz,\քF`J̊;Ӯ*`!9v|6y\"|3*? ^X-QXg|,Ax^9~cﲍYZf|-|\0PAXVR+oz%P\X=¾l\K G!T =H-G8p$jNR.|Y &`!2su)>""""".+Lb3 ꁡU BCCOx<`V^E/GoFL=9gOk׮"D׻. [ߌ*WbglES:r&y_mg7vTm(`ayc;s0e p<бz U͎gc掞>vCnk"\ T* |,lpYQEήFQm 3L`/ ,#lWp {544`ĉXb>C2LU@?xHb.gS"sl OဈAjj3zg^Ē@GX8E| q Abooo;xAEo7ph3f/klڔJf=(pwaR'g#33]sVE+k?Pʮ8T%]˨B\QmXz5,cW?4ܣ>3ogs6<&핁v&bw-1?by,8}Rǖ=ʌ % ʪ6? l 3[a˭ns@a}9RρYyyypss֭[o;.Á3 oJa <[4ĺl٠E)q1}Y }=}mo/VĚ5V[Z{9px;[9[pƌv "W.p@uw#$᱋vE.r, \W Qlrr v; Ga};KUc9kevŕbwcМKs,z3,l{ bFpl|$ s@4:{g#aJ /C o#cD2eSD`97fe k9`ޅn[Zh6 ~UvDLܟXf;[ QZD!i-X% FndN*hkk؋8 --mKqm!n_|(_B;V)>"""""c?75"1﹥ԥk EGG pf!e˰m6[V@+ޭ 'd?k#X&`tQ8:o/ ^c5$ qg_-Ab2β,U,<;(l+Iev@QjW5w2;%(ovF3&G /g/ ,LTt<-/L=#0`=v%+Crל+#H,iDjy3 ; {LT?6b0=/H[2V9.'#ҼOX~)`nߣ\1Ƞ~8eCԐ84E/9V =Tz]U椏 S8 """"2C=?ȳM c{!;Gav?sPA eHA,qop0'z< נ+C mYFk{g8Argx̪֜!D4Ǯ ɷf{e⍽ɘ5av`<4~(lrC 8p̰î$io~s=D_<˱.\,+ OmË;{u$i9P7Φg0sZWz4t`䭘•\5ЊXy$W`kt]` xtc,1S[e)`ȼDa_b-=ŒSх X \܀Mm(PXEph>8 1/k qk3z HUWu{ ;Y݆^(pi.bgU"n %㝉v$0`m;`:&O{ҮD[v3YvȆ%v94^f,;zum=F"4&iMpO,_d<5ֆw+"pp5n44[V=!`):o9ޑ;Cyy]XR {ևbW6>fTz<ݼg%lP3Ć*<%ݴrE);@W}jR ރS8 """"2ksg*!ǺXx9CoWZG{?\J絣6mFvU^gg?mg:dR;]Inɘi.L%23ṵ[bgֳ?4Pη6{ tr6Z^V`/ OlΜCfc8p˲pgyn]f~n)6(`ߡE#_v2)3= Ѹm+lHpϚ(<}mǔsVnKJm.bL[ \0hcߒ?S PPP80| DDDDDưpXOއV]iK1fz$nDE f䠽{ -2* YYh}8]؁f;? /LoLL;u_ٕv$ͱxz[7`w w/Oz%k@\;*jvfR}{zm#8ܷ.Oـ 6"}e8.NMKCqϚHܻ6snx/ ̵!Yj018q:{Ub_/P઀g% 1MKBק&qvkgMB|}ӫ-(.)u>4y?\TzF >çp@DDDDd ;: .K+^u0q诼1E }WB \[Td2oKwݖbyOn=#J9^\lL2ױkm6ؙ/LhXZ'(ƖM<,۸e{8A=g 1y*cp(ܱ*X+_KBI>kM4wM]e0ޜ#(նQT4;ZbbHw ڐX`sYPm>|hܻ: ^bFxz0ca0-W/cyp>ܦpE)8J7Px@:Gb5>. Oဈ РGxyW"]Ea{ fcځ4g͈ef"<-%,3-iv lsT~(g͝}/nk{hܼ,Ԗa)΃cp0\4ߖyyWM8Lٟfc>xqWmڛR҈<# Y7w8VL / exUbv n[Kfq3`I) 3cg&zk "' ptܰ8e7?=/NDsZဋTx;>] GL)q1Xb XXf ҰtR!++޶111 F6_-Fre7ֆ96HBWO_)XUɶq0<=/N-"pP<9lsp ;Hds$; LLܗb 5rۨ3cKm*,Xܸ8%3p03&uQxxC n1gT=.S؇3_7ÜׂڶSN`-ȮnEpv vŕ`ES}?=-_Mo^Wv'ےCL?Uqr{ƼGƛs̜(RY!4fjG| Țc~9o áp`8x 80[]] oooL2WFUU"##lٲ{˙݀a"c"ntU8ڸr #+ -LpVmf^6ע`sQ(/L}qܸ$̆,b[X۸+ 8~tۼ z3*Zl3MvGq_OSib!16 )"m~xqGy GF@gxDlܻ. Ù?8wrJ|׆:sp\;/~Ԗx7(ݷ6`^{}䞆yZ9d80Tlx%ВRNçp@DDDDŰ|~xvǎطoc7IJBqqqZ90TvT%r^ \[Td$2QPۊ:H.NJ\;{Y,U!xuO5nC CuQDHiD kɆGrK{=*;.3`Kgq`Wus̹ʁ'bI`?pH{91ǹmE9ALs]PV5*~L B QXߌ*cb_66%3p@ܺ,ךcg`:`wO;WG9-1/OXbpҧ;> 21`y07dR80| DDDDD\ cvGCd[F_ @1g?1gy1#ͮxfk^ޙDexaGϐateYgb6M(jDQ].;fI Ǔ[d3=8mn.aKq+?+"l0h' l ue܎x[B?+#ó;9V+\Ԙ t9#h3WN) k0iWmt:)pmQQJ_z6Ea[t1ؙ,'t@\1+, ĺs}213/t ?3lX|p\ w£cl[ ^jߛyIDJY-s&t 啁w$9Ӽ"\13gȳ|γ[1;/J|Q3YAxjk9 ] ~bI`.H9Cg +Brj쪁v5_cÅ|!vS[l0# +B6lKK{Tp=pPp%p(sEy?lP0p(>"""""c@Vu"^Zsfzf;/6{b)2_E9vf招cJ֟es&D[Jxm f^0nZE޵qqB`lL/ĺӍKy΂@+#q`jWWs0+˂r9 <<9XcK n-qp.g{3p_F:N)>! 5"6Ih^w?OoⲙkuߞhC3<31lL۟_MٟfW0@aY?2}V(kI 7:z?S8 """"2)]W>N祣%{C64# <(֖uYs??Xi<,̰ykD>E6@ϊHc!#lc0 ήAVU3Nܦ^5+q0\=;?NMKBjVl.YlKO%pKk{lفh.ؙ\RڈށV^UV quKKlxܼl:} ?qhW}0xn{}-'O2>Y"s_}$(QrhnBဋrp0[]|(#4^N%çp@DDDDd S802 3/3T)S8bb1sG Vbq@ؿ]$w}pÒ0<3"(זY 2m?Wv% d;r:|0`ϗbֲ|9؟\:[t@UK1' Olfπ`ܼ$Μ突+g6fWEp7yuX8 u<5Ά%o&ypl<6 msk؅}̿޾A4s]Raa{L-Ux/zk&ˎJavŕ`mh}q/5Xc^~9xnFwtt1 ΟeC\24nwDt r)>"""""?xwvvۭ?/#CGo6Ư`%1±j-q5~x?eٶB?|2D o鷃R@ :QڅܚVgVא Y~5d5Qi6/˂rl%+)XjhCd!ė[P\p`4A Dff P}}=BBB\'݁n_Ҧ=u+tQ80| DDDDDF!ȋPXXh?/#C]G-fOD]̨T8XVh{6G۾ D9$ogEZcKʐ<;CM9%s<ʰc lRD/N%3p4\94c {t5&,> ,υ~A4x4q\cZVF$5"0 1w%<kyiC'6Ǜ/{⏓mcb0q'7ݶ1Ņ |uRJ5-H,i[| F]aW5p|qyXNQ6aؼ{JSj)A_g# 3[9mU80JCrr2"##|Ml2#//}go;y(XhF,S8 """"2 q~YY^9x yP8,Shܿ. 7, ƥ3wmm˕\ <ѰxmX2*_}eaYb{fxe^b/Z!kuy,[چ*:mS y;{mIl,yvf{bZ9잊v@= ki}; Z(nW0@]l)lҳ͹jOn.3oA9w[:?p˗lwm U M(k@9xz[nY9ܴ4Ԇںu]A043=_~ T4 zWp`b,)`>p0ee}t "g^gօM'O3pup0W'544=77٭gM O['}& ͷ*XҀb[t~YwSdև;4gEoLuzuw Xf' vN;z݇vxVY į{qsY=G7cȬFVe9f5vq ˨3gە,)# *$Ϯ` p\/+A^mo3tWc0ꁖ^i_b{%<9 Mݼ4ZJ6fk^o>,Yقd}lQ=%aiP% berrrj{%""btR^w4c/9c Qau}9OggqЈY9` }XmxR Ɯ S(ikbn[K]!۶mÁj%n{voM]9p`\;/]+#lv$Sl0ȭnANU+Kmz"A€[Zfq=߻6 2,sH;BrjX9m!ۨdc~xsh\5'q[9J m#jn7EP؉v2dUaw|]Q |=5vƥ37+<,Ğ l{ U]v`olvlmݨiBZy=ϋrcA0.2=_wzls-U">b \ڄ*9^ތ6OMgZܒ0e6(vI:(bv| Cyi Oဈ(WʲVK]֮(6oތ4[Bh޽68?A`yyr:7t`0{K]0%W(4%Y({I.k㎕x|s,ߑIiigs8Τg(Pe۹z3,c|l/ZumhqH\4e_0)ATA`~CvDn:h쵳ccG ؈7 q竣{7nԵש 6Ęh^G7Ђb(bK*=#<|A-(1ϕ2s{ qEϪM{PRp I vCHp`Bpݿ?oߎ={.)D,+ ;T+rss儈%B+983wct m(v^ RiH|L4FT_[^ڕh{L;nK q;pP׶vUuWp|yP]=r3 sl~_L3J6'f=}0w`оz߳<[}hC_g)exiw]qLc^Yi3΄gqѩh%{Xί_F$ۯPg?`>g_/~AjA5O_zohFk_ẕ3t~2j\dܺ< ߙi~ -B(LrO=ظخ17_~t灁ALay/ӮL`PRނ[Qt;2_6?9(>"""""`8cǎq!^ي(݀oC@נp1o^c^ ~37.g LؗYޙ^[F( U-]vн˯heg{lЪ|s`O]p(;3!&ypxtcmj=o豍tpxށ-@Mk-P܀:l(s8s؈ mF<~G#wa};zT37u:Y5fFJi#}+'HÖ+g7=S}pۊp+6ofY,-tq/ޗ83Љ G3e7}Ԗ8un8⍋RG ӱ'6!q_{pZl0_Tdۃ6z檂Uiv&5:V DZ10")>"""""gs5==QQQvUѫ>(ggCln |_ܐ0pF"xYxbs۞gmvx_&_iksp9k<yu6`bBLL _W9O[ZLݟfCsjl>;R 8h7pΚECG- ܿ6`S7=W_\1wn 88_f/9V20tu̯7΁HO+k+sx;šdoUo\H9ZX m2kܧqŘ螊{VG>DOn in_F]k͕\-ך`K U&͹m((U~LLb'bT=@@F QyS8pf%cst)&lL[u(pm1zV;h} LpO#b0q_ !0N3`Nl.T4wt5ڕg&XkgM-_)>y׹͓["8S+@~ES *8(tX_^6r %uj~]"xLHGX^-2*`<߻z%vuz % vv~Qco.3lr([+]jh}SG鞖>{ǃ1F|2_w`X^uS𛉞T_ܴ4/;Ok΁Cc{Wjsnlp`qqqv'"08&v~QBF"çp@DDDDdr~wb~M㗃N'o}w":yP8bcǗ6EAkēbQ@mV6, wsPg6, r2`9xp} "._Me3E,À tys9s2mcag"_N_F7Dl)Fr&;ve8;! !{!/{10mJUe:?!l1f~.mW/|G&1|v{L¾FK.Ů]l5*z `my+dR80| DDDDDF ̜9P8pzԵco2Zqm>/cYjpu(pm |s+egߗ\5Pf9# MlBkW(`4gs=pp;gWY|$ܷ.6aq$O\='7`E`.|+WjWp`!?Oo#DpEam aO|mxhCmD7]c_v9f8{p}(++w,_ȕٮsȜ tМBF2çp@DDDDd+?O>>~pIl,(A}#*keK/'gwf:sߘzD8?O*7GbymoX?Mo楡#s?v׭1|y3= ܇^(+W80Ҥ>@ p:2)>"""""{ S3e=Ӫ*!u跣5{ۢg "qCp1><5ڙaovŗA}hglAcqs\ƙJ;m`/n_Ffdŝ%x~ui(؛I4aq&~Kg~'5YR 9˝Y 2l,"lģcqJ~.[᾵Q)H+kBJi}<seuں`SwO<E4(o+ <7&z?$,ϱ͒ܥgwȞgn|^W10H9 "jJ Ú|,˶͆0_+&XR%^ٝl^l{<`2=}9|܇6<0al^>>(W80pe"K 1pPqppW@M   Q΁}> S`ōQMQ%HhBq:,J&{{Wp1x|LrOML ېYT4uٙ|+p177:ٰf}QuUdKdW 4Κ]y1eXBOiWMqvexjexv=j< W*;XY,Þ,?~_ [lJ|12dנH `Pp\xg^>3ysUL*숯B@N; V D,De,8 rp1xa'^ogOtOl*sjZl s{м 80$X~h? *lg'p__rd/\4VOؗbg1gC^[h>q?s3Ϳ[RgWtK}(W{I0n[Ww'۾l[ds&<9 c`g `.X͆y<=p0\2x ?{ E34 n vFESkX0< )x" ]*, ʱ2IoycT<Ѹ-n7hF]I{Rg7hACbX`ύ^_> Fm(o5"ÁL @J`.˙p`B\)]]]hkk;zh-hik;jPQrԙleo+Cm[)j[KPmʖbT6ȬELQ&|2R%:1;sB:,{@nF^H<<ǚ* Bws\V+8=ePfE-('v>9K1899p?Tne~l!,uۢ0q]=,󇩾o\0 s1'1~a}tgiC쏐Qlgδ 9kN\7- 3Gᇛ@bwBrjl0\cdky Hz_y烁 /g^wo_Q={FpF\8YD"6ѐXFçp@DDDDdEFF ooo7ˑ{qDDv܉ !مuu :rv`CTl|_Ʀ3[bgbm 7ǫo0k"fa3"|VGʈX:sT_sIx|`n9cK|l~ BcKp_в`d6fw׹wIkʁĄ)ϺxVؙ5lfn;8M,#o5o6;h3>}Iխ.g/ ʵ8( o&>4o<16]c{<l"9gv[^'#< 9ՙȫB~]5d!ǹ墤1eMy(oCEKZblmACO3z\w6ϭ>wcW&ܓlo6oӮhlw[z*48h~_91 ^΀@}{mL/4~0ggy(~7_l3<ӱ>a0ڻY mveKD`j`Oml/L½kl0p@\.\-7̱8BGIckf;szm9$>&go*ijs|Nï&z7=qTo<5_5a i*,G3h|-:l& `4w 7fT@M ҳ ͹\1ߞsy^ *s~lD#1404?Tn۩!ȳh"lܸӧOx'p"aee]8id@6`%:Whp`Bpq~2`r //444A:?3m%8e0'~6s E$kŠ0;!9 Ȯ@Q}78Pɒp1xvA+[lIr/k{m7 8嬋Akβ 2k7,Ar˻pHܷ&ΦDo;P~ _;[mls%gn;۝3=SmuaQBl{pܰ8[kslG6 8^y>B9fךC~(4`#aǜ#+9f/ʆ ){[elZ<~_]~`#)TcEsRnu j"UxI7Om[8ܹ& 4BoYh1gURF<^lBoǮ 1; Ǫ& E=_{o)1wWwCs"2 `B'ݱrJ[p/d3} }8=BF+çp@DDDDduJ=Pxp٣/kiiM]1`߸Xlގz ɫ˻SXڌN;8)'ƞ-rBL ymj@t9Aq_yͅvKp=3 l܉hxq`6f{g ѸwMm ޸-_[Z覥4 ͷ嬇Wӊ.rkQlWpAMk7 ^NUI Lٟf^l<-"OvX\6;ዿƒcŽDH)Gjy[`}>z!y m`6$ds6{þ^ F<BCC~+V@ff-u;'-p"onlقI gLg% @s4)>""""" C3+,Fk( $m,`st k\95ؘh;b Qьz;2=]v=9h΁~PΞgHpȆ=08r zrleA9xaGYEَkfqll,JjΤk붥|+myٶ l,+6}l\5p@\6c CU6xϬBd~-;rP0<ƣG8k̾gnc~G`\puCϲ4]v`{39þî`M9镁GZ_Lp[>Y{zWaglQbeU= q2?ۖ+[l߇6D!@en)FTaKm(\1-c2_A3YȖT2'®0_p@+3gikS})>gG8pٸ=X9yu_W  ?D@f3P}0= nrIOmaWi\o1K =)>R$6tǀy;;B#!9fjH~yc g[{sz;0 6`80_9ad^wH,:l)b<-w]#p ?v=oOeK>g "Sʱ:$31i۞905߷& / Up0ܻ.ҮT`sy~Y؟\fWL4uf3x/~Ѡ0XihEam;0 µqlG@p<>H&0ɮ,9uͨB@fvŖ8+xe ['6`+0Ys^ 6@aj` >h1Pw x`(@yU80pxbZp4$6o4`7?Z+P80| DDDDDF&TVz0)5om˯,;WN-:* a 6r[mY09@_9 gsXv&3[;l }Vg9Uynwe޶_؁I)㝉%pK(OZ%b p0JlbOlXn!#bi)gGQxhCgj$8VLw C 3s=~x6 8d rkbc>;E!nA0m%!kUiұ( ]:l)1]j;<Π/lȐ_ӆf;V 0$`)AΦ7y{[|bhs+Yb*, 6=Kp`[1RAw n^nW ŝ veK89L Ę+ػЪNJZ3 *tR80_} qFv -`''?6t5 Oဈ(UTTu!;;[+ޡ6_hu0 1ō+x)p(!IA^u J;6ٌݜρmyؔرʇ|eApthv-ֆۙۊ0;}+fu6? +Cmݕꑆpp 1veyu1om9x<%r?eM(o>;ݱjg3L9g X H jl)Sb +f\97ȖaQ-4=ņl+&R+q Jl9UrI=nZv&70L{𒹌O4e%)]s1'/DcG=_np`{EkksÁA;&Ywr QڵkCdY0"6\|, ̳9R80z!??QQQyqq1cW)J@iC-ycg;~oPQflH}dW"2+07p߲4_=7Yn/ ,g᥁X䟃~x#,eY` `9m УYͽxew-_&/=vߖS239`>xms?p~{9 #>YZ(Z'6?#ly!Z`HBS1~_)}XNf{e`Asppr5QxF-$|gK3Q3K0M,ClQ jm RbxhWP w322޷qÁ"׀׀j Q9}8 _c9NTWWޞ̻m]184k}<)ux&(6 Okaa!֬Y2{=!<>U-@KW*-P+8|<6&@9'5f,$[o楡nA .ꇋōm?7$;ρr oM+q˲0ܼ$?!xbK1ئX#6}<€K,GJiϒI̷H7g;V |2J+"""""`]}}e0tRZʆ]]]ܹsx}ddK=*B7cSdgʉQ8:4VVV)**ai(o@97*g=p@`=<m=}ntA=%ao / u)q?{ LHXg =1g#/ }cmUlfAtu7H¦" 5ݶPf .e+8c8Ȫjf5Pnw3`iKgxhC4^ڙcw- ؇+ \6+ws;qrƈ"ۛaTۘx9Zֵ2B p)p]-8dއ_.ۜW+R80| DDDDDF!K9C3B^jgr~8g8d~ڹs'***}]AsO3<ZZ5p(pmwod2ںm0`Á^G}|s3\89C,g]PМZl(ģcq{q 2߆axp}_k޺,̆,s՜@Ϧ[ L;7Rʮ$;32P`vQhhйnqJ>_icYg'UxyWcr RKoū ;7`_~pzsbK 1Q`Blͷܓܷucop3,;/Wp`B vmWp8<lŊ68PY? qqqE]i@i[ ^_AWgʉQ8ڸr "1 f,ajwU'c8{+8Cќ|Δ&aO| &uqP\2#7, X>xtS,Yi9pl|fbځ43o)2UԷYo-]Gq9>#,JЏ/jL Mzbs98u^0(Ԇ]iMxfk &s%3l\q&NX䟍MRLCV6ԴtzpN봄}@Ko.S8 """"2 q5lݺ.kkۺe\=PZȷr^ii8d=!-?op [Yڰ|'ٖ- ʮ5S˚cgsf?p_>@> E:Ù E*QŁ9vuoEp!(ܰ$7/Cclsb^c|h} ; ܵ6 W #|l偹X8c7;J_܀ sy󵨾5hͽ)v"{/ xxc4{D㟋pRIm+g.ōK<5Sɭ1<(Ϯ`03*ȪGjtbH0duÁ. }=cS8 """"2 q@JJm0h{ #y" ?X!g>'ٶYoQ~ٌxh}}{Eˬl;Ξ_'ڒ9`0[^塸fNh."lz6,Uh9K6=jW W resp8 1TACڶ.;obda4L1>Qxx፱f~0XIwm4[9bUOo > FVڰ-bʌ:{͇ lJ|V uJÁCӀ]_^2pWS80| DDDDDF!m(**Bww3& ap1FWOf}s3O6>8d{X=Aj ΩB;K"̖b=6Aǂ`\0㖥axtS]2B]%>̷c lV;@hBpT8{ܲdU;r0`WfoI+kCpJs>s|lgـxwH 03`Ktl&" % 4YePϽ9cauJÁT2(>"""""cqxvϳ(i.r^"g6TV b>\=FCvlc>U"*+0@G٦m o\-5sp@[Z9~O&b}Dօcg\c?^iW W-FN5^saeWbK,0o[7ņ\0e*p\Lޟr\P+  ʰ֨braHJ9fTFĵm=h3_֍nڟL \) zJCMçp@DDDDd sp`'}5KLQ8ڸr ++,+Ùħba汹zcW8ۺهffTAv$ͱm&YAj k#X;k~Tۀwi`9[#!5@Q]}s&Nq;q@5YNNDt~%a6bGfy9V+S'P٠ XN(`97+s5ː]jWcd`6.k{sW1`dR80zqE; ypK@m@OB+ 1̕^Lڼ^FswR9S($go=,s_ms>s Z;mM(k{z{f`G{Fٯ揻D.qp|O<# 9UȬ{bb Y5|^b3dܟ.s5u06k}+T܈zl/"""""c+Sj1,6r(pmQHDgOmc3߹zv8,^~9`S޹>YIT;npH'ܶxĮR[N'$Ɔ >qumv>p쪁S\Rh3~?W2Qp\QZ]%9vcQEXgW'D؛amsCDfcozy 2*[Z֌l󵴡 cxC!ɦp`EEPWWgZ 6$?>P]]mÃp^(cS8 """"2piВk]%`CEز5N^N5r(pm,+pltAy֮~[bY5v]vl ;`UOnf{lS9ؙkC Tȼ:_&6Z͝hv<;,dW 翥7v)͈+l@ZyR˛' .Yٶ<Mʱ/5烗ʰXBe"<WJs+ARN};dɢp``784c޽v@jj*֭[g ^^^Xf-/tRf{ xLBk ߏLqimm-"""PYYz!~y[[G}8Rފصa溡gy) \fdQZt2`/e+m(03ibG6⍽lΛjvp<"Ȫlu 䙍uj[l0fȍGVKUCx3ն#f3_˛lAvcPrI7<8\xgۢlOfݖ/bCũp`t+WDtt[#==@OO `xR pCO%;oXp`˗/NJ+PTTd?O:֭[v&ʲ3X`4[YsJ?x]Q8ڢ"#iӸj`uCMkϬƮ[s f9ͽ)ow%At(kCq}fOstQo ؐfI%`=gWte' u[bWDz\ۃ`tr˟F,Qт,y5\%/ _|\9! `=}')p]'!qM0 @߀XFçp@DDDDŰkrɓ-[غ뎯/CN?݊$[D,3g;B n~>gԳQoI}'Q%iKL9j3`cw%`_bJ_ۊ.p 92y5h4r[R wW pڻދ҆v[( WH.ǦB 7[Vcuh`JBkl_ӆs FB[9 ԵpTP8NZ8 Ph2p`8xUCqqp @Zjc`ݾ;PܘF$m(s^r 5&> w (o괍3Wg,ttu0f:ؗ;Paу*w6"mUdmhc`b_s&ٞ J%lxѐ[jWir6^6_y~Aus=SUFJ:i4sȨp`8:ZАx_" 6k|GEF@Vv6z\8@\=jΦ`vX+ 95H.iDD~ք:LHl [Zh6Wz6vܯ U-]{ݷv3x \92CEu+WIev5AlQ=kQao0+Q" < 5e>U6~{l0\pjVp`a=mֵu ~ ]ukLR8ؐ8=sL⻋=8hhYN*i` 4-EBQ#"rkm !g״t m17>r5C`SIv8= m)p`a9Ȩl[b9x~yuB$_+rrrpTWW;oDDDpv8A|bi!s3+ب[#\r<=v{.'3 {<;?Ӹsg/:li%oW?4l:(5Ԇs=oSdW<2Kb`>Vw_ =} xũpu +,s89yAဈ6Á!ϯw#Yo/ãW쾮ŝqwb@` @Fݽ]n02G՝UU묵ٰ5HŐ?''jPRR'NL}]߿YYYM8 =/wK79YmȬlEnm;b_V{~pU h0<ԍgСS^fGBAevFF0C*۔!] VȩѶ>eIDAT$*[eXᯎ)[+q{4Rig/ 9]O7ph {Q*j_`؊y}7 Z/..ƹspС-3gΨHW8j^LwM]Rރܚv!>YN"PTK帄ʖ70/;2]%Q+$ |;(ϥn= t[XQ]o MSߐ^$|~w8]labzÁc8@DDDD4@hF nfס[k@>ߵCYf1xyܿ}8GbbsխQ65 nyЗt/$]}(kv#X^Ql;"\O* B*!&R/ϥ2V@~Ww_ޣ'_[?C+ӿ6~w8 i[ &i18DDDDD/12E =H(iN#8ꃚ}ӟalw޽.rTlhV2ȰTK7CM]H-kFQ}: 8,_K^v)?<]2e~=o%,<,,k i7r2X<z] wW,A I`G ^^+">hM$z1 """"zݽ.\PBXXrss;~}vvv"55 b q.J2D~ʼn~8s-}-ic8r{^"{hXm= R-JxCg:Hqa}jG[H:ƗpAxHץ5[e1B_۵|Yni=3R y(oQߨ5j^e>j0ǭX]V- dnZPYPT(K0 sYIC2]m}[Q~s8SɺD18DDDDD/f;w^^^EyQQΞ=7nrQ% *++u|-j‰r{ blp"üypB*}^HH . hV+˥_{7[\~y j,g6QR)!.Ԗ: dci)! %#0xyp YƮmH4Ác8@DDDDH+Q 8ݐxWƔ "^mAj*rh 0N?ÁHTlKJp$R-⠲܃#.{}"եo%'-ߥgPAZD)Ae,G@׈ %6u- $H@'?3xypG".hH(p%Hy" H[{hMQP5NCNKn"=ܞp@*ɥ\Ay^pWyM -jTˠ@hW<0k R\K ]-uK*F ]I/.66hkkåK{|C^pp2-x &8v JGz -p6,X58[ ^Heeejw8uz^777C)#z{{q-?~\}ѳPwS7h| &+p2btE]G⏴`\/~mWI|`8r{b|D}TchؑJq{@Weٕ~Am y;@8|7{AZH =NtoS0xqHk)QQQ+WBGGr@^n$ Vr!oCCnQq@&`I7h| &"W3kU1e8y*00<_s1x=ှ_*ťt?VKKΞA=}B d5#P+[NK&ں k[M0xqHCww7BCC9uAO5@ o\߁ ?L"""")y! :uSt[v@E}Ґ?~Lz/>Pr׼lRQ;8NabxD[qtŧ~o#$A ] -d{@Ci]T@5߁@wn"18DDDDDSH}rEHlS+F r.9.Z=/ܞp@MPw@WI.Ӥŀt1$$ %ӷJiE!ှ{!=|Xc9 ^^ i( О ph {;,{# p`rS{ Q~xw"=^p@{?@cH;oR@˴u; DBNwM{0xyj8p&rrHd &rjG ZsPh}m]/ :,݋1xHသTmj?m $P]ÁÁ@H Ác8@DDDDihh@XXHJJB\\^ӧOŋHHH@ppz@ NU4aw-6_\!i=j#CÇf +mWRy'w{11x=hFn5hM$z: &KF*c"UXFss?/ˇˍM/'v+D;18DDDDDS>M!z6d\ // Yb8҇!ѳ"cf1DDDDDSPNNza9I~~z|<ƪd, >^d!!! _RDEEDm8?:ǟ>\C_eϧ߇קqQK._ʕ+>[@xGPP:˗}}"qD2o0z'pN<9)>u9t[~DݖF/ ䷗}y,={vמC3gLwϑ`&l2|H-m2H5qA2zHew$c}QӟC)OvIևIq>w;! ܑs^C 9;]53]vI"""""Q]]]n={Bݳg+##CmizyOʼsssiV!eeegGRݳ!e|Aݔ!3Hdtے~ݔ!e|D]rܖճ&qٳ%E2Inx֚{H+A9Myvdp!y T8{ry]TOCW.@y˝iNdb2O}2o]%+CO(o(;ԡ/82o &P>G|G+,gH@Cϑ-zd{}P'mY?SϚۻW,YerYS+)dU y7g"wݺr\`o|w]GG;[e̫sYVy^WW7 Ieyfffyumܲ~ !%->dX&][wNS֏WJzINt}d'Byyy~"Hyy32?}"JٓxwEP{Nl|5-M"C9~9mV^J!eH}C*}kr哇!DCRwy>&3BO9K$#]f9r^}..-rr$a8@DDDDDȅt%(+å^[[ w!wJS'N}Ů4OJJR/&B.ne,{ 2PE{ WXK7\.~e}M\PKׯ_W[,u둉!{yŒwYRI22oȑ-] w~"O*äp77I|֓HϗmX㑊+?m$377W?Yl)qwQ&}]ʞH$xDl~2eXR'|Ǐ= 9Kc-ۓEjjz J`٧'/<<\Fٗc>>N~G#DnH-k|z^$2Jr|9:!胇rÀ{I/CϢ _jY:󰉄%BUB)x >8ѳpFjh uK..q+u"]E\4ŵ.IeR/d7Yv3dw}Y$E/%dnr㈜Ⱦ?mYwN d%',[^3Q2٧+ϑ,7_Yvr%e^Sі>_reTKB2@<ד&\!B/qy\Kҏ4ٟUDDDs<M6DDDDD4e20Tڻ}ǫw|2Gsrr‘#GJ` d<T+տA#-d ciy =${Q%&"""""2^*]ooo ^@$%%﫯WÀdB_:qyyy9\/-})))IDrLwssSرcjk"""HWr -yR/] xՁ(}K7Fcє" L DDDDD/:DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """"?CCay rj_Qvtjty +߹Ve)FnE=;7{sN38^ ւZ+FRnVգ&G܀²&t +4C^WZFՆZ)RnDSgXhmh@~m;:}4zܢ=ΞX *jQ1Dr5`Q٠G|VV?x]=brmEn@'*P֯a8@DDDDGGK}?y ט__sw3C𸖉feFMI.BN hf,5 #+h}dkѸt%iDVd,َVʍچWbcQGU +^!xA=OZ3]Ѩ+菢oCafQfxw-3ka(hFǀwN^գD_Dvh)N(.G{vZ-[O+2"OXbtЎNO?\s gosL)I7҄ ?E|aX9[Ny1 """"l!zs > =DZ ,0Hj3/pN^혱+=O]܌`O}K[>] scc)OFn*7˅`7pU #Xjbf[`\t!;P/ƇZm'ÁEܷ=qa8u{~\K8q8t7Ǵ5{n%:H)<=eD~")w CVxT3;͗pxoa6mŖuhw  y e60jMt qsR*ñrPNy1 """"d#=u: ,r<0W{⍟ma}/J'<9Mi\.AӊKG >ӞΒ{0_i5G݈&#[luhL> 5Dat=L4SfY_LHo׽Zo|NLꏠ\]+Q@NzZu`w\AAAYlar.aڝ0l28g1 X b8@DDDDD@C ۭ6A{Kp[17muCTr6n"\ w [1)ٸztjZ\˯Fø]`@̬췹(߾Eyy,_dF7(Yy7]0bI?kSP1zSӑr ;bpMJd*]DR6;@ Fp"ZWĴe[ERa5Zta1ZJʾv'uc%Rޣ/- ))P^aZ>1}no@g#:.gܩ**]e_r"6 AJ9آ:Zh*CtR G}㎔?,Fnm27C(mIeH-G[+彉h<)@UYʤle]tqtk3v+fׁ', 8&9Titw\ٿ}$)y9WP wRPw.(G϶d˹vZڈ1۬J97 㾲+P} rf=BĤVeH|-CDjvGP~*)RdUݍʾ%O85"~,8୍8~'薲SlլR$W{~^ :uReCs񌠭૕&Xr$!#yPypGУڲ;NNeHN+DjqJ9Yvi@g]&BJݩj[*cKOFAӭrTʹ[Z#L)äYbdki7ڎ-.3k`١qyW˙p)W. Qz?:K|,߉Xcɾ!w]pm8V6Ɛ,$] ҭ 3lfGqYm7/Bp1au7-1}0>SuߑHd "5r [eVX'#8W=ȀU8۔}G?^e&t!xEW_`iÁ! &N1,7;aw! wFF܄%[b2<4*Nqc:o\ f,ގܮ !G}ch >3ډͱlKU }h:5Mr+|uWҢ+4h/Kێmxu~Kd 4f ) ^W7ƒ|=䍹eJ cYlA@qB)M/"F)- J9r #X\Dr7,\ l[sj݃p@Zg|NLe]9bH)}vsz |4LV_M!_:ehB//L39cWc.7l-ށ7V`ljx :fldwu{ˬ0+ 7rFWh>7~>l'>P,5[|p> +PW^9V~0Gka}&NDYÝa) kĚۭ +?yFW  ^X)sfY0>?o7g7kbKP~)My2u?$S)XNxM#Ӄ,V׌ J\qfrm~/vŔ 'b|t[O$[ <Նr>!GePٞ7c@mMw`UDZ9wJYyb2 ڞz?HP3 """"gAӍX w 9elxl_GXn6`rKE읛ƇvbT/pont[vDn'% Xo.NX?Rqvlʽp7D{]T.*68ooO W IL\dW1w)6ن;ܺ}.Nb +`{{,1HJIK}5|?Nf5( er3V]DPt2bcoeL< :79;'4IIz6eW &ނ3_Pʞh܊R(^r$oumX F$"^4· Su=/m`#=l[, ;)|XYCL~&]7"7"W:vZuOYkkwwp3^)bqR66!]zBZN{*o]N_ҕJ9RN;ӗ:sb6|wxs%Fm/PEġ]B'VGآOHYVrCH>Z {+St+䭜[('wck_b*tnA]xu {_ı[I6tLVm3?FpD[q,, ~p8i}w)m0 >ұ_l?w{woW9yuooXp]p.+Y.r\v1kZ>V3 R+!7Bab .َ+cbpbC`T<נKJVYbוx*o,\k7WM}9ԏKXb>8o$6MYapg|q0A)GZ"A|FoF\r$H-/' sR &. ~^x9fѕh sbQx:'bWzvaGgf7{)}=۷>Xeꎍ>pn2bsXOW, 8&cP9I8}[ٟr3'0g1ޱ6m;P_cN昱+ܽay"z%ؗP\G.95d+RF>u+֘o Y(񪳍r{YsDDDDD ,&v>M mxo3VzƞTj&Íӗ+vLWBkssku R[ZKk%!ntSu,Ձ;cɪ}8.^…xo)({`}?`,sF/p[ d]-A_}c+Cb\/@v%ذoJDq>L7څ+:Ka9]GBS1]u7j \)n`gC rk=q4oԽ4 #< ̝ve٦rnl>#IQ ö+ u+R٘ر'\ijn=EqڲBјf*g0OWnAuJ8/h0}F*p-DH`k:lט-'ewaAڠ)a{?Hk`fQ֕87Dxn~FAwqϷc)۫ucE,Y'sׄ`? f!iHuWc *iMс~^+}XF{wTEnٔ2o-yuוQxMo(=VͿAwXp.J k p@Em':Ya#i"k/iE>n2nB{,l?\Au8m>-W4xm1g9>t&Du=p7])s?x){ &`xh]a8_Nu?ض o,±Cxo3_Uz">^JzJYyqU7A ^~QMiAu#Ed4o. 3L7=zUQ gSs|~6(U~́fHp;-LJsѝ ՖxwK( =~|?ekP|(~\ǡps |,d>taH4Sk!'.qº+Uj6X_N{|pGPwKaspՠ% f-5Q-5DI|8%=MÙ ap;m.{mxmVFfndXacrZm[?_VSrq%Stݸjx:܋ڻG7]=J(V+|_Z7\];9\ΨDQq 򊴏2؇/5 &w#7ka]t]VMeCnG}e)Rcla@a8;ČrAvaq 4߅Jk:-wo( k{P}^Ա4iZ*|7 φIlrGs7oZA_=.ǖH,Gn+*Eaa&Bw>pkE@/kQK~RW =h;Nw+{h)0}96ߔ$`]gb;,re I7(e{=l#Q\l/.EfJ 6k7qTXkÎ>wF4d:R˵ t98=L3•FuRNߍ\rzT-RN'Zi?P#d&Ԡz ~Z` KqH80ԣΘ{Q`P+ɍr+|`~OQcOBJt+5o[(ӍSxo/UqOjk}Cͅٶ_Z_F#cKl8 r($|NF Z;>>n9&װd~P 5"q'> B˜1R;c}p Af.x{{ u9 :#hKǿ;c:ñz5~vGvE٨ d>sk$f݉uBL_Їwle֝E(C3PJݨ9?0}L%^wlZSѣ;'RÁ6wyg1FZ?\_#vqƎ_ܛX&!7FИ3P et7*{?NpݿDDDDD'mDJ\ yƻ̰T ڲ6Vm7ŏGRP1^< 0Uv+4(I !{ +4h)*kOR/V3œ.]s~\c|BJC0mޓ(Mo׷w+$UKWo'}c+~`[rrgu1k.,p໵"0]6BӈKV tT=:WVy`O^p8Brf=eױ|9\_v`nUN 5.ig%2`:N{x^iWf% 8֟F\SϨJ Um.Ƈyʮ3p_jGjDcO^xWjۑr?w}.u}׃Þxށ'!$2k-ŦeQ&nV+]|WlQeʔG 5 f$^o 6ٺc&Y8H8ӆuJ9]+n r,ޗur3?内P,ٰ/Aj Y6\wsVrہWA죳YϹᯫ=aJGH87#a>enWw GѾx7.7K ~\8 5 .q,]_ՙص?FLb?L0Xz#h?-uď,Nٳ0m#f.tWFo[y>0;Gw63=L"ۆoB9ʹM>n-[2/1~uD{u.]|iIrd}Omp f+}.xe;RT:ؙlp wCy?$WY܎Qcљ/XbVii[ [iĪu#Պ(?Y Á>T$b9>ut,ш쵵ۋ  =p`$Glp5Xy Ńp yI*.݋TikV-۞0X ;o>| .G# <Qڊކ"\:oXc:t 'RFcZH8 z l ÁbpmlfKz!8l780bz H Y(1ܥGPpVm.r0*Ԡ {m,v= !8uBpz HˁFAmLkLJ?rţ)mE,^BK(8 xGօϹH,T>:{z(S'tۄ$\M/Cc7k7C5zrp 2^pyT8`6ဍ.xZe y| nGN`e3^[<4ɶp`\yfϮ.u u!3ԄÁw,5a:2~ 85)[|~csq(4#2R5^yГÁHx\8pQcÁBzHr ycIΗ>ՆLzpcp` 'L%d=v{%qB@ثeuNxw+[%""""Iу 3;2NxC7FHrҙss[p*Yp+x{qL.A~MUl0}R9ʯ~;bQbx))sޑ;s;E~ )"ovKdm5rRy'I! p<2M iFȪĈua2KEv+GKUk8ݭ:# #-=#5,7 uv|hqIOG̾MƘsYˈ|b8=d,0}/(;PsW!(xCN -d ./!TjG}Ybnf ؍Džch ێ0vciTxt+q7G%Qsg;`-Py(n{p bp@Z>E8 -q偡_=lܨdjBlu=d/׮Qj4\#wLH8g:Gx= O|""""I5 xlى︒q*\zP MHFn Ic/jp ϷǞ 8V:Aw3bO\ʿt?l^˯ TA̜?]]m誑,w9 k6}A{A<7n,;(Uןo?AƢ4ʽ+@ p@Xƪi[#OG 2;gwE2 FC)|L)l/F٘>p3CbBS*Uɡ0V}8^8fp>*m9s4n~X=Qvx{_l]k5#^|NgT+F* Nq [Rao!ul`ALera| |7F)A 4o 9R3l*hCWc̬sԭ\wמEB˘6V&xcq8tdQcXWӇw9uVڡq$nO<b_Au|5TYc[nы@3؉K^xr:q!u8:nMo zab3|7cc )ŠJ}w9+sWFӘ&an ўx{\#r[>{>8á8|&N\ljP8E[E4*FFPv +Wo{)2'8K5h-/F!R9 ?h)~9ߐ O[k|&AѸ= }P%zp@+*RdlhIIǭa≯> =VnŒnpQʹP3ú =ȏ  CBfnߺ+GL[ EAƟ@Sz SPƦyPNzp@BرX Kw`7mGrنҭ[K,r1iOJc9_En%P-1G}R/ܫTVC/\Xgߋu0 """"2ЉXۇYv߳lU[0m^AhN:Fջw~<|v+#|2k`jF?.ht9YѕNyز`'.NTރ*|  QFZq?.|J|k1|3M/^4%c;X﮴Ʒ\=\>w ?-[tg^x^`~ mj"Ng-O2e6fI=faֵ*4+N|`+=AijvMg="`fs DZ/1~lU]abt dT~l$p7½I.ii@pw _(۳Rnފ6x"(c2Q[pqsLZke6[5,[,oG0{ Zd0=X~+>G?M/vaWhb,puC"Ela*割+?["  pnX ]C 9/f:3nXэNx廽ZepK!?eYeIVh9rF t<2.؎!հVy%K]8-TʪVJYiJkXR~uqN')+o+|{6\z)ۤӕ>}myvu÷kRS|}\B2Q}pXcFf.&D-y,l+YDeu2v೥fʾgO}ef6y(j18)F#sG0nV {mh҇ "*Вxo Y6K2CMF=vey*02WJdy1ٵTzQ]eb>\iW)e GB]d'"ܬN.hš9;#噁A\X67.NxN _or`yPUР%65K/@{vZ]64ݏ1/MT&+gygk2S|kzQE~=q|}>,BcP[JَǏpol8P mqe? GN>az.e{aLSP7} b]~|6NjGce]w@F:{'^E)>]e|G ҇jcۄ"kLy֭܈e'RN: +qv#`&>;͘Pza)f1%p4^\D%f",.#659HUHt޻*dQ}a4zR򵷿>0ل̤H,M[jv7b,g\n5ZG߲Ԇj[q%6גrZR֞~t il#mY9|܊+A`Z WI({OӃM7Wզ^o-%\;4Tݸ<6bMy2n5܎v9eJ&Rt=MHOB2MQJy3~@?SNJ9mc֌.l֕>n; Ft*ν4ؐ ;!/- Ч]P;ʺEʋ)+OCnZB_ܸ_1ǝa6^1nTo=aAoS9Rˣ-caX [9IOeRjdp{ r~Jf7"M9Ƈ?:jpO9?ytʔs \Q+" ]={lQ{ZQ9ӽR4ʹ^jݘV8Wqz h.B|,1:Jr}+_W_ĔLSe- >_h1XFEyf&"t8$(Oz~z0Z-#3WRmP_C bk ~*vy4帩~Ύ*$%hר̲QDqV:brkRαӕƔjRn7(J-k|嘥G#V?b;tgQr]wz !1> Qc"""""s ZBѣtrSkE0yAk|MD1"""""""a8w4;s;2ǏpZ,ChH(,]p)Ob8@DDDDDDDB{U="zz#n@Jyliy,8W1]X ODPkUX2Va^v DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM24kI*׽2<_HnM}}(MStYq?luӞtUGXeWWq_@Rv&]F{DW]ҋ_|nwcX&u%a#;a.""ggݵ8OWZc}z8 Gmpo$<=MRn=ar)O[Y<⟳v9_Gb$Ub'Ȼ}_yBns 8ᯋ(vG#5%{}s+%> WY.m^$ݍǵ.ݙxp:""z;qZٮ'm\La{cD4""""I& mx}5>^n^ayLげ`,fH2BȈxr+~t>#} >4 wGDDliӜ-: W{1M2 ;mW;`(~ruT^olq? ]{R {ރz=e8e`0oX{9v1C{3c>u"0 """"dj89G@x .wx@S&^p}n^xp@\| o-~2C\ XZZExk5 ͢n(*}80;%Oⵥ6zAyo˘z墿.֘o.6G=aw55=;Lhp3]+>^n%VTYCVV X iF\gaz;,4S3ϥ]EF{x8egXr"]_߀K>fʼd~xu2c~ :` J{ЭP)QtwXcYV;b(v ^ve}4q9|omFF.;|i͏`RcV[L_jnKip*j?e8bn}Ҟ3n|9Xllrn}NDD.dۅ.P1-]aH(Am=ܠ[! \ڍ3Xo-25p]̛md tA~73 "{CMloA|]װzo"f(xBb⨧3XfWex^쾑G "\ &6؄Ol[zuoP "j fY՜4yzQðvr{p88˕$Vc|+| 'Cq(WMR/cg[l>Bq[\kc3f,b8p6Gh/0W[W[,E{+L8'[k|fg.a_|,uH[;W `"6,t=}/c|1FV|g :1{>pnNh)^ʁj 1>pיs}إ,{h/=O5p VkCO+.X A07wm0* *szӱ q((Nnp >[n׬ү"vnie ƭ2yCxp޵<[|my{^|o?9ߐCǰR9}i?<_ť uۋᎤC}?d7W!ANo RC}bK,܀/Cm@Wf֭7˹hT/ԛx2!Q=HCCWg.9`B$ pDžZ{=j"PN{4-Jf|'YahGzE8Znt h ;1kmkDq8Rk|| SnD+kmb][5 jJ^W+~%KX3s]oՅ֘f^]ci%vcKDDsW[M7 3\ǼFjXu ~|5&,o/Gǚna]R;cz_G|gp'ph= N!RjQ[rq`fyu-ԋ0 .ʝ89&3W{˃>{cln7Hr=Xsxw $` Vh~9:ð~:, LCkChjh@Qa MmRs I+L_QˡAos@]J86.T4S9V9&`.l?ijjGGߨjXf!UPtk.m`u-VJ~N \9o¯9CSc= q'*0-1}l8`lC! ZK`ÁBД 7K|غU>$fY5w7L9☻pY`6\.^~ <%cNHh0 """"dm&ܻt3^r-]^X ~ jokFw|3G=7ZT102XxEwD:m8OTNAoS.qgg| l"la7j9p 3VCh0Ԥ\ڥv0STw.Ŭnrf,6ͳ֓)2(#7"zL#2a,' ؆@5t㳿hZ}2+I wmo 6G-0/=_2'S|yͱ.O4#qp`=lB_|+|cyر?7qBI"M>DDDDDlT8?ƌ[شah.6S8Fjy4hu_/3{czwjGs{/a<B##nCYm ZF_0(zR8޼^xwcUہ t+ }ӵhJmx"Ɔ#hjlDEs؀N}ЉT ZSg5o'V^oȯ%;- ј ,'5d`yu㳸p@ӂ[[CzM+4IG҆WQC)Á<m^56jAlt8_D<SH+k3JvtcxMDDDDDp@\t qBqǘ/QwsWXcYhWv_C!:VAn]_so2@o 65TgfiFpC ^O9rls +[jUٳdt/©^7tƋ2@*.c|k,8QtD`;9mxMD""""I6n8iBܥct|1N8 \Q.g}dW75U 1;0Om9cst׋kPwob= 9ʛG̸=Vv-N-GMc QR\s>.x^Yp&WBfuZ[PSUK೥S^!o/fwǮɺiE}m5B`2{E7(֠EU%2)P+R;,<*e}47" gyXǥ'ygfVrT7QGYq qôeAHы)Asn 6+`\̮S55Hq/|MчͲXMqZ &S3y!EMhnQU> >&""""I6P#6k1"JWASm\ c/Vk+pK,P(q{Yjovxb/Vo5;+wa[ ksX/֮?^\p7ނ=o=iO Ūի0x cv!34k֛ᕥvXlm.X ?۝=^gjkx}ɟ?Q.Zo/nV֎܈| ;=XIr>VZ-XeZ;*.xw4ލ ~Xمo_]%Ќ evDzv}xlJEg.v`}(t96ǝ&U(ŽÁx> 2gၡ.FnؓH[n+,zWqļ.XfXop[ 8$Vn+w{hwo7I8P6ަThgAK=-3żqmNxʱ<\Kȝ=fM?8η#N֬;kMV9N؝Xp,Kvm^u6kN!K}0K°`>PNms'q^,jWlNAczO$ph !~BEo{` u# >6;Y7uǰzifm]#z+qX 6;beh*å#p1)I>?x.F݃hGѽ[ P>_YCT8Fd1Sh{y<2@G%B60MWރʌHXy?Lp cqq<>8Z蠼Ez\bs-FY2c;Ŵ>`s9.+R[Ka<]y1phL\Ðܪ룹7'|F t] EaI?>^H R2vhGP8y29Lv4}(O}'^نVyPD6Ԟjb]Y)_e7p" "]u>м0&ʱ[OZqWBGVN`a&d_tӞn >2 FW5.&g?xƷkfCȿv3vGLuS^ +CȽ|_rÁZ :S?u9Ք-VnJ:"C|Vudd GO"Px4fw޸JDDDDBa8@DDDD)y! E7ltFxo@ p2Ԙlt\}Xx n!(UNkjq;,ۋmՎig0}0P`y¡K9aXz7H{3x_ۆ p([b8P8ri 1'2?u8 Ki~,B0BJCuj\9̹~g} RVVy$U;q+9{o=:FB-t:d囌 !fmsKvo,u D`״@lVKbZ P0 """""""I׋زr烰rۃ,bKoDlX06oû^lpتv(ކ>\jMb>\oV7 : -a{7a>ppǷ L1^/+gb Sgwj;^1Eۑvb\e| KsW:b}#|p=f6xw Vv]n3 ;E8ot<3o,4߃f촵ǷvG_mqFXW:';(Hso(9A  a|g +S.~4Eaf|K}/oU~+e}~nWcDDDDb8@DDDD4qo=hhDGG;r|> raH >n{?< # o,vR7M ?7a։l!t&_-zo&աmm Ȉ:vb6\BES;P[=#*֮3<4i'9[CgSRpd߿)L "7TJնP\݇ϗaOF2FPta-ž|t+^jʴoQiY],0% ۻKq fXCDcD9D~abmhnf4.-xe$q uphi˭ᓫ

\h9G`8T@}_;U[)kွ#;EACaˌ!=?/H:[Ƚ{]N)K)wMDbxu3yѠpq·+f'|fd-gPXi>6;jF*S`54ϓ; kg on9]Fjpyn| lj 8_mזFpiM?;n '9xVFp*mWNp->4<'=}fʴff{YM,ƆRvW܄< n"%062++hW a\ု6;UX럀 uыd>+VNx%2Nq.=p4>w;x*skZh_ =iZ=0 NCA8$+?6ȄFks]xvwxo^{-'qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@ApF WRJkq6D^{MC a#R:͠W6"˯drE Τ";bV5pP8`8ʷ*߀}lKAl+ E wv2^9  GrJdq^y-3vky%U 10ְ  G+pt^sό>` :*;߹tLW#/5GTmRq"¸zj+V5Im"Ԋ1|{=CpWS\nt :f:/\#lE('ӷ\ot#v.46YIv]>[ܽŐMw}EsE F,!ƿ938pp''&:lqM;o#.Bצx/-H"x qӡ8*w[yVyg/!M "T{Wڤ`qaT\jPd.AGDa"In]D930pn*&Eq!*5Z 0/At@T-Ia\E!h\E5"!Qweܢe&$jʪXaVkE'~9=ì^}~c:5qs ]!*frĪ xꞧ+V\p;ݾʶ}#w9T`>35>Z\2=4N8hE.AJnW񺿻Ƭ̋kݵo/\b %E[6t;7Ω䊣q,^O+0 Z*BEIUUWUG4G(5K޺K3D5ܢw KUҾ~aunPy~:kT"0q^kjygQM?y_z<^^4Ld-*F-zgVMTPY(?TA-8: '"!aDHUgen"`Fp~ FSr0EGsw4Jx\]+g5XW~d14s~1ܶ)~jPSACvϚXMg||p2]%""ԉB#os^\ʔMx}ë8bS/*ݱ/4NϧiWC{mFmQwj]AttiWVV;^ݺ/%:JWR>`*mv;ë&>긨n{ۻߞxMO^qcCD "ic8:+hh 5@8(@A qPM(m0 SJ;v=I)M+*i0-{YK)]I)WU GG=26鵙}>J G;êp%'㕵4_shﱩmm(M?wL\0APD3ud}HWv3t.^|.j?qx9nF͸L+]Py~;g'6K/ޞşJ)}f0]mޛ'mI^{5MG >X]"H>۾şwW^!KEnuZ"ٜ96h_$OT&h-`(aFRz)7k ?侔R^{%IX#9c[%Cnx(ptnqDӹn@+ƒ\Y_<-lӶjΓO7:pOétwtO}/<4}[\m}wo)rgɳŵ6Lbi~An"{M/"4NX5uipVunw<|QWzAhhÈF?nK%+{;h)sUO#J\L)}`2^9#Yo %aE wʪwV }}gU[ 7|߻׾Yd_)W-^iy?\j[6^A1T y`9bj<S@nvٳsZXy>oWѥǭN7,* n'c- e:2+o{a,UoA\w L˹ima4y =U拕)6~}^sЄ 1&?W좸Tj XSB 'ԉybnwoa\A]Kw[ γJjj*tfYeE`1ί398Sf7G1L* cYY?sta4Ecy_nȵ_r*m8sA DeUQ6O Sr_ `!"t YBD1De)K5vS.N\!;:dqj|o.0gUTӾa7±uϓ?J }!9HAU=ızq F`a"XBĜCJT}H&o2Ƶٸs \chj&Ӣ Âq2ĕhޭC>]Ed2ΕGkhUq PrdKR s WǕ*_\(t9l/X}7W Ԁc:TőcvB';*heHckBCK})4h:AXqQvV{yb[Tg-"@>*;CRakhL*]6lʶX`WG,,Bt*.[i\: : BWOD5U:71 P#7Dߋ>W[pW]x;WM%ZϵdUR ZXRp|0./AtDi|߭F(pqgsN "L0.ǟU6FFpTrEEr\juU h{˂ 4QѠv`&R{m>Swcm`.ZkQsZaŕ(8JYz@'4حJSZ`8:T7 r<ڲq@ar7GnjVE&+g+U ,#ڨUeU_mX&}V*ļ 5u?[^ "V}i6,0L)YE5, <JQI#.:SmVV0h*H8qتp+cpQ*6yKdֹJ-ҰR Wjцc@a`8ZJ)a `x%BrЪRp" P ATU܉V΋::u^'Bq&-0L))t,"8E#B9RjӍxeRh_;8[Uqh::R 0.j+HfJi\h_K8R6 Gׄqh)tWp\"8x%Bm*T5\HTO+Uwb0-pGo0\u.N+%ձh0t>UA8G;Bv:J`1L zMSj,+R z71-wWo v/a2^YK)]*t0VJL) z料7t]n+ݵtugz[/ϴoj={{Jo~+}ɯ'_I4RTP׬a@8>'xۭoM}׶^\!y\A=>7ǿ|z?G#4d:6Z54@ABEOsߧݏ,*CN'o_ߟJ>poV;w' :wc~0}'OVnWS*(+Ud0AtHTE"DuܟS.p3u*DE0Zi:h,ϩv姸1Lp+8l[?!-2UpgYp8ڮdgp ;8耨Ag1r0QTh%

!h| jpTbA*oB}]. K5DU]44#{Pr}۬Oo3<9>ZRhVVn{ꊾ!@ak۫YU n/9[nyKJT-\! =jǭa%ڨ>Jak˫Y<څd2iq@o LSj}Qr~[\ݢ@%‚ʕF'+ͦ!G+F'[ZY"\JfއL[6q+U=4laQݰԃT` j=ՕDՏ -ZL)KՑ>.Ah}H|oUTf} Gm_ři y-P#Afb:Q`W9mueAM{5þ:S`dAF" 8͢-GBCAjV^*hkDpT`?CS.?A]Cp ;:V dKVbX,lVmUqM.dٕ jpyRRJ)ső~_hXc}ZBt>x fWj)5Yk4%K-r؄]<M:<B+]i JMh|ϬX)uTaLW.5%6}I"x%~T̯d?M+XYWX\rH#-Z@j5=yܡ,8Kb~r2.XeqR2=Ҕ0.+ kt2^Y-8<5<&#_wmoD&4 ZøL VKzuJnȎ¥Cq|Arg4 Z¸g\M!\݁@Hq~o $*¸kT{j͔N7!kɔҺUWJtݑ\Mn 7r% PӰ"ڏ{85%䡪Q9AM՘5̋M cAlP3gU+5S*lnC\O,|*SSp"pq!Zg5})yX,@8o꠨uוA4ⵜ_SU-^iYΩ#yS9x nr|}j>XAӅC>տO!Jl4@ >;_FjՉRC4ATpAnGon+t-Z}#}.X :mZ+6:t{,!Cj_ G:%2P  'hMpEm|N7rH^c 7|I'hxaL kߨzj+ASa*6пcs5S^yd[U- h<7ۡᆮ1F6V~H ʺ~Gn{AJM>4L0J0>#=qֺ*.+>vC^4 cm#HX,üF Eqp[(0ZlW,V0_-!h&!\ q9-ߥW}% 4q[:M"#64s8fµ@øXa!Զmj*`0Pㅎd!t ~!p- k|f8ܿoՋXsyԒ 'XZHPܖQKL ޲X}eZL:JK :T^O+%=.RB}, ^0=PU<*}Sp8Uj7D~s#%Vά2h0pi' R5 QaWU9AxD_~cCUAq+m2^)opP\b*EpFd8tDd Z.O:Pw9Yث@2\פKjW+Oa=LkOVxrX\k}̫{.> t vL445McC|l~y[Иvorl;ƥ4OV,B-ƎvԦ[=o^ac܌gk?0,bT:+r"N G- jY8:'/"&)yGZ|N In5-> CRqt`8XKg34J*t .\慃n%'[z. qtB^"kv >B8葛t6iN8q^)P%U׾٩. ᠟qr~sϗO}=tRb^Ϻt:0L vsrQ%+Z JdhPʯdBmT؜J(,[ nON+Cc_8hRHोT{?xhh߿Nʌ[mگDeb(9 Z..[DD@$Uon}̿N G;zHznwc8hT5-:mkbK`8: GEɟAgSJfd|E[|NOTg%ME[yk}6Wޜ>[y@\ThM^y71}nA O4.(.ځ~>W?_֛ nj0-*?aWZG @Ϡx-qz[B7 8:IwF|tHw}&oYĹ<;ߑn卯 {i\Zy޻g[[)B9w\wO=|N2 qG8:I zb P  @8(@A qP  @8(@A qP  @8(@A qP j6Lt Zӳ@+zh  Tmqt h @Oxep/%q8-xVLec 4E!&60`0uzhx{ ;=#208A}pV/ sn2^S_qt^bC5 8~vZ5 ^C d2 O8zc2^YK)}R5lJ7k}ozf0NwX%uɐT7qdr:tAD780(`­ilTz0UBRPL0U 8v),SW+);=CdbuT:*je$WZ{s*p^Th)tq\pg'U ܈ -h'`1}2^k qphybioxeF3ʕr_@7WRJky.I @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qP  @8(@A qPtTQ4UIENDB`sphinx-gallery-0.16.0/doc/_static/switcher.json000066400000000000000000000003041461331107500214600ustar00rootroot00000000000000[ { "version": "stable", "url": "https://sphinx-gallery.github.io/stable/" }, { "version": "dev", "url": "https://sphinx-gallery.github.io/dev/" } ]sphinx-gallery-0.16.0/doc/_static/theme_override.css000066400000000000000000000011641461331107500224550ustar00rootroot00000000000000.wy-nav-top { background-color: #FF8C38 !important; } .wy-side-nav-search { background-color: #FF8C38 !important; } div[class^="highlight"] a { background-color: #E6E6E6; } div[class^="highlight"] a:hover { background-color: #ABECFC; } .rst-versions { position: relative; } .rst-versions.shift-up { overflow-y: visible; } a[class^="sphx-glr-backref-module-"] { text-decoration: none; background-color: rgba(0, 0, 0, 0) !important; } a.sphx-glr-backref-module-sphinx_gallery { text-decoration: underline; background-color: #E6E6E6; } .anim-state label { display: inline-block; } sphinx-gallery-0.16.0/doc/_templates/000077500000000000000000000000001461331107500174475ustar00rootroot00000000000000sphinx-gallery-0.16.0/doc/_templates/module.rst000066400000000000000000000017401461331107500214700ustar00rootroot00000000000000.. Please when editing this file make sure to keep it matching the docs in ../configuration.rst:reference_to_examples {{ fullname }} {{ underline }} .. automodule:: {{ fullname }} {% block functions %} {% if functions %} Functions --------- {% for item in functions %} .. autofunction:: {{ item }} .. _sphx_glr_backref_{{fullname}}.{{item}}: .. minigallery:: {{fullname}}.{{item}} :add-heading: {%- endfor %} {% endif %} {% endblock %} {% block classes %} {% if classes %} Classes ------- {% for item in classes %} .. autoclass:: {{ item }} :members: .. _sphx_glr_backref_{{fullname}}.{{item}}: .. minigallery:: {{fullname}}.{{item}} :add-heading: {%- endfor %} {% endif %} {% endblock %} {% block exceptions %} {% if exceptions %} Exceptions ---------- .. autosummary:: {% for item in exceptions %} {{ item }} {%- endfor %} {% endif %} {% endblock %} sphinx-gallery-0.16.0/doc/advanced.rst000066400000000000000000000512031461331107500176120ustar00rootroot00000000000000.. _advanced_usage: ============== Advanced usage ============== This page contains more advanced topics in case you want to understand how to use Sphinx-Gallery more deeply. .. contents:: **Contents** :local: :depth: 2 Extend your Makefile for Sphinx-Gallery ======================================= This section describes some common extensions to the documentation Makefile that are useful for Sphinx-Gallery. Cleaning the gallery files -------------------------- Once your gallery is working you might need completely remove all generated files by Sphinx-Gallery to have a clean build. For this we recommend adding the following to your Sphinx ``Makefile``: .. code-block:: bash clean: rm -rf $(BUILDDIR)/* rm -rf auto_examples/ You need to adapt the second ``rm`` command if you have changed the ``gallery_dirs`` config variable. Build the gallery without running any examples ---------------------------------------------- If you wish to build your gallery without running examples first (e.g., if an example takes a long time to run), add the following to your ``Makefile``. .. code-block:: bash html-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(SOURCEDIR) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." Know your Gallery files ======================= The Gallery has been built, now you and all of your project's users can already start enjoying it. All the temporary files needed to generate the gallery (rst files, images, cache objects, etc) are stored where you configured in ``gallery_dirs``. The final files that go into the HTML version of your documentation have a particular namespace, to avoid collisions with your own files and images. Our namespace convention is to prefix everything with ``sphx_glr`` and change path separators with underscores. For example the first image generated by the example 'plot_0_sin.py' has the name ``sphx_glr_plot_0_sin_001.png`` and its thumbnail is ``sphx_glr_plot_0_sin_thumb.png`` You can also include part of a gallery script elsewhere in your documentation using the :rst:dir:`literalinclude` directive, in order to limit code duplication: .. code-block:: rst .. literalinclude:: ../examples/plot_0_sin.py :language: python :start-after: # License: BSD 3 clause :end-before: # To avoid matplotlib The above directive inserts the following block: .. literalinclude:: ../examples/plot_0_sin.py :language: python :start-after: # License: BSD 3 clause :end-before: # To avoid matplotlib .. warning:: Using literalinclude is fragile and can break easily when examples are changed (all the more when line numbers are used instead of ``start-after`` and ``end-before``). Use with caution: linking directly to examples is a more robust alternative. Cross referencing ----------------- You can also cross reference an example using similar naming convention. For example if we want to reference the example :ref:`sphx_glr_auto_examples_plot_0_sin.py`, we just call its reference ``:ref:`sphx_glr_auto_examples_plot_0_sin.py```. Note that we have included the path to the example file (relative to the ``conf.py`` file) after ``sphx_glr_``. Path separators are replaced with underscores. .. _warning_errors: Understanding warning and error outputs ======================================= Any warnings or errors that occur when executing code blocks in the gallery Python files will be printed in pink during building of the documentation. The ``.py`` file path and the line number that the error occurred in will also be printed. For example, the example :ref:`sphx_glr_auto_examples_no_output_plot_raise.py` will raise the following error:: File "/examples/no_output/plot_raise.py", line 27, in iae NameError: name 'iae' is not defined Problems in the text (reST) blocks of the gallery Python files will result in warnings or errors when Sphinx is converting the generated ``.rst`` files to HTML. These will be printed by Sphinx in pink, after code block errors, during building of the documentation. In this case, the ``.rst`` file path and ``.rst`` file line number will be printed. To fix the problem, you will need to amend the original ``.py`` file, **not** the generated ``.rst`` file. To figure out where the problem is, you will need to match the content of the ``.rst`` file at the line number printed to the original ``.py`` file. Example ``.rst`` warning:: /auto_examples/plot_example.rst:19: WARNING: Explicit markup ends without a blank line; unexpected unindent. The warning above occurred due to line 19 in ``plot_example.rst``. The original ``plot_example.py`` file will need to be amended to fix it. Sphinx-Gallery only (re)builds new, modified or failed examples, so re-running the documentation build should rebuild just the modified example, allowing for quick iteration. .. _custom_scraper: Write a custom image scraper ============================ .. warning:: The API for custom scrapers is currently experimental. By default, Sphinx-Gallery supports image scraping for Matplotlib (:func:`~sphinx_gallery.scrapers.matplotlib_scraper`). If you wish to capture output from other python packages, first determine if the object you wish to capture has a ``_repr_html_`` method. If so, you can use the configuration ``capture_repr`` (:ref:`capture_repr`) to control the display of the object, without the need to write a custom scraper. This configuration allows capture of the raw html output, in a process similar to other html-based displays such as `jupyter `_. If the first option does not work, this section describes how to write a custom scraper. Image scrapers are functions (or callable class instances) that do the following things: 1. Collect a list of images created in the latest execution of code. 2. Write these images to disk in PNG, JPEG, SVG, GIF, or WebP format (with .png, .jpg, .svg, .gif, or .webp extensions, respectively) 3. Return reST that embeds these figures in the built documentation. The function should take the following inputs (in this order): 1. ``block`` - a Sphinx-Gallery ``.py`` file is separated into consecutive lines of 'code' and reST 'text', called 'blocks'. For each block, a tuple containing the (label, content, line_number) (e.g. ``('code', 'print("Hello world")', 5)``) of the block is created. * 'label' is a string that can either be ``'text'`` or ``'code'``. In this context, it should only be ``'code'`` as this function is only called for code blocks. * 'content' is a string containing the actual content of the code block. * 'line_number' is an integer, indicating the line number that the block starts at. 2. ``block_vars`` - dictionary of configuration and runtime variables. Of interest for image scrapers is the element ``'image_path_iterator'`` which is an iterable that returns an absolute path to an image file name adhering to Sphinx-Gallery naming convention. The path directs to the ``gallery_dirs/images`` directory (:ref:`configure_and_use_sphinx_gallery`) and the image file name is ``'sphx_glr_'`` followed by the name of the source ``.py`` file then a number, which starts at 1 and increases by 1 at each iteration. The default file format is ``.'png'``. For example: ``'home/user/Documents/module/auto_examples/images/sphx_glr_plot_mymodule_001.png'``. If a different image extension is desired, the scraper is responsible for replacing the default `.png` extension. Only supported image extensions (see above) will enable the file to be picked up by Sphinx-Gallery. 3. ``gallery_conf`` - dictionary containing the configuration of Sphinx-Gallery, set under ``sphinx_gallery_conf`` in ``doc/conf.py`` (:ref:`configuration`). Of note, the :ref:`image_srcset ` configuration will provide user specified image resolutions (as floats) and can be used by your custom scraper to enable multi-resolution images. It should return a string containing the reST for embedding this figure in the documentation. See :func:`~sphinx_gallery.scrapers.matplotlib_scraper` for an example of a scraper function (click on 'source' below the function name to see the source code). The :func:`~sphinx_gallery.scrapers.matplotlib_scraper` uses the helper function :func:`sphinx_gallery.scrapers.figure_rst` to help generate reST (see below). This function will be called once for each code block of your examples. Sphinx-Gallery will take care of scaling images for the gallery index page thumbnails. PNG, JPEG and WebP images are scaled using Pillow, and SVG and GIF images are copied. .. warning:: SVG images do not work with ``latex`` build modes, thus will not work while building a PDF version of your documentation. You may want to consider `sphinxcontrib-svg2pdfconverter `_. Example 1: a Matplotlib-style scraper ------------------------------------- For example, we will show sample code for a scraper for a hypothetical package. It uses an approach similar to what :func:`sphinx_gallery.scrapers.matplotlib_scraper` does under the hood, which use the helper function :func:`sphinx_gallery.scrapers.figure_rst` to create the standardized reST. If your package will be used to write an image file to disk (e.g., PNG or JPEG), we recommend you use a similar approach:: def my_module_scraper(block, block_vars, gallery_conf): import mymodule # We use a list to collect references to image names image_names = list() # The `image_path_iterator` is created by Sphinx-Gallery, it will yield # a path to a file name that adheres to Sphinx-Gallery naming convention. image_path_iterator = block_vars['image_path_iterator'] # Define a list of our already-created figure objects. list_of_my_figures = mymodule.get_figures() # Iterate through figure objects, save to disk, and keep track of paths. for fig, image_path in zip(list_of_my_figures, image_path_iterator): fig.save_png(image_path) image_names.append(image_path) # Close all references to figures so they aren't used later. mymodule.close('all') # Use the `figure_rst` helper function to generate the reST for this # code block's figures. Alternatively you can define your own reST. return figure_rst(image_names, gallery_conf['src_dir']) This code could be defined either in your ``conf.py`` file, or as a module that you import into your ``conf.py`` file (see :ref:`importing_callables`). The configuration needed to use this scraper would look like:: sphinx_gallery_conf = { ... 'image_scrapers': ('matplotlib', "my_module._scraper.my_module_scraper"), } Where Sphinx-Gallery will parse the string ``"my_module._scraper.my_module_scraper"`` to import the callable function. Example 2: detecting image files on disk ---------------------------------------- Here's another example that assumes that images have *already been written to disk*. In this case we won't *generate* any image files, we'll only generate the reST needed to embed them in the documentation. Note that the example scripts will still need to be executed to scrape the files, but the images don't need to be produced during the execution. We assume the function is defined within your package in a module called ``_scraper``. Here is the scraper code:: from glob import glob import shutil import os from sphinx_gallery.scrapers import figure_rst def png_scraper(block, block_vars, gallery_conf): # Find all PNG files in the directory of this example. path_current_example = os.path.dirname(block_vars['src_file']) pngs = sorted(glob(os.path.join(path_current_example, '*.png'))) # Iterate through PNGs, copy them to the Sphinx-Gallery output directory image_names = list() image_path_iterator = block_vars['image_path_iterator'] seen = set() for png in pngs: if png not in seen: seen |= set(png) this_image_path = image_path_iterator.next() image_names.append(this_image_path) shutil.move(png, this_image_path) # Use the `figure_rst` helper function to generate reST for image files return figure_rst(image_names, gallery_conf['src_dir']) Then, in our ``conf.py`` file, we include the following code:: sphinx_gallery_conf = { ... 'image_scrapers': ('matplotlib', 'my_module._scraper.png_scraper'), } Example 3: matplotlib with SVG format ------------------------------------- The :func:`sphinx_gallery.scrapers.matplotlib_scraper` supports ``**kwargs`` to pass to :meth:`matplotlib.figure.Figure.savefig`, one of which is the ``format`` argument. See :ref:`custom_scraper` for supported formats. To use SVG you can define the following function and ensure it is :ref:`importable `:: def matplotlib_svg_scraper(*args, **kwargs): return matplotlib_scraper(*args, format='svg', **kwargs) Then in your ``conf.py``:: sphinx_gallery_conf = { ... 'image_scrapers': ("sphinxext.matplotlib_svg_scraper",), ... } You can also use different formats on a per-image basis, but this requires writing a customized scraper class or function. .. _mayavi_scraper: Example 4: Mayavi scraper ------------------------- Historically, Sphinx-Gallery supported scraping Mayavi figures as well as matplotlib figures. However, due to the complexity of maintaining the scraper, support was deprecated in version 0.12.0. To continue using a Mayavi scraping, consider using something like the following:: from sphinx_gallery.scrapers import figure_rst def mayavi_scraper(self, block, block_vars, gallery_conf): from mayavi import mlab image_path_iterator = block_vars['image_path_iterator'] image_paths = list() e = mlab.get_engine() for scene, image_path in zip(e.scenes, image_path_iterator): try: mlab.savefig(image_path, figure=scene) except Exception: mlab.close(all=True) raise # make sure the image is not too large scale_image(image_path, image_path, 850, 999) if 'images' in gallery_conf['compress_images']: optipng(image_path, gallery_conf['compress_images_args']) image_paths.append(image_path) mlab.close(all=True) return figure_rst(image_paths, gallery_conf['src_dir']) Integrate custom scrapers with Sphinx-Gallery --------------------------------------------- Sphinx-Gallery plans to internally maintain only one scraper: matplotlib. If you have extended or fixed bugs with this scraper, we welcome PRs to improve it! On the other hand, if you have developed a custom scraper for a different plotting library that would be useful to the broader community, we encourage you to get it working with Sphinx-Gallery and then maintain it externally (probably in the package that it scrapes), and then integrate and advertise it with Sphinx-Gallery. You can: 1. Contribute it to the list of externally supported scrapers located in :ref:`reset_modules`. 2. Optional: add a custom hook to your module root to simplify scraper use. Taking PyVista as an example, adding ``pyvista._get_sg_image_scraper()`` that returns the ``callable`` scraper to be used by Sphinx-Gallery allows PyVista users to just use strings as they already can for ``'matplotlib'``:: sphinx_gallery_conf = { ... 'image_scrapers': ('pyvista',) } Sphinx-Gallery will import the named module (here, ``pyvista``) and use the ``_get_sg_image_scraper`` function defined there as a scraper. .. _custom_reset: Resetting before each example ============================= Sphinx-Gallery supports 'resetting' function(s) that are run before and/or after each example script is executed. This is used natively in Sphinx-Gallery to 'reset' the behavior of the visualization packages ``matplotlib`` and ``seaborn`` (:ref:`reset_modules`). However, this functionality could be used to reset other libraries, modify the resetting behavior for a natively-reset library or run an arbitrary custom function at the start and/or end of each script. This is done by adding a custom function to the resetting tuple defined in ``conf.py``. The function should take two variables: a dictionary called ``gallery_conf`` (which is your Sphinx-Gallery configuration) and a string called ``fname`` (which is the file name of the currently-executed Python script). These generally don't need to be used in order to perform whatever resetting behavior you want, but must be included in the function definition for compatibility reasons. For example, to reset matplotlib to always use the ``ggplot`` style, you could do:: def reset_mpl(gallery_conf, fname): from matplotlib import style style.use('ggplot') Any custom functions can be defined (or imported) in ``conf.py`` and given to the ``reset_modules`` configuration key. To add the function defined above (assuming you've make it :ref:`importable `):: sphinx_gallery_conf = { ... 'reset_modules': ("sphinxext.reset_mpl", "seaborn"), } In the config above ``"seaborn"`` refers to the native seaborn resetting function (see :ref:`reset_modules`). .. note:: Using resetters such as ``reset_mpl`` that deviate from the standard behavior that users will experience when manually running examples themselves is discouraged due to the inconsistency that results between the rendered examples and local outputs. If the custom function needs to be aware of whether it is being run before or after an example, a function signature with three parameters can be used, where the third parameter is required to be named ``when``:: def reset_mpl(gallery_conf, fname, when): import matplotlib as mpl mpl.rcParams['lines.linewidth'] = 2 if when == 'after' and fname=='dashed_lines': mpl.rcParams['lines.linestyle'] = '-' The value passed into ``when`` can be ``'before'`` or ``'after'``. If ``reset_modules_order`` in the :ref:`configuration ` is set to ``'before'`` or ``'after'``, ``when`` will always be the same value as what ``reset_modules_order`` is set to. This function signature is only useful when used in conjunction with ``reset_modules_order`` set to ``'both'``. Altering Sphinx-Gallery CSS =========================== The Sphinx-Gallery ``.css`` files that control the appearance of your example gallery can be found `here `_. These default ``.css`` files are added to your build. Specifically, they are copied into ``_build/html/_static/`` of your ``gallery_dir``. You can add your own custom ``.css`` files by using the :doc:`Sphinx configuration ` ``html_static_path``. For example, list any path(s) that contain your custom static files, using the ``html_static_path`` configuration, in your ``conf.py`` file:: html_static_path = ['_static'] # Custom CSS paths should either relative to html_static_path # or fully qualified paths (eg. https://...) html_css_files = ['my_custom.css', 'other_change.css'] The default Sphinx-Gallery ``.css`` files are copied to your build **after** files in your ``html_static_path`` config. This means that files in your ``html_static_path`` that are named the same as Sphinx-Gallery ``.css`` files will be over-written. You can easily avoid this as all Sphinx-Gallery ``.css`` files are prepended with 'sg\_' (e.g., 'sg_gallery.css'). More details on this can be found in `PR #845 `_. Custom css can be used for example to alter the appearance of :ref:`code links ` and :ref:`thumbnail size `. Hide the download buttons in the example headers ------------------------------------------------ .. code-block:: css div.sphx-glr-download-link-note { display: none; } Disable thumbnail text on hover ------------------------------- .. code-block:: css .sphx-glr-thumbcontainer[tooltip]:hover::before, .sphx-glr-thumbcontainer[tooltip]:hover::after { display: none; } Using (only) Sphinx-Gallery styles ================================== If you just want to make use of sphinx-Gallery CSS files, instead of using the ``sphinx_gallery.gen_gallery`` extension, you can use in ``conf.py``:: extensions = ['sphinx_gallery.load_style'] This will only cause the ``gallery.css`` file to be added to your build. sphinx-gallery-0.16.0/doc/advanced_index.rst000066400000000000000000000003331461331107500207770ustar00rootroot00000000000000Advanced topics =============== This section contains more advanced topics such as advanced configuration, frequently asked questions, and utilities. .. toctree:: :hidden: advanced faq utils reference sphinx-gallery-0.16.0/doc/binder/000077500000000000000000000000001461331107500165555ustar00rootroot00000000000000sphinx-gallery-0.16.0/doc/binder/requirements.txt000066400000000000000000000002471461331107500220440ustar00rootroot00000000000000# Requirements for the documentation. # These are extra dependencies that aren't required to use sphinx-gallery. numpy matplotlib pillow seaborn joblib sphinx-gallery sphinx-gallery-0.16.0/doc/changes.rst000066400000000000000000000000351461331107500174520ustar00rootroot00000000000000 .. include:: ../CHANGES.rst sphinx-gallery-0.16.0/doc/conf.py000066400000000000000000000326711461331107500166220ustar00rootroot00000000000000"""Sphinx-Gallery documentation build configuration file.""" # Sphinx-Gallery documentation build configuration file, created by # sphinx-quickstart on Mon Nov 17 16:01:26 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os from datetime import date import warnings import sphinx_gallery # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "sphinxext")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # 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", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx.ext.autosummary", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.mathjax", "sphinx_gallery.gen_gallery", "sphinx.ext.graphviz", "jupyterlite_sphinx", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # generate autosummary even if no references autosummary_generate = True # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Sphinx-Gallery" copyright = "2014-%s, Sphinx-gallery developers" % date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = sphinx_gallery.__version__ # The full version, including alpha/beta/rc tags. release = sphinx_gallery.__version__ + "-git" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build", "sphinxext"] # See warnings about bad links nitpicky = True nitpick_ignore = [("", "Pygments lexer name 'ipython' is not known")] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" highlight_language = "python3" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # The theme is set by the make target html_theme = "pydata_sphinx_theme" def setup(app): """Sphinx setup function.""" app.add_css_file("theme_override.css") app.add_object_type( "confval", "confval", objname="configuration value", indextemplate="pair: %s; configuration value", ) # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "navbar_center": ["navbar-nav"], "show_toc_level": 2, "show_nav_level": 2, "navbar_end": ["theme-switcher", "version-switcher", "navbar-icon-links"], "navigation_with_keys": False, "logo": { "text": "🖼️ Sphinx-Gallery", }, "switcher": dict( json_url="https://sphinx-gallery.github.io/dev/_static/switcher.json", version_match="dev" if "dev" in version else "stable", ), "github_url": "https://github.com/sphinx-gallery/sphinx-gallery", "icon_links": [ { "name": "PyPI", "url": "https://pypi.org/project/sphinx-gallery", "icon": "fa-solid fa-box", }, ], } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # 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"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { "reference": [], } # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Sphinx-Gallerydoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( "index", "Sphinx-Gallery.tex", "Sphinx-Gallery Documentation", "Óscar Nájera", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ("index", "sphinx-gallery", "Sphinx-Gallery Documentation", ["Óscar Nájera"], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "Sphinx-Gallery", "Sphinx-Gallery Documentation", "Óscar Nájera", "Sphinx-Gallery", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": (f"https://docs.python.org/{sys.version_info.major}", None), "numpy": ("https://numpy.org/doc/stable/", None), "matplotlib": ("https://matplotlib.org/stable", None), "pyvista": ("https://docs.pyvista.org/version/stable", None), "sklearn": ("https://scikit-learn.org/stable", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), } examples_dirs = ["../examples", "../tutorials"] gallery_dirs = ["auto_examples", "tutorials"] image_scrapers = ("matplotlib",) try: # Run the PyVista examples and find the PyVista figures if PyVista is # installed import pyvista except Exception: # can raise all sorts of errors pass else: image_scrapers += ("pyvista",) examples_dirs.append("../pyvista_examples") gallery_dirs.append("auto_pyvista_examples") pyvista.OFF_SCREEN = True # Preferred plotting style for documentation pyvista.set_plot_theme("document") pyvista.global_theme.window_size = [1024, 768] pyvista.global_theme.font.size = 22 pyvista.global_theme.font.label_size = 22 pyvista.global_theme.font.title_size = 22 pyvista.global_theme.return_cpos = False # necessary when building the sphinx gallery pyvista.BUILDING_GALLERY = True pyvista.set_jupyter_backend(None) # Set plotly renderer to capture _repr_html_ for sphinx-gallery try: import plotly.io except ImportError: pass else: plotly.io.renderers.default = "sphinx_gallery" examples_dirs.append("../plotly_examples") gallery_dirs.append("auto_plotly_examples") min_reported_time = 0 if "SOURCE_DATE_EPOCH" in os.environ: min_reported_time = sys.maxint if sys.version_info[0] == 2 else sys.maxsize sphinx_gallery_conf = { "backreferences_dir": "gen_modules/backreferences", "doc_module": ("sphinx_gallery", "numpy"), "reference_url": { "sphinx_gallery": None, }, "examples_dirs": examples_dirs, "gallery_dirs": gallery_dirs, "image_scrapers": image_scrapers, "compress_images": ("images", "thumbnails"), # specify the order of examples to be according to filename "within_subsection_order": "FileNameSortKey", "expected_failing_examples": [ "../examples/no_output/plot_raise.py", "../examples/no_output/plot_syntaxerror.py", ], "min_reported_time": min_reported_time, "binder": { "org": "sphinx-gallery", "repo": "sphinx-gallery.github.io", "branch": "master", "binderhub_url": "https://mybinder.org", "dependencies": "./binder/requirements.txt", "notebooks_dir": "notebooks", "use_jupyter_lab": True, }, "jupyterlite": { "notebook_modification_function": "sg_doc_build.notebook_modification_function", }, "show_memory": True, "promote_jupyter_magic": False, "junit": os.path.join("sphinx-gallery", "junit-results.xml"), # capture raw HTML or, if not present, __repr__ of last expression in # each code block "capture_repr": ("_repr_html_", "__repr__"), "matplotlib_animations": True, "image_srcset": ["2x"], "nested_sections": True, "show_api_usage": True, } # Remove matplotlib agg warnings from generated doc when using plt.show warnings.filterwarnings( "ignore", category=UserWarning, message="Matplotlib is currently using agg, which is a" " non-GUI backend, so cannot show the figure.", ) sphinx-gallery-0.16.0/doc/configuration.rst000066400000000000000000002735161461331107500207310ustar00rootroot00000000000000.. _configuration: ============= Configuration ============= Configuration and customization of Sphinx-Gallery is done primarily with a dictionary specified in your ``conf.py`` file. A list of the possible keys are listed :ref:`below ` and explained in greater detail in subsequent sections. When using these flags, it is good practice to make sure the source Python files are equivalent to the generated HTML and iPython notebooks (i.e. make sure ``.py == .html == .ipynb``). This principle should be violated only when necessary, and on a case-by-case basis. .. _list_of_options: Configuration options ====================== Global ``conf.py`` configurations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sphinx-Gallery configuration options that can be set in the Sphinx ``conf.py`` file, inside a ``sphinx_gallery_conf`` dictionary. **Gallery files and ordering** - ``examples_dirs`` and ``gallery_dirs`` (:ref:`multiple_galleries_config`) - ``filename_pattern``, ``ignore_pattern``, ``example_extensions``, and ``filetype_parsers`` (:ref:`build_pattern`) - ``copyfile_regex`` (:ref:`manual_passthrough`) - ``subsection_order`` (:ref:`sub_gallery_order`) - ``within_subsection_order`` (:ref:`within_gallery_order`) - ``nested_sections`` (:ref:`nested_sections`) **Example execution** - ``reset_argv`` (:ref:`reset_argv`) - ``capture_repr`` and ``ignore_repr_types`` (:ref:`capture_repr`) - ``plot_gallery`` (:ref:`without_execution`) - ``run_stale_examples`` (:ref:`run_stale_examples`) - ``abort_on_example_error`` (:ref:`abort_on_first`) - ``expected_failing_examples`` (:ref:`dont_fail_exit`) - ``only_warn_on_example_error`` (:ref:`warning_on_error`) **Cross-referencing** - ``reference_url``, ``prefer_full_module`` (:ref:`link_to_documentation`) - ``backreferences_dir``, ``doc_module``, ``exclude_implicit_doc``, and ``inspect_global_variables`` (:ref:`references_to_examples`) - ``minigallery_sort_order`` (:ref:`minigallery_order`) **Images and thumbnails** - ``default_thumb_file`` (:ref:`custom_default_thumb`) - ``thumbnail_size`` (:ref:`setting_thumbnail_size`) - ``image_srcset`` (:ref:`image_srcset`) - ``image_scrapers`` (:ref:`image_scrapers`) - ``compress_images`` (:ref:`compress_images`) **Compute costs** - ``min_reported_time`` (:ref:`min_reported_time`) - ``show_memory`` (:ref:`show_memory`) - ``junit`` (:ref:`junit_xml`) **Jupyter notebooks and interactivity** - ``notebook_extensions`` (:ref:`notebook_extensions`) - ``promote_jupyter_magic`` (:ref:`promote_jupyter_magic`) - ``first_notebook_cell`` and ``last_notebook_cell`` (:ref:`own_notebook_cell`) - ``notebook_images`` (:ref:`notebook_images`) - ``pypandoc`` (:ref:`use_pypandoc`) - ``binder`` (:ref:`binder_links`) - ``jupyterlite`` (:ref:`jupyterlite`) **Appearance** - ``line_numbers`` (:ref:`adding_line_numbers`) - ``remove_config_comments`` (:ref:`removing_config_comments`) - ``show_signature`` (:ref:`show_signature`) - ``download_all_examples`` (:ref:`disable_all_scripts_download`) **Miscellaneous** - ``reset_modules`` and ``reset_modules_order`` (:ref:`reset_modules`) - ``recommender`` (:ref:`recommend_examples`) - ``log_level`` (:ref:`log_level`) - ``show_api_usage`` and ``api_usage_ignore`` (:ref:`show_api_usage`) Configurations inside examples ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some options can also be set or overridden on a file-by-file basis: - ``# sphinx_gallery_line_numbers`` (:ref:`adding_line_numbers`) - ``# sphinx_gallery_thumbnail_number`` (:ref:`choosing_thumbnail`) - ``# sphinx_gallery_thumbnail_path`` (:ref:`providing_thumbnail`) - ``# sphinx_gallery_dummy_images`` (:ref:`dummy_images`) - ``# sphinx_gallery_capture_repr`` (:ref:`capture_repr`) Some options can be set on a per-code-block basis in a file: - ``# sphinx_gallery_defer_figures`` (:ref:`defer_figures`) Some options can be set on a per-line basis in a file: - ``# sphinx_gallery_start_ignore`` and ``# sphinx_gallery_end_ignore`` (:ref:`hiding_code_blocks`) See also :ref:`removing_config_comments` to hide config comments in files from the rendered examples. Build options ^^^^^^^^^^^^^ Some options can be set during the build execution step, e.g. using a Makefile: - ``make html-noplot`` (:ref:`without_execution`) - ``make html_abort_on_example_error`` (:ref:`abort_on_first`) CSS changes ^^^^^^^^^^^ Some things can be tweaked directly in CSS: - ``.sphx-glr-thumbcontainer`` (:ref:`setting_thumbnail_size`) .. _removing_warnings: Removing warnings ================= To prevent warnings from being captured and included in your built documentation, you can use the package ``warnings`` in the ``conf.py`` file. For example, to remove the specific Matplotlib agg warning, you can add:: import warnings warnings.filterwarnings("ignore", category=UserWarning, message='Matplotlib is currently using agg, which is a' ' non-GUI backend, so cannot show the figure.' '|(\n|.)*is non-interactive, and thus cannot be shown') to your ``conf.py`` file. Note that the above Matplotlib warning is removed by default. .. _importing_callables: Importing callables =================== Sphinx-Gallery configuration values that are instantiated classes, classes or functions should be passed as fully qualified name strings to the objects. The object needs to be importable by Sphinx-Gallery. Two common ways to achieve this are: 1. Define your object with your package. For example, you could write a function ``def my_sorter`` and put it in ``mymod/utils.py``, then use:: sphinx_gallery_conf = { #..., "minigallery_sort_order": "mymod.utils.my_sorter", #... } 2. Define your object with your documentation. For example, you can add documentation-specific stuff in a different path and ensure that it can be resolved at build time. For example, you could create a file ``doc/sphinxext.py`` and define your function: .. code-block:: def plotted_sorter(fname): return not fname.startswith("plot_"), fname And set in your configuration: .. code-block:: sys.path.insert(0, os.path.dirname(__file__)) sphinx_gallery_conf = { #..., "minigallery_sort_order": "sphinxext.plotted_sorter", #... } And Sphinx-Gallery would resolve ``"sphinxext.plotted_sorter"`` to the ``plotted_sorter`` object because the ``doc/`` directory is first on the path. Built in classes like :class:`sphinx_gallery.sorting.FileNameSortKey` and similar can be used with shorter direct alias strings like ``"FileNameSortKey"`` (see :ref:`within_gallery_order` for details). .. note:: Sphinx-Gallery >0.16.0 supports use of fully qualified name strings as a response to the Sphinx >7.3.0 changes to caching and serialization checks of the ``conf.py`` file. This means that the previous use of class instances as configuration values to ensure the ``__repr__`` was stable across builds is redundant *if* you are passing configuration values via name strings. When using name strings, the configuration object can just be a function. .. _stable_repr: Ensuring a stable ``__repr__`` ============================== For backwards compatibility Sphinx-Gallery allows certain configuration values to be a callable object instead of a :ref:`importable name string `. If you wish to use a callable object you will have to ensure that the ``__repr__`` is stable across runs. Sphinx determines if the build environment has changed, and thus if *all* documents should be rewritten, by examining the config values using ``md5(str(obj).encode()).hexdigest()`` in ``sphinx/builders/html.py``. Default class instances in Python have their memory address in their ``__repr__`` which is why generally the ``__repr__`` changes in each build. Your callable should be a class that defines a stable ``__repr__`` method. For example, :class:`sphinx_gallery.sorting.ExplicitOrder` stability is ensured via the custom ``__repr__``:: def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.ordered_list) Therefore, the files are only all rebuilt when the specified ordered list is changed. .. _multiple_galleries_config: Manage multiple galleries ========================= Sphinx-Gallery only supports one level of subfolder nesting in its gallery directories. For example our :ref:`examples-index`, has the parent gallery in `examples/` and the sub-gallery in `examples/no_output/`. Further sub-folders are not supported. This might be a limitation for you. Or you might want to have separate galleries for different purposes; an examples gallery and a tutorials gallery. To do this set the Sphinx-Gallery configuration dictionary keys `examples_dirs` and `gallery_dirs` in your Sphinx ``conf.py`` file to be a list of directories:: sphinx_gallery_conf = { ... 'examples_dirs': ['../examples', '../tutorials'], 'gallery_dirs': ['auto_examples', 'tutorials'], } Keep in mind that both lists have to be of the same length. .. note:: If your examples take a long time to run, consider looking at the :ref:`execution times ` file that is generated for each gallery dir (as long as any examples were actually executed in that directory during the build). .. _build_pattern: Parsing and executing examples via matching patterns ==================================================== By default, Sphinx-Gallery will **parse and add** all files with a ``.py`` extension to the gallery, but only **execute** files beginning with ``plot_``. These behaviors are controlled by the ``ignore_pattern``, ``filename_pattern``, and ``example_extensions`` entries, which have the default values:: sphinx_gallery_conf = { ... 'filename_pattern': '/plot_', 'ignore_pattern': r'__init__\.py', 'example_extensions': {'.py'} } To omit some files from the gallery entirely (i.e., not execute, parse, or add them), you can change the ``ignore_pattern`` option. To choose which of the parsed and added Python scripts are actually executed, you can modify ``filename_pattern``. For example:: sphinx_gallery_conf = { ... 'filename_pattern': '/plot_compute_', } will build all examples starting with ``plot_compute_``. The key ``filename_pattern`` (and ``ignore_pattern``) accepts `regular expressions`_ which will be matched with the full path of the example. This is the reason the leading ``'/'`` is required. Users are advised to use ``re.escape(os.sep)`` instead of ``'/'`` if they want to be agnostic to the operating system. The ``filename_pattern`` option is also useful if you want to build only a subset of the examples. For example, you may want to build only one example so that you can link it in the documentation. In that case, you would do:: sphinx_gallery_conf = { ... 'filename_pattern': r'plot_awesome_example\.py', } Here, one should escape the dot ``r'\.'`` as otherwise python `regular expressions`_ matches any character. Nevertheless, as one is targeting a specific file, it would match the dot in the filename even without this escape character. .. note:: Sphinx-Gallery only re-runs examples that have changed (according to their md5 hash). See :ref:`run_stale_examples` below for information. Similarly, to build only examples in a specific directory, you can do:: sphinx_gallery_conf = { ... 'filename_pattern': '/directory/plot_', } Alternatively, you can skip executing some examples. For example, to skip building examples starting with ``plot_long_examples_``, you would do:: sphinx_gallery_conf = { ... 'filename_pattern': '/plot_(?!long_examples)', } As the patterns are parsed as `regular expressions`_, users are advised to consult the `regular expressions`_ module for more details. .. note:: Remember that Sphinx allows overriding ``conf.py`` values from the command line, so you can for example build a single example directly via something like: .. code-block:: console $ sphinx-build -D sphinx_gallery_conf.filename_pattern=plot_specific_example\.py ... You can also parse and highlight syntax examples in other languages by adding their extensions to ``example_extensions``, though they will not be executed. For example, to include examples in Python, Julia, and C++:: sphinx_gallery_conf = { ... 'example_extensions': {'.py', '.jl', '.cpp'} } Parsing and syntax highlighting is supported by the Pygments library, with the language determined by the file extension. To override Pygments' default file associations, the ``filetype_parsers`` option can be used to specify a ``dict`` mapping any of the file extensions in ``example_extensions`` to any of the `pygments language names `__. For example:: sphinx_gallery_conf = { ... 'filetype_parsers': {'.m': 'Matlab'} } .. _run_stale_examples: Rerunning stale examples ======================== By default, Sphinx-Gallery only rebuilds examples that have changed. For example, when starting from a clean ``doc/`` directory, running your HTML build once will result in Sphinx-Gallery executing all examples that match your given :ref:`filename/ignore patterns `. Then, running the exact same command a second time *should not run any examples*, because the MD5 hash of each example will be checked against the MD5 hash (saved to disk as ``.md5`` in the generated directory) that the example file had during the first build. These will match and thus the example will be determined to be "stale", and it will not be rebuilt by Sphinx-Gallery. This design feature allows for more rapid documentation iteration by only rebuilding examples when they change. However, this presents a problem during some modes of debugging and iteration. Let's say that you have one particular example that you want to rebuild repeatedly while modifying some function in your underlying library but do not want to change the example file contents themselves. To do this, you'd either need to make some change (e.g., add/delete a newline) to your example or delete the ``.md5`` file to force Sphinx-Gallery to rebuild the example. Instead, you can use the configuration value:: sphinx_gallery_conf = = { ... 'run_stale_examples': True, } With this configuration, all examples matching the filename/ignore pattern will be rebuilt, even if their MD5 hash shows that the example did not change. You can combine this with :ref:`filename/ignore patterns ` to repeatedly rerun a single example. This could be done from the command line, for example: .. code-block:: console $ make html SPHINXOPTS="-D sphinx_gallery_conf.run_stale_examples=True -D sphinx_gallery_conf.filename_pattern='my_example_name'" This command will cause any examples matching the filename pattern ``'my_example_name'`` to be rebuilt, regardless of their MD5 hashes. .. _reset_argv: Passing command line arguments to example scripts ================================================= By default, Sphinx-Gallery will not pass any command line arguments to example scripts. By setting the ``reset_argv`` option, it is possible to change this behavior and pass command line arguments to example scripts. ``reset_argv`` needs to be a ``Callable`` that accepts the ``gallery_conf`` and ``script_vars`` dictionaries as input and returns a list of strings that are passed as additional command line arguments to the interpreter. A ``reset_argv`` example could be:: from pathlib import Path def reset_argv(sphinx_gallery_conf, script_vars): src_file = Path(script_vars['src_file']).name if src_file == 'example1.py': return ['-a', '1'] elif src_file == 'example2.py': return ['-a', '2'] else: return [] This function is defined in ``doc/sphinxext.py`` and we ensured that it is importable (see :ref:`importing_callables`). This can be included in the configuration dictionary as:: sphinx_gallery_conf = { ... 'reset_argv': "sphinxext.reset_argv", } which is then resolved by Sphinx-Gallery to the callable ``reset_argv`` and used as:: import sys sys.argv[0] = script_vars['src_file'] sys.argv[1:] = reset_argv(gallery_conf, script_vars) .. note:: For backwards compatibility you can also set your configuration to be a callable object but you will have to ensure that the ``__repr__`` is stable across runs. See :ref:`stable_repr`. .. _sub_gallery_order: Sorting gallery subsections =========================== Gallery subsections are sorted by default alphabetically by their folder name, and as such you can always organize them by changing your folder names. Alternatively, you can specify the order via the config value 'subsection_order' by providing a list of the subsections as paths relative to :file:`conf.py` in the desired order:: sphinx_gallery_conf = { ... 'examples_dirs': ['../examples','../tutorials'], 'subsection_order': ['../examples/sin_func', '../examples/no_output', '../tutorials/seaborn'], } Here we build 2 main galleries `examples` and `tutorials`, each of them with subsections. You must list all subsections. If that's too cumbersome, one entry can be "*", which will collect all not-listed subsections, e.g. ``["first_subsection", "*", "last_subsection"]``. Even more generally, you can set 'subsection_order' to any callable, which will be used as sorting key function on the subsection paths. See :ref:`own_sort_keys` for more information. In fact, the above list is a convenience shortcut and it is internally wrapped in :class:`sphinx_gallery.sorting.ExplicitOrder` as a sortkey. .. note:: Sphinx-Gallery <0.16.0 required to wrap the list in :class:`.ExplicitOrder` :: from sphinx_gallery.sorting import ExplicitOrder sphinx_gallery_conf = { ... 'subsection_order': ExplicitOrder([...]) } This pattern is discouraged in favor of passing the simple list. Keep in mind that we use a single sort key for all the galleries that are built, thus we include the prefix of each gallery in the corresponding subsection folders. One does not define a sortkey per gallery. You can use Linux paths, and if your documentation is built in a Windows system, paths will be transformed to work accordingly, the converse does not hold. .. _within_gallery_order: Sorting gallery examples ======================== Within a given gallery (sub)section, the example files are ordered by using the standard :func:`sorted` function with the ``key`` argument by default set to :class:`NumberOfCodeLinesSortKey(src_dir) `, which sorts the files based on the number of code lines:: sphinx_gallery_conf = { ... 'within_subsection_order': "NumberOfCodeLinesSortKey", } Built in convenience classes supported by ``within_subsection_order``: - :class:`sphinx_gallery.sorting.NumberOfCodeLinesSortKey` (default) to sort by the number of code lines. - :class:`sphinx_gallery.sorting.FileSizeSortKey` to sort by file size. - :class:`sphinx_gallery.sorting.FileNameSortKey` to sort by file name. - :class:`sphinx_gallery.sorting.ExampleTitleSortKey` to sort by example title. .. note:: These built in Sphinx-Gallery classes can be specified using just the stem, e.g., ``"NumberOfLinesSortKey"``. It is functionally equivalent to providing the fully qualified name ``"sphinx_gallery.sorting.NumberOfCodeLinesSortKey"``. See :ref:`importing_callables` for details. .. _own_sort_keys: Custom sort keys ================ You can create a custom sort key callable for the following configurations: * :ref:`subsection_order ` * :ref:`minigallery_sort_order ` The best way to do this is to define a sort function, that takes the passed path string:: def plotted_sorter(fname): return not fname.startswith("plot_"), fname ensure it is importable (see :ref:`importing_callables`) and set your configuration:: sphinx_gallery_conf = { #..., "minigallery_sort_order": "sphinxext.plotted_sorter", #... } For backwards compatibility you can also set your configuration to be a callable object but you will have to ensure that the ``__repr__`` is stable across runs. See :ref:`stable_repr` for details. We recommend that you use the :class:`sphinx_gallery.sorting.FunctionSortKey` because it will ensure that the ``__repr__`` is stable across runs. :class:`sphinx_gallery.sorting.FunctionSortKey` takes a function on init. You can create your sort key callable by instantiating a :class:`~sphinx_gallery.sorting.FunctionSortKey` instance with your sort key function. For example, the following ``minigallery_sort_order`` configuration (which sorts on paths) will sort using the first 10 letters of each filename: .. code-block:: python sphinx_gallery_conf = { #..., "minigallery_sort_order": FunctionSortKey( lambda filename: filename[:10]), #... } .. _link_to_documentation: Add intersphinx links to your examples ====================================== Sphinx-Gallery enables you to add hyperlinks in your example scripts so that you can link used functions/methods/attributes/objects/classes to their matching online documentation. Such code snippets within the gallery appear like this: .. raw:: html
    y = np.sin(x)
    
Have a look at this in full action in our example :ref:`sphx_glr_auto_examples_plot_0_sin.py`. To make this work in your documentation you need to include to the configuration dictionary within your Sphinx ``conf.py`` file:: sphinx_gallery_conf = { ... 'reference_url': { # The module you locally document uses None 'sphinx_gallery': None, } } To link to external modules, if you use the Sphinx extension :mod:`sphinx.ext.intersphinx`, no additional changes are necessary, as the ``intersphinx`` inventory will automatically be used. If you do not use ``intersphinx``, then you should add entries that point to the directory containing ``searchindex.js``, such as ``'matplotlib': 'https://matplotlib.org'``. If you wish to do the same for ordinary reST documentation, see :ref:`plain_rst`. Resolving module paths ^^^^^^^^^^^^^^^^^^^^^^ When finding links to objects we use, by default, the shortest module path, checking that it still directs to the same object. This is because it is common for a class that is defined in a deeper module to be documented in a shallower one because it is imported in a higher level modules' ``__init__.py`` (thus that's the namespace users expect it to be). However, if you are using inherited classes in your code and are experiencing incorrect links in the sense that links point to the base class of an object instead of the child, the option ``prefer_full_module`` might solve your issue. See `the GitHub issue `__ for more context. To make this work in your documentation you need to include ``prefer_full_module`` in the Sphinx-Gallery configuration dictionary in ``conf.py``:: sphinx_gallery_conf = { ... # Regexes to match the fully qualified names of objects where the full # module name should be used. To use full names for all objects use: '.*' 'prefer_full_module': {r'module\.submodule'} } In the above example, all fully qualified names matching the regex ``'module\.submodule'`` would use the full module name (e.g., module.submodule.meth) when creating links, instead of the short module name (e.g., module.meth). All others will use the (default) way of linking. .. _minigalleries_to_examples: Add mini-galleries ================== Sphinx-Gallery provides the :class:`sphinx_gallery.directives.MiniGallery` directive so that you can easily add a reduced version of the Gallery to your Sphinx documentation ``.rst`` files. The mini-gallery directive therefore supports passing a list (space separated) of any of the following: * full qualified name of object (see :ref:`references_to_examples`) * pathlike strings to example Python files, including glob-style (see :ref:`file_based_minigalleries`) .. _references_to_examples: Add mini-galleries for API documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When documenting a given function/method/attribute/object/class, Sphinx-Gallery enables you to link to any examples that either: 1. Use the function/method/attribute/object or instantiate the class in the code (generates *implicit backreferences*). 2. Refer to that function/method/attribute/object/class using sphinx markup ``:func:`` / ``:meth:`` / ``:attr:`` / ``:obj:`` / ``:class:`` in a text block. You can omit this role markup if you have set the `default_role `_ in your ``conf.py`` to any of these roles (generates *explicit backreferences*). The former is useful for auto-documenting functions/methods/attributes/objects that are used and classes that are explicitly instantiated. The generated links are called implicit backreferences. The latter is useful for classes that are typically implicitly returned rather than explicitly instantiated (e.g., :class:`matplotlib.axes.Axes` which is most often instantiated only indirectly within function calls). Such links are called explicit backreferences. For example, we can embed a small gallery of all examples that use or refer to :obj:`numpy.exp`, which looks like this: .. minigallery:: numpy.exp :add-heading: For such behavior to be available, you have to activate it in your Sphinx-Gallery configuration ``conf.py`` file with:: sphinx_gallery_conf = { ... # directory where function/class granular galleries are stored 'backreferences_dir' : 'gen_modules/backreferences', # Modules for which function/class level galleries are created. In # this case sphinx_gallery and numpy in a tuple of strings. 'doc_module' : ('sphinx_gallery', 'numpy'), # Regexes to match objects to exclude from implicit backreferences. # The default option is an empty set, i.e. exclude nothing. # To exclude everything, use: '.*' 'exclude_implicit_doc': {r'pyplot\.show'}, } The path you specify in ``backreferences_dir`` (here we choose ``gen_modules/backreferences``) will be populated with ReStructuredText files. Each .rst file will contain a reduced version of the gallery specific to every function/class that is used across all the examples and belonging to the modules listed in ``doc_module``. ``backreferences_dir`` should be a string or ``pathlib.Path`` object that is **relative** to the ``conf.py`` file, or ``None``. It is ``None`` by default. Sometimes, there are functions that are being used in practically every example for the given module, for instance the ``pyplot.show`` or ``pyplot.subplots`` functions in Matplotlib, so that a large number of often spurious examples will be linked to these functions. To prevent this, you can exclude implicit backreferences for certain objects by including them as regular expressions in ``exclude_implicit_doc``. The following setting will exclude any implicit backreferences so that examples galleries are only created for objects explicitly mentioned by Sphinx markup in a documentation block: ``{'.*'}``. To exclude the functions mentioned above you would use ``{r'pyplot\.show', r'pyplot\.subplots'}`` (note the escape to match a dot instead of any character, if the name is unambiguous you can also write ``pyplot.show`` or just ``show``). .. _file_based_minigalleries: Create mini-galleries using file paths ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you may want to explicitly create a :class:`mini-gallery ` using files that do not have functions in common, for example a set of tutorials. The mini-gallery directive therefore also supports passing in: * pathlike strings to sphinx gallery example files (relative to ``conf.py``) * glob-style pathlike strings to Sphinx-Gallery example files (relative to ``conf.py``) For example, the rst below adds the reduced version of the Gallery for all examples that use the specific function ``numpy.exp``, the example ``examples/plot_sin_.py``, and all files matching the string ``/examples/plot_4*``: .. code-block:: rst .. minigallery:: :add-heading: numpy.exp ../examples/plot_0_sin.py ../examples/plot_4* Listing multiple items merges all the examples into a single mini-gallery. The mini-gallery will only be shown if the files exist or the items are actually used or referred to in an example. Thumbnails may also be duplicated if they correspond to multiple object names or an object name and file/glob input. .. minigallery:: :add-heading: numpy.exp ../examples/plot_0_sin.py ../examples/plot_4* You can also provide the list of items in the body of the directive: .. code-block:: rst .. minigallery:: :add-heading: numpy.exp ../examples/plot_0_sin.py ../examples/plot_4* The ``add-heading`` option adds a heading for the mini-gallery. If no string argument is provided, when only a single item is listed the default heading is: "Examples using *{full qualified object name}*" Specifying a custom heading message is recommended for a gallery with multiple items because otherwise the default message is: "Examples of one of multiple objects". The example mini-gallery shown above uses the default heading level ``^``. This can be changed using the ``heading-level`` option, which accepts a single character (e.g., ``-``). You can also pass inputs to the minigallery directive as a space separated list of arguments: .. code-block:: rst .. minigallery:: numpy.exp ../examples/plot_0_sin.py ../examples/plot_4* .. _minigallery_order: Sort mini-gallery thumbnails from files """"""""""""""""""""""""""""""""""""""" The :ref:`minigallery ` directive generates a gallery of thumbnails corresponding to the input file strings or object names. You can specify minigallery thumbnails order via the ``minigallery_sort_order`` configuration, which gets passed to the :py:func:`sorted` ``key`` parameter when sorting all minigalleries. Sorting uses the paths to the gallery examples (e.g., ``path/to/plot_example.py``) and backreferences (e.g., ``path/to/numpy.exp.examples``) corresponding to the inputs. See :ref:`own_sort_keys` for details on writing a custom sort key. As an example, to put backreference thumbnails at the end, we could define the function below in ``doc/sphinxext.py`` (note that backreference filenames do not start with "plot\_" and ``False`` gets sorted ahead of ``True`` as 0 is less than 1):: def function_sorter(x) return (not os.path.basename(x).startswith("plot_"), x):: We can then set the configuration to be (ensuring the function is :ref:`importable `):: sphinx_gallery_conf = { #..., "minigallery_sort_order": "sphinxext.function_sorter", #... } Sphinx-Gallery would resolve ``"sphinxext.function_sorter"`` to the ``function_sorter`` object. Sorting the set of thumbnails generated from :ref:`API backreferences ` (i.e. the thumbnails linked to a qualified object name input) is not supported, but the sorting function can be used to position the entire set of backreference thumbnails since minigallery sort gets passed the ``{input}.example`` backreference filename. Thumbnails may be duplicated if they correspond to multiple object names or an object name and file/glob input. Auto-documenting your API with links to examples ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The previous feature can be automated for all your modules combining it with the standard sphinx extensions `autodoc `_ and `autosummary `_. First enable them in your ``conf.py`` extensions list:: import sphinx_gallery extensions = [ ... 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx_gallery.gen_gallery', ] # generate autosummary even if no references autosummary_generate = True `autodoc `_ and `autosummary `_ are very powerful extensions, please read about them. In this example we'll explain how the :ref:`sphx_glr_api_reference` is automatically generated. The documentation is done at the module level. We first start with the ``reference.rst`` file .. literalinclude:: reference.rst :language: rst The important directives are ``currentmodule`` where we specify which module we are documenting, for our purpose is ``sphinx_gallery``. The ``autosummary`` directive is responsible for generating the ``rst`` files documenting each module. ``autosummary`` takes the option *toctree* which is where the ``rst`` files are saved and *template* which is the file that describes how the module ``rst`` documentation file is to be constructed, finally we write the modules we wish to document, in this case all modules of Sphinx-Gallery. The template file ``module.rst`` for the ``autosummary`` directive has to be saved in the path ``_templates/module.rst``. We present our configuration in the following block. The most relevant part is the loop defined between lines **12-21** that parses all the functions/classes of the module. There we have used the ``minigallery`` directive introduced in the previous section. We also add a cross referencing label (on line 16) before including the examples mini-gallery. This enables you to reference the mini-gallery for all functions/classes of the module using ``:ref:`sphx_glr_backref_```, where '' is the full path to the function/class using dot notation (e.g., ``sphinx_gallery.backreferences.identify_names``). For example, see: :ref:`sphx_glr_backref_sphinx_gallery.backreferences.identify_names`. .. literalinclude:: _templates/module.rst :language: rst :lines: 3- :emphasize-lines: 12-21, 31-38 :linenos: Toggling global variable inspection ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, Sphinx-Gallery will inspect global variables (and code objects) at the end of each code block to try to find classes of variables and method calls. It also tries to find methods called on classes. For example, this code:: lst = [1, 2] fig, ax = plt.subplots() ax.plot(lst) should end up with the following links (assuming intersphinx is set up properly): - :class:`lst ` - :func:`plt.subplots ` - :class:`fig ` - :class:`ax ` - :meth:`ax.plot ` However, this feature might not work properly in all instances. Moreover, if variable names get reused in the same script to refer to different classes, it will break. To disable this global variable introspection, you can use the configuration key:: sphinx_gallery_conf = { ... 'inspect_global_variables' : False, } .. _stylizing_code_links: Stylizing code links using CSS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each link in the code blocks will be decorated with two or three CSS classes. 1. ``sphx-glr-backref-module-*`` CSS class named after the module where the object is documented. ``*`` represents the module, e.g., ``sphx-glr-backref-module-matplotlib-figure``. 2. ``sphx-glr-backref-type-*`` CSS class named after the type of the object, where ``*`` represents the object type. This is a sanitized intersphinx type, e.g., a ``py:class`` will have the CSS class ``sphx-glr-backref-type-py-class``. 3. ``sphx-glr-backref-instance`` The third 'optional' class that is added only if the object is an instance of a class (rather than, e.g., a class itself, method, or function). By default, Sphinx-Gallery adds the following CSS in ``gallery.css``: .. code-block:: css a.sphx-glr-backref-instance { text-decoration: none; } This is done to reduce the visual impact of instance linking in example code. This means that for the following code:: x = Figure() ``x``, an instance of a class, will have the ``sphx-glr-backref-instance`` CSS class, and will not be decorated. ``Figure`` however, is a class, so will not have the ``sphx-glr-backref-instance`` CSS class, and will thus be decorated the standard way for links in the given parent styles. These three CSS classes are meant to give fine-grained control over how different links are decorated. For example, using CSS selectors you could choose to avoid highlighting any ``sphx-glr-backref-*`` links except for ones that you allowlist (e.g., those from your own module). For example, the following css prevents any module except for matplotlib from being decorated: .. code-block:: css a[class^="sphx-glr-backref-module-"] { text-decoration: none; } a[class^="sphx-glr-backref-module-matplotlib"] { text-decoration: underline; } There are likely elements other than ``text-decoration`` that might be worth setting, as well. You can add these CSS classes by including your own CSS file via the Sphinx configuration :confval:`sphinx:html_static_path`, which will override the default CSS classes in `Sphinx-Gallery CSS files `_. .. _custom_default_thumb: Using a custom default thumbnail ================================ In case you want to use your own image for the thumbnail of examples that do not generate any plot, you can specify it by editing your Sphinx ``conf.py`` file. You need to add to the configuration dictionary a key called `default_thumb_file`. For example:: sphinx_gallery_conf = { ... 'default_thumb_file': 'path/to/thumb/file.png', } .. _adding_line_numbers: Adding line numbers to examples =============================== Line numbers can be displayed in listings by adding the global ``line_numbers`` setting:: sphinx_gallery_conf = { ... 'line_numbers': True, } or by adding a comment to the example script, which overrides any global setting:: # sphinx_gallery_line_numbers = True .. _removing_config_comments: Removing config comments ======================== Some configurations can be specified within a file by adding a special comment with the pattern :samp:`# sphinx_gallery_{config} [= {value}]` to the example source files. By default, the source files are parsed as is and thus the comment will appear in the example. To remove the comment from the rendered example set the option:: sphinx_gallery_conf = { ... 'remove_config_comments': True, } This only removes configuration comments from code blocks, not from text blocks. However, note that technically, file-level configuration comments will work when put in either code blocks or text blocks. .. _own_notebook_cell: Add your own first and last notebook cell ========================================= Sphinx-Gallery allows you to add your own first and/or last cell to *every* generated notebook. Adding a first cell can be useful for including code that is required to run properly in the notebook, but not in a ``.py`` file. By default, no first cell is added. Adding a last cell can be useful for performing a desired action such as reporting on the user's environment. By default no last cell is added. You can choose whatever text you like by modifying the ``first_notebook_cell`` and ``last_notebook_cell`` configuration parameters. For example, you can add the following first cell: .. code-block:: ipython # This cell is added by Sphinx-Gallery # It can be customized to whatever you like Which is achieved by the following configuration:: sphinx_gallery_conf = { ... 'first_notebook_cell': ("# This cell is added by Sphinx-Gallery\n" "# It can be customized to whatever you like\n" ) } A last cell may be added similarly by setting the ``last_notebook_cell`` parameter:: sphinx_gallery_conf = { ... 'first_notebook_cell': ("# This cell is added by Sphinx-Gallery\n" "# It can be customized to whatever you like\n" ), 'last_notebook_cell': "# This is the last cell", } If the value of ``first_notebook_cell`` or ``last_notebook_cell`` is set to ``None``, then no extra first or last cell will be added to the notebook. .. _notebook_images: Adding images to notebooks ========================== When notebooks are produced, by default (``notebook_images = False``) image paths from the `image` directive in reST documentation blocks (not images generated from code) are included in markdown using their original paths. This includes paths to images expected to be present on the local filesystem which is unlikely to be the case for those downloading the notebook. By setting ``notebook_images = True``, images will be embedded in the generated notebooks via Base64-encoded `data URIs `_. As inclusion of images via data URIs can significantly increase size of the notebook, it's suggested this only be used when small images are used throughout galleries. An alternative is to instead provide a prefix string that'll be used for images e.g. the root URL of where your documentation is hosted. So for example the following configuration:: sphinx_gallery_conf = { ... 'examples_dirs': ['../examples'], 'gallery_dirs': ['auto_examples'], ... 'notebook_images': 'https://project.example.com/en/latest/', ... } with an example `image` directive in an reST documentation block being: .. code-block:: rst .. image:: ../_static/example.jpg :alt: An example image The image will be added to the generated notebook pointing to the source URL ``https://project.example.com/en/latest/_static/example.jpg``. Note the image path in the reST examples above is a relative path, therefore the URL doesn't contain ``auto_examples`` as ``../`` moved up a directory to the documentation source directory. Both relative and absolute (from source directory) paths are supported; so in the example above ``/_static/example.jpg`` would have resulted in the same URL being produced. Note that the prefix is applied directly, so a trailing ``/`` should be included in the prefix if it's required. .. tip:: If building multiple versions of your documentation on a hosted service and using prefix, consider using `sphinx build -D `_ command line option to ensure links point to the correct version. For example: .. code-block:: sh sphinx-build \ -b html \ -D sphinx_gallery_conf.notebook_images="https://project.example.com/docs/${VERSION}/" \ source_dir build_dir .. _use_pypandoc: Using pypandoc to convert reST to markdown ========================================== Sphinx-Gallery can use `pypandoc `_ (if installed) to convert reST text blocks to markdown for the iPython notebooks (``.ipynb`` files) generated for each example. These are made available for download, along with the raw ``.py`` version, at the bottom of each example. The Sphinx-Gallery reST to markdown converter has limited support for more complex reST syntax. If your examples have more complex reST, ``pypandoc`` may produce better results. By default, the 'pypandoc' configuration is set to ``False`` and ``pypandoc`` is not used. To use ``pypandoc`` you can set:: sphinx_gallery_conf = { ... 'pypandoc': True, } You can also use pandoc options by setting the ``pypandoc.convert_text()`` parameters ``extra_args`` and ``filters``. To use these parameters, set the 'pypandoc' configuration to be a dictionary of keyword argument(s):: sphinx_gallery_conf = { ... 'pypandoc': {'extra_args': ['--mathjax',], 'filters': ['pandoc-citeproc',], } .. warning:: Certain pandoc options may result in undesirable effects. Use with caution. .. _junit_xml: Using JUnit XML files ===================== Sphinx-Gallery can create a JUnit XML file of your example run times, successes, and failures. To create a file named e.g. ``junit-result.xml`` in the ``/build`` output directory, set the configuration key (path is relative to the HTML output directory):: sphinx_gallery_conf = { ... 'junit': '../test-results/sphinx-gallery/junit.xml', } By default, JUnit XML file generation is disabled (by setting ``'junit': ''``). JUnit XML files are useful for example on CircleCI builds, where you can add a line like this to get a summary of your example run times in the CircleCI GUI (which will parse the file path ``doc/_build/test-results/sphinx-gallery/junit.xml`` and infer the tests came from ``sphinx-gallery`` based on the nested subdirectory name): .. code-block:: yaml - store_test_results: path: doc/_build/test-results - store_artifacts: path: doc/_build/test-results For more information on CircleCI integration, peruse the related `CircleCI doc `__ and `blog post `__. .. _log_level: Setting log level ================= Sphinx-Gallery logs output at several stages. Warnings can be generated for code that requires case sensitivity (e.g., ``plt.subplot`` and ``plt.Subplot``) when building docs on a filesystem that does not support case sensitive naming (e.g., Windows). In this case, by default a ``logger.warning`` is emitted, which will lead to a build failure when building with ``-W``. The log level can be set with:: sphinx_gallery_conf = { ... 'log_level': {'backreference_missing': 'warning'}, } The only valid key currently is ``backreference_missing``. The valid values are ``'debug'``, ``'info'``, ``'warning'``, and ``'error'``. .. _disable_all_scripts_download: Disabling download button of all scripts ======================================== By default Sphinx-Gallery collects all python scripts and all Jupyter notebooks from each gallery into zip files which are made available for download at the bottom of each gallery. To disable this behavior add to the configuration dictionary in your ``conf.py`` file:: sphinx_gallery_conf = { ... 'download_all_examples': False, } .. _choosing_thumbnail: Choosing the thumbnail image ============================ For examples that generate multiple figures, the default behavior will use the first figure created in each as the thumbnail image displayed in the gallery. To change the thumbnail image to a figure generated later in an example script, add a comment to the example script to specify the number of the figure you would like to use as the thumbnail. For example, to use the 2nd figure created as the thumbnail:: # sphinx_gallery_thumbnail_number = 2 You can also use negative numbers, which counts from the last figure. For example -1 means using the last figure created in the example as the thumbnail:: # sphinx_gallery_thumbnail_number = -1 The default behavior is ``sphinx_gallery_thumbnail_number = 1``. See :ref:`sphx_glr_auto_examples_plot_4_choose_thumbnail.py` for an example of this functionality. .. _providing_thumbnail: Providing an image for the thumbnail image ========================================== An arbitrary image can be used to serve as the thumbnail image for an example. To specify an image to serve as the thumbnail, add a comment to the example script specifying the path to the desired image. The path to the image should be relative to the ``conf.py`` file and the comment should be somewhere below the docstring (ideally in a code block, see :ref:`removing_config_comments`). For example, the following defines that the image ``demo.png`` in the folder ``_static/`` should be used to create the thumbnail:: # sphinx_gallery_thumbnail_path = '_static/demo.png' Note that ``sphinx_gallery_thumbnail_number`` overrules ``sphinx_gallery_thumbnail_path``. See :ref:`sphx_glr_auto_examples_plot_4b_provide_thumbnail.py` for an example of this functionality. .. _binder_links: Generate Binder links for gallery notebooks (experimental) ========================================================== Sphinx-Gallery automatically generates Jupyter notebooks for any examples built with the gallery. `Binder `_ makes it possible to create interactive GitHub repositories that connect to cloud resources. If you host your documentation on a GitHub repository, it is possible to auto-generate a Binder link for each notebook. Clicking this link will take users to a live version of the Jupyter notebook where they may run the code interactively. For more information see the `Binder documentation `__. .. warning:: Binder is still beta technology, so there may be instability in the experience of users who click Binder links. In order to enable Binder links with Sphinx-Gallery, you must specify a few pieces of information in ``conf.py``. These are given as a nested dictionary following the pattern below:: sphinx_gallery_conf = { ... 'binder': { # Required keys 'org': '', 'repo': '', 'branch': '', # Can be any branch, tag, or commit hash. Use a branch that hosts your docs. 'binderhub_url': '', # Any URL of a binderhub deployment. Must be full URL (e.g. https://mybinder.org). 'dependencies': '', # Optional keys 'filepath_prefix': '' # A prefix to prepend to any filepaths in Binder links. 'notebooks_dir': '' # Jupyter notebooks for Binder will be copied to this directory (relative to built documentation root). 'use_jupyter_lab': # Whether Binder links should start Jupyter Lab instead of the Jupyter Notebook interface. } } If a Sphinx-Gallery configuration for Binder is discovered, the following extra things will happen: 1. The dependency files specified in ``dependencies`` will be copied to a ``binder/`` folder in your built documentation. 2. The built Jupyter Notebooks from the documentation will be copied to a folder called ```` at the root of your built documentation (they will follow the same folder hierarchy within the notebooks directory folder. 3. The reST output of each Sphinx-Gallery example will now have a ``launch binder`` button in it. 4. That button will point to a binder link with the following structure .. code-block:: html /v2/gh///?filepath=//path/to/notebook.ipynb Below is a more complete explanation of each field. org (type: string) The GitHub organization where your documentation is stored. repo (type: string) The GitHub repository where your documentation is stored. branch (type: string) A reference to the version of your repository where your documentation exists. For example, if your built documentation is stored on a ``gh-pages`` branch, then this field should be set to ``gh-pages``. binderhub_url (type: string) The full URL to a BinderHub deployment where you want your examples to run. One public BinderHub deployment is at ``https://mybinder.org``, though if you (and your users) have access to another, this can be configured with this field. dependencies (type: list) A list of paths (relative to ``conf.py``) to dependency files that Binder uses to infer the environment needed to run your examples. For example, a ``requirements.txt`` file. These will be copied into a folder called ``binder/`` in your built documentation folder. For a list of all the possible dependency files you can use, see `the Binder configuration documentation `_. filepath_prefix (type: string | None, default: ``None``) A prefix to append to the filepath in the Binder links. You should use this if you will store your built documentation in a sub-folder of a repository, instead of in the root. notebooks_dir (type: string, default: ``notebooks``) The name of a folder where the built Jupyter notebooks will be copied. This ensures that all the notebooks are in one place (though they retain their folder hierarchy) in case you'd like users to browse multiple notebook examples in one session. use_jupyter_lab (type: bool, default: ``False``) Whether the default interface activated by the Binder link will be for Jupyter Lab or the classic Jupyter Notebook interface. Each generated Jupyter Notebook will be copied to the folder specified in ``notebooks_dir``. This will be a subfolder of the sphinx output directory and included with your site build. Binder links will point to these notebooks. .. note:: It is not currently possible to host notebooks generated by Sphinx-Gallery with readthedocs.org, as RTD does not provide you with a GitHub repository you could link Binder to. If you'd like to use readthedocs with Sphinx-Gallery and Binder links, you should independently build your documentation and host it on a GitHub branch as well as building it with readthedocs. See the Sphinx-Gallery `Sphinx configuration file `_ for an example that uses the `public Binder server `_. .. _jupyterlite: Generate JupyterLite links for gallery notebooks (experimental) =============================================================== Sphinx-Gallery automatically generates Jupyter notebooks for any examples built with the gallery. `JupyterLite `__ makes it possible to run an example in your browser. The functionality is quite similar to Binder in the sense that you will get a Jupyter environment where you can run the example interactively as a notebook. The main difference with Binder are: - with JupyterLite, the example actually runs in your browser, there is no need for a separate machine in the cloud to run your Python code. That means that starting a Jupyter server is generally quicker, no need to wait for the Binder image to be built - with JupyterLite the first imports take time. At the time of writing (February 2023) ``import scipy`` can take ~15-30s. Some innocuously looking Python code may just not work and break in an unexpected fashion. The Jupyter kernel is based on Pyodide, see `here `__ for some Pyodide limitations. - with JupyterLite environments are not as flexible as Binder, for example you can not use a docker image but only the default `Pyodide `__ environment. That means that some non pure-Python packages may not be available, see list of `available packages in Pyodide `__. .. warning:: JupyterLite is still beta technology and less mature than Binder, so there may be instability or unexpected behaviour in the experience of users who click JupyterLite links. In order to enable JupyterLite links with Sphinx-Gallery, you need to install the `jupyterlite-sphinx `_ package. For `jupyterlite-sphinx>=0.8` (released 15 March 2023) you also need to install `jupyterlite-pyodide-kernel`. You then need to add `jupyterlite_sphinx` to your Sphinx extensions in ``conf.py``:: extensions = [ ..., 'jupyterlite_sphinx', ] You can configure JupyterLite integration by setting ``sphinx_gallery_conf['jupyterlite']`` in ``conf.py`` like this:: sphinx_gallery_conf = { ... 'jupyterlite': { 'use_jupyter_lab': , # Whether JupyterLite links should start Jupyter Lab instead of the Retrolab Notebook interface. 'notebook_modification_function': , # fully qualified name of a function that implements JupyterLite-specific modifications of notebooks 'jupyterlite_contents': , # where to copy the example notebooks (relative to Sphinx source directory) } } Below is a more complete explanation of each field. use_jupyter_lab (type: bool, default: ``True``) Whether the default interface activated by the JupyterLite link will be for Jupyter Lab or the RetroLab Notebook interface. notebook_modification_function (type: str, default: ``None``) Fully qualified name of a function that implements JupyterLite-specific modifications of notebooks. By default, it is ``None`` which means that notebooks are not going to be modified. Its signature should be ``notebook_modification_function(json_dict: dict, notebook_filename: str) -> None`` where ``json_dict`` is what you get when you do ``json.load(open(notebook_filename))``. The function is expected to modify ``json_dict`` in place by adding notebook cells. It is not expected to write to the file, since ``sphinx-gallery`` is in charge of this. ``notebook_filename`` is provided for convenience because it is useful to modify the notebook based on its filename. Potential usages of this function are installing additional packages with a ``%pip install seaborn`` code cell, or adding a markdown cell to indicate that a notebook is not expected to work inside JupyterLite, for example because it is using packages that are not packaged inside Pyodide. For backward compatibility it can also be a callable but this will not be cached properly as part of the environment by Sphinx. jupyterlite_contents (type: string, default: ``jupyterlite_contents``) The name of a folder where the built Jupyter notebooks will be copied, relative to the Sphinx source directory. This is used as Jupyterlite contents. You can set variables in ``conf.py`` to configure ``jupyterlite-sphinx``, see the `jupyterlite-sphinx doc `__ for more details. If a Sphinx-Gallery configuration for JupyterLite is discovered, the following extra things will happen: 1. Configure ``jupyterlite-sphinx`` with some reasonable defaults, e.g. set ``jupyterlite_bind_ipynb_suffix = False``. 2. The built Jupyter Notebooks from the documentation will be copied to a folder called ``/`` (relative to Sphinx source directory) 3. If ``notebook_modification_function`` is not ``None``, this function is going to add JupyterLite-specific modifications to notebooks 4. The reST output of each Sphinx-Gallery example will now have a ``launch JupyterLite`` button in it. 5. That button will point to a JupyterLite link which will start a Jupyter server in your browser with the current example as notebook If, for some reason, you want to enable the ``jupyterlite-sphinx`` extension but not use Sphinx-Gallery Jupyterlite integration you can do:: extensions = [ ..., jupyterlite_sphinx, ] sphinx_gallery_conf = { ... 'jupyterlite': None } See the Sphinx-Gallery `Sphinx configuration file `_ for an example that uses the JupyterLite integration. .. _notebook_extensions: Controlling notebook download links =================================== By default, links to download Jupyter noteooks and launch Binder or JupyterLite (if enabled) are shown only for Python examples. If parsing other file extensions has been enabled (using the ``example_extensions`` option; see :ref:`build_pattern`), notebook downloads can be enabled using the ``notebook_extensions`` option. For example:: sphinx_gallery_conf = { "notebook_extensions": {".py", ".jl"} } where the listed extensions are compared to file names in the gallery directory. .. note:: Currently, all generated notebooks specify Python as the kernel. After downloading, the user will need to manually change to the correct kernel. .. _promote_jupyter_magic: Making cell magic executable in notebooks ========================================= Often times, tutorials will include bash code for the user to copy/paste into their terminal. This code should not be run when someone is building the documentation, as they will already have those dependencies in their environment. Hence they are normally written as code blocks inside text:: #%% # Installing dependencies # # .. code-block:: bash # # pip install -q tensorflow # apt-get -qq install curl This works fine for the ``.py`` and ``.html`` files, but causes problems when rendered as an Jupyter notebook. The downloaded ``.ipynb`` file will not have those dependencies installed, and will not work without running the bash code. To fix this, we can set the ``promote_jupyter_magic`` flag in ``conf.py``:: sphinx_gallery_conf = { ... 'promote_jupyter_magic': True, } If this flag is ``True``, then when a Jupyter notebook is being built, any code block starting with `Jupyter cell magics `_ (e.g. ``%%bash`` or ``%%writefile``) will be turned into a runnable code block. For our earlier example, we could change the Markdown text to:: #%% # Installing dependencies # # .. code-block:: bash # # %%bash # pip install -q tensorflow # apt-get -qq install curl meaning TensorFlow and Curl would be automatically installed upon running the Jupyter notebook. This works for any cell magic (not just those mentioned above) and only affects the creation of Jupyter notebooks. .. warning:: It is good practice to ensure the ``.py`` and ``.html`` files match the ``.ipynb`` files as closely as possible. This functionality should only be used when the relevant code is intended to be executed by the end user. .. _without_execution: Building without executing examples =================================== Sphinx-Gallery can parse all your examples and build the gallery without executing any of the scripts. This is just for speed visualization processes of the gallery and the size it takes your website to display, or any use you can imagine for it. To achieve this you need to pass the no plot option in the build process by modifying your ``Makefile`` with: .. code-block:: Makefile html-noplot: $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(SOURCEDIR) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." Remember that for ``Makefile`` white space is significant and the indentation are tabs and not spaces. Alternatively, you can add the ``plot_gallery`` option to the ``sphinx_gallery_conf`` dictionary inside your ``conf.py`` to have it as a default:: sphinx_gallery_conf = { ... 'plot_gallery': 'False', } The highest precedence is always given to the `-D` flag of the ``sphinx-build`` command. .. note:: If adding ``html-noplot`` to your ``Makefile``, you will also need to explicitly set the default value for ``plot_gallery`` in the ``sphinx_gallery_conf`` dictionary inside your ``conf.py`` file to avoid a sphinx configuration warning. .. _compress_images: Compressing images ================== When writing PNG files (the default scraper format), Sphinx-Gallery can be configured to use ``optipng`` to optimize the PNG file sizes. Typically this yields roughly a 50% reduction in file sizes, thus reducing the loading time of galleries. However, it can increase build time. The allowed values are ``'images'`` and ``'thumbnails'``, or a tuple/list (to optimize both), such as:: sphinx_gallery_conf = { ... 'compress_images': ('images', 'thumbnails'), } The default is ``()`` (no optimization) and a warning will be emitted if optimization is requested but ``optipng`` is not available. You can also pass additional command-line options (starting with ``'-'``), for example to optimize less but speed up the build time you could do:: sphinx_gallery_conf = { ... 'compress_images': ('images', 'thumbnails', '-o1'), } See ``$ optipng --help`` for a complete list of options. .. _image_srcset: Multi-resolution images ======================= Web browsers allow a ``srcset`` parameter to the ```` tag that allows the browser to support `responsive resolution images `__ for hi-dpi/retina displays. Sphinx Gallery supports this via the ``image_srcset`` parameter:: sphinx_gallery_conf = { ... 'image_srcset': ["2x"], } that saves a 1x image at the normal figure dpi (usually 100 dpi) and a 2x version at twice the density (e.g. 200 dpi). The default is no extra images (``'image_srcset': []``), and you can specify other resolutions if desired as a list: ``["2x", "1.5x"]``. The matplotlib scraper creates a custom image directive, ``image-sg`` in the rst file:: .. image-sg:: /examples/images/sphx_glr_test_001.png :alt: test :srcset: /examples/images/sphx_glr_test_001.png, /examples/images/sphx_glr_test_001_2_0x.png 2.0x :class: sphx-glr-single-img This is converted to html by the custom directive as:: .. test` or :class:`matplotlib.animation.FFMpegWriter.isAvailable() ` to check. We recommend FFMpeg writer, unless you are using Matplotlib <3.3.1. The following scrapers are supported: - matplotlib Sphinx-Gallery maintains a scraper for :mod:`matplotlib ` figures via the string ``'matplotlib'``. - PyVista `PyVista `__ maintains a scraper (for PyVista >= 0.20.3) enabled by the string ``'pyvista'``. - PyGMT See `their website `__ for more information on how to integrate with Sphinx-Gallery. - qtgallery This library provides a scraper for Qt windows. See `their repository `_ for instructions on integrating with Sphinx-Gallery. It is possible to write custom scrapers for images generated by packages outside of those listed above. This is accomplished by writing your own Python function to define how to detect and retrieve images produced by an arbitrary package. For instructions, see :ref:`custom_scraper`. If you come up with an implementation that would be useful for general use (e.g., a custom scraper for a plotting library) feel free to add it to the list above (see discussion `here `__)! .. _defer_figures: Using multiple code blocks to create a single figure ==================================================== By default, images are scraped following each code block in an example. Thus, the following produces two plots, with one plot per code block:: # %% # This first code block produces a plot with two lines import matplotlib.pyplot as plt plt.plot([1, 0]) plt.plot([0, 1]) # %% # This second code block produces a plot with one line plt.plot([2, 2]) plt.show() However, sometimes it can be useful to use multiple code blocks to create a single figure, particularly if the figure takes a large number commands that would benefit from being interleaved with text blocks. The optional flag ``sphinx_gallery_defer_figures`` can be inserted as a comment anywhere in a code block to defer the scraping of images to the next code block (where it can be further deferred, if desired). The following produces only one plot:: # %% # This first code block does not produce any plot import matplotlib.pyplot as plt plt.plot([1, 0]) plt.plot([0, 1]) # sphinx_gallery_defer_figures # %% # This second code block produces a plot with three lines plt.plot([2, 2]) plt.show() .. _hiding_code_blocks: Hiding lines of code ==================== Normally, Sphinx-Gallery will render every line of Python code when building HTML and iPython notebooks. This is usually desirable, as we want to ensure the Python source files, HTML, and iPython notebooks all do the same thing. However, it is sometimes useful to have Python code that runs, but is not included in any user-facing documentation. For example, suppose we wanted to add some ``assert`` statements to verify the docs were built successfully, but did not want these shown to users. We could use the ``sphinx_gallery_start_ignore`` and ``sphinx_gallery_end_ignore`` flags to achieve this:: model.compile() # sphinx_gallery_start_ignore assert len(model.layers) == 5 assert model.count_params() == 219058 # sphinx_gallery_end_ignore model.fit() When the HTML or iPython notebooks are built, this code block will be shown as:: model.compile() model.fit() The ``sphinx_gallery_start_ignore`` and ``sphinx_gallery_end_ignore`` flags may be used in any code block, and multiple pairs of flags may be used in the same block. Every start flag must always have a corresponding end flag, or an error will be raised during doc generation. These flags and the code between them are always removed, regardless of what ``remove_config_comments`` is set to. Note that any output from the ignored code will still be captured. .. warning:: This flag should be used sparingly, as it makes the ``.py`` source files less equivalent to the generated ``.html`` and ``.ipynb`` files. It is bad practice to use this when other methods that preserve this relationship are possible. .. _dummy_images: Generating dummy images ======================= For quick visualization of your gallery, especially during the writing process, Sphinx-Gallery allows you to build your gallery without executing the code (see :ref:`without_execution` and :ref:`filename/ignore patterns `). This however, can cause warnings about missing image files if you have manually written links to automatically generated images. To prevent these warnings you can tell Sphinx-Gallery to create a number of dummy images for an example. For example, you may have an example ('my_example.py') that generates 2 figures, which you then reference manually elsewhere, e.g.,: .. code-block:: rst Below is a great figure: .. figure:: ../auto_examples/images/sphx_glr_my_example_001.png Here is another one: .. figure:: ../auto_examples/images/sphx_glr_my_example_002.png To prevent missing image file warnings when building without executing, you can add the following to the example file:: # sphinx_gallery_dummy_images=2 This will cause Sphinx-Gallery to generate 2 dummy images with the same naming convention and stored in the same location as images that would be generated when building with execution. No dummy images will be generated if there are existing images (e.g., from a previous run of the build), so they will not be overwritten. .. note:: This configuration **only** works when the example is set to not execute (i.e., the ``plot_gallery`` is ``'False'``, the example is in `ignore_pattern` or the example is not in ``filename_pattern`` - see :ref:`filename/ignore patterns `). This means that you will not need to remove any ``sphinx_gallery_dummy_images`` lines in your examples when you switch to building your gallery with execution. .. _reset_modules: Resetting modules ================= Often you wish to "reset" the behavior of your visualization packages in order to ensure that any changes made to plotting behavior in one example do not propagate to the other examples. By default, before each example file executes, Sphinx-Gallery will reset ``matplotlib`` (by using :func:`matplotlib.pyplot.rcdefaults` and reloading submodules that populate the units registry) and ``seaborn`` (by trying to unload the module from ``sys.modules``). This is equivalent to the following configuration:: sphinx_gallery_conf = { ... 'reset_modules': ('matplotlib', 'seaborn'), } Currently, Sphinx-Gallery natively supports resetting ``matplotlib`` and ``seaborn``. However, you can also add your own custom function to this tuple in order to define resetting behavior for other visualization libraries. To do so, follow the instructions in :ref:`custom_reset`. .. _reset_modules_order: Order of resetting modules ^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, Sphinx-Gallery will reset modules before each example is run. The choices for ``reset_modules_order`` are ``before`` (default), ``after``, and ``both``. If the last example run in Sphinx-Gallery modifies a module, it is recommended to use ``after`` or ``both`` to avoid leaking out a modified module to other parts of the Sphinx build process. For example, set ``reset_modules_order`` to ``both`` in the configuration:: sphinx_gallery_conf = { ... 'reset_modules_order': 'both', } Custom functions can be constructed to have custom functionality depending on whether they are called before or after the examples. See :ref:`custom_reset` for more information. Dealing with failing Gallery example scripts ============================================ As your project evolves some of your example scripts might stop executing properly. Sphinx-Gallery will assist you in the discovery process of those bugged examples. The default behavior is to replace the thumbnail of those examples in the gallery with the broken thumbnail. That allows you to find with a quick glance of the gallery which examples failed. Broken examples remain accessible in the html view of the gallery and the traceback message is written for the failing code block. Refer to example :ref:`sphx_glr_auto_examples_no_output_plot_raise.py` to view the default behavior. The build is also failed exiting with code 1 and giving you a summary of the failed examples with their respective traceback. This way you are aware of failing examples right after the build and can find them easily. There are some additional options at your hand to deal with broken examples. .. _abort_on_first: Abort build on first fail ^^^^^^^^^^^^^^^^^^^^^^^^^ Sphinx-Gallery provides the early fail option. In this mode the gallery build process breaks as soon as an exception occurs in the execution of the examples scripts. To activate this behavior you need to pass a flag at the build process. It can be done by including in your ``Makefile``: .. code-block:: makefile html_abort_on_example_error: $(SPHINXBUILD) -D abort_on_example_error=1 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." Remember that for ``Makefile`` white space is significant and the indentation are tabs and not spaces. Alternatively, you can add the ``abort_on_example_error`` option to the ``sphinx_gallery_conf`` dictionary inside your ``conf.py`` configuration file to have it as a default:: sphinx_gallery_conf = { ... 'abort_on_example_error': True, } The highest precedence is always given to the `-D` flag of the ``sphinx-build`` command. .. _dont_fail_exit: Don't fail the build if specific examples error ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It might be the case that you want to keep the gallery even with failed examples. Thus you can configure Sphinx-Gallery to allow certain examples to fail and still exit with a 0 exit code. For this you need to list all the examples you want to allow to fail during build. Change your `conf.py` accordingly:: sphinx_gallery_conf = { ... 'expected_failing_examples': ['../examples/plot_raise.py'] } Here you list the examples you allow to fail during the build process, keep in mind to specify the full relative path from your `conf.py` to the example script. .. note:: If an example is expected to fail, Sphinx-Gallery will error if the example runs without error. .. _warning_on_error: Never fail the build on error ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sphinx-Gallery can be configured to only log warnings when examples fail. This means that sphinx will only exit with a non-zero exit code if the ``-W`` flag is passed to ``sphinx-build``. This can be enabled by setting:: sphinx_gallery_conf = { ... 'only_warn_on_example_error': True } .. _recommend_examples: Enabling the example recommender system ======================================= Sphinx-Gallery can be configured to generate content-based recommendations for an example gallery. A list of related examples is automatically generated by computing the closest examples in the `TF-IDF `_ space of their text contents. Only examples within a single gallery (including it's sub-galleries) are used to compute the closest examples. The most similar content is then displayed at the bottom of each example as a set of thumbnails. The recommender system can be enabled by setting ``enable`` to ``True``. To configure it, pass a dictionary to the ``sphinx_gallery_conf``, e.g.:: sphinx_gallery_conf = { ... "recommender": {"enable": True, "n_examples": 5, "min_df": 3, "max_df": 0.9}, } The only necessary parameter is ``enable``. If any other parameters is not specified, the default value is used. Below is a more complete explanation of each field: enable (type: bool, default: False) Whether to generate recommendations inside the example gallery. Enabling this feature requires adding `numpy` to the dependencies. n_examples (type: int, default: 5) Number of most relevant examples to display. min_df (type: float in range [0.0, 1.0] | int, default: 3) When building the vocabulary ignore terms that have a document frequency strictly lower than the given threshold. If float, the parameter represents a proportion of documents, integer represents absolute counts. This value is also called cut-off in the literature. max_df (type: float in range [0.0, 1.0] | int, default: 0.9) When building the vocabulary ignore terms that have a document frequency strictly higher than the given threshold. If float, the parameter represents a proportion of documents, integer represents absolute counts. rubric_header (type: str, default: "Related examples") Customizable rubric header. It can be edited to more descriptive text or to add external links, e.g. to the API doc of the recommender system on the Sphinx-Gallery documentation. The parameters ``min_df`` and ``max_df`` can be customized by the user to trim the very rare/very common words. This may improve the recommendations quality, but more importantly, it spares some computation resources that would be wasted on non-informative tokens. Currently example recommendations are only computed for ``.py`` files. .. _setting_thumbnail_size: Setting gallery thumbnail size ============================== By default Sphinx-Gallery will generate thumbnails at size ``(400, 280)``. The thumbnail image will then be scaled to the size specified by ``thumbnail_size``, adding pillarboxes or letterboxes as necessary to maintain the original aspect ratio. The default ``thumbnail_size`` is ``(400, 280)`` (no scaling) and can be changed via the ``thumbnail_size`` configuration, e.g.:: sphinx_gallery_conf = { ... 'thumbnail_size': (250, 250), } The gallery uses various CSS classes to display these thumbnails, which default to maximum 160x112px. To change this you can modify the default CSS by including your own CSS file via the Sphinx configuration :confval:`sphinx:html_static_path` (which will override default CSS classes in `Sphinx-Gallery CSS files `_). The following CSS would display the images at 250x250px instead of the default 160x112px: .. code-block:: css .sphx-glr-thumbcontainer { min-height: 320px !important; margin: 20px !important; } .sphx-glr-thumbcontainer .figure { width: 250px !important; } .sphx-glr-thumbcontainer img { max-height: 250px !important; width: 250px !important; } .sphx-glr-thumbcontainer a.internal { padding: 270px 10px 0 !important; } .. note:: The default value of ``thumbnail_size`` will change from ``(400, 280)`` (2.5x maximum specified by CSS) to ``(320, 224)`` (2x maximum specified by CSS) in version 0.9.0. This is to prevent unnecessary over-sampling. .. _min_reported_time: Minimal reported time ===================== By default, Sphinx-Gallery logs and embeds in the html output the time it took to run each script. If the majority of your examples runs quickly, you may not need this information. The ``min_reported_time`` parameter can be set to a number of seconds. The duration of scripts that ran faster than that amount will not be logged nor embedded in the html output. .. _show_memory: Showing memory consumption ========================== Sphinx-Gallery can use ``memory_profiler``, if installed, to report the peak memory during the run of an example. After installing ``memory_profiler``, you can do:: sphinx_gallery_conf = { ... 'show_memory': True, } It's also possible to use your own custom memory reporter, for example if you would rather see the GPU memory. In that case, ``show_memory`` must be a callable that takes a single function to call (i.e., one generated internally to run an individual script code block), and returns a two-element tuple containing: 1. The memory used in MiB while running the function, and 2. The function output A version of this that would always report 0 memory used would be:: sphinx_gallery_conf = { ... 'show_memory': lambda func: (0., func()), } .. _show_signature: Show signature ============== By default, Sphinx-Gallery writes a **Generated by ...** notice in the generated output. The ``show_signature`` parameter can be used to disable it. .. _capture_repr: Controlling what output is captured =================================== .. note:: Configure ``capture_repr`` to be an empty tuple (i.e., `capture_repr: ()`) to return to the output capturing behaviour prior to release v0.5.0. The ``capture_repr`` configuration allows the user to control what output is captured, while executing the example ``.py`` files, and subsequently incorporated into the built documentation. Data directed to standard output is always captured. The value of the last statement of *each* code block, *if* it is an expression, can also be captured. This can be done by providing the name of the 'representation' method to be captured in the ``capture_repr`` tuple, in order of preference. The representation methods currently supported are: * ``__repr__`` - returns the official string representation of an object. This is what is returned when your Python shell evaluates an expression. * ``__str__`` - returns a string containing a nicely printable representation of an object. This is what is used when you ``print()`` an object or pass it to ``format()``. * ``_repr_html_`` - returns a HTML version of the object. This method is only present in some objects, for example, pandas dataframes. Output capture can be controlled globally by the ``capture_repr`` configuration setting or file-by-file by adding a comment to the example file, which overrides any global setting:: # sphinx_gallery_capture_repr = () The default setting is:: sphinx_gallery_conf = { ... 'capture_repr': ('_repr_html_', '__repr__'), } With the default setting Sphinx-Gallery would first attempt to capture the ``_repr_html_`` of the last statement of a code block, *if* it is an expression. If this method does not exist for the expression, the second 'representation' method in the tuple, ``__repr__``, would be captured. If the ``__repr__`` also does not exist (unlikely for non-user defined objects), nothing would be captured. Data directed to standard output is **always** captured. For several examples, see :ref:`capture_repr_examples`. To capture only data directed to standard output, configure ``'capture_repr'`` to be an empty tuple: ``'capture_repr': ()``. This will imitate the behaviour of Sphinx-Gallery prior to v0.5.0. From another perspective, take for example the following code block:: print('Hello world') a=2 a # this is an expression ``'Hello world'`` would be captured for every ``capture_repr`` setting as this is directed to standard output. Further, * if ``capture_repr`` is an empty tuple, nothing else would be captured. * if ``capture_repr`` is ``('__repr__')``, ``2`` would also be captured. * if ``capture_repr`` is ``('_repr_html_', '__repr__')`` (the default) Sphinx-Gallery would first attempt to capture ``_repr_html_``. Since this does not exist for ``a``, it will then attempt to capture ``__repr__``. The ``__repr__`` method does exist for ``a``, thus ``2`` would be also captured in this case. **Matplotlib note**: if the ``'capture_repr'`` tuple includes ``'__repr__'`` and/or ``'__str__'``, code blocks which have a Matplotlib function call as the last expression will generally produce a yellow output box in the built documentation, as well as the figure. This is because matplotlib function calls usually return something as well as creating/amending the plot in standard output. For example, ``matplotlib.plot()`` returns a list of ``Line2D`` objects representing the plotted data. This list has a ``__repr__`` and a ``__str__`` method which would thus be captured. You can prevent this by: * assigning the (last) plotting function to a temporary variable. For example:: import matplotlib.pyplot as plt _ = plt.plot([1, 2, 3, 4], [1, 4, 9, 16]) * add ``plt.show()`` (which does not return anything) to the end of your code block. For example:: import matplotlib.pyplot as plt plt.plot([1, 2, 3, 4], [1, 4, 9, 16]) plt.show() The unwanted string output will not occur if ``'capture_repr'`` is an empty tuple or does not contain ``__repr__`` or ``__str__``. .. _regular expressions: https://docs.python.org/3/library/re.html Prevent capture of certain classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you wish to capture a representation of the last expression of each code blocks **unless** the last expression is of a certain type, you can use ``'ignore_repr_types'``. ``'ignore_repr_types'`` is by default an empty raw string (``r''``), meaning no types are ignored. To exclude specific type(s) from being captured, ``'ignore_repr_types'`` can be set to a regular expression matching the name(s) of the type(s) to be excluded. For example, the configuration below would capture the ``__repr__`` of the last expression of each code block unless the name of the ``type()`` of the last expression includes the string 'matplotlib.text' *or* 'matplotlib.axes'. This would prevent capturing of all subclasses of 'matplotlib.text', e.g. expressions of type 'matplotlib.text.Annotation', 'matplotlib.text.OffsetFrom' etc. Similarly subclasses of 'matplotlib.axes' (e.g. 'matplotlib.axes.Axes', 'matplotlib.axes.Axes.plot' etc.) will also not be captured. :: sphinx_gallery_conf = { ... 'capture_repr': ('__repr__'), 'ignore_repr_types': r'matplotlib\.(text|axes)', } .. _nested_sections: Nesting gallery sections ======================== By default, ``nested_sections=True``. In this case, for each folder present in the gallery's root folder, Sphinx-Gallery expects to find a readme and uses it to build a specific index file for this subsection. This index file will contain the section's description and a toctree linking to each gallery item which belongs to this subsection. Eventually, the gallery's main index files will contain the gallery's description and a toctree linking to each subsections's index file. With this behaviour, generated file structure and toctrees mimic that of the original gallery folder. This is useful to generate sidebars with nested sections representing the gallery's file structure. .. note:: When ``nested_sections=True``, gallery items located in the gallery's root folder should be move to a new subfolder, otherwise the sidebar might not behave as expected (due to the fuzzy toctree structure). If ``nested_sections=False``, Sphinx-Gallery will behave as it used to previous to version 0.10.2. Specifically, it will generate a single index file for the whole gallery. This index file will contain descriptions for the whole gallery as well as for each subsection, and a specific toctree for each subsection. In particular, sidebars generated using these toctrees might not reflect the actual section / folder structure. .. _manual_passthrough: Manually passing files ====================== By default, Sphinx-Gallery creates all the files that are written in the sphinx-build directory, either by generating rst and images from a ``*.py`` in the gallery-source, or from creating ``index.rst`` from ``README.txt`` in the gallery-source. However, sometimes it is desirable to pass files from the gallery-source to the sphinx-build. For example, you may want to pass an image that a gallery refers to, but does not generate itself. You may also want to pass raw rst from the gallery-source to the sphinx-build, because that material fits in thematically with your gallery, but is easier to write as rst. To accommodate this, you may set ``copyfile_regex`` in ``sphinx_gallery_conf``. The following copies across rst files. :: sphinx_gallery_conf = { ... 'copyfile_regex': r'.*\.rst', } Note that if you copy across files rst files, for instance, it is your responsibility to ensure that they are in a sphinx ``toctree`` somewhere in your document. You can, of course, add a ``toctree`` to your ``README.txt``. Manually passing ``index.rst`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can bypass Sphinx-Gallery automatically creating an ``index.rst`` from a ``README.txt`` in a gallery directory or subdirectory. If your ``copyfile_regex`` includes ``index.rst``, and you have an ``index.rst`` in the gallery-source instead of the README, Sphinx-Gallery will use that instead of the index it automatically makes. If you do this, you are responsible for adding your own Sphinx ``toctree`` in that index (or elsewhere in your Sphinx documentation) that includes any gallery items or other files in that directory. .. _show_api_usage: Showing API Usage ================= Graphs and documentation of both unused API entries and the examples that each API entry is used in are generated in the sphinx output directory under ``sg_api_usage.html``. See the `Sphinx-Gallery API usage documentation and graphs `_ for example. In large projects, there are many modules and, since a graph of API usage is generated for each module, this can use a lot of resources so ``show_api_usage`` is set to ``'unused'`` by default. The unused API entries are all shown in one graph so this scales much better for large projects. Setting ``show_api_usage`` to ``True`` will make one graph per module showing all of the API entries connected to the example that they are used in. This could be helpful for making a map of which examples to look at if you want to learn about a particular module. Setting ``show_api_usage`` to ``False`` will not make any graphs or documentation about API usage. Note, ``graphviz`` is required for making the unused and used API entry graphs. .. _api_usage_ignore: Ignoring API entries ^^^^^^^^^^^^^^^^^^^^ By default, ``api_usage_ignore='.*__.*__'`` ignores files that match this regular expression in documenting and graphing the usage of API entries within the example gallery. This regular expression can be modified to ignore any kind of file that should not be considered. The default regular expression ignores functions like ``__len__()`` for which it may not be desirable to document if they are used in examples. sphinx-gallery-0.16.0/doc/contribute.rst000066400000000000000000000101301461331107500202150ustar00rootroot00000000000000.. _contribute-guide: ========== Contribute ========== We appreciate your help in improving this document and our library! Please `open an issue `_ if this document is unclear or missing a step. .. _development-workflow: Development Workflow ==================== If you are interested in contributing code or documentation, we strongly recommend that you install a development version of sphinx-gallery in a development environment. If you are unfamiliar with the git/github workflow, please see Github's guide to `contributing to projects `_. This guide assumes familiarity with the Github workflow and focuses on aspects specific to contributing to Sphinx-Gallery. .. _checkout-source: Get Latest Source ----------------- You can get the latest development source from our `Github repository `_. .. code-block:: console git clone https://github.com//sphinx-gallery .. _virtual-environment: Create a Dedicated Environment ------------------------------ We strongly recommend that you create a virtual environment for developing Sphinx Gallery to isolate it from other Python installations on your system. Create a new virtual environment: .. code-block:: console python -m venv Activate the virtual environment using one of the following: .. code-block:: console source /bin/activate # Linux/macOS \Scripts\activate.bat # Windows cmd.exe \Scripts\Activate.ps1 .. _install-dependencies: Install Dependencies -------------------- Most of the Sphinx Gallery dependencies are listed in :file:`requirements.txt` and :file:`dev-requirements.txt` and can be installed from those files: .. code-block:: console python -m pip install -r requirements.txt -r dev-requirements.txt Sphinx Gallery requires `graphviz `_ for drawing API entry graphs: .. code-block:: console python -m pip install graphviz Sphinx Gallery requires that `setuptools `_ is installed. It is usually packaged with python, but if necessary can be installed using ``pip``: .. code-block:: console python -m pip install setuptools .. _editable-install: Install for Development ----------------------- Editable installs means that the environment Python will always use the most recently changed version of your code. To install Sphinx Gallery in editable mode, ensure you are in the sphinx-gallery directory .. code-block:: console cd sphinx-gallery Then install using the editable flag: .. code-block:: console python -m pip install -e . .. _verify-install: Verify install -------------- Check that you are all set by running the tests: .. code-block:: console python -m pytest sphinx_gallery And by building the docs: .. code-block:: console cd doc make html .. _pre-commit-hooks: Install pre-commit hooks ------------------------ pre-commit hooks check for things like spelling and formatting in contributed code and documentation. To set up pre-commit hooks: .. code-block:: console python -m pip install pre-commit pre-commit install .. _code-contributions: Guidelines ========== .. _code-contrib-testing: Testing ------- All code contributions should be tested. We use the `pytest `_ testing framework and ``tinybuild`` to build test pages. Tests can be found in :file:`sphinx_gallery/tests`. .. _testing-tinybuild: tinybuild ^^^^^^^^^ ``tinybuild`` is designed as the minimal full sphinx doc build that you can run with ``make html`` from :file:`tinybuild/doc` to get a traditional build experience. ``tinybuild`` gets run in :file:`tests/test_full.py` to build a test page using the ``.rst`` document files in :file:`tests/doc/tinybuild`. The tests examine the ``html`` output to verify the behavior of the directives in the ``.rst`` files. sphinx-gallery-0.16.0/doc/faq.rst000066400000000000000000000053141461331107500166160ustar00rootroot00000000000000Frequently Asked Questions ========================== .. contents:: **Contents** :local: :depth: 1 Why is `__file__` not defined? What can I use? ---------------------------------------------- The global `__file__` variable defined by Python when running scripts is not defined on Jupyter notebooks. Since Sphinx-Gallery supports notebook styled examples and also exports to Jupyter notebooks we agreed on keeping this variable out of scope when executing the example scripts. Instead of `__file__` use :func:`os.getcwd` to get the directory where the file is located. Sphinx-Gallery executes the examples scripts in their source directory. .. seealso:: `Github PR #166 `_ `Github PR #212 `_ Why am I getting text output for Matplotlib functions? ------------------------------------------------------ The output capturing behavior of Sphinx-Gallery changed with Sphinx-Gallery v0.5.0. Previous to v0.5.0, only data directed to standard output (e.g., only Matplotlib figures) was captured. In, v0.5.0, the configuration ``'capture_repr'`` (:ref:`capture_repr`) was added. This configuration allows a 'representation' of the last statement of each code block, if it is an expression, to be captured. The default setting of this new configuration, ``'capture_repr': ('_repr_html_', '__repr__')``, first attempts to capture the ``'_repr_html_'`` and if this does not exist, the ``'__repr__'``. This means that if the last statement was a Matplotlib function, which usually returns a value, the representation of that value will be captured as well. To prevent Matplotlib function calls from outputting text as well as the figure, you can assign the last plotting function to a temporary variable (e.g. ``_ = matplotlib.pyploy.plot()``) or add ``matplotlib.pyplot.show()`` to the end of your code block (see :ref:`capture_repr`). Alternatively, you can set ``capture_repr`` to be an empty tuple (``'capture_repr': ()``), which will imitate the behavior of Sphinx-Gallery prior to v0.5.0. This will also prevent you from getting any other unwanted output that did not occur prior to v0.5.0. Why has my thumbnail appearance changed? ---------------------------------------- The DOM structure of thumbnails was refactored in order to make them responsive and aligned on a css grid. These changes might make your existing custom css obsolete. You can read our `custom css migration guide for thumbnails `_ for pointers on how to update your css. .. seealso:: `Github PR #906 `_ sphinx-gallery-0.16.0/doc/galleries.rst000066400000000000000000000004041461331107500200110ustar00rootroot00000000000000Example Galleries ================= This section contains several example galleries generated using different plotting backends. .. toctree:: :hidden: auto_examples/index tutorials/index auto_plotly_examples/index auto_pyvista_examples/index sphinx-gallery-0.16.0/doc/getting_started.rst000066400000000000000000000207021461331107500212340ustar00rootroot00000000000000=================================== Getting Started with Sphinx-Gallery =================================== .. _create_simple_gallery: Creating a basic Gallery ======================== This section describes how to set up a basic gallery for your examples using the Sphinx extension Sphinx-Gallery, which will do the following: * Automatically generate `Sphinx reST `_ out of your ``.py`` example files. The rendering of the resulting reST will provide the users with ``.ipynb`` (Jupyter notebook) and ``.py`` files of each example, which users can download. * Create a gallery with thumbnails for each of these examples (such as `the one that scikit-learn `_ uses). A `template repository `_, with sample example galleries and basic configurations is also available to help you get started. .. note:: Working `sphinx builders `_ for sphinx_gallery include `html`, `dirhtml` and `latex`. .. _set_up_your_project: Overview your project files and folders --------------------------------------- This section describes the general files and structure needed for Sphinx-Gallery to build your examples. Let's say your Python project has the following structure: .. code-block:: none . ├── doc │ ├── conf.py │ ├── index.rst | ├── make.bat │ └── Makefile ├── my_python_module │ ├── __init__.py │ └── mod.py └── examples ├── plot_example.py ├── example.py └── README.txt (or .rst) * ``doc`` is the Sphinx 'source directory'. It contains the Sphinx base configuration files. Default versions of these base files can obtained from executing ``sphinx-quickstart`` (more details at `Sphinx-quickstart `_). Sphinx ``.rst`` source files are generally also placed here (none included in our example directory structure above) but these are unassociated with Sphinx-Gallery functions. * ``my_python_module`` contains the ``.py`` files of your Python module. This directory is not required and Sphinx-Gallery can be used for a variety of purposes outside of documenting examples for a package, for example creating a website for a Python tutorial. * ``examples`` contains the files used by Sphinx-Gallery to build the gallery. Sphinx-Gallery expects the ``examples`` directory to have a specific structure, which we'll cover next. Structure the examples folder ----------------------------- In order for Sphinx-Gallery to build a gallery from your ``examples`` folder, this folder must have the following things: * **The gallery header**: A file named ``README.txt`` or ``README.rst`` that contains reST to be used as a header for the gallery welcome page, which will also include thumbnails generated from this folder. It must have at least a title. For example:: This is my gallery ================== Below is a gallery of examples * **Example Python scripts**: A collection of Python scripts that will be processed when you build your HTML documentation. For information on how to structure these Python scripts with embedded reST, see :ref:`python_script_syntax`. * By default **only** files prefixed with ``plot_`` will be executed and their outputs captured to incorporate them in the HTML output of the script. Files without that prefix will be only parsed and presented in a rich literate programming fashion, without any output. To change the default file pattern for execution and capture see :ref:`build_pattern`. * The output that is captured while executing the ``.py`` files and subsequently incorporated into the built documentation can be finely tuned. See :ref:`capture_repr`. * You can have sub-directories in your ``examples`` directory. These will be included as sub-sections of your gallery. They **must** contain their own ``README.txt`` or ``README.rst`` file as well. .. warning:: The variable name ``___`` (3 underscores) should never be used in your example Python scripts as it is used as an internal Sphinx-Gallery variable. .. _configure_and_use_sphinx_gallery: Configure and use Sphinx-Gallery -------------------------------- After Sphinx-Gallery is installed, we must enable and configure it to build with Sphinx. First, enable Sphinx-Gallery in the Sphinx ``doc/conf.py`` file with:: extensions = [ ... 'sphinx_gallery.gen_gallery', ] This loads Sphinx-Gallery as one of your extensions, the ellipsis ``...`` represents your other loaded extensions. Next, create your configuration dictionary for Sphinx-Gallery. Here we will simply set the minimal required configurations. We must set the location of the 'examples' directory (containing the gallery header file and our example Python scripts) and the directory to place the output files generated. The path to both of these directories should be relative to the ``doc/conf.py`` file. The following configuration declares the location of the 'examples' directory (``'example_dirs'``) to be ``../examples`` and the 'output' directory (``'gallery_dirs'``) to be ``auto_examples``:: sphinx_gallery_conf = { 'examples_dirs': '../examples', # path to your example scripts 'gallery_dirs': 'auto_examples', # path to where to save gallery generated output } After building your documentation, ``gallery_dirs`` will contain the following files and directories: * ``index.rst`` - the master document of the gallery containing the gallery header, table of contents tree and thumbnails for each example. It will serve as the welcome page for that gallery. * ``sg_execution_times.rst`` - execution time of all example ``.py`` files, summarised in table format (`original pull request on GitHub `_). * ``images`` - directory containing images produced during execution of the example ``.py`` files (more details in :ref:`image_scrapers`) and thumbnail images for the gallery. * A directory for each sub-directory in ``'example_dirs'``. Within each directory will be the above and below listed files for that 'sub-gallery'. Additionally for **each** ``.py`` file, a file with the following suffix is generated: * ``.rst`` - the rendered reST version of the ``.py`` file, ready for Sphinx to build. * ``.ipynb`` - to enable the user to download a Jupyter notebook version of the example. * ``.py`` - to enable the user to download a ``.py`` version of the example. * ``.py.md5`` - a md5 hash of the ``.py`` file, used to determine if changes have been made to the file and thus if new output files need to be generated. * ``_codeobj.pickle`` - used to identify function names and to which module they belong (more details in :ref:`sphx_glr_auto_examples_plot_6_function_identifier.py`) Additionally, two compressed ``.zip`` files containing all the ``.ipynb`` and ``.py`` files are generated, as well as a root-level ``sg_execution_times.rst`` file containing all of the execution times. For more advanced configuration, see the :ref:`configuration` page. Add your gallery to the documentation ------------------------------------- The ``index.rst`` file generated for your gallery can be added to the table of contents tree in the main Sphinx ``doc/index.rst`` file or embedded in a Sphinx source ``.rst`` file with an ``.. include::`` statement. Build the documentation ----------------------- In your Sphinx source directory, (e.g., ``myproject/doc``) execute: .. code-block:: bash $ make html This will start the build of your complete documentation. Both the Sphinx-Gallery output files described above and the Sphinx built HTML documentation will be generated. Once a build is completed, all the outputs from your examples will be cached. In the future, only examples that have changed will be re-built. You should now have a gallery built from your example scripts! For more advanced usage and configuration, check out the :ref:`advanced_usage` page or the :ref:`configuration` reference. .. note:: Sphinx-Gallery may work for non-HTML Sphinx `builders `_ but support for this is mostly untested and results may vary. sphinx-gallery-0.16.0/doc/index.rst000066400000000000000000000033241461331107500171550ustar00rootroot00000000000000============== Sphinx-Gallery ============== .. include:: ../README.rst :start-after: tagline-begin-content :end-before: tagline-end-content Features ======== * 🚀 :ref:`Create example galleries ` automatically by running pure Python example scripts while capturing outputs + figures, rendering them into reST files built into your documentation by Sphinx: * 📝 :ref:`embedding_rst`, allowing you to interweave narrative-like content with code that generates plots in your documentation. Sphinx-Gallery also automatically generates a Jupyter Notebook for each your example page. * 📋 :ref:`references_to_examples`. Sphinx-Gallery can generate mini-galleries listing all examples that use a particular function/method/etc. * 🔗 :ref:`link_to_documentation`. Sphinx-Gallery can automatically add links to API documentation for functions/methods/classes that are used in your examples (for any Python module that uses intersphinx). * 🗒️ :ref:`multiple_galleries_config` to create and embed galleries for several folders of examples. .. _install_sg: Installation ============ .. include:: ../README.rst :start-after: installation-begin-content :end-before: installation-end-content How to cite =========== .. include:: ../README.rst :start-after: citation-begin-content :end-before: citation-end-content .. This sets the top-level (middle) header items of pydata-sphinx-theme Contribute ========== Thank you for your interest! Please see our :ref:`contributing guide ` to get started. .. toctree:: :hidden: User guide Advanced Demo galleries Contribution Guide Changelog sphinx-gallery-0.16.0/doc/maintainers.rst000066400000000000000000000104631461331107500203620ustar00rootroot00000000000000:orphan: ========================== Maintaining Sphinx Gallery ========================== This document contains tips for maintenance. .. contents:: :local: :depth: 2 How to make a release ===================== .. highlight:: console 1. Update ``CHANGES.rst`` and version in a PR --------------------------------------------- 1. Use `github_changelog_generator `_ to gather all merged pull requests and closed issues during the development cycle. You will likely need to `generate a Github token `_ as Github only allows 50 unauthenticated requests per hour. In the command below ```` is the current (not development) version of the package, e.g., ``0.6.0``. The changelog can generated with the following:: github_changelog_generator --since-tag=v --token To avoid the need to pass ``--token``, you can use ``export CHANGELOG_GITHUB_TOKEN=`` instead. 2. Update `PR labels on GitHub `__ if necessary and regenerate ``CHANGELOG.md`` so that PRs are categorized correctly. The labels we currently use are: ``bug`` For fixed bugs. ``enhancement`` For enhancements. ``api`` For API changes (deprecations and removals). ``maintenance`` For general project maintenance (e.g., CIs). ``documentation`` For documentation improvements. Once all PRs land in one of these categories using the changelog generator, manually edit CHANGELOG.md to look reasonable if necessary. 3. Propagate the relevant changes to `CHANGES.rst `_. You can easily convert it reST with pandoc:: pandoc CHANGELOG.md --wrap=none -o CHANGELOG.rst Then copy just the sections to ``CHANGES.rst``. **Keep ``CHANGELOG.md`` for later.** 4. Update the version in ``sphinx_gallery/__init__.py``, which should end in ``.dev0``. You should replace ``.dev0`` with ``0`` to obtain a semantic version (e.g., ``0.12.dev0`` to ``0.12.0``). 5. Open a PR with the above **changelog** and **version** changes (along with any updates to this ``maintainers.rst`` document!). 6. Make sure CIs are green. 7. Check that the built documentation looks correct. 8. Get somebody else to make sure all looks well, and merge this pull request. 2. Finalize the release ------------------------ 1. Make sure CIs are green following the "Release" PR. 2. Create a new release on GitHub * Go to the `Draft a new release `_ page. * The **tag version** is whatever the version is in ``__init__.py`` prepended with ``v``. E.g., ``v0.7.0``. * The **release title** is ``Release ``. * The **description** should contain the markdown changelog you generated above (in the ``CHANGELOG.md`` file) and include a "Full changelog" link at the top, e.g.; ``[Full Changelog](https://github.com/sphinx-gallery/sphinx-gallery/compare/v0.13.0...v0.14.0)`` * Click **Publish release** when you are done. * Confirm that the new version of Sphinx Gallery `is posted to PyPI `_. 3. Now that the releases are complete, we need to switch the ``master`` branch back into a developer mode. Bump the `Sphinx Gallery version number `_ to the next minor (or major) release and append ``.dev0`` to the end, and make a PR for this change. 4. Celebrate! You've just released a new version of Sphinx Gallery! 3. Post-release tasks --------------------- 1. Check for any deprecations (e.g., ``git grep eprecat``) and complete them, usually by removing code or some behavior in a PR. 2. Check and update **minimum supported (old)** Python and Sphinx versions (older than 2 years) plus check for any **new** ones, and update if necessary in a PR: - ``setup.py::python_requires`` (old) - ``requirements.txt`` (old) - ``.github/workflows/tests.yml`` (old and new) - ``README.rst::Installation`` (old) sphinx-gallery-0.16.0/doc/make.bat000066400000000000000000000154771461331107500167350ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* if exist auto_examples rd /q /s auto_examples if exist auto_plotly_examples rd /q /s auto_plotly_examples if exist auto_pyvista_examples rd /q /s auto_pyvista_examples if exist tutorials rd /q /s tutorials if exist gen_modules rd /q /s gen_modules goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Sphinx-Gallery.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Sphinx-Gallery.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end sphinx-gallery-0.16.0/doc/projects_list.rst000066400000000000000000000003531461331107500207310ustar00rootroot00000000000000======================= Who uses Sphinx-Gallery ======================= Here is an incomplete list of projects using `sphinx-gallery`. .. include:: ../README.rst :start-after: projects_list_start :end-before: projects_list_end sphinx-gallery-0.16.0/doc/reference.rst000066400000000000000000000020051461331107500177770ustar00rootroot00000000000000 .. _sphx_glr_api_reference: Sphinx-Gallery API Reference ============================ .. note:: Sphinx-Gallery is typically used indirectly via Sphinx execution and configuration variables, see :ref:`configuration` for how to do this. However, as a standard Python project, we document many functions and classes as well below, even though these will typically not be needed by end users. .. currentmodule:: sphinx_gallery .. automodule:: sphinx_gallery :no-members: :no-inherited-members: :py:mod:`sphinx_gallery`: .. autosummary:: :toctree: gen_modules/ :template: module.rst gen_gallery backreferences gen_rst scrapers py_source_parser block_parser docs_resolv notebook downloads sorting interactive_example directives .. currentmodule:: sphinx_gallery.utils .. automodule:: sphinx_gallery.utils :no-members: :no-inherited-members: :py:mod:`sphinx_gallery.utils`: .. autosummary:: :toctree: gen_modules/ :template: module.rst optipng sphinx-gallery-0.16.0/doc/sphinxext/000077500000000000000000000000001461331107500173445ustar00rootroot00000000000000sphinx-gallery-0.16.0/doc/sphinxext/sg_doc_build.py000066400000000000000000000035571461331107500223450ustar00rootroot00000000000000"""Utilities for building docs.""" from sphinx_gallery.notebook import add_code_cell, add_markdown_cell def notebook_modification_function(notebook_content, notebook_filename): """Implement JupyterLite-specific modifications of notebooks.""" notebook_content_str = str(notebook_content) warning_template = "\n".join( [ "
", "", "# JupyterLite warning", "", "{message}", "
", ] ) if "pyvista_examples" in notebook_filename: message_class = "danger" message = ( "PyVista is not packaged in Pyodide, this notebook is not " "expected to work inside JupyterLite" ) elif "import plotly" in notebook_content_str: message_class = "danger" message = ( "This notebook is not expected to work inside JupyterLite for now." " There seems to be some issues with Plotly, see " "[this]('https://github.com/jupyterlite/jupyterlite/pull/950') " "for more details." ) else: message_class = "warning" message = ( "JupyterLite integration in sphinx-gallery is beta " "and it may break in unexpected ways" ) markdown = warning_template.format(message_class=message_class, message=message) dummy_notebook_content = {"cells": []} add_markdown_cell(dummy_notebook_content, markdown) code_lines = [] if "seaborn" in notebook_content_str: code_lines.append("%pip install seaborn") if code_lines: code_lines = ["# JupyterLite-specific code"] + code_lines code = "\n".join(code_lines) add_code_cell(dummy_notebook_content, code) notebook_content["cells"] = ( dummy_notebook_content["cells"] + notebook_content["cells"] ) sphinx-gallery-0.16.0/doc/syntax.rst000066400000000000000000000211301461331107500173670ustar00rootroot00000000000000.. _python_script_syntax: ============================================= Structuring Python scripts for Sphinx-Gallery ============================================= This page describes the structure and syntax that can be used in Python scripts to generate rendered HTML gallery pages. A simple example ================ Sphinx-Gallery expects each Python file to have two things: 1. **A docstring**, written in reST, that defines the header for the example. It must begin by defining a reST title. The title may contain any punctuation mark but cannot start with the same punctuation mark repeated more than 3 times. For example:: """ "This" is my example-script =========================== This example doesn't do much, it just makes a simple plot """ 2. **Python code**. This can be any valid Python code that you wish. Any Matplotlib images that are generated will be saved to disk, and the reST generated will display these images with the built examples. By default only images generated by Matplotlib, or packages based on Matplotlib (e.g., Seaborn or Yellowbrick) are saved and displayed. However, you can change this to include other packages, see :ref:`image_scrapers`. For a quick reference have a look at the example :ref:`sphx_glr_auto_examples_plot_0_sin.py` .. _embedding_rst: Embed reST in your example Python files ======================================= Additionally, you may embed reST syntax within your Python scripts. reST allows you to easily add formatted text, math equations and reference links, including :ref:`cross referencing other examples `. This will be rendered in-line with the Python code and its outputs, similar to how Jupyter Notebooks are structured (in fact, Sphinx-Gallery also **creates** a Jupyter Notebook for each example that is built). You can embed reST in your Python examples by including a line of >= 20 ``#`` symbols, ``#%%``, or ``# %%``. For consistency, it is recommended that you use only one of the above three 'block splitter' options in your project. If using a line of ``#``'s, we recommend using 79 ``#``'s, like this:: ############################################################################### Any commented lines (line beginning with ``#`` followed by a space, to be PEP8-compliant) that immediately follow a block splitter will be rendered as reST in the built gallery examples. To switch back to writing code, either stop starting lines with ``#`` and a space or leave an empty line before writing code comments. You can thus easily alternate between text and code 'blocks'. For example:: # This is commented python myvariable = 2 print("my variable is {}".format(myvariable)) # %% # This is a section header # ------------------------ # # In the built documentation, it will be rendered as reST. All reST lines # must begin with '# ' (note the space) including underlines below section # headers. # These lines won't be rendered as reST because there is a gap after the last # commented reST block. Instead, they'll resolve as regular Python comments. # Normal Python code can follow these comments. print('my variable plus 2 is {}'.format(myvariable + 2)) The ``#%%`` and ``# %%`` syntax is consistent with the 'code block' (or 'code cell') separator syntax in `Visual Studio Code Python extension `_, `Visual Studio Python Tools `_, `Jupytext `_, `Pycharm Professional `_, `Hydrogen plugin (for Atom) `_ and `Spyder `_. Note that although the documentation of these editors/IDEs may only mention one of ``#%%`` or ``# %%``, in practice both work. With these editors/IDEs, ``#%%`` or ``# %%`` at the start of a line signifies the start of a new code block. Code blocks allow you to separate your code into chunks, like in Jupyter Notebooks. All the code within a code block can be easily executed together. This functionality can be helpful when writing a Sphinx-Gallery ``.py`` example as the blocks allow you to easily create pairs of subsequent Sphinx-Gallery text and code blocks. Here are the contents of an example Python file using the 'code block' functionality:: """ This is my example script ========================= This example doesn't do much, it just makes a simple plot """ # %% # This is a section header # ------------------------ # This is the first section! # The `#%%` signifies to Sphinx-Gallery that this text should be rendered as # reST and if using one of the above IDE/plugin's, also signifies the start of a # 'code block'. # This line won't be rendered as reST because there's a space after the last block. myvariable = 2 print("my variable is {}".format(myvariable)) # This is the end of the 'code block' (if using an above IDE). All code within # this block can be easily executed all at once. # %% # This is another section header # ------------------------------ # # In the built documentation, it will be rendered as reST after the code above! # This is also another code block. print('my variable plus 2 is {}'.format(myvariable + 2)) For a clear example refer to the rendered example :ref:`sphx_glr_tutorials_plot_parse.py` and compare it to the generated :download:`original python script ` .. _non_python_source: Examples in other programming languages ======================================= Sphinx-Gallery also supports rendering HTML pages for examples written in programming languages other than Python, although these examples are not currently executed or scanned for output. See :ref:`filename/ignore patterns ` for configuration settings. For such examples, the header for the example is defined by the first comment block in the file, which must contain a reST title, and may contain any additional reST content that should appear above the first code block. For example, a C++ example could start with: .. code:: C++ // My Awesome Example // ================== // // The description continues as long as there are lines // that start with a comment character. reST content can likewise be embedded in comments that are marked with a special delimiter, where that delimiter depends on the comment characters used by the language of the example. Valid special delimiters are: 1. The comment character followed by ``%%``. For example ``//%%`` for C++. 2. The comment character followed by a space, followed by ``%%``. For example, ``// %%`` for C++. 3. A line of at least 20 comment characters. For example, ``////////////////////`` for C++. Any text following the special delimiter on the same line will be converted into a reST heading (underlined with ``-``). The reST block continues until a line that does not start with a comment character is encountered. Some examples: .. code:: C++ // %% Important Heading // This is some text in a reST block in C++, appearing underneath a heading. // // * Start a list // * Check it twice .. code:: Fortran !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! This is the start of a reST block in Fortran 90. ! ! It can contain multiple paragraphs. For languages that use C-style multiline comments, the following styles are supported: .. code:: C /* %% * Subheading * ---------- * * Description */ int y = 3; /**************************/ /* Another subheading */ /* ------------------ */ /* */ /* Description */ /**************************/ double z = 1.5; Finally, for compatibility with Matlab's use of a simple ``%%`` delimiter to mark code sections, this is allowed as a special delimiter in Matlab source files, in addition to the multi-language syntax described above: .. code:: Matlab %% Heading % some text below the heading .. _plain_rst: Plain reST examples =================== Sphinx-Gallery generates examples from Python scripts, so examples written in plain reST files are not supported. If you're looking to generate hyperlinks for functions (linking to their corresponding online documentation) in code blocks of ordinary reST documentation, you might find `sphinx-codeautolink `_ helpful. sphinx-gallery-0.16.0/doc/usage.rst000066400000000000000000000003471461331107500171540ustar00rootroot00000000000000Using Sphinx-Gallery ==================== This section contains what you need to know to use and configure basic aspects of Sphinx-Gallery. .. toctree:: :hidden: getting_started syntax configuration projects_list sphinx-gallery-0.16.0/doc/utils.rst000066400000000000000000000030131461331107500172010ustar00rootroot00000000000000======================== Sphinx-Gallery Utilities ======================== Convert Python scripts into Jupyter Notebooks ============================================= Sphinx Gallery exposes its python source to Jupyter notebook converter as a executable script too. To use this utility just call the script and give the Python source file as argument: .. code-block:: console $ python -m sphinx_gallery_py2jupyter python_script.py Embedding Sphinx-Gallery inside your documentation script extensions ==================================================================== If you want to embed Sphinx-Gallery in your project instead of putting it as a dependency you can call our embedding script inside your Sphinx extensions folder: .. code-block:: console # Script to do a local install of sphinx-gallery rm -rf tmp sphinx_gallery easy_install -Zeab tmp sphinx-gallery cp -vru tmp/sphinx-gallery/sphinx_gallery/ . echo "Remember to add sphinx_gallery to your version control" echo "Use in case of git:" echo "$ git add sphinx_gallery" This will download directly from PyPI our latest released code and save it to the current folder. This is a stripped version of the Sphinx-Gallery module to incorporate in your project. You should also add it to your version control system. Minigallery directive ====================== Sphinx-Gallery provides the ``minigallery`` directive so you can easily add a reduced version of the gallery to your documentation. See :ref:`minigalleries_to_examples` for details. sphinx-gallery-0.16.0/examples/000077500000000000000000000000001461331107500163635ustar00rootroot00000000000000sphinx-gallery-0.16.0/examples/README.txt000066400000000000000000000014311461331107500200600ustar00rootroot00000000000000.. _examples-index: Basics Gallery with Matplotlib ============================== This page consists of the 'Basics Gallery with matplotlib' and a sub-gallery, 'No image output examples'. This sub-gallery is generated from a sub-directory within the general examples directory. The file structure of this gallery looks like this: .. code-block:: none examples/ # base 'Basics Gallery with Matplotlib' directory ├── README.txt ├── <.py files> └── no_output/ # generates the 'No image output examples' sub-gallery ├── README.txt └── <.py files> .. _general_examples: General examples ---------------- This gallery consists of introductory examples and examples demonstrating specific features of Sphinx-Gallery.sphinx-gallery-0.16.0/examples/local_module.py000066400000000000000000000003071461331107500213740ustar00rootroot00000000000000""" Local module ============ This example demonstrates how local modules can be imported. This module is imported in the example 'Plotting the exponential function' (``plot_exp.py``). """ N = 100 sphinx-gallery-0.16.0/examples/no_output/000077500000000000000000000000001461331107500204175ustar00rootroot00000000000000sphinx-gallery-0.16.0/examples/no_output/README.txt000066400000000000000000000003721461331107500221170ustar00rootroot00000000000000.. _no_out_examples: No image output examples ------------------------ This section gathers examples which don't produce any figures. Some examples only output to standard output, others demonstrate how Sphinx-Gallery handles examples with errors. sphinx-gallery-0.16.0/examples/no_output/just_code.py000066400000000000000000000005511461331107500227510ustar00rootroot00000000000000""" A short Python script ===================== This demonstrates an example ``.py`` file that is not executed when gallery is generated (see :ref:`build_pattern`) but nevertheless gets included as an example. Note that no output is capture as this file is not executed. """ # Code source: Óscar Nájera # License: BSD 3 clause print([i for i in range(10)]) sphinx-gallery-0.16.0/examples/no_output/plot_raise.py000066400000000000000000000017761461331107500231450ustar00rootroot00000000000000""" Example that fails to execute ============================= This example demonstrates a code block that raises an error and how any code blocks that follow are not executed. When scripts fail, their gallery thumbnail is replaced with the broken image stamp. This allows easy identification in the gallery display. You will also get the python traceback of the failed code block. """ # Code source: Óscar Nájera # License: BSD 3 clause # sphinx_gallery_line_numbers = True import numpy as np import matplotlib.pyplot as plt plt.pcolormesh(np.random.randn(100, 100)) # %% # This next block will raise a NameError iae # noqa # %% # Sphinx gallery will stop executing the remaining code blocks after # the exception has occurred in the example script. Nevertheless the # html will still render all the example annotated text and # code blocks, but no output will be shown. # %% # Here is another error raising block but will not be executed plt.plot("Strings are not a valid argument for the plot function") sphinx-gallery-0.16.0/examples/no_output/plot_strings.py000066400000000000000000000006061461331107500235220ustar00rootroot00000000000000""" Constrained Text output frame ============================= This example captures the standard output and includes it in the example. If output is too long it becomes automatically framed into a text area. """ # Code source: Óscar Nájera # License: BSD 3 clause print("This is a long test Output\n" * 50) #################################### # One line out print("one line out") sphinx-gallery-0.16.0/examples/no_output/plot_syntaxerror.py000066400000000000000000000005311461331107500244260ustar00rootroot00000000000000""" Example with SyntaxError ======================== Sphinx-Gallery uses Python's AST parser, thus you need to have written valid python code for Sphinx-Gallery to parse it. If your script has a SyntaxError you'll be presented the traceback and the original code. """ # Code source: Óscar Nájera # License: BSD 3 clause Invalid Python code sphinx-gallery-0.16.0/examples/plot_0_sin.py000066400000000000000000000064261461331107500210130ustar00rootroot00000000000000r""" Introductory example - Plotting sin =================================== This is a general example demonstrating a Matplotlib plot output, embedded reST, the use of math notation and cross-linking to other examples. It would be useful to compare the :download:`source Python file ` with the output below. Source files for gallery examples should start with a triple-quoted header docstring. Anything before the docstring is ignored by Sphinx-Gallery and will not appear in the rendered output, nor will it be executed. This docstring requires a reST header, which is used as the title of the example and to correctly build cross-referencing links. Code and embedded reST text blocks follow the docstring. The first block immediately after the docstring is deemed a code block, by default, unless you specify it to be a text block using a line of ``#``'s or ``#%%`` (see below). All code blocks get executed by Sphinx-Gallery and any output, including plots will be captured. Typically, code and text blocks are interspersed to provide narrative explanations of what the code is doing or interpretations of code output. Mathematical expressions can be included as LaTeX, and will be rendered with MathJax. To include displayed math notation, use the directive ``.. math::``. To include inline math notation use the ``:math:`` role. For example, we are about to plot the following function: .. math:: x \rightarrow \sin(x) Here the function :math:`\sin` is evaluated at each point the variable :math:`x` is defined. When including LaTeX in a Python string, ensure that you escape the backslashes or use a :ref:`raw docstring `. You do not need to do this in text blocks (see below). """ # Code source: Óscar Nájera # License: BSD 3 clause import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() # %% # To include embedded reST, use a line of >= 20 ``#``'s or ``#%%`` between your # reST and your code (see :ref:`embedding_rst`). This separates your example # into distinct text and code blocks. You can continue writing code below the # embedded reST text block: print("This example shows a sin plot!") # %% # LaTeX syntax in the text blocks does not require backslashes to be escaped: # # .. math:: # \sin # # .. _cross_ref_example: # # Cross referencing # ^^^^^^^^^^^^^^^^^ # # You can refer to an example from any part of the documentation, # including from other examples. Sphinx-Gallery automatically creates reference # labels for each example. The label consists of the ``.py`` file name, # prefixed with ``sphx_glr_`` and the name of the # folder(s) the example is in. Below, the example we want to # cross-reference is in ``auto_examples`` (the ``gallery_dirs``; see # :ref:`configure_and_use_sphinx_gallery`), then the subdirectory ``no_output`` # (since the example is within a sub-gallery). The file name of the example is # ``plot_syntaxerror.py``. We can thus cross-link to the example 'SyntaxError' # using: # ``:ref:`sphx_glr_auto_examples_no_output_plot_syntaxerror.py```. # # .. seealso:: # See :ref:`sphx_glr_auto_examples_no_output_plot_syntaxerror.py` for # an example with an error. # # .. |docstring| replace:: """ sphinx-gallery-0.16.0/examples/plot_1_exp.py000066400000000000000000000021461461331107500210120ustar00rootroot00000000000000""" Plotting the exponential function ================================= This example demonstrates how to import a local module and how images are stacked when two plots are created in one code block. The variable ``N`` from the example 'Local module' (file ``local_module.py``) is imported in the code below. Further, note that when there is only one code block in an example, the output appears before the code block. """ # Code source: Óscar Nájera # License: BSD 3 clause import numpy as np import matplotlib.pyplot as plt # You can use modules local to the example being run, here we import # N from local_module from local_module import N # = 100 def main(): """Plot exponential functions.""" x = np.linspace(-1, 2, N) y = np.exp(x) plt.figure() plt.plot(x, y) plt.xlabel("$x$") plt.ylabel(r"$\exp(x)$") plt.title("Exponential function") plt.figure() plt.plot(x, -np.exp(-x)) plt.xlabel("$x$") plt.ylabel(r"$-\exp(-x)$") plt.title("Negative exponential\nfunction") # To avoid matplotlib text output plt.show() if __name__ == "__main__": main() sphinx-gallery-0.16.0/examples/plot_2_seaborn.py000066400000000000000000000015251461331107500216500ustar00rootroot00000000000000r""" Seaborn example =============== This example demonstrates a Seaborn plot. Figures produced Matplotlib **and** by any package that is based on Matplotlib (e.g., Seaborn), will be captured by default. See :ref:`image_scrapers` for details. """ # Author: Michael Waskom & Lucy Liu # License: BSD 3 clause import numpy as np import seaborn as sns import matplotlib.pyplot as plt # Enforce the use of default set style # Create a noisy periodic dataset y_array = np.array([]) x_array = np.array([]) rs = np.random.RandomState(8) for _ in range(15): x = np.linspace(0, 30 / 2, 30) y = np.sin(x) + rs.normal(0, 1.5) + rs.normal(0, 0.3, 30) y_array = np.append(y_array, y) x_array = np.append(x_array, x) # Plot the average over replicates with confidence interval sns.lineplot(y=y_array, x=x_array) # to avoid text output plt.show() sphinx-gallery-0.16.0/examples/plot_3_capture_repr.py000066400000000000000000000114531461331107500227140ustar00rootroot00000000000000""" .. _capture_repr_examples: Capturing output representations ================================ This example demonstrates how the configuration ``capture_repr`` (:ref:`capture_repr`) works. The default ``capture_repr`` setting is ``capture_repr: ('_repr_html_', '__repr__')`` and was used when building the Sphinx-Gallery documentation. The output that is captured with this setting is demonstrated in this example. Differences in outputs that would be captured with other ``capture_repr`` settings is also explained. """ # %% # Nothing is captured for the code block below because no data is directed to # standard output and the last statement is an assignment, not an expression. # example 1 a = 2 b = 10 # %% # If you did wish to capture the value of ``b``, you would need to use: # example 2 a = 2 b = 10 b # this is an expression # %% # Sphinx-Gallery first attempts to capture the ``_repr_html_`` of ``b`` as this # is the first 'representation' method in the ``capture_repr`` tuple. As this # method does not exist for ``b``, Sphinx-Gallery moves on and tries to capture # the ``__repr__`` method, which is second in the tuple. This does exist for # ``b`` so it is captured and the output is seen above. # # A pandas dataframe is used in the code block below to provide an example of # an expression with a ``_repr_html_`` method. # example 3 import pandas as pd df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) df # %% # The pandas dataframe ``df`` has both a ``__repr__`` and ``_repr_html_`` # method. As ``_repr_html_`` appears first in the ``capture_repr`` tuple, the # ``_repr_html_`` is captured in preference to ``__repr__``. # # For the example below, there is data directed to standard output and the last # statement is an expression. # example 4 print("Hello world") a + b # %% # Statsmodels tables should also be styled appropriately: # example 5 import numpy as np import statsmodels.iolib.table statsmodels.iolib.table.SimpleTable(np.zeros((3, 3))) # %% # ``print()`` outputs to standard output, which is always captured. The # string ``'Hello world'`` is thus captured. A 'representation' of the last # expression is also captured. Again, since this expression ``a + b`` does not # have a ``_repr_html_`` method, the ``__repr__`` method is captured. # # Matplotlib output # ################## # # Matplotlib function calls generally return a Matplotlib object as well as # outputting the figure. For code blocks where the last statement is a # Matplotlib expression, a 'representation' of the object will be captured, as # well as the plot. This is because Matplotlib objects have a ``__repr__`` # method and our ``capture_repr`` tuple contains ``__repr__``. Note that # Matplotlib objects also have a ``__str__`` method. # # In the example below, ``matplotlib.pyplot.plot()`` returns a list of # ``Line2D`` objects representing the plotted data and the ``__repr__`` of the # list is captured as well as the figure: import matplotlib.pyplot as plt plt.plot([1, 2, 3]) # %% # To avoid capturing the text representation, you can assign the last Matplotlib # expression to a temporary variable: _ = plt.plot([1, 2, 3]) # %% # Alternatively, you can add ``plt.show()``, which does not return anything, # to the end of the code block: plt.plot([1, 2, 3]) plt.show() # %% # The ``capture_repr`` configuration # ################################## # # The ``capture_repr`` configuration is ``('_repr_html_', '__repr__')`` by # default. This directs Sphinx-Gallery to capture 'representations' of the last # statement of a code block, if it is an expression. Sphinx-Gallery does # this according to the order 'representations' appear in the tuple. With the # default ``capture_repr`` setting, ``_repr_html_`` is attempted to be captured # first. If this method does not exist, the ``__repr__`` method would be # captured. If the ``__repr__`` also does not exist (unlikely for non-user # defined objects), nothing would be captured. For example, if the the # configuration was set to ``'capture_repr': ('_repr_html_')`` nothing would be # captured for example 2 as ``b`` does not have a ``_repr_html_``. # You can change the 'representations' in the ``capture_repr`` tuple to finely # tune what is captured in your example ``.py`` files. # # To only capture data directed to standard output you can set ``capture_repr`` # to be an empty tuple: ``capture_repr: ()``. With this setting, only data # directed to standard output is captured. For the examples above, output would # only be captured for example 4. Although the last statement is an expression # for examples 2, 3 and 4 no 'representation' of the last expression would be # output. You would need to add ``print()`` to the last expression to capture # a 'representation' of it. The empty tuple setting imitates the behaviour of # Sphinx-Gallery prior to v0.5.0, when this configuration was introduced. sphinx-gallery-0.16.0/examples/plot_4_choose_thumbnail.py000066400000000000000000000021611461331107500235410ustar00rootroot00000000000000""" Choosing the thumbnail figure ============================= This example demonstrates how to choose the figure that is displayed as the thumbnail, if the example generates more than one figure. This is done by specifying the keyword-value pair ``sphinx_gallery_thumbnail_number = `` as a comment somewhere below the docstring in the example file. In this example, we specify that we wish for the second figure to be the thumbnail. """ # Code source: Óscar Nájera # License: BSD 3 clause import numpy as np import matplotlib.pyplot as plt def main(): """Plot expoential functions.""" x = np.linspace(-1, 2, 100) y = np.exp(x) plt.figure() plt.plot(x, y) plt.xlabel("$x$") plt.ylabel(r"$\exp(x)$") # The next line sets the thumbnail for the second figure in the gallery # (plot with negative exponential in orange) # sphinx_gallery_thumbnail_number = 2 plt.figure() plt.plot(x, -np.exp(-x), color="orange", linewidth=4) plt.xlabel("$x$") plt.ylabel(r"$-\exp(-x)$") # To avoid matplotlib text output plt.show() if __name__ == "__main__": main() sphinx-gallery-0.16.0/examples/plot_4b_provide_thumbnail.py000066400000000000000000000016251461331107500240770ustar00rootroot00000000000000""" Providing a figure for the thumbnail image ========================================== This example demonstrates how to provide a figure that is displayed as the thumbnail. This is done by specifying the keyword-value pair ``sphinx_gallery_thumbnail_path = 'fig path'`` as a comment somewhere below the docstring in the example file. In this example, we specify that we wish the figure ``demo.png`` in the folder ``_static`` to be used for the thumbnail. """ import numpy as np import matplotlib.pyplot as plt # sphinx_gallery_thumbnail_path = '_static/demo.png' # %% x = np.linspace(0, 4 * np.pi, 301) y1 = np.sin(x) y2 = np.cos(x) # %% # Plot 1 # ------ plt.figure() plt.plot(x, y1, label="sin") plt.plot(x, y2, label="cos") plt.legend() plt.show() # %% # Plot 2 # ------ plt.figure() plt.plot(x, y1, label="sin") plt.plot(x, y2, label="cos") plt.legend() plt.xscale("log") plt.yscale("log") plt.show() sphinx-gallery-0.16.0/examples/plot_5_unicode_everywhere.py000066400000000000000000000016641461331107500241210ustar00rootroot00000000000000""" Using Unicode everywhere 🤗 =========================== This example demonstrates how to include non-ASCII characters, mostly emoji 🎉 to stress test the build and test environments that parse the example files. """ # 🎉 👍 # Code source: Óscar Nájera # License: BSD 3 clause import numpy as np import matplotlib.pyplot as plt plt.rcParams["font.size"] = 20 plt.rcParams["font.monospace"] = ["DejaVu Sans Mono"] plt.rcParams["font.family"] = "monospace" plt.figure() x = np.random.randn(100) * 2 + 1 y = np.random.randn(100) * 6 + 3 s = np.random.rand(*x.shape) * 800 + 500 plt.scatter(x, y, s, marker=r"$\oint$") x = np.random.randn(60) * 7 - 4 y = np.random.randn(60) * 3 - 2 s = s[: x.size] plt.scatter(x, y, s, alpha=0.5, c="g", marker=r"$\clubsuit$") plt.xlabel("⇒") plt.ylabel("⇒") plt.title("♲" * 10) print("Std out capture 😎") # To avoid matplotlib text output plt.show() # %% # Debug fonts print(plt.rcParams) sphinx-gallery-0.16.0/examples/plot_6_function_identifier.py000066400000000000000000000051371461331107500242550ustar00rootroot00000000000000""" Identifying function names in a script ====================================== This demonstrates how Sphinx-Gallery identifies when 1. a function/method/attribute/object is used or class instantiated in a code block 2. a function/method/attribute/object/class is referred to using sphinx markup in a text block. Sphinx-Gallery examines both the executed code itself, as well as the text blocks (such as this one, or the one below) for these references and identifies the module they belong to. This means that by writing :obj:`numpy.sin` and :obj:`numpy.exp` here, they will be identified even though they are not used in the code. This is useful in particular when functions return classes (meaning it is not explicitly instantiated) -- if you add them to the documented blocks of examples that use them, they will be added to backreferences. This functionality is used to add documentation hyperlinks to your code (:ref:`link_to_documentation`) and for mini-gallery creation (:ref:`references_to_examples`). """ # Code source: Óscar Nájera # License: BSD 3 clause import os.path as op # noqa, analysis:ignore import matplotlib.pyplot as plt import sphinx_gallery from sphinx_gallery.backreferences import identify_names, _make_ref_regex from sphinx_gallery.py_source_parser import split_code_and_text_blocks filename = "plot_6_function_identifier.py" if not op.exists(filename): filename = ( __file__ if "__file__" in locals() else op.join(op.dirname(sphinx_gallery.__path__[0]), "examples", filename) ) _, script_blocks = split_code_and_text_blocks(filename) names = identify_names(script_blocks, _make_ref_regex()) # %% # In the code block above, we use the internal function ``identify_names`` to # obtain all identified names from this file and their full resolved import # path. We then plot this below, where the identified names functions are # on the left and the full resolved import path is on the right. fontsize = 12.5 figheight = 3 * len(names) * fontsize / 72 fig, ax = plt.subplots(figsize=(7.5, figheight)) ax.set_visible(False) for i, (name, obj) in enumerate(names.items(), 1): fig.text( 0.48, 1 - i / (len(names) + 1), name, ha="right", va="center", size=fontsize, transform=fig.transFigure, bbox=dict(boxstyle="square", fc="w", ec="k"), ) fig.text( 0.52, 1 - i / (len(names) + 1), obj[0]["module"], ha="left", va="center", size=fontsize, transform=fig.transFigure, bbox=dict(boxstyle="larrow,pad=0.1", fc="w", ec="k"), ) plt.show() sphinx-gallery-0.16.0/examples/plot_7_sys_argv.py000066400000000000000000000013621461331107500220600ustar00rootroot00000000000000""" Using ``sys.argv`` in examples ============================== This example demonstrates the use of ``sys.argv`` in example ``.py`` files. By default, all example ``.py`` files will be run by Sphinx-Gallery **without** any arguments. Notice below that ``sys.argv`` is a list consisting of only the file name. Further, any arguments added will take on the default value. This behavior can be changed by using the `reset_argv` option in the sphinx configuration, see :ref:`reset_argv`. """ # noqa: E501 import argparse import sys parser = argparse.ArgumentParser(description="Toy parser") parser.add_argument("--option", default="default", help="a dummy optional argument") print("sys.argv:", sys.argv) print("parsed args:", parser.parse_args()) sphinx-gallery-0.16.0/examples/plot_8_animations.py000066400000000000000000000013671461331107500223730ustar00rootroot00000000000000""" Matplotlib animation support ============================ Show a Matplotlib animation, which should end up nicely embedded below. In order to enable support for animations ``'matplotlib_animations'`` must be set to ``True`` in the sphinx gallery :ref:`configuration `. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation # Adapted from # https://matplotlib.org/gallery/animation/basic_example.html def _update_line(num): line.set_data(data[..., :num]) return (line,) fig, ax = plt.subplots() data = np.random.RandomState(0).rand(2, 25) (line,) = ax.plot([], [], "r-") ax.set(xlim=(0, 1), ylim=(0, 1)) ani = animation.FuncAnimation(fig, _update_line, 25, interval=100, blit=True) sphinx-gallery-0.16.0/ignore_words.txt000066400000000000000000000000071461331107500200040ustar00rootroot00000000000000master sphinx-gallery-0.16.0/plotly_examples/000077500000000000000000000000001461331107500177665ustar00rootroot00000000000000sphinx-gallery-0.16.0/plotly_examples/README.rst000066400000000000000000000003001461331107500214460ustar00rootroot00000000000000.. _plotly-examples-index: Plotly Gallery ============== .. _general_plotly_examples: Plotly Examples --------------- Examples from the Sphinx-Gallery using Plotly for embedding 3D plots. sphinx-gallery-0.16.0/plotly_examples/plot_0_plotly.py000066400000000000000000000052721461331107500231460ustar00rootroot00000000000000""" ======================================== Example with the plotly graphing library ======================================== Sphinx-Gallery supports examples made with the `plotly library`_. Sphinx-Gallery is able to capture the ``_repr_html_`` of plotly figure objects (see :ref:`capture_repr`). To display the figure, the last line in your code block should therefore be the plotly figure object. In order to use plotly, the ``conf.py`` of the project should include the following lines to select the appropriate plotly renderer:: import plotly.io as pio pio.renderers.default = 'sphinx_gallery' **Optional**: the ``sphinx_gallery`` renderer of plotly will not generate png thumbnails. For png thumbnails, you can use instead the ``sphinx_gallery_png`` renderer, and add ``plotly.io._sg_scraper.plotly_sg_scraper`` to the list of :ref:`image_scrapers`. The scraper requires you to `install the orca package `_. This tutorial gives a few examples of plotly figures, starting with its high-level API `plotly express `_. .. _plotly library: https://plotly.com/python/ """ import plotly.express as px import numpy as np df = px.data.tips() fig = px.bar( df, x="sex", y="total_bill", facet_col="day", color="smoker", barmode="group", template="presentation+plotly", ) fig.update_layout(height=400) fig # %% # In addition to the classical scatter or bar charts, plotly provides a large # variety of traces, such as the sunburst hierarchical trace of the following # example. plotly is an interactive library: click on one of the continents # for a more detailed view of the drill-down. df = px.data.gapminder().query("year == 2007") fig = px.sunburst( df, path=["continent", "country"], values="pop", color="lifeExp", hover_data=["iso_alpha"], color_continuous_scale="RdBu", color_continuous_midpoint=np.average(df["lifeExp"], weights=df["pop"]), ) fig.update_layout(title_text="Life expectancy of countries and continents") fig # %% # While plotly express is often the high-level entry point of the plotly # library, complex figures mixing different types of traces can be made # with the low-level ``graph_objects`` imperative API. from plotly.subplots import make_subplots import plotly.graph_objects as go fig = make_subplots(rows=1, cols=2, specs=[[{}, {"type": "domain"}]]) fig.add_trace(go.Bar(x=[2018, 2019, 2020], y=[3, 2, 5], showlegend=False), 1, 1) fig.add_trace(go.Pie(labels=["A", "B", "C"], values=[1, 3, 6]), 1, 2) fig.update_layout(height=400, template="presentation", yaxis_title_text="revenue") fig # sphinx_gallery_thumbnail_path = '_static/plotly_logo.png' sphinx-gallery-0.16.0/pyproject.toml000066400000000000000000000100001461331107500174500ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools", "setuptools-scm", ] [project] name = "sphinx-gallery" description = "A Sphinx extension that builds an HTML gallery of examples from any set of Python scripts." readme = "README.rst" license = {"text" = '3-clause BSD'} authors = [ {name = "Óscar Nájera", email = 'najera.oscar@gmail.com'}, ] requires-python = '>=3.8' classifiers = [ 'Development Status :: 4 - Beta', 'Framework :: Sphinx :: Extension', 'Intended Audience :: Developers', 'Programming Language :: Python', "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dynamic = [ "version", ] dependencies = [ "pillow", "sphinx>=4", ] [project.optional-dependencies] jupyterlite= [ "jupyterlite_sphinx", ] recommender = [ "numpy", ] show_api_usage= [ "graphviz", ] show_memory= [ "memory_profiler", ] [project.urls] Documentation = "https://sphinx-gallery.github.io" Source = "https://github.com/sphinx-gallery/sphinx-gallery" [project.scripts] sphinx_gallery_py2jupyter = "sphinx_gallery.notebook:python_to_jupyter_cli" [tool.setuptools] include-package-data = true [tool.setuptools.dynamic] version = {attr = "sphinx_gallery.__version__"} [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) include = ["sphinx_gallery*"] # package names should match these glob patterns (["*"] by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) [tool.ruff] lint.select = ["E", "F", "W", "D"] exclude = ["__init__.py"] lint.ignore = [ # TODO: A lot of these we should actually fix eventually "D105", # Missing docstring in magic method "D401", # First line of docstring should be in imperative mood ] [tool.ruff.lint.pydocstyle] convention = "numpy" [tool.ruff.lint.per-file-ignores] "sphinx_gallery/tests/*.py" = [ "E501", # line too long "D103", # Missing docstring in public function ] "examples/no_output/plot_syntaxerror.py" = [ "E999", # SyntaxError ] "tutorials/plot_parse.py" = [ "D202", # [*] No blank lines allowed after function docstring (found 1) "D209", # Multi-line docstring closing quotes should be on a separate line ] # Example files cannot satisfy below rules due to header and imports in code blocks "*examples*/*.py" = [ "D400", # First line should end with a period "E402", # Module level import not at top of file "D205", # 1 blank line required between summary line and description ] "tutorials/*.py" = [ "D400", # First line should end with a period "E402", # Module level import not at top of file "D205", # 1 blank line required between summary line and description ] "bin/*.py" = [ "D400", # First line should end with a period "D205", # 1 blank line required between summary line and description ] "sphinx_gallery/tests/testconfs/src/*.py" = [ "D400", # First line should end with a period "D205", # 1 blank line required between summary line and description ] [tool.codespell] builtin = "clear,rare,informal,names,usage" ignore-words = "ignore_words.txt" [tool.pytest.ini_options] addopts = [ "--color=yes", "--cov-report=", "--cov=sphinx_gallery", "--durations=5", "-r a", "--tb=short", "--junit-xml=junit-results.xml", ] python_files = "tests/*.py" norecursedirs = "build _build auto_examples gen_modules sphinx_gallery/tests/tinybuild" filterwarnings = [ "ignore:.*HasTraits.trait_.*:DeprecationWarning", "ignore:.*importing the ABCs.*:DeprecationWarning", "ignore:np.loads is deprecated, use pickle.loads instead:DeprecationWarning", "ignore:'U' mode is deprecated:DeprecationWarning", "ignore:node class .* is already registered.*:", "ignore:node.Node.* is obsoleted by Node.*:", ] junit_family = "xunit2" markers = [ "conf_file: Configuration file." ] sphinx-gallery-0.16.0/pyvista_examples/000077500000000000000000000000001461331107500201425ustar00rootroot00000000000000sphinx-gallery-0.16.0/pyvista_examples/README.rst000066400000000000000000000007611461331107500216350ustar00rootroot00000000000000.. _pyvista-examples-index: PyVista Gallery =============== .. _general_pyvista_examples: PyVista Examples ---------------- Examples from the Sphinx-Gallery using PyVista for embedding 3D plots. `Learn more about PyVista `_ and see their `extensive examples gallery `_ built with Sphinx-Gallery! Take a look at the :ref:`image_scrapers` section to learn more about how to enable and use PyVista in your example gallery. sphinx-gallery-0.16.0/pyvista_examples/plot_collisions.py000066400000000000000000000046731461331107500237420ustar00rootroot00000000000000""" .. _collision_example: Collision ~~~~~~~~~ Perform a collision detection between two meshes. This example uses the ``collision`` filter to detect the faces from one sphere colliding with another sphere. .. note:: Due to the nature of the `vtk.vtkCollisionDetectionFilter `_, repeated uses of this method will be slower that using the ``vtk.vtkCollisionDetectionFilter`` directly. The first update of the filter creates two instances of `vtkOBBTree `_, which can be subsequently updated by modifying the transform or matrix of the input meshes. This method assumes no transform and is easier to use for single collision tests, but it is recommended to use a combination of ``pyvista`` and ``vtk`` for rapidly computing repeated collisions. See the `Collision Detection Example `_ """ import numpy as np import pyvista as pv pv.set_plot_theme("document") ############################################################################### # Create the main mesh and the secondary "moving" mesh. # # Collision faces will be plotted on this sphere, and to do so we # initialize an initial ``"collisions"`` mask. sphere0 = pv.Sphere() sphere0["collisions"] = np.zeros(sphere0.n_cells, dtype=bool) # This mesh will be the moving mesh sphere1 = pv.Sphere(radius=0.6, center=(-1, 0, 0)) ############################################################################### # Setup the plotter open a movie, and write a frame after moving the sphere. # pl = pv.Plotter() pl.enable_hidden_line_removal() pl.add_mesh(sphere0, show_scalar_bar=False, cmap="bwr") pl.camera_position = "xz" pl.add_mesh(sphere1, style="wireframe", color="green", line_width=5) # for this example pl.open_gif("collision_movie.gif") # alternatively, to disable movie generation: # pl.show(auto_close=False, interactive=False) delta_x = 0.05 for i in range(int(2 / delta_x)): sphere1.translate([delta_x, 0, 0]) col, n_contacts = sphere0.collision(sphere1) collision_mask = np.zeros(sphere0.n_cells, dtype=bool) if n_contacts: collision_mask[col["ContactCells"]] = True sphere0["collisions"] = collision_mask pl.write_frame() # alternatively, disable movie plotting and simply render the image # pl.render() pl.close() sphinx-gallery-0.16.0/pyvista_examples/plot_glyphs.py000066400000000000000000000052301461331107500230600ustar00rootroot00000000000000""" .. _glyph_example: Plotting Glyphs (Vectors or PolyData) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use vectors in a dataset to plot and orient glyphs/geometric objects. """ import numpy as np # sphinx_gallery_thumbnail_number = 4 import pyvista as pv from pyvista import examples ############################################################################### # Glyphying can be done via the :meth:`pyvista.DataSetFilters.glyph` filter mesh = examples.download_carotid().threshold(145, scalars="scalars") mask = mesh["scalars"] < 210 mesh["scalars"][mask] = 0 # null out smaller vectors # Make a geometric object to use as the glyph geom = pv.Arrow() # This could be any dataset # Perform the glyph glyphs = mesh.glyph(orient="vectors", scale="scalars", factor=0.003, geom=geom) # plot using the plotting class pl = pv.Plotter() pl.add_mesh(glyphs, show_scalar_bar=False, lighting=False, cmap="coolwarm") pl.camera_position = [ (146.53, 91.28, 21.70), (125.00, 94.45, 19.81), (-0.086, 0.007, 0.996), ] # view only part of the vector field cpos = pl.show(return_cpos=True) ############################################################################### # Another approach is to load the vectors directly to the mesh object and then # access the :attr:`pyvista.DataSet.arrows` property. sphere = pv.Sphere(radius=3.14) # make cool swirly pattern vectors = np.vstack( ( np.sin(sphere.points[:, 0]), np.cos(sphere.points[:, 1]), np.cos(sphere.points[:, 2]), ) ).T # add and scale sphere["vectors"] = vectors * 0.3 sphere.set_active_vectors("vectors") # plot just the arrows sphere.arrows.plot() ############################################################################### # Plot the arrows and the sphere. p = pv.Plotter() p.add_mesh(sphere.arrows, lighting=False, scalar_bar_args={"title": "Vector Magnitude"}) p.add_mesh(sphere, color="grey", ambient=0.6, opacity=0.5, show_edges=False) p.show() ############################################################################### # Subset of Glyphs # ++++++++++++++++ # # Sometimes you might not want glyphs for every node in the input dataset. In # this case, you can choose to build glyphs for a subset of the input dataset # by using a merging tolerance. Here we specify a merging tolerance of five # percent which equates to five percent of the bounding box's length. # Example dataset with normals mesh = examples.load_random_hills() # create a subset of arrows using the glyph filter arrows = mesh.glyph(scale="Normals", orient="Normals", tolerance=0.05) p = pv.Plotter() p.add_mesh(arrows, color="black") p.add_mesh(mesh, scalars="Elevation", cmap="terrain", smooth_shading=True) p.show() sphinx-gallery-0.16.0/pyvista_examples/plot_lighting.py000066400000000000000000000036351461331107500233660ustar00rootroot00000000000000""" .. _ref_lighting_properties_example: Lighting Properties ~~~~~~~~~~~~~~~~~~~ Control aspects of the rendered mesh's lighting such as Ambient, Diffuse, and Specular. These options only work if the ``lighting`` argument to ``add_mesh`` is ``True`` (it's ``True`` by default). You can turn off all lighting for the given mesh by passing ``lighting=False`` to ``add_mesh``. """ # sphinx_gallery_thumbnail_number = 4 import pyvista as pv from pyvista import examples mesh = examples.download_st_helens().warp_by_scalar() cpos = [ (575848.0, 5128459.0, 22289.0), (562835.0, 5114981.5, 2294.5), (-0.5, -0.5, 0.7), ] ############################################################################### # First, lets take a look at the mesh with default lighting conditions mesh.plot(cpos=cpos, show_scalar_bar=False) ############################################################################### # What about with no lighting mesh.plot(lighting=False, cpos=cpos, show_scalar_bar=False) ############################################################################### # Demonstration of the specular property p = pv.Plotter(shape=(1, 2), window_size=[1500, 500]) p.subplot(0, 0) p.add_mesh(mesh, show_scalar_bar=False) p.add_text("No Specular") p.subplot(0, 1) s = 1.0 p.add_mesh(mesh, specular=s, show_scalar_bar=False) p.add_text(f"Specular of {s}") p.link_views() p.view_isometric() p.show(cpos=cpos) ############################################################################### # Just specular mesh.plot(specular=0.5, cpos=cpos, show_scalar_bar=False) ############################################################################### # Specular power mesh.plot(specular=0.5, specular_power=15, cpos=cpos, show_scalar_bar=False) ############################################################################### # Demonstration of all three in use mesh.plot(diffuse=0.5, specular=0.5, ambient=0.5, cpos=cpos, show_scalar_bar=False) sphinx-gallery-0.16.0/pyvista_examples/plot_ray_trace.py000066400000000000000000000013441461331107500235250ustar00rootroot00000000000000""" .. _ray_trace_example: Ray Tracing ~~~~~~~~~~~ Single line segment ray tracing for PolyData objects. """ import pyvista as pv # Create source to ray trace sphere = pv.Sphere(radius=0.85) # Define line segment start = [0, 0, 0] stop = [0.25, 1, 0.5] # Perform ray trace points, ind = sphere.ray_trace(start, stop) # Create geometry to represent ray trace ray = pv.Line(start, stop) intersection = pv.PolyData(points) # Render the result p = pv.Plotter() p.add_mesh( sphere, show_edges=True, opacity=0.5, color="w", lighting=False, label="Test Mesh" ) p.add_mesh(ray, color="blue", line_width=5, label="Ray Segment") p.add_mesh(intersection, color="maroon", point_size=25, label="Intersection Points") p.add_legend() p.show() sphinx-gallery-0.16.0/sphinx_gallery/000077500000000000000000000000001461331107500175755ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/__init__.py000066400000000000000000000005351461331107500217110ustar00rootroot00000000000000""" Sphinx Gallery ============== """ import os # dev versions should have "dev" in them, stable should not. # doc/conf.py makes use of this to set the version drop-down. __version__ = "0.16.0" def glr_path_static(): """Returns path to packaged static files""" return os.path.abspath(os.path.join(os.path.dirname(__file__), "_static")) sphinx-gallery-0.16.0/sphinx_gallery/_static/000077500000000000000000000000001461331107500212235ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/_static/binder_badge_logo.svg000066400000000000000000000064641461331107500253630ustar00rootroot00000000000000 launchlaunchbinderbinder sphinx-gallery-0.16.0/sphinx_gallery/_static/broken_example.png000066400000000000000000000516341461331107500247350ustar00rootroot00000000000000PNG  IHDR@rsBIT|d pHYs  tEXtSoftwarewww.inkscape.org< IDATxweoz $z ޤ DĆEE`r "":!BK-ϚٳO99{>|>sΞ=yfgk tmuB{/ ""}`!0 Xv^RDDDD}a` m2k :lopAܾ""""G m\WDDDD`AWOtt0[rkX` S[L*sHk %җ,)sHk 66\=y"`#q[x#b }{!dٽL@=gEiwz& ^z W/X9f8v߶{QpMB^@DDD`LqȒ֠' DcY["=VwA.JsXj?gw50XX [q2<LCe|-Pq߶36y# GD40 2x$b?#koBK~DA$?Ki`DDo{/%/@V:4 {w2֞Q-I5r{|wP.^Xs<ƾ~wPLpCLDD42z(q`H2%<^i Lf0=cM|C#  ,(_Jv#2|Beopj$<(F `DD;Iew`l?8UsћZhͺG;3Az~Y0D6*Hޒ!5T;S&èqFPeLk7b 0"dU@zI^F`x8 kX րH#"J`A׼lHЀWrW[}z(B-f`GF#" Src_m,pw7G1CQmh)% Х|Yۢq[~fz `DDiAHԭ$I7EKGMer[ɍֈ.pDD$U cNޅo'T x̸O5DM9&{\'XD蓃֛ZTFG$6)y pc8_Os|͐yDX{| ?yFV]$yvR6"bGAoS'~~|E~"YChC^cwm;kFFDLC0_y2WdH$k)j>î 6<)8`iA# #"ZϚ  cvL4X2{'EI܁s &n(u9jİ B\>O~ϣJ(oX6oű5N"FfӰL64-lbi ?tk"-gw.[o;zRt:,pD*`9rWP ܋H7x;5 kw;;G@֧R3'AС:"Lxt'}s8SQ/Д^7ܱ's7f#" "3зJ.zZx'Q*v;g&T $cۮ~Y dKaN\̶n#uP]M!e(I2ȈVó{ LiAHSf揭: r26Zs>/e$=YuDh3 `Re1z1o$ XkI? kkck"?#2Yh= ,tk,9w YjD]f-ivnJA$x.uAVdV2 "2:CIOBɗhlf@u6%]J3D0]+2%莪N5݂vyl诓XɺȻ{oEfl-O kZJG# ,ϣ,O,|$X`SoDY lrFYndrٛ'pr,uҝX =<)`ڂekgDB̺]'S|{@ڄ`rJX^c:י|(S s^o4Ci]!` >|P{e'_E2!WvǑ16U[߭NPy(3{3ɜ8%,yNXCFiɡ`? 6iKYMmj!(7zJw"u2\dߖ)5$^W :h4ǎGD'=?Ze"`=U \]G[; ˫0j$ GW$р;JZj̮mHf,f9?̺-  >oz2|-.PNbjݑ*7}ra; ;{&_`ijw)g`H1f0䦿g@&ܘ\wD$ 7x/?bbhˀ'`DžbpX&Y$cF_\>EA℻Z5y H .VI@n1:'b1F#kcDPIGYBb 7a|[T~)h9ExN"з"Y"FtllE5 b_F?sݪY`|Yk. 5HQ&kp.Hd- 7pJB죉罆HXQbː&*uǠU`}KEIhhi -oŹT ]2Nn. 1'Q)9g$^@9s0 `k%I^D18|sjhrzb2r{s=`Kn7D 0!HQ.y֠O[~^A#J)%`g(du181_Yk,4#_Pb}lgq1?k"MƁ⦫`~Pr>J1Zsrĺa9< ;C dּ5>m{r'՞.X$cvڈQ6UѐIPwfP7y}=xr=e9Jl LjtmtΜ+NE' Rg&>vXdY^Ҋó3ӇUxYVɾ}1'Xvow*%7o,ys 0*KnMK cI[.y[;>bph "3n D\nqr[YJn<׆T5͐,qOzYx!DlGV"qy%jk҂57Ye6O%f ?D̥-M3  L2XsXHk1j*uZ-xhXrT;&qtn{-5EYaw6W'O %/*j  vKDFj%ߖ !#wLNU{(Lы&y)~ԈOG+ =Qfzt Zk ܂8Y$:$8 ]DߗeGkz$+p6F4q^MfyAYO UD5D3Prrȟ? Cr"U*%7ۙ\̺ Y~ΐ"'% QYBDm;+!S@wQӂRTQ Dwo$vRb?j4e񿇩YD]l3x H| pBXT\+X}_ Ӆ݃Ԓpz K4Oԩ2iж.bLy?cIq(Jn1~h} Ѝ;V_JJ#InXv-8G᥄TZH&ہ!'5]@ۣp|gK /aj'tA?E9%A?4cdjD-GeHXODl"DNFoN&αR?#\wyGR&]؝'**˗uEkd/ Y"v0|P& WtǞ;R!҆#Ÿ§ ~{]} g2n$)wdYDZ`^oh.CQe&Lem )i'{ %7*kmH/A(Et<ܐ_7-7"$f\ ڸ?/=H'ݖ&p? 3ȺGP>v8d"liCI#i+xbf,"ُ=ݧA IDATJ:`߳f!`Ãg!OfDP*45KgI;!9VMFUҟz hwg흎yH8$>f PD>uV6r݅s,|CШRH t~ݍ \?Ҁymӑ#p+頡?WCuAj7ۊ+ 7_ ]@[Y[u^|@>\_GkލDl@w] yiA.{25L.VR"WVy/Q?#s!efMM+ OYux)J#!`w9[rC}A,("&-0j[K5{oGC֎JxʥJfe?>N=Y74Y>OS\4@*j;:"6L#PܾuїZ5LĸW E1ι I<i& P;{x'ֳM`n ;IEz"IoxS׵(|<텈ۭ)؎p ]+#iDD~ whuXɐ'IA*xD3.yUWSmB:Zf5Y~ LBxD kIaf32Е= `L|P ,|)?AesIp6-$['D '$+J;~1L8YGEbfǣpA,4%.GYzB?C"|!տZc{"`;e!An@:D.{BI \e Q-{ҮumJ><ZX[ܮǨll vG1AT^#t3dB2"Gr|"װ1ՑwB2Ea_Zn BDIr?T[5KAL6!Wev@Ĥ(&8`'d[yijG./*K+u"7)B~Y9yqtJҬ jW{%aP|೹SE'*(jYE?Ȫ3$.׼!B'̚~ 'fF.18Z?!oJ nj_N5F|8%ݣP[*Hj,ӤXDҖ!5N"rۗ#W1[Ȫ+!OI<\Y퇬,yz ŧ]o6"dHґyL3}6ۡʸi%05o뱼J&fMW?,'$:",m>(۟<>.֗q7k>E:Ob)>bS[m%7?`T/UbACr_rkpWo""סyyLA|bA;%tFCZ9Ã:.?2 )z"ro 4z p5aoFZQi}!Wl^uAq}="n+DRI0ڏ[ oIAS9sN1~oF%yr[\A p1J\H";r(j[zlolɈAYXpFH{)b0ǹ(@qtuv(%6?@N2tZO7%@@STw;:ge1,8}Qp3\SO#Vb|7 _ǽNs$6I(E~ "IC T:]PA;R)c{`?!H5>Pch0 Ie㿐zxf[@x[fL55cǵ@+"QwE.x͊}Ư >q3xA5tWu'-!2oD:&ҤZ\x n.}Ոw2ɲ 0 YDUQӇ^kfM""_&l%e#XN0Y]g# DTRIT wNtN3>A/(v2\r{!?'X?a%@'Vkb}w&</kk "6&|TP!g_++FTK*~!!c۞]@VPHHq]ʖDB_쨟5# tg S8H$Kz Kːڡ5 "6ģ? [qr]KɤN8%F#78Q:;d{e_'*wIV3a3bCtۧLպ'{JB&k`tU2hb$Ttv2>?4Kw2mEұB0bԾheg*gdB'n 7zDfCczеAN8 h">"`VCnbeU7Dw1^.S {H1 G:uFa\s)*m[rקya⯡%c< a`Y)c}:t zmH zz8X-40MZёhdṂmm_댠KЍTYiuF^g [ezɱn x,ڂH? d_OQAk5l!vA @R)?LIp 7߸ }Z;uZvAS|ҪG$0eE&&A]A.@Ț:5Y2nH"LJх7eY^0^X‚ln67 0TbZ(SG ua !_D+suB;XQgh}g->߿ Hru'TG D#5?Ro$mp8q$Dx$~ rŖblι^1 IVC6I(;SuxUķf>irA:TD˹F rl.5eϩ\AP Qrs_%Ig"[pXڜs%L%vG"k{ {Cu 8Fb @S7dv74\ґ? ͏Q5&y2IM бڭ U-6.8^EP-8j]-GSv~ri;w0k DVD|<%אΣ\؀5kFd{(Q"XDPOh&uC&;;Lj?*4@79!WP򱠱5=20iAߕ@{|k@YQ7dI'.FݟC^n똓H 7c=Hƫν҉q9 !L`˾ň-Qm4QZGc#@KkN#m]<[(Q9$o3BTWX;# f zT# $8ŧ'\<ߋZC/J.ȭ.p$iOv}PBj9"RGC^u:R~HΓ|Nf ZI;2 `Ӓj:1HBʆ/>f0LzeU 0T=xLFE7WzQ: !vwȍt{uYӑRwBJԞyEQCGP?+Zn}}M5Sx,3 n$<{C| YDkLth4”8 8#H?uw{;AOk &k}>!i$* X7y_2qv˸+H:.g}c++0M녒fPCЦh.r,_k`tH4Xԇ. mzHfq 7%]tn{ AYʣssc/4@<! Lb<]s۟mmt53] ɣ*@.zIR8W}c!Yak]v+I ȇ H; rXc3QLhsH[qj} [3,7%Eg&CYFTZk/u ~m*|ʹ zNài碐H*uYgT۲vD;!*A"Ys]Ԯ#s27l׹.}7M"%, o;$k]Dl7]}n?7QBd)31ISvFdkG$id;W@BȂJ ui0&y;n@]|49/ %nj(J/:Y]ds8Yw#%5 0RWq ,A!6}g7 `AK{ .THn*nm@6 I~G' 9"Ga"3R9)ҶMz*bB@Hvlş,`ut!2 ;I MzU]2, ڹ2O@ zH[kGɜ|N"Il8`*KBV{i]P/C:uQ9ϯe 5ok0JM.׏Đnʶ(n5"2>wcFD"Nb QC[3@%yɡbۛ <ϐGAV ruFvGBXQACBCPZ )=ۡ"X*5Ze}@I/5W.23E>D=Pl Y;8j{ɎVܭU Eq d-;ʓHwئ"  +ti"in]o2|= .7|Z+;"/ n)˘80<q49Y[]2P Yn.:LSH.G@,6A7Qf A.nN"[.\\k?Qf-+Q}2ʈNpM5v" N(['nAVJ1T̥ۜCi8f]}8?#SS| "_*Y QT&%kPV鞅 R?۩)"+L|K%;6ט.c̢-iD(ؗ݊tZѹFI-tA 磲=Cq?->e#"6vVoS 6g!2*E H2Y~d)%,W 74{:Ynx =.Glz<`'Y18*-džgnwGVܟQ,O~ۢQO,eAĚz/n4pH0 % F1-wP,܌$)("RSr[ tF`$*"M1vXnp'S~&fW|jHH+BC킒Dnvhkڜ]vjzoN`אY'T^8cn#H1 ߨSqET3q+JP,xz[-"LHsOMYcr˞k  . Mck1э' \ HzH#>ԫP6.x"Ms4]˵s&mor#}cBJxP ϟ6daϭg\se(.ϙU0Tq]†#mA (h@ԅ»(x&~\b-؛ ?IDAT3ۺz1wX$5 &4n$oۚ `A>,C^@bWP㑵SJ~/G-ܗq@^ "|2j/ j$x۳̱:Ơ }fD*QZGh7wk qv\'b_B-m?57`Oǝ7 V-oK^8T-N cn-w$`{*+j|1ED0ߔ N8$sCn f p>lr[,7?#Inq!27 00mݍXېL1N8N ϛ!U'Z'| z#LL40[ZtB[W5e)Pp>IUKc<_2FD4J x<ϔX՟Igo{ʔҔy۔gO6x?\g0$xnqcMRm,$3=U$%0U\4 $ǹŸ2_K dfg}w7MM=^w\>Vo'w272sk< ~S廝߷U "wyS4R3iL~^K>R/&׽#LFaA72+;v{NwG?eg7Ǥ+h2Ϧ0o[:3dߟH_vs+j*MSƘZ0 X`G:^w99 9/L6M[4o0.6) ~'}LC))|wJ_0(O3UҵS-8.2عuMB>]`u$6 SdSw n)CSގN~YAyZʳ9 S;MdqgL܎3ǔ/SÁynndݽg ?El5P1iƪU2 CNDDD[m%1-Sa wrw'Oipn5ʈ-m9fMM/gM"͛vJ:3I9_*+!=le64JGZa Lqn zeﻵIC8ed3IrFǟd}4tRq}|1=MU+W[nZ^DDD?gn04gJΚZڑS%ʓV0:ӓ5Ǚ$00@"ԥI`mɇfh-v6I|bm""""`cZ.v믟NU#'~$޸ܕFDDtx~6l+Mu_M""""Zw{0ZReawSYg-bv7""bajmIX\%1i`V<8iziZODDZ|͓ >UnnФk 6o9%6GVbI6|MMnяPp\Suǁm5ajs2wmtZ)zVP ?򵈈FbS湿74d&]yHzDDDtA_Tp»(#""߾w?ni 49jjzX۾V*_seM 7MTiR"`/[Lk]#""" /2mg&;4p-DDDDn˷fV3'3]Ϸe3l_p6`2KDDDDapef|Z/2S'8 jA/'xKSE mXA@a1ZB""""V+g SMkv+EDDDtXip*վe+"""Y}kۻ=QWlb$7니h5] LG.po.*""" q/YlSѡ+>7IENDB`sphinx-gallery-0.16.0/sphinx_gallery/_static/broken_stamp.svg000066400000000000000000001500241461331107500244320ustar00rootroot00000000000000 image/svg+xml sphinx-gallery-0.16.0/sphinx_gallery/_static/jupyterlite_badge_logo.svg000066400000000000000000000154301461331107500264710ustar00rootroot00000000000000 launchlaunchlitelitesphinx-gallery-0.16.0/sphinx_gallery/_static/no_image.png000066400000000000000000000103331461331107500235070ustar00rootroot00000000000000PNG  IHDRy XsRGB pHYs B(xtIME+'gbKGDC[IDATx흋o (Wܨ$#hP@p H>Í#VEPEbRvgZh;3{gwnodP3g<"o@Z )h)R$R$RHH@K""E-E]0kH$>6bS#^0{   2{nbXU!fjrƻN0ukUL2m>.-x\DT`uC$yafa vA-YCvh+ t7_6 GrMn{[Nug=x|zcs% 7B n@w@ܗq ^;A'aFd t\hۥZCݠ ڣ%(?`;$KවVs/(=ki]S[7H t1m#"  U",Pa6Շ|:rGz_y'>z%jM(P'|Y~@M:E]@ 1k|1ac2*>Gj8^]H `3X륞5fi)`f9RkE *<F1ay'0@.FE * t>F|¬~>9 IATZ{@}"@PyRYU:4gܬu n!wyO4e)iݠlܣI77A}8Q@ & `|Sf0FABa:^a kz7< = @> Zc;>T܀] =GDm:V@5¬z9 sV.@mA>}6{fP{pBoKh.ߑBSs4ބb t@ ֭- t0lmƆq ځ5[%%l `">G"F8qD4S@k t@ipf:kJw7*h=h@ f(h讅:3Sp l3d3+mOJY; h>+=¡*lѧ $QYvt>]YhC[@/р`輄G-ʐTYy!q9hl4I0;"5?娛'RɨoJD4] r<>R]@0Set8jQ/qt$.\).Kւ}*lCO$ש>+)"Bbŏ;oBF< `/Ѝh{z89}_oE$6xr=ZxMz.s=q;OjtRөY{ٮbs? NL t鳣6.Cv0+Ц]h;GIKOW~[WKr}}fఽwXVmUzrϴT88܌f Y(ś ) OvpPte^KoD砬V#woJэ!ӾuMT7xcOĚIȈ_sKPk"΅NG2L{\ rݗ% +I۪Y?e+ T@`+WT_(r+܋v-r5ЙfmZs݈AuWL{^M^^Gyvf޹d10}x;m8xp-f&:=<ߤIoO_SDl~車i-QU2TТBi# z̯9 [ڜwuΗytOM[jUw{.;1*#Qklxm*EznsTpXZ] Nu׎ODю Oy-E-EZ t}U)RyL(a; iJǑOJ>?MDcf|pͱEuo%e.Y:9yL{e8\m~w:kCۓ(r ФIsC`c2-aX4/ODqj  tP׋{2uSw78$('h:pN +u$=D/qqR/@)5[.C|׬GJfj@z *Xz_ 3>;VP"P?.8<4+Wfڰ&vf},1֔ ЃGDL@Wtpy_0WID__\w- ]{ qTT/<>DߥQXn͡$}0&0FiLo7}IENDB`sphinx-gallery-0.16.0/sphinx_gallery/_static/sg_gallery-binder.css000066400000000000000000000002471461331107500253310ustar00rootroot00000000000000/* CSS for binder integration */ div.binder-badge { margin: 1em auto; vertical-align: middle; } div.lite-badge { margin: 1em auto; vertical-align: middle; } sphinx-gallery-0.16.0/sphinx_gallery/_static/sg_gallery-dataframe.css000066400000000000000000000022001461331107500260010ustar00rootroot00000000000000/* Pandas dataframe css */ /* Taken from: https://github.com/spatialaudio/nbsphinx/blob/fb3ba670fc1ba5f54d4c487573dbc1b4ecf7e9ff/src/nbsphinx.py#L587-L619 */ html[data-theme="light"] { --sg-text-color: #000; --sg-tr-odd-color: #f5f5f5; --sg-tr-hover-color: rgba(66, 165, 245, 0.2); } html[data-theme="dark"] { --sg-text-color: #fff; --sg-tr-odd-color: #373737; --sg-tr-hover-color: rgba(30, 81, 122, 0.2); } table.dataframe { border: none !important; border-collapse: collapse; border-spacing: 0; border-color: transparent; color: var(--sg-text-color); font-size: 12px; table-layout: fixed; width: auto; } table.dataframe thead { border-bottom: 1px solid var(--sg-text-color); vertical-align: bottom; } table.dataframe tr, table.dataframe th, table.dataframe td { text-align: right; vertical-align: middle; padding: 0.5em 0.5em; line-height: normal; white-space: normal; max-width: none; border: none; } table.dataframe th { font-weight: bold; } table.dataframe tbody tr:nth-child(odd) { background: var(--sg-tr-odd-color); } table.dataframe tbody tr:hover { background: var(--sg-tr-hover-color); } sphinx-gallery-0.16.0/sphinx_gallery/_static/sg_gallery-rendered-html.css000066400000000000000000000103521461331107500266160ustar00rootroot00000000000000/* Adapted from notebook/static/style/style.min.css */ html[data-theme="light"] { --sg-text-color: #000; --sg-background-color: #ffffff; --sg-code-background-color: #eff0f1; --sg-tr-hover-color: rgba(66, 165, 245, 0.2); --sg-tr-odd-color: #f5f5f5; } html[data-theme="dark"] { --sg-text-color: #fff; --sg-background-color: #121212; --sg-code-background-color: #2f2f30; --sg-tr-hover-color: rgba(66, 165, 245, 0.2); --sg-tr-odd-color: #1f1f1f; } .rendered_html { color: var(--sg-text-color); /* any extras will just be numbers: */ } .rendered_html em { font-style: italic; } .rendered_html strong { font-weight: bold; } .rendered_html u { text-decoration: underline; } .rendered_html :link { text-decoration: underline; } .rendered_html :visited { text-decoration: underline; } .rendered_html h1 { font-size: 185.7%; margin: 1.08em 0 0 0; font-weight: bold; line-height: 1.0; } .rendered_html h2 { font-size: 157.1%; margin: 1.27em 0 0 0; font-weight: bold; line-height: 1.0; } .rendered_html h3 { font-size: 128.6%; margin: 1.55em 0 0 0; font-weight: bold; line-height: 1.0; } .rendered_html h4 { font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0; } .rendered_html h5 { font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0; font-style: italic; } .rendered_html h6 { font-size: 100%; margin: 2em 0 0 0; font-weight: bold; line-height: 1.0; font-style: italic; } .rendered_html h1:first-child { margin-top: 0.538em; } .rendered_html h2:first-child { margin-top: 0.636em; } .rendered_html h3:first-child { margin-top: 0.777em; } .rendered_html h4:first-child { margin-top: 1em; } .rendered_html h5:first-child { margin-top: 1em; } .rendered_html h6:first-child { margin-top: 1em; } .rendered_html ul:not(.list-inline), .rendered_html ol:not(.list-inline) { padding-left: 2em; } .rendered_html ul { list-style: disc; } .rendered_html ul ul { list-style: square; margin-top: 0; } .rendered_html ul ul ul { list-style: circle; } .rendered_html ol { list-style: decimal; } .rendered_html ol ol { list-style: upper-alpha; margin-top: 0; } .rendered_html ol ol ol { list-style: lower-alpha; } .rendered_html ol ol ol ol { list-style: lower-roman; } .rendered_html ol ol ol ol ol { list-style: decimal; } .rendered_html * + ul { margin-top: 1em; } .rendered_html * + ol { margin-top: 1em; } .rendered_html hr { color: var(--sg-text-color); background-color: var(--sg-text-color); } .rendered_html pre { margin: 1em 2em; padding: 0px; background-color: var(--sg-background-color); } .rendered_html code { background-color: var(--sg-code-background-color); } .rendered_html p code { padding: 1px 5px; } .rendered_html pre code { background-color: var(--sg-background-color); } .rendered_html pre, .rendered_html code { border: 0; color: var(--sg-text-color); font-size: 100%; } .rendered_html blockquote { margin: 1em 2em; } .rendered_html table { margin-left: auto; margin-right: auto; border: none; border-collapse: collapse; border-spacing: 0; color: var(--sg-text-color); font-size: 12px; table-layout: fixed; } .rendered_html thead { border-bottom: 1px solid var(--sg-text-color); vertical-align: bottom; } .rendered_html tr, .rendered_html th, .rendered_html td { text-align: right; vertical-align: middle; padding: 0.5em 0.5em; line-height: normal; white-space: normal; max-width: none; border: none; } .rendered_html th { font-weight: bold; } .rendered_html tbody tr:nth-child(odd) { background: var(--sg-tr-odd-color); } .rendered_html tbody tr:hover { color: var(--sg-text-color); background: var(--sg-tr-hover-color); } .rendered_html * + table { margin-top: 1em; } .rendered_html p { text-align: left; } .rendered_html * + p { margin-top: 1em; } .rendered_html img { display: block; margin-left: auto; margin-right: auto; } .rendered_html * + img { margin-top: 1em; } .rendered_html img, .rendered_html svg { max-width: 100%; height: auto; } .rendered_html img.unconfined, .rendered_html svg.unconfined { max-width: none; } .rendered_html .alert { margin-bottom: initial; } .rendered_html * + .alert { margin-top: 1em; } [dir="rtl"] .rendered_html p { text-align: right; } sphinx-gallery-0.16.0/sphinx_gallery/_static/sg_gallery.css000066400000000000000000000224721461331107500240740ustar00rootroot00000000000000/* Sphinx-Gallery has compatible CSS to fix default sphinx themes Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, scrolls, agogo, traditional, nature, haiku, pyramid Tested for Read the Docs theme 0.1.7 */ /* Define light colors */ :root, html[data-theme="light"], body[data-theme="light"]{ --sg-tooltip-foreground: black; --sg-tooltip-background: rgba(250, 250, 250, 0.9); --sg-tooltip-border: #ccc transparent; --sg-thumb-box-shadow-color: #6c757d40; --sg-thumb-hover-border: #0069d9; --sg-script-out: #888; --sg-script-pre: #fafae2; --sg-pytb-foreground: #000; --sg-pytb-background: #ffe4e4; --sg-pytb-border-color: #f66; --sg-download-a-background-color: #ffc; --sg-download-a-background-image: linear-gradient(to bottom, #ffc, #d5d57e); --sg-download-a-border-color: 1px solid #c2c22d; --sg-download-a-color: #000; --sg-download-a-hover-background-color: #d5d57e; --sg-download-a-hover-box-shadow-1: rgba(255, 255, 255, 0.1); --sg-download-a-hover-box-shadow-2: rgba(0, 0, 0, 0.25); } @media(prefers-color-scheme: light) { :root[data-theme="auto"], html[data-theme="auto"], body[data-theme="auto"] { --sg-tooltip-foreground: black; --sg-tooltip-background: rgba(250, 250, 250, 0.9); --sg-tooltip-border: #ccc transparent; --sg-thumb-box-shadow-color: #6c757d40; --sg-thumb-hover-border: #0069d9; --sg-script-out: #888; --sg-script-pre: #fafae2; --sg-pytb-foreground: #000; --sg-pytb-background: #ffe4e4; --sg-pytb-border-color: #f66; --sg-download-a-background-color: #ffc; --sg-download-a-background-image: linear-gradient(to bottom, #ffc, #d5d57e); --sg-download-a-border-color: 1px solid #c2c22d; --sg-download-a-color: #000; --sg-download-a-hover-background-color: #d5d57e; --sg-download-a-hover-box-shadow-1: rgba(255, 255, 255, 0.1); --sg-download-a-hover-box-shadow-2: rgba(0, 0, 0, 0.25); } } html[data-theme="dark"], body[data-theme="dark"] { --sg-tooltip-foreground: white; --sg-tooltip-background: rgba(10, 10, 10, 0.9); --sg-tooltip-border: #333 transparent; --sg-thumb-box-shadow-color: #79848d40; --sg-thumb-hover-border: #003975; --sg-script-out: rgb(179, 179, 179); --sg-script-pre: #2e2e22; --sg-pytb-foreground: #fff; --sg-pytb-background: #1b1717; --sg-pytb-border-color: #622; --sg-download-a-background-color: #443; --sg-download-a-background-image: linear-gradient(to bottom, #443, #221); --sg-download-a-border-color: 1px solid #3a3a0d; --sg-download-a-color: #fff; --sg-download-a-hover-background-color: #616135; --sg-download-a-hover-box-shadow-1: rgba(0, 0, 0, 0.1); --sg-download-a-hover-box-shadow-2: rgba(255, 255, 255, 0.25); } @media(prefers-color-scheme: dark){ html[data-theme="auto"], body[data-theme="auto"] { --sg-tooltip-foreground: white; --sg-tooltip-background: rgba(10, 10, 10, 0.9); --sg-tooltip-border: #333 transparent; --sg-thumb-box-shadow-color: #79848d40; --sg-thumb-hover-border: #003975; --sg-script-out: rgb(179, 179, 179); --sg-script-pre: #2e2e22; --sg-pytb-foreground: #fff; --sg-pytb-background: #1b1717; --sg-pytb-border-color: #622; --sg-download-a-background-color: #443; --sg-download-a-background-image: linear-gradient(to bottom, #443, #221); --sg-download-a-border-color: 1px solid #3a3a0d; --sg-download-a-color: #fff; --sg-download-a-hover-background-color: #616135; --sg-download-a-hover-box-shadow-1: rgba(0, 0, 0, 0.1); --sg-download-a-hover-box-shadow-2: rgba(255, 255, 255, 0.25); } } .sphx-glr-thumbnails { width: 100%; margin: 0px 0px 20px 0px; /* align thumbnails on a grid */ justify-content: space-between; display: grid; /* each grid column should be at least 160px (this will determine the actual number of columns) and then take as much of the remaining width as possible */ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 15px; } .sphx-glr-thumbnails .toctree-wrapper { /* hide empty toctree divs added to the DOM by sphinx even though the toctree is hidden (they would fill grid places with empty divs) */ display: none; } .sphx-glr-thumbcontainer { background: transparent; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; box-shadow: 0 0 10px var(--sg-thumb-box-shadow-color); /* useful to absolutely position link in div */ position: relative; /* thumbnail width should include padding and borders and take all available space */ box-sizing: border-box; width: 100%; padding: 10px; border: 1px solid transparent; /* align content in thumbnail */ display: flex; flex-direction: column; align-items: center; gap: 7px; } .sphx-glr-thumbcontainer p { position: absolute; top: 0; left: 0; } .sphx-glr-thumbcontainer p, .sphx-glr-thumbcontainer p a { /* link should cover the whole thumbnail div */ width: 100%; height: 100%; } .sphx-glr-thumbcontainer p a span { /* text within link should be masked (we are just interested in the href) */ display: none; } .sphx-glr-thumbcontainer:hover { border: 1px solid; border-color: var(--sg-thumb-hover-border); cursor: pointer; } .sphx-glr-thumbcontainer a.internal { bottom: 0; display: block; left: 0; box-sizing: border-box; padding: 150px 10px 0; position: absolute; right: 0; top: 0; } /* Next one is to avoid Sphinx traditional theme to cover all the thumbnail with its default link Background color */ .sphx-glr-thumbcontainer a.internal:hover { background-color: transparent; } .sphx-glr-thumbcontainer p { margin: 0 0 0.1em 0; } .sphx-glr-thumbcontainer .figure { margin: 10px; width: 160px; } .sphx-glr-thumbcontainer img { display: inline; max-height: 112px; max-width: 160px; } .sphx-glr-thumbcontainer[tooltip]:hover:after { background: var(--sg-tooltip-background); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; color: var(--sg-tooltip-foreground); content: attr(tooltip); padding: 10px; z-index: 98; width: 100%; height: 100%; position: absolute; pointer-events: none; top: 0; box-sizing: border-box; overflow: hidden; backdrop-filter: blur(3px); } .sphx-glr-script-out { color: var(--sg-script-out); display: flex; gap: 0.5em; } .sphx-glr-script-out::before { content: "Out:"; /* These numbers come from the pre style in the pydata sphinx theme. This * turns out to match perfectly on the rtd theme, but be a bit too low for * the pydata sphinx theme. As I could not find a dimension to use that was * scaled the same way, I just picked one option that worked pretty close for * both. */ line-height: 1.4; padding-top: 10px; } .sphx-glr-script-out .highlight { background-color: transparent; /* These options make the div expand... */ flex-grow: 1; /* ... but also keep it from overflowing its flex container. */ overflow: auto; } .sphx-glr-script-out .highlight pre { background-color: var(--sg-script-pre); border: 0; max-height: 30em; overflow: auto; padding-left: 1ex; /* This margin is necessary in the pydata sphinx theme because pre has a box * shadow which would be clipped by the overflow:auto in the parent div * above. */ margin: 2px; word-break: break-word; } .sphx-glr-script-out + p { margin-top: 1.8em; } blockquote.sphx-glr-script-out { margin-left: 0pt; } .sphx-glr-script-out.highlight-pytb .highlight pre { color: var(--sg-pytb-foreground); background-color: var(--sg-pytb-background); border: 1px solid var(--sg-pytb-border-color); margin-top: 10px; padding: 7px; } div.sphx-glr-footer { text-align: center; } div.sphx-glr-download { margin: 1em auto; vertical-align: middle; } div.sphx-glr-download a { background-color: var(--sg-download-a-background-color); background-image: var(--sg-download-a-background-image); border-radius: 4px; border: 1px solid var(--sg-download-a-border-color); color: var(--sg-download-a-color); display: inline-block; font-weight: bold; padding: 1ex; text-align: center; } div.sphx-glr-download code.download { display: inline-block; white-space: normal; word-break: normal; overflow-wrap: break-word; /* border and background are given by the enclosing 'a' */ border: none; background: none; } div.sphx-glr-download a:hover { box-shadow: inset 0 1px 0 var(--sg-download-a-hover-box-shadow-1), 0 1px 5px var(--sg-download-a-hover-box-shadow-2); text-decoration: none; background-image: none; background-color: var(--sg-download-a-hover-background-color); } .sphx-glr-example-title:target::before { display: block; content: ""; margin-top: -50px; height: 50px; visibility: hidden; } ul.sphx-glr-horizontal { list-style: none; padding: 0; } ul.sphx-glr-horizontal li { display: inline; } ul.sphx-glr-horizontal img { height: auto !important; } .sphx-glr-single-img { margin: auto; display: block; max-width: 100%; } .sphx-glr-multi-img { max-width: 42%; height: auto; } div.sphx-glr-animation { margin: auto; display: block; max-width: 100%; } div.sphx-glr-animation .animation { display: block; } p.sphx-glr-signature a.reference.external { -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; padding: 3px; font-size: 75%; text-align: right; margin-left: auto; display: table; } .sphx-glr-clear { clear: both; } a.sphx-glr-backref-instance { text-decoration: none; } sphinx-gallery-0.16.0/sphinx_gallery/backreferences.py000066400000000000000000000376401461331107500231230ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Backreferences Generator. Parses example file code in order to keep track of used functions. """ import ast import codecs import collections from html import escape import inspect import os from pathlib import Path import re import sys from sphinx.errors import ExtensionError import sphinx.util from .scrapers import _find_image_ext from .utils import _replace_md5 THUMBNAIL_PARENT_DIV = """ .. raw:: html
.. thumbnail-parent-div-open """ THUMBNAIL_PARENT_DIV_CLOSE = """ .. thumbnail-parent-div-close .. raw:: html
""" class DummyClass: """Dummy class for testing method resolution.""" def run(self): """Do nothing.""" pass @property def prop(self): """Property.""" return "Property" class NameFinder(ast.NodeVisitor): """Finds the longest form of variable names and their imports in code. Only retains names from imported modules. """ def __init__(self, global_variables=None): super().__init__() self.imported_names = {} self.global_variables = global_variables or {} self.accessed_names = set() def visit_Import(self, node, prefix=""): """For 'import' add node names to `imported_names`.""" for alias in node.names: local_name = alias.asname or alias.name self.imported_names[local_name] = prefix + alias.name def visit_ImportFrom(self, node): """For 'from import' add node names to `imported_names`, incl module prefix.""" self.visit_Import(node, node.module + ".") def visit_Name(self, node): """Add node id to `accessed_names`.""" self.accessed_names.add(node.id) def visit_Attribute(self, node): """Add attributes, including their prefix, to `accessed_names`.""" attrs = [] while isinstance(node, ast.Attribute): attrs.append(node.attr) node = node.value if isinstance(node, ast.Name): # This is a.b, not e.g. a().b attrs.append(node.id) self.accessed_names.add(".".join(reversed(attrs))) else: # need to get a in a().b self.visit(node) def get_mapping(self): """Map names used in code, using AST nodes, to their fully qualified names. Returns ------- options : List[Tuple[str]] List of tuples, each tuple containing the following information about an `accessed_name`: accessed name : str, fully qualified name : str if it is a class attribute (i.e., property or method) : bool if it is a class : bool if it is an explicit backreference : bool (always false here) """ options = list() for name in self.accessed_names: local_name_split = name.split(".") # first pass: by global variables and object inspection (preferred) for split_level in range(len(local_name_split)): local_name = ".".join(local_name_split[: split_level + 1]) remainder = name[len(local_name) :] if local_name in self.global_variables: obj = self.global_variables[local_name] class_attr, method = False, [] if remainder: for level in remainder[1:].split("."): last_obj = obj # determine if it's a property prop = getattr(last_obj.__class__, level, None) if isinstance(prop, property): obj = last_obj class_attr, method = True, [level] break # For most objects this will emit a AttributeError, # but for some (e.g., PyQt5) you can get other # errors like "RuntimeError: wrapped C/C++ object # ... has been deleted" so let's be safer with # plain Exception try: obj = getattr(obj, level) except Exception: break if inspect.ismethod(obj): obj = last_obj class_attr, method = True, [level] break del remainder is_class = inspect.isclass(obj) if is_class or class_attr: # Traverse all bases classes = [obj if is_class else obj.__class__] offset = 0 while offset < len(classes): for base in classes[offset].__bases__: # "object" as a base class is not very useful if base not in classes and base is not object: classes.append(base) offset += 1 else: classes = [obj.__class__] for cc in classes: module = inspect.getmodule(cc) if module is not None: module = module.__name__.split(".") class_name = cc.__qualname__ # a.b.C.meth could be documented as a.C.meth, # so go down the list for depth in range(len(module), 0, -1): full_name = ".".join( module[:depth] + [class_name] + method ) options.append( (name, full_name, class_attr, is_class, False) ) # second pass: by import (can't resolve as well without doing # some actions like actually importing the modules, so use it # as a last resort) for split_level in range(len(local_name_split)): local_name = ".".join(local_name_split[: split_level + 1]) remainder = name[len(local_name) :] if local_name in self.imported_names: full_name = self.imported_names[local_name] + remainder is_class = class_attr = False # can't tell without import options.append((name, full_name, class_attr, is_class, False)) return options def _get_short_module_name(module_name, obj_name): """Get the shortest possible module name.""" if "." in obj_name: obj_name, attr = obj_name.split(".") else: attr = None try: # look only in sys.modules to avoid importing the module, which may # otherwise have side effects real_obj = getattr(sys.modules[module_name], obj_name) if attr is not None: getattr(real_obj, attr) except (AttributeError, KeyError): # AttributeError: wrong class # KeyError: wrong object or module not previously imported return None parts = module_name.split(".") short_name = module_name for i in range(len(parts) - 1, 0, -1): short_name = ".".join(parts[:i]) try: assert real_obj is getattr(sys.modules[short_name], obj_name) except (AssertionError, AttributeError, KeyError): # AssertionError: shortened object is not what we expect # KeyError: short module name not previously imported # AttributeError: wrong class or object # get the last working module name short_name = ".".join(parts[: (i + 1)]) break return short_name def _make_ref_regex(default_role=""): """Make regex to find reference to python objects.""" # keep roles variable in sync values shown in configuration.rst # "Add mini-galleries for API documentation" roles = "func|meth|attr|obj|class" def_role_regex = ( "|[^:]?" if re.fullmatch(f"(?:py:)?({roles})", default_role) else "" ) # reference can have a separate title `title `, # don't match ``literal`` or `!disabled.references` return ( rf"(?::(?:{roles}):{def_role_regex})" # role prefix r"(?!``)`~?[^!]*?`]+)>?(?!``)`" ) # reference def identify_names(script_blocks, ref_regex, global_variables=None, node=""): """Build a codeobj summary by identifying and resolving used names. Parameters ---------- script_blocks : list (label, content, line_number) List where each element is a tuple with the label ('text' or 'code'), the corresponding content string of block and the leading line number. ref_regex : str Regex to find references to python objects. example_globals: Optional[Dict[str, Any]] Global variables for examples. Default=None node : ast.Module or str The parsed node. Default="". Returns ------- example_code_obj : OrderedDict[str, Any] OrderedDict with information about all code object references found in an example. OrderedDict contains the following keys: - example_code_obj['name'] : function or class name (str) - example_code_obj['module'] : module name (str) - example_code_obj['module_short'] : shortened module name (str) - example_code_obj['is_class'] : whether object is class (bool) - example_code_obj['is_explicit'] : whether object is an explicit backreference (referred to by sphinx markup) (bool) """ if node == "": # mostly convenience for testing functions c = "\n".join(txt for kind, txt, _ in script_blocks if kind == "code") node = ast.parse(c) # Get matches from the code (AST, implicit matches) finder = NameFinder(global_variables) if node is not None: finder.visit(node) names = list(finder.get_mapping()) # Get matches from docstring inspection (explicit matches) text = "\n".join(txt for kind, txt, _ in script_blocks if kind == "text") names.extend((x, x, False, False, True) for x in re.findall(ref_regex, text)) example_code_obj = collections.OrderedDict() # order is important # Make a list of all guesses, in `_embed_code_links` we will break # when we find a match for name, full_name, class_like, is_class, is_explicit in names: if name not in example_code_obj: example_code_obj[name] = list() # name is as written in file (e.g. np.asarray) # full_name includes resolved import path (e.g. numpy.asarray) splits = full_name.rsplit(".", 1 + class_like) if len(splits) == 1: splits = ("builtins", splits[0]) elif len(splits) == 3: # class-like assert class_like splits = (splits[0], ".".join(splits[1:])) else: assert not class_like module, attribute = splits # get shortened module name module_short = _get_short_module_name(module, attribute) cobj = { "name": attribute, "module": module, "module_short": module_short or module, "is_class": is_class, "is_explicit": is_explicit, } example_code_obj[name].append(cobj) return example_code_obj THUMBNAIL_TEMPLATE = """ .. raw:: html
.. only:: html .. image:: /{thumbnail} :alt: :ref:`sphx_glr_{ref_name}` .. raw:: html
{title}
""" BACKREF_THUMBNAIL_TEMPLATE = ( THUMBNAIL_TEMPLATE + """ .. only:: not html * :ref:`sphx_glr_{ref_name}` """ ) def _thumbnail_div( target_dir, src_dir, fname, snippet, title, is_backref=False, check=True ): """Generate reST to place a thumbnail in a gallery.""" fname = Path(fname) thumb, _ = _find_image_ext( os.path.join(target_dir, "images", "thumb", f"sphx_glr_{fname.stem}_thumb.png") ) if check and not os.path.isfile(thumb): # This means we have done something wrong in creating our thumbnail! raise ExtensionError( "Could not find internal Sphinx-Gallery thumbnail" f" file:\n{thumb}" ) thumb = os.path.relpath(thumb, src_dir) full_dir = os.path.relpath(target_dir, src_dir) # Inside rst files forward slash defines paths thumb = thumb.replace(os.sep, "/") ref_name = os.path.join(full_dir, fname).replace(os.path.sep, "_") template = BACKREF_THUMBNAIL_TEMPLATE if is_backref else THUMBNAIL_TEMPLATE return template.format( snippet=escape(snippet), thumbnail=thumb, title=title, ref_name=ref_name ) def _write_backreferences( backrefs, seen_backrefs, gallery_conf, target_dir, fname, snippet, title ): """Write backreference file for one example including a thumbnail list of examples. Parameters ---------- backrefs : set[str] Back references to write. seen_backrefs: set Back references already encountered when parsing this example. gallery_conf : Dict[str, Any] Gallery configurations. target_dir : str Absolute path to directory where examples are saved. fname : str Filename of current example python file. snippet : str Introductory docstring of example. title: str Title of example. """ if gallery_conf["backreferences_dir"] is None: return for backref in backrefs: include_path = os.path.join( gallery_conf["src_dir"], gallery_conf["backreferences_dir"], f"{backref}.examples.new", ) seen = backref in seen_backrefs with codecs.open( include_path, "a" if seen else "w", encoding="utf-8" ) as ex_file: if not seen: # Be aware that if the number of lines of this heading changes, # the minigallery directive should be modified accordingly heading = f"Examples using ``{backref}``" ex_file.write("\n\n" + heading + "\n") ex_file.write("^" * len(heading) + "\n") ex_file.write("\n\n.. start-sphx-glr-thumbnails\n\n") # Open a div which will contain all thumbnails # (it will be closed in _finalize_backreferences) ex_file.write(THUMBNAIL_PARENT_DIV) ex_file.write( _thumbnail_div( target_dir, gallery_conf["src_dir"], fname, snippet, title, is_backref=True, ) ) seen_backrefs.add(backref) def _finalize_backreferences(seen_backrefs, gallery_conf): """Replace backref files only if necessary.""" logger = sphinx.util.logging.getLogger("sphinx-gallery") if gallery_conf["backreferences_dir"] is None: return for backref in seen_backrefs: path = os.path.join( gallery_conf["src_dir"], gallery_conf["backreferences_dir"], f"{backref}.examples.new", ) if os.path.isfile(path): # Close div containing all thumbnails # (it was open in _write_backreferences) with codecs.open(path, "a", encoding="utf-8") as ex_file: ex_file.write(THUMBNAIL_PARENT_DIV_CLOSE) _replace_md5(path, mode="t") else: level = gallery_conf["log_level"]["backreference_missing"] func = getattr(logger, level) func(f"Could not find backreferences file: {path}") func( "The backreferences are likely to be erroneous " "due to file system case insensitivity." ) sphinx-gallery-0.16.0/sphinx_gallery/block_parser.py000066400000000000000000000352331461331107500226230ustar00rootroot00000000000000"""BlockParser divides non `.py` source files into blocks of code and markup text.""" import ast import codecs from pathlib import Path import pygments.lexers import pygments.token import re from textwrap import dedent from sphinx.errors import ExtensionError from sphinx.util.logging import getLogger from .py_source_parser import FLAG_BODY logger = getLogger("sphinx-gallery") # Don't just use "x in pygments.token.Comment" because it also includes preprocessor # statements COMMENT_TYPES = ( pygments.token.Comment.Single, pygments.token.Comment, pygments.token.Comment.Multiline, ) class BlockParser: """ A parser that breaks a source file into blocks of code and markup text. Determines the source language and identifies comment blocks using pygments. Parameters ---------- source_file : str A file name that has a suffix compatible with files that are subsequently parsed gallery_conf : dict Contains the configuration of Sphinx-Gallery. """ def __init__(self, source_file, gallery_conf): source_file = Path(source_file) if name := gallery_conf["filetype_parsers"].get(source_file.suffix): self.lexer = pygments.lexers.find_lexer_class_by_name(name)() else: self.lexer = pygments.lexers.find_lexer_class_for_filename(source_file)() self.language = self.lexer.name # determine valid comment syntaxes. For each possible syntax, the tuple contains # - A test comment # - The comment start character # - The comment end character (for multiline comments) or None # - A regex for the start of a block based on repeated characters, or None comment_tests = [ ("#= comment =#", "#=", "=#", None), # Julia multiline ("# comment", "#", None, "#{20,}"), # Julia, Ruby, Bash, Perl, etc. ("// comment", "//", None, "/{20,}"), # C++, C#, Java, Rust, etc. ("/* comment */", r"/\*", r"\*/", r"/\*{20,}/"), # C/C++ etc. multiline ("% comment", "%", None, "%{20,}"), # Matlab ("! comment", "!", None, "!{20,}"), # Fortran ("c comment", r"^c(?:$| )", None, None), # Fortran 77 ] self.allowed_comments = [] allowed_special = [] self.multiline_end = re.compile(chr(0)) # unmatchable regex for test, start, end, special in comment_tests: if next(self.lexer.get_tokens(test))[0] in COMMENT_TYPES: self.allowed_comments.append(start) if end: self.multiline_end = re.compile(rf"(.*?)\s*{end}") if special: allowed_special.append(special) if self.language == "Matlab": # Matlab treats code sections starting with "%%" in a similar manner to # tools that recognize "# %%" in Python code, so we accept that style as # an alternative. allowed_special.append(r"%%(?:$|\s)") if r"/\*" in self.allowed_comments: # Remove decorative asterisks and comment starts from C-style multiline # comments self.multiline_cleanup = re.compile(r"\s*/?\*\s*") else: self.multiline_cleanup = re.compile(r"\s*") comment_start = "|".join(self.allowed_comments) allowed_special = "|".join(allowed_special) if allowed_special: self.start_special = re.compile( f"(?:(?:{comment_start}) ?%% ?|{allowed_special})(.*)" ) else: self.start_special = re.compile(f"(?:{comment_start}) ?%% ?(.*)") self.continue_text = re.compile(f"(?:{comment_start}) ?(.*)") # The pattern for in-file config comments is designed to not greedily match # newlines at the start and end, except for one newline at the end. This # ensures that the matched pattern can be removed from the code without # changing the block structure; i.e. empty newlines are preserved, e.g. in # # a = 1 # # # sphinx_gallery_thumbnail_number = 2 # # b = 2 flag_start = rf"^[\ \t]*(?:{comment_start})\s*" self.infile_config_pattern = re.compile(flag_start + FLAG_BODY, re.MULTILINE) self.start_ignore_flag = flag_start + "sphinx_gallery_start_ignore" self.end_ignore_flag = flag_start + "sphinx_gallery_end_ignore" self.ignore_block_pattern = re.compile( rf"{self.start_ignore_flag}(?:[\s\S]*?){self.end_ignore_flag}\n?", re.MULTILINE, ) def split_code_and_text_blocks(self, source_file, return_node=False): """Return list with source file separated into code and text blocks. Parameters ---------- source_file : str Path to the source file. return_node : bool Ignored; returning an ast node is not supported Returns ------- file_conf : dict File-specific settings given in source file comments as: ``# sphinx_gallery_ = `` blocks : list (label, content, line_number) List where each element is a tuple with the label ('text' or 'code'), the corresponding content string of block and the leading line number. node : None Returning an ast node is not supported. """ with codecs.open(source_file, "r", "utf-8") as fid: content = fid.read() # change from Windows format to UNIX for uniformity content = content.replace("\r\n", "\n") return self._split_content(content) def _get_content_lines(self, content): """ Combine individual tokens into lines. Use the first non-whitespace token (if any) as the characteristic token type for the line. """ current_line = [] line_token = pygments.token.Whitespace for token, text in self.lexer.get_tokens(content): if line_token == pygments.token.Whitespace: line_token = token if "\n" in text: text_lines = text.split("\n") # first item belongs to the previous line current_line.append(text_lines.pop(0)) yield line_token, "".join(current_line) # last item belongs to the line after this token current_line = [text_lines.pop()] # Anything left is a block of lines to add directly for ln in text_lines: if not ln.strip(): line_token = pygments.token.Whitespace yield line_token, ln if not current_line[0].strip(): line_token = pygments.token.Whitespace else: current_line.append(text) def _get_blocks(self, content): """ Generate a sequence of "blocks" from the lines in ``content``. Each block is a tuple of (label, content, line_number), matching the format ultimately returned by `split_code_and_text_blocks`. """ start_text = self.continue_text # No special delimiter needed for first block needs_multiline_cleanup = False def cleanup_multiline(lines): nonlocal needs_multiline_cleanup first_line = 1 if start_text == self.continue_text else 0 longest = max(len(line) for line in lines) matched = False for i, line in enumerate(lines[first_line:]): if m := self.multiline_cleanup.match(line): matched = True if (n := len(m.group(0))) < len(line): longest = min(longest, n) if matched and longest: for i, line in enumerate(lines[first_line:], start=first_line): lines[i] = lines[i][longest:] needs_multiline_cleanup = False return lines def finalize_block(mode, block): nonlocal start_text if mode == "text": if needs_multiline_cleanup: cleanup_multiline(block) # subsequent blocks need to have the special delimiter start_text = self.start_special # Remove leading blank lines, and end in a single newline first = 0 for i, line in enumerate(block): first = i if line.strip(): break last = None for i, line in enumerate(reversed(block)): last = -i or None if line.strip(): break block.append("") text = dedent("\n".join(block[first:last])) else: text = "\n".join(block) return mode, text, n - len(block) block = [] mode = None for n, (token, text) in enumerate(self._get_content_lines(content)): if mode == "text" and token in pygments.token.Whitespace: # Blank line ends current text block if block: yield finalize_block(mode, block) mode, block = None, [] elif ( mode != "text" and token in COMMENT_TYPES and (m := start_text.search(text)) ): # start of a text block; end the current block if block: yield finalize_block(mode, block) mode, block = "text", [] if (trailing_text := m.group(1)) is not None: if start_text == self.continue_text: if (delimited := self.start_special.search(text)) and ( heading := delimited.group(1).strip() ): # Treat text on the same line as the delimiter as a title block.extend((heading, "=" * len(heading), "")) else: # Keep any text on the first line of the title block as is block.append(trailing_text) elif heading := trailing_text.strip(): # Treat text on the same line as the delimiter as a heading block.extend((heading, "-" * len(heading), "")) elif mode == "text" and token in COMMENT_TYPES: # Continuation of a text block if m := self.start_special.search(text): # Starting a new text block now is just a continuation of the # existing one, possibly including a new heading. if heading := m.group(1).strip(): block.extend((heading, "-" * len(heading), "")) pass elif token == pygments.token.Comment.Multiline: if m := self.multiline_end.search(text): block.append(m.group(1)) needs_multiline_cleanup = True else: block.append(text) else: block.append(self.continue_text.search(text).group(1)) elif mode != "code": # start of a code block if block: yield finalize_block(mode, block) mode, block = "code", [text] else: # continuation of a code block block.append(text) # end of input ends final block if block: yield finalize_block(mode, block) def _split_content(self, content): """ Split the input content into blocks. Return a tuple of (file_conf, blocks, None) that corresponds to the return values of ``split_code_and_text_blocks``. """ file_conf = self.extract_file_config(content) blocks = list(self._get_blocks(content)) # For examples that start with a code block before the file docstring due to # language conventions or requirements, swap these blocks so the title block # comes first and merge consecutive code blocks if needed. if len(blocks) >= 2 and blocks[0][0] == "code" and blocks[1][0] == "text": blocks[0], blocks[1] = blocks[1], blocks[0] if len(blocks) >= 3 and blocks[2][0] == "code": blocks[1] = ("code", f"{blocks[1][1]}\n{blocks[2][1]}", blocks[1][2]) blocks.pop(2) return file_conf, blocks, None def extract_file_config(self, content): """Pull out the file-specific config specified in the docstring.""" file_conf = {} for match in re.finditer(self.infile_config_pattern, content): name = match.group(1) value = match.group(3) if value is None: # a flag rather than a config setting continue try: value = ast.literal_eval(value) except (SyntaxError, ValueError): logger.warning( "Sphinx-gallery option %s was passed invalid value %s", name, value ) else: file_conf[name] = value return file_conf def remove_ignore_blocks(self, code_block): """ Return the content of *code_block* with ignored areas removed. An ignore block starts with ``?? sphinx_gallery_start_ignore`` and ends with ``?? sphinx_gallery_end_ignore`` where ``??`` is the active language's line comment marker. These lines and anything in between them will be removed, but surrounding empty lines are preserved. Parameters ---------- code_block : str A code segment. """ num_start_flags = len(re.findall(self.start_ignore_flag, code_block)) num_end_flags = len(re.findall(self.end_ignore_flag, code_block)) if num_start_flags != num_end_flags: raise ExtensionError( 'All "sphinx_gallery_start_ignore" flags must have a matching ' '"sphinx_gallery_end_ignore" flag!' ) return re.subn(self.ignore_block_pattern, "", code_block)[0] def remove_config_comments(self, code_block): """ Return the content of *code_block* with in-file config comments removed. Comment lines with the pattern ``sphinx_gallery_[option] = [val]`` after the line comment character are removed, but surrounding empty lines are preserved. Parameters ---------- code_block : str A code segment. """ parsed_code, _ = re.subn(self.infile_config_pattern, "", code_block) return parsed_code sphinx-gallery-0.16.0/sphinx_gallery/directives.py000066400000000000000000000265371461331107500223250ustar00rootroot00000000000000"""Custom Sphinx directives.""" import os from pathlib import PurePosixPath, Path import shutil from docutils import nodes from docutils import statemachine from docutils.parsers.rst import Directive, directives from docutils.parsers.rst.directives import images from sphinx.errors import ExtensionError from .backreferences import ( _thumbnail_div, THUMBNAIL_PARENT_DIV, THUMBNAIL_PARENT_DIV_CLOSE, ) from .gen_rst import extract_intro_and_title from .py_source_parser import split_code_and_text_blocks class MiniGallery(Directive): """Custom directive to insert a mini-gallery. The required argument is one or more of the following: * fully qualified names of objects * pathlike strings to example Python files * glob-style pathlike strings to example Python files The string list of arguments is separated by spaces. The mini-gallery will be the subset of gallery examples that make use of that object from that specific namespace Options: * `add-heading` adds a heading to the mini-gallery. If an argument is provided, it uses that text for the heading. Otherwise, it uses default text. * `heading-level` specifies the heading level of the heading as a single character. If omitted, the default heading level is `'^'`. """ required_arguments = 0 has_content = True optional_arguments = 1 final_argument_whitespace = True option_spec = { "add-heading": directives.unchanged, "heading-level": directives.single_char_or_unicode, } def run(self): """Generate mini-gallery from backreference and example files.""" from .gen_rst import _get_callables if not (self.arguments or self.content): raise ExtensionError("No arguments passed to 'minigallery'") # Respect the same disabling options as the `raw` directive if ( not self.state.document.settings.raw_enabled or not self.state.document.settings.file_insertion_enabled ): raise self.warning(f'"{self.name}" directive disabled.') # Retrieve the backreferences directory config = self.state.document.settings.env.config backreferences_dir = config.sphinx_gallery_conf["backreferences_dir"] # Retrieve source directory src_dir = config.sphinx_gallery_conf["src_dir"] # Parse the argument into the individual objects obj_list = [] if self.arguments: obj_list.extend([c.strip() for c in self.arguments[0].split()]) if self.content: obj_list.extend([c.strip() for c in self.content]) lines = [] # Add a heading if requested if "add-heading" in self.options: heading = self.options["add-heading"] if heading == "": if len(obj_list) == 1: heading = f"Examples using ``{obj_list[0]}``" else: heading = "Examples using one of multiple objects" lines.append(heading) heading_level = self.options.get("heading-level", "^") lines.append(heading_level * len(heading)) def has_backrefs(obj): path = Path(src_dir, backreferences_dir, f"{obj}.examples") return path if (path.is_file() and (path.stat().st_size > 0)) else False file_paths = [] for obj in obj_list: if path := has_backrefs(obj): file_paths.append((obj, path)) elif paths := Path(src_dir).glob(obj): file_paths.extend([(obj, p) for p in paths]) if len(file_paths) == 0: return [] lines.append(THUMBNAIL_PARENT_DIV) # sort on the str(file_path) but keep (obj, path) pair if config.sphinx_gallery_conf["minigallery_sort_order"] is None: sortkey = None else: (sortkey,) = _get_callables( config.sphinx_gallery_conf, "minigallery_sort_order" ) for obj, path in sorted( set(file_paths), key=((lambda x: sortkey(os.path.abspath(x[-1]))) if sortkey else None), ): if path.suffix == ".examples": # Insert the backreferences file(s) using the `include` directive. # / is the src_dir for include lines.append( f"""\ .. include:: /{path.relative_to(src_dir).as_posix()} :start-after: thumbnail-parent-div-open :end-before: thumbnail-parent-div-close""" ) else: dirs = [ (e, g) for e, g in zip( config.sphinx_gallery_conf["examples_dirs"], config.sphinx_gallery_conf["gallery_dirs"], ) if (obj.find(e) != -1) ] if len(dirs) != 1: raise ExtensionError( f"Error in gallery lookup: input={obj}, matches={dirs}, " f"examples={config.sphinx_gallery_conf['examples_dirs']}" ) example_dir, target_dir = [Path(src_dir, d) for d in dirs[0]] # finds thumbnails in subdirs target_dir = target_dir / path.relative_to(example_dir).parent _, script_blocks = split_code_and_text_blocks( str(path), return_node=False ) intro, title = extract_intro_and_title(str(path), script_blocks[0][1]) thumbnail = _thumbnail_div(target_dir, src_dir, path.name, intro, title) lines.append(thumbnail) lines.append(THUMBNAIL_PARENT_DIV_CLOSE) text = "\n".join(lines) include_lines = statemachine.string2lines(text, convert_whitespace=True) self.state_machine.insert_input(include_lines, str(path)) return [] """ Image sg for responsive images """ class imgsgnode(nodes.General, nodes.Element): """Sphinx Gallery image node class.""" pass class ImageSg(images.Image): """Implements a directive to allow an optional hidpi image. Meant to be used with the `image_srcset` configuration option. e.g.:: .. image-sg:: /plot_types/basic/images/sphx_glr_bar_001.png :alt: bar :srcset: /plot_types/basic/images/sphx_glr_bar_001.png, /plot_types/basic/images/sphx_glr_bar_001_2_00x.png 2.00x :class: sphx-glr-single-img The resulting html is:: bar """ has_content = False required_arguments = 1 optional_arguments = 3 final_argument_whitespace = False option_spec = { "srcset": directives.unchanged, "class": directives.class_option, "alt": directives.unchanged, } def run(self): """Update node contents.""" image_node = imgsgnode() imagenm = self.arguments[0] image_node["alt"] = self.options.get("alt", "") image_node["class"] = self.options.get("class", None) # we would like uri to be the highest dpi version so that # latex etc will use that. But for now, lets just make # imagenm image_node["uri"] = imagenm image_node["srcset"] = self.options.get("srcset", None) return [image_node] def _parse_srcset(st): """Parse st.""" entries = st.split(",") srcset = {} for entry in entries: spl = entry.strip().split(" ") if len(spl) == 1: srcset[0] = spl[0] elif len(spl) == 2: mult = spl[1][:-1] srcset[float(mult)] = spl[0] else: raise ExtensionError('srcset argument "{entry}" is invalid.') return srcset def visit_imgsg_html(self, node): """Handle HTML image tag depending on 'srcset' configuration. If 'srcset' is not `None`, copy images, generate image html tag with 'srcset' and add to HTML `body`. If 'srcset' is `None` run `visit_image` on `node`. """ if node["srcset"] is None: self.visit_image(node) return imagedir, srcset = _copy_images(self, node) # /doc/examples/subd/plot_1.rst docsource = self.document["source"] # /doc/ # make sure to add the trailing slash: srctop = os.path.join(self.builder.srcdir, "") # examples/subd/plot_1.rst relsource = os.path.relpath(docsource, srctop) # /doc/build/html desttop = os.path.join(self.builder.outdir, "") # /doc/build/html/examples/subd dest = os.path.join(desttop, relsource) # ../../_images/ for dirhtml and ../_images/ for html imagerel = os.path.relpath(imagedir, os.path.dirname(dest)) if self.builder.name == "dirhtml": imagerel = os.path.join("..", imagerel, "") else: # html imagerel = os.path.join(imagerel, "") if "\\" in imagerel: imagerel = imagerel.replace("\\", "/") # make srcset str. Need to change all the prefixes! srcsetst = "" for mult in srcset: nm = os.path.basename(srcset[mult][1:]) # ../../_images/plot_1_2_0x.png relpath = imagerel + nm srcsetst += f"{relpath}" if mult == 0: srcsetst += ", " else: srcsetst += f" {mult:1.2f}x, " # trim trailing comma and space... srcsetst = srcsetst[:-2] # make uri also be relative... nm = os.path.basename(node["uri"][1:]) uri = imagerel + nm alt = node["alt"] if node["class"] is not None: classst = node["class"][0] classst = f'class = "{classst}"' else: classst = "" html_block = f'{alt}" self.body.append(html_block) def visit_imgsg_latex(self, node): """Copy images, set node[uri] to highest resolution image and call `visit_image`.""" if node["srcset"] is not None: imagedir, srcset = _copy_images(self, node) maxmult = -1 # choose the highest res version for latex: for key in srcset.keys(): maxmult = max(maxmult, key) node["uri"] = str(PurePosixPath(srcset[maxmult]).name) self.visit_image(node) def _copy_images(self, node): srcset = _parse_srcset(node["srcset"]) # where the sources are. i.e. myproj/source srctop = self.builder.srcdir # copy image from source to imagedir. This is # *probably* supposed to be done by a builder but... # ie myproj/build/html/_images imagedir = os.path.join(self.builder.imagedir, "") imagedir = PurePosixPath(self.builder.outdir, imagedir) os.makedirs(imagedir, exist_ok=True) # copy all the sources to the imagedir: for mult in srcset: abspath = PurePosixPath(srctop, srcset[mult][1:]) shutil.copyfile(abspath, imagedir / abspath.name) return imagedir, srcset def depart_imgsg_html(self, node): """HTML depart node visitor function.""" pass def depart_imgsg_latex(self, node): """LaTeX depart node visitor function.""" self.depart_image(node) def imagesg_addnode(app): """Add `imgsgnode` to Sphinx app with visitor functions for HTML and LaTeX.""" app.add_node( imgsgnode, html=(visit_imgsg_html, depart_imgsg_html), latex=(visit_imgsg_latex, depart_imgsg_latex), ) sphinx-gallery-0.16.0/sphinx_gallery/docs_resolv.py000066400000000000000000000434241461331107500225000ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Link resolver objects.""" import codecs import gzip from io import BytesIO import os import pickle import posixpath import re import shelve import sys import urllib.request as urllib_request import urllib.parse as urllib_parse from pathlib import Path from urllib.error import HTTPError, URLError from sphinx.errors import ExtensionError from sphinx.search import js_index import sphinx.util from .utils import status_iterator logger = sphinx.util.logging.getLogger("sphinx-gallery") def _get_data(url): """Get data over http(s) or from a local file.""" if urllib_parse.urlparse(url).scheme in ("http", "https"): user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11" # noqa: E501 headers = {"User-Agent": user_agent} req = urllib_request.Request(url, None, headers) resp = urllib_request.urlopen(req) encoding = resp.headers.get("content-encoding", "plain") data = resp.read() if encoding == "gzip": data = gzip.GzipFile(fileobj=BytesIO(data)).read() elif encoding != "plain": raise ExtensionError(f"unknown encoding {encoding!r}") data = data.decode("utf-8") else: with codecs.open(url, mode="r", encoding="utf-8") as fid: data = fid.read() return data def get_data(url, gallery_dir): """Persistent dictionary usage to retrieve the search indexes.""" # shelve keys need to be str in python 2 if sys.version_info[0] == 2 and isinstance(url, str): url = url.encode("utf-8") cached_file = os.path.join(gallery_dir, "searchindex") search_index = shelve.open(cached_file) if url in search_index: data = search_index[url] else: data = _get_data(url) search_index[url] = data search_index.close() return data def parse_sphinx_docopts(index): """Parse the Sphinx index for documentation options. Parameters ---------- index : str The Sphinx index page Returns ------- docopts : dict The documentation options from the page. """ pos = index.find("var DOCUMENTATION_OPTIONS") if pos < 0: pos = index.find("const DOCUMENTATION_OPTIONS") # Sphinx 7.2+ if pos < 0: raise ExtensionError("Documentation options could not be found in index.") pos = index.find("{", pos) if pos < 0: raise ExtensionError("Documentation options could not be found in index.") endpos = index.find("};", pos) if endpos < 0: raise ExtensionError("Documentation options could not be found in index.") block = index[pos + 1 : endpos].strip() docopts = {} for line in block.splitlines(): key, value = line.split(":", 1) key = key.strip().strip('"') value = value.strip() if value[-1] == ",": value = value[:-1].rstrip() if value[0] in "\"'": value = value[1:-1] elif value == "false": value = False elif value == "true": value = True else: try: value = int(value) except ValueError: # In Sphinx 1.7.5, URL_ROOT is a JavaScript fragment. # Ignoring this entry since URL_ROOT is not used # elsewhere. # https://github.com/sphinx-gallery/sphinx-gallery/issues/382 continue docopts[key] = value return docopts class SphinxDocLinkResolver: """Resolve documentation links using searchindex.js generated by Sphinx. Parameters ---------- doc_url : str The base URL of the project website. relative : bool Return relative links (only useful for links to documentation of this package). """ def __init__(self, config, doc_url, gallery_dir, relative=False): self.config = config self.doc_url = doc_url self.gallery_dir = gallery_dir self.relative = relative self._link_cache = {} if isinstance(doc_url, Path): index_url = os.path.join(doc_url, "index.html") searchindex_url = os.path.join(doc_url, "searchindex.js") docopts_url = os.path.join(doc_url, "_static", "documentation_options.js") else: if relative: raise ExtensionError( "Relative links are only supported for local " "URLs (doc_url cannot be absolute)" ) index_url = doc_url + "/" searchindex_url = doc_url + "/searchindex.js" docopts_url = doc_url + "/_static/documentation_options.js" # detect if we are using relative links on a Windows system if os.name.lower() == "nt" and isinstance(doc_url, Path): if not relative: raise ExtensionError( "You have to use relative=True for the local" " package on a Windows system." ) self._is_windows = True else: self._is_windows = False # Download and find documentation options. As of Sphinx 1.7, these # options are now kept in a standalone file called # 'documentation_options.js'. Since SphinxDocLinkResolver can be called # not only for the documentation which is being built but also ones # that are being referenced, we need to try and get the index page # first and if that doesn't work, check for the # documentation_options.js file. index = get_data(index_url, gallery_dir) if "var DOCUMENTATION_OPTIONS" in index: self._docopts = parse_sphinx_docopts(index) else: docopts = get_data(docopts_url, gallery_dir) self._docopts = parse_sphinx_docopts(docopts) # download and initialize the search index sindex = get_data(searchindex_url, gallery_dir) self._searchindex = js_index.loads(sindex) def _get_index_match(self, first, second): try: match = self._searchindex["objects"][first] except KeyError: return None else: if isinstance(match, dict): try: match = match[second] except KeyError: return None elif isinstance(match, (list, tuple)): # Sphinx 5.0.0 dev try: for item in match: if item[4] == second: match = item[:4] break else: return None except Exception: return None return match def _get_link_type(self, cobj, use_full_module=False): """Get a valid link and type_, False if not found.""" module_type = "module_short" if use_full_module: module_type = "module" first, second = cobj[module_type], cobj["name"] match = self._get_index_match(first, second) if match is None and "." in second: # possible class attribute first, second = second.split(".", 1) first = ".".join([cobj["module_short"], first]) match = self._get_index_match(first, second) if match is None: link = type_ = None else: fname_idx = match[0] objname_idx = str(match[1]) anchor = match[3] type_ = self._searchindex["objtypes"][objname_idx] fname = self._searchindex["filenames"][fname_idx] # In 1.5+ Sphinx seems to have changed from .rst.html to only # .html extension in converted files. Find this from the options. ext = self._docopts.get("FILE_SUFFIX", ".rst.html") fname = os.path.splitext(fname)[0] + ext if self._is_windows: fname = fname.replace("/", "\\") link = os.path.join(self.doc_url, fname) else: link = posixpath.join(self.doc_url, fname) fullname = ".".join([first, second]) if anchor == "": anchor = fullname elif anchor == "-": anchor = self._searchindex["objnames"][objname_idx][1] + "-" + fullname link = link + "#" + anchor return link, type_ def resolve(self, cobj, this_url, return_type=False): """Resolve the link to the documentation, returns None if not found. Parameters ---------- cobj : OrderedDict[str, Any] OrderedDict with information about the "code object" for which we are resolving a link. - cobj['name'] : function or class name (str) - cobj['module'] : module name (str) - cobj['module_short'] : shortened module name (str) - cobj['is_class'] : whether object is class (bool) - cobj['is_explicit'] : whether object is an explicit backreference (referred to by sphinx markup) (bool) this_url: str URL of the current page. Needed to construct relative URLs (only used if relative=True in constructor). return_type : bool If True, return the type as well. Returns ------- link : str or None The link (URL) to the documentation. type_ : str The type. Only returned if return_type is True. """ full_name = cobj["module_short"] + "." + cobj["name"] if full_name not in self._link_cache: # we don't have it cached use_full_module = False for pattern in self.config["prefer_full_module"]: if re.search(pattern, cobj["module"] + "." + cobj["name"]): use_full_module = True break self._link_cache[full_name] = self._get_link_type(cobj, use_full_module) link, type_ = self._link_cache[full_name] if self.relative and link is not None: link = os.path.relpath(link, start=this_url) if self._is_windows: # replace '\' with '/' so it on the web link = link.replace("\\", "/") # for some reason, the relative link goes one directory too high up link = link[3:] return (link, type_) if return_type else link def _handle_http_url_error(e, msg="fetching"): if isinstance(e, HTTPError): error_msg = f"{msg} {e.url}: {e.code} ({e.msg})" elif isinstance(e, URLError): error_msg = f"{msg}: {e.reason}" logger.warning( "The following {} has occurred {}".format(type(e).__name__, error_msg) ) def _sanitize_css_class(s): for x in "~!@$%^&*()+=,./';:\"?><[]\\{}|`#": s = s.replace(x, "-") return s def _embed_code_links(app, gallery_conf, gallery_dir): """Add resolvers for the packages for which we want to show links.""" doc_resolvers = {} src_gallery_dir = os.path.join(app.builder.srcdir, gallery_dir) for this_module, url in gallery_conf["reference_url"].items(): try: if url is None: doc_resolvers[this_module] = SphinxDocLinkResolver( app.config.sphinx_gallery_conf, Path(app.builder.outdir), src_gallery_dir, relative=True, ) else: doc_resolvers[this_module] = SphinxDocLinkResolver( app.config.sphinx_gallery_conf, url, src_gallery_dir ) except (URLError, HTTPError) as e: _handle_http_url_error(e) html_gallery_dir = os.path.abspath(os.path.join(app.builder.outdir, gallery_dir)) # patterns for replacement link_pattern = '{text}' orig_pattern = '%s' period = '.' # This could be turned into a generator if necessary, but should be okay flat = [ [dirpath, filename] for dirpath, _, filenames in os.walk(html_gallery_dir) for filename in filenames ] iterator = status_iterator( flat, f"embedding documentation hyperlinks for {gallery_dir}... ", color="fuchsia", length=len(flat), stringify_func=lambda x: os.path.basename(x[1]), ) intersphinx_inv = getattr(app.env, "intersphinx_named_inventory", dict()) builtin_modules = set( intersphinx_inv.get("python", dict()).get("py:module", dict()).keys() ) for dirpath, fname in iterator: full_fname = os.path.join(html_gallery_dir, dirpath, fname) subpath = dirpath[len(html_gallery_dir) + 1 :] pickle_fname = os.path.join( src_gallery_dir, subpath, fname[:-5] + "_codeobj.pickle" ) if not os.path.exists(pickle_fname): continue # we have a pickle file with the objects to embed links for with open(pickle_fname, "rb") as fid: example_code_obj = pickle.load(fid) # generate replacement strings with the links str_repl = {} for name in sorted(example_code_obj): cobjs = example_code_obj[name] # possible names from identify_names, which in turn gets # possibilities from NameFinder.get_mapping link = type_ = None for cobj in cobjs: for modname in (cobj["module_short"], cobj["module"]): this_module = modname.split(".")[0] cname = cobj["name"] # Try doc resolvers first if this_module in doc_resolvers: try: link, type_ = doc_resolvers[this_module].resolve( cobj, full_fname, return_type=True ) except (HTTPError, URLError) as e: _handle_http_url_error( e, msg=f"resolving {modname}.{cname}" ) # next try intersphinx if this_module == modname == "builtins": this_module = "python" elif modname in builtin_modules: this_module = "python" if link is None and this_module in intersphinx_inv: inv = intersphinx_inv[this_module] if modname == "builtins": want = cname else: want = f"{modname}.{cname}" for key, value in inv.items(): # only python domain if key.startswith("py") and want in value: link = value[want][2] type_ = key break # differentiate classes from instances is_instance = ( type_ is not None and "py:class" in type_ and not cobj["is_class"] ) if link is not None: # Add CSS classes name_html = period.join( orig_pattern % part for part in name.split(".") ) full_function_name = f"{modname}.{cname}" css_class = "sphx-glr-backref-module-" + _sanitize_css_class( modname ) if type_ is not None: css_class += ( " sphx-glr-backref-type-" + _sanitize_css_class(type_) ) if is_instance: css_class += " sphx-glr-backref-instance" str_repl[name_html] = link_pattern.format( link=link, title=full_function_name, css_class=css_class, text=name_html, ) break # loop over possible module names if link is not None: break # loop over cobjs # do the replacement in the html file # ensure greediness names = sorted(str_repl, key=len, reverse=True) regex_str = "|".join(re.escape(name) for name in names) regex = re.compile(regex_str) def substitute_link(match): return str_repl[match.group()] if len(str_repl) > 0: with codecs.open(full_fname, "r", "utf-8") as fid: lines_in = fid.readlines() with codecs.open(full_fname, "w", "utf-8") as fid: for line in lines_in: line_out = regex.sub(substitute_link, line) fid.write(line_out) def embed_code_links(app, exception): """Embed hyperlinks to documentation into example code.""" if exception is not None: return gallery_conf = app.config.sphinx_gallery_conf # XXX: Allowlist of builders for which it makes sense to embed # hyperlinks inside the example html. Note that the link embedding # require searchindex.js to exist for the links to the local doc # and there does not seem to be a good way of knowing which # builders creates a searchindex.js. if app.builder.name not in ["html", "readthedocs"]: return logger.info("embedding documentation hyperlinks...", color="white") gallery_dirs = gallery_conf["gallery_dirs"] if not isinstance(gallery_dirs, list): gallery_dirs = [gallery_dirs] for gallery_dir in gallery_dirs: _embed_code_links(app, gallery_conf, gallery_dir) sphinx-gallery-0.16.0/sphinx_gallery/downloads.py000066400000000000000000000105101461331107500221360ustar00rootroot00000000000000r"""Utilities for downloadable items.""" # Author: Óscar Nájera # License: 3-clause BSD import os import zipfile from .utils import _replace_md5 CODE_ZIP_DOWNLOAD = """ .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-gallery .. container:: sphx-glr-download sphx-glr-download-python :download:`Download all examples {0} source code: {1} ` """ NOTEBOOK_ZIP_DOWNLOAD = """ .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download all examples in Jupyter notebooks: {0} ` """ def python_zip(file_list, gallery_path, extension=".py"): """Store all files in file_list into an zip file. Parameters ---------- file_list : list Holds all the file names to be included in zip file gallery_path : str path to where the zipfile is stored extension : str | None In order to deal with downloads of plain source files and jupyter notebooks, if this value is not None, the file extension from files in file_list will be removed and replace with the value of this variable while generating the zip file Returns ------- zipname : str zip file name, written as `target_dir_python.zip`, `target_dir_jupyter.zip`, or `target_dir.zip` depending on the extension """ zipname = os.path.basename(os.path.normpath(gallery_path)) if extension == ".py": zipname += "_python" elif extension == ".ipynb": zipname += "_jupyter" zipname = os.path.join(gallery_path, zipname + ".zip") zipname_new = zipname + ".new" with zipfile.ZipFile(zipname_new, mode="w") as zipf: for fname in file_list: if extension is not None: fname = os.path.splitext(fname)[0] + extension zipf.write(fname, os.path.relpath(fname, gallery_path)) _replace_md5(zipname_new) return zipname def list_downloadable_sources(target_dir, extensions=(".py",)): """Return a list of source files in target_dir. Parameters ---------- target_dir : str path to the directory where source file are extensions : tuple[str] tuple of file extensions to include Returns ------- list list of paths to all source files in `target_dir` ending with one of the specified extensions """ return [ os.path.join(target_dir, fname) for fname in os.listdir(target_dir) if fname.endswith(extensions) ] def generate_zipfiles(gallery_dir, src_dir, gallery_conf): """Collects downloadable sources and makes zipfiles of them. Collects all source files and Jupyter notebooks in gallery_dir. Parameters ---------- gallery_dir : str path of the gallery to collect downloadable sources src_dir : str The build source directory. Needed to make the reST paths relative. gallery_conf : dict[str, Any] Sphinx-Gallery configuration dictionary Return ------ download_rst: str RestructuredText to include download buttons to the generated files """ src_ext = tuple(gallery_conf["example_extensions"]) notebook_ext = tuple(gallery_conf["notebook_extensions"]) source_files = list_downloadable_sources(gallery_dir, src_ext) notebook_files = list_downloadable_sources(gallery_dir, notebook_ext) for directory in sorted(os.listdir(gallery_dir)): if os.path.isdir(os.path.join(gallery_dir, directory)): target_dir = os.path.join(gallery_dir, directory) source_files.extend(list_downloadable_sources(target_dir, src_ext)) notebook_files.extend(list_downloadable_sources(target_dir, notebook_ext)) def rst_path(filepath): filepath = os.path.relpath(filepath, os.path.normpath(src_dir)) return filepath.replace(os.sep, "/") all_python = all(f.endswith(".py") for f in source_files) py_zipfile = python_zip(source_files, gallery_dir, ".py" if all_python else None) dw_rst = CODE_ZIP_DOWNLOAD.format( "in Python" if all_python else "as", os.path.basename(py_zipfile), rst_path(py_zipfile), ) if notebook_files: jy_zipfile = python_zip(notebook_files, gallery_dir, ".ipynb") dw_rst += NOTEBOOK_ZIP_DOWNLOAD.format( os.path.basename(jy_zipfile), rst_path(jy_zipfile), ) return dw_rst sphinx-gallery-0.16.0/sphinx_gallery/gen_gallery.py000066400000000000000000001602131461331107500224420ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Sphinx-Gallery Generator. Attaches Sphinx-Gallery to Sphinx in order to generate the galleries when building the documentation. """ import codecs import copy from datetime import timedelta, datetime from difflib import get_close_matches from pathlib import Path from textwrap import indent import re import os import pathlib from xml.sax.saxutils import quoteattr, escape from itertools import chain from sphinx.errors import ConfigError, ExtensionError import sphinx.util from sphinx.util.console import blue, red, purple, bold from . import glr_path_static, __version__ as _sg_version from .utils import _replace_md5, _has_optipng, _has_pypandoc, _has_graphviz from .backreferences import _finalize_backreferences from .gen_rst import ( generate_dir_rst, SPHX_GLR_SIG, _get_readme, _get_class, _get_callables, _get_call_memory_and_base, ) from .scrapers import _import_matplotlib from .docs_resolv import embed_code_links from .downloads import generate_zipfiles from .interactive_example import ( copy_binder_files, check_binder_conf, check_jupyterlite_conf, ) from .interactive_example import pre_configure_jupyterlite_sphinx from .interactive_example import post_configure_jupyterlite_sphinx from .interactive_example import create_jupyterlite_contents from .directives import MiniGallery, ImageSg, imagesg_addnode from .recommender import ExampleRecommender, _write_recommendations from .sorting import ExplicitOrder _KNOWN_CSS = ( "sg_gallery", "sg_gallery-binder", "sg_gallery-dataframe", "sg_gallery-rendered-html", ) class DefaultResetArgv: """Provides default 'reset_argv' callable that returns empty list.""" def __repr__(self): return "DefaultResetArgv" def __call__(self, gallery_conf, script_vars): """Return empty list.""" return [] DEFAULT_GALLERY_CONF = { "filename_pattern": re.escape(os.sep) + "plot", "ignore_pattern": r"__init__\.py", "examples_dirs": os.path.join("..", "examples"), "example_extensions": {".py"}, "filetype_parsers": {}, "notebook_extensions": {".py"}, "reset_argv": DefaultResetArgv(), "subsection_order": None, "within_subsection_order": "NumberOfCodeLinesSortKey", "minigallery_sort_order": None, "gallery_dirs": "auto_examples", "backreferences_dir": None, "doc_module": (), "exclude_implicit_doc": set(), "reference_url": {}, "capture_repr": ("_repr_html_", "__repr__"), "ignore_repr_types": r"", # Build options # ------------- # 'plot_gallery' also accepts strings that evaluate to a bool, e.g. "True", # "False", "1", "0" so that they can be easily set via command line # switches of sphinx-build "plot_gallery": "True", "download_all_examples": True, "abort_on_example_error": False, "only_warn_on_example_error": False, "recommender": {"enable": False}, "failing_examples": {}, "passing_examples": [], "stale_examples": [], # ones that did not need to be run due to md5sum "run_stale_examples": False, "expected_failing_examples": set(), "thumbnail_size": (400, 280), # Default CSS does 0.4 scaling (160, 112) "min_reported_time": 0, "binder": {}, "jupyterlite": {}, "promote_jupyter_magic": False, "image_scrapers": ("matplotlib",), "compress_images": (), "reset_modules": ("matplotlib", "seaborn"), "reset_modules_order": "before", "first_notebook_cell": None, "last_notebook_cell": None, "notebook_images": False, "pypandoc": False, "remove_config_comments": False, "show_memory": False, "show_signature": True, "junit": "", "log_level": {"backreference_missing": "warning"}, "inspect_global_variables": True, "css": _KNOWN_CSS, "matplotlib_animations": False, "image_srcset": [], "default_thumb_file": None, "line_numbers": False, "nested_sections": True, "prefer_full_module": set(), "api_usage_ignore": ".*__.*__", "show_api_usage": False, # if this changes, change write_api_entries, too "copyfile_regex": "", } logger = sphinx.util.logging.getLogger("sphinx-gallery") def _bool_eval(x): """Evaluate bool only configs, to allow setting via -D on the command line.""" if isinstance(x, str): try: x = eval(x) except TypeError: pass return bool(x) def _update_gallery_conf_exclude_implicit_doc(gallery_conf): """Update gallery config exclude_implicit_doc. This is separate function for better testability. """ # prepare regex for exclusions from implicit documentation exclude_regex = ( re.compile("|".join(gallery_conf["exclude_implicit_doc"])) if gallery_conf["exclude_implicit_doc"] else False ) gallery_conf["exclude_implicit_doc_regex"] = exclude_regex def _update_gallery_conf_builder_inited( sphinx_gallery_conf, src_dir, plot_gallery=True, abort_on_example_error=False, builder_name="html", ): sphinx_gallery_conf.update(plot_gallery=plot_gallery) sphinx_gallery_conf.update(abort_on_example_error=abort_on_example_error) sphinx_gallery_conf["src_dir"] = src_dir # Make it easy to know which builder we're in sphinx_gallery_conf["builder_name"] = builder_name def _fill_gallery_conf_defaults(sphinx_gallery_conf, app=None, check_keys=True): """Handle user configs and update default gallery configs.""" gallery_conf = copy.deepcopy(DEFAULT_GALLERY_CONF) options = sorted(gallery_conf) extra_keys = sorted(set(sphinx_gallery_conf) - set(options)) if extra_keys and check_keys: msg = "Unknown key(s) in sphinx_gallery_conf:\n" for key in extra_keys: options = get_close_matches(key, options, cutoff=0.66) msg += repr(key) if len(options) == 1: msg += f", did you mean {options[0]!r}?" elif len(options) > 1: msg += f", did you mean one of {options!r}?" msg += "\n" raise ConfigError(msg.strip()) gallery_conf.update(sphinx_gallery_conf) # XXX anything that can only be a bool (rather than str) should probably be # evaluated this way as it allows setting via -D on the command line for key in ( "promote_jupyter_magic", "run_stale_examples", ): gallery_conf[key] = _bool_eval(gallery_conf[key]) gallery_conf["default_role"] = "" gallery_conf["source_suffix"] = {".rst": "restructuredtext"} if app is not None: if app.config["default_role"]: gallery_conf["default_role"] = app.config["default_role"] gallery_conf["source_suffix"] = app.config["source_suffix"] if isinstance(gallery_conf["source_suffix"], str): gallery_conf["source_suffix"] = {gallery_conf["source_suffix"]: None} # Check capture_repr capture_repr = gallery_conf["capture_repr"] supported_reprs = ["__repr__", "__str__", "_repr_html_"] if isinstance(capture_repr, tuple): for rep in capture_repr: if rep not in supported_reprs: raise ConfigError( "All entries in 'capture_repr' must be one " f"of {supported_reprs}, got: {rep}" ) else: raise ConfigError(f"'capture_repr' must be a tuple, got: {type(capture_repr)}") # Check ignore_repr_types if not isinstance(gallery_conf["ignore_repr_types"], str): raise ConfigError( "'ignore_repr_types' must be a string, got: " + type(gallery_conf["ignore_repr_types"]) ) # deal with show_memory _get_call_memory_and_base(gallery_conf) # check callables for key in ( "image_scrapers", "reset_argv", "minigallery_sort_order", "reset_modules", ): if key == "minigallery_sort_order" and gallery_conf[key] is None: continue _get_callables(gallery_conf, key) # Here we try to set up matplotlib but don't raise an error, # we will raise an error later when we actually try to use it # (if we do so) in scrapers.py. # In principle we could look to see if there is a matplotlib scraper # in our scrapers list, but this would be backward incompatible with # anyone using or relying on our Agg-setting behavior (e.g., for some # custom matplotlib SVG scraper as in our docs). # Eventually we can make this a config var like matplotlib_agg or something # if people need us not to set it to Agg. try: _import_matplotlib() except (ImportError, ValueError): pass # Check for srcset hidpi images srcset = gallery_conf["image_srcset"] if not isinstance(srcset, (list, tuple)): raise ConfigError( "image_srcset must be a list of strings with the " 'multiplicative factor followed by an "x", ' 'e.g. ["2.0x", "1.5x"]' ) srcset_mult_facs = set() for st in srcset: if not (isinstance(st, str) and st[-1:] == "x"): raise ConfigError( f"Invalid value for image_srcset parameter: {st!r}. " "Must be a list of strings with the multiplicative " 'factor followed by an "x". e.g. ["2.0x", "1.5x"]' ) # "2x" -> "2.0" srcset_mult_facs.add(float(st[:-1])) srcset_mult_facs -= {1} # 1x is always saved. gallery_conf["image_srcset"] = [*sorted(srcset_mult_facs)] del srcset, srcset_mult_facs # compress_images compress_images = gallery_conf["compress_images"] if isinstance(compress_images, str): compress_images = [compress_images] elif not isinstance(compress_images, (tuple, list)): raise ConfigError( "compress_images must be a tuple, list, or str, " f"got {type(compress_images)}" ) compress_images = list(compress_images) allowed_values = ("images", "thumbnails") pops = list() for ki, kind in enumerate(compress_images): if kind not in allowed_values: if kind.startswith("-"): pops.append(ki) continue raise ConfigError( "All entries in compress_images must be one of " f"{allowed_values} or a command-line switch " f'starting with "-", got {kind!r}' ) compress_images_args = [compress_images.pop(p) for p in pops[::-1]] if len(compress_images) and not _has_optipng(): logger.warning( "optipng binaries not found, PNG %s will not be optimized", " and ".join(compress_images), ) compress_images = () gallery_conf["compress_images"] = compress_images gallery_conf["compress_images_args"] = compress_images_args # check resetters _get_callables(gallery_conf, "reset_modules") if not isinstance(gallery_conf["reset_modules_order"], str): raise ConfigError( "reset_modules_order must be a str, " f'got {gallery_conf["reset_modules_order"]!r}' ) if gallery_conf["reset_modules_order"] not in ["before", "after", "both"]: raise ConfigError( "reset_modules_order must be in" "['before', 'after', 'both'], " f"got {gallery_conf['reset_modules_order']!r}" ) # Ensure the first cell text is a string if we have it first_cell = gallery_conf.get("first_notebook_cell") if (not isinstance(first_cell, str)) and (first_cell is not None): raise ConfigError( "The 'first_notebook_cell' parameter must be type " f"str or None, found type {type(first_cell)}" ) # Ensure the last cell text is a string if we have it last_cell = gallery_conf.get("last_notebook_cell") if (not isinstance(last_cell, str)) and (last_cell is not None): raise ConfigError( "The 'last_notebook_cell' parameter must be type str" f" or None, found type {type(last_cell)}" ) # Check pypandoc pypandoc = gallery_conf["pypandoc"] if not isinstance(pypandoc, (dict, bool)): raise ConfigError( "'pypandoc' parameter must be of type bool or dict," f"got: {type(pypandoc)}." ) gallery_conf["pypandoc"] = dict() if pypandoc is True else pypandoc has_pypandoc, version = _has_pypandoc() if isinstance(gallery_conf["pypandoc"], dict) and has_pypandoc is None: logger.warning( "'pypandoc' not available. Using Sphinx-Gallery to " "convert rst text blocks to markdown for .ipynb files." ) gallery_conf["pypandoc"] = False elif isinstance(gallery_conf["pypandoc"], dict): logger.info( "Using pandoc version: %s to convert rst text blocks to " "markdown for .ipynb files", version, ) else: logger.info( "Using Sphinx-Gallery to convert rst text blocks to " "markdown for .ipynb files." ) if isinstance(pypandoc, dict): accepted_keys = ("extra_args", "filters") for key in pypandoc: if key not in accepted_keys: raise ConfigError( "'pypandoc' only accepts the following key " f"values: {accepted_keys}, got: {key}." ) gallery_conf["titles"] = {} # Ensure 'backreferences_dir' is str, pathlib.Path or None backref = gallery_conf["backreferences_dir"] if (not isinstance(backref, (str, pathlib.Path))) and (backref is not None): raise ConfigError( "The 'backreferences_dir' parameter must be of type " "str, pathlib.Path or None, " f"found type {type(backref)}" ) # if 'backreferences_dir' is pathlib.Path, make str for Python <=3.5 # compatibility if isinstance(backref, pathlib.Path): gallery_conf["backreferences_dir"] = str(backref) # binder gallery_conf["binder"] = check_binder_conf(gallery_conf["binder"]) # jupyterlite gallery_conf["jupyterlite"] = check_jupyterlite_conf( gallery_conf["jupyterlite"], app, ) if not isinstance(gallery_conf["css"], (list, tuple)): raise ConfigError( 'gallery_conf["css"] must be list or tuple, got ' f'{gallery_conf["css"]!r}' ) for css in gallery_conf["css"]: if css not in _KNOWN_CSS: raise ConfigError(f"Unknown css {css!r}, must be one of {_KNOWN_CSS!r}") if app is not None: # can be None in testing app.add_css_file(css + ".css") # check API usage if not isinstance(gallery_conf["api_usage_ignore"], str): raise ConfigError( 'gallery_conf["api_usage_ignore"] must be str, ' f'got {type(gallery_conf["api_usage_ignore"])}' ) if ( not isinstance(gallery_conf["show_api_usage"], bool) and gallery_conf["show_api_usage"] != "unused" ): raise ConfigError( 'gallery_conf["show_api_usage"] must be True, False or "unused", ' f'got {gallery_conf["show_api_usage"]}' ) # classes (not pickleable so need to resolve using fully qualified name) _get_class(gallery_conf, "within_subsection_order") # make sure it works _update_gallery_conf_exclude_implicit_doc(gallery_conf) return gallery_conf def get_subsections(srcdir, examples_dir, gallery_conf, check_for_index=True): """Return the list of subsections of a gallery. Parameters ---------- srcdir : str absolute path to directory containing conf.py examples_dir : str path to the examples directory relative to conf.py gallery_conf : Dict[str, Any] Sphinx-Gallery configuration dictionary. check_for_index : bool only return subfolders with a ReadMe, default True Returns ------- out : list sorted list of gallery subsection folder names """ if gallery_conf["subsection_order"] is None: sortkey = None else: (sortkey,) = _get_callables(gallery_conf, "subsection_order") if isinstance(sortkey, list): sortkey = ExplicitOrder(sortkey) subfolders = [subfolder for subfolder in os.listdir(examples_dir)] if check_for_index: subfolders = [ subfolder for subfolder in subfolders if _get_readme( os.path.join(examples_dir, subfolder), gallery_conf, raise_error=False ) is not None ] else: # just make sure its a directory subfolders = [ subfolder for subfolder in subfolders if os.path.isdir(os.path.join(examples_dir, subfolder)) ] base_examples_dir_path = os.path.relpath(examples_dir, srcdir) subfolders_with_path = [ os.path.join(base_examples_dir_path, item) for item in subfolders ] sorted_subfolders = sorted(subfolders_with_path, key=sortkey) return [ subfolders[i] for i in [subfolders_with_path.index(item) for item in sorted_subfolders] ] def _prepare_sphx_glr_dirs(gallery_conf, srcdir): """Creates necessary folders for sphinx_gallery files.""" examples_dirs = gallery_conf["examples_dirs"] gallery_dirs = gallery_conf["gallery_dirs"] if not isinstance(examples_dirs, list): examples_dirs = [examples_dirs] if not isinstance(gallery_dirs, list): gallery_dirs = [gallery_dirs] if bool(gallery_conf["backreferences_dir"]): backreferences_dir = os.path.join(srcdir, gallery_conf["backreferences_dir"]) if not os.path.exists(backreferences_dir): os.makedirs(backreferences_dir) return list(zip(examples_dirs, gallery_dirs)) def _format_toctree(items, includehidden=False): """Format a toc tree.""" st = """ .. toctree:: :hidden:""" if includehidden: st += """ :includehidden: """ st += """ {}\n""".format("\n ".join(items)) st += "\n" return st def generate_gallery_rst(app): """Generate the Main examples gallery reStructuredText. Start the Sphinx-Gallery configuration and recursively scan the examples directories in order to populate the examples gallery. We create a 2-level nested structure by iterating through every sibling folder of the current index file. In each of these folders, we look for a section index file, for which we generate a toctree pointing to sibling scripts. Then, we append the content of this section index file to the current index file, after we remove toctree (to keep a clean nested structure) and sphinx tags (to prevent tag duplication) Eventually, we create a toctree in the current index file which points to section index files. """ logger.info("generating gallery...", color="white") gallery_conf = app.config.sphinx_gallery_conf seen_backrefs = set() costs = [] workdirs = _prepare_sphx_glr_dirs(gallery_conf, app.builder.srcdir) # Check for duplicate filenames to make sure linking works as expected examples_dirs = [ex_dir for ex_dir, _ in workdirs] files = collect_gallery_files(examples_dirs, gallery_conf) check_duplicate_filenames(files) check_spaces_in_filenames(files) for examples_dir, gallery_dir in workdirs: examples_dir_abs_path = os.path.join(app.builder.srcdir, examples_dir) gallery_dir_abs_path = os.path.join(app.builder.srcdir, gallery_dir) # Create section rst files and fetch content which will # be added to current index file. This only includes content # from files located in the root folder of the current gallery # (ie not in subfolders) ( _, this_content, this_costs, this_toctree_items, ) = generate_dir_rst( examples_dir_abs_path, gallery_dir_abs_path, gallery_conf, seen_backrefs, include_toctree=False, ) has_readme = this_content is not None costs += this_costs write_computation_times(gallery_conf, gallery_dir_abs_path, this_costs) # We create an index.rst with all examples # (this will overwrite the rst file generated by the previous call # to generate_dir_rst) if this_content: # :orphan: to suppress "not included in TOCTREE" sphinx warnings indexst = ":orphan:\n\n" + this_content else: # we are not going to use the index.rst.new that gets made here, # but go through the motions to run through all the subsections... indexst = "Never used!" # Write toctree with gallery items from gallery root folder if len(this_toctree_items) > 0: this_toctree = _format_toctree(this_toctree_items) indexst += this_toctree # list all paths to subsection index files in this array subsection_index_files = [] subsecs = get_subsections( app.builder.srcdir, examples_dir_abs_path, gallery_conf, check_for_index=has_readme, ) for subsection in subsecs: src_dir = os.path.join(examples_dir_abs_path, subsection) target_dir = os.path.join(gallery_dir_abs_path, subsection) subsection_index_files.append( "/".join(["", gallery_dir, subsection, "index.rst"]).replace( os.sep, "/" ) # fwd slashes needed in rst ) ( subsection_index_path, subsection_index_content, subsection_costs, subsection_toctree_filenames, ) = generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs) if subsection_index_content: # Filter out tags from subsection content # to prevent tag duplication across the documentation tag_regex = r"^\.\.(\s+)\_(.+)\:(\s*)$" subsection_index_content = "\n".join( [ line for line in subsection_index_content.splitlines() if re.match(tag_regex, line) is None ] + [""] ) indexst += subsection_index_content has_readme_subsection = True else: has_readme_subsection = False # Write subsection toctree in main file only if # nested_sections is False or None, and # toctree filenames were generated for the subsection. if not gallery_conf["nested_sections"]: if len(subsection_toctree_filenames) > 0: subsection_index_toctree = _format_toctree( subsection_toctree_filenames ) indexst += subsection_index_toctree # Otherwise, a new index.rst.new file should # have been created and it needs to be parsed elif has_readme_subsection: _replace_md5(subsection_index_path, mode="t") costs += subsection_costs write_computation_times(gallery_conf, target_dir, subsection_costs) # Build recommendation system if gallery_conf["recommender"]["enable"]: try: import numpy as np # noqa: F401 except ImportError: raise ConfigError("gallery_conf['recommender'] requires numpy") recommender_params = copy.deepcopy(gallery_conf["recommender"]) recommender_params.pop("enable") recommender_params.pop("rubric_header", None) recommender = ExampleRecommender(**recommender_params) gallery_py_files = [] # root and subsection directories containing python examples gallery_directories = [gallery_dir_abs_path] + subsecs for current_dir in gallery_directories: src_dir = os.path.join(gallery_dir_abs_path, current_dir) # sort python files to have a deterministic input across call py_files = sorted( [ fname for fname in Path(src_dir).iterdir() if fname.suffix == ".py" ], key=_get_class(gallery_conf, "within_subsection_order")(src_dir), ) gallery_py_files.append( [os.path.join(src_dir, fname) for fname in py_files] ) # flatten the list of list gallery_py_files = list(chain.from_iterable(gallery_py_files)) recommender.fit(gallery_py_files) for fname in gallery_py_files: _write_recommendations(recommender, fname, gallery_conf) # generate toctree with subsections if gallery_conf["nested_sections"] is True: subsections_toctree = _format_toctree( subsection_index_files, includehidden=True ) # add toctree to file only if there are subsections if len(subsection_index_files) > 0: indexst += subsections_toctree if gallery_conf["download_all_examples"]: download_fhindex = generate_zipfiles( gallery_dir_abs_path, app.builder.srcdir, gallery_conf ) indexst += download_fhindex if app.config.sphinx_gallery_conf["show_signature"]: indexst += SPHX_GLR_SIG if has_readme: index_rst_new = os.path.join(gallery_dir_abs_path, "index.rst.new") with codecs.open(index_rst_new, "w", encoding="utf-8") as fhindex: fhindex.write(indexst) _replace_md5(index_rst_new, mode="t") # Write a single global sg_execution_times write_computation_times(gallery_conf, None, costs) if gallery_conf["show_api_usage"] is not False: _init_api_usage(app.builder.srcdir) _finalize_backreferences(seen_backrefs, gallery_conf) if gallery_conf["plot_gallery"]: logger.info("computation time summary:", color="white") lines, lens = _format_for_writing( costs, src_dir=gallery_conf["src_dir"], kind="console" ) for name, t, m in lines: text = (f" - {name}: ").ljust(lens[0] + 10) if t is None: text += "(not run)" logger.info(text) else: t_float = float(t.split()[0]) if t_float >= gallery_conf["min_reported_time"]: text += t.rjust(lens[1]) + " " + m.rjust(lens[2]) logger.info(text) # Also create a junit.xml file, useful e.g. on CircleCI write_junit_xml(gallery_conf, app.builder.outdir, costs) SPHX_GLR_ORPHAN = """ :orphan: .. _{0}: """ SPHX_GLR_COMP_TIMES = ( SPHX_GLR_ORPHAN + """ Computation times ================= """ ) def _sec_to_readable(t): """Convert a number of seconds to a more readable representation.""" # This will only work for < 1 day execution time # And we reserve 2 digits for minutes because presumably # there aren't many > 99 minute scripts, but occasionally some # > 9 minute ones t = datetime(1, 1, 1) + timedelta(seconds=t) t = "{:02d}:{:02d}.{:03d}".format( t.hour * 60 + t.minute, t.second, int(round(t.microsecond / 1000.0)) ) return t def _cost_key(cost): """Cost sorting function.""" # sort by descending computation time, descending memory, alphabetical name return (-cost["t"], -cost["mem"], cost["src_file"]) def _format_for_writing(costs, *, src_dir, kind="rst"): """Provide formatted computation summary text. Parameters ---------- costs: List[Dict] List of dicts of computation costs and paths, see gen_rst.py for details. src_dir : pathlib.Path The Sphinx source directory. kind: 'rst', 'rst-full' or 'console', default='rst' Format for printing to 'console' or for writing `sg_execution_times.rst' ('rst' for single galleries and 'rst-full' for all galleries). Returns ------- lines: List[List[str]] Formatted computation text for each example, of format: [example_file, time_elapsed, memory_used] lens: List[int] Character length of each string in `lines`. """ lines = list() for cost in sorted(costs, key=_cost_key): src_file = cost["src_file"] rel_path = os.path.relpath(src_file, src_dir) if kind in ("rst", "rst-full"): # like in sg_execution_times target_dir_clean = os.path.relpath(cost["target_dir"], src_dir).replace( os.path.sep, "_" ) paren = rel_path if kind == "rst-full" else os.path.basename(src_file) name = ":ref:`sphx_glr_{0}_{1}` (``{2}``)".format( target_dir_clean, os.path.basename(src_file), paren ) t = _sec_to_readable(cost["t"]) else: # like in generate_gallery assert kind == "console" name = rel_path t = f'{cost["t"]:0.2f} sec' m = f'{cost["mem"]:.1f} MB' lines.append([name, t, m]) lens = [max(x) for x in zip(*[[len(item) for item in cost] for cost in lines])] return lines, lens def write_computation_times(gallery_conf, target_dir, costs): """Write computation times to `sg_execution_times.rst`. Parameters ---------- gallery_conf : Dict[str, Any] Sphinx-Gallery configuration dictionary. target_dir : str | None Path to directory where example python source file are. costs: List[Dict] List of dicts of computation costs and paths, see gen_rst.py for details. """ total_time = sum(cost["t"] for cost in costs) if target_dir is None: # all galleries together out_dir = gallery_conf["src_dir"] where = "all galleries" kind = "rst-full" ref_extra = "" else: # a single gallery out_dir = target_dir where = os.path.relpath(target_dir, gallery_conf["src_dir"]) kind = "rst" ref_extra = f'{where.replace(os.path.sep, "_")}_' new_ref = f"sphx_glr_{ref_extra}sg_execution_times" out_file = Path(out_dir) / "sg_execution_times.rst" if out_file.is_file() and total_time == 0: # a re-run return with out_file.open("w", encoding="utf-8") as fid: fid.write(SPHX_GLR_COMP_TIMES.format(new_ref)) fid.write( f"**{_sec_to_readable(total_time)}** total execution time for " f"{len(costs)} file{'s' if len(costs) != 1 else ''} **from {where}**:\n\n" ) lines, lens = _format_for_writing( costs, src_dir=gallery_conf["src_dir"], kind=kind, ) del costs # https://datatables.net/examples/styling/bootstrap5.html fid.write( # put it in a container to make the scoped style work """\ .. container:: .. raw:: html .. list-table:: :header-rows: 1 :class: table table-striped sg-datatable * - Example - Time - Mem (MB) """ # noqa: E501 ) # Need at least one entry or Sphinx complains for ex, t, mb in lines or [["N/A", "N/A", "N/A"]]: fid.write( f"""\ * - {ex} - {t} - {mb.rsplit(maxsplit=1)[0]} """ ) # remove the "MB" from the right def write_api_entries(app, what, name, obj, options, lines): """Write api entries to `_sg_api_entries` configuration. To connect to `autodoc-process-docstring` event. Parameters ---------- app : The Sphinx application object. what: str The type of the object which the docstring belongs to. One of "module", "class", "exception", "function", "method", "attribute". name : The fully qualified name of the object. obj : The object itself. options : The options given to the directive: an object with attributes inherited_members, undoc_members, show_inheritance and no-index that are true if the flag option of same name was given to the auto directive. lines : The lines of the docstring, see above. """ if app.config.sphinx_gallery_conf["show_api_usage"] is False: return if "_sg_api_entries" not in app.config.sphinx_gallery_conf: app.config.sphinx_gallery_conf["_sg_api_entries"] = dict() if what not in app.config.sphinx_gallery_conf["_sg_api_entries"]: app.config.sphinx_gallery_conf["_sg_api_entries"][what] = set() app.config.sphinx_gallery_conf["_sg_api_entries"][what].add(name) def _init_api_usage(gallery_dir): with codecs.open( os.path.join(gallery_dir, "sg_api_usage.rst"), "w", encoding="utf-8" ): pass # Colors from https://personal.sron.nl/~pault/data/colourschemes.pdf # 3 Diverging Colour Schemes, Figure 12, plus alpha=AA API_COLORS = dict( edge="#00000080", # gray (by alpha) okay="#98CAE180", # blue bad_1="#FEDA8B80", # yellow bad_2="#F67E4B80", # orange bad_3="#A5002680", # red ) def _make_graph(fname, entries, gallery_conf): """Make a graph of unused and used API entries. The used API entries themselves are documented in the list, so for the graph, we'll focus on the number of unused API entries per modules. Modules with lots of unused entries (11+) will be colored red, those with less (6+) will be colored orange, those with only a few (1-5) will be colored yellow and those with no unused entries will be colored blue. The API entries that are used are shown with one graph per module. That way you can see the examples that each API entry is used in for that module (if this was done for the whole project at once, the graph would get too large very large quickly). Parameters ---------- fname: str Path to '*sg_api_unused.dot' file. entries: Dict[str, List] or List[str] Used (List) or unused (Dict) API entries. gallery_conf : Dict[str, Any] Sphinx-Gallery configuration dictionary. """ import graphviz dg = graphviz.Digraph( filename=fname, graph_attr={ "overlap": "scale", "pad": "0.5", }, node_attr={ "color": API_COLORS["okay"], "style": "filled", "fontsize": "20", "shape": "box", "fontname": "Open Sans,Arial", }, ) if isinstance(entries, list): connections = set() lut = dict() # look up table for connections so they don't repeat structs = [entry.split(".") for entry in entries] for struct in sorted(structs, key=len): for level in range(len(struct) - 2): if (struct[level], struct[level + 1]) in connections: continue connections.add((struct[level], struct[level + 1])) node_from = ( lut[struct[level]] if struct[level] in lut else struct[level] ) dg.node(node_from) node_to = struct[level + 1] node_kwargs = dict() # count, don't show leaves if len(struct) - 3 == level: leaf_count = 0 for struct2 in structs: # find structures of the same length as struct if len(struct2) != level + 3: continue # find structures with two entries before # the leaf that are the same as struct if all( [ struct2[level2] == struct[level2] for level2 in range(level + 2) ] ): leaf_count += 1 node_to += f" ({leaf_count})" lut[struct[level + 1]] = node_to if leaf_count > 10: color_key = "bad_3" elif leaf_count > 5: color_key = "bad_2" else: color_key = "bad_1" node_kwargs["color"] = API_COLORS[color_key] dg.node(node_to, **node_kwargs) dg.edge(node_from, node_to, color=API_COLORS["edge"]) # add modules with all API entries for module in gallery_conf["_sg_api_entries"]["module"]: struct = module.split(".") for i in range(len(struct) - 1): if struct[i + 1] not in lut: dg.edge(struct[i], struct[i + 1]) else: assert isinstance(entries, dict) for entry, refs in entries.items(): dg.node(entry) for ref in refs: dg.node(ref, color=API_COLORS["bad_1"]) dg.edge(entry, ref, color=API_COLORS["edge"]) dg.save() def write_api_entry_usage(app, docname, source): """Write an html page describing which API entries are used and unused. To document and graph only those API entries that are used by autodoc, we have to wait for autodoc to finish and hook into the ``source-read`` event. This intercepts the text from the rst such that it can be modified. Since, we only touched an empty file, we have to add 1) a list of all the API entries that are unused and a graph of the number of unused API entries per module and 2) a list of API entries that are used in examples, each with a sub-list of which examples that API entry is used in, and a graph that connects all of the API entries in a module to the examples that they are used in. Parameters ---------- app : The Sphinx application object. docname : Docname of the document currently being parsed. source : List whose single element is the contents of the source file """ docname = docname or "" # can be None on Sphinx 7.2 if docname != "sg_api_usage": return gallery_conf = app.config.sphinx_gallery_conf if gallery_conf["show_api_usage"] is False: return # since this is done at the gallery directory level (as opposed # to in a gallery directory, e.g. auto_examples), it runs last # which means that all the api entries will be in gallery_conf # Always write at least the title source[0] = SPHX_GLR_ORPHAN.format("sphx_glr_sg_api_usage") title = "Unused API Entries" source[0] += title + "\n" + "^" * len(title) + "\n\n" if ( "_sg_api_entries" not in gallery_conf or gallery_conf["backreferences_dir"] is None ): source[0] += "No API entries found, not computed.\n\n" return backreferences_dir = os.path.join( gallery_conf["src_dir"], gallery_conf["backreferences_dir"] ) example_files = set.union( *[ gallery_conf["_sg_api_entries"][obj_type] for obj_type in ("class", "method", "function") if obj_type in gallery_conf["_sg_api_entries"] ] ) if len(example_files) == 0: source[0] += "No examples run, not computed.\n\n" return def get_entry_type(entry): if entry in gallery_conf["_sg_api_entries"].get("class", []): return "class" elif entry in gallery_conf["_sg_api_entries"].get("method", []): return "meth" else: assert entry in gallery_conf["_sg_api_entries"]["function"] return "func" # find used and unused API entries unused_api_entries = list() used_api_entries = dict() for entry in example_files: # don't include built-in methods etc. if re.match(gallery_conf["api_usage_ignore"], entry) is not None: continue # check if backreferences empty example_fname = os.path.join(backreferences_dir, f"{entry}.examples.new") if not os.path.isfile(example_fname): # use without new example_fname = os.path.splitext(example_fname)[0] assert os.path.isfile(example_fname) if os.path.getsize(example_fname) == 0: unused_api_entries.append(entry) else: used_api_entries[entry] = list() with open(example_fname, encoding="utf-8") as fid2: for line in fid2: if line.startswith(" :ref:"): example_name = line.split("`")[1] used_api_entries[entry].append(example_name) for entry in sorted(unused_api_entries): source[0] += f"- :{get_entry_type(entry)}:`{entry}`\n" source[0] += "\n\n" has_graphviz = _has_graphviz() if has_graphviz and unused_api_entries: source[0] += ( ".. graphviz:: ./sg_api_unused.dot\n" " :alt: API unused entries graph\n" " :layout: neato\n\n" ) used_count = len(used_api_entries) total_count = used_count + len(unused_api_entries) used_percentage = used_count / max(total_count, 1) # avoid div by zero source[0] += ( "\nAPI entries used: " f"{round(used_percentage * 100, 2)}% " f"({used_count}/{total_count})\n\n" ) if has_graphviz and unused_api_entries: _make_graph( os.path.join(app.builder.srcdir, "sg_api_unused.dot"), unused_api_entries, gallery_conf, ) if gallery_conf["show_api_usage"] is True and used_api_entries: title = "Used API Entries" source[0] += title + "\n" + "^" * len(title) + "\n\n" for entry in sorted(used_api_entries): source[0] += f"- :{get_entry_type(entry)}:`{entry}`\n\n" for ref in used_api_entries[entry]: source[0] += f" - :ref:`{ref}`\n" source[0] += "\n\n" if has_graphviz: used_modules = {entry.split(".")[0] for entry in used_api_entries} for module in sorted(used_modules): source[0] += ( f"{module}\n" + "^" * len(module) + "\n\n" f".. graphviz:: ./{module}_sg_api_used.dot\n" f" :alt: {module} usage graph\n" " :layout: neato\n\n" ) for module in used_modules: logger.info("Making API usage graph for %s", module) # select and format entries for this module entries = dict() for entry, ref in used_api_entries.items(): if entry.split(".")[0] == module: entry = entry.replace("sphx_glr_", "") # remove prefix for target_dir in gallery_conf["gallery_dirs"]: if entry.startswith(target_dir): entry = entry[len(target_dir) + 1 :] _make_graph( os.path.join(app.builder.srcdir, f"{module}_sg_api_used.dot"), entries, gallery_conf, ) def clean_api_usage_files(app, exception): """Remove api usage .dot files. To connect to 'build-finished' event. """ if os.path.isfile(os.path.join(app.builder.srcdir, "sg_api_usage.rst")): os.remove(os.path.join(app.builder.srcdir, "sg_api_usage.rst")) if os.path.isfile(os.path.join(app.builder.srcdir, "sg_api_unused.dot")): os.remove(os.path.join(app.builder.srcdir, "sg_api_unused.dot")) for file in os.listdir(app.builder.srcdir): if "sg_api_used.dot" in file: os.remove(os.path.join(app.builder.srcdir, file)) def write_junit_xml(gallery_conf, target_dir, costs): """Write JUnit XML file of example run times, successes, and failures. Parameters ---------- gallery_conf : Dict[str, Any] Sphinx-Gallery configuration dictionary. target_dir : Union[str, pathlib.Path] Build directory. costs: List[Tuple[Tuple[float], str]] List of dicts of computation costs and paths, see gen_rst.py for details. """ if not gallery_conf["junit"] or not gallery_conf["plot_gallery"]: return failing_as_expected, failing_unexpectedly, passing_unexpectedly = _parse_failures( gallery_conf ) n_tests = 0 n_failures = 0 n_skips = 0 elapsed = 0.0 src_dir = gallery_conf["src_dir"] output = "" for cost in costs: t, fname = cost["t"], cost["src_file"] if not any( fname in x for x in ( gallery_conf["passing_examples"], failing_unexpectedly, failing_as_expected, passing_unexpectedly, ) ): continue # not subselected by our regex title = gallery_conf["titles"][fname] output += ( ''.format( quoteattr(os.path.splitext(os.path.basename(fname))[0]), quoteattr(os.path.relpath(fname, src_dir)), quoteattr(title), t, ) ) if fname in failing_as_expected: output += '' n_skips += 1 elif fname in failing_unexpectedly or fname in passing_unexpectedly: if fname in failing_unexpectedly: traceback = gallery_conf["failing_examples"][fname] else: # fname in passing_unexpectedly traceback = "Passed even though it was marked to fail" n_failures += 1 output += "{!s}".format( quoteattr(traceback.splitlines()[-1].strip()), escape(traceback) ) output += "" n_tests += 1 elapsed += t output += "" output = ( '' ''.format( n_failures, n_skips, n_tests, elapsed ) ) + output # Actually write it fname = os.path.normpath(os.path.join(target_dir, gallery_conf["junit"])) junit_dir = os.path.dirname(fname) if not os.path.isdir(junit_dir): os.makedirs(junit_dir) with codecs.open(fname, "w", encoding="utf-8") as fid: fid.write(output) def touch_empty_backreferences(app, what, name, obj, options, lines): """Generate empty back-reference example files. This avoids inclusion errors/warnings if there are no gallery examples for a class / module that is being parsed by autodoc. """ if not bool(app.config.sphinx_gallery_conf["backreferences_dir"]): return examples_path = os.path.join( app.srcdir, app.config.sphinx_gallery_conf["backreferences_dir"], f"{name}.examples", ) if not os.path.exists(examples_path): # touch file open(examples_path, "w").close() def _expected_failing_examples(gallery_conf): return { os.path.normpath(os.path.join(gallery_conf["src_dir"], path)) for path in gallery_conf["expected_failing_examples"] } def _parse_failures(gallery_conf): """Split the failures.""" failing_examples = set(gallery_conf["failing_examples"].keys()) expected_failing_examples = _expected_failing_examples(gallery_conf) failing_as_expected = failing_examples.intersection(expected_failing_examples) failing_unexpectedly = failing_examples.difference(expected_failing_examples) passing_unexpectedly = expected_failing_examples.difference(failing_examples) # filter from examples actually run passing_unexpectedly = [ src_file for src_file in passing_unexpectedly if re.search(gallery_conf["filename_pattern"], src_file) ] return failing_as_expected, failing_unexpectedly, passing_unexpectedly def summarize_failing_examples(app, exception): """Collects the list of falling examples and prints them with a traceback. Raises ValueError if there where failing examples. """ if exception is not None: return # Under no-plot Examples are not run so nothing to summarize if not app.config.sphinx_gallery_conf["plot_gallery"]: logger.info( 'Sphinx-Gallery gallery_conf["plot_gallery"] was ' "False, so no examples were executed.", color="brown", ) return gallery_conf = app.config.sphinx_gallery_conf failing_as_expected, failing_unexpectedly, passing_unexpectedly = _parse_failures( gallery_conf ) idt = " " if failing_as_expected: logger.info(bold("Examples failing as expected:"), color="blue") for fail_example in failing_as_expected: path = os.path.relpath(fail_example, gallery_conf["src_dir"]) logger.info( f"{bold(blue(path))} failed leaving traceback:\n\n" f"{indent(gallery_conf['failing_examples'][fail_example], idt)}" ) fail_msgs = [] if failing_unexpectedly: fail_msgs.append(bold(red("Unexpected failing examples:\n"))) for fail_example in failing_unexpectedly: path = os.path.relpath(fail_example, gallery_conf["src_dir"]) fail_msgs.append( f" {bold(red(path))} failed leaving traceback:\n\n" f"{indent(gallery_conf['failing_examples'][fail_example], idt)}" ) if passing_unexpectedly: paths = [ os.path.relpath(p, gallery_conf["src_dir"]) for p in passing_unexpectedly ] fail_msgs.append( bold(red("Examples expected to fail, but not failing:\n\n")) + red("\n".join(indent(p, idt) for p in paths)) + "\n\nPlease remove these examples from " + "sphinx_gallery_conf['expected_failing_examples'] " + "in your conf.py file." ) # standard message n_good = len(gallery_conf["passing_examples"]) n_tot = len(gallery_conf["failing_examples"]) + n_good n_stale = len(gallery_conf["stale_examples"]) logger.info( "\nSphinx-Gallery successfully executed %d out of %d " "file%s subselected by:\n\n" ' gallery_conf["filename_pattern"] = %r\n' ' gallery_conf["ignore_pattern"] = %r\n' "\nafter excluding %d file%s that had previously been run " "(based on MD5).\n", n_good, n_tot, "s" if n_tot != 1 else "", gallery_conf["filename_pattern"], gallery_conf["ignore_pattern"], n_stale, "s" if n_stale != 1 else "", color="brown", ) if fail_msgs: fail_message = bold( purple( "Here is a summary of the problems encountered " "when running the examples:\n\n" + "\n".join(fail_msgs) + "\n" + "-" * 79 ) ) if gallery_conf["only_warn_on_example_error"]: logger.warning(fail_message) else: raise ExtensionError(fail_message) def collect_gallery_files(examples_dirs, gallery_conf): """Collect python files from the gallery example directories.""" files = [] for example_dir in examples_dirs: for root, dirnames, filenames in os.walk(example_dir): for filename in filenames: if filename.endswith(".py"): if re.search(gallery_conf["ignore_pattern"], filename) is None: files.append(os.path.join(root, filename)) return files def check_duplicate_filenames(files): """Check for duplicate filenames across gallery directories.""" # Check whether we'll have duplicates used_names = set() dup_names = list() for this_file in files: this_fname = os.path.basename(this_file) if this_fname in used_names: dup_names.append(this_file) else: used_names.add(this_fname) if len(dup_names) > 0: logger.warning( "Duplicate example file name(s) found. Having duplicate file " "names will break some links. " "List of files: %s", sorted(dup_names), ) def check_spaces_in_filenames(files): """Check for spaces in filenames across example directories.""" regex = re.compile(r"[\s]") files_with_space = list(filter(regex.search, files)) if files_with_space: logger.warning( "Example file name(s) with space(s) found. Having space(s) in " "file names will break some links. " "List of files: %s", sorted(files_with_space), ) def get_default_config_value(key): """Get default configuration function.""" def default_getter(conf): return conf["sphinx_gallery_conf"].get(key, DEFAULT_GALLERY_CONF[key]) return default_getter def fill_gallery_conf_defaults(app, config, check_keys=True): """Check the sphinx-gallery config and set its defaults. This is called early at config-inited, so that all the rest of the code can do things like ``sphinx_gallery_conf['binder']['use_jupyter_lab']``, even if the keys have not been set explicitly in conf.py. """ new_sphinx_gallery_conf = _fill_gallery_conf_defaults( config.sphinx_gallery_conf, app=app, check_keys=check_keys ) config.sphinx_gallery_conf = new_sphinx_gallery_conf config.html_static_path.append(glr_path_static()) def update_gallery_conf_builder_inited(app): """Update the the sphinx-gallery config at builder-inited.""" plot_gallery = _bool_eval(app.builder.config.plot_gallery) src_dir = app.builder.srcdir abort_on_example_error = _bool_eval(app.builder.config.abort_on_example_error) _update_gallery_conf_builder_inited( app.config.sphinx_gallery_conf, src_dir, plot_gallery=plot_gallery, abort_on_example_error=abort_on_example_error, builder_name=app.builder.name, ) def setup(app): """Setup Sphinx-Gallery sphinx extension.""" app.add_config_value("sphinx_gallery_conf", DEFAULT_GALLERY_CONF, "html") for key in ["plot_gallery", "abort_on_example_error"]: app.add_config_value(key, get_default_config_value(key), "html") # Early filling of sphinx_gallery_conf defaults at config-inited app.connect("config-inited", fill_gallery_conf_defaults, priority=10) # set small priority value, so that pre_configure_jupyterlite_sphinx is # called before jupyterlite_sphinx config-inited app.connect("config-inited", pre_configure_jupyterlite_sphinx, priority=100) # set high priority value, so that post_configure_jupyterlite_sphinx is # called after jupyterlite_sphinx config-inited app.connect("config-inited", post_configure_jupyterlite_sphinx, priority=900) if "sphinx.ext.autodoc" in app.extensions: app.connect("autodoc-process-docstring", touch_empty_backreferences) app.connect("autodoc-process-docstring", write_api_entries) app.connect("source-read", write_api_entry_usage) # Add the custom directive app.add_directive("minigallery", MiniGallery) app.add_directive("image-sg", ImageSg) imagesg_addnode(app) # Early update of sphinx_gallery_conf at builder-inited app.connect("builder-inited", update_gallery_conf_builder_inited, priority=10) app.connect("builder-inited", generate_gallery_rst) app.connect("build-finished", copy_binder_files) app.connect("build-finished", create_jupyterlite_contents) app.connect("build-finished", summarize_failing_examples) app.connect("build-finished", embed_code_links) app.connect("build-finished", clean_api_usage_files) metadata = { "parallel_read_safe": True, "parallel_write_safe": True, "version": _sg_version, } return metadata def setup_module(): """Hack to stop nosetests running setup() above.""" pass sphinx-gallery-0.16.0/sphinx_gallery/gen_rst.py000066400000000000000000001550051461331107500216160ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """reST file generator. Generate the rst files for the examples by iterating over the python example files. Files that generate images should start with 'plot'. """ from time import time import copy import contextlib import ast import codecs from functools import partial, lru_cache import gc import pickle import importlib import inspect from io import StringIO import os from pathlib import Path import re import stat from textwrap import indent import warnings from shutil import copyfile import subprocess import sys import traceback import codeop from sphinx.errors import ExtensionError, ConfigError import sphinx.util from sphinx.util.console import blue, red, bold from .scrapers import ( save_figures, ImagePathIterator, clean_modules, _find_image_ext, _scraper_dict, _reset_dict, ) from .utils import ( scale_image, get_md5sum, _replace_md5, optipng, status_iterator, ) from . import glr_path_static from .backreferences import ( _write_backreferences, _thumbnail_div, identify_names, _make_ref_regex, THUMBNAIL_PARENT_DIV, THUMBNAIL_PARENT_DIV_CLOSE, ) from . import py_source_parser from .block_parser import BlockParser from .notebook import jupyter_notebook, save_notebook from .interactive_example import gen_binder_rst from .interactive_example import gen_jupyterlite_rst logger = sphinx.util.logging.getLogger("sphinx-gallery") ############################################################################### class _LoggingTee: """A tee object to redirect streams to the logger.""" def __init__(self, src_filename): self.logger = logger self.src_filename = src_filename self.logger_buffer = "" self.set_std_and_reset_position() # For TextIO compatibility self.closed = False self.encoding = "utf-8" def set_std_and_reset_position(self): if not isinstance(sys.stdout, _LoggingTee): self.origs = (sys.stdout, sys.stderr) sys.stdout = sys.stderr = self self.first_write = True self.output = StringIO() return self def restore_std(self): sys.stdout.flush() sys.stderr.flush() sys.stdout, sys.stderr = self.origs def write(self, data): self.output.write(data) if self.first_write: self.logger.verbose("Output from %s", self.src_filename, color="brown") self.first_write = False data = self.logger_buffer + data lines = data.splitlines() if data and data[-1] not in "\r\n": # Wait to write last line if it's incomplete. It will write next # time or when the LoggingTee is flushed. self.logger_buffer = lines[-1] lines = lines[:-1] else: self.logger_buffer = "" for line in lines: self.logger.verbose("%s", line) def flush(self): self.output.flush() if self.logger_buffer: self.logger.verbose("%s", self.logger_buffer) self.logger_buffer = "" # For TextIO compatibility def close(self): pass def fileno(self): return self.output.fileno() def isatty(self): return self.output.isatty() def readable(self): return False def seekable(self): return False def tell(self): return self.output.tell() def writable(self): return True @property def errors(self): return self.output.errors @property def newlines(self): return self.output.newlines # When called in gen_rst, conveniently use context managing def __enter__(self): return self def __exit__(self, type_, value, tb): self.restore_std() ############################################################################### # The following strings are used when we have several pictures: we use # an html div tag that our CSS uses to turn the lists into horizontal # lists. HLIST_HEADER = """ .. rst-class:: sphx-glr-horizontal """ HLIST_IMAGE_TEMPLATE = """ * .. image:: /%s :class: sphx-glr-multi-img """ SINGLE_IMAGE = """ .. image:: /%s :class: sphx-glr-single-img """ CODE_OUTPUT = """.. rst-class:: sphx-glr-script-out .. code-block:: none {0}\n""" TIMING_CONTENT = """ .. rst-class:: sphx-glr-timing **Total running time of the script:** ({0:.0f} minutes {1:.3f} seconds) """ SPHX_GLR_SIG = """\n .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_ """ # Header used to include raw html HTML_HEADER = """.. raw:: html
{0}


""" DOWNLOAD_LINKS_HEADER = """ .. _sphx_glr_download_{0}: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example """ CODE_DOWNLOAD = """ .. container:: sphx-glr-download sphx-glr-download-python :download:`Download {1} source code: {0} <{0}>` """ NOTEBOOK_DOWNLOAD = """ .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: {0} <{0}>` """ RECOMMENDATIONS_INCLUDE = """\n .. include:: {0}.recommendations """ def codestr2rst(codestr, lang="python", lineno=None): """Return reStructuredText code block from code string.""" if lineno is not None: # Sphinx only starts numbering from the first non-empty line. blank_lines = codestr.count("\n", 0, -len(codestr.lstrip())) lineno = f" :lineno-start: {lineno + blank_lines}\n" else: lineno = "" # If the whole block is indented, prevent Sphinx from removing too much whitespace dedent = " :dedent: 1\n" for line in codestr.splitlines(): if line and not line.startswith((" ", "\t")): dedent = "" break code_directive = f".. code-block:: {lang}\n{dedent}{lineno}\n" indented_block = indent(codestr, " " * 4) return code_directive + indented_block def _regroup(x): x = x.groups() return x[0] + x[1].split(".")[-1] + x[2] def _sanitize_rst(string): """Use regex to remove at least some sphinx directives.""" # :class:`a.b.c `, :ref:`abc ` --> thing here p, e = r"(\s|^):[^:\s]+:`", r"`(\W|$)" string = re.sub(p + r"\S+\s*<([^>`]+)>" + e, r"\1\2\3", string) # :class:`~a.b.c` --> c string = re.sub(p + r"~([^`]+)" + e, _regroup, string) # :class:`a.b.c` --> a.b.c string = re.sub(p + r"([^`]+)" + e, r"\1\2\3", string) # ``whatever thing`` --> whatever thing p = r"(\s|^)`" string = re.sub(p + r"`([^`]+)`" + e, r"\1\2\3", string) # `whatever thing` --> whatever thing string = re.sub(p + r"([^`]+)" + e, r"\1\2\3", string) # **string** --> string string = re.sub(r"\*\*([^\*]*)\*\*", r"\1", string) # *string* --> string string = re.sub(r"\*([^\*]*)\*", r"\1", string) # `link text `_ --> link text string = re.sub(r"`([^`<>]+) <[^`<>]+>`\_\_?", r"\1", string) # :anchor:`the term` --> the term string = re.sub(r":[a-z]+:`([^`<>]+)( <[^`<>]+>)?`", r"\1", string) # r'\\dfrac' --> r'\dfrac' string = string.replace("\\\\", "\\") return string def extract_intro_and_title(filename, docstring): """Extract and clean the first paragraph of module-level docstring.""" # lstrip is just in case docstring has a '\n\n' at the beginning paragraphs = docstring.lstrip().split("\n\n") # remove comments and other syntax like `.. _link:` paragraphs = [p for p in paragraphs if not p.startswith(".. ") and len(p) > 0] if len(paragraphs) == 0: raise ExtensionError( "Example docstring should have a header for the example title. " "Please check the example file:\n {}\n".format(filename) ) # Title is the first paragraph with any reStructuredText title chars # removed, i.e. lines that consist of (3 or more of the same) 7-bit # non-ASCII chars. # This conditional is not perfect but should hopefully be good enough. title_paragraph = paragraphs[0] match = re.search(r"^(?!([\W _])\1{3,})(.+)", title_paragraph, re.MULTILINE) if match is None: raise ExtensionError( f"Could not find a title in first paragraph:\n{title_paragraph}" ) title = match.group(0).strip() # Use the title if no other paragraphs are provided intro_paragraph = title if len(paragraphs) < 2 else paragraphs[1] # Concatenate all lines of the first paragraph and truncate at 95 chars intro = re.sub("\n", " ", intro_paragraph) intro = _sanitize_rst(intro) if len(intro) > 95: intro = intro[:95] + "..." title = _sanitize_rst(title) return intro, title def md5sum_is_current(src_file, mode="b"): """Checks whether src_file has the same md5 hash as the one on disk.""" src_md5 = get_md5sum(src_file, mode) src_md5_file = str(src_file) + ".md5" if os.path.exists(src_md5_file): with open(src_md5_file) as file_checksum: ref_md5 = file_checksum.read() return src_md5 == ref_md5 return False def save_thumbnail(image_path_template, src_file, script_vars, file_conf, gallery_conf): """Generate and Save the thumbnail image. Parameters ---------- image_path_template : str holds the template where to save and how to name the image src_file : str path to source python file script_vars : dict Configuration and run time variables file_conf : dict File-specific settings given in source file comments as: ``# sphinx_gallery_ = `` gallery_conf : dict Sphinx-Gallery configuration dictionary """ thumb_dir = os.path.join(os.path.dirname(image_path_template), "thumb") if not os.path.exists(thumb_dir): os.makedirs(thumb_dir) # read specification of the figure to display as thumbnail from main text thumbnail_number = file_conf.get("thumbnail_number", None) thumbnail_path = file_conf.get("thumbnail_path", None) # thumbnail_number has priority. if thumbnail_number is None and thumbnail_path is None: # If no number AND no path, set to default thumbnail_number thumbnail_number = 1 if thumbnail_number is None: image_path = os.path.join(gallery_conf["src_dir"], thumbnail_path) else: if not isinstance(thumbnail_number, int): raise ExtensionError( "sphinx_gallery_thumbnail_number setting is not a number, " f"got {thumbnail_number!r}" ) # negative index means counting from the last one if thumbnail_number < 0: thumbnail_number += len(script_vars["image_path_iterator"]) + 1 image_path = image_path_template.format(thumbnail_number) del thumbnail_number, thumbnail_path, image_path_template thumbnail_image_path, ext = _find_image_ext(image_path) base_image_name = os.path.splitext(os.path.basename(src_file))[0] thumb_file = os.path.join(thumb_dir, f"sphx_glr_{base_image_name}_thumb.{ext}") if src_file in gallery_conf["failing_examples"]: img = os.path.join(glr_path_static(), "broken_example.png") elif os.path.exists(thumbnail_image_path): img = thumbnail_image_path elif not os.path.exists(thumb_file): # create something to replace the thumbnail default_thumb_path = gallery_conf["default_thumb_file"] if default_thumb_path is None: default_thumb_path = os.path.join( glr_path_static(), "no_image.png", ) img, ext = _find_image_ext(default_thumb_path) else: return # update extension, since gallery_conf setting can be different # from file_conf # Here we have to do .new.ext so that optipng and PIL behave well thumb_file = f"{os.path.splitext(thumb_file)[0]}.new.{ext}" if ext in ("svg", "gif"): copyfile(img, thumb_file) else: scale_image(img, thumb_file, *gallery_conf["thumbnail_size"]) if "thumbnails" in gallery_conf["compress_images"]: optipng(thumb_file, gallery_conf["compress_images_args"]) fname_old = f"{os.path.splitext(thumb_file)[0][:-3]}{ext}" _replace_md5(thumb_file, fname_old=fname_old) def _get_readme(dir_, gallery_conf, raise_error=True): # first check if there is an index.rst and that index.rst is in the # copyfile regexp: if re.match(gallery_conf["copyfile_regex"], "index.rst"): fpth = os.path.join(dir_, "index.rst") if os.path.isfile(fpth): return None # now look for README.txt, README.rst etc... extensions = [".txt"] + sorted(gallery_conf["source_suffix"]) for ext in extensions: for fname in ("README", "readme"): fpth = os.path.join(dir_, fname + ext) if os.path.isfile(fpth): return fpth if raise_error: raise ExtensionError( "Example directory {} does not have a README file with one " "of the expected file extensions {}. Please write one to " "introduce your gallery.".format(dir_, extensions) ) return None def generate_dir_rst( src_dir, target_dir, gallery_conf, seen_backrefs, include_toctree=True, ): """Generate the gallery reStructuredText for an example directory. Parameters ---------- src_dir: str, Path to example directory containing python files and possibly sub categories target_dir: str, Path where parsed examples (rst, python files, etc) will be outputted gallery_conf : Dict[str, Any] Gallery configurations. seen_backrefs: set, Back references encountered when parsing this gallery will be stored in this set. include_toctree: bool Whether or not toctree should be included in generated rst file. Default = True. Returns ------- index_path: str, Path to index rst file presenting the current example gallery index_content: str, Content which will be written to the index rst file presenting the current example gallery costs: List[Dict] List of dicts of costs for building each element of the gallery with keys "t", "mem", "src_file", and "target_dir". toctree_items: list, List of files included in toctree (independent of include_toctree's value) """ head_ref = os.path.relpath(target_dir, gallery_conf["src_dir"]) subsection_index_content = "" subsection_readme_fname = _get_readme(src_dir, gallery_conf) have_index_rst = False if subsection_readme_fname: with codecs.open(subsection_readme_fname, "r", encoding="utf-8") as fid: subsection_readme_content = fid.read() subsection_index_content += subsection_readme_content else: have_index_rst = True # Add empty lines to avoid bug in issue #165 subsection_index_content += "\n\n" if not os.path.exists(target_dir): os.makedirs(target_dir) # get filenames listdir = [ fname for fname in os.listdir(src_dir) if (s := Path(fname).suffix) and s in gallery_conf["example_extensions"] ] # limit which to look at based on regex (similar to filename_pattern) listdir = [ fname for fname in listdir if re.search( gallery_conf["ignore_pattern"], os.path.normpath(os.path.join(src_dir, fname)), ) is None ] # sort them sorted_listdir = sorted( listdir, key=_get_class(gallery_conf, "within_subsection_order")(src_dir) ) # Add div containing all thumbnails; # this is helpful for controlling grid or flexbox behaviours subsection_index_content += THUMBNAIL_PARENT_DIV entries_text = [] costs = [] subsection_toctree_filenames = [] build_target_dir = os.path.relpath(target_dir, gallery_conf["src_dir"]) iterator = status_iterator( sorted_listdir, f"generating gallery for {build_target_dir}... ", length=len(sorted_listdir), ) for fname in iterator: intro, title, (t, mem) = generate_file_rst( fname, target_dir, src_dir, gallery_conf, seen_backrefs ) src_file = os.path.normpath(os.path.join(src_dir, fname)) costs.append(dict(t=t, mem=mem, src_file=src_file, target_dir=target_dir)) gallery_item_filename = ( (Path(build_target_dir) / fname).with_suffix("").as_posix() ) this_entry = _thumbnail_div( target_dir, gallery_conf["src_dir"], fname, intro, title ) entries_text.append(this_entry) subsection_toctree_filenames.append("/" + gallery_item_filename) for entry_text in entries_text: subsection_index_content += entry_text # Close thumbnail parent div subsection_index_content += THUMBNAIL_PARENT_DIV_CLOSE # Write subsection index file # only if nested_sections is True subsection_index_path = None if gallery_conf["nested_sections"] is True and not have_index_rst: subsection_index_path = os.path.join(target_dir, "index.rst.new") with codecs.open(subsection_index_path, "w", encoding="utf-8") as (findex): findex.write( "\n\n.. _sphx_glr_{}:\n\n".format(head_ref.replace(os.path.sep, "_")) ) findex.write(subsection_index_content) # Create toctree for index file # with all gallery items which belong to current subsection # and add it to generated index rst file if need be. # Toctree cannot be empty # and won't be added if include_toctree is false # (this is useful when generating the example gallery's main # index rst file, which should contain only one toctree) if len(subsection_toctree_filenames) > 0 and include_toctree: subsection_index_toctree = """ .. toctree:: :hidden: {}\n """.format("\n ".join(subsection_toctree_filenames)) findex.write(subsection_index_toctree) if have_index_rst: # the user has supplied index.rst, so blank out the content subsection_index_content = None # Copy over any other files. copyregex = gallery_conf["copyfile_regex"] if copyregex: listdir = [fname for fname in os.listdir(src_dir) if re.match(copyregex, fname)] readme = _get_readme(src_dir, gallery_conf, raise_error=False) # don't copy over the readme if readme: listdir = [fname for fname in listdir if fname != os.path.basename(readme)] for fname in listdir: src_file = os.path.normpath(os.path.join(src_dir, fname)) target_file = os.path.join(target_dir, fname) _replace_md5(src_file, fname_old=target_file, method="copy") return ( subsection_index_path, subsection_index_content, costs, subsection_toctree_filenames, ) def handle_exception(exc_info, src_file, script_vars, gallery_conf): """Trim and format exception, maybe raise error, etc.""" from .gen_gallery import _expected_failing_examples etype, exc, tb = exc_info stack = traceback.extract_tb(tb) # The full traceback will look something like: # # File "/home/larsoner/python/sphinx-gallery/sphinx_gallery/gen_rst.py... # mem_max, _ = gallery_conf['call_memory']( # File "/home/larsoner/python/sphinx-gallery/sphinx_gallery/gen_galler... # mem, out = memory_usage(func, max_usage=True, retval=True, # File "/home/larsoner/.local/lib/python3.8/site-packages/memory_profi... # returned = f(*args, **kw) # File "/home/larsoner/python/sphinx-gallery/sphinx_gallery/gen_rst.py... # exec(self.code, self.fake_main.__dict__) # File "/home/larsoner/python/sphinx-gallery/sphinx_gallery/tests/tiny... # raise RuntimeError('some error') # RuntimeError: some error # # But we should trim these to just the relevant trace at the user level, # so we inspect the traceback to find the start and stop points. start = 0 stop = len(stack) root = os.path.dirname(__file__) + os.sep for ii, s in enumerate(stack, 1): # Trim our internal stack if s.name.startswith("_sg_call_memory"): start = max(ii, start) elif s.filename.startswith(root + "gen_rst.py"): # SyntaxError if s.name == "execute_code_block" and ( "compile(" in s.line or "save_figures" in s.line ): start = max(ii, start) # Any other error elif s.name == "__call__": start = max(ii, start) # Our internal input() check elif s.name == "_check_input" and ii == len(stack): stop = ii - 1 stack = stack[start:stop] formatted_exception = "Traceback (most recent call last):\n" + "".join( traceback.format_list(stack) + traceback.format_exception_only(etype, exc) ) expected = src_file in _expected_failing_examples(gallery_conf) src_file_rel = os.path.relpath(src_file, gallery_conf["src_dir"]) if expected: func, color, kind = logger.info, blue, "expectedly" else: func, color, kind = logger.warning, red, "unexpectedly" func( # needs leading newline to get away from iterator f"\n{bold(color('%s'))} {kind} failed to execute correctly:\n\n%s", src_file_rel, color(indent(formatted_exception, " ")), ) except_rst = codestr2rst(formatted_exception, lang="pytb") # Breaks build on first example error if gallery_conf["abort_on_example_error"]: raise # Stores failing file gallery_conf["failing_examples"][src_file] = formatted_exception script_vars["execute_script"] = False # Ensure it's marked as our style except_rst = ".. rst-class:: sphx-glr-script-out\n\n" + except_rst return except_rst # Adapted from github.com/python/cpython/blob/3.7/Lib/warnings.py def _showwarning(message, category, filename, lineno, file=None, line=None): if file is None: file = sys.stderr if file is None: # sys.stderr is None when run with pythonw.exe: # warnings get lost return text = warnings.formatwarning(message, category, filename, lineno, line) try: file.write(text) except OSError: # the file (probably stderr) is invalid - this warning gets lost. pass @contextlib.contextmanager def patch_warnings(): """Patch warnings.showwarning to actually write out the warning.""" # Sphinx or logging or someone is patching warnings, but we want to # capture them, so let's patch over their patch... orig_showwarning = warnings.showwarning try: warnings.showwarning = _showwarning yield finally: warnings.showwarning = orig_showwarning class _exec_once: """Deal with memory_usage calling functions more than once (argh).""" def __init__(self, code, fake_main): self.code = code self.fake_main = fake_main self.run = False def __call__(self): if not self.run: self.run = True old_main = sys.modules.get("__main__", None) with patch_warnings(): sys.modules["__main__"] = self.fake_main try: exec(self.code, self.fake_main.__dict__) finally: if old_main is not None: sys.modules["__main__"] = old_main def _get_memory_base(): """Get the base amount of memory used by running a Python process.""" # There might be a cleaner way to do this at some point from memory_profiler import memory_usage if sys.platform in ("win32", "darwin"): sleep, timeout = (1, 2) else: sleep, timeout = (0.5, 1) proc = subprocess.Popen( [sys.executable, "-c", f"import time, sys; time.sleep({sleep}); sys.exit(0)"], close_fds=True, ) memories = memory_usage(proc, interval=1e-3, timeout=timeout) kwargs = dict(timeout=timeout) if sys.version_info >= (3, 5) else {} proc.communicate(**kwargs) # On OSX sometimes the last entry can be None memories = [mem for mem in memories if mem is not None] + [0.0] memory_base = max(memories) return memory_base def _ast_module(): """Get ast.Module function, dealing with: https://bugs.python.org/issue35894.""" if sys.version_info >= (3, 8): ast_Module = partial(ast.Module, type_ignores=[]) else: ast_Module = ast.Module return ast_Module def _check_reset_logging_tee(src_file): # Helper to deal with our tests not necessarily calling execute_script # but rather execute_code_block directly if isinstance(sys.stdout, _LoggingTee): logging_tee = sys.stdout else: logging_tee = _LoggingTee(src_file) logging_tee.set_std_and_reset_position() return logging_tee def _exec_and_get_memory(compiler, ast_Module, code_ast, gallery_conf, script_vars): """Execute ast, capturing output if last line expression and get max mem usage. Parameters ---------- compiler : codeop.Compile Compiler to compile AST of code block. ast_Module : Callable ast.Module function. code_ast : ast.Module AST parsed code to execute. gallery_conf : Dict[str, Any] Gallery configurations. script_vars : Dict[str, Any] Configuration and runtime variables. Returns ------- is_last_expr : bool Whether the last expression in `code_ast` is an ast.Expr. mem_max : float Max memory used during execution. """ src_file = script_vars["src_file"] # capture output if last line is expression is_last_expr = False call_memory, _ = _get_call_memory_and_base(gallery_conf) if len(code_ast.body) and isinstance(code_ast.body[-1], ast.Expr): is_last_expr = True last_val = code_ast.body.pop().value # exec body minus last expression mem_body, _ = call_memory( _exec_once(compiler(code_ast, src_file, "exec"), script_vars["fake_main"]) ) # exec last expression, made into assignment body = [ ast.Assign(targets=[ast.Name(id="___", ctx=ast.Store())], value=last_val) ] last_val_ast = ast_Module(body=body) ast.fix_missing_locations(last_val_ast) mem_last, _ = call_memory( _exec_once( compiler(last_val_ast, src_file, "exec"), script_vars["fake_main"] ) ) mem_max = max(mem_body, mem_last) else: mem_max, _ = call_memory( _exec_once(compiler(code_ast, src_file, "exec"), script_vars["fake_main"]) ) return is_last_expr, mem_max def _get_last_repr(capture_repr, ___): """Get repr of last expression, using first method in 'capture_repr' available.""" for meth in capture_repr: try: last_repr = getattr(___, meth)() # for case when last statement is print() if last_repr is None or last_repr == "None": repr_meth = None else: repr_meth = meth except Exception: last_repr = None repr_meth = None else: if isinstance(last_repr, str): break return last_repr, repr_meth def _get_code_output( is_last_expr, example_globals, gallery_conf, logging_tee, images_rst, file_conf ): """Obtain standard output and html output in reST. Parameters ---------- is_last_expr : bool Whether the last expression in executed code is an ast.Expr. example_globals: Dict[str, Any] Global variables for examples. logging_tee : _LoggingTee Logging tee. images_rst : str rst code to embed the images in the document. gallery_conf : Dict[str, Any] Gallery configurations. file_conf : Dict[str, Any] File-specific settings given in source file comments as: ``# sphinx_gallery_ = ``. Returns ------- code_output : str reST of output of executed code block, including images and captured output. """ last_repr = None repr_meth = None if is_last_expr: # capture the last repr variable ___ = example_globals["___"] ignore_repr = False if gallery_conf["ignore_repr_types"]: ignore_repr = re.search(gallery_conf["ignore_repr_types"], str(type(___))) capture_repr = file_conf.get("capture_repr", gallery_conf["capture_repr"]) if capture_repr != () and not ignore_repr: last_repr, repr_meth = _get_last_repr(capture_repr, ___) captured_std = logging_tee.output.getvalue().expandtabs() # normal string output if repr_meth in ["__repr__", "__str__"] and last_repr: captured_std = f"{captured_std}\n{last_repr}" if captured_std and not captured_std.isspace(): captured_std = CODE_OUTPUT.format(indent(captured_std, " " * 4)) else: captured_std = "" # Sanitize ANSI escape characters for reST output ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") captured_std = ansi_escape.sub("", captured_std) # give html output its own header if repr_meth == "_repr_html_": captured_html = HTML_HEADER.format(indent(last_repr, " " * 4)) else: captured_html = "" code_output = f"\n{images_rst}\n\n{captured_std}\n{captured_html}\n\n" return code_output def _reset_cwd_syspath(cwd, sys_path): """Reset cwd and sys.path.""" os.chdir(cwd) sys.path = sys_path def execute_code_block( compiler, block, example_globals, script_vars, gallery_conf, file_conf ): """Execute the code block of the example file. Parameters ---------- compiler : codeop.Compile Compiler to compile AST of code block. block : List[Tuple[str, str, int]] List of Tuples, each Tuple contains label ('text' or 'code'), the corresponding content string of block and the leading line number. example_globals: Dict[str, Any] Global variables for examples. script_vars : Dict[str, Any] Configuration and runtime variables. gallery_conf : Dict[str, Any] Gallery configurations. file_conf : Dict[str, Any] File-specific settings given in source file comments as: ``# sphinx_gallery_ = ``. Returns ------- code_output : str Output of executing code in reST. """ if example_globals is None: # testing shortcut example_globals = script_vars["fake_main"].__dict__ blabel, bcontent, lineno = block # If example is not suitable to run, skip executing its blocks if not script_vars["execute_script"] or blabel == "text": return "" cwd = os.getcwd() # Redirect output to stdout src_file = script_vars["src_file"] logging_tee = _check_reset_logging_tee(src_file) assert isinstance(logging_tee, _LoggingTee) # First cd in the original example dir, so that any file # created by the example get created in this directory os.chdir(os.path.dirname(src_file)) sys_path = copy.deepcopy(sys.path) sys.path.append(os.getcwd()) # Save figures unless there is a `sphinx_gallery_defer_figures` flag match = re.search( r"^[\ \t]*#\s*sphinx_gallery_defer_figures[\ \t]*\n?", bcontent, re.MULTILINE ) need_save_figures = match is None try: ast_Module = _ast_module() code_ast = ast_Module([bcontent]) flags = ast.PyCF_ONLY_AST | compiler.flags code_ast = compile(bcontent, src_file, "exec", flags, dont_inherit=1) ast.increment_lineno(code_ast, lineno - 1) is_last_expr, mem_max = _exec_and_get_memory( compiler, ast_Module, code_ast, gallery_conf, script_vars ) script_vars["memory_delta"].append(mem_max) # This should be inside the try block, e.g., in case of a savefig error logging_tee.restore_std() if need_save_figures: need_save_figures = False images_rst = save_figures(block, script_vars, gallery_conf) else: images_rst = "" except Exception: logging_tee.restore_std() except_rst = handle_exception( sys.exc_info(), src_file, script_vars, gallery_conf ) code_output = f"\n{except_rst}\n\n\n\n" # still call this even though we won't use the images so that # figures are closed if need_save_figures: save_figures(block, script_vars, gallery_conf) else: _reset_cwd_syspath(cwd, sys_path) code_output = _get_code_output( is_last_expr, example_globals, gallery_conf, logging_tee, images_rst, file_conf, ) finally: _reset_cwd_syspath(cwd, sys_path) logging_tee.restore_std() return code_output def executable_script(src_file, gallery_conf): """Validate if script has to be run according to gallery configuration. Parameters ---------- src_file : str path to python script gallery_conf : dict Contains the configuration of Sphinx-Gallery Returns ------- bool True if script has to be executed """ filename_pattern = gallery_conf["filename_pattern"] execute = re.search(filename_pattern, src_file) and gallery_conf["plot_gallery"] return execute def _check_input(prompt=None): raise ExtensionError( "Cannot use input() builtin function in Sphinx-Gallery examples" ) def execute_script(script_blocks, script_vars, gallery_conf, file_conf): """Execute and capture output from python script already in block structure. Parameters ---------- script_blocks : list (label, content, line_number) List where each element is a tuple with the label ('text' or 'code'), the corresponding content string of block and the leading line number script_vars : dict Configuration and run time variables gallery_conf : dict Contains the configuration of Sphinx-Gallery file_conf : dict File-specific settings given in source file comments as: ``# sphinx_gallery_ = `` Returns ------- output_blocks : list List of strings where each element is the restructured text representation of the output of each block time_elapsed : float Time elapsed during execution """ # Examples may contain if __name__ == '__main__' guards # for in example scikit-learn if the example uses multiprocessing. # Here we create a new __main__ module, and temporarily change # sys.modules when running our example call_memory, _ = _get_call_memory_and_base(gallery_conf) fake_main = importlib.util.module_from_spec( importlib.util.spec_from_loader("__main__", None) ) example_globals = fake_main.__dict__ example_globals.update( { # A lot of examples contains 'print(__doc__)' for example in # scikit-learn so that running the example prints some useful # information. Because the docstring has been separated from # the code blocks in sphinx-gallery, __doc__ is actually # __builtin__.__doc__ in the execution context and we do not # want to print it "__doc__": "", # Don't ever support __file__: Issues #166 #212 # Don't let them use input() "input": _check_input, } ) script_vars["example_globals"] = example_globals argv_orig = sys.argv[:] if script_vars["execute_script"]: # We want to run the example without arguments. See # https://github.com/sphinx-gallery/sphinx-gallery/pull/252 # for more details. sys.argv[0] = script_vars["src_file"] (reset_argv,) = _get_callables(gallery_conf, "reset_argv") sys.argv[1:] = reset_argv(gallery_conf, script_vars) gc.collect() memory_start, _ = call_memory(lambda: None) else: memory_start = 0.0 t_start = time() compiler = codeop.Compile() # include at least one entry to avoid max() ever failing script_vars["memory_delta"] = [memory_start] script_vars["fake_main"] = fake_main output_blocks = list() with _LoggingTee(script_vars.get("src_file", "")) as logging_tee: for block in script_blocks: logging_tee.set_std_and_reset_position() output_blocks.append( execute_code_block( compiler, block, example_globals, script_vars, gallery_conf, file_conf, ) ) time_elapsed = time() - t_start sys.argv = argv_orig script_vars["memory_delta"] = max(script_vars["memory_delta"]) if script_vars["execute_script"]: script_vars["memory_delta"] -= memory_start # Write md5 checksum if the example was meant to run (no-plot # shall not cache md5sum) and has built correctly with open(script_vars["target_file"] + ".md5", "w") as file_checksum: file_checksum.write(get_md5sum(script_vars["target_file"], "t")) gallery_conf["passing_examples"].append(script_vars["src_file"]) return output_blocks, time_elapsed def generate_file_rst(fname, target_dir, src_dir, gallery_conf, seen_backrefs=None): """Generate the rst file for a given example. Parameters ---------- fname : str Filename of python script. target_dir : str Absolute path to directory in documentation where examples are saved. src_dir : str Absolute path to directory where source examples are stored. gallery_conf : dict Contains the configuration of Sphinx-Gallery. seen_backrefs : set The seen backreferences. Returns ------- intro: str The introduction of the example. cost : tuple A tuple containing the ``(time_elapsed, memory_used)`` required to run the script. """ seen_backrefs = set() if seen_backrefs is None else seen_backrefs src_file = os.path.normpath(os.path.join(src_dir, fname)) target_file = Path(target_dir) / fname _replace_md5(src_file, target_file, "copy", mode="t") if fname.endswith(".py"): parser = py_source_parser language = "Python" else: parser = BlockParser(fname, gallery_conf) language = parser.language file_conf, script_blocks, node = parser.split_code_and_text_blocks( src_file, return_node=True ) intro, title = extract_intro_and_title(fname, script_blocks[0][1]) gallery_conf["titles"][src_file] = title executable = executable_script(src_file, gallery_conf) if md5sum_is_current(target_file, mode="t"): do_return = True if executable: if gallery_conf["run_stale_examples"]: do_return = False else: gallery_conf["stale_examples"].append(str(target_file)) if do_return: return intro, title, (0, 0) image_dir = os.path.join(target_dir, "images") if not os.path.exists(image_dir): os.makedirs(image_dir) base_image_name = os.path.splitext(fname)[0] image_fname = "sphx_glr_" + base_image_name + "_{0:03}.png" image_path_template = os.path.join(image_dir, image_fname) script_vars = { "execute_script": executable, "image_path_iterator": ImagePathIterator(image_path_template), "src_file": src_file, "target_file": str(target_file), } if executable and gallery_conf["reset_modules_order"] in ["before", "both"]: clean_modules(gallery_conf, fname, "before") output_blocks, time_elapsed = execute_script( script_blocks, script_vars, gallery_conf, file_conf ) logger.debug("%s ran in : %.2g seconds\n", src_file, time_elapsed) # Create dummy images if not executable: dummy_image = file_conf.get("dummy_images", None) if dummy_image is not None: if isinstance(dummy_image, bool) or not isinstance(dummy_image, int): raise ExtensionError( "sphinx_gallery_dummy_images setting is not an integer, " "got {dummy_image!r}" ) image_path_iterator = script_vars["image_path_iterator"] stock_img = os.path.join(glr_path_static(), "no_image.png") for _, path in zip(range(dummy_image), image_path_iterator): if not os.path.isfile(path): copyfile(stock_img, path) # Ignore blocks must be processed before the # remaining config comments are removed. script_blocks = [ (label, parser.remove_ignore_blocks(content), line_number) for label, content, line_number in script_blocks ] if gallery_conf["remove_config_comments"]: script_blocks = [ (label, parser.remove_config_comments(content), line_number) for label, content, line_number in script_blocks ] # Remove final empty block, which can occur after config comments # are removed if script_blocks[-1][1].isspace(): script_blocks = script_blocks[:-1] output_blocks = output_blocks[:-1] example_rst = rst_blocks( script_blocks, output_blocks, file_conf, gallery_conf, language=language ) _, memory_base = _get_call_memory_and_base(gallery_conf) memory_used = memory_base + script_vars["memory_delta"] if not executable: time_elapsed = memory_used = 0.0 # don't let the output change save_rst_example( example_rst, target_file, time_elapsed, memory_used, gallery_conf, language=language, ) save_thumbnail(image_path_template, src_file, script_vars, file_conf, gallery_conf) if target_file.suffix in gallery_conf["notebook_extensions"]: example_nb = jupyter_notebook(script_blocks, gallery_conf, target_dir) ipy_fname = target_file.with_suffix(".ipynb.new") save_notebook(example_nb, ipy_fname) _replace_md5(ipy_fname, mode="t") # Write names if gallery_conf["inspect_global_variables"]: global_variables = script_vars["example_globals"] else: global_variables = None ref_regex = _make_ref_regex(gallery_conf["default_role"]) example_code_obj = identify_names(script_blocks, ref_regex, global_variables, node) if example_code_obj: codeobj_fname = target_file.with_name(target_file.stem + "_codeobj.pickle.new") with open(codeobj_fname, "wb") as fid: pickle.dump(example_code_obj, fid, pickle.HIGHEST_PROTOCOL) _replace_md5(codeobj_fname) exclude_regex = gallery_conf["exclude_implicit_doc_regex"] backrefs = { "{module_short}.{name}".format(**cobj) for cobjs in example_code_obj.values() for cobj in cobjs if cobj["module"].startswith(gallery_conf["doc_module"]) and ( cobj["is_explicit"] or (not exclude_regex) or (not exclude_regex.search("{module}.{name}".format(**cobj))) ) } # Write backreferences _write_backreferences( backrefs, seen_backrefs, gallery_conf, target_dir, fname, intro, title ) # This can help with garbage collection in some instances if global_variables is not None and "___" in global_variables: del global_variables["___"] del script_vars, global_variables # don't keep these during reset if executable and gallery_conf["reset_modules_order"] in ["after", "both"]: clean_modules(gallery_conf, fname, "after") return intro, title, (time_elapsed, memory_used) EXAMPLE_HEADER = """ .. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "{0}" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code.{2} .. rst-class:: sphx-glr-example-title .. _sphx_glr_{1}: """ RST_BLOCK_HEADER = """\ .. GENERATED FROM PYTHON SOURCE LINES {0}-{1} """ def rst_blocks( script_blocks, output_blocks, file_conf, gallery_conf, *, language="python" ): """Generate the rst string containing the script prose, code and output. Parameters ---------- script_blocks : list (label, content, line_number) List where each element is a tuple with the label ('text' or 'code'), the corresponding content string of block and the leading line number output_blocks : list List of strings where each element is the restructured text representation of the output of each block file_conf : dict File-specific settings given in source file comments as: ``# sphinx_gallery_ = `` language : str The language to be used for syntax highlighting in code blocks. Must be a name or alias recognized by Pygments. gallery_conf : dict Contains the configuration of Sphinx-Gallery Returns ------- out : str rst notebook """ # A simple example has two blocks: one for the # example introduction/explanation and one for the code is_example_notebook_like = len(script_blocks) > 2 example_rst = "" for bi, ((blabel, bcontent, lineno), code_output) in enumerate( zip(script_blocks, output_blocks) ): # do not add comment to the title block, otherwise the linking does # not work properly if bi > 0: example_rst += RST_BLOCK_HEADER.format( lineno, lineno + bcontent.count("\n") ) if blabel == "code": if not file_conf.get("line_numbers", gallery_conf["line_numbers"]): lineno = None code_rst = codestr2rst(bcontent, lang=language, lineno=lineno) + "\n" if is_example_notebook_like: example_rst += code_rst example_rst += code_output else: example_rst += code_output if "sphx-glr-script-out" in code_output: # Add some vertical space after output example_rst += "\n\n|\n\n" example_rst += code_rst else: block_separator = "\n\n" if not bcontent.endswith("\n") else "\n" example_rst += bcontent + block_separator return example_rst def save_rst_example( example_rst, example_file, time_elapsed, memory_used, gallery_conf, *, language="python", ): """Saves the rst notebook to example_file including header & footer. Parameters ---------- example_rst : str rst containing the executed file content example_file : str Filename with full path of python example file in documentation folder language : str Name of the programming language the example is in time_elapsed : float Time elapsed in seconds while executing file memory_used : float Additional memory used during the run. gallery_conf : dict Sphinx-Gallery configuration dictionary """ example_file = Path(example_file) example_fname = str(example_file.relative_to(gallery_conf["src_dir"])) ref_fname = example_fname.replace(os.path.sep, "_") binder_conf = gallery_conf["binder"] is_binder_enabled = len(binder_conf) > 0 jupyterlite_conf = gallery_conf["jupyterlite"] is_jupyterlite_enabled = jupyterlite_conf is not None interactive_example_text = "" if is_binder_enabled or is_jupyterlite_enabled: interactive_example_text += " or to run this example in your browser via " if is_binder_enabled and is_jupyterlite_enabled: interactive_example_text += "JupyterLite or Binder" elif is_binder_enabled: interactive_example_text += "Binder" elif is_jupyterlite_enabled: interactive_example_text += "JupyterLite" example_rst = ( EXAMPLE_HEADER.format(example_fname, ref_fname, interactive_example_text) + example_rst ) if time_elapsed > gallery_conf["min_reported_time"]: time_m, time_s = divmod(time_elapsed, 60) example_rst += TIMING_CONTENT.format(time_m, time_s) if gallery_conf["show_memory"]: example_rst += f"**Estimated memory usage:** {memory_used: .0f} MB\n\n" example_rst += DOWNLOAD_LINKS_HEADER.format(ref_fname) save_notebook = example_file.suffix in gallery_conf["notebook_extensions"] # Generate a binder URL if specified if is_binder_enabled and save_notebook: binder_badge_rst = gen_binder_rst(example_file, binder_conf, gallery_conf) binder_badge_rst = indent(binder_badge_rst, " ") # need an extra two example_rst += binder_badge_rst if is_jupyterlite_enabled and save_notebook: jupyterlite_rst = gen_jupyterlite_rst(example_file, gallery_conf) jupyterlite_rst = indent(jupyterlite_rst, " ") # need an extra two example_rst += jupyterlite_rst if save_notebook: example_rst += NOTEBOOK_DOWNLOAD.format(example_file.with_suffix(".ipynb").name) example_rst += CODE_DOWNLOAD.format(example_file.name, language) if gallery_conf["recommender"]["enable"]: # extract the filename without the extension recommend_fname = Path(example_fname).stem example_rst += RECOMMENDATIONS_INCLUDE.format(recommend_fname) if gallery_conf["show_signature"]: example_rst += SPHX_GLR_SIG write_file_new = example_file.with_suffix(".rst.new") with codecs.open(write_file_new, "w", encoding="utf-8") as f: f.write(example_rst) # make it read-only so that people don't try to edit it mode = os.stat(write_file_new).st_mode ro_mask = 0x777 ^ (stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH) os.chmod(write_file_new, mode & ro_mask) # in case it wasn't in our pattern, only replace the file if it's # still stale. _replace_md5(write_file_new, mode="t") def _get_class(gallery_conf, key): """Get a class for the given conf key.""" what = f"sphinx_gallery_conf[{repr(key)}]" val = gallery_conf[key] if key == "within_subsection_order": alias = ( # convenience aliases "ExampleTitleSortKey", "FileNameSortKey", "FileSizeSortKey", "NumberOfCodeLinesSortKey", ) if val in alias: val = f"sphinx_gallery.sorting.{val}" if isinstance(val, str): if "." not in val: raise ConfigError( f"{what} must be a fully qualified name string, got {val}" ) mod, attr = val.rsplit(".", 1) try: val = getattr(importlib.import_module(mod), attr) except Exception: raise ConfigError( f"{what} must be a fully qualified name string, could not import " f"{attr} from {mod}" ) if not inspect.isclass(val): raise ConfigError( f"{what} must be 1) a fully qualified name (string) that resolves " f"to a class or 2) or a class itself, got {val.__class__.__name__} " f"({repr(val)})" ) return val def _get_callables(gallery_conf, key): """Get callables for the given conf key, returning tuple of callable(s).""" singletons = ("reset_argv", "minigallery_sort_order", "subsection_order") # the following should be the case (internal use only): assert key in ("image_scrapers", "reset_modules", "jupyterlite") + singletons, key which = gallery_conf[key] if key == "jupyterlite": which = [which["notebook_modification_function"]] elif key in singletons: which = [which] if not isinstance(which, (tuple, list)): which = [which] which = list(which) for wi, what in enumerate(which): if key == "jupyterlite": readable = f"{key}['notebook_modification_function']" elif key in singletons: readable = f"{key}={repr(what)}" else: readable = f"{key}[{wi}]={repr(what)}" if isinstance(what, str): if "." in what: mod, attr = what.rsplit(".", 1) try: what = getattr(importlib.import_module(mod), attr) except Exception: raise ConfigError( f"Unknown string option {readable} " f"when importing {attr} from {mod}" ) elif key == "image_scrapers": if what in _scraper_dict: what = _scraper_dict[what] else: try: what = importlib.import_module(what) what = getattr(what, "_get_sg_image_scraper") what = what() except Exception: raise ConfigError(f"Unknown string option for {readable}") elif key == "reset_modules": if what not in _reset_dict: raise ConfigError(f"Unknown string option for {readable}: {what}") what = _reset_dict[what] which[wi] = what if inspect.isclass(what): raise ConfigError( f"Got class rather than callable instance for {readable}: {what}" ) if not callable(what): raise ConfigError(f"{readable} must be callable") return tuple(which) # Default no-op version def _sg_call_memory_noop(func): return 0.0, func() def _get_call_memory_and_base(gallery_conf): show_memory = gallery_conf["show_memory"] # Default to no-op version call_memory = _sg_call_memory_noop memory_base = 0.0 if show_memory: if callable(show_memory): call_memory = show_memory elif gallery_conf["plot_gallery"]: # True-like out = _get_memprof_call_memory() if out is not None: call_memory, memory_base = out else: gallery_conf["show_memory"] = False assert callable(call_memory) return call_memory, memory_base def _sg_call_memory_memprof(func): from memory_profiler import memory_usage # noqa mem, out = memory_usage(func, max_usage=True, retval=True, multiprocess=True) try: mem = mem[0] # old MP always returned a list except TypeError: # 'float' object is not subscriptable pass return mem, out @lru_cache() def _get_memprof_call_memory(): try: from memory_profiler import memory_usage # noqa except ImportError: logger.warning( "Please install 'memory_profiler' to enable peak memory measurements." ) return None else: return _sg_call_memory_memprof, _get_memory_base() sphinx-gallery-0.16.0/sphinx_gallery/interactive_example.py000066400000000000000000000376131461331107500242110ustar00rootroot00000000000000# Author: Chris Holdgraf # License: 3-clause BSD """Binder and Jupyterlite utility functions. Integration with Binder and Jupyterlite is on an experimental stage. Note that this API may change in the future. .. warning:: Binder is still beta technology, so there may be instability in the experience of users who click Binder links. """ import os from pathlib import Path import shutil from urllib.parse import quote import glob import json from sphinx.errors import ConfigError import sphinx.util from .utils import status_iterator from . import glr_path_static logger = sphinx.util.logging.getLogger("sphinx-gallery") def gen_binder_url(fpath, binder_conf, gallery_conf): """Generate a Binder URL according to the configuration in conf.py. Parameters ---------- fpath: str The path to the `.py` file for which a Binder badge will be generated. binder_conf: dict or None The Binder configuration dictionary. See `gen_binder_rst` for details. Returns ------- binder_url : str A URL that can be used to direct the user to the live Binder environment. """ # Build the URL fpath_prefix = binder_conf["filepath_prefix"] link_base = binder_conf["notebooks_dir"] # We want to keep the relative path to sub-folders relative_link = os.path.relpath(fpath, gallery_conf["src_dir"]) path_link = os.path.join(link_base, Path(relative_link).with_suffix(".ipynb")) # In case our website is hosted in a sub-folder if fpath_prefix is not None: path_link = "/".join([fpath_prefix.strip("/"), path_link]) # Make sure we have the right slashes (in case we're on Windows) path_link = path_link.replace(os.path.sep, "/") # Create the URL binder_url = "/".join( [ binder_conf["binderhub_url"], "v2", "gh", binder_conf["org"], binder_conf["repo"], quote(binder_conf["branch"]), ] ) if binder_conf["use_jupyter_lab"] is True: binder_url += f"?urlpath=lab/tree/{quote(path_link)}" else: binder_url += f"?filepath={quote(path_link)}" return binder_url def gen_binder_rst(fpath, binder_conf, gallery_conf): """Generate the reST + link for the Binder badge. Parameters ---------- fpath: str The path to the `.py` file for which a Binder badge will be generated. binder_conf: dict or None If a dictionary it must have the following keys: 'binderhub_url' The URL of the BinderHub instance that's running a Binder service. 'org' The GitHub organization to which the documentation will be pushed. 'repo' The GitHub repository to which the documentation will be pushed. 'branch' The Git branch on which the documentation exists (e.g., gh-pages). 'dependencies' A list of paths to dependency files that match the Binderspec. gallery_conf : dict Sphinx-Gallery configuration dictionary. Returns ------- rst : str The reStructuredText for the Binder badge that links to this file. """ binder_url = gen_binder_url(fpath, binder_conf, gallery_conf) # In theory we should be able to use glr_path_static for this, but Sphinx # only allows paths to be relative to the build root. On Linux, absolute # paths can be used and they work, but this does not seem to be # documented behavior: # https://github.com/sphinx-doc/sphinx/issues/7772 # And in any case, it does not work on Windows, so here we copy the SVG to # `images` for each gallery and link to it there. This will make # a few copies, and there will be an extra in `_static` at the end of the # build, but it at least works... physical_path = os.path.join( os.path.dirname(fpath), "images", "binder_badge_logo.svg" ) os.makedirs(os.path.dirname(physical_path), exist_ok=True) if not os.path.isfile(physical_path): shutil.copyfile( os.path.join(glr_path_static(), "binder_badge_logo.svg"), physical_path ) rst = ( "\n" " .. container:: binder-badge\n\n" " .. image:: images/binder_badge_logo.svg\n" " :target: {}\n" " :alt: Launch binder\n" " :width: 150 px\n" ).format(binder_url) return rst def copy_binder_files(app, exception): """Copy all Binder requirements and notebooks files.""" if exception is not None: return if app.builder.name not in ["html", "readthedocs"]: return gallery_conf = app.config.sphinx_gallery_conf binder_conf = gallery_conf["binder"] if not len(binder_conf) > 0: return logger.info("copying binder requirements...", color="white") _copy_binder_reqs(app, binder_conf) _copy_binder_notebooks(app) def _copy_binder_reqs(app, binder_conf): """Copy Binder requirements files to a "binder" folder in the docs.""" path_reqs = binder_conf["dependencies"] for path in path_reqs: if not os.path.exists(os.path.join(app.srcdir, path)): raise ConfigError( "Couldn't find the Binder requirements file: {}, " "did you specify the path correctly?".format(path) ) binder_folder = os.path.join(app.outdir, "binder") if not os.path.isdir(binder_folder): os.makedirs(binder_folder) # Copy over the requirements to the output directory for path in path_reqs: shutil.copy(os.path.join(app.srcdir, path), binder_folder) def _remove_ipynb_files(path, contents): """Remove files with `.ipynb` and directories named `images` from list of files. `contents` should be a list of files, which is returned after file removal. Used with the `shutil` "ignore" keyword to filter out non-ipynb files. """ contents_return = [] for entry in contents: if entry.endswith(".ipynb"): # Don't include ipynb files pass elif (entry != "images") and os.path.isdir(os.path.join(path, entry)): # Don't include folders not called "images" pass else: # Keep everything else contents_return.append(entry) return contents_return def _copy_binder_notebooks(app): """Copy Jupyter notebooks to the binder notebooks directory. Copy each output gallery directory structure but only including the Jupyter notebook files. """ gallery_conf = app.config.sphinx_gallery_conf gallery_dirs = gallery_conf["gallery_dirs"] binder_conf = gallery_conf["binder"] notebooks_dir = os.path.join(app.outdir, binder_conf["notebooks_dir"]) shutil.rmtree(notebooks_dir, ignore_errors=True) os.makedirs(notebooks_dir) if not isinstance(gallery_dirs, (list, tuple)): gallery_dirs = [gallery_dirs] iterator = status_iterator( gallery_dirs, "copying binder notebooks...", length=len(gallery_dirs) ) for i_folder in iterator: shutil.copytree( os.path.join(app.srcdir, i_folder), os.path.join(notebooks_dir, i_folder), ignore=_remove_ipynb_files, ) def check_binder_conf(binder_conf): """Check to make sure that the Binder configuration is correct.""" # Grab the configuration and return None if it's not configured binder_conf = {} if binder_conf is None else binder_conf.copy() if not isinstance(binder_conf, dict): raise ConfigError("`binder_conf` must be a dictionary or None.") if len(binder_conf) == 0: return binder_conf # Ensure all fields are populated req_values = ["binderhub_url", "org", "repo", "branch", "dependencies"] optional_values = ["filepath_prefix", "notebooks_dir", "use_jupyter_lab"] missing_values = [] for val in req_values: if binder_conf.get(val) is None: missing_values.append(val) if len(missing_values) > 0: raise ConfigError(f"binder_conf is missing values for: {missing_values}") for key in binder_conf.keys(): if key not in (req_values + optional_values): raise ConfigError(f"Unknown Binder config key: {key}") # Ensure we have http in the URL if not any( binder_conf["binderhub_url"].startswith(ii) for ii in ["http://", "https://"] ): raise ConfigError( "did not supply a valid url, " "gave binderhub_url: {}".format( binder_conf["binderhub_url"] ) ) # Ensure we have at least one dependency file # Need at least one of these three files required_reqs_files = ["requirements.txt", "environment.yml", "Dockerfile"] path_reqs = binder_conf["dependencies"] if isinstance(path_reqs, str): path_reqs = [path_reqs] binder_conf["dependencies"] = path_reqs elif not isinstance(path_reqs, (list, tuple)): raise ConfigError( "`dependencies` value should be a list of strings. " "Got type {}.".format( type(path_reqs) ) ) binder_conf.setdefault("filepath_prefix") binder_conf.setdefault("notebooks_dir", "notebooks") binder_conf.setdefault("use_jupyter_lab", False) path_reqs_filenames = [os.path.basename(ii) for ii in path_reqs] if not any(ii in path_reqs_filenames for ii in required_reqs_files): raise ConfigError( "Did not find one of `requirements.txt` or `environment.yml` " 'in the "dependencies" section of the binder configuration ' "for Sphinx-Gallery. A path to at least one of these files " "must exist in your Binder dependencies." ) return binder_conf def pre_configure_jupyterlite_sphinx(app, config): """Configure 'jupyterlite_bind_ipynb_suffix' if jupyterlite enabled. Connected to "config-inited" event. """ is_jupyterlite_enabled = ( "jupyterlite_sphinx" in app.extensions and config.sphinx_gallery_conf["jupyterlite"] is not None ) if not is_jupyterlite_enabled: return # Do not use notebooks as sources for the documentation. See # https://jupyterlite-sphinx.readthedocs.io/en/latest/configuration.html#disable-the-ipynb-docs-source-binding # for more details config.jupyterlite_bind_ipynb_suffix = False def post_configure_jupyterlite_sphinx(app, config): """Check SG "jupyterlite" and update Jupyterlite config "jupyterlite_contents". Connected to "config-inited" event but lower priority so called after `pre_configure_jupyterlite_sphinx`. """ config.sphinx_gallery_conf["jupyterlite"] = check_jupyterlite_conf( config.sphinx_gallery_conf["jupyterlite"], app ) if config.sphinx_gallery_conf["jupyterlite"] is None: return if config.jupyterlite_contents is None: config.jupyterlite_contents = [] # Append to jupyterlite_contents in case they have been set in conf.py config.jupyterlite_contents.append( config.sphinx_gallery_conf["jupyterlite"]["jupyterlite_contents"] ) def create_jupyterlite_contents(app, exception): """Create Jupyterlite contents according to "jupyterlite" configuration.""" from .gen_rst import _get_callables if exception is not None: return if app.builder.name not in ["html", "readthedocs"]: return gallery_conf = app.config.sphinx_gallery_conf is_jupyterlite_enabled = ( "jupyterlite_sphinx" in app.extensions and gallery_conf["jupyterlite"] is not None ) if not is_jupyterlite_enabled: return logger.info("copying Jupyterlite contents ...", color="white") gallery_dirs = gallery_conf["gallery_dirs"] contents_dir = gallery_conf["jupyterlite"]["jupyterlite_contents"] shutil.rmtree(contents_dir, ignore_errors=True) os.makedirs(contents_dir) if not isinstance(gallery_dirs, (list, tuple)): gallery_dirs = [gallery_dirs] iterator = status_iterator( gallery_dirs, "Copying Jupyterlite contents ...", length=len(gallery_dirs) ) for i_folder in iterator: shutil.copytree( os.path.join(app.srcdir, i_folder), os.path.join(contents_dir, i_folder), ignore=_remove_ipynb_files, ) notebook_modification_function = gallery_conf["jupyterlite"][ "notebook_modification_function" ] if notebook_modification_function is None: return (notebook_modification_function,) = _get_callables(gallery_conf, "jupyterlite") notebook_pattern = os.path.join(contents_dir, "**", "*.ipynb") notebook_filename_list = sorted(glob.glob(notebook_pattern, recursive=True)) logger.info("Modifying Jupyterlite notebooks ...", color="white") for notebook_filename in notebook_filename_list: with open(notebook_filename) as f: notebook_content = json.load(f) notebook_modification_function(notebook_content, notebook_filename) with open(notebook_filename, "w") as f: json.dump(notebook_content, f, indent=2) def gen_jupyterlite_rst(fpath, gallery_conf): """Generate the reST + link for the Binder badge. Parameters ---------- fpath: str The path to the `.py` file for which a JupyterLite badge will be generated. gallery_conf : dict Sphinx-Gallery configuration dictionary. Returns ------- rst : str The reStructuredText for the JupyterLite badge that links to this file. """ relative_link = os.path.relpath(fpath, gallery_conf["src_dir"]) notebook_location = relative_link.replace(".py", ".ipynb") # Make sure we have the right slashes (in case we're on Windows) notebook_location = notebook_location.replace(os.path.sep, "/") lite_root_url = os.path.relpath( os.path.join(gallery_conf["src_dir"], "lite"), os.path.dirname(fpath) ) # Make sure we have the right slashes (in case we're on Windows) lite_root_url = lite_root_url.replace(os.path.sep, "/") if gallery_conf["jupyterlite"]["use_jupyter_lab"]: lite_root_url += "/lab" else: lite_root_url += "/retro/notebooks" lite_url = f"{lite_root_url}/?path={notebook_location}" # Similar work-around for badge file as in # gen_binder_rst physical_path = os.path.join( os.path.dirname(fpath), "images", "jupyterlite_badge_logo.svg" ) os.makedirs(os.path.dirname(physical_path), exist_ok=True) if not os.path.isfile(physical_path): shutil.copyfile( os.path.join(glr_path_static(), "jupyterlite_badge_logo.svg"), physical_path ) rst = ( "\n" " .. container:: lite-badge\n\n" " .. image:: images/jupyterlite_badge_logo.svg\n" " :target: {}\n" " :alt: Launch JupyterLite\n" " :width: 150 px\n" ).format(lite_url) return rst def check_jupyterlite_conf(jupyterlite_conf, app): """Return full JupyterLite configuration with defaults.""" # app=None can happen for testing if app is None: is_jupyterlite_disabled = True else: is_jupyterlite_disabled = ( "jupyterlite_sphinx" not in app.extensions or jupyterlite_conf is None ) if is_jupyterlite_disabled: return None if not isinstance(jupyterlite_conf, dict): raise ConfigError("`jupyterlite_conf` must be a dictionary") conf_defaults = { "jupyterlite_contents": "jupyterlite_contents", "notebook_modification_function": None, "use_jupyter_lab": True, } result = jupyterlite_conf.copy() unknown_keys = set(result) - set(conf_defaults) if unknown_keys: raise ConfigError( f"Found some unknown keys in sphinx_gallery_conf['jupyterlite']: " f"{sorted(unknown_keys)}. " f"Allowed keys are: {list(conf_defaults)}" ) for key, default_value in conf_defaults.items(): result.setdefault(key, default_value) result["jupyterlite_contents"] = os.path.join( app.srcdir, result["jupyterlite_contents"] ) return result sphinx-gallery-0.16.0/sphinx_gallery/load_style.py000066400000000000000000000013601461331107500223060ustar00rootroot00000000000000"""Only load CSS and modify html_static_path. This should not be used at the same time as sphinx_gallery.gen_gallery. """ from . import __version__, glr_path_static from .directives import ImageSg, imagesg_addnode def config_inited(app, config): """Append path to packaged static files to `html_static_path`.""" path = glr_path_static() if path not in config.html_static_path: config.html_static_path.append(path) app.add_css_file("sg_gallery.css") def setup(app): """Sphinx setup.""" app.require_sphinx("1.8") app.connect("config-inited", config_inited) app.add_directive("image-sg", ImageSg) imagesg_addnode(app) return { "parallel_read_safe": True, "version": __version__, } sphinx-gallery-0.16.0/sphinx_gallery/notebook.py000066400000000000000000000315541461331107500217770ustar00rootroot00000000000000r"""Parser for Jupyter notebooks. Class that holds the Jupyter notebook information """ # Author: Óscar Nájera # License: 3-clause BSD from collections import defaultdict from functools import partial from itertools import count import argparse import base64 import copy import json import mimetypes import os from pathlib import Path import re import sys import textwrap from sphinx.errors import ExtensionError import sphinx.util from .py_source_parser import split_code_and_text_blocks logger = sphinx.util.logging.getLogger("sphinx-gallery") def jupyter_notebook_skeleton(): """Returns a dictionary with the elements of a Jupyter notebook.""" py_version = sys.version_info notebook_skeleton = { "cells": [], "metadata": { "kernelspec": { "display_name": "Python " + str(py_version[0]), "language": "python", "name": "python" + str(py_version[0]), }, "language_info": { "codemirror_mode": {"name": "ipython", "version": py_version[0]}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython" + str(py_version[0]), "version": "{}.{}.{}".format(*sys.version_info[:3]), }, }, "nbformat": 4, "nbformat_minor": 0, } return notebook_skeleton def directive_fun(match, directive): """Helper to fill in directives.""" directive_to_alert = dict(note="info", warning="danger") return '

{}

{}

'.format( directive_to_alert[directive], directive.capitalize(), match.group(1).strip() ) def convert_code_to_md(text): """Rewrite code blocks to use Markdown's preferred backtick notation. Backtick notation preserves syntax highlighting. Parameters ---------- text: str A mostly converted string of markdown text. May contain zero, one, or multiple code blocks in code-block format. """ code_regex = r"[ \t]*\.\. code-block::[ \t]*(\S*)\n[ \t]*\n([ \t]+)" while True: code_block = re.search(code_regex, text) if not code_block: break indent = code_block.group(2) start_index = code_block.span()[1] - len(indent) # Find first non-empty, non-indented line end = re.compile(rf"^(?!{re.escape(indent)})[ \t]*\S+", re.MULTILINE) code_end_match = end.search(text, start_index) end_index = code_end_match.start() if code_end_match else len(text) contents = textwrap.dedent(text[start_index:end_index]).rstrip() new_code = f"```{code_block.group(1)}\n{contents}\n```\n" text = text[: code_block.span()[0]] + new_code + text[end_index:] return text def rst2md(text, gallery_conf, target_dir, heading_levels): """Converts reST from docstrings and text blocks to markdown for Jupyter notebooks. Parameters ---------- text: str reST input to be converted to MD gallery_conf : dict The sphinx-gallery configuration dictionary. target_dir : str Path that notebook is intended for. Used where relative paths may be required. heading_levels: dict Mapping of heading style ``(over_char, under_char)`` to heading level. Note that ``over_char`` is `None` when only underline is present. """ # Characters recommended for use with headings # https://docutils.readthedocs.io/en/sphinx-docs/user/rst/quickstart.html#sections adornment_characters = "=`:.'\"~^_*+#<>-" headings = re.compile( # Start of string or blank line r"(?P
\A|^[ \t]*\n)"
        # Optional over characters, allowing leading space on heading text
        r"(?:(?P[{0}])(?P=over)*\n[ \t]*)?"
        # The heading itself, with at least one non-white space character
        r"(?P\S[^\n]*)\n"
        # Under character, setting to same character if over present.
        r"(?P(?(over)(?P=over)|[{0}]))(?P=under)*$"
        r"".format(adornment_characters),
        flags=re.M,
    )

    text = re.sub(
        headings,
        lambda match: "{1}{0} {2}".format(
            "#" * heading_levels[match.group("over", "under")],
            *match.group("pre", "heading"),
        ),
        text,
    )

    math_eq = re.compile(r"^\.\. math::((?:.+)?(?:\n+^  .+)*)", flags=re.M)
    text = re.sub(
        math_eq,
        lambda match: r"\begin{{align}}{0}\end{{align}}".format(match.group(1).strip()),
        text,
    )
    inline_math = re.compile(r":math:`(.+?)`", re.DOTALL)
    text = re.sub(inline_math, r"$\1$", text)

    directives = ("warning", "note")
    for directive in directives:
        directive_re = re.compile(
            r"^\.\. %s::((?:.+)?(?:\n+^  .+)*)" % directive, flags=re.M
        )
        text = re.sub(directive_re, partial(directive_fun, directive=directive), text)

    footnote_links = re.compile(r"^ *\.\. _.*:.*$\n", flags=re.M)
    text = re.sub(footnote_links, "", text)

    embedded_uris = re.compile(r"`([^`]*?)\s*<([^`]*)>`_")
    text = re.sub(embedded_uris, r"[\1](\2)", text)

    refs = re.compile(r":ref:`")
    text = re.sub(refs, "`", text)

    contents = re.compile(r"^\s*\.\. contents::.*$(\n +:\S+: *$)*\n", flags=re.M)
    text = re.sub(contents, "", text)

    images = re.compile(r"^\.\. image::(.*$)((?:\n +:\S+:.*$)*)\n", flags=re.M)
    image_opts = re.compile(r"\n +:(\S+): +(.*)$", flags=re.M)
    text = re.sub(
        images,
        lambda match: '\n'.format(
            generate_image_src(match.group(1).strip(), gallery_conf, target_dir),
            re.sub(image_opts, r' \1="\2"', match.group(2) or ""),
        ),
        text,
    )

    text = convert_code_to_md(text)

    return text


def generate_image_src(image_path, gallery_conf, target_dir):
    """Modify image path for notebook, according to "notebook_images" config.

    URLs are unchanged.
    If "notebook_images" config is a str, it is used as a prefix to image path, relative
    to "src_dir". If "notebook_images" is `True`, image is embedded as URI. If
    "notebook_images" is `False`, "file://" is prepended.
    """
    if re.match(r"https?://", image_path):
        return image_path

    if not gallery_conf["notebook_images"]:
        return "file://" + image_path.lstrip("/")

    # If absolute path from source directory given
    if image_path.startswith("/"):
        # Path should now be relative to source dir, not target dir
        target_dir = gallery_conf["src_dir"]
        image_path = image_path.lstrip("/")
    full_path = os.path.join(target_dir, image_path.replace("/", os.sep))

    if isinstance(gallery_conf["notebook_images"], str):
        # Use as prefix e.g. URL
        prefix = gallery_conf["notebook_images"]
        rel_path = os.path.relpath(full_path, gallery_conf["src_dir"])
        return prefix + rel_path.replace(os.sep, "/")
    else:
        # True, but not string. Embed as data URI.
        try:
            with open(full_path, "rb") as image_file:
                data = base64.b64encode(image_file.read())
        except OSError:
            raise ExtensionError(
                f"Unable to open {full_path} to generate notebook data URI"
            )
        mime_type = mimetypes.guess_type(full_path)
        return f"data:{mime_type[0]};base64,{data.decode('ascii')}"


def jupyter_notebook(script_blocks, gallery_conf, target_dir):
    """Generate a Jupyter notebook file cell-by-cell.

    Parameters
    ----------
    script_blocks : list
        Script execution cells.
    gallery_conf : dict
        The sphinx-gallery configuration dictionary.
    target_dir : str
        Path that notebook is intended for. Used where relative paths
        may be required.
    """
    first_cell = gallery_conf["first_notebook_cell"]
    last_cell = gallery_conf["last_notebook_cell"]
    work_notebook = jupyter_notebook_skeleton()
    if first_cell is not None:
        add_code_cell(work_notebook, first_cell)
    fill_notebook(work_notebook, script_blocks, gallery_conf, target_dir)
    if last_cell is not None:
        add_code_cell(work_notebook, last_cell)

    return work_notebook


def add_code_cell(work_notebook, code):
    """Add a code cell to the notebook.

    Parameters
    ----------
    code : str
        Cell content
    """
    code_cell = {
        "cell_type": "code",
        "execution_count": None,
        "metadata": {"collapsed": False},
        "outputs": [],
        "source": [code.strip()],
    }
    work_notebook["cells"].append(code_cell)


def add_markdown_cell(work_notebook, markdown):
    """Add a markdown cell to the notebook.

    Parameters
    ----------
    markdown : str
        Markdown cell content.
    """
    markdown_cell = {"cell_type": "markdown", "metadata": {}, "source": [markdown]}
    work_notebook["cells"].append(markdown_cell)


def promote_jupyter_cell_magic(work_notebook, markdown):
    """Promote Jupyter cell magic in text blocks to code block in notebooks.

    Parses a block of markdown text looking for code blocks starting with a
    Jupyter cell magic (e.g. %%bash). Whenever one is found, the text before it
    and the code (as a runnable code block) are added to work_notebook. Any
    remaining text is returned.

    Parameters
    ----------
    markdown : str
        Markdown cell content.
    """
    # Regex detects all code blocks that use %% Jupyter cell magic
    cell_magic_regex = r"\n?```\s*[a-z]*\n(%%(?:[\s\S]*?))\n?```\n?"

    text_cell_start = 0
    for magic_cell in re.finditer(cell_magic_regex, markdown):
        # Extract the preceding text block, and add it if non-empty
        text_block = markdown[text_cell_start : magic_cell.span()[0]]
        if text_block and not text_block.isspace():
            add_markdown_cell(work_notebook, text_block)
        text_cell_start = magic_cell.span()[1]

        code_block = magic_cell.group(1)
        add_code_cell(work_notebook, code_block)

    # Return remaining text (which equals markdown if no magic cells exist)
    return markdown[text_cell_start:]


def fill_notebook(work_notebook, script_blocks, gallery_conf, target_dir):
    """Writes the Jupyter notebook cells.

    If available, uses pypandoc to convert rst to markdown.

    Parameters
    ----------
    script_blocks : list
        Each list element should be a tuple of (label, content, lineno).
    """
    heading_level_counter = count(start=1)
    heading_levels = defaultdict(lambda: next(heading_level_counter))
    for blabel, bcontent, lineno in script_blocks:
        if blabel == "code":
            add_code_cell(work_notebook, bcontent)
        else:
            if gallery_conf["pypandoc"] is False:
                markdown = rst2md(
                    bcontent + "\n", gallery_conf, target_dir, heading_levels
                )
            else:
                import pypandoc

                # pandoc automatically adds \n to the end
                markdown = pypandoc.convert_text(
                    bcontent, to="md", format="rst", **gallery_conf["pypandoc"]
                )

            if gallery_conf["promote_jupyter_magic"]:
                remaining = promote_jupyter_cell_magic(work_notebook, markdown)
                if remaining and not remaining.isspace():
                    add_markdown_cell(work_notebook, remaining)
            else:
                add_markdown_cell(work_notebook, markdown)


def save_notebook(work_notebook, write_file):
    """Saves the Jupyter work_notebook to write_file."""
    with open(write_file, "w") as out_nb:
        json.dump(work_notebook, out_nb, indent=2)


###############################################################################
# Notebook shell utility


def python_to_jupyter_cli(args=None, namespace=None, sphinx_gallery_conf=None):
    """Exposes the jupyter notebook renderer to the command line.

    Takes the same arguments as ArgumentParser.parse_args.
    `sphinx_gallery_conf` functions same as in `conf.py`.
    """
    from . import gen_gallery  # To avoid circular import

    parser = argparse.ArgumentParser(description="Sphinx-Gallery Notebook converter")
    parser.add_argument(
        "python_src_file",
        nargs="+",
        help="Input Python file script to convert. "
        "Supports multiple files and shell wildcards"
        " (e.g. *.py)",
    )
    args = parser.parse_args(args, namespace)

    # handle `sphinx_gallery_conf`
    gallery_conf = copy.deepcopy(gen_gallery.DEFAULT_GALLERY_CONF)
    if sphinx_gallery_conf is not None:
        gallery_conf.update(sphinx_gallery_conf)

    # run script
    for src_file in args.python_src_file:
        file_conf, blocks = split_code_and_text_blocks(src_file)
        print(f"Converting {src_file}")
        target_dir = os.path.dirname(src_file)
        example_nb = jupyter_notebook(blocks, copy.deepcopy(gallery_conf), target_dir)
        save_notebook(example_nb, Path(src_file).with_suffix(".ipynb"))
sphinx-gallery-0.16.0/sphinx_gallery/py_source_parser.py000066400000000000000000000167031461331107500235420ustar00rootroot00000000000000r"""Parser for python source files."""

# Created Sun Nov 27 14:03:07 2016
# Author: Óscar Nájera

import codecs
import ast
from io import BytesIO
import re
import tokenize
from textwrap import dedent

from sphinx.errors import ExtensionError
from sphinx.util.logging import getLogger

logger = getLogger("sphinx-gallery")

SYNTAX_ERROR_DOCSTRING = """
SyntaxError
===========

Example script with invalid Python syntax
"""

# The pattern for in-file config comments is designed to not greedily match
# newlines at the start and end, except for one newline at the end. This
# ensures that the matched pattern can be removed from the code without
# changing the block structure; i.e. empty newlines are preserved, e.g. in
#
#     a = 1
#
#     # sphinx_gallery_thumbnail_number = 2
#
#     b = 2
FLAG_START = r"^[\ \t]*#\s*"
FLAG_BODY = r"sphinx_gallery_([A-Za-z0-9_]+)(\s*=\s*(.+))?[\ \t]*\n?"
INFILE_CONFIG_PATTERN = re.compile(FLAG_START + FLAG_BODY, re.MULTILINE)

START_IGNORE_FLAG = FLAG_START + "sphinx_gallery_start_ignore"
END_IGNORE_FLAG = FLAG_START + "sphinx_gallery_end_ignore"
IGNORE_BLOCK_PATTERN = re.compile(
    rf"{START_IGNORE_FLAG}(?:[\s\S]*?){END_IGNORE_FLAG}\n?", re.MULTILINE
)


def parse_source_file(filename):
    """Parse source file into AST node.

    Parameters
    ----------
    filename : str
        File path

    Returns
    -------
    node : AST node
    content : utf-8 encoded string
    """
    with codecs.open(filename, "r", "utf-8") as fid:
        content = fid.read()
    # change from Windows format to UNIX for uniformity
    content = content.replace("\r\n", "\n")

    try:
        node = ast.parse(content)
        return node, content
    except SyntaxError:
        return None, content


def _get_docstring_and_rest(filename):
    """Separate ``filename`` content between docstring and the rest.

    Strongly inspired from ast.get_docstring.

    Returns
    -------
    docstring : str
        docstring of ``filename``
    rest : str
        ``filename`` content without the docstring
    lineno : int
        The line number.
    node : ast.Module
        The ast node. When `filename` parsed with `mode='exec'` node should be
        of type `ast.Module`.
    """
    node, content = parse_source_file(filename)

    if node is None:
        return SYNTAX_ERROR_DOCSTRING, content, 1, node

    if not isinstance(node, ast.Module):
        raise ExtensionError(
            "This function only supports modules. " "You provided {}".format(
                node.__class__.__name__
            )
        )
    if not (
        node.body
        and isinstance(node.body[0], ast.Expr)
        and isinstance(node.body[0].value, ast.Constant)
    ):
        raise ExtensionError(
            'Could not find docstring in file "{}". '
            "A docstring is required by sphinx-gallery "
            'unless the file is ignored by "ignore_pattern"'.format(filename)
        )

    # Python 3.7+ way
    docstring = ast.get_docstring(node)
    assert docstring is not None  # should be guaranteed above
    # This is just for backward compat
    if node.body[0].value.value[:1] == "\n":
        # just for strict backward compat here
        docstring = "\n" + docstring
    ts = tokenize.tokenize(BytesIO(content.encode()).readline)
    # find the first string according to the tokenizer and get its end row
    for tk in ts:
        if tk.exact_type == 3:
            lineno, _ = tk.end
            break
    else:
        lineno = 0

    # This get the content of the file after the docstring last line
    # Note: 'maxsplit' argument is not a keyword argument in python2
    rest = "\n".join(content.split("\n")[lineno:])
    lineno += 1
    return docstring, rest, lineno, node


def extract_file_config(content):
    """Pull out the file-specific config specified in the docstring."""
    file_conf = {}
    for match in re.finditer(INFILE_CONFIG_PATTERN, content):
        name = match.group(1)
        value = match.group(3)
        if value is None:  # a flag rather than a config setting
            continue
        try:
            value = ast.literal_eval(value)
        except (SyntaxError, ValueError):
            logger.warning(
                "Sphinx-gallery option %s was passed invalid value %s", name, value
            )
        else:
            file_conf[name] = value
    return file_conf


def split_code_and_text_blocks(source_file, return_node=False):
    """Return list with source file separated into code and text blocks.

    Parameters
    ----------
    source_file : str
        Path to the source file.
    return_node : bool
        If True, return the ast node.

    Returns
    -------
    file_conf : dict
        File-specific settings given in source file comments as:
        ``# sphinx_gallery_ = ``
    blocks : list
        (label, content, line_number)
        List where each element is a tuple with the label ('text' or 'code'),
        the corresponding content string of block and the leading line number.
    node : ast.Module
        The parsed ast node.
    """
    docstring, rest_of_content, lineno, node = _get_docstring_and_rest(source_file)
    blocks = [("text", docstring, 1)]

    file_conf = extract_file_config(rest_of_content)

    pattern = re.compile(
        r"(?P^#{20,}.*|^# ?%%.*)\s(?P(?:^#.*\s?)*)",
        flags=re.M,
    )
    sub_pat = re.compile("^#", flags=re.M)

    pos_so_far = 0
    for match in re.finditer(pattern, rest_of_content):
        code_block_content = rest_of_content[pos_so_far : match.start()]
        if code_block_content.strip():
            blocks.append(("code", code_block_content, lineno))
        lineno += code_block_content.count("\n")

        lineno += 1  # Ignored header line of hashes.
        text_content = match.group("text_content")
        text_block_content = dedent(re.sub(sub_pat, "", text_content)).lstrip()
        if text_block_content.strip():
            blocks.append(("text", text_block_content, lineno))
        lineno += text_content.count("\n")

        pos_so_far = match.end()

    remaining_content = rest_of_content[pos_so_far:]
    if remaining_content.strip():
        blocks.append(("code", remaining_content, lineno))

    out = (file_conf, blocks)
    if return_node:
        out += (node,)
    return out


def remove_ignore_blocks(code_block):
    """
    Return the content of *code_block* with ignored areas removed.

    An ignore block starts with # sphinx_gallery_start_ignore, and ends with
    # sphinx_gallery_end_ignore. These lines and anything in between them will
    be removed, but surrounding empty lines are preserved.

    Parameters
    ----------
    code_block : str
        A code segment.
    """
    num_start_flags = len(re.findall(START_IGNORE_FLAG, code_block, re.MULTILINE))
    num_end_flags = len(re.findall(END_IGNORE_FLAG, code_block, re.MULTILINE))

    if num_start_flags != num_end_flags:
        raise ExtensionError(
            'All "sphinx_gallery_start_ignore" flags must have a matching '
            '"sphinx_gallery_end_ignore" flag!'
        )
    return re.subn(IGNORE_BLOCK_PATTERN, "", code_block)[0]


def remove_config_comments(code_block):
    """
    Return the content of *code_block* with in-file config comments removed.

    Comment lines of the pattern '# sphinx_gallery_[option] = [val]' are
    removed, but surrounding empty lines are preserved.

    Parameters
    ----------
    code_block : str
        A code segment.
    """
    parsed_code, _ = re.subn(INFILE_CONFIG_PATTERN, "", code_block)
    return parsed_code
sphinx-gallery-0.16.0/sphinx_gallery/recommender.py000066400000000000000000000224061461331107500224530ustar00rootroot00000000000000# -*- coding: utf-8 -*-
"""Recommendation system generator.

Generate recommendations based on TF-IDF representation and a KNN model.
"""
# Author: Arturo Amor
# License: 3-clause BSD

import numbers
import re
from collections import defaultdict
from pathlib import Path

from .backreferences import (
    _thumbnail_div,
    THUMBNAIL_PARENT_DIV,
    THUMBNAIL_PARENT_DIV_CLOSE,
)
from .py_source_parser import split_code_and_text_blocks
from .gen_rst import extract_intro_and_title
from .utils import _replace_md5


class ExampleRecommender:
    """Compute content-based KNN-TF-IFD recommendation system.

    Parameters
    ----------
    n_examples : int, default=5
        Number of most relevant examples to display.
    min_df : float in range [0.0, 1.0] or int, default=1
        When building the vocabulary ignore terms that have a document frequency
        strictly lower than the given threshold. If float, the parameter
        represents a proportion of documents, integer represents absolute
        counts. This value is also called cut-off in the literature.
    max_df : float in range [0.0, 1.0] or int, default=1.0
        When building the vocabulary ignore terms that have a document frequency
        strictly higher than the given threshold. If float, the parameter
        represents a proportion of documents, integer represents absolute
        counts.

    Attributes
    ----------
    file_names_ : list of str
        The list of file names used for computing the similarity matrix.
        The recommended examples are chosen among this list.

    similarity_matrix_ : dense matrix
        Fitted matrix of pairwise cosine similarities.
    """

    def __init__(self, *, n_examples=5, min_df=3, max_df=0.9):
        self.n_examples = n_examples
        self.min_df = min_df
        self.max_df = max_df

    def token_freqs(self, doc):
        """Extract a dict mapping raw tokens from doc to their occurrences."""
        token_generator = (tok.lower() for tok in re.findall(r"\w+", doc))
        return self.dict_freqs(token_generator)

    @staticmethod
    def dict_freqs(doc):
        """Extract a dict mapping list of tokens to their occurrences."""
        freq = defaultdict(int)
        for tok in doc:
            freq[tok] += 1
        return freq

    def dict_vectorizer(self, data):
        """Convert a dictionary of feature occurrence frequencies into a matrix.

        Parameters
        ----------
        data : list of dict
            Each dictionary represents a document where tokens are keys and
            values are their occurrence frequencies.

        Returns
        -------
        X : ndarray of shape (n_samples, n_features)
            A matrix of occurrences where n_samples is the number of samples in
            the dataset and n_features is the total number of features across
            all samples.
        """
        import numpy as np

        tokens = []
        all_values = defaultdict(list)
        for dict_of_freqs in data:
            for token, freq in dict_of_freqs.items():
                tokens.append(token)
                all_values[token].append(freq)

        token_dict = {
            token: token_idx for token_idx, token in enumerate(sorted(all_values))
        }
        X = np.zeros((len(data), len(token_dict)))
        for dict_of_freqs_idx, dict_of_freqs in enumerate(data):
            for token, freq in dict_of_freqs.items():
                X[dict_of_freqs_idx, token_dict[token]] = freq
        return X

    def compute_tf_idf(self, X):
        """Transform a term frequency matrix into a TF-IDF matrix.

        Parameters
        ----------
        X : ndarray of shape (n_samples, n_features)
            A term frequency matrix.

        Returns
        -------
        X_tfidf : ndarray of shape (n_samples, n_features)
            A tf-idf matrix of the same shape as X.
        """
        import numpy as np

        n_samples = X.shape[0] + 1  # avoid taking log of 0

        df = np.count_nonzero(X, axis=0) + 1
        idf = np.log(n_samples / df) + 1
        X_tfidf = X * idf
        X_tfidf = (X_tfidf.T / np.linalg.norm(X_tfidf, axis=1)).T

        return X_tfidf

    def cosine_similarity(self, X, Y=None):
        """Compute the cosine similarity between two vectors X and Y.

        Parameters
        ----------
        X : ndarray of shape (n_samples_X, n_features)
            Input data.

        Y : ndarray of shape (n_samples_Y, n_features), default=None
            Input data. If `None`, the output will be the pairwise
            similarities between all samples in `X`.

        Returns
        -------
        cosine_similarity : ndarray of shape (n_samples_X, n_samples_Y)
            Cosine similarity matrix.
        """
        import numpy as np

        if Y is X or Y is None:
            Y = X

        X_normalized = X / np.linalg.norm(X)
        if X is Y:
            Y_normalized = X_normalized
        else:
            Y_normalized = Y / np.linalg.norm(Y)
        similarity = X_normalized @ Y_normalized.T

        return similarity

    def fit(self, file_names):
        """Compute the similarity matrix of a group of documents.

        Parameters
        ----------
        file_names : list or generator of file names.

        Returns
        -------
        self : object
            Fitted recommender.
        """
        import numpy as np

        n_examples = self.n_examples
        min_df = self.min_df
        max_df = self.max_df
        if not isinstance(n_examples, numbers.Integral):
            raise ValueError("n_examples must be an integer")
        elif n_examples < 1:
            raise ValueError("n_examples must be strictly positive")
        if not (
            (isinstance(min_df, numbers.Integral) and min_df >= 1)
            or (isinstance(min_df, float) and 0.0 <= min_df <= 1.0)
        ):
            raise ValueError("min_df must be float in range [0.0, 1.0] or int")
        if not (
            (isinstance(max_df, numbers.Integral) and max_df > 1)
            or (isinstance(max_df, float) and 0.0 <= max_df <= 1.0)
        ):
            raise ValueError("max_df must be float in range [0.0, 1.0] or int")

        freq_func = self.token_freqs
        counts_matrix = self.dict_vectorizer(
            [freq_func(Path(fname).read_text(encoding="utf-8")) for fname in file_names]
        )
        if isinstance(min_df, float):
            min_df = int(np.ceil(min_df * counts_matrix.shape[0]))
        if isinstance(max_df, float):
            max_df = int(np.floor(max_df * counts_matrix.shape[0]))
        doc_appearances = np.sum(counts_matrix, axis=0)
        mask = (doc_appearances >= min_df) & (doc_appearances <= max_df)
        self.similarity_matrix_ = self.cosine_similarity(
            self.compute_tf_idf(counts_matrix[:, mask])
        )
        self.file_names_ = file_names
        return self

    def predict(self, file_name):
        """Compute the `n_examples` most similar documents to the query.

        Parameters
        ----------
        file_name : str
            Name of the file corresponding to the query index `item_id`.

        Returns
        -------
        recommendations : list of str
            Name of the files most similar to the query.
        """
        item_id = self.file_names_.index(file_name)
        similar_items = list(enumerate(self.similarity_matrix_[item_id]))
        sorted_items = sorted(similar_items, key=lambda x: x[1], reverse=True)

        # Get the top k items similar to item_id. Note that `sorted_items[0]`
        # is always the query document itself, hence it is discarded from the
        # returned list of recommendations.
        top_k_items = [idx for idx, _ in sorted_items[1 : self.n_examples + 1]]
        recommendations = [self.file_names_[idx] for idx in top_k_items]
        return recommendations


def _write_recommendations(recommender, fname, gallery_conf):
    """Generate `.recommendations` reST file for a given example.

    Parameters
    ----------
    recommender : ExampleRecommender
        Instance of a fitted ExampleRecommender.

    fname : str
        Path to the example file.

    gallery_conf : dict
        Configuration dictionary for the sphinx-gallery extension.
    """
    path_fname = Path(fname)
    recommend_fname = f"{path_fname.parent / path_fname.stem}.recommendations.new"
    recommended_examples = recommender.predict(fname)

    default_rubric_header = "Related examples"
    rubric_header = gallery_conf["recommender"].get(
        "rubric_header", default_rubric_header
    )

    with open(recommend_fname, "w", encoding="utf-8") as ex_file:
        ex_file.write(f"\n\n.. rubric:: {rubric_header}\n")
        ex_file.write(THUMBNAIL_PARENT_DIV)
        for example_fname in recommended_examples:
            example_path = Path(example_fname)
            _, script_blocks = split_code_and_text_blocks(
                example_fname, return_node=False
            )
            intro, title = extract_intro_and_title(fname, script_blocks[0][1])
            ex_file.write(
                _thumbnail_div(
                    example_path.parent,
                    gallery_conf["src_dir"],
                    example_path.name,
                    intro,
                    title,
                    is_backref=True,
                )
            )
        ex_file.write(THUMBNAIL_PARENT_DIV_CLOSE)
    _replace_md5(recommend_fname, mode="t")
sphinx-gallery-0.16.0/sphinx_gallery/scrapers.py000066400000000000000000000422261461331107500217770ustar00rootroot00000000000000# Author: Óscar Nájera
# License: 3-clause BSD
"""Scrapers for embedding images.

Collect images that have been produced by code blocks.

The only scraper we natively support is Matplotlib, others should
live in modules that will support them (e.g., PyVista, Plotly).  Scraped
images are injected as rst ``image-sg`` directives into the ``.rst``
file generated for each example script.
"""

import importlib
import inspect
import os
import sys
import re
from textwrap import indent
from pathlib import PurePosixPath
from warnings import filterwarnings

from sphinx.errors import ExtensionError
from .utils import optipng

__all__ = [
    "save_figures",
    "figure_rst",
    "ImagePathIterator",
    "clean_modules",
    "matplotlib_scraper",
]


###############################################################################
# Scrapers


def _import_matplotlib():
    """Import matplotlib safely."""
    # make sure that the Agg backend is set before importing any
    # matplotlib
    import matplotlib

    matplotlib.use("agg")
    matplotlib_backend = matplotlib.get_backend().lower()

    filterwarnings(
        "ignore",
        category=UserWarning,
        message="Matplotlib is currently using agg, which is a"
        " non-GUI backend, so cannot show the figure."
        "|(\n|.)*is non-interactive, and thus cannot be"
        " shown",
    )

    if matplotlib_backend != "agg":
        raise ExtensionError(
            "Sphinx-Gallery relies on the matplotlib 'agg' backend to "
            "render figures and write them to files. You are "
            "currently using the {} backend. Sphinx-Gallery will "
            "terminate the build now, because changing backends is "
            "not well supported by matplotlib. We advise you to move "
            "sphinx_gallery imports before any matplotlib-dependent "
            "import. Moving sphinx_gallery imports at the top of "
            "your conf.py file should fix this issue".format(matplotlib_backend)
        )

    import matplotlib.pyplot as plt

    return matplotlib, plt


def _matplotlib_fig_titles(fig):
    titles = []
    # get supertitle if exists
    suptitle = getattr(fig, "_suptitle", None)
    if suptitle is not None:
        titles.append(suptitle.get_text())
    # get titles from all axes, for all locs
    title_locs = ["left", "center", "right"]
    for ax in fig.axes:
        for loc in title_locs:
            text = ax.get_title(loc=loc)
            if text:
                titles.append(text)
    fig_titles = ", ".join(titles)
    return fig_titles


_ANIMATION_RST = """
.. container:: sphx-glr-animation

    .. raw:: html

        {0}
"""


def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
    """Scrape Matplotlib images.

    Parameters
    ----------
    block : tuple
        A tuple containing the (label, content, line_number) of the block.
    block_vars : dict
        Dict of block variables.
    gallery_conf : dict
        Contains the configuration of Sphinx-Gallery
    **kwargs : dict
        Additional keyword arguments to pass to
        :meth:`~matplotlib.figure.Figure.savefig`, e.g. ``format='svg'``. The
        ``format`` keyword argument in particular is used to set the file
        extension of the output file (currently only 'png', 'jpg', 'svg',
        'gif', and 'webp' are supported).

        This is not used internally, but intended for use when overriding the scraper.

    Returns
    -------
    rst : str
        The reStructuredText that will be rendered to HTML containing
        the images. This is often produced by :func:`figure_rst`.
    """
    # Do not use _import_matplotlib() to avoid potentially changing the backend
    import matplotlib
    import matplotlib.pyplot as plt

    from matplotlib.animation import Animation

    image_path_iterator = block_vars["image_path_iterator"]
    image_rsts = []
    srcset = gallery_conf["image_srcset"]

    # Check for animations
    anims = {}
    if gallery_conf["matplotlib_animations"]:
        for ani in block_vars["example_globals"].values():
            if isinstance(ani, Animation):
                anims[ani._fig] = ani
    # Then standard images
    for fig_num, image_path in zip(plt.get_fignums(), image_path_iterator):
        image_path = PurePosixPath(image_path)
        if "format" in kwargs:
            image_path = image_path.with_suffix("." + kwargs["format"])
        # Convert figure number to Figure.
        fig = plt.figure(fig_num)
        # Deal with animations
        if anim := anims.get(fig):
            image_rsts.append(_anim_rst(anim, image_path, gallery_conf))
            continue
        # get fig titles
        fig_titles = _matplotlib_fig_titles(fig)
        to_rgba = matplotlib.colors.colorConverter.to_rgba
        # shallow copy should be fine here, just want to avoid changing
        # "kwargs" for subsequent figures processed by the loop
        these_kwargs = kwargs.copy()
        for attr in ["facecolor", "edgecolor"]:
            fig_attr = getattr(fig, "get_" + attr)()
            default_attr = matplotlib.rcParams["figure." + attr]
            if to_rgba(fig_attr) != to_rgba(default_attr) and attr not in kwargs:
                these_kwargs[attr] = fig_attr

        # save the figures, and populate the srcsetpaths
        try:
            fig.savefig(image_path, **these_kwargs)
            dpi0 = matplotlib.rcParams["savefig.dpi"]
            if dpi0 == "figure":
                dpi0 = fig.dpi
            dpi0 = these_kwargs.get("dpi", dpi0)
            srcsetpaths = {0: image_path}

            # save other srcset paths, keyed by multiplication factor:
            for mult in srcset:
                multst = f"{mult:.2f}".replace(".", "_")
                name = f"{image_path.stem}_{multst}x{image_path.suffix}"
                hipath = image_path.parent / PurePosixPath(name)
                hikwargs = {**these_kwargs, "dpi": mult * dpi0}
                fig.savefig(hipath, **hikwargs)
                srcsetpaths[mult] = hipath
            srcsetpaths = [srcsetpaths]
        except Exception:
            plt.close("all")
            raise

        if "images" in gallery_conf["compress_images"]:
            optipng(str(image_path), gallery_conf["compress_images_args"])
            for hipath in srcsetpaths[0].items():
                optipng(str(hipath), gallery_conf["compress_images_args"])

        image_rsts.append(
            figure_rst(
                [image_path],
                gallery_conf["src_dir"],
                fig_titles,
                srcsetpaths=srcsetpaths,
            )
        )

    plt.close("all")
    rst = ""
    if len(image_rsts) == 1:
        rst = image_rsts[0]
    elif len(image_rsts) > 1:
        image_rsts = [
            re.sub(r":class: sphx-glr-single-img", ":class: sphx-glr-multi-img", image)
            for image in image_rsts
        ]
        image_rsts = [
            HLIST_IMAGE_MATPLOTLIB + indent(image, " " * 6) for image in image_rsts
        ]
        rst = HLIST_HEADER + "".join(image_rsts)
    return rst


def _anim_rst(anim, image_path, gallery_conf):
    from matplotlib.animation import FFMpegWriter, ImageMagickWriter

    # output the thumbnail as the image, as it will just be copied
    # if it's the file thumbnail
    fig = anim._fig
    image_path = image_path.with_suffix(".gif")
    fig_size = fig.get_size_inches()
    thumb_size = gallery_conf["thumbnail_size"]
    use_dpi = round(min(t_s / f_s for t_s, f_s in zip(thumb_size, fig_size)))
    if FFMpegWriter.isAvailable():
        writer = "ffmpeg"
    elif ImageMagickWriter.isAvailable():
        writer = "imagemagick"
    else:
        writer = None
    anim.save(str(image_path), writer=writer, dpi=use_dpi)
    html = anim._repr_html_()
    if html is None:  # plt.rcParams['animation.html'] == 'none'
        html = anim.to_jshtml()
    html = indent(html, "     ")
    return _ANIMATION_RST.format(html)


_scraper_dict = dict(
    matplotlib=matplotlib_scraper,
)


class ImagePathIterator:
    """Iterate over image paths for a given example.

    Parameters
    ----------
    image_path : str
        The template image path.
    """

    def __init__(self, image_path):
        self.image_path = image_path
        self.paths = list()
        self._stop = 1000000

    def __len__(self):
        """Return the number of image paths used.

        Returns
        -------
        n_paths : int
            The number of paths.
        """
        return len(self.paths)

    def __iter__(self):
        """Iterate over paths.

        Returns
        -------
        paths : iterable of str

        This enables the use of this Python pattern::

            >>> for epoch in epochs:  # doctest: +SKIP
            >>>     print(epoch)  # doctest: +SKIP

        Where ``epoch`` is given by successive outputs of
        :func:`mne.Epochs.next`.
        """
        # we should really never have 1e6, let's prevent some user pain
        for ii in range(self._stop):
            yield self.next()
        else:
            raise ExtensionError(f"Generated over {self._stop} images")

    def next(self):
        """Return the next image path, with numbering starting at 1."""
        return self.__next__()

    def __next__(self):
        # The +1 here is because we start image numbering at 1 in filenames
        path = self.image_path.format(len(self) + 1)
        self.paths.append(path)
        return path


# For now, these are what we support
# Update advanced.rst if this list is changed
_KNOWN_IMG_EXTS = ("png", "svg", "jpg", "gif", "webp")


def _find_image_ext(path):
    """Find an image, tolerant of different file extensions."""
    path = os.path.splitext(path)[0]
    for ext in _KNOWN_IMG_EXTS:
        this_path = f"{path}.{ext}"
        if os.path.isfile(this_path):
            break
    else:
        ext = "png"
    return (f"{path}.{ext}", ext)


def save_figures(block, block_vars, gallery_conf):
    """Save all open figures of the example code-block.

    Parameters
    ----------
    block : tuple
        A tuple containing the (label, content, line_number) of the block.
    block_vars : dict
        Dict of block variables.
    gallery_conf : dict
        Contains the configuration of Sphinx-Gallery

    Returns
    -------
    images_rst : str
        rst code to embed the images in the document.
    """
    from .gen_rst import _get_callables

    image_path_iterator = block_vars["image_path_iterator"]
    all_rst = ""
    prev_count = len(image_path_iterator)
    for scraper in _get_callables(gallery_conf, "image_scrapers"):
        rst = scraper(block, block_vars, gallery_conf)
        if not isinstance(rst, str):
            raise ExtensionError(
                f"rst from scraper {scraper!r} was not a "
                f"string, got type {type(rst)}:\n{rst!r}"
            )
        n_new = len(image_path_iterator) - prev_count
        for ii in range(n_new):
            current_path, _ = _find_image_ext(
                image_path_iterator.paths[prev_count + ii]
            )
            if not os.path.isfile(current_path):
                raise ExtensionError(
                    f"Scraper {scraper} did not produce expected image:"
                    f"\n{current_path}"
                )
        all_rst += rst
    return all_rst


def figure_rst(figure_list, sources_dir, fig_titles="", srcsetpaths=None):
    """Generate reST for a list of image filenames.

    Depending on whether we have one or more figures, we use a
    single rst call to 'image' or a horizontal list.

    Parameters
    ----------
    figure_list : list
        List of strings of the figures' absolute paths.
    sources_dir : str
        absolute path of Sphinx documentation sources
    fig_titles : str
        Titles of figures, empty string if no titles found. Currently
        only supported for matplotlib figures, default = ''.
    srcsetpaths : list or None
        List of dictionaries containing absolute paths.  If
        empty, then srcset field is populated with the figure path.
        (see ``image_srcset`` configuration option).  Otherwise,
        each dict is of the form
        {0: /images/image.png, 2.0: /images/image_2_00x.png}
        where the key is the multiplication factor and the contents
        the path to the image created above.

    Returns
    -------
    images_rst : str
        rst code to embed the images in the document

    The rst code will have a custom ``image-sg`` directive that allows
    multiple resolution images to be served e.g.:
    ``:srcset: /plot_types/imgs/img_001.png,
      /plot_types/imgs/img_2_00x.png 2.00x``

    """
    if srcsetpaths is None:
        # this should never happen, but figure_rst is public, so
        # this has to be a kwarg...
        srcsetpaths = [{0: fl} for fl in figure_list]

    figure_paths = [
        os.path.relpath(figure_path, sources_dir).replace(os.sep, "/").lstrip("/")
        for figure_path in figure_list
    ]

    # Get alt text
    alt = ""
    if fig_titles:
        alt = fig_titles
    elif figure_list:
        file_name = os.path.split(figure_list[0])[1]
        # remove ext & 'sphx_glr_' from start & n#'s from end
        file_name_noext = os.path.splitext(file_name)[0][9:-4]
        # replace - & _ with \s
        file_name_final = re.sub(r"[-,_]", " ", file_name_noext)
        alt = file_name_final
    alt = _single_line_sanitize(alt)

    images_rst = ""
    if len(figure_paths) == 1:
        figure_name = figure_paths[0]
        hinames = srcsetpaths[0]
        srcset = _get_srcset_st(sources_dir, hinames)
        images_rst = SG_IMAGE % (figure_name, alt, srcset)

    elif len(figure_paths) > 1:
        images_rst = HLIST_HEADER
        for nn, figure_name in enumerate(figure_paths):
            hinames = srcsetpaths[nn]
            srcset = _get_srcset_st(sources_dir, hinames)

            images_rst += HLIST_SG_TEMPLATE % (figure_name, alt, srcset)

    return images_rst


def _get_srcset_st(sources_dir, hinames):
    """Create the srcset string for including on the rst line.

    For example; `sources_dir` might be `/home/sample-proj/source`,
    hinames posix paths:
    0: /home/sample-proj/source/plot_types/images/img1.png,
    2.0: /home/sample-proj/source/plot_types/images/img1_2_00x.png,

    The result would be:
    '/plot_types/basic/images/sphx_glr_pie_001.png,
    /plot_types/basic/images/sphx_glr_pie_001_2_00x.png 2.00x'
    """
    srcst = ""
    for k in hinames.keys():
        path = os.path.relpath(hinames[k], sources_dir).replace(os.sep, "/").lstrip("/")
        srcst += "/" + path
        if k == 0:
            srcst += ", "
        else:
            srcst += f" {k:1.2f}x, "
    if srcst[-2:] == ", ":
        srcst = srcst[:-2]
    srcst += ""

    return srcst


def _single_line_sanitize(s):
    """Remove problematic newlines."""
    # For example, when setting a :alt: for an image, it shouldn't have \n
    # This is a function in case we end up finding other things to replace
    return s.replace("\n", " ")


# The following strings are used when we have several pictures: we use
# an html div tag that our CSS uses to turn the lists into horizontal
# lists.
HLIST_HEADER = """
.. rst-class:: sphx-glr-horizontal

"""

HLIST_IMAGE_MATPLOTLIB = """
    *
"""

HLIST_SG_TEMPLATE = """
    *

      .. image-sg:: /%s
          :alt: %s
          :srcset: %s
          :class: sphx-glr-multi-img
"""

SG_IMAGE = """
.. image-sg:: /%s
   :alt: %s
   :srcset: %s
   :class: sphx-glr-single-img
"""

# keep around for back-compat:
SINGLE_IMAGE = """
 .. image:: /%s
     :alt: %s
     :class: sphx-glr-single-img
"""

###############################################################################
# Module resetting


def _reset_matplotlib(gallery_conf, fname):
    """Reset matplotlib."""
    mpl, plt = _import_matplotlib()
    plt.rcdefaults()
    importlib.reload(mpl.units)
    importlib.reload(mpl.dates)
    importlib.reload(mpl.category)


def _reset_seaborn(gallery_conf, fname):
    """Reset seaborn."""
    seaborn_module = sys.modules.get("seaborn")
    if seaborn_module is not None:
        seaborn_module.reset_defaults()


_reset_dict = {
    "matplotlib": _reset_matplotlib,
    "seaborn": _reset_seaborn,
}


def clean_modules(gallery_conf, fname, when):
    """Remove, unload, or reset modules.

    After a script is executed it can load a variety of settings that one
    does not want to influence in other examples in the gallery.

    Parameters
    ----------
    gallery_conf : dict
        The gallery configuration.
    fname : str or None
        The example being run. Will be None when this is called entering
        a directory of examples to be built.
    when : str
        Whether this module is run before or after examples.

        This parameter is only forwarded when the callables accept 3
        parameters.
    """
    from .gen_rst import _get_callables

    for reset_module in _get_callables(gallery_conf, "reset_modules"):
        sig = inspect.signature(reset_module)
        if len(sig.parameters) == 3:
            third_param = list(sig.parameters.keys())[2]
            if third_param != "when":
                raise ValueError(
                    f"3rd parameter in {reset_module.__name__} "
                    "function signature must be 'when', "
                    f"got {third_param}"
                )
            reset_module(gallery_conf, fname, when=when)
        else:
            reset_module(gallery_conf, fname)
sphinx-gallery-0.16.0/sphinx_gallery/sorting.py000066400000000000000000000127451461331107500216450ustar00rootroot00000000000000r"""Sorters for Sphinx-Gallery (sub)sections.

Sorting key functions for gallery subsection folders and section files.
"""

# Created: Sun May 21 20:38:59 2017
# Author: Óscar Nájera
# License: 3-clause BSD

import os
import types

from sphinx.errors import ConfigError

from .gen_rst import extract_intro_and_title
from .py_source_parser import split_code_and_text_blocks


class ExplicitOrder:
    """Sorting key for all gallery subsections.

    All subsections folders must be listed, otherwise an exception is raised.
    However, you can add '*' as a placeholder to the list. All not-listed
    subsection folders will be inserted at the given position and no
    exception is raised.

    Parameters
    ----------
    ordered_list : list, tuple, or :term:`python:generator`
        Hold the paths of each galleries' subsections.

    Raises
    ------
    ValueError
        Wrong input type or Subgallery path missing.
    """

    def __init__(self, ordered_list):
        if not isinstance(ordered_list, (list, tuple, types.GeneratorType)):
            raise ConfigError(
                "ExplicitOrder sorting key takes a list, "
                "tuple or Generator, which hold"
                "the paths of each gallery subfolder"
            )

        self.ordered_list = [
            "*" if path == "*" else os.path.normpath(path) for path in ordered_list
        ]
        try:
            i = ordered_list.index("*")
            self.has_wildcard = True
            self.ordered_list_start = self.ordered_list[:i]
            self.ordered_list_end = self.ordered_list[i + 1 :]
        except ValueError:  # from index("*")
            self.has_wildcard = False
            self.ordered_list_start = []
            self.ordered_list_end = self.ordered_list

    def __call__(self, item):
        """
        Return an integer suited for ordering the items.

        If the ordered_list contains a wildcard "*", items before "*" will return
        negative numbers, items after "*" will have positive numbers, and
        not-listed items will return 0.

        If there is no wildcard, all items with return positive numbers, and
        not-listed items will raise a ConfigError.
        """
        if item in self.ordered_list_start:
            return self.ordered_list_start.index(item) - len(self.ordered_list_start)
        elif item in self.ordered_list_end:
            return self.ordered_list_end.index(item) + 1
        else:
            if self.has_wildcard:
                return 0
            else:
                raise ConfigError(
                    "The subsection folder {!r} was not found in the "
                    "'subsection_order' config. If you use an explicit "
                    "'subsection_order', you must specify all subsection folders "
                    "or add '*' as a wildcard to collect all not-listed subsection "
                    "folders.".format(item)
                )

    def __repr__(self):
        return f"<{self.__class__.__name__} : {self.ordered_list}>"


class _SortKey:
    """Base class for section order key classes."""

    def __init__(self, src_dir):
        self.src_dir = src_dir

    def __repr__(self):
        return f"<{self.__class__.__name__}>"


class NumberOfCodeLinesSortKey(_SortKey):
    """Sort examples in src_dir by the number of code lines.

    Parameters
    ----------
    src_dir : str
        The source directory.
    """

    def __call__(self, filename):
        """Return number of code lines in `filename`."""
        src_file = os.path.normpath(os.path.join(self.src_dir, filename))
        file_conf, script_blocks = split_code_and_text_blocks(src_file)
        amount_of_code = sum(
            [
                len(bcontent)
                for blabel, bcontent, lineno in script_blocks
                if blabel == "code"
            ]
        )
        return amount_of_code


class FileSizeSortKey(_SortKey):
    """Sort examples in src_dir by file size.

    Parameters
    ----------
    src_dir : str
        The source directory.
    """

    def __call__(self, filename):
        """Return file size."""
        src_file = os.path.normpath(os.path.join(self.src_dir, filename))
        return int(os.stat(src_file).st_size)


class FileNameSortKey(_SortKey):
    """Sort examples in src_dir by file name.

    Parameters
    ----------
    src_dir : str
        The source directory.
    """

    def __call__(self, filename):
        """Return `filename`."""
        return filename


class ExampleTitleSortKey(_SortKey):
    """Sort examples in src_dir by example title.

    Parameters
    ----------
    src_dir : str
        The source directory.
    """

    def __call__(self, filename):
        """Return title of example."""
        src_file = os.path.normpath(os.path.join(self.src_dir, filename))
        _, script_blocks = split_code_and_text_blocks(src_file)
        _, title = extract_intro_and_title(src_file, script_blocks[0][1])
        return title


class FunctionSortKey:
    """Sort examples using a function passed through to :py:func:`sorted`.

    Parameters
    ----------
    func : :external+python:term:`callable`
           sorting key function,
           can only take one argument, i.e. lambda func = arg: arg[0] * arg[1]
    r : str, None
        printable representation of object
    """

    def __init__(self, func, r=None):
        self.f = func
        self.r = r

    def __repr__(self):
        return self.r if self.r else "FunctionSortKey"

    def __call__(self, arg):
        """Return func(arg)."""
        return self.f(arg)
sphinx-gallery-0.16.0/sphinx_gallery/tests/000077500000000000000000000000001461331107500207375ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/__init__.py000066400000000000000000000000001461331107500230360ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/conftest.py000066400000000000000000000134541461331107500231450ustar00rootroot00000000000000"""Pytest fixtures."""

from contextlib import contextmanager
from io import StringIO
import os
import shutil
from unittest.mock import Mock

import pytest

import sphinx
from sphinx.application import Sphinx
from sphinx.errors import ExtensionError
from sphinx.util.docutils import docutils_namespace
from sphinx_gallery import docs_resolv, gen_gallery, gen_rst, py_source_parser
from sphinx_gallery.scrapers import _import_matplotlib
from sphinx_gallery.utils import _get_image


def pytest_report_header(config, startdir=None):
    """Add information to the pytest run header."""
    return f"Sphinx:  {sphinx.__version__} ({sphinx.__file__})"


@pytest.fixture
def gallery_conf(tmpdir):
    """Set up a test sphinx-gallery configuration."""
    app = Mock(
        spec=Sphinx,
        config=dict(source_suffix={".rst": None}, default_role=None),
        extensions=[],
    )
    gallery_conf = gen_gallery._fill_gallery_conf_defaults({}, app=app)
    gen_gallery._update_gallery_conf_builder_inited(gallery_conf, str(tmpdir))
    gallery_conf.update(examples_dir=str(tmpdir), gallery_dir=str(tmpdir))
    return gallery_conf


@pytest.fixture
def log_collector(monkeypatch):
    app = Mock(spec=Sphinx, name="FakeSphinxApp")()
    monkeypatch.setattr(docs_resolv, "logger", app)
    monkeypatch.setattr(gen_gallery, "logger", app)
    monkeypatch.setattr(py_source_parser, "logger", app)
    monkeypatch.setattr(gen_rst, "logger", app)
    yield app


@pytest.fixture
def unicode_sample(tmpdir):
    """Return temporary python source file with Unicode in various places."""
    code_str = b"""# -*- coding: utf-8 -*-
'''
\xc3\x9anicode in header
=================

U\xc3\xb1icode in description
'''

# Code source: \xc3\x93scar N\xc3\xa1jera
# License: BSD 3 clause

import os
path = os.path.join('a','b')

a = 'hei\xc3\x9f'  # Unicode string

import sphinx_gallery.back_references as br
br.identify_names

from sphinx_gallery.back_references import identify_names
identify_names

from sphinx_gallery.back_references import DummyClass
DummyClass().prop

import matplotlib.pyplot as plt
_ = plt.figure()

"""

    fname = tmpdir.join("unicode_sample.py")
    fname.write(code_str, "wb")
    return fname.strpath


@pytest.fixture
def req_mpl_jpg(tmpdir, req_mpl, scope="session"):
    """Raise SkipTest if JPEG support is not available."""
    # mostly this is needed because of
    # https://github.com/matplotlib/matplotlib/issues/16083
    import matplotlib.pyplot as plt

    fig, ax = plt.subplots()
    ax.plot(range(10))
    try:
        plt.savefig(str(tmpdir.join("testplot.jpg")))
    except Exception as exp:
        pytest.skip(f"Matplotlib jpeg saving failed: {exp}")
    finally:
        plt.close(fig)


@pytest.fixture(scope="session")
def req_mpl():
    try:
        _import_matplotlib()
    except (ImportError, ValueError):
        pytest.skip("Test requires matplotlib")


@pytest.fixture(scope="session")
def req_pil():
    try:
        _get_image()
    except ExtensionError:
        pytest.skip("Test requires pillow")


@pytest.fixture
def conf_file(request):
    try:
        env = request.node.get_closest_marker("conf_file")
    except AttributeError:  # old pytest
        env = request.node.get_marker("conf_file")
    kwargs = env.kwargs if env else {}
    result = {
        "content": "",
        "extensions": ["sphinx_gallery.gen_gallery"],
    }
    result.update(kwargs)

    return result


class SphinxAppWrapper:
    """Wrapper for sphinx.application.Application.

    This allows control over when the sphinx application is initialized, since
    part of the sphinx-gallery build is done in
    sphinx.application.Application.__init__ and the remainder is done in
    sphinx.application.Application.build.
    """

    def __init__(self, srcdir, confdir, outdir, doctreedir, buildername, **kwargs):
        self.srcdir = srcdir
        self.confdir = confdir
        self.outdir = outdir
        self.doctreedir = doctreedir
        self.buildername = buildername
        self.kwargs = kwargs

    def create_sphinx_app(self):
        """Create Sphinx app."""
        # Avoid warnings about re-registration, see:
        # https://github.com/sphinx-doc/sphinx/issues/5038
        with self.create_sphinx_app_context() as app:
            pass
        return app

    @contextmanager
    def create_sphinx_app_context(self):
        """Create Sphinx app inside context."""
        with docutils_namespace():
            app = Sphinx(
                self.srcdir,
                self.confdir,
                self.outdir,
                self.doctreedir,
                self.buildername,
                **self.kwargs,
            )
            yield app

    def build_sphinx_app(self, *args, **kwargs):
        """Build Sphinx app."""
        with self.create_sphinx_app_context() as app:
            # building should be done in the same docutils_namespace context
            app.build(*args, **kwargs)
        return app


@pytest.fixture
def sphinx_app_wrapper(tmpdir, conf_file, req_mpl, req_pil):
    _fixturedir = os.path.join(os.path.dirname(__file__), "testconfs")
    srcdir = os.path.join(str(tmpdir), "config_test")
    shutil.copytree(_fixturedir, srcdir)
    shutil.copytree(
        os.path.join(_fixturedir, "src"), os.path.join(str(tmpdir), "examples")
    )

    base_config = f"""
import os
import sphinx_gallery
extensions = {conf_file['extensions']!r}
exclude_patterns = ['_build']
source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'Sphinx-Gallery '\n\n
"""
    with open(os.path.join(srcdir, "conf.py"), "w") as conffile:
        conffile.write(base_config + conf_file["content"])

    return SphinxAppWrapper(
        srcdir,
        srcdir,
        os.path.join(srcdir, "_build"),
        os.path.join(srcdir, "_build", "toctree"),
        "html",
        warning=StringIO(),
        status=StringIO(),
    )
sphinx-gallery-0.16.0/sphinx_gallery/tests/reference_parse.txt000066400000000000000000000071201461331107500246300ustar00rootroot00000000000000[('text',  # 0
  '\n'
  'Alternating text and code\n'
  '=========================\n'
  '\n'
  'Sphinx-Gallery is capable of transforming Python files into reST files\n'
  'with a notebook structure. For this to be used you need to respect some '
  'syntax\n'
  'rules. This example demonstrates how to alternate text and code blocks and '
  'some\n'
  'edge cases. It was designed to be compared with the\n'
  ':download:`source Python script `.',
  1),
('text',  # 1
  'This is the first text block and directly follows the header docstring '
  'above.\n',
  12,
),
('code', '\nimport numpy as np  # noqa: F401\n\n', 13),  # 2
('code',  # 3
  '\n'
  "# You can separate code blocks using either a single line of ``#``'s\n"
  '# (>=20 columns), ``#%%``, or ``# %%``. For consistency, it is recommend '
  'that\n'
  "# you use only one of the above three 'block splitter' options in your "
  'project.\n'
  'A = 1\n'
  '\n'
  'import matplotlib.pyplot as plt  # noqa: F401\n'
  '\n',
  17),
('text',
  'Block splitters allow you alternate between code and text blocks **and**\n'
  'separate sequential blocks of code (above) and text (below).\n',
  26,
),
('text',
  "A line of ``#``'s also works for separating blocks. The above line of "
  "``#``'s\n"
  'separates the text block above from this text block. Notice however, that\n'
  'separated text blocks only shows as a new lines between text, in the '
  'rendered\n'
  'output.\n',
  30),
('code',  # 4
  '\n'
  '\n'
  'def dummy():\n'
  '    """This should not be part of a \'text\' block\'"""  # noqa: D404\n'
  '\n'
  '    # %%\n'
  '    # This comment inside a code block will remain in the code block\n'
  '    pass\n'
  '\n'
  '\n'
  "# this line should not be part of a 'text' block\n"
  '\n',
  34),
('text',
  '####################################################################\n'
  '\n'
  'The above syntax makes a line cut in Sphinx. Note the space between the '
  'first\n'
  "``#`` and the line of ``#``'s.\n",
  47,
),
('text',
  '.. warning::\n'
  '    The next kind of comments are not supported (notice the line of '
  "``#``'s\n"
  '    and the ``# %%`` start at the margin instead of being indented like\n'
  "    above) and become too hard to escape so just don't use code like "
  'this::\n'
  '\n'
  '        def dummy2():\n'
  '            """Function docstring"""\n'
  '        ####################################\n'
  '        # This comment\n'
  '        # %%\n'
  '        # and this comment inside python indentation\n'
  '        # breaks the block structure and is not\n'
  '        # supported\n'
  '            dummy2\n'
  '\n',
  54),
('code',
  '\n'
  '"""Free strings are not supported. They will be rendered as a code '
  'block"""\n'
  '\n',
  69,
),
('text',
  'New lines can be included in your text block and the parser\n'
  'is capable of retaining this important whitespace to work with Sphinx.\n'
  'Everything after a block splitter and starting with ``#`` then one space,\n'
  'is interpreted by Sphinx-Gallery to be a reST text block. Keep your text\n'
  'block together using ``#`` and a space at the beginning of each line.\n'
  '\n'
  'reST header within text block\n'
  '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
  73,
),
('code', '\nprint("one")\n\n', 81),
('code',
  '\n# another way to separate code blocks shown above\nB = 1\n\n',
  86,
),
('text',
  'Code blocks containing Jupyter magic are executable\n'
  '    .. code-block:: bash\n'
  '\n'
  '      %%bash\n'
  '      # This could be run!\n\n',
  91,
),
('text',
  'Last text block.\n'
  '\n'
  "That's all folks !\n"
  '\n'
  '.. literalinclude:: plot_parse.py\n'
  '\n'
  '\n',
  99,
)]
sphinx-gallery-0.16.0/sphinx_gallery/tests/test_backreferences.py000066400000000000000000000175551461331107500253270ustar00rootroot00000000000000# Author: Óscar Nájera
# License: 3-clause BSD
"""Testing the rst files generator."""

import pytest
import sys
from unittest.mock import MagicMock, patch

from sphinx.errors import ExtensionError

import sphinx_gallery.backreferences as sg
from sphinx_gallery.py_source_parser import split_code_and_text_blocks
from sphinx_gallery.gen_rst import _sanitize_rst


REFERENCE = r"""
.. raw:: html

    
.. only:: html .. image:: /fake_dir/images/thumb/sphx_glr_test_file_thumb.png :alt: :ref:`sphx_glr_fake_dir_test_file.py` .. raw:: html
test title
{1}""" @pytest.mark.parametrize( "content, tooltip, is_backref", [ # HTML sanitizing ('<"test">', "<"test">", False), # backref support ("test formatting", "test formatting", True), # reST sanitizing ( "1 :class:`~a.b`. 2 :class:`a.b` 3 :ref:`whatever `", "1 b. 2 a.b 3 better name", False, ), ("use :meth:`mne.io.Raw.plot_psd` to", "use mne.io.Raw.plot_psd to", False), ( "`this` and ``that``; and `these things` and ``those things``", "this and that; and these things and those things", False, ), ], ) def test_thumbnail_div(content, tooltip, is_backref): """Test if the thumbnail div generates the correct string.""" with pytest.raises(ExtensionError, match="internal Sphinx-Gallery thumb"): html_div = sg._thumbnail_div( "fake_dir", "", "test_file.py", '<"test">', '<"title">' ) content = _sanitize_rst(content) title = "test title" html_div = sg._thumbnail_div( "fake_dir", "", "test_file.py", content, title, is_backref=is_backref, check=False, ) if is_backref: extra = """ .. only:: not html * :ref:`sphx_glr_fake_dir_test_file.py` """ else: extra = "" reference = REFERENCE.format(tooltip, extra) assert html_div == reference def test_identify_names(unicode_sample, gallery_conf): """Test name identification.""" expected = { "os.path.join": [ { "name": "join", "module": "os.path", "module_short": "os.path", "is_class": False, "is_explicit": False, } ], "br.identify_names": [ { "name": "identify_names", "module": "sphinx_gallery.back_references", "module_short": "sphinx_gallery.back_references", "is_class": False, "is_explicit": False, } ], "identify_names": [ { "name": "identify_names", "module": "sphinx_gallery.back_references", "module_short": "sphinx_gallery.back_references", "is_class": False, "is_explicit": False, } ], # Check `get_short_module_name` points to correct object. # Here, `matplotlib.pyplot.figure` (func) can be shortened to # `matplotlib.figure` (module) (but should not be) "plt.figure": [ { "name": "figure", "module": "matplotlib.pyplot", "module_short": "matplotlib.pyplot", "is_class": False, "is_explicit": False, } ], # Check we get a in a().b in `visit_Attribute` "DummyClass": [ { "name": "DummyClass", "module": "sphinx_gallery.back_references", "module_short": "sphinx_gallery.back_references", "is_class": False, "is_explicit": False, } ], } _, script_blocks = split_code_and_text_blocks(unicode_sample) ref_regex = sg._make_ref_regex() res = sg.identify_names(script_blocks, ref_regex) assert expected == res @pytest.mark.parametrize(("mock", "short_module"), [("same", "A"), ("diff", "A.B")]) def test_get_short_module_name(mock, short_module): """Check `_get_short_module_name` correctly finds shortest module.""" if mock == "same": mock_mod_1 = mock_mod_2 = MagicMock() else: mock_mod_1 = MagicMock() mock_mod_2 = MagicMock() # Mock will return whatever object/attr we ask of it # When mock objects are the same, the `obj_name` from "A" is the same as # `obj_name` from "A.B", so "A" is an accepted short module with patch.dict(sys.modules, {"A": mock_mod_1, "A.B": mock_mod_2}): short_mod = sg._get_short_module_name("A.B", "C") short_mod_with_attr = sg._get_short_module_name("A.B", "C.D") assert short_mod == short_mod_with_attr == short_module # Deleting desired attr will cause failure of `hasattr` check (we # check this to ensure it is the right class) del mock_mod_2.C.D short_mod_no_attr = sg._get_short_module_name("A.B", "C.D") assert short_mod_no_attr is None def test_identify_names_implicit(tmpdir, gallery_conf): """Test implicit name identification.""" code_str = b""" ''' Title ----- This is an example. ''' # -*- coding: utf-8 -*- # \xc3\x9f from a.b import c import d as e import h.i print(c) e.HelloWorld().f.g h.i.j() """ expected = { "c": [ { "name": "c", "module": "a.b", "module_short": "a.b", "is_class": False, "is_explicit": False, } ], "e.HelloWorld": [ { "name": "HelloWorld", "module": "d", "module_short": "d", "is_class": False, "is_explicit": False, } ], "h.i.j": [ { "name": "j", "module": "h.i", "module_short": "h.i", "is_class": False, "is_explicit": False, } ], } fname = tmpdir.join("identify_names.py") fname.write(code_str, "wb") _, script_blocks = split_code_and_text_blocks(fname.strpath) ref_regex = sg._make_ref_regex() res = sg.identify_names(script_blocks, ref_regex) assert expected == res cobj = dict(module="m", module_short="m", name="n", is_class=False, is_explicit=True) @pytest.mark.parametrize( "text, default_role, ref, cobj", [ (":func:`m.n`", None, "m.n", cobj), (":func:`~m.n`", "obj", "m.n", cobj), (":func:`Title `", None, "m.n", cobj), (":func:`!m.n` or `!t <~m.n>`", None, None, None), ("`m.n`", "obj", "m.n", cobj), ("`m.n`", None, None, None), # see comment below (":ref:`m.n`", None, None, None), ("`m.n`", "ref", None, None), ("``literal``", "obj", None, None), ], ids=[ "regular", "show only last component", "with title", "no link for !", "default_role obj", "no default_role", # see comment below "non-python role", "non-python default_role", "literal", ], ) # the sphinx default value for default_role is None = no change, the docutils # default is title-reference (set by the default-role directive), see # www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role # and docutils.sourceforge.io/docs/ref/rst/roles.html def test_identify_names_explicit(text, default_role, ref, cobj, gallery_conf): """Test explicit name identification.""" default_role = "" if default_role is None else default_role script_blocks = [("text", text, 1)] expected = {ref: [cobj]} if ref else {} ref_regex = sg._make_ref_regex(default_role) actual = sg.identify_names(script_blocks, ref_regex) assert expected == actual sphinx-gallery-0.16.0/sphinx_gallery/tests/test_block_parser.py000066400000000000000000000146351461331107500250270ustar00rootroot00000000000000"""test BlockParser.""" import os import pytest import tempfile from textwrap import dedent from sphinx_gallery.block_parser import BlockParser from sphinx_gallery.gen_gallery import DEFAULT_GALLERY_CONF CXX_BODY = dedent( """ int x = 3; return; """ ) @pytest.mark.parametrize( "comment, expected_docstring", [ pytest.param( dedent( """\ // Title // ===== // // description """ ), dedent( """\ Title ===== description """ ), id="single-line style", ), # A simple multiline header pytest.param( dedent( """\ /* Title ===== */ """ ), dedent( """\ Title ===== """ ), id="simple multiline", ), # A multiline comment with aligned decorations on intermediate lines pytest.param( dedent( """\ /* * Title * ===== */ """ ), dedent( """\ Title ===== """ ), id="decorated-multiline", ), # A multiline comment that starts on the same line as the start symbol pytest.param( dedent( """\ /* Title * ===== * */ """ ), dedent( """\ Title ===== """ ), id="early-multiline", ), ], ) def test_cxx_titles(comment, expected_docstring): doc = comment + CXX_BODY parser = BlockParser("*.cpp", DEFAULT_GALLERY_CONF) file_conf, blocks, _ = parser._split_content(doc) assert len(blocks) == 2 assert blocks[0][0] == "text" assert blocks[0][1] == expected_docstring @pytest.mark.parametrize( "filetype, title, special, expected", [ pytest.param( "*.py", """# Title""", "# %%\n# A simple comment\n# with two lines", "A simple comment\nwith two lines\n", id="simple", ), pytest.param( "*.f90", """! Title""", " !%%\n ! Indented single-line comment", "Indented single-line comment\n", id="single-line-indented", ), pytest.param( "*.cpp", """// Title""", ( " // %%\n" " // First comment line\n" " // Second comment line\n" ), "First comment line\nSecond comment line\n", id="indented-separate-sentinel", ), pytest.param( "*.cs", """// Title""", ( " //////////////////////////////\n" " // Indented multi-line comment\n" " //\n" " // with a blank line\n" ), "Indented multi-line comment\n\nwith a blank line\n", id="block-two-paragraphs", ), pytest.param( "*.c", """/* Title */""", ( " /* %% \n" " Indented multi-line comment\n" " continued on a second line */\n" ), "Indented multi-line comment\ncontinued on a second line\n", id="multiline-block-comment", ), pytest.param( "*.c", """/* Title */""", ( " /*%%\n" " * * List item\n" " * * Another item\n" " * * Item 3\n" " */" ), ("* List item\n* Another item\n* Item 3\n"), id="multiline-comment-short-form", ), pytest.param( "*.jl", "# Title", ( " #=%%\n" " * list item 1\n" " * list item 2\n" " * subitem\n" " =#" ), "* list item 1\n* list item 2\n * subitem\n", id="julia-multiline", ), pytest.param( "*.m", "% Title", "%% Heading\n% Extended description", "Heading\n-------\n\nExtended description\n", id="matlab-autoheading", ), ], ) def test_rst_blocks(filetype, title, special, expected): doc = f"{title}\n{CXX_BODY}\n\n{special}\n{CXX_BODY}" gallery_conf = DEFAULT_GALLERY_CONF.copy() gallery_conf["filetype_parsers"] = {".m": "Matlab"} parser = BlockParser(filetype, gallery_conf) file_conf, blocks, _ = parser._split_content(doc) assert len(blocks) == 4 assert blocks[2][0] == "text" assert blocks[2][1] == expected def test_cpp_file_to_rst(): CODE = """\ // Do stuff // -------- int main(int argc, char** argv) { //%% First heading //This is the start of ``main`` // This is just a comment because of the preceding blank line int y = 4; // sphinx_gallery_start_ignore y = 5; // don't look: this is a secret! // sphinx_gallery_end_ignore if (y == 4) { return 1; } // sphinx_gallery_foobar = 14 } """ with tempfile.NamedTemporaryFile("wb", suffix=".cpp", delete=False) as f: f.write(CODE.encode()) try: parser = BlockParser(f.name, DEFAULT_GALLERY_CONF) file_conf, blocks, _ = parser.split_code_and_text_blocks(f.name) finally: os.remove(f.name) assert parser.language == "C++" assert file_conf == {"foobar": 14} assert "First heading\n-------------\n\nThis is the start" in blocks[2][1] assert "// This is just a comment" in blocks[3][1] assert "secret" in blocks[3][1] assert "sphinx_gallery_foobar" in blocks[3][1] assert "sphinx_gallery_foobar" not in parser.remove_config_comments(blocks[3][1]) cleaned = parser.remove_ignore_blocks(blocks[3][1]) assert "secret" not in cleaned assert "y == 4" in cleaned sphinx-gallery-0.16.0/sphinx_gallery/tests/test_docs_resolv.py000066400000000000000000000052251461331107500246760ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Testing the rst files generator.""" import os import tempfile import sys import pytest from sphinx.errors import ExtensionError import sphinx_gallery.docs_resolv as sg def test_embed_code_links_get_data(): """Test that we can get data for code links.""" sg._get_data("https://numpy.org/doc/1.18/reference") sg._get_data("http://scikit-learn.org/stable/") # GZip def test_shelve(tmpdir): """Test if shelve can cache and retrieve data after file is deleted.""" test_string = "test information" tmp_cache = str(tmpdir) with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(test_string) try: # recovers data from temporary file and caches it in the shelve file_data = sg.get_data(f.name, tmp_cache) finally: os.remove(f.name) # tests recovered data matches assert file_data == test_string # test if cached data is available after temporary file has vanished assert sg.get_data(f.name, tmp_cache) == test_string # shelve keys need to be str in python 2, deal with unicode input if sys.version_info[0] == 2: assert sg.get_data(f.name, tmp_cache) == test_string def test_parse_sphinx_docopts(): data = """ """ assert sg.parse_sphinx_docopts(data) == { "URL_ROOT": "./", "VERSION": "2.0.2", "COLLAPSE_INDEX": False, "FILE_SUFFIX": ".html", "HAS_SOURCE": True, "SOURCELINK_SUFFIX": ".txt", } data_sphinx_175 = """ """ assert sg.parse_sphinx_docopts(data_sphinx_175) == { "VERSION": "2.0.2", "COLLAPSE_INDEX": False, "FILE_SUFFIX": ".html", "HAS_SOURCE": True, "SOURCELINK_SUFFIX": ".txt", } with pytest.raises(ExtensionError): sg.parse_sphinx_docopts("empty input") with pytest.raises(ExtensionError): sg.parse_sphinx_docopts("DOCUMENTATION_OPTIONS = ") with pytest.raises(ExtensionError): sg.parse_sphinx_docopts("DOCUMENTATION_OPTIONS = {") sphinx-gallery-0.16.0/sphinx_gallery/tests/test_full.py000066400000000000000000001466071461331107500233300ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Test the SG pipeline used with Sphinx and tinybuild.""" import codecs from io import StringIO import os import os.path as op from pathlib import Path import re import shutil import sys import time import glob import json import lxml.html from packaging.version import Version from sphinx import __version__ as sphinx_version from sphinx.application import Sphinx from sphinx.errors import ExtensionError from sphinx.util.docutils import docutils_namespace from sphinx_gallery.utils import ( _get_image, scale_image, _has_optipng, _has_pypandoc, _has_graphviz, ) import pytest # file inventory for tinybuild: # total number of plot_*.py files in tinybuild/examples + examples_rst_index # + examples_with_rst N_EXAMPLES = 15 + 3 + 2 N_FAILING = 2 N_GOOD = N_EXAMPLES - N_FAILING # galleries that run w/o error # passthroughs and non-executed examples in examples + examples_rst_index # + examples_with_rst N_PASS = 3 + 0 + 2 # indices SG generates (extra non-plot*.py file) # + examples_rst_index + examples_with_rst N_INDEX = 2 + 1 + 3 # SG execution times (examples + examples_rst_index + examples_with_rst + root-level) N_EXECUTE = 2 + 3 + 1 + 1 # gen_modules + sg_api_usage + doc/index.rst + minigallery.rst N_OTHER = 9 + 1 + 1 + 1 + 1 N_RST = N_EXAMPLES + N_PASS + N_INDEX + N_EXECUTE + N_OTHER N_RST = f"({N_RST}|{N_RST - 1}|{N_RST - 2})" # AppVeyor weirdness pytest.importorskip("jupyterlite_sphinx") # needed for tinybuild @pytest.fixture(scope="module") def sphinx_app(tmpdir_factory, req_mpl, req_pil): return _sphinx_app(tmpdir_factory, "html") @pytest.fixture(scope="module") def sphinx_dirhtml_app(tmpdir_factory, req_mpl, req_pil): return _sphinx_app(tmpdir_factory, "dirhtml") def _sphinx_app(tmpdir_factory, buildername): # Skip if numpy not installed pytest.importorskip("numpy") temp_dir = (tmpdir_factory.getbasetemp() / f"root_{buildername}").strpath src_dir = op.join(op.dirname(__file__), "tinybuild") def ignore(src, names): return ("_build", "gen_modules", "auto_examples") shutil.copytree(src_dir, temp_dir, ignore=ignore) # For testing iteration, you can get similar behavior just doing `make` # inside the tinybuild/doc directory conf_dir = op.join(temp_dir, "doc") out_dir = op.join(conf_dir, "_build", buildername) toctrees_dir = op.join(conf_dir, "_build", "toctrees") # Avoid warnings about re-registration, see: # https://github.com/sphinx-doc/sphinx/issues/5038 with docutils_namespace(): app = Sphinx( conf_dir, conf_dir, out_dir, toctrees_dir, buildername=buildername, status=StringIO(), warning=StringIO(), ) # need to build within the context manager # for automodule and backrefs to work app.build(False, []) return app def test_timings(sphinx_app): """Test that a timings page is created.""" out_dir = sphinx_app.outdir src_dir = sphinx_app.srcdir # local folder timings_rst = op.join(src_dir, "auto_examples", "sg_execution_times.rst") assert op.isfile(timings_rst) with codecs.open(timings_rst, "r", "utf-8") as fid: content = fid.read() assert ":ref:`sphx_glr_auto_examples_plot_numpy_matplotlib.py`" in content parenthetical = "(``plot_numpy_matplotlib.py``)" assert parenthetical in content # HTML output timings_html = op.join(out_dir, "auto_examples", "sg_execution_times.html") assert op.isfile(timings_html) with codecs.open(timings_html, "r", "utf-8") as fid: content = fid.read() assert 'href="plot_numpy_matplotlib.html' in content # printed status = sphinx_app._status.getvalue() fname = op.join("..", "examples", "plot_numpy_matplotlib.py") assert f"- {fname}: " in status def test_api_usage(sphinx_app): """Test that an api usage page is created.""" out_dir = sphinx_app.outdir src_dir = sphinx_app.srcdir # the rst file was empty but is removed in post-processing api_rst = op.join(src_dir, "sg_api_usage.rst") assert not op.isfile(api_rst) # HTML output api_html = op.join(out_dir, "sg_api_usage.html") assert op.isfile(api_html) with codecs.open(api_html, "r", "utf-8") as fid: content = fid.read() has_graphviz = _has_graphviz() # spot check references assert ( 'href="gen_modules/sphinx_gallery.gen_gallery.html' '#sphinx_gallery.gen_gallery.setup"' ) in content # check used and unused if has_graphviz: assert 'alt="API unused entries graph"' in content if sphinx_app.config.sphinx_gallery_conf["show_api_usage"]: assert 'alt="sphinx_gallery usage graph"' in content else: assert 'alt="sphinx_gallery usage graph"' not in content # check graph output assert 'src="_images/graphviz-' in content else: assert 'alt="API unused entries graph"' not in content assert 'alt="sphinx_gallery usage graph"' not in content # printed status = sphinx_app._status.getvalue() fname = op.join("..", "examples", "plot_numpy_matplotlib.py") assert f"- {fname}: " in status def test_optipng(sphinx_app): """Test that optipng is detected.""" status = sphinx_app._status.getvalue() w = sphinx_app._warning.getvalue() substr = "will not be optimized" if _has_optipng(): assert substr not in w else: assert substr in w assert "optipng version" not in status.lower() # catch the --version def test_junit(sphinx_app, tmpdir): out_dir = sphinx_app.outdir junit_file = op.join(out_dir, "sphinx-gallery", "junit-results.xml") assert op.isfile(junit_file) with codecs.open(junit_file, "r", "utf-8") as fid: contents = fid.read() assert contents.startswith(" 0.99 def test_negative_thumbnail_config(sphinx_app, tmpdir): """Test 'sphinx_gallery_thumbnail_number' config correct for negative numbers.""" import numpy as np # Make sure our thumbnail is the 2nd (last) image fname_orig = op.join( sphinx_app.outdir, "_images", "sphx_glr_plot_matplotlib_alt_002.png" ) fname_thumb = op.join( sphinx_app.outdir, "_images", "sphx_glr_plot_matplotlib_alt_thumb.png" ) fname_new = str(tmpdir.join("new.png")) scale_image( fname_orig, fname_new, *sphinx_app.config.sphinx_gallery_conf["thumbnail_size"] ) Image = _get_image() orig = np.asarray(Image.open(fname_thumb)) new = np.asarray(Image.open(fname_new)) assert new.shape[:2] == orig.shape[:2] assert new.shape[2] in (3, 4) # optipng can strip the alpha channel corr = np.corrcoef(new[..., :3].ravel(), orig[..., :3].ravel())[0, 1] assert corr > 0.99 def test_command_line_args_img(sphinx_app): generated_examples_dir = op.join(sphinx_app.outdir, "auto_examples") thumb_fname = "../_images/sphx_glr_plot_command_line_args_thumb.png" file_fname = op.join(generated_examples_dir, thumb_fname) assert op.isfile(file_fname), file_fname def test_image_formats(sphinx_app): """Test Image format support.""" generated_examples_dir = op.join(sphinx_app.outdir, "auto_examples") generated_examples_index = op.join(generated_examples_dir, "index.html") with codecs.open(generated_examples_index, "r", "utf-8") as fid: html = fid.read() thumb_fnames = [ "../_images/sphx_glr_plot_svg_thumb.svg", "../_images/sphx_glr_plot_numpy_matplotlib_thumb.png", "../_images/sphx_glr_plot_animation_thumb.gif", "../_images/sphx_glr_plot_webp_thumb.webp", ] for thumb_fname in thumb_fnames: file_fname = op.join(generated_examples_dir, thumb_fname) assert op.isfile(file_fname), file_fname want_html = f'src="{thumb_fname}"' assert want_html in html # the original GIF does not get copied because it's not used in the # reST/HTML, so can't add it to this check for ex, ext, nums, extra in ( ("plot_svg", "svg", [1], None), ("plot_numpy_matplotlib", "png", [1], None), ("plot_animation", "png", [1, 3], "function Animation"), ("plot_webp", "webp", [1], None), ): html_fname = op.join(generated_examples_dir, f"{ex}.html") with codecs.open(html_fname, "r", "utf-8") as fid: html = fid.read() for num in nums: img_fname0 = f"../_images/sphx_glr_{ex}_{num:03}.{ext}" file_fname = op.join(generated_examples_dir, img_fname0) assert op.isfile(file_fname), file_fname want_html = f'src="{img_fname0}"' assert want_html in html img_fname2 = f"../_images/sphx_glr_{ex}_{num:03}_2_00x.{ext}" file_fname2 = op.join(generated_examples_dir, img_fname2) want_html = f'srcset="{img_fname0}, {img_fname2} 2.00x"' if ext in ("png", "jpg", "svg", "webp"): # check 2.00x (tests directive) assert op.isfile(file_fname2), file_fname2 assert want_html in html if extra is not None: assert extra in html def test_repr_html_classes(sphinx_app): """Test appropriate _repr_html_ classes.""" example_file = op.join(sphinx_app.outdir, "auto_examples", "plot_repr.html") with codecs.open(example_file, "r", "utf-8") as fid: lines = fid.read() assert 'div class="output_subarea output_html rendered_html output_result"' in lines assert "gallery-rendered-html.css" in lines def test_embed_links_and_styles(sphinx_app): """Test that links and styles are embedded properly in doc.""" out_dir = sphinx_app.outdir src_dir = sphinx_app.srcdir examples_dir = op.join(out_dir, "auto_examples") assert op.isdir(examples_dir) example_files = os.listdir(examples_dir) assert "plot_numpy_matplotlib.html" in example_files example_file = op.join(examples_dir, "plot_numpy_matplotlib.html") with codecs.open(example_file, "r", "utf-8") as fid: lines = fid.read() # ensure we've linked properly assert "#module-matplotlib.colors" in lines assert "matplotlib.colors.is_color_like" in lines assert ( 'class="sphx-glr-backref-module-matplotlib-colors sphx-glr-backref-type-py-function">' in lines ) # noqa: E501 assert "#module-numpy" in lines assert "numpy.arange.html" in lines assert ( 'class="sphx-glr-backref-module-numpy sphx-glr-backref-type-py-function">' in lines ) # noqa: E501 assert "#module-matplotlib.pyplot" in lines assert "pyplot.html" in lines or "pyplot_summary.html" in lines assert ".html#matplotlib.figure.Figure.tight_layout" in lines assert "matplotlib.axes.Axes.plot.html#matplotlib.axes.Axes.plot" in lines assert "matplotlib_configuration_api.html#matplotlib.RcParams" in lines assert "stdtypes.html#list" in lines assert "warnings.html#warnings.warn" in lines assert "itertools.html#itertools.compress" in lines assert "numpy.ndarray.html" in lines # see issue 617 id_names = re.search( r"sphinx_gallery.backreferences.html#sphinx[_,-]gallery[.,-]backreferences[.,-]identify[_,-]names", # noqa: E501 lines, ) assert id_names is not None # instances have an extra CSS class assert ( 'class="sphx-glr-backref-module-matplotlib-figure sphx-glr-backref-type-py-class sphx-glr-backref-instance">x' in lines ) # noqa: E501 assert ( 'class="sphx-glr-backref-module-matplotlib-figure sphx-glr-backref-type-py-class">Figure' in lines ) # noqa: E501 # gh-587: no classes that are only marked as module without type assert re.search(r'"sphx-glr-backref-module-\S*"', lines) is None assert ( 'class="sphx-glr-backref-module-sphinx_gallery-backreferences sphx-glr-backref-type-py-function">sphinx_gallery.backreferences.identify_names' in lines ) # noqa: E501 # gh-587: np.random.RandomState links properly # NumPy has had this linked as numpy.random.RandomState and # numpy.random.mtrand.RandomState so we need regex... assert ( re.search( r'\.html#numpy\.random\.(mtrand\.?)?RandomState" title="numpy\.random\.(mtrand\.?)?RandomState" class="sphx-glr-backref-module-numpy-random(-mtrand?)? sphx-glr-backref-type-py-class">np', lines, ) is not None ) # noqa: E501 assert ( re.search( r'\.html#numpy\.random\.(mtrand\.?)?RandomState" title="numpy\.random\.(mtrand\.?)?RandomState" class="sphx-glr-backref-module-numpy-random(-mtrand?)? sphx-glr-backref-type-py-class sphx-glr-backref-instance">rng', lines, ) is not None ) # noqa: E501 # gh-587: methods of classes in the module currently being documented # issue 617 (regex '-'s) # instance dummy_class_inst = re.search( r'sphinx_gallery.backreferences.html#sphinx[_,-]gallery[.,-]backreferences[.,-][D,d]ummy[C,c]lass" title="sphinx_gallery.backreferences.DummyClass" class="sphx-glr-backref-module-sphinx_gallery-backreferences sphx-glr-backref-type-py-class sphx-glr-backref-instance">dc', # noqa: E501 lines, ) assert dummy_class_inst is not None # class dummy_class_class = re.search( r'sphinx_gallery.backreferences.html#sphinx[_,-]gallery[.,-]backreferences[.,-][D,d]ummy[C,c]lass" title="sphinx_gallery.backreferences.DummyClass" class="sphx-glr-backref-module-sphinx_gallery-backreferences sphx-glr-backref-type-py-class">sphinx_gallery.backreferences.DummyClass', # noqa: E501 lines, ) assert dummy_class_class is not None # method dummy_class_meth = re.search( r'sphinx_gallery.backreferences.html#sphinx[_,-]gallery[.,-]backreferences[.,-][D,d]ummy[C,c]lass[.,-]run" title="sphinx_gallery.backreferences.DummyClass.run" class="sphx-glr-backref-module-sphinx_gallery-backreferences sphx-glr-backref-type-py-method">dc.run', # noqa: E501 lines, ) assert dummy_class_meth is not None # property (Sphinx 2+ calls it a method rather than attribute, so regex) dummy_class_prop = re.compile( r'sphinx_gallery.backreferences.html#sphinx[_,-]gallery[.,-]backreferences[.,-][D,d]ummy[C,c]lass[.,-]prop" title="sphinx_gallery.backreferences.DummyClass.prop" class="sphx-glr-backref-module-sphinx_gallery-backreferences sphx-glr-backref-type-py-(attribute|method|property)">dc.prop' ) # noqa: E501 assert dummy_class_prop.search(lines) is not None try: import memory_profiler # noqa: F401 except ImportError: assert "memory usage" not in lines else: assert "memory usage" in lines # CSS styles assert 'class="sphx-glr-signature"' in lines assert 'class="sphx-glr-timing"' in lines for kind in ("python", "jupyter"): assert ( f'class="sphx-glr-download sphx-glr-download-{kind} docutils container"' in lines ) # noqa:E501 # highlight language fname = op.join(src_dir, "auto_examples", "plot_numpy_matplotlib.rst") assert op.isfile(fname) with codecs.open(fname, "r", "utf-8") as fid: rst = fid.read() assert ".. code-block:: Python\n" in rst # warnings want_warn = ( r".*plot_numpy_matplotlib\.py:[0-9][0-9]: RuntimeWarning: " r"This warning should show up in the output.*" ) assert re.match(want_warn, lines, re.DOTALL) is not None sys.stdout.write(lines) example_file = op.join(examples_dir, "plot_pickle.html") with codecs.open(example_file, "r", "utf-8") as fid: lines = fid.read() assert "joblib.Parallel.html" in lines def test_backreferences(sphinx_app): """Test backreferences.""" out_dir = sphinx_app.outdir mod_file = op.join(out_dir, "gen_modules", "sphinx_gallery.sorting.html") with codecs.open(mod_file, "r", "utf-8") as fid: lines = fid.read() assert "ExplicitOrder" in lines # in API doc assert "plot_second_future_imports.html" in lines # backref via code use assert "FileNameSortKey" in lines # in API doc assert "plot_numpy_matplotlib.html" in lines # backref via :class: in str mod_file = op.join(out_dir, "gen_modules", "sphinx_gallery.backreferences.html") with codecs.open(mod_file, "r", "utf-8") as fid: lines = fid.read() assert "NameFinder" in lines # in API doc assert "plot_future_imports.html" in lines # backref via doc block # rendered file html = op.join(out_dir, "auto_examples", "plot_second_future_imports.html") assert op.isfile(html) with codecs.open(html, "r", "utf-8") as fid: html = fid.read() assert "sphinx_gallery.sorting.html#sphinx_gallery.sorting.ExplicitOrder" in html # noqa: E501 assert "sphinx_gallery.scrapers.html#sphinx_gallery.scrapers.clean_modules" in html # noqa: E501 assert "figure_rst.html" not in html # excluded @pytest.mark.parametrize( "rst_file, example_used_in", [ pytest.param( "sphinx_gallery.backreferences.identify_names.examples", "plot_numpy_matplotlib", id="identify_names", ), pytest.param( "sphinx_gallery.sorting.ExplicitOrder.examples", "plot_second_future_imports", id="ExplicitOrder", ), ], ) def test_backreferences_examples_rst(sphinx_app, rst_file, example_used_in): """Test linking to mini-galleries using backreferences_dir.""" backref_dir = sphinx_app.srcdir examples_rst = op.join(backref_dir, "gen_modules", "backreferences", rst_file) with codecs.open(examples_rst, "r", "utf-8") as fid: lines = fid.read() assert example_used_in in lines # check the .. raw:: html div count n_open = lines.count("= Version("4.1") for orig, new in zip(list_orig, list_new): check_name = op.splitext(op.basename(orig))[0] if check_name.endswith("_codeobj"): check_name = check_name[:-8] if check_name in different: if good_sphinx: assert np.abs(op.getmtime(orig) - op.getmtime(new)) > 0.1 elif check_name not in ignore: assert_allclose( op.getmtime(orig), op.getmtime(new), atol=1e-3, rtol=1e-20, err_msg=op.basename(orig), ) def test_rebuild(tmpdir_factory, sphinx_app): """Test examples that haven't been changed aren't run twice.""" # First run completes in the fixture. status = sphinx_app._status.getvalue() lines = [line for line in status.split("\n") if "removed" in line] want = f".*{N_RST} added, 0 changed, 0 removed.*" assert re.match(want, status, re.MULTILINE | re.DOTALL) is not None, lines want = ".*targets for [2-3] source files that are out of date$.*" lines = [line for line in status.split("\n") if "out of date" in line] assert re.match(want, status, re.MULTILINE | re.DOTALL) is not None, lines lines = [line for line in status.split("\n") if "on MD5" in line] want = ".*executed %d out of %d.*after excluding 0 files.*based on MD5.*" % ( N_GOOD, N_EXAMPLES, ) assert re.match(want, status, re.MULTILINE | re.DOTALL) is not None, lines old_src_dir = (tmpdir_factory.getbasetemp() / "root_old").strpath shutil.copytree(sphinx_app.srcdir, old_src_dir) generated_modules_0 = sorted( op.join(old_src_dir, "gen_modules", f) for f in os.listdir(op.join(old_src_dir, "gen_modules")) if op.isfile(op.join(old_src_dir, "gen_modules", f)) ) generated_backrefs_0 = sorted( op.join(old_src_dir, "gen_modules", "backreferences", f) for f in os.listdir(op.join(old_src_dir, "gen_modules", "backreferences")) ) generated_rst_0 = sorted( op.join(old_src_dir, "auto_examples", f) for f in os.listdir(op.join(old_src_dir, "auto_examples")) if f.endswith(".rst") ) generated_pickle_0 = sorted( op.join(old_src_dir, "auto_examples", f) for f in os.listdir(op.join(old_src_dir, "auto_examples")) if f.endswith(".pickle") ) copied_py_0 = sorted( op.join(old_src_dir, "auto_examples", f) for f in os.listdir(op.join(old_src_dir, "auto_examples")) if f.endswith(".py") ) copied_ipy_0 = sorted( op.join(old_src_dir, "auto_examples", f) for f in os.listdir(op.join(old_src_dir, "auto_examples")) if f.endswith(".ipynb") ) assert len(generated_modules_0) > 0 assert len(generated_backrefs_0) > 0 assert len(generated_rst_0) > 0 assert len(generated_pickle_0) > 0 assert len(copied_py_0) > 0 assert len(copied_ipy_0) > 0 assert len(sphinx_app.config.sphinx_gallery_conf["stale_examples"]) == 0 assert op.isfile( op.join(sphinx_app.outdir, "_images", "sphx_glr_plot_numpy_matplotlib_001.png") ) # # run a second time, no files should be updated # src_dir = sphinx_app.srcdir del sphinx_app # don't accidentally use it below conf_dir = src_dir out_dir = op.join(src_dir, "_build", "html") toctrees_dir = op.join(src_dir, "_build", "toctrees") time.sleep(0.1) with docutils_namespace(): new_app = Sphinx( src_dir, conf_dir, out_dir, toctrees_dir, buildername="html", status=StringIO(), ) new_app.build(False, []) status = new_app._status.getvalue() lines = [line for line in status.split("\n") if "0 removed" in line] # XXX on Windows this can be more if sys.platform.startswith("win"): assert ( re.match( ".*[0|1] added, ([1-9]|1[0-4]) changed, 0 removed$.*", status, re.MULTILINE | re.DOTALL, ) is not None ), lines else: assert ( re.match( ".*[0|1] added, ([1-9]|10) changed, 0 removed$.*", status, re.MULTILINE | re.DOTALL, ) is not None ), lines want = ".*executed 0 out of %s.*after excluding %s files.*based on MD5.*" % ( N_FAILING, N_GOOD, ) assert re.match(want, status, re.MULTILINE | re.DOTALL) is not None n_stale = len(new_app.config.sphinx_gallery_conf["stale_examples"]) assert n_stale == N_GOOD assert op.isfile( op.join(new_app.outdir, "_images", "sphx_glr_plot_numpy_matplotlib_001.png") ) generated_modules_1 = sorted( op.join(new_app.srcdir, "gen_modules", f) for f in os.listdir(op.join(new_app.srcdir, "gen_modules")) if op.isfile(op.join(new_app.srcdir, "gen_modules", f)) ) generated_backrefs_1 = sorted( op.join(new_app.srcdir, "gen_modules", "backreferences", f) for f in os.listdir(op.join(new_app.srcdir, "gen_modules", "backreferences")) ) generated_rst_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".rst") ) generated_pickle_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".pickle") ) copied_py_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".py") ) copied_ipy_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".ipynb") ) # mtimes for modules _assert_mtimes(generated_modules_0, generated_modules_1) # mtimes for backrefs (gh-394) _assert_mtimes(generated_backrefs_0, generated_backrefs_1) # generated reST files ignore = ( # these two should almost always be different, but in case we # get extremely unlucky and have identical run times # on the one script that gets re-run (because it's a fail)... "sg_execution_times", "sg_api_usage", "plot_future_imports_broken", "plot_scraper_broken", ) _assert_mtimes(generated_rst_0, generated_rst_1, ignore=ignore) # mtimes for pickles _assert_mtimes(generated_pickle_0, generated_pickle_1) # mtimes for .py files (gh-395) _assert_mtimes(copied_py_0, copied_py_1) # mtimes for .ipynb files _assert_mtimes(copied_ipy_0, copied_ipy_1) # # run a third and a fourth time, changing one file or running one stale # for how in ("run_stale", "modify"): # modify must be last as this rerun setting tries to run the # broken example (subsequent tests depend on it) _rerun( how, src_dir, conf_dir, out_dir, toctrees_dir, generated_modules_0, generated_backrefs_0, generated_rst_0, generated_pickle_0, copied_py_0, copied_ipy_0, ) def _rerun( how, src_dir, conf_dir, out_dir, toctrees_dir, generated_modules_0, generated_backrefs_0, generated_rst_0, generated_pickle_0, copied_py_0, copied_ipy_0, ): """Rerun the sphinx build and check that the right files were changed.""" time.sleep(0.1) confoverrides = dict() if how == "modify": fname = op.join(src_dir, "../examples", "plot_numpy_matplotlib.py") with codecs.open(fname, "r", "utf-8") as fid: lines = fid.readlines() with codecs.open(fname, "w", "utf-8") as fid: for line in lines: # Make a tiny change that won't affect the recommender if "FYI this" in line: line = line.replace("FYI this", "FYA this") fid.write(line) out_of, excluding = N_FAILING + 1, N_GOOD - 1 n_stale = N_GOOD - 1 else: assert how == "run_stale" confoverrides["sphinx_gallery_conf.run_stale_examples"] = "True" confoverrides["sphinx_gallery_conf.filename_pattern"] = "plot_numpy_ma" out_of, excluding = 1, 0 n_stale = 0 with docutils_namespace(): new_app = Sphinx( src_dir, conf_dir, out_dir, toctrees_dir, buildername="html", status=StringIO(), confoverrides=confoverrides, ) new_app.build(False, []) status = new_app._status.getvalue() lines = [line for line in status.split("\n") if "source files that" in line] lines = "\n".join([how] + lines) flags = re.MULTILINE | re.DOTALL # for some reason, setting "confoverrides" above causes Sphinx to show # all targets out of date, even though they haven't been modified... want = f".*targets for {N_RST} source files that are out of date$.*" assert re.match(want, status, flags) is not None, lines # ... but then later detects that only some have actually changed: lines = [line for line in status.split("\n") if "changed," in line] # Ones that can change on stale: # # - auto_examples/future/plot_future_imports_broken # - auto_examples/future/sg_execution_times # - auto_examples/plot_scraper_broken # - auto_examples/sg_execution_times # - auto_examples_rst_index/sg_execution_times # - auto_examples_with_rst/sg_execution_times # - sg_api_usage # - sg_execution_times # # Sometimes it's not all 8, for example when the execution time and # memory usage reported ends up being the same. # # Modifying an example then adds these two: # - auto_examples/index # - auto_examples/plot_numpy_matplotlib if how == "modify": n_ch = "([3-9]|10|11)" else: n_ch = "[1-9]" lines = "\n".join([f"\n{how} != {n_ch}:"] + lines) want = f".*updating environment:.*[0|1] added, {n_ch} changed, 0 removed.*" assert re.match(want, status, flags) is not None, lines want = ".*executed 1 out of %s.*after excluding %s files.*based on MD5.*" % ( out_of, excluding, ) assert re.match(want, status, flags) is not None got_stale = len(new_app.config.sphinx_gallery_conf["stale_examples"]) assert got_stale == n_stale assert op.isfile( op.join(new_app.outdir, "_images", "sphx_glr_plot_numpy_matplotlib_001.png") ) generated_modules_1 = sorted( op.join(new_app.srcdir, "gen_modules", f) for f in os.listdir(op.join(new_app.srcdir, "gen_modules")) if op.isfile(op.join(new_app.srcdir, "gen_modules", f)) ) generated_backrefs_1 = sorted( op.join(new_app.srcdir, "gen_modules", "backreferences", f) for f in os.listdir(op.join(new_app.srcdir, "gen_modules", "backreferences")) ) generated_rst_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".rst") ) generated_pickle_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".pickle") ) copied_py_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".py") ) copied_ipy_1 = sorted( op.join(new_app.srcdir, "auto_examples", f) for f in os.listdir(op.join(new_app.srcdir, "auto_examples")) if f.endswith(".ipynb") ) # mtimes for modules _assert_mtimes(generated_modules_0, generated_modules_1) # mtimes for backrefs (gh-394) _assert_mtimes(generated_backrefs_0, generated_backrefs_1) # generated reST files different = ("plot_numpy_matplotlib",) ignore = ( # this one should almost always be different, but in case we # get extremely unlucky and have identical run times # on the one script above that changes... "sg_execution_times", "sg_api_usage", # this one will not change even though it was retried "plot_future_imports_broken", "plot_scraper_broken", ) # not reliable on Windows and one Ubuntu run bad = sys.platform.startswith("win") or os.getenv("BAD_MTIME", "0") == "1" if not bad: _assert_mtimes(generated_rst_0, generated_rst_1, different, ignore) # mtimes for pickles use_different = () if how == "run_stale" else different _assert_mtimes(generated_pickle_0, generated_pickle_1, ignore=ignore) # mtimes for .py files (gh-395) _assert_mtimes(copied_py_0, copied_py_1, different=use_different) # mtimes for .ipynb files _assert_mtimes(copied_ipy_0, copied_ipy_1, different=use_different) @pytest.mark.parametrize( "name, want", [ pytest.param( "future/plot_future_imports_broken", ".*RuntimeError.*Forcing this example to fail on Python 3.*", id="future", ), pytest.param( "plot_scraper_broken", ".*ValueError.*zero-size array to reduction.*", id="scraper", ), ], ) def test_error_messages(sphinx_app, name, want): """Test that informative error messages are added.""" src_dir = Path(sphinx_app.srcdir) rst = (src_dir / "auto_examples" / (name + ".rst")).read_text("utf-8") assert re.match(want, rst, re.DOTALL) is not None @pytest.mark.parametrize( "name, want", [ pytest.param( "future/plot_future_imports_broken", ".*RuntimeError.*Forcing this example to fail on Python 3.*", id="future", ), pytest.param( "plot_scraper_broken", ".*ValueError.*zero-size array to reduction.*", id="scraper", ), ], ) def test_error_messages_dirhtml(sphinx_dirhtml_app, name, want): """Test that informative error messages are added.""" src_dir = sphinx_dirhtml_app.srcdir example_rst = op.join(src_dir, "auto_examples", name + ".rst") with codecs.open(example_rst, "r", "utf-8") as fid: rst = fid.read() rst = rst.replace("\n", " ") assert re.match(want, rst) is not None def test_alt_text_image(sphinx_app): """Test alt text for matplotlib images in html and rst.""" out_dir = sphinx_app.outdir src_dir = sphinx_app.srcdir # alt text is fig titles, rst example_rst = op.join(src_dir, "auto_examples", "plot_matplotlib_alt.rst") with codecs.open(example_rst, "r", "utf-8") as fid: rst = fid.read() # suptitle and axes titles assert ":alt: This is a sup title, subplot 1, subplot 2" in rst # multiple titles assert ":alt: Left Title, Center Title, Right Title" in rst # no fig title - alt text is file name, rst example_rst = op.join(src_dir, "auto_examples", "plot_numpy_matplotlib.rst") with codecs.open(example_rst, "r", "utf-8") as fid: rst = fid.read() assert ":alt: plot numpy matplotlib" in rst # html example_html = op.join(out_dir, "auto_examples", "plot_numpy_matplotlib.html") with codecs.open(example_html, "r", "utf-8") as fid: html = fid.read() assert 'alt="plot numpy matplotlib"' in html def test_alt_text_thumbnail(sphinx_app): """Test alt text for thumbnail in html and rst.""" out_dir = sphinx_app.outdir src_dir = sphinx_app.srcdir # check gallery index thumbnail, html generated_examples_index = op.join(out_dir, "auto_examples", "index.html") with codecs.open(generated_examples_index, "r", "utf-8") as fid: html = fid.read() assert 'alt=""' in html # check backreferences thumbnail, html backref_html = op.join(out_dir, "gen_modules", "sphinx_gallery.backreferences.html") with codecs.open(backref_html, "r", "utf-8") as fid: html = fid.read() assert 'alt=""' in html # check gallery index thumbnail, rst generated_examples_index = op.join(src_dir, "auto_examples", "index.rst") with codecs.open(generated_examples_index, "r", "utf-8") as fid: rst = fid.read() assert ":alt:" in rst def test_backreference_labels(sphinx_app): """Tests that backreference labels work.""" src_dir = sphinx_app.srcdir out_dir = sphinx_app.outdir # Test backreference label backref_rst = op.join(src_dir, "gen_modules", "sphinx_gallery.backreferences.rst") with codecs.open(backref_rst, "r", "utf-8") as fid: rst = fid.read() label = ".. _sphx_glr_backref_sphinx_gallery.backreferences.identify_names:" # noqa: E501 assert label in rst # Test html link index_html = op.join(out_dir, "index.html") with codecs.open(index_html, "r", "utf-8") as fid: html = fid.read() link = 'href="gen_modules/sphinx_gallery.backreferences.html#sphx-glr-backref-sphinx-gallery-backreferences-identify-names">' # noqa: E501 assert link in html @pytest.fixture(scope="module") def minigallery_tree(sphinx_app): out_dir = sphinx_app.outdir minigallery_html = op.join(out_dir, "minigallery.html") with codecs.open(minigallery_html, "r", "utf-8") as fid: tree = lxml.html.fromstring(fid.read()) names = tree.xpath('//p[starts-with(text(), "Test")]') divs = tree.find_class("sphx-glr-thumbnails") assert len(names) == len(divs) return {name.text_content(): div for name, div in zip(names, divs)} @pytest.mark.parametrize( "test, heading, sortkey", [ # first example, no heading ("Test 1-N", None, {"explicit"}), # first example, default heading, default level ( "Test 1-D-D", ("h2", "Examples using sphinx_gallery.sorting.ExplicitOrder"), {"explicit"}, ), # first example, default heading, custom level ( "Test 1-D-C", ("h3", "Examples using sphinx_gallery.sorting.ExplicitOrder"), {"explicit"}, ), # first example, custom heading, default level ("Test 1-C-D", ("h2", "This is a custom heading"), {"explicit"}), # both examples, no heading ("Test 2-N", None, {"explicit", "filename"}), # both examples, default heading, default level ( "Test 2-D-D", ("h2", "Examples using one of multiple objects"), {"explicit", "filename"}, ), # both examples, custom heading, custom level ( "Test 2-C-C", ("h1", "This is a different custom heading"), {"explicit", "filename"}, ), # filepath, no heading ("Test 1-F", None, {"path"}), # glob, no heading ("Test 2-F-G", None, {"glob"}), # all files ( "Test 3-F-G-B", ("h2", "All the input types", ""), {"path", "glob", "explicit", "filename"}, ), ("Test 1-F-R", None, ["plot_boo", "plot_cos"]), ("Test 1-S", None, ["plot_sub2", "plot_sub1"]), ("Test 3-N", None, {"path", "glob", "explicit", "filename"}), ], ) def test_minigallery_directive(minigallery_tree, test, heading, sortkey): """Tests the functionality of the minigallery directive.""" assert test in minigallery_tree text = minigallery_tree[test] assert text is not None heading_element = text.xpath( 'preceding-sibling::*[position()=1 and starts-with(name(), "h")]' ) # Check headings if heading: assert heading_element[0].tag == heading[0] assert heading_element[0].text_content().startswith(heading[1]) else: assert heading_element == [] if test in ["Test 1-F-R", "Test 1-S"]: img = text.xpath('descendant::img[starts-with(@src, "_images/sphx_glr")]') href = text.xpath('descendant::a[contains(@href, "rst")]') assert img and href for p, i, h in zip(sortkey, img, href): assert (p in i.values()[-1]) and (p in h.values()[-1]) else: examples = { "explicit": "plot_second_future_imports", "filename": "plot_numpy_matplotlib", "path": "plot_log", "glob": "plot_matplotlib_alt", } for key, fname in examples.items(): img = text.xpath( f'descendant::img[@src = "_images/sphx_glr_{fname}_thumb.png"]' ) href = text.xpath( f'descendant::a[starts-with(@href, "auto_examples/{fname}.html")]' ) if key in sortkey: assert img and href else: assert not (img or href) print(f"{test}: {lxml.html.tostring(text)}") def test_matplotlib_warning_filter(sphinx_app): """Test Matplotlib agg warning is removed.""" out_dir = sphinx_app.outdir example_html = op.join(out_dir, "auto_examples", "plot_matplotlib_alt.html") with codecs.open(example_html, "r", "utf-8") as fid: html = fid.read() warning = ( "Matplotlib is currently using agg, which is a" " non-GUI backend, so cannot show the figure." ) assert warning not in html warning = "is non-interactive, and thus cannot be shown" assert warning not in html def test_jupyter_notebook_pandoc(sphinx_app): """Test using pypandoc.""" src_dir = sphinx_app.srcdir fname = op.join(src_dir, "auto_examples", "plot_numpy_matplotlib.ipynb") with codecs.open(fname, "r", "utf-8") as fid: md = fid.read() md_sg = r"Use :mod:`sphinx_gallery` to link to other packages, like\n:mod:`numpy`, :mod:`matplotlib.colors`, and :mod:`matplotlib.pyplot`." # noqa md_pandoc = r"Use `sphinx_gallery`{.interpreted-text role=\"mod\"} to link to other\npackages, like `numpy`{.interpreted-text role=\"mod\"},\n`matplotlib.colors`{.interpreted-text role=\"mod\"}, and\n`matplotlib.pyplot`{.interpreted-text role=\"mod\"}." # noqa if any(_has_pypandoc()): assert md_pandoc in md else: assert md_sg in md def test_md5_hash(sphinx_app): """Test MD5 hashing.""" src_dir = sphinx_app.srcdir fname = op.join(src_dir, "auto_examples", "plot_log.py.md5") expected_md5 = "0edc2de97f96f3b55f8b4a21994931a8" with open(fname) as md5_file: actual_md5 = md5_file.read() assert actual_md5 == expected_md5 def test_interactive_example_logo_exists(sphinx_app): """Test that the binder logo path is correct.""" root = op.join(sphinx_app.outdir, "auto_examples") with codecs.open(op.join(root, "plot_svg.html"), "r", "utf-8") as fid: html = fid.read() path = re.match( r'.*Launch binder.*', html, re.DOTALL ) assert path is not None path = path.groups()[0] img_fname = op.abspath(op.join(root, path)) assert "binder_badge_logo" in img_fname # can have numbers appended assert op.isfile(img_fname) assert ( "https://mybinder.org/v2/gh/sphinx-gallery/sphinx-gallery.github.io/master?urlpath=lab/tree/notebooks/auto_examples/plot_svg.ipynb" in html ) # noqa: E501 path = re.match( r'.*Launch JupyterLite.*', html, re.DOTALL ) assert path is not None path = path.groups()[0] img_fname = op.abspath(op.join(root, path)) assert "jupyterlite_badge_logo" in img_fname # can have numbers appended assert op.isfile(img_fname) def test_download_and_interactive_note(sphinx_app): """Test text saying go to the end to download code or run the example.""" root = op.join(sphinx_app.outdir, "auto_examples") with codecs.open(op.join(root, "plot_svg.html"), "r", "utf-8") as fid: html = fid.read() pattern = ( r"to download the full example.+" r"in your browser via JupyterLite or Binder" ) assert re.search(pattern, html) def test_defer_figures(sphinx_app): """Test the deferring of figures.""" root = op.join(sphinx_app.outdir, "auto_examples") fname = op.join(root, "plot_defer_figures.html") with codecs.open(fname, "r", "utf-8") as fid: html = fid.read() # The example has two code blocks with plotting commands, but the first # block has the flag ``sphinx_gallery_defer_figures``. Thus, there should # be only one image, not two, in the output. assert "../_images/sphx_glr_plot_defer_figures_001.png" in html assert "../_images/sphx_glr_plot_defer_figures_002.png" not in html def test_no_dummy_image(sphinx_app): """Test sphinx_gallery_dummy_images NOT created when executable is True.""" img1 = op.join( sphinx_app.srcdir, "auto_examples", "images", "sphx_glr_plot_repr_001.png" ) img2 = op.join( sphinx_app.srcdir, "auto_examples", "images", "sphx_glr_plot_repr_002.png" ) assert not op.isfile(img1) assert not op.isfile(img2) def test_jupyterlite_modifications(sphinx_app): src_dir = sphinx_app.srcdir jupyterlite_notebook_pattern = op.join( src_dir, "jupyterlite_contents", "**", "*.ipynb" ) jupyterlite_notebook_filenames = glob.glob( jupyterlite_notebook_pattern, recursive=True ) for notebook_filename in jupyterlite_notebook_filenames: with open(notebook_filename) as f: notebook_content = json.load(f) first_cell = notebook_content["cells"][0] assert first_cell["cell_type"] == "markdown" assert ( f"JupyterLite-specific change for {notebook_filename}" in first_cell["source"] ) def test_cpp_rst(sphinx_app): cpp_rst = Path(sphinx_app.srcdir) / "auto_examples" / "parse_this.rst" content = cpp_rst.read_text() assert content.count(".. code-block:: C++") == 3 assert content.count(":dedent: 1", 1) assert "Download C++ source code" in content assert "binder-badge" not in content assert "lite-badge" not in content assert "Download Jupyter notebook" not in content def test_matlab_rst(sphinx_app): matlab_rst = Path(sphinx_app.srcdir) / "auto_examples" / "isentropic.rst" content = matlab_rst.read_text() assert content.count(".. code-block:: Matlab", 3) assert "isentropic, adiabatic flow example\n==============" in content assert "Download Matlab source code" in content def test_julia_rst(sphinx_app): julia_rst = Path(sphinx_app.srcdir) / "auto_examples" / "julia_sample.rst" content = julia_rst.read_text() assert content.count(".. code-block:: Julia", 3) assert "Julia example\n=============" in content assert "Download Julia source code" in content def test_recommend_n_examples(sphinx_app): """Test correct thumbnails are displayed for an example.""" pytest.importorskip("numpy") root = op.join(sphinx_app.outdir, "auto_examples") fname = op.join(root, "plot_defer_figures.html") with codecs.open(fname, "r", "utf-8") as fid: html = fid.read() count = html.count('
') n_examples = sphinx_app.config.sphinx_gallery_conf["recommender"]["n_examples"] assert '

Related examples

' in html assert count == n_examples # Check the same 3 related examples are shown assert "sphx-glr-auto-examples-plot-repr-py" in html assert "sphx-glr-auto-examples-plot-webp-py" in html assert "sphx-glr-auto-examples-plot-matplotlib-backend-py" in html sphinx-gallery-0.16.0/sphinx_gallery/tests/test_full_noexec.py000066400000000000000000000035221461331107500246550ustar00rootroot00000000000000# License: 3-clause BSD """Test the SG pipeline using Sphinx and tinybuild.""" from io import StringIO import os.path as op import shutil from sphinx.application import Sphinx from sphinx.util.docutils import docutils_namespace import pytest @pytest.fixture(scope="module") def sphinx_app(tmpdir_factory, req_mpl, req_pil): temp_dir = (tmpdir_factory.getbasetemp() / "root_nonexec").strpath src_dir = op.join(op.dirname(__file__), "tinybuild") def ignore(src, names): return ("_build", "gen_modules", "auto_examples") shutil.copytree(src_dir, temp_dir, ignore=ignore) # For testing iteration, you can get similar behavior just doing `make` # inside the tinybuild/doc directory src_dir = temp_dir conf_dir = op.join(temp_dir, "doc") out_dir = op.join(conf_dir, "_build", "html") toctrees_dir = op.join(temp_dir, "doc", "_build", "toctrees") # Avoid warnings about re-registration, see: # https://github.com/sphinx-doc/sphinx/issues/5038 confoverrides = { "sphinx_gallery_conf.plot_gallery": 0, } with docutils_namespace(): app = Sphinx( conf_dir, conf_dir, out_dir, toctrees_dir, buildername="html", confoverrides=confoverrides, status=StringIO(), warning=StringIO(), ) # need to build within the context manager # for automodule and backrefs to work app.build(False, []) return app def test_dummy_image(sphinx_app): """Test that sphinx_gallery_dummy_images are created.""" img1 = op.join( sphinx_app.srcdir, "auto_examples", "images", "sphx_glr_plot_repr_001.png" ) img2 = op.join( sphinx_app.srcdir, "auto_examples", "images", "sphx_glr_plot_repr_002.png" ) assert op.isfile(img1) assert op.isfile(img2) sphinx-gallery-0.16.0/sphinx_gallery/tests/test_gen_gallery.py000066400000000000000000000571531461331107500246530ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD r"""Test Sphinx-Gallery gallery generation.""" import codecs import os import re from pathlib import Path import json import pytest from sphinx.config import is_serializable from sphinx.errors import ConfigError, ExtensionError, SphinxWarning from sphinx_gallery.gen_gallery import ( check_duplicate_filenames, check_spaces_in_filenames, collect_gallery_files, write_computation_times, _fill_gallery_conf_defaults, write_api_entry_usage, fill_gallery_conf_defaults, ) from sphinx_gallery.interactive_example import create_jupyterlite_contents from sphinx_gallery.utils import _escape_ansi def test_bad_config(): """Test that bad config values are caught.""" sphinx_gallery_conf = dict(example_dir="") with pytest.raises( ConfigError, match="example_dir.*did you mean 'examples_dirs'?.*" ): _fill_gallery_conf_defaults(sphinx_gallery_conf) sphinx_gallery_conf = dict(n_subsection_order="") with pytest.raises( ConfigError, match=r"did you mean one of \['subsection_order', 'within_.*" ): _fill_gallery_conf_defaults(sphinx_gallery_conf) sphinx_gallery_conf = dict(within_subsection_order="sphinx_gallery.a.b.Key") with pytest.raises(ConfigError, match="must be a fully qualified"): _fill_gallery_conf_defaults(sphinx_gallery_conf) sphinx_gallery_conf = dict(within_subsection_order=1.0) with pytest.raises(ConfigError, match="a fully qualified.*got float"): _fill_gallery_conf_defaults(sphinx_gallery_conf) sphinx_gallery_conf = dict(minigallery_sort_order=int) with pytest.raises(ConfigError, match="Got class rather than callable instance"): _fill_gallery_conf_defaults(sphinx_gallery_conf) def test_default_config(sphinx_app_wrapper): """Test default Sphinx-Gallery config loaded when extension added to Sphinx.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() cfg = sphinx_app.config assert cfg.project == "Sphinx-Gallery " # no duplicate values allowed The config is present already with pytest.raises(ExtensionError) as excinfo: sphinx_app.add_config_value("sphinx_gallery_conf", "x", True) assert "already present" in str(excinfo.value) def test_serializable(sphinx_app_wrapper): """Test that the default config is serializable.""" bad = list() for key, val in _fill_gallery_conf_defaults({}).items(): if not is_serializable(val): bad.append(f"{repr(key)}: {repr(val)}") bad = "\n".join(bad) assert not bad, f"Non-serializable values found:\n{bad}" @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', }""" ) def test_no_warning_simple_config(sphinx_app_wrapper): """Testing that no warning is issued with a simple config. The simple config only specifies input (examples_dirs) and output (gallery_dirs) directories. """ sphinx_app = sphinx_app_wrapper.create_sphinx_app() cfg = sphinx_app.config assert cfg.project == "Sphinx-Gallery " build_warn = sphinx_app._warning.getvalue() assert build_warn == "" # This changed from passing the ValueError directly to # raising "sphinx.errors.ConfigError" with "threw an exception" @pytest.mark.parametrize( "err_class, err_match", [ pytest.param( ConfigError, "Unknown string option for reset_modules", id="Resetter unknown", marks=pytest.mark.conf_file( content="sphinx_gallery_conf={'reset_modules': ('f',)}" ), ), pytest.param( ConfigError, "reset_modules.* must be callable", id="Resetter not callable", marks=pytest.mark.conf_file( content="sphinx_gallery_conf={'reset_modules': (1.,),}" ), ), ], ) def test_bad_reset(sphinx_app_wrapper, err_class, err_match): with pytest.raises(err_class, match=err_match): sphinx_app_wrapper.create_sphinx_app() @pytest.mark.parametrize( "err_class, err_match", [ pytest.param( ConfigError, "reset_modules_order must be a str", id="Resetter unknown", marks=pytest.mark.conf_file( content=("sphinx_gallery_conf=" "{'reset_modules_order': 1,}") ), ), pytest.param( ConfigError, "reset_modules_order must be in", id="reset_modules_order not valid", marks=pytest.mark.conf_file( content=("sphinx_gallery_conf=" "{'reset_modules_order': 'invalid',}") ), ), ], ) def test_bad_reset_modules_order(sphinx_app_wrapper, err_class, err_match): with pytest.raises(err_class, match=err_match): sphinx_app_wrapper.create_sphinx_app() @pytest.mark.parametrize( "err_class, err_match", [ pytest.param( ConfigError, "Unknown css", id="CSS str error", marks=pytest.mark.conf_file( content="sphinx_gallery_conf={'css': ('foo',)}" ), ), pytest.param( ConfigError, "must be list or tuple", id="CSS type error", marks=pytest.mark.conf_file(content="sphinx_gallery_conf={'css': 1.}"), ), ], ) def test_bad_css(sphinx_app_wrapper, err_class, err_match): """Test 'css' configuration validation is correct.""" with pytest.raises(err_class, match=err_match): sphinx_app_wrapper.create_sphinx_app() def test_bad_api(): """Test that we raise an error for bad API usage arguments.""" sphinx_gallery_conf = dict(api_usage_ignore=("foo",)) with pytest.raises(ConfigError, match=".*must be str.*"): _fill_gallery_conf_defaults(sphinx_gallery_conf) sphinx_gallery_conf = dict(show_api_usage="foo") with pytest.raises(ConfigError, match='.*must be True, False or "unused".*'): _fill_gallery_conf_defaults(sphinx_gallery_conf) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'backreferences_dir': os.path.join('gen_modules', 'backreferences'), 'examples_dirs': 'src', 'gallery_dirs': 'ex', }""" ) def test_config_backreferences(sphinx_app_wrapper): """Test no warning is issued under the new configuration.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() cfg = sphinx_app.config assert cfg.project == "Sphinx-Gallery " assert cfg.sphinx_gallery_conf["backreferences_dir"] == os.path.join( "gen_modules", "backreferences" ) build_warn = sphinx_app._warning.getvalue() assert build_warn == "" def test_duplicate_files_warn(sphinx_app_wrapper): """Test for a warning when two files with the same filename exist.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() files = ["./a/file1.py", "./a/file2.py", "a/file3.py", "./b/file1.py"] msg = ( "Duplicate example file name(s) found. Having duplicate file names " "will break some links. List of files: {}" ) m = "['./b/file1.py']" # No warning because no overlapping names check_duplicate_filenames(files[:-1]) build_warn = sphinx_app._warning.getvalue() assert build_warn == "" # Warning because last file is named the same check_duplicate_filenames(files) build_warn = sphinx_app._warning.getvalue() assert msg.format(m) in build_warn def test_spaces_in_files_warn(sphinx_app_wrapper): """Test for a exception when an example filename has a space in it.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() files = ["./a/file1.py", "./a/file2.py", "./a/file 3.py"] msg = ( "Example file name(s) with space(s) found. Having space(s) in " "file names will break some links. " "List of files: {}" ) m = "['./a/file 3.py']" # No warning because no filename with space check_spaces_in_filenames(files[:-1]) build_warn = sphinx_app._warning.getvalue() assert build_warn == "" # Warning because last file has space check_spaces_in_filenames(files) build_warn = sphinx_app._warning.getvalue() assert msg.format(m) in build_warn def _check_order(sphinx_app, key): """Iterates through sphx-glr-thumbcontainer divs and reads key from the tooltip. Used to test that these keys (in index.rst) appear in a specific order. """ index_fname = os.path.join(sphinx_app.outdir, "..", "ex", "index.rst") order = list() regex = f".*:{key}=(.):.*" with codecs.open(index_fname, "r", "utf-8") as fid: for line in fid: if "sphx-glr-thumbcontainer" in line: order.append(int(re.match(regex, line).group(1))) assert len(order) == 3 assert order == [1, 2, 3] @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', }""" ) def test_example_sorting_default(sphinx_app_wrapper): """Test sorting of examples by default key (number of code lines).""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() _check_order(sphinx_app, "lines") @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'within_subsection_order': "FileSizeSortKey", }""" ) def test_example_sorting_filesize(sphinx_app_wrapper): """Test sorting of examples by filesize.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() _check_order(sphinx_app, "filesize") @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'within_subsection_order': "FileNameSortKey", }""" ) def test_example_sorting_filename(sphinx_app_wrapper): """Test sorting of examples by filename.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() _check_order(sphinx_app, "filename") @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'within_subsection_order': "ExampleTitleSortKey", }""" ) def test_example_sorting_title(sphinx_app_wrapper): """Test sorting of examples by title.""" sphinx_app = sphinx_app_wrapper.create_sphinx_app() _check_order(sphinx_app, "title") def test_collect_gallery_files(tmpdir, gallery_conf): """Test that example files are collected properly.""" rel_filepaths = [ "examples/file1.py", "examples/test.rst", "examples/README.txt", "examples/folder1/file1.py", "examples/folder1/file2.py", "examples/folder2/file1.py", "tutorials/folder1/subfolder/file1.py", "tutorials/folder2/subfolder/subsubfolder/file1.py", ] abs_paths = [tmpdir.join(rp) for rp in rel_filepaths] for ap in abs_paths: ap.ensure() examples_path = tmpdir.join("examples") dirs = [examples_path.strpath] collected_files = set(collect_gallery_files(dirs, gallery_conf)) expected_files = { ap.strpath for ap in abs_paths if re.search(r"examples.*\.py$", ap.strpath) } assert collected_files == expected_files tutorials_path = tmpdir.join("tutorials") dirs = [examples_path.strpath, tutorials_path.strpath] collected_files = set(collect_gallery_files(dirs, gallery_conf)) expected_files = { ap.strpath for ap in abs_paths if re.search(r".*\.py$", ap.strpath) } assert collected_files == expected_files def test_collect_gallery_files_ignore_pattern(tmpdir, gallery_conf): """Test that ignore pattern example files are not collected.""" rel_filepaths = [ "examples/file1.py", "examples/folder1/fileone.py", "examples/folder1/file2.py", "examples/folder2/fileone.py", ] abs_paths = [tmpdir.join(rp) for rp in rel_filepaths] for ap in abs_paths: ap.ensure() gallery_conf["ignore_pattern"] = r"one" examples_path = tmpdir.join("examples") dirs = [examples_path.strpath] collected_files = set(collect_gallery_files(dirs, gallery_conf)) expected_files = { ap.strpath for ap in abs_paths if re.search(r"one", os.path.basename(ap.strpath)) is None } assert collected_files == expected_files @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], 'binder': {'binderhub_url': 'http://test1.com', 'org': 'org', 'repo': 'repo', 'branch': 'branch', 'notebooks_dir': 'ntbk_folder', 'dependencies': 'requirements.txt'} }""" ) def test_binder_copy_files(sphinx_app_wrapper): """Test that notebooks are copied properly.""" from sphinx_gallery.interactive_example import copy_binder_files sphinx_app = sphinx_app_wrapper.create_sphinx_app() gallery_conf = sphinx_app.config.sphinx_gallery_conf # Create requirements file with open(os.path.join(sphinx_app.srcdir, "requirements.txt"), "w"): pass copy_binder_files(sphinx_app, None) for i_file in ["plot_1", "plot_2", "plot_3"]: assert os.path.exists( os.path.join( sphinx_app.outdir, "ntbk_folder", gallery_conf["gallery_dirs"][0], i_file + ".ipynb", ) ) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', }""" ) def test_failing_examples_raise_exception(sphinx_app_wrapper): example_dir = os.path.join(sphinx_app_wrapper.srcdir, "src") with codecs.open( os.path.join(example_dir, "plot_3.py"), "a", encoding="utf-8" ) as fid: fid.write("raise SyntaxError") with pytest.raises(ExtensionError) as excinfo: sphinx_app_wrapper.build_sphinx_app() assert "Unexpected failing examples" in str(excinfo.value) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'filename_pattern': 'plot_1.py', }""" ) def test_expected_failing_examples_were_executed(sphinx_app_wrapper): """Testing that no exception is issued when broken example is not built. See #335 for more details. """ sphinx_app_wrapper.build_sphinx_app() @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'only_warn_on_example_error': True, }""" ) def test_only_warn_on_example_error(sphinx_app_wrapper): """Test behaviour of only_warn_on_example_error flag.""" example_dir = Path(sphinx_app_wrapper.srcdir) / "src" with codecs.open(example_dir / "plot_3.py", "a", encoding="utf-8") as fid: fid.write("raise ValueError") sphinx_app = sphinx_app_wrapper.build_sphinx_app() build_warn = _escape_ansi(sphinx_app._warning.getvalue()) assert "plot_3.py unexpectedly failed to execute correctly" in build_warn assert "WARNING: Here is a summary of the problems" in build_warn @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'only_warn_on_example_error': True, }""" ) def test_only_warn_on_example_error_sphinx_warning(sphinx_app_wrapper): """Test behaviour of only_warn_on_example_error flag.""" sphinx_app_wrapper.kwargs["warningiserror"] = True example_dir = Path(sphinx_app_wrapper.srcdir) / "src" with codecs.open(example_dir / "plot_3.py", "a", encoding="utf-8") as fid: fid.write("raise ValueError") with pytest.raises(SphinxWarning) as excinfo: sphinx_app_wrapper.build_sphinx_app() exc = _escape_ansi(str(excinfo.value)) assert "plot_3.py unexpectedly failed to execute" in exc @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'examples_dirs': 'src', 'gallery_dirs': 'ex', 'expected_failing_examples' :['src/plot_2.py'], }""" ) def test_examples_not_expected_to_pass(sphinx_app_wrapper): with pytest.raises(ExtensionError) as excinfo: sphinx_app_wrapper.build_sphinx_app() exc = _escape_ansi(str(excinfo.value)) assert "expected to fail, but not failing" in exc @pytest.mark.conf_file( content=""" from sphinx_gallery.gen_rst import _sg_call_memory_noop sphinx_gallery_conf = { 'show_memory': _sg_call_memory_noop, 'gallery_dirs': 'ex', }""" ) def test_show_memory_callable(sphinx_app_wrapper): sphinx_app = sphinx_app_wrapper.build_sphinx_app() status = sphinx_app._status.getvalue() assert "0.0 MB" in status @pytest.mark.parametrize( "", [ pytest.param( id="first notebook cell", marks=pytest.mark.conf_file( content="""sphinx_gallery_conf = {'first_notebook_cell': 2,}""" ), ), pytest.param( id="last notebook cell", marks=pytest.mark.conf_file( content="""sphinx_gallery_conf = {'last_notebook_cell': 2,}""" ), ), ], ) def test_notebook_cell_config(sphinx_app_wrapper): """Tests that first and last cell configuration validated.""" with pytest.raises(ConfigError): app = sphinx_app_wrapper.create_sphinx_app() fill_gallery_conf_defaults(app, app.config, check_keys=False) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'backreferences_dir': False, }""" ) def test_backreferences_dir_config(sphinx_app_wrapper): """Tests 'backreferences_dir' type checking.""" with pytest.raises( ConfigError, match="The 'backreferences_dir' parameter must be of" ): app = sphinx_app_wrapper.create_sphinx_app() fill_gallery_conf_defaults(app, app.config, check_keys=False) @pytest.mark.conf_file( content=""" import pathlib sphinx_gallery_conf = { 'backreferences_dir': pathlib.Path('.'), }""" ) def test_backreferences_dir_pathlib_config(sphinx_app_wrapper): """Tests pathlib.Path does not raise exception.""" app = sphinx_app_wrapper.create_sphinx_app() fill_gallery_conf_defaults(app, app.config, check_keys=False) def test_write_computation_times_noop(sphinx_app_wrapper): app = sphinx_app_wrapper.create_sphinx_app() write_computation_times(app.config.sphinx_gallery_conf, None, []) def test_write_api_usage_noop(sphinx_app_wrapper): write_api_entry_usage(sphinx_app_wrapper.create_sphinx_app(), list(), None) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'pypandoc': ['list',], }""" ) def test_pypandoc_config_list(sphinx_app_wrapper): """Tests 'pypandoc' type checking.""" with pytest.raises( ConfigError, match="'pypandoc' parameter must be of type bool or " "dict" ): app = sphinx_app_wrapper.create_sphinx_app() fill_gallery_conf_defaults(app, app.config, check_keys=False) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'pypandoc': {'bad key': 1}, }""" ) def test_pypandoc_config_keys(sphinx_app_wrapper): """Tests 'pypandoc' dictionary key checking.""" with pytest.raises( ConfigError, match="'pypandoc' only accepts the following key " "values:" ): app = sphinx_app_wrapper.create_sphinx_app() fill_gallery_conf_defaults(app, app.config, check_keys=False) @pytest.mark.conf_file( content=""" extensions += ['jupyterlite_sphinx'] sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], }""" ) def test_create_jupyterlite_contents(sphinx_app_wrapper): """Test that JupyterLite contents are created properly.""" pytest.importorskip("jupyterlite_sphinx") sphinx_app = sphinx_app_wrapper.create_sphinx_app() gallery_conf = sphinx_app.config.sphinx_gallery_conf create_jupyterlite_contents(sphinx_app, exception=None) for i_file in ["plot_1", "plot_2", "plot_3"]: assert os.path.exists( os.path.join( sphinx_app.srcdir, "jupyterlite_contents", gallery_conf["gallery_dirs"][0], i_file + ".ipynb", ) ) @pytest.mark.conf_file( content=""" extensions += ['jupyterlite_sphinx'] sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], 'jupyterlite': {'jupyterlite_contents': 'this_is_the_contents_dir'} }""" ) def test_create_jupyterlite_contents_non_default_contents(sphinx_app_wrapper): """Test that JupyterLite contents are created properly.""" pytest.importorskip("jupyterlite_sphinx") sphinx_app = sphinx_app_wrapper.create_sphinx_app() gallery_conf = sphinx_app.config.sphinx_gallery_conf create_jupyterlite_contents(sphinx_app, exception=None) for i_file in ["plot_1", "plot_2", "plot_3"]: assert os.path.exists( os.path.join( sphinx_app.srcdir, "this_is_the_contents_dir", gallery_conf["gallery_dirs"][0], i_file + ".ipynb", ) ) @pytest.mark.conf_file( content=""" sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], }""" ) def test_create_jupyterlite_contents_without_jupyterlite_sphinx_loaded( sphinx_app_wrapper, ): """Test JupyterLite contents creation without jupyterlite_sphinx loaded.""" pytest.importorskip("jupyterlite_sphinx") sphinx_app = sphinx_app_wrapper.create_sphinx_app() create_jupyterlite_contents(sphinx_app, exception=None) assert not os.path.exists(os.path.join(sphinx_app.srcdir, "jupyterlite_contents")) @pytest.mark.conf_file( content=""" extensions += ['jupyterlite_sphinx'] sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], 'jupyterlite': None, }""" ) def test_create_jupyterlite_contents_with_jupyterlite_disabled_via_config( sphinx_app_wrapper, ): """Test JupyterLite contents created with jupyterlite_sphinx loaded but disabled. JupyterLite disabled via config. """ pytest.importorskip("jupyterlite_sphinx") sphinx_app = sphinx_app_wrapper.create_sphinx_app() create_jupyterlite_contents(sphinx_app, exception=None) assert not os.path.exists(os.path.join(sphinx_app.outdir, "jupyterlite_contents")) @pytest.mark.conf_file( content=""" extensions += ['jupyterlite_sphinx'] def notebook_modification_function(notebook_content, notebook_filename): source = f'JupyterLite-specific change for {notebook_filename}' markdown_cell = { 'cell_type': 'markdown', 'metadata': {}, 'source': source } notebook_content['cells'] = [markdown_cell] + notebook_content['cells'] sphinx_gallery_conf = { 'backreferences_dir' : os.path.join('modules', 'gen'), 'examples_dirs': 'src', 'gallery_dirs': ['ex'], 'jupyterlite': { 'notebook_modification_function': notebook_modification_function } }""" ) def test_create_jupyterlite_contents_with_modification(sphinx_app_wrapper): pytest.importorskip("jupyterlite_sphinx") sphinx_app = sphinx_app_wrapper.create_sphinx_app() gallery_conf = sphinx_app.config.sphinx_gallery_conf create_jupyterlite_contents(sphinx_app, exception=None) for i_file in ["plot_1", "plot_2", "plot_3"]: notebook_filename = os.path.join( sphinx_app.srcdir, "jupyterlite_contents", gallery_conf["gallery_dirs"][0], i_file + ".ipynb", ) assert os.path.exists(notebook_filename) with open(notebook_filename) as f: notebook_content = json.load(f) first_cell = notebook_content["cells"][0] assert first_cell["cell_type"] == "markdown" assert ( f"JupyterLite-specific change for {notebook_filename}" in first_cell["source"] ) sphinx-gallery-0.16.0/sphinx_gallery/tests/test_gen_rst.py000066400000000000000000001145021461331107500240140ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD """Testing the rst files generator.""" import ast import codecs import importlib import io import logging from pathlib import Path import tempfile import re import os import shutil import sys from unittest import mock import zipfile import codeop import pytest from sphinx.errors import ExtensionError import sphinx_gallery.gen_rst as sg from sphinx_gallery import downloads from sphinx_gallery.gen_gallery import ( generate_dir_rst, _update_gallery_conf_exclude_implicit_doc, ) # TODO: The tests of this method should probably be moved to test_py_source_parser.py from sphinx_gallery.py_source_parser import split_code_and_text_blocks from sphinx_gallery.scrapers import ImagePathIterator, figure_rst from sphinx_gallery.interactive_example import check_binder_conf root = Path(__file__).parents[2] CONTENT = [ '"""', "================", "Docstring header", "================", "", "This is the description of the example", "which goes on and on, Óscar", "", "", "And this is a second paragraph", '"""', "", "# sphinx_gallery_thumbnail_number = 1", "# sphinx_gallery_defer_figures", "# and now comes the module code", "# sphinx_gallery_start_ignore", "pass # Will be run but not rendered", "# sphinx_gallery_end_ignore", "import logging", "import sys", "from warnings import warn", "x, y = 1, 2", 'print("Óscar output") # need some code output', "logger = logging.getLogger()", "logger.setLevel(logging.INFO)", "lh = logging.StreamHandler(sys.stdout)", 'lh.setFormatter(logging.Formatter("log:%(message)s"))', "logger.addHandler(lh)", 'logger.info("Óscar")', 'print(r"$\\langle n_\\uparrow n_\\downarrow \\rangle$")', 'warn("WarningsAbound", RuntimeWarning)', ] def test_split_code_and_text_blocks(): """Test if a known example file gets properly split.""" file_conf, blocks = split_code_and_text_blocks( root / "examples" / "no_output" / "just_code.py", ) assert file_conf == {} assert blocks[0][0] == "text" assert blocks[1][0] == "code" def test_bug_cases_of_notebook_syntax(): """Test the block splitting with supported syntax in notebook styled example. `plot_parse.py` uses both '#'s' and '#%%' as cell separators. """ ref_blocks = ast.literal_eval( (Path(__file__).parent / "reference_parse.txt").read_text("utf-8") ) file_conf, blocks = split_code_and_text_blocks(root / "tutorials" / "plot_parse.py") assert file_conf == {} assert blocks == ref_blocks def test_direct_comment_after_docstring(): # For more details see # https://github.com/sphinx-gallery/sphinx-gallery/pull/49 with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write( "\n".join( [ '"Docstring"', "# and now comes the module code", "# with a second line of comment", "x, y = 1, 2", "", ] ) ) try: file_conf, result = split_code_and_text_blocks(f.name) finally: os.remove(f.name) assert file_conf == {} expected_result = [ ("text", "Docstring", 1), ( "code", "\n".join( [ "# and now comes the module code", "# with a second line of comment", "x, y = 1, 2", "", ] ), 2, ), ] assert result == expected_result def test_final_rst_last_word(tmpdir): """Tests last word in final rst block included as text.""" filename = str(tmpdir.join("temp.py")) with open(filename, "w") as f: f.write( "\n".join( [ '"Docstring"', "# comment only code block", "#%%", "# Include this whole sentence.", ] ) ) file_conf, result = split_code_and_text_blocks(f.name) assert file_conf == {} expected_result = [ ("text", "Docstring", 1), ("code", "# comment only code block\n", 2), ("text", "Include this whole sentence.", 4), ] assert result == expected_result def test_rst_block_after_docstring(gallery_conf, tmpdir): """Assert there is a blank line between the docstring and rst blocks.""" filename = str(tmpdir.join("temp.py")) with open(filename, "w") as f: f.write( "\n".join( [ '"Docstring"', "####################", "# Paragraph 1", "# is long.", "", "#%%", "# Paragraph 2", "", "# %%", "# Paragraph 3", "", ] ) ) file_conf, blocks = split_code_and_text_blocks(filename) assert file_conf == {} assert len(blocks) == 4 assert blocks[0][0] == "text" assert blocks[1][0] == "text" assert blocks[2][0] == "text" assert blocks[3][0] == "text" script_vars = {"execute_script": ""} file_conf = {} output_blocks, time_elapsed = sg.execute_script( blocks, script_vars, gallery_conf, file_conf ) example_rst = sg.rst_blocks(blocks, output_blocks, file_conf, gallery_conf) want_rst = """\ Docstring .. GENERATED FROM PYTHON SOURCE LINES 3-5 Paragraph 1 is long. .. GENERATED FROM PYTHON SOURCE LINES 7-8 Paragraph 2 .. GENERATED FROM PYTHON SOURCE LINES 10-11 Paragraph 3 """ assert example_rst == want_rst def test_rst_empty_code_block(gallery_conf, tmpdir): """Test that we can "execute" a code block containing only comments.""" gallery_conf.update(image_scrapers=()) filename = str(tmpdir.join("temp.py")) with open(filename, "w") as f: f.write( "\n".join( [ '"Docstring"', "####################", "# Paragraph 1", "", "# just a comment" "", ] ) ) file_conf, blocks = split_code_and_text_blocks(filename) assert file_conf == {} assert len(blocks) == 3 assert blocks[0][0] == "text" assert blocks[1][0] == "text" assert blocks[2][0] == "code" gallery_conf["abort_on_example_error"] = True script_vars = dict( execute_script=True, src_file=filename, image_path_iterator=[], target_file=filename, ) output_blocks, time_elapsed = sg.execute_script( blocks, script_vars, gallery_conf, file_conf ) example_rst = sg.rst_blocks(blocks, output_blocks, file_conf, gallery_conf) want_rst = """\ Docstring .. GENERATED FROM PYTHON SOURCE LINES 3-4 Paragraph 1 .. GENERATED FROM PYTHON SOURCE LINES 4-5 .. code-block:: python # just a comment""" assert example_rst.rstrip("\n") == want_rst def test_script_vars_globals(gallery_conf, tmpdir): """Assert the global vars get stored.""" gallery_conf.update(image_scrapers=()) filename = str(tmpdir.join("temp.py")) with open(filename, "w") as f: f.write( """ ''' My example ---------- This is it. ''' a = 1. b = 'foo' """ ) file_conf, blocks = split_code_and_text_blocks(filename) assert len(blocks) == 2 assert blocks[0][0] == "text" assert blocks[1][0] == "code" assert file_conf == {} script_vars = { "execute_script": True, "src_file": filename, "image_path_iterator": [], "target_file": filename, } output_blocks, time_elapsed = sg.execute_script( blocks, script_vars, gallery_conf, file_conf ) assert "example_globals" in script_vars assert script_vars["example_globals"]["a"] == 1.0 assert script_vars["example_globals"]["b"] == "foo" def test_codestr2rst(): """Test the correct translation of a code block into rst.""" output = sg.codestr2rst('print("hello world")') reference = """\ .. code-block:: python print("hello world")""" assert reference == output def test_extract_intro_and_title(): intro, title = sg.extract_intro_and_title("", "\n".join(CONTENT[1:10])) assert title == "Docstring header" assert "Docstring" not in intro assert intro == "This is the description of the example which goes on and on, Óscar" # noqa assert "second paragraph" not in intro # SG incorrectly grabbing description when a label is defined (gh-232) intro_label, title_label = sg.extract_intro_and_title( "", "\n".join([".. my_label", ""] + CONTENT[1:10]) ) assert intro_label == intro assert title_label == title intro_whitespace, title_whitespace = sg.extract_intro_and_title( "", "\n".join(CONTENT[1:4] + [""] + CONTENT[5:10]) ) assert intro_whitespace == intro assert title_whitespace == title # Make example title optional (gh-222) intro, title = sg.extract_intro_and_title("", "Title\n-----") assert intro == title == "Title" # Title beginning with a space (gh-356) intro, title = sg.extract_intro_and_title( "filename", "^^^^^\n Title two \n^^^^^" ) assert intro == title == "Title two" # Title with punctuation (gh-517) intro, title = sg.extract_intro_and_title( "", ' ------------\n"-`Header"-with:; `punct` mark\'s\n----------------', ) # noqa: E501 assert title == '"-`Header"-with:; punct mark\'s' # Long intro paragraph gets shortened intro_paragraph = "\n".join(["this is one line" for _ in range(10)]) intro, _ = sg.extract_intro_and_title( "filename", "Title\n-----\n\n" + intro_paragraph ) assert len(intro_paragraph) > 100 assert len(intro) < 100 assert intro.endswith("...") assert intro_paragraph.replace("\n", " ")[:95] == intro[:95] # Errors with pytest.raises(ExtensionError, match="should have a header"): sg.extract_intro_and_title("", "") # no title with pytest.raises(ExtensionError, match="Could not find a title"): sg.extract_intro_and_title("", "=====") # no real title @pytest.mark.parametrize( "mode,expected_md5", ( ["b", "a546be453c8f436e744838a4801bd3a0"], ["t", "ea8a570e9f3afc0a7c3f2a17a48b8047"], ), ) def test_md5sums(mode, expected_md5): """Test md5sum check functions work on know file content.""" file_content = b"Local test\r\n" with tempfile.NamedTemporaryFile("wb", delete=False) as f: f.write(file_content) try: file_md5 = sg.get_md5sum(f.name, mode) # verify correct md5sum assert file_md5 == expected_md5 # False because is a new file assert not sg.md5sum_is_current(f.name) # Write md5sum to file to check is current with open(f.name + ".md5", "w") as file_checksum: file_checksum.write(file_md5) try: assert sg.md5sum_is_current(f.name, mode) finally: os.remove(f.name + ".md5") finally: os.remove(f.name) @pytest.mark.parametrize( "failing_code, want", [ ( CONTENT + ["#" * 79, "First_test_fail", "#" * 79, "second_fail"], "not defined", ), ( CONTENT + ["#" * 79, 'input("foo")', "#" * 79, "second_fail"], "Cannot use input", ), (CONTENT + ["#" * 79, "bad syntax", "#" * 79, "second_fail"], "invalid syntax"), ], ) def test_fail_example(gallery_conf, failing_code, want, log_collector, req_pil): """Test that failing examples are only executed until failing block.""" gallery_conf.update(image_scrapers=(), reset_modules=()) gallery_conf.update(filename_pattern="raise.py") with codecs.open( os.path.join(gallery_conf["examples_dir"], "raise.py"), mode="w", encoding="utf-8", ) as f: f.write("\n".join(failing_code)) sg.generate_file_rst( "raise.py", gallery_conf["gallery_dir"], gallery_conf["examples_dir"], gallery_conf, ) log_collector.warning.assert_called_once() msg = log_collector.warning.call_args[0][2] assert want in msg assert "gen_gallery" not in msg # can only check that gen_rst is removed on non-input ones if "Cannot use input" not in msg: assert "gen_rst" not in msg assert "_check_input" not in msg # read rst file and check if it contains traceback output with codecs.open( os.path.join(gallery_conf["gallery_dir"], "raise.rst"), mode="r", encoding="utf-8", ) as f: ex_failing_blocks = f.read().count("pytb") assert ex_failing_blocks != 0, "Did not run into errors in bad code" assert ex_failing_blocks <= 1, "Did not stop executing script after error" def _generate_rst(gallery_conf, fname, content): """Return the reST text of a given example content. This writes a file gallery_conf['examples_dir']/fname with *content*, creates the corresponding rst file by running generate_file_rst() and returns the generated reST code. Parameters ---------- gallery_conf A gallery_conf as created by the gallery_conf fixture. fname : str A filename; e.g. 'test.py'. This is relative to gallery_conf['examples_dir'] content : str The content of fname. Returns ------- rst : str The generated reST code. """ with codecs.open( os.path.join(gallery_conf["examples_dir"], fname), mode="w", encoding="utf-8" ) as f: f.write("\n".join(content)) # generate rst file sg.generate_file_rst( fname, gallery_conf["gallery_dir"], gallery_conf["examples_dir"], gallery_conf ) # read rst file and check if it contains code output rst_fname = os.path.splitext(fname)[0] + ".rst" with codecs.open( os.path.join(gallery_conf["gallery_dir"], rst_fname), mode="r", encoding="utf-8" ) as f: rst = f.read() return rst ALPHA_CONTENT = ''' """ Make a plot =========== Plot. """ import matplotlib.pyplot as plt plt.plot([0, 1], [0, 1]) '''.split("\n") def _alpha_mpl_scraper(block, block_vars, gallery_conf): import matplotlib.pyplot as plt image_path_iterator = block_vars["image_path_iterator"] image_paths = list() for fig_num, image_path in zip(plt.get_fignums(), image_path_iterator): fig = plt.figure(fig_num) assert image_path.endswith(".png") # use format that does not support alpha image_path = Path(image_path).with_suffix(".jpg") fig.savefig(image_path) image_paths.append(image_path) plt.close("all") return figure_rst(image_paths, gallery_conf["src_dir"]) def test_custom_scraper_thumbnail_alpha(gallery_conf, req_mpl_jpg): """Test that thumbnails without an alpha channel work w/custom scraper.""" gallery_conf["image_scrapers"] = [_alpha_mpl_scraper] rst = _generate_rst(gallery_conf, "plot_test.py", ALPHA_CONTENT) assert ".jpg" in rst def test_remove_config_comments(gallery_conf, req_pil): """Test the gallery_conf['remove_config_comments'] setting.""" rst = _generate_rst(gallery_conf, "test.py", CONTENT) assert "# sphinx_gallery_thumbnail_number = 1" in rst assert "# sphinx_gallery_defer_figures" in rst gallery_conf["remove_config_comments"] = True rst = _generate_rst(gallery_conf, "test.py", CONTENT) assert "# sphinx_gallery_thumbnail_number = 1" not in rst assert "# sphinx_gallery_defer_figures" not in rst @pytest.mark.parametrize("remove_config_comments", [True, False]) def test_remove_ignore_blocks(gallery_conf, req_pil, remove_config_comments): """Test removal of ignore blocks.""" gallery_conf["remove_config_comments"] = remove_config_comments rst = _generate_rst(gallery_conf, "test.py", CONTENT) assert "pass # Will be run but not rendered" in CONTENT assert "pass # Will be run but not rendered" not in rst assert "# sphinx_gallery_start_ignore" in CONTENT assert "# sphinx_gallery_start_ignore" not in rst def test_dummy_image_error(gallery_conf, req_pil): """Test sphinx_gallery_dummy_images configuration validated.""" content_image = CONTENT + [ "# sphinx_gallery_dummy_images=False", ] msg = "sphinx_gallery_dummy_images setting is not an integer" with pytest.raises(ExtensionError, match=msg): _generate_rst(gallery_conf, "test.py", content_image) def test_final_empty_block(gallery_conf, req_pil): """Test empty final block is removed. Empty final block can occur after sole config comment is removed from final block. """ content_block = CONTENT + ["# %%", "", "# sphinx_gallery_line_numbers = True"] gallery_conf["remove_config_comments"] = True gallery_conf["min_reported_time"] = -1 # Force timing info to be shown rst = _generate_rst(gallery_conf, "test.py", content_block) want = "RuntimeWarning)\n\n\n.. rst-class:: sphx-glr-timing" assert want in rst def test_download_link_note_only_html(gallery_conf, req_pil): """Test html only directive for download_link.""" rst = _generate_rst(gallery_conf, "test.py", CONTENT) download_link_note = ( ".. only:: html\n\n" " .. note::\n" " :class: sphx-glr-download-link-note\n\n" ) assert download_link_note in rst def test_download_link_classes(gallery_conf, req_pil): """Test classes for download links.""" rst = _generate_rst(gallery_conf, "test.py", CONTENT) for kind in ("python", "jupyter"): assert "sphx-glr-download sphx-glr-download-" + kind in rst EXCLUDE_CONTENT = ''' """:obj:`numpy.pi` :func:`numpy.sin`""" import numpy numpy.pi numpy.e '''.split("\n") @pytest.mark.parametrize( "exclusion, expected", [ (None, {"numpy.sin", "numpy.pi", "numpy.e"}), ({".*"}, {"numpy.sin", "numpy.pi"}), ({"pi"}, {"numpy.sin", "numpy.pi", "numpy.e"}), ({r"numpy\.e", "sin"}, {"numpy.sin", "numpy.pi"}), ], ids=[ "exclude nothing (default)", "exclude anything (explicit backreferences only)", "explicit backref not shadowed by implicit one", "exclude implicit backref", ], ) def test_exclude_implicit(gallery_conf, exclusion, expected, monkeypatch, req_pil): mock_write_backreferences = mock.create_autospec(sg._write_backreferences) monkeypatch.setattr(sg, "_write_backreferences", mock_write_backreferences) gallery_conf["doc_module"] = ("numpy",) if exclusion: gallery_conf["exclude_implicit_doc"] = exclusion _update_gallery_conf_exclude_implicit_doc(gallery_conf) _generate_rst(gallery_conf, "test_exclude_implicit.py", EXCLUDE_CONTENT) if sys.version_info >= (3, 8, 0): assert mock_write_backreferences.call_args.args[0] == expected else: assert mock_write_backreferences.call_args[0][0] == expected @pytest.mark.parametrize("ext", (".txt", ".rst", ".bad")) def test_gen_dir_rst(gallery_conf, ext): """Test gen_dir_rst.""" print(os.listdir(gallery_conf["examples_dir"])) fname_readme = os.path.join(gallery_conf["src_dir"], "README.txt") with open(fname_readme, "wb") as fid: fid.write("Testing\n=======\n\nÓscar here.".encode()) fname_out = os.path.splitext(fname_readme)[0] + ext if fname_readme != fname_out: shutil.move(fname_readme, fname_out) args = (gallery_conf["src_dir"], gallery_conf["gallery_dir"], gallery_conf, []) if ext == ".bad": # not found with correct ext with pytest.raises(ExtensionError, match="does not have a README"): generate_dir_rst(*args) else: out = generate_dir_rst(*args) assert "Óscar here" in out[1] def test_pattern_matching(gallery_conf, log_collector, req_pil): """Test if only examples matching pattern are executed.""" gallery_conf.update(image_scrapers=(), reset_modules=()) gallery_conf.update(filename_pattern=re.escape(os.sep) + "plot_0") code_output = ( "\n .. code-block:: none\n" "\n" " Óscar output\n" " log:Óscar\n" " $\\langle n_\\uparrow n_\\downarrow \\rangle$" ) warn_output = "RuntimeWarning: WarningsAbound" # create three files in tempdir (only one matches the pattern) fnames = ["plot_0.py", "plot_1.py", "plot_2.py"] for fname in fnames: rst = _generate_rst(gallery_conf, fname, CONTENT) rst_fname = os.path.splitext(fname)[0] + ".rst" if re.search( gallery_conf["filename_pattern"], os.path.join(gallery_conf["gallery_dir"], rst_fname), ): assert code_output in rst assert warn_output in rst else: assert code_output not in rst assert warn_output not in rst @pytest.mark.parametrize( "test_str", [ "# sphinx_gallery_thumbnail_number= 2", "# sphinx_gallery_thumbnail_number=2", "#sphinx_gallery_thumbnail_number = 2", " # sphinx_gallery_thumbnail_number=2", ], ) def test_thumbnail_number(test_str): """Test correct plot used as thumbnail image.""" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write("\n".join(['"Docstring"', test_str])) try: file_conf, blocks = split_code_and_text_blocks(f.name) finally: os.remove(f.name) assert file_conf == {"thumbnail_number": 2} @pytest.mark.parametrize( "test_str", [ "# sphinx_gallery_thumbnail_path= '_static/demo.png'", "# sphinx_gallery_thumbnail_path='_static/demo.png'", "#sphinx_gallery_thumbnail_path = '_static/demo.png'", " # sphinx_gallery_thumbnail_path='_static/demo.png'", ], ) def test_thumbnail_path(test_str): """Test correct plot used for thumbnail image.""" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write("\n".join(['"Docstring"', test_str])) try: file_conf, blocks = split_code_and_text_blocks(f.name) finally: os.remove(f.name) assert file_conf == {"thumbnail_path": "_static/demo.png"} def test_zip_python(gallery_conf): """Test generated zipfiles are not corrupt and have expected name and contents.""" gallery_conf.update(examples_dir=os.path.join(gallery_conf["src_dir"], "examples")) shutil.copytree( os.path.join(os.path.dirname(__file__), "tinybuild", "examples"), gallery_conf["examples_dir"], ) examples = downloads.list_downloadable_sources(gallery_conf["examples_dir"]) zipfilepath = downloads.python_zip(examples, gallery_conf["gallery_dir"]) assert zipfilepath.endswith("_python.zip") zipf = zipfile.ZipFile(zipfilepath) check = zipf.testzip() assert not check, f"Bad file in zipfile: {check}" filenames = {item.filename for item in zipf.filelist} assert "examples/plot_command_line_args.py" in filenames assert "examples/julia_sample.jl" not in filenames def test_zip_mixed_source(gallery_conf): """Test generated zipfiles are not corrupt and have expected name and contents.""" gallery_conf.update(examples_dir=os.path.join(gallery_conf["src_dir"], "examples")) shutil.copytree( os.path.join(os.path.dirname(__file__), "tinybuild", "examples"), gallery_conf["examples_dir"], ) examples = downloads.list_downloadable_sources( gallery_conf["examples_dir"], (".py", ".jl") ) zipfilepath = downloads.python_zip(examples, gallery_conf["gallery_dir"], None) zipf = zipfile.ZipFile(zipfilepath) check = zipf.testzip() assert not check, f"Bad file in zipfile: {check}" filenames = {item.filename for item in zipf.filelist} assert "examples/plot_command_line_args.py" in filenames assert "examples/julia_sample.jl" in filenames def test_rst_example(gallery_conf): """Test generated rst file includes the correct paths for binder.""" binder_conf = check_binder_conf( { "org": "sphinx-gallery", "repo": "sphinx-gallery.github.io", "binderhub_url": "https://mybinder.org", "branch": "master", "dependencies": "./binder/requirements.txt", "use_jupyter_lab": True, } ) gallery_conf.update(binder=binder_conf) gallery_conf["min_reported_time"] = -1 example_file = os.path.join(gallery_conf["gallery_dir"], "plot.py") sg.save_rst_example("example_rst", example_file, 0, 0, gallery_conf) test_file = re.sub(r"\.py$", ".rst", example_file) with codecs.open(test_file) as f: rst = f.read() assert "lab/tree/notebooks/plot.ipynb" in rst # CSS classes assert "rst-class:: sphx-glr-signature" in rst assert "rst-class:: sphx-glr-timing" in rst @pytest.fixture(scope="function") def script_vars(tmpdir): fake_main = importlib.util.module_from_spec( importlib.util.spec_from_loader("__main__", None) ) fake_main.__dict__.update({"__doc__": ""}) script_vars = { "execute_script": True, "image_path_iterator": ImagePathIterator(str(tmpdir.join("temp.png"))), "src_file": __file__, "memory_delta": [], "fake_main": fake_main, } return script_vars def test_output_indentation(gallery_conf, script_vars): """Test whether indentation of code output is retained.""" gallery_conf.update(image_scrapers=()) compiler = codeop.Compile() test_string = r"\n".join([" A B", "A 1 2", "B 3 4"]) code = "print('" + test_string + "')" code_block = ("code", code, 1) file_conf = {} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) output_test_string = "\n".join( [line[4:] for line in output.strip().split("\n")[-3:]] ) assert output_test_string == test_string.replace(r"\n", "\n") def test_output_no_ansi(gallery_conf, script_vars): """Test ANSI characters are removed. See: https://en.wikipedia.org/wiki/ANSI_escape_code """ gallery_conf.update(image_scrapers=()) compiler = codeop.Compile() code = 'print("\033[94m0.25")' code_block = ("code", code, 1) file_conf = {} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) output_test_string = "\n".join( [line[4:] for line in output.strip().split("\n")[-3:]] ) assert output_test_string.split("\n")[-1] == "0.25" def test_absl_logging(gallery_conf, script_vars): """Test using absl logging does not throw error. This is important, as many popular libraries like Tensorflow use absl for logging. """ pytest.importorskip("absl") gallery_conf.update(image_scrapers=()) compiler = codeop.Compile() code = 'from absl import logging\nlogging.info("Should not crash!")' sg.execute_code_block( compiler, ("code", code, 1), None, script_vars, gallery_conf, {} ) # Previously, absl crashed during shutdown, after all tests had run, so # tests would pass even if this issue occurred. To address this issue, we # call shutdown inside the test. logging.shutdown() def test_empty_output_box(gallery_conf, script_vars): """Tests that `print(__doc__)` doesn't produce an empty output box.""" gallery_conf.update(image_scrapers=()) compiler = codeop.Compile() code_block = ("code", "print(__doc__)", 1) file_conf = {} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) assert output.isspace() code_repr_only = """ class repr_only_class(): def __init__(self): pass def __repr__(self): return "This is the __repr__" class_inst = repr_only_class() class_inst """ code_repr_and_html = """ class repr_and_html_class(): def __init__(self): pass def __repr__(self): return "This is the __repr__" def _repr_html_(self): return "
This is the _repr_html_ div
" class_inst = repr_and_html_class() class_inst """ code_print_and_repr_and_html = """ print("print statement") class repr_and_html_class(): def __init__(self): pass def __repr__(self): return "This is the __repr__" def _repr_html_(self): return "
This is the _repr_html_ div
" class_inst = repr_and_html_class() class_inst """ code_plt = """ import matplotlib.pyplot as plt fig = plt.figure() plt.close('all') fig """ html_out = """.. raw:: html
This is the _repr_html_ div


""" text_above_html = """.. code-block:: none print statement """ def _clean_output(output): is_text = ".. rst-class:: sphx-glr-script-out" in output is_html = ".. raw:: html" in output if output.isspace(): return "" elif is_text and is_html: output_test_string = "\n".join(output.strip().split("\n")[2:]) return output_test_string.strip() elif is_text: output_test_string = "\n".join( [line[4:] for line in output.strip().split("\n")[4:]] ) return output_test_string.strip() elif is_html: output_test_string = "\n".join(output.strip().split("\n")) return output_test_string @pytest.mark.parametrize( "capture_repr, code, expected_out", [ pytest.param(tuple(), "a=2\nb=3", "", id="assign,()"), pytest.param(tuple(), "a=2\na", "", id="var,()"), pytest.param(tuple(), "a=2\nprint(a)", "2", id="print(var),()"), pytest.param(tuple(), 'print("hello")\na=2\na', "hello", id="print+var,()"), pytest.param(("__repr__",), "a=2\nb=3", "", id="assign,(repr)"), pytest.param(("__repr__",), "a=2\na", "2", id="var,(repr)"), pytest.param(("__repr__",), "a=2\nprint(a)", "2", id="print(var),(repr)"), pytest.param( ("__repr__",), 'print("hello")\na=2\na', "hello\n\n2", id="print+var,(repr)" ), pytest.param( ("__repr__",), code_repr_and_html, "This is the __repr__", id="repr_and_html,(repr)", ), pytest.param( ("__repr__",), code_print_and_repr_and_html, "print statement\n\nThis is the __repr__", id="print and repr_and_html,(repr)", ), pytest.param(("_repr_html_",), code_repr_only, "", id="repr_only,(html)"), pytest.param( ("_repr_html_",), code_repr_and_html, html_out, id="repr_and_html,(html)" ), pytest.param( ("_repr_html_",), code_print_and_repr_and_html, "".join([text_above_html, html_out]), id="print and repr_and_html,(html)", ), pytest.param( ("_repr_html_", "__repr__"), code_repr_and_html, html_out, id="repr_and_html,(html,repr)", ), pytest.param( ("__repr__", "_repr_html_"), code_repr_and_html, "This is the __repr__", id="repr_and_html,(repr,html)", ), pytest.param( ("_repr_html_", "__repr__"), code_repr_only, "This is the __repr__", id="repr_only,(html,repr)", ), pytest.param(("_repr_html_",), code_plt, "", id="html_none"), ], ) def test_capture_repr( gallery_conf, capture_repr, code, expected_out, req_mpl, req_pil, script_vars ): """Tests output capturing with various capture_repr settings.""" compiler = codeop.Compile() code_block = ("code", code, 1) gallery_conf["capture_repr"] = capture_repr file_conf = {} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) assert _clean_output(output) == expected_out @pytest.mark.parametrize( "caprepr_gallery, caprepr_file, expected_out", [ pytest.param(tuple(), ("__repr__",), "2", id="() --> repr"), pytest.param(("__repr__",), "()", "", id="repr --> ()"), ], ) def test_per_file_capture_repr( gallery_conf, caprepr_gallery, caprepr_file, expected_out, req_mpl, req_pil, script_vars, ): """Tests that per file capture_repr overrides gallery_conf.""" compiler = codeop.Compile() code_block = ("code", "a=2\n2", 1) gallery_conf["capture_repr"] = caprepr_gallery file_conf = {"capture_repr": caprepr_file} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) assert _clean_output(output) == expected_out def test_ignore_repr_types(gallery_conf, req_mpl, req_pil, script_vars): """Tests output capturing with various capture_repr settings.""" compiler = codeop.Compile() code_block = ("code", "a=2\na", 1) gallery_conf["ignore_repr_types"] = r"int" file_conf = {} output = sg.execute_code_block( compiler, code_block, None, script_vars, gallery_conf, file_conf ) assert _clean_output(output) == "" @pytest.mark.parametrize( ("order", "call_count"), [("before", 1), ("after", 1), ("both", 2)] ) def test_reset_module_order_2_param(gallery_conf, order, call_count, req_pil): """Test that reset module with 2 parameters.""" def cleanup_2_param(gallery_conf, fname): pass mock_reset_module = mock.create_autospec(cleanup_2_param) gallery_conf["reset_modules"] = (mock_reset_module,) gallery_conf["reset_modules_order"] = order _generate_rst(gallery_conf, "plot_test.py", CONTENT) assert mock_reset_module.call_count == call_count @pytest.mark.parametrize( ("order", "call_count", "expected_call_order"), [ ("before", 1, ("before",)), ("after", 1, ("after",)), ("both", 2, ("before", "after")), ], ) def test_reset_module_order_3_param( gallery_conf, order, call_count, expected_call_order, req_pil ): """Test reset module with 3 parameters.""" def cleanup_3_param(gallery_conf, fname, when): pass mock_reset_module = mock.create_autospec(cleanup_3_param) gallery_conf["reset_modules"] = (mock_reset_module,) gallery_conf["reset_modules_order"] = order _generate_rst(gallery_conf, "plot_test.py", CONTENT) assert mock_reset_module.call_count == call_count expected_calls = [ mock.call(mock.ANY, mock.ANY, order) for order in expected_call_order ] mock_reset_module.assert_has_calls(expected_calls) def test_reset_module_order_3_param_invalid_when(gallery_conf): """Test reset module with unknown 3rd parameter.""" def cleanup_3_param(gallery_conf, fname, invalid): pass mock_reset_module = mock.create_autospec(cleanup_3_param) gallery_conf["reset_modules"] = (mock_reset_module,) gallery_conf["reset_modules_order"] = "before" with pytest.raises( ValueError, match=("3rd parameter in cleanup_3_param " "function signature must be 'when'"), ): _generate_rst(gallery_conf, "plot_test.py", CONTENT) assert mock_reset_module.call_count == 0 @pytest.fixture def log_collector_wrap(log_collector): """Wrap our log_collector.""" src_filename = "source file name" tee = sg._LoggingTee(src_filename) output_file = tee.output yield log_collector, src_filename, tee, output_file def test_full_line(log_collector_wrap): # A full line is output immediately. log_collector, src_filename, tee, output_file = log_collector_wrap tee.write("Output\n") tee.flush() assert output_file.getvalue() == "Output\n" assert log_collector.verbose.call_count == 2 assert src_filename in log_collector.verbose.call_args_list[0][0] assert "Output" in log_collector.verbose.call_args_list[1][0] def test_incomplete_line_with_flush(log_collector_wrap): # An incomplete line ... log_collector, src_filename, tee, output_file = log_collector_wrap tee.write("Output") assert output_file.getvalue() == "Output" log_collector.verbose.assert_called_once() assert src_filename in log_collector.verbose.call_args[0] # ... should appear when flushed. tee.flush() assert log_collector.verbose.call_count == 2 assert "Output" in log_collector.verbose.call_args_list[1][0] def test_incomplete_line_with_more_output(log_collector_wrap): # An incomplete line ... log_collector, src_filename, tee, output_file = log_collector_wrap tee.write("Output") assert output_file.getvalue() == "Output" log_collector.verbose.assert_called_once() assert src_filename in log_collector.verbose.call_args[0] # ... should appear when more data is written. tee.write("\nMore output\n") assert output_file.getvalue() == "Output\nMore output\n" assert log_collector.verbose.call_count == 3 assert "Output" in log_collector.verbose.call_args_list[1][0] assert "More output" in log_collector.verbose.call_args_list[2][0] def test_multi_line(log_collector_wrap): log_collector, src_filename, tee, output_file = log_collector_wrap tee.write("first line\rsecond line\nthird line") assert output_file.getvalue() == "first line\rsecond line\nthird line" assert log_collector.verbose.call_count == 3 assert src_filename in log_collector.verbose.call_args_list[0][0] assert "first line" in log_collector.verbose.call_args_list[1][0] assert "second line" in log_collector.verbose.call_args_list[2][0] assert tee.logger_buffer == "third line" def test_textio_compat(log_collector_wrap): _, _, tee, _ = log_collector_wrap assert not tee.readable() assert not tee.seekable() assert tee.writable() def test_fileno(log_collector_wrap): _, _, tee, _ = log_collector_wrap with pytest.raises(io.UnsupportedOperation): tee.fileno() def test_isatty(monkeypatch, log_collector_wrap): _, _, tee, _ = log_collector_wrap assert not tee.isatty() monkeypatch.setattr(tee.output, "isatty", lambda: True) assert tee.isatty() def test_tell(monkeypatch, log_collector_wrap): _, _, tee, _ = log_collector_wrap assert tee.tell() == tee.output.tell() monkeypatch.setattr(tee.output, "tell", lambda: 42) assert tee.tell() == tee.output.tell() def test_errors(log_collector_wrap): _, _, tee, _ = log_collector_wrap assert tee.errors == tee.output.errors def test_newlines(log_collector_wrap): _, _, tee, _ = log_collector_wrap assert tee.newlines == tee.output.newlines # TODO: test that broken thumbnail does appear when needed # TODO: test that examples are executed after a no-plot and produce # the correct image in the thumbnail sphinx-gallery-0.16.0/sphinx_gallery/tests/test_interactive_example.py000066400000000000000000000217611461331107500264070ustar00rootroot00000000000000# Author: Chris Holdgraf # License: 3-clause BSD """Testing the binder badge functionality.""" from copy import deepcopy import os import re from unittest.mock import Mock import pytest from sphinx.application import Sphinx from sphinx.errors import ConfigError from sphinx_gallery.interactive_example import ( gen_binder_url, check_binder_conf, _copy_binder_reqs, gen_binder_rst, gen_jupyterlite_rst, check_jupyterlite_conf, ) def test_binder(): """Testing binder URL generation and checks.""" file_path = "blahblah/mydir/myfile.py" conf_base = { "binderhub_url": "http://test1.com", "org": "org", "repo": "repo", "branch": "branch", "dependencies": "../requirements.txt", } conf_base = check_binder_conf(conf_base) gallery_conf_base = {"gallery_dirs": "mydir", "src_dir": "blahblah"} url = gen_binder_url(file_path, conf_base, gallery_conf_base) expected = ( "http://test1.com/v2/gh/org/repo/" "branch?filepath=notebooks/mydir/myfile.ipynb" ) assert url == expected # Assert url quoted correctly conf0 = deepcopy(conf_base) special_file_path = "blahblah/mydir/files_&_stuff.py" conf0["branch"] = "100%_tested" url = gen_binder_url(special_file_path, conf0, gallery_conf_base) expected = ( "http://test1.com/v2/gh/org/repo/" "100%25_tested?filepath=notebooks/mydir/files_%26_stuff.ipynb" ) assert url == expected # Assert filepath prefix is added prefix = "my_prefix/foo" conf1 = deepcopy(conf_base) conf1["filepath_prefix"] = prefix url = gen_binder_url(file_path, conf1, gallery_conf_base) expected = ( "http://test1.com/v2/gh/org/repo/" "branch?filepath={}/notebooks/" "mydir/myfile.ipynb" ).format(prefix) assert url == expected conf1.pop("filepath_prefix") # URL must have http conf2 = deepcopy(conf1) conf2["binderhub_url"] = "test1.com" with pytest.raises(ConfigError, match="did not supply a valid url"): url = check_binder_conf(conf2) # Assert missing params optional_keys = ["filepath_prefix", "notebooks_dir", "use_jupyter_lab"] for key in conf1.keys(): if key in optional_keys: continue conf3 = deepcopy(conf1) conf3.pop(key) with pytest.raises(ConfigError, match="binder_conf is missing values"): url = check_binder_conf(conf3) # Dependencies file dependency_file_tests = ["requirements_not.txt", "doc-requirements.txt"] for ifile in dependency_file_tests: conf3 = deepcopy(conf1) conf3["dependencies"] = ifile with pytest.raises( ConfigError, match=r"Did not find one of `requirements.txt` " "or `environment.yml`", ): url = check_binder_conf(conf3) conf6 = deepcopy(conf1) conf6["dependencies"] = {"test": "test"} with pytest.raises( ConfigError, match="`dependencies` value should be a " "list of strings" ): url = check_binder_conf(conf6) # Missing requirements file should raise an error conf7 = deepcopy(conf1) conf7["dependencies"] = ["requirements.txt", "this_doesntexist.txt"] # Hack to test the case when dependencies point to a non-existing file # w/o needing to do a full build def apptmp(): pass apptmp.srcdir = "/" with pytest.raises( ConfigError, match="Couldn't find the Binder " "requirements file" ): url = _copy_binder_reqs(apptmp, conf7) # Check returns the correct object conf4 = check_binder_conf({}) conf5 = check_binder_conf(None) for iconf in [conf4, conf5]: assert iconf == {} # Assert extra unknown params conf7 = deepcopy(conf1) conf7["foo"] = "blah" with pytest.raises(ConfigError, match="Unknown Binder config key"): url = check_binder_conf(conf7) # Assert using lab correctly changes URL conf_lab = deepcopy(conf_base) conf_lab["use_jupyter_lab"] = True url = gen_binder_url(file_path, conf_lab, gallery_conf_base) expected = ( "http://test1.com/v2/gh/org/repo/" "branch?urlpath=lab/tree/notebooks/mydir/myfile.ipynb" ) assert url == expected # Assert using static folder correctly changes URL conf_static = deepcopy(conf_base) file_path = "blahblah/mydir/myfolder/myfile.py" conf_static["notebooks_dir"] = "ntbk_folder" url = gen_binder_url(file_path, conf_static, gallery_conf_base) expected = ( "http://test1.com/v2/gh/org/repo/" "branch?filepath=ntbk_folder/mydir/myfolder/myfile.ipynb" ) assert url == expected def test_gen_binder_rst(tmpdir): """Check binder rst generated correctly.""" gallery_conf_base = {"gallery_dirs": [str(tmpdir)], "src_dir": "blahblah"} file_path = str(tmpdir.join("blahblah", "mydir", "myfile.py")) conf_base = { "binderhub_url": "http://test1.com", "org": "org", "repo": "repo", "branch": "branch", "dependencies": "../requirements.txt", } conf_base = check_binder_conf(conf_base) orig_dir = os.getcwd() os.chdir(str(tmpdir)) try: rst = gen_binder_rst(file_path, conf_base, gallery_conf_base) finally: os.chdir(orig_dir) image_rst = " .. image:: images/binder_badge_logo.svg" target_rst = ":target: http://test1.com/v2/gh/org/repo/branch?filepath=notebooks/mydir/myfile.ipynb" # noqa E501 alt_rst = ":alt: Launch binder" assert image_rst in rst assert target_rst in rst assert alt_rst in rst image_fname = os.path.join( os.path.dirname(file_path), "images", "binder_badge_logo.svg" ) assert os.path.isfile(image_fname) @pytest.mark.parametrize("use_jupyter_lab", [True, False]) @pytest.mark.parametrize( "example_file", [ os.path.join("example_dir", "myfile.py"), os.path.join("example_dir", "subdir", "myfile.py"), ], ) def test_gen_jupyterlite_rst(use_jupyter_lab, example_file, tmpdir): """Check binder rst generated correctly.""" gallery_conf = { "gallery_dirs": [str(tmpdir)], "src_dir": "blahblah", "jupyterlite": {"use_jupyter_lab": use_jupyter_lab}, } file_path = str(tmpdir.join("blahblah", example_file)) orig_dir = os.getcwd() os.chdir(str(tmpdir)) try: rst = gen_jupyterlite_rst(file_path, gallery_conf) finally: os.chdir(orig_dir) image_rst = " .. image:: images/jupyterlite_badge_logo.svg" target_rst_template = ( ":target: {root_url}/lite/{jupyter_part}.+path={notebook_path}" ) if "subdir" not in file_path: root_url = r"\.\." notebook_path = r"example_dir/myfile\.ipynb" else: root_url = r"\.\./\.\." notebook_path = r"example_dir/subdir/myfile\.ipynb" if use_jupyter_lab: jupyter_part = "lab" else: jupyter_part = "retro/notebooks" target_rst = target_rst_template.format( root_url=root_url, jupyter_part=jupyter_part, notebook_path=notebook_path ) alt_rst = ":alt: Launch JupyterLite" assert image_rst in rst assert re.search(target_rst, rst), rst assert alt_rst in rst image_fname = os.path.join( os.path.dirname(file_path), "images", "jupyterlite_badge_logo.svg" ) assert os.path.isfile(image_fname) def test_check_jupyterlite_conf(): app = Mock( spec=Sphinx, config=dict(source_suffix={".rst": None}), extensions=[], srcdir="srcdir", ) assert check_jupyterlite_conf(None, app) is None assert check_jupyterlite_conf({}, app) is None app.extensions = ["jupyterlite_sphinx"] assert check_jupyterlite_conf(None, app) is None assert check_jupyterlite_conf({}, app) == { "jupyterlite_contents": os.path.join("srcdir", "jupyterlite_contents"), "use_jupyter_lab": True, "notebook_modification_function": None, } conf = { "jupyterlite_contents": "this_is_the_contents_dir", "use_jupyter_lab": False, } expected = { "jupyterlite_contents": os.path.join("srcdir", "this_is_the_contents_dir"), "use_jupyter_lab": False, "notebook_modification_function": None, } assert check_jupyterlite_conf(conf, app) == expected def notebook_modification_function(notebook_content, notebook_filename): pass conf = {"notebook_modification_function": notebook_modification_function} expected = { "jupyterlite_contents": os.path.join("srcdir", "jupyterlite_contents"), "use_jupyter_lab": True, "notebook_modification_function": notebook_modification_function, } assert check_jupyterlite_conf(conf, app) == expected match = ( "Found.+unknown keys.+another_unknown_key.+unknown_key.+" "Allowed keys are.+jupyterlite_contents.+" "notebook_modification_function.+use_jupyter_lab" ) with pytest.raises(ConfigError, match=match): check_jupyterlite_conf( {"unknown_key": "value", "another_unknown_key": "another_value"}, app ) sphinx-gallery-0.16.0/sphinx_gallery/tests/test_load_style.py000066400000000000000000000013161461331107500245100ustar00rootroot00000000000000"""Testing sphinx_gallery.load_style extension.""" import os import pytest @pytest.mark.conf_file(extensions=["sphinx_gallery.load_style"]) def test_load_style(sphinx_app_wrapper): """Testing that style loads properly.""" sphinx_app = sphinx_app_wrapper.build_sphinx_app() cfg = sphinx_app.config assert cfg.project == "Sphinx-Gallery " build_warn = sphinx_app._warning.getvalue() assert build_warn == "" index_html = os.path.join(sphinx_app_wrapper.outdir, "index.html") assert os.path.isfile(index_html) with open(index_html) as fid: content = fid.read() assert ( 'link rel="stylesheet" type="text/css" href="_static/sg_gallery.css' in content ) sphinx-gallery-0.16.0/sphinx_gallery/tests/test_notebook.py000066400000000000000000000273571461331107500242060ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD r"""Testing the Jupyter notebook parser.""" from collections import defaultdict from itertools import count from pathlib import Path import json import tempfile import os import pytest import re import base64 import shutil import textwrap from sphinx.errors import ExtensionError from sphinx_gallery.py_source_parser import split_code_and_text_blocks from sphinx_gallery.notebook import ( rst2md, jupyter_notebook, save_notebook, promote_jupyter_cell_magic, python_to_jupyter_cli, ) root = Path(__file__).parents[2] def test_latex_conversion(gallery_conf): """Test Latex parsing from rst into Jupyter Markdown.""" double_inline_rst = r":math:`T<0` and :math:`U>0`" double_inline_jmd = r"$T<0$ and $U>0$" assert double_inline_jmd == rst2md(double_inline_rst, gallery_conf, "", {}) align_eq = r""" .. math:: \mathcal{H} &= 0 \\ \mathcal{G} &= D""" align_eq_jmd = r""" \begin{align}\mathcal{H} &= 0 \\ \mathcal{G} &= D\end{align}""" assert align_eq_jmd == rst2md(align_eq, gallery_conf, "", {}) def test_code_conversion(): """Test `rst2md` results in ``` code format so Jupyter syntax highlighting works.""" rst = textwrap.dedent( """ Regular text .. code-block:: # Bash code More regular text .. code-block:: cpp //cpp code //more cpp code non-indented code blocks are not valid .. code-block:: cpp // not a real code block """ ) assert rst2md(rst, {}, "", {}) == textwrap.dedent( """ Regular text ``` # Bash code ``` More regular text ```cpp //cpp code //more cpp code ``` non-indented code blocks are not valid .. code-block:: cpp // not a real code block """ ) def test_convert(gallery_conf): """Test reST conversion to markdown.""" rst = """hello .. contents:: :local: This is :math:`some` math :math:`stuff`. .. note:: Interpolation is a linear operation that can be performed also on Raw and Epochs objects. .. warning:: Go away For more details on interpolation see the page :ref:`channel_interpolation`. .. _foo: bar .. image:: foobar :alt: me :whatever: you :width: 200px :class: img_class `See more `_. """ markdown = """hello This is $some$ math $stuff$.

Note

Interpolation is a linear operation that can be performed also on Raw and Epochs objects.

Warning

Go away

For more details on interpolation see the page `channel_interpolation`. me [See more](https://en.wikipedia.org/wiki/Interpolation). """ # noqa assert rst2md(rst, gallery_conf, "", {}) == markdown def test_headings(): rst = textwrap.dedent( """\ ========= Heading 1 ========= Heading 2 ========= ============= Heading 1-2 ============= Heading 3 --------- ============= Not a Heading ------------- Mismatch top and bottom Not another heading -=-=-=-=-=-=-=-=-=- Multiple characters ------- Bad heading but okay ------------- Over and under mismatch, not rendered and warning raised by Sphinx Another bad heading, but passable ^^^^^^^^^^^^^^^^^^^^^ Too short, warning raised but is rendered by Sphinx A * BC ** Some text And then a heading ------------------ Not valid with no blank line above ======================= Centered ======================= ------------------------ ------------------------ Blank heading above. {0} ==================== White space above ==================== """ ).format(" ") # add whitespace afterward to avoid editors from automatically # removing the whitespace on save heading_level_counter = count(start=1) heading_levels = defaultdict(lambda: next(heading_level_counter)) text = rst2md(rst, {}, "", heading_levels) assert text.startswith("# Heading 1\n") assert "\n## Heading 2\n" in text assert "\n# Heading 1-2\n" in text assert "\n### Heading 3\n" in text assert "# Not a Heading" not in text assert "# Not another Heading" not in text assert "\n#### Bad heading but okay\n" in text assert "\n##### Another bad heading, but passable\n" in text assert "\n###### A\n" in text assert "\n###### BC\n" in text assert "# And then a heading\n" not in text assert "\n# Centered\n" in text assert "#\nBlank heading above." not in text assert "# White space above\n" in text def test_cell_magic_promotion(): markdown = textwrap.dedent( """\ # Should be rendered as text ``` bash # This should be rendered as normal ``` ``` bash %%bash # bash magic ``` ```cpp %%writefile out.cpp // This c++ cell magic will write a file // There should NOT be a text block above this ``` Interspersed text block ```javascript %%javascript // Should also be a code block // There should NOT be a trailing text block after this ``` """ ) work_notebook = {"cells": []} promote_jupyter_cell_magic(work_notebook, markdown) cells = work_notebook["cells"] assert len(cells) == 5 assert cells[0]["cell_type"] == "markdown" assert "``` bash" in cells[0]["source"][0] assert cells[1]["cell_type"] == "code" assert cells[1]["source"][0] == "%%bash\n# bash magic" assert cells[2]["cell_type"] == "code" assert cells[3]["cell_type"] == "markdown" assert cells[3]["source"][0] == "Interspersed text block" assert cells[4]["cell_type"] == "code" @pytest.mark.parametrize( "rst_path,md_path,prefix_enabled", ( ("../_static/image.png", "file://../_static/image.png", False), ("/_static/image.png", "file://_static/image.png", False), ("../_static/image.png", "https://example.com/_static/image.png", True), ("/_static/image.png", "https://example.com/_static/image.png", True), ("https://example.com/image.png", "https://example.com/image.png", False), ("https://example.com/image.png", "https://example.com/image.png", True), ), ids=( "rel_no_prefix", "abs_no_prefix", "rel_prefix", "abs_prefix", "url_no_prefix", "url_prefix", ), ) def test_notebook_images_prefix(gallery_conf, rst_path, md_path, prefix_enabled): if prefix_enabled: gallery_conf = gallery_conf.copy() gallery_conf["notebook_images"] = "https://example.com/" target_dir = os.path.join(gallery_conf["src_dir"], gallery_conf["gallery_dirs"]) rst = textwrap.dedent( """\ .. image:: {} :alt: My Image :width: 100px :height: 200px :class: image """ ).format(rst_path) markdown = rst2md(rst, gallery_conf, target_dir, {}) assert f'src="{md_path}"' in markdown assert 'alt="My Image"' in markdown assert 'width="100px"' in markdown assert 'height="200px"' in markdown assert 'class="image"' in markdown def test_notebook_images_data_uri(gallery_conf): gallery_conf = gallery_conf.copy() gallery_conf["notebook_images"] = True target_dir = os.path.join(gallery_conf["src_dir"], gallery_conf["gallery_dirs"]) test_image = os.path.join( os.path.dirname(__file__), "tinybuild", "doc", "_static_nonstandard", "demo.png" ) # For windows we need to copy this to tmpdir because if tmpdir and this # file are on different drives there is no relpath between them dest_dir = os.path.join(gallery_conf["src_dir"], "_static_nonstandard") os.mkdir(dest_dir) dest_image = os.path.join(dest_dir, "demo.png") shutil.copyfile(test_image, dest_image) # Make into "absolute" path from source directory test_image_rel = os.path.relpath(dest_image, gallery_conf["src_dir"]) test_image_abs = "/" + test_image_rel.replace(os.sep, "/") rst = textwrap.dedent( """\ .. image:: {} :width: 100px """ ).format(test_image_abs) markdown = rst2md(rst, gallery_conf, target_dir, {}) assert "data" in markdown assert 'src="data:image/png;base64,' in markdown with open(test_image, "rb") as test_file: data = base64.b64encode(test_file.read()) assert data.decode("ascii") in markdown rst = textwrap.dedent( """\ .. image:: /this/image/is/missing.png :width: 500px """ ) with pytest.raises(ExtensionError): rst2md(rst, gallery_conf, target_dir, {}) def test_jupyter_notebook(gallery_conf): """Test that written ipython notebook file corresponds to python object.""" file_conf, blocks = split_code_and_text_blocks(root / "tutorials" / "plot_parse.py") target_dir = "tutorials" example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) with tempfile.NamedTemporaryFile("w", delete=False) as f: save_notebook(example_nb, f.name) try: with open(f.name) as fname: assert json.load(fname) == example_nb finally: os.remove(f.name) # Test custom first cell text test_text = "# testing\n%matplotlib notebook" gallery_conf["first_notebook_cell"] = test_text example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) assert example_nb.get("cells")[0]["source"][0] == test_text # Test empty first cell text test_text = None gallery_conf["first_notebook_cell"] = test_text example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) cell_src = example_nb.get("cells")[0]["source"][0] assert re.match("^[\n]?# Alternating text and code", cell_src) # Test custom last cell text test_text = "# testing last cell" gallery_conf["last_notebook_cell"] = test_text example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) assert example_nb.get("cells")[-1]["source"][0] == test_text # Test empty first cell text test_text = None gallery_conf["last_notebook_cell"] = test_text example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) cell_src = example_nb.get("cells")[-1]["source"][0] assert re.match("^Last text block.\n\nThat[\\\\]?'s all folks !", cell_src) # Test Jupyter magic code blocks are promoted gallery_conf["promote_jupyter_magic"] = True example_nb = jupyter_notebook(blocks, gallery_conf, target_dir) bash_block = example_nb.get("cells")[-2] assert bash_block["cell_type"] == "code" assert bash_block["source"][0] == "%%bash\n# This could be run!" # Test text above Jupyter magic code blocks is intact md_above_bash_block = example_nb.get("cells")[-3] assert md_above_bash_block["cell_type"] == "markdown" assert "Code blocks containing" in md_above_bash_block["source"][0] ############################################################################### # Notebook shell utility def test_with_empty_args(): """User passes no args, should fail with `SystemExit`.""" with pytest.raises(SystemExit): python_to_jupyter_cli([]) def test_missing_file(): """User passes non existing file, should fail with `FileNotFoundError`.""" with pytest.raises(FileNotFoundError) as excinfo: python_to_jupyter_cli(["nofile.py"]) excinfo.match(r"No such file or directory.+nofile\.py") def test_file_is_generated(tmp_path): """Check notebook file created when user passes good python file.""" out = str(tmp_path / "plot_0_sin.py") shutil.copyfile(root / "examples" / "plot_0_sin.py", out) python_to_jupyter_cli([out]) assert os.path.isfile(f"{out[:-3]}.ipynb") sphinx-gallery-0.16.0/sphinx_gallery/tests/test_py_source_parser.py000066400000000000000000000073731461331107500257460ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD r"""Test source parser.""" import os.path as op import pytest import textwrap from sphinx.errors import ExtensionError import sphinx_gallery.py_source_parser as sg def test_get_docstring_and_rest(unicode_sample, tmpdir, monkeypatch): """Test `_get_docstring_and_rest` correctly splits docstring and rest of example.""" docstring, rest, lineno, _ = sg._get_docstring_and_rest(unicode_sample) assert "Únicode" in docstring assert "heiß" in rest # degenerate fname = op.join(str(tmpdir), "temp") with open(fname, "w") as fid: fid.write('print("hello")\n') with pytest.raises(ExtensionError, match="Could not find docstring"): sg._get_docstring_and_rest(fname) with open(fname, "w") as fid: fid.write("print hello\n") assert sg._get_docstring_and_rest(fname)[0] == sg.SYNTAX_ERROR_DOCSTRING monkeypatch.setattr(sg, "parse_source_file", lambda x: ("", None)) with pytest.raises(ExtensionError, match="only supports modules"): sg._get_docstring_and_rest("") @pytest.mark.parametrize( "content, file_conf", [ ("No config\nin here.", {}), ("# sphinx_gallery_line_numbers = True", {"line_numbers": True}), (" # sphinx_gallery_line_numbers = True ", {"line_numbers": True}), ("#sphinx_gallery_line_numbers=True", {"line_numbers": True}), ("#sphinx_gallery_thumbnail_number\n=\n5", {"thumbnail_number": 5}), ("#sphinx_gallery_thumbnail_number=1foo", None), ("# sphinx_gallery_defer_figures", {}), ], ) def test_extract_file_config(content, file_conf, log_collector): """Test file config correctly extracted.""" if file_conf is None: assert sg.extract_file_config(content) == {} log_collector.warning.assert_called_once() assert "1foo" == log_collector.warning.call_args[0][2] else: assert sg.extract_file_config(content) == file_conf log_collector.warning.assert_not_called() @pytest.mark.parametrize( "contents, result", [ ("No config\nin here.", "No config\nin here."), ("# sphinx_gallery_line_numbers = True", ""), (" # sphinx_gallery_line_numbers = True ", ""), ("#sphinx_gallery_line_numbers=True", ""), ("#sphinx_gallery_thumbnail_number\n=\n5", ""), ("a = 1\n# sphinx_gallery_line_numbers = True\nb = 1", "a = 1\nb = 1"), ("a = 1\n\n# sphinx_gallery_line_numbers = True\n\nb = 1", "a = 1\n\n\nb = 1"), ( "# comment\n# sphinx_gallery_line_numbers = True\n# comment 2", "# comment\n# comment 2", ), ("# sphinx_gallery_defer_figures", ""), ], ) def test_remove_config_comments(contents, result): assert sg.remove_config_comments(contents) == result def test_remove_ignore_comments(): normal_code = "# Regular code\n# should\n# be untouched!" assert sg.remove_ignore_blocks(normal_code) == normal_code mismatched_code = "x=5\n# sphinx_gallery_start_ignore\ny=4" with pytest.raises(ExtensionError) as error: sg.remove_ignore_blocks(mismatched_code) assert "must have a matching" in str(error) code_with_ignores = textwrap.dedent( """\ # Indented ignores should work # sphinx_gallery_start_ignore # The variable name should do nothing sphinx_gallery_end_ignore = 0 # sphinx_gallery_end_ignore # New line above should stay intact # sphinx_gallery_start_ignore # sphinx_gallery_end_ignore # Empty ignore blocks are fine too """ ) assert sg.remove_ignore_blocks(code_with_ignores) == textwrap.dedent( """\ # Indented ignores should work # New line above should stay intact # Empty ignore blocks are fine too """ ) sphinx-gallery-0.16.0/sphinx_gallery/tests/test_recommender.py000066400000000000000000000061051461331107500246520ustar00rootroot00000000000000# Author: Arturo Amor # License: 3-clause BSD """Test the example recommender plugin.""" import codecs import os import re import pytest import sphinx_gallery.gen_rst as sg from sphinx_gallery.recommender import ExampleRecommender, _write_recommendations def test_example_recommender_methods(): """Test dict_vectorizer and compute_tf_idf methods.""" np = pytest.importorskip("numpy") recommender = ExampleRecommender() D = [{"foo": 1, "bar": 2}, {"foo": 3, "baz": 1}] X = recommender.dict_vectorizer(D) expected_X = np.array([[2.0, 0.0, 1.0], [0.0, 1.0, 3.0]]) np.testing.assert_array_equal(X, expected_X) X_tfidf = recommender.compute_tf_idf(X) expected_X_tfidf = np.array( [[0.94215562, 0.0, 0.33517574], [0.0, 0.42423963, 0.90554997]] ) np.testing.assert_array_almost_equal(X_tfidf, expected_X_tfidf) def test_recommendation_files(gallery_conf): """Test generated files and that recommendations are relevant.""" pytest.importorskip("numpy") gallery_conf["recommender"].update( [("enable", True), ("rubric_header", "Custom header")] ) file_dict = { "fox_jumps_dog.py": "The quick brown fox jumped over the lazy dog", "dog_sleeps.py": "The lazy dog slept all day", "fox_eats_dog_food.py": "The quick brown fox ate the lazy dog's food", "dog_jumps_fox.py": "The quick dog jumped over the lazy fox", } for file_name, content in file_dict.items(): file_path = os.path.join(gallery_conf["gallery_dir"], file_name) with open(file_path, "w") as f: f.write(content) sg.save_rst_example("example_rst", file_path, 0, 0, gallery_conf) test_file = re.sub(r"\.py$", ".rst", file_path) recommendation_file = re.sub(r"\.py$", ".recommendations", file_name) with codecs.open(test_file) as f: rst = f.read() assert recommendation_file in rst py_files = [ fname for fname in os.listdir(gallery_conf["gallery_dir"]) if os.path.splitext(fname)[1] == ".py" ] gallery_py_files = [ os.path.join(gallery_conf["gallery_dir"], fname) for fname in py_files ] recommender = ExampleRecommender(n_examples=1, min_df=1) recommender.fit(gallery_py_files) recommended_example = recommender.predict(file_path) # dog_jumps_fox.py assert os.path.basename(recommended_example[0]) == "fox_jumps_dog.py" # _write_recommendations needs a thumbnail, for writing the # `_thumbnail_div` we then create a blank png thumb_path = os.path.join(gallery_conf["gallery_dir"], "images/thumb") os.makedirs(thumb_path, exist_ok=True) png_file = "sphx_glr_fox_jumps_dog_thumb.png" png_file_path = os.path.join(thumb_path, png_file) with open(png_file_path, "wb") as f: b"\x89PNG\r\n\x1a\n" # generic png file signature recommendation_file = re.sub(r"\.py$", ".recommendations", file_path) _write_recommendations(recommender, file_path, gallery_conf) with codecs.open(recommendation_file) as f: rst = f.read() assert ".. rubric:: Custom header" in rst sphinx-gallery-0.16.0/sphinx_gallery/tests/test_scrapers.py000066400000000000000000000230241461331107500241730ustar00rootroot00000000000000"""Testing image scrapers.""" import os import pytest from sphinx.errors import ConfigError, ExtensionError import sphinx_gallery from sphinx_gallery.gen_gallery import _fill_gallery_conf_defaults from sphinx_gallery.scrapers import ( figure_rst, SG_IMAGE, matplotlib_scraper, ImagePathIterator, save_figures, _KNOWN_IMG_EXTS, _reset_matplotlib, ) @pytest.fixture(scope="function") def make_gallery_conf(tmpdir): """Sets up a test sphinx-gallery configuration.""" # Skip if numpy not installed pytest.importorskip("numpy") def make_gallery_conf(init=None): gallery_conf = _fill_gallery_conf_defaults(init or {}) gallery_conf.update( src_dir=str(tmpdir), examples_dir=str(tmpdir), gallery_dir=str(tmpdir) ) return gallery_conf return make_gallery_conf class matplotlib_svg_scraper: """Test matplotlib svg scraper.""" def __repr__(self): return self.__class__.__name__ def __call__(self, *args, **kwargs): """Call matplotlib scraper with 'svg' format.""" return matplotlib_scraper(*args, format="svg", **kwargs) @pytest.mark.parametrize("ext", ("png", "svg")) def test_save_matplotlib_figures(make_gallery_conf, ext): """Test matplotlib figure save.""" gallery_conf = make_gallery_conf( {"image_scrapers": (matplotlib_svg_scraper(),)} if ext == "svg" else {} ) import matplotlib.pyplot as plt # nest these so that Agg can be set plt.plot(1, 1) fname_template = os.path.join(gallery_conf["gallery_dir"], "image{0}.png") image_path_iterator = ImagePathIterator(fname_template) block = ("",) * 3 block_vars = dict(image_path_iterator=image_path_iterator) image_rst = save_figures(block, block_vars, gallery_conf) assert len(image_path_iterator) == 1 fname = f"/image1.{ext}" assert fname in image_rst fname = gallery_conf["gallery_dir"] + fname assert os.path.isfile(fname) # Test capturing 2 images with shifted start number image_path_iterator.next() image_path_iterator.next() plt.plot(1, 1) plt.figure() plt.plot(1, 1) image_rst = save_figures(block, block_vars, gallery_conf) assert len(image_path_iterator) == 5 for ii in range(4, 6): fname = f"/image{ii}.{ext}" assert fname in image_rst fname = gallery_conf["gallery_dir"] + fname assert os.path.isfile(fname) def test_image_srcset_config(make_gallery_conf): with pytest.raises(ConfigError, match="image_srcset must be a list of strings"): make_gallery_conf({"image_srcset": "2x"}) with pytest.raises(ConfigError, match="Invalid value for image_srcset parameter"): make_gallery_conf({"image_srcset": [False]}) with pytest.raises(ConfigError, match="Invalid value for image_srcset parameter"): make_gallery_conf({"image_srcset": ["200"]}) conf = make_gallery_conf({"image_srcset": ["2x"]}) assert conf["image_srcset"] == [2.0] conf = make_gallery_conf({"image_srcset": ["1x", "2x"]}) assert conf["image_srcset"] == [2.0] # "1x" is implied. def test_save_matplotlib_figures_hidpi(make_gallery_conf): """Test matplotlib hidpi figure save.""" gallery_conf = make_gallery_conf({"image_srcset": ["2x"]}) ext = "png" import matplotlib.pyplot as plt # nest these so that Agg can be set plt.plot(1, 1) fname_template = os.path.join(gallery_conf["gallery_dir"], "image{0}.png") image_path_iterator = ImagePathIterator(fname_template) block = ("",) * 3 block_vars = dict(image_path_iterator=image_path_iterator) image_rst = save_figures(block, block_vars, gallery_conf) fname = f"/image1.{ext}" assert fname in image_rst assert f"/image1_2_00x.{ext} 2.00x" in image_rst assert len(image_path_iterator) == 1 fname = gallery_conf["gallery_dir"] + fname fnamehi = gallery_conf["gallery_dir"] + f"/image1_2_00x.{ext}" assert os.path.isfile(fname) assert os.path.isfile(fnamehi) # Test capturing 2 images with shifted start number image_path_iterator.next() image_path_iterator.next() plt.plot(1, 1) plt.figure() plt.plot(1, 1) image_rst = save_figures(block, block_vars, gallery_conf) assert len(image_path_iterator) == 5 for ii in range(4, 6): fname = f"/image{ii}.{ext}" assert fname in image_rst fname = gallery_conf["gallery_dir"] + fname assert os.path.isfile(fname) fname = f"/image{ii}_2_00x.{ext}" assert fname in image_rst fname = gallery_conf["gallery_dir"] + fname assert os.path.isfile(fname) def _custom_func(x, y, z): return y["image_path_iterator"].next() def test_custom_scraper(make_gallery_conf, monkeypatch): """Test custom scrapers.""" # Test the API contract for custom scrapers with monkeypatch.context() as m: m.setattr( sphinx_gallery, "_get_sg_image_scraper", lambda: _custom_func, raising=False ) for cust in (_custom_func, "sphinx_gallery"): # smoke test that it works make_gallery_conf({"image_scrapers": [cust]}) # degenerate # without the monkey patch to add sphinx_gallery._get_sg_image_scraper, # we should get an error with pytest.raises(ConfigError, match="Unknown string option"): make_gallery_conf({"image_scrapers": ["sphinx_gallery"]}) # other degenerate conditions with pytest.raises(ConfigError, match="Unknown string option for image_scraper"): make_gallery_conf({"image_scrapers": ["foo"]}) for cust, msg in [ (_custom_func, "did not produce expected image"), (lambda x, y, z: 1.0, "was not a string"), ]: conf = make_gallery_conf({"image_scrapers": [cust]}) fname_template = os.path.join(conf["gallery_dir"], "image{0}.png") image_path_iterator = ImagePathIterator(fname_template) block = ("",) * 3 block_vars = dict(image_path_iterator=image_path_iterator) with pytest.raises(ExtensionError, match=msg): save_figures(block, block_vars, conf) # degenerate string interface with monkeypatch.context() as m: m.setattr(sphinx_gallery, "_get_sg_image_scraper", "foo", raising=False) with pytest.raises(ConfigError, match="^Unknown string option for image_"): make_gallery_conf({"image_scrapers": ["sphinx_gallery"]}) with monkeypatch.context() as m: m.setattr(sphinx_gallery, "_get_sg_image_scraper", lambda: "foo", raising=False) with pytest.raises(ConfigError, match="craper.*must be callable"): make_gallery_conf({"image_scrapers": ["sphinx_gallery"]}) @pytest.mark.parametrize("ext", _KNOWN_IMG_EXTS) def test_figure_rst(ext): """Test reST generation of images.""" figure_list = ["sphx_glr_plot_1." + ext] image_rst = figure_rst(figure_list, ".") single_image = f""" .. image-sg:: /sphx_glr_plot_1.{ext} :alt: pl :srcset: /sphx_glr_plot_1.{ext} :class: sphx-glr-single-img """ assert image_rst == single_image image_rst = figure_rst(figure_list + ["second." + ext], ".") image_list_rst = f""" .. rst-class:: sphx-glr-horizontal * .. image-sg:: /sphx_glr_plot_1.{ext} :alt: pl :srcset: /sphx_glr_plot_1.{ext} :class: sphx-glr-multi-img * .. image-sg:: /second.{ext} :alt: pl :srcset: /second.{ext} :class: sphx-glr-multi-img """ assert image_rst == image_list_rst def test_figure_rst_path(): """Test figure path correct in figure reSt.""" # Tests issue #229 local_img = [os.path.join(os.getcwd(), "third.png")] image_rst = figure_rst(local_img, ".") single_image = SG_IMAGE % ("third.png", "", "/third.png") assert image_rst == single_image def test_figure_rst_srcset(): """Test reST generation of images with srcset paths correct.""" figure_list = ["sphx_glr_plot_1.png"] hipaths = [{0: "sphx_glr_plot_1.png", 2.0: "sphx_glr_plot_1_2_00.png"}] image_rst = figure_rst(figure_list, ".", srcsetpaths=hipaths) single_image = """ .. image-sg:: /sphx_glr_plot_1.png :alt: pl :srcset: /sphx_glr_plot_1.png, /sphx_glr_plot_1_2_00.png 2.00x :class: sphx-glr-single-img """ assert image_rst == single_image hipaths += [{0: "second.png", 2.0: "second_2_00.png"}] image_rst = figure_rst(figure_list + ["second.png"], ".", srcsetpaths=hipaths) image_list_rst = """ .. rst-class:: sphx-glr-horizontal * .. image-sg:: /sphx_glr_plot_1.png :alt: pl :srcset: /sphx_glr_plot_1.png, /sphx_glr_plot_1_2_00.png 2.00x :class: sphx-glr-multi-img * .. image-sg:: /second.png :alt: pl :srcset: /second.png, /second_2_00.png 2.00x :class: sphx-glr-multi-img """ assert image_rst == image_list_rst # test issue #229 local_img = [os.path.join(os.getcwd(), "third.png")] image_rst = figure_rst(local_img, ".") single_image = SG_IMAGE % ("third.png", "", "/third.png") assert image_rst == single_image def test_iterator(): """Test ImagePathIterator.""" ipi = ImagePathIterator("foo{0}") ipi._stop = 10 with pytest.raises(ExtensionError, match="10 images"): for ii in ipi: pass def test_reset_matplotlib(make_gallery_conf): """Test _reset_matplotlib.""" import matplotlib matplotlib.rcParams["lines.linewidth"] = 42 matplotlib.units.registry.clear() gallery_conf = make_gallery_conf() _reset_matplotlib(gallery_conf, "") assert matplotlib.rcParams["lines.linewidth"] != 42 assert len(matplotlib.units.registry) > 0 sphinx-gallery-0.16.0/sphinx_gallery/tests/test_sorting.py000066400000000000000000000050011461331107500240310ustar00rootroot00000000000000# Author: Óscar Nájera # License: 3-clause BSD r"""Tests for sorting keys on gallery (sub)sections.""" import os.path as op import pytest from sphinx.errors import ConfigError from sphinx_gallery.sorting import ( ExplicitOrder, NumberOfCodeLinesSortKey, FileNameSortKey, FileSizeSortKey, ExampleTitleSortKey, FunctionSortKey, ) def test_ExplicitOrder_sorting_key(): """Test ExplicitOrder.""" explicit_folders = ["f", "d"] key = ExplicitOrder(explicit_folders) sorted_folders = sorted(["d", "f"], key=key) assert sorted_folders == explicit_folders # str(obj) stability for sphinx non-rebuilds assert str(key).startswith("" out = sorter(op.basename(__file__)) assert isinstance(out, type_), type(out) def test_ExplicitOrder_sorting_wildcard(): # wildcard at start sorted_folders = sorted(list("abcd"), key=ExplicitOrder(["*", "b", "a"])) assert sorted_folders == ["c", "d", "b", "a"] # wildcard in the middle sorted_folders = sorted(list("abcde"), key=ExplicitOrder(["b", "a", "*", "c"])) assert sorted_folders == ["b", "a", "d", "e", "c"] # wildcard at end sorted_folders = sorted(list("abcd"), key=ExplicitOrder(["b", "a", "*"])) assert sorted_folders == ["b", "a", "c", "d"] def test_ExplicitOrder_sorting_errors(): # Test fails on wrong input with pytest.raises(ConfigError, match="ExplicitOrder sorting key takes a list"): ExplicitOrder("nope") # Test folder not listed in ExplicitOrder with pytest.raises(ConfigError, match="subsection folder .* was not found"): sorted(["a", "b", "c"], key=ExplicitOrder(["a", "b"])) def test_Function_sorting_key(): data = [(1, 0), (3, 2), (5, 4), (7, 6), (9, 8)] def f(x): return x[0] * x[1] sorter = FunctionSortKey(f) assert str(sorter).startswith("FunctionSortKey") assert sorted(data, key=f) == sorted(data, key=sorter) sorter_repr = FunctionSortKey(f, "f(x,y) = x*y") assert str(sorter_repr).startswith("f(x,y) = x*y") assert sorted(data, key=sorter_repr) == sorted(data, key=sorter) sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/000077500000000000000000000000001461331107500227475ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/index.rst000066400000000000000000000000711461331107500246060ustar00rootroot00000000000000Tiny test build =============== Minimal testing example sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/src/000077500000000000000000000000001461331107500235365ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/src/README.txt000066400000000000000000000000421461331107500252300ustar00rootroot00000000000000The Test Gallery ================ sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/src/plot_1.py000066400000000000000000000001601461331107500253030ustar00rootroot00000000000000""" ====== B test ====== :filename=1:title=2:lines=3:filesize=2: """ print("foo") print("bar") print("again") sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/src/plot_2.py000066400000000000000000000001241461331107500253040ustar00rootroot00000000000000""" ====== C test ====== :filename=2:title=3:lines=1:filesize=1: """ print("foo") sphinx-gallery-0.16.0/sphinx_gallery/tests/testconfs/src/plot_3.py000066400000000000000000000004471461331107500253150ustar00rootroot00000000000000#!/usr/bin/env python2 """ .. _extra_ref: =========================================================== A test with a really long title to make the filesize larger =========================================================== :filename=3:title=1:lines=2:filesize=3: """ print("foo") print("bar") sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/000077500000000000000000000000001461331107500227425ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/000077500000000000000000000000001461331107500235075ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/Makefile000066400000000000000000000010761461331107500251530ustar00rootroot00000000000000SPHINXOPTS=-v all: clean html show clean: rm -rf _build/* rm -rf auto_examples/ rm -rf auto_examples_with_rst/ rm -rf auto_examples_rst_index/ rm -rf gen_modules/ html: sphinx-build -b html -d _build/doctrees $(SPHINXOPTS) . _build/html latexpdf: sphinx-build -b latex -d _build/doctrees . _build/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C _build/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." show: @python -c "import webbrowser; webbrowser.open_new_tab('file://$(abspath .)/_build/html/index.html')" sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/_static_nonstandard/000077500000000000000000000000001461331107500275305ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/_static_nonstandard/demo.png000066400000000000000000007016011461331107500311670ustar00rootroot00000000000000PNG  IHDRrFsRGBgAMA a pHYseIDATx^XUpsq)4;FPTJ:[ x }k}8; (HHH`aa7M,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,Qa20 0 0ƾ,3 0 0 üM,QarJ VVkV4ܳI_V'^ը4ό,}.MGU}0 Pץ}Ѩ8na&%78@DDDDy/$ʴIX&mVNeըUP*RE.\k8 #˦'>6wriąAV+Y QTb% 21nܸD^3W}ƭ׾:J7<.14JE"k8uN\1P@0 0y*""""3}Y_TS*;wp5;{\ _<Gl:aBT#,_l/~in#c=vck/kYHQ3W|c7Ggqf ZhT֛{1-D}ktA9s[~/)DF_Si枨 ),cˈ8,[ؠ#ʘ;T}{4G(f8Gb-*蒽1u{qũ]FcT$PōQD(i3n;80 0y,""""3}Y~4Щ-l_NP s@)sg{y %MPV;1ӷ_ͧqןKaq D2> OEͺPJ=UZF>Sap1^tP"o5).󜺣Xw(bo2fs6 Ł^X`a&""""3}Y~(Zh#}z7vj&P\N#Jء*[z#7| ϊ vK$j9s;= oGAa=r+,Q&#axC KWrCUQCQ!>8}|ۇK/Tno*0m,30 ü7M,QakG1nX W{7ri 3q:r..\Y6`U1m1Q-V܊V\ {HE}PEWm=7߇*9 z i*j *xX`n`h l=!- 0 $78@DDDDy/˯ h;n(k ]Q*QĨ $!5YDdBpj ;Oy ,W[tE9c=S{^W͌'$qIFJ_[$c^3,0 0{ƾ,:JtJD=1^(Y10`ꈯcxAjTk8O<{qw wLQeuCy7Ÿ{Ɗð;FgdX`a&""""3}Y~eTZbaq-6pCs[T!"AD@(U$&ygr^XxT"JV-Qu/tT",0 0{ƾ,J0:aM,QaKRC珃  .W?t[ͷc׫D!9IU?.>ի7bU5WdZLYveP(岺L%4$$x2vm݆i Vck0MXRGR2JqѾ>kF׏W_EgFDu,^+WnmR,#ֿx:{aҌ5g9,:cD[N)E'SnY{p>x{idũk1{~X6r2$阹+0d }[QW0|Ĭ5<|夻unqq@6>7Nø`h3VbRgyMzIڃ e>wEX"kIr29!~.a-0XmŠMqCIIԾ}P;w ,9pA8U*1u\=M7`\z_ ĥXMTLPi8g,^m؏1Wc}8t"Tzrr"bm:k%F7_p ufL3wcDk j-RR5H~s1YZ ,m<ֵ w{nǬE~SĔW~2 0L~Jnbq c__9fnB. uEEX"OR He]݇MDKnJm"E١;~j-{MF۱V(t9O)x^1|5t1Opl* ?.^P8M8Lkms/w+m'ȫ%t)l2[~kT%A*͝g^#g sTo-{sv\IZ-k22M;rFճEf}. } y8_ڢ-A8pOlc1ܽVSgKp@0qQ0fys3G2E1qC_UPjG慪frA샽aRuPbLPv 0bXa2&feޫjZ5 ձEqo9Cv>F1 f(kbz^&'צn>n1Fr ύQLoI3&PJ\Yuah?RfnOhB1!^8PmD|=jvέ׊#,IIxq]1\=pblo`Rb%M=`5v7.>U"9[_)T: +7 `.Q#WFBeBX Fò㰘lk93M[QAUp/VJ ]r<.Aծq!>v(~0Ca1 עe!J7fV`X8j$cK_Ư-;2bR0{"Yv8xCK+ (c?^}c{|Y3};z (ǚX%MlQY_8ۂAO+p0&MPJ~wHk1.S6r엨gmي3JhlO1c_dq`WrJ}0OF\< !P|-a=Mnbq c_s݉6D9f劲ۡVF;kZTJ}/`K;|n&aM[c#XqNFm]P3Jֳםcۭh܆/)tF?ճѤ+JINs{3qidr7ĬY2(#l};Tl3=x W_lf`AZ5XVnX=J(}qXPA=79pF!JhX fL:x#˾n u*ĘrAzҼUlߏE+Vިa&mS%̽`=<e{<_oCX Vmُ0P'j[;~bal;vOm\Q -FqssL1`cHqB !:쏪uD;T^:B3]BRR .r,L=G4B8<9cDǍzaLY 6"}"!9ʚ٣|.btٮ2Z(m,_/4芪6(l WcXq;> -[;szFl L; 'ߚc]X XzŷڡaR6Ntb֫+lBzaJxk1nj#~r@YyV ڣM%GioUc\D}ҥ3*b,ܴW8e+0h`˔4c\~G4"*88G ݆_Ob֜9prJ[Ӧ0d}_v3 0LInbq c_sR 2g7͇ym9i&Nh;~'B6wT񑸵oZ5Q5_=DBFޛ['q|tb*6pBCB J&iD=w4kOޅ~pYc#pXv=PYlw+GlqT*b?ErZlo=hZջxX9]MrQն}nF%^ރڣ3׷C5Xw=Jߚd=*Z *y.1[ϼJ@6;OBm-oդ"2~b=7tE)qL]Cj$eWTj5n BNCOqŸ֯ʉfʚ9 ^cK'H ؼd,ۡPa< Almy82&|:<~# cJ+SǸq6, +'KOL^xܷ+*;x"^;ah@7ދA(/r;cAS~EŐ(i(h8u? rӰ<}.>On]y"ȵ.0 &(0e9Ǩ4H%QC8 ĿH联d)@p }! :$4ШTh~/E.>VVN| X}q/6'j!ahPXu l7J?Z`_05g}0woNqƿXpE9 gh=3>EhgZOZۨ夯FvƷZGe9A+S=t:<\??6Oۦ m{Ee_/*$Bqv"X53Lr5q@KV$:헓J j(<}&'FQa .FeY|J# y3[d;1%Un ˩8 ǜ* .@ӖPE;~7BHJL) -PEZLĜ3c: K7D^ h3=zKن(k;=6@y3g_ RZY--7:ѿsSwTJoJ;iE<UC-O*hı Ƀ1- rBqh?"Sϯd^0GX{5zƘJ[J-Eu?έnmm`q"獊*-`qex(^mŹ yEAZgy&j|u:^MBrvNC-Qx1dg>aM,QarQiFzh(xqaQ8 ' c/DϑawM|!aZ'4z$\ÂPPaV\VZP{LO(c儢 kE+9 gEFP EzcQ}y*ʫ6l؞8uBM"Ðw3tcHE[pZ9mJd}]la"Y'8l e|,g/9@>Ɗny{ҢC;ŚDqƆ)#𫩝/{1vChzp|'ڢ#n#8s^,rr 딡#J!'/ˈ~eFSPIF+T"{RjS@E"faM,Qar_r2%¨>IȹTc}Łj&bk*?6)p0"k`-GL6V}n'1MWh^X+GMJY %3aY8X~BeQ-TCzݚOz@vcS>%MLv1\q#vf=22“S EW~~2 0L>Onbq c_sR'8| 4&fql_ֶA lax9΋ZIj^¦+0hD6ͻ G0`j6=QHL{Yq@l^ [YNdʉz.puղՓnH7irBMOr(mZn+qImG/Dz>0e4~5G3BYo#'//x]0|2M"HWyg'2my9(2 `ݕeorrLp俳8 ּ"vN/kѭg%v@qs1atv,6/ŴYb6_ʤl&Mū=1b}<}iq zg.L y 1Vh5j'x]nVrCVqt 8-x~hiJ+kȦʋ6mTQ6z4H9EIVm2#vx#>)k|Ho؟`煪E8D t~btXI G{؎- pqxXu Xm(f(ajcIDW%+g8Ł3Z~[v7(uI:آy1l(6~_\碡}, y@ ykYv(,W{rǷmz4L)WU2FG0 05""""3}Y~Yz\؇ hnS1x Uy((UJ<%}P e+~v1+`|._{7cp]ŁY>P-דobuڡw[HPASKACyh+Atj9&)/M-QP,)Y3G`FqXツ-X*1 їKw1eeL]vy<WXkuTNy,ɱrؼ<ŵkq!̙: :hP8C+߂s28 QT1/q،;L&El3вI;&?#ˋ~Ņ*ͺ}Ca&?%78@DDDDy//REK=3 :CM\ǃ$h)(UPD^d8#̝HL{OB#g4eJ 8hԱC鿩8Wڹ0PJNNZH}7`b=WwSGZfͧ h  -Q{8ZœX?^2 _MP!"R^c'*~cB{o+Cr-,7ȳ r -cLjP<%/DfcI^AGw81ı8<<=&m&dxl x qi'tGq31v|{56_ @PPye9TJD!oY w{7W0\plbaarDDDDg&vaa=ʶeBmyJJ€3~pע2ibY'UЧ9hw Ḳ}ԵE(Y3:.8$*ok`øQMWY+{o2kAaenhf1"ⲞQ>Ry'=eY<67T>XNn˫IoŽqq@wI^6-1ov; -Pz6 A_9\:<;*7BǍ0\V`X>mB>A'Ł'8-X:s J{G1w'0C.+NaYcE8DPʥ (xFo8ʫsa=Onbq c__%:%ىnvijR]`=m/i_`⌼8*.~{g蓷i\FVȈ 0["Z\؃^.i)cj Qq_DMe&8 '";Ď\wX86Wl8MǎGFQpo%Eۗi;uG&=!ǛFᷖVGbX:Ҥ3ߞ_Y!ꕸpmU(kjӑ[j,w?kʝ(D$< \Who:m3,/'RȃUF4vO^ly‼%%M8u@ u!Y?yv*5.u1YgXwor0 üM,Qa+ 16jrKNY9Ewx-cԯw{!k/)Ip\Z:͜ gJe;cOD[qvśvけ‼eMPNOalowYHPi~ CU3;nN=QUnnƞd|oh0=޳j%X]&-4wS|cr+Y}_* ?1~@W/odb,9/V˗3wD푇q7$3t~eL~7ބNӱ1}{\Qq{YH|..#Øٕs\|Y3Hِ󲠤[ѩh/KW^|f{*^2a?M,QaD  tA1ÃQ儚 Xt,Yɸ[[;if5VPD(.'E-Pf  GjN~>}~瀲/+[[ v Łv-C?*U rB>yl`6 C2MN> :M8mZf["5u@[p5nq"m;siq@nkrN-b#uH_fV.(m[ּImrs$PWޞ%uAE7^㭅^mVbeAIlkpi,^^TZ$GP sxFEReL_c8]VBP<2OkaQhO$C * -J6|_JwMOMFZXD җŁ0;_}!`=*oPLZṣ́-mycQyB ^}ϋmYa&rDDDDgzQBǽVގ&mB+gTm;h*P(E|<_;=F 2/VH 3+QTNUgX܉J틓* _ǒ1]DiN(^o uȉנmY ^r4YoQcR؈hRr~kf\@.#/*=vl' J94SN *>}nr"3ֱJ;R~rD e8ū{Q4 ^Sp r!Fz|ZٶGY1+oET\Ƙɴ\XzOu"g@o^ L3d<>-EsݼPw*~.Q|Y k^rCSؕhj)E6J8ȵ8 ?8`a"fc@(o Sk煁|lW_#U G]bxm2^A \^ء(l<L9& @> :2iO\Fch0_u+lbSszM>u1S3va&&78@DDDDy/˯`YMo\VM g?\ˈ 7co4ufHȲ PJ\P>U| 3<PZNɉWw,>t"4 Р\n&KءGp&Kw8 qh*x *e*)c,|5˯ YюarLuUX}&<;`pn*7| T*DxcY?'-|y-ǰup? r>5* cc1e`nc1j1'abqlX< vm= RA| 4(?#bxw xpwv#1z)\ BWRa ?g% &Qg|譡#R=0l"T#1>'VFX~nh7Yq=R,d?c;-oqMゲfak &B@ ↫N~tAץQ.)KcP8临tDg@t":9MʿXAW6 &B%3'3g mĕ03Y(|>>o C)H^V/mj`@ 3߶G,v̚&iS?.Lm'|2]QT!YۛeLJEYi.ps|10 È&(0em]s&uGTkӴ_3&&YUGM.o ֝ @Be|"Q*>~;6vʹN9:kzهpdnTw_=/{\>{hCdn96ZFW8*/S$80%;WQyw4X{Łs73y88TԬgg.U#7uWiuזi!#xaGW]ۑ>isNqB bFN(k%iK{n>#v@$BO/&MP k(ϮtkAqs'-G8 ڡ r쉺qgȴ\u.p[xFn%H£n*bVmX&8p {qYDǓAh3#ҨҸ=mr=2K@LM8?}X,d.,ujb۪oxPol"  0 ƾ,u$V ]L(܏'LDN}P]7h?킺ж _?BB F kL<*Gpu遟t6`4]F3>шu)靓X4~ͦm~u`H AO[z᧶^ql6=aot cR8]}h=d(x\UkVQk>}d#,Q z-腑ǃbp&uEٮԭ$β}w"TLڟq%tIa;6^x0&EK2~17ԴA//9=Pm Γf>| qM8$7bYй/j٤s5]zB,v !jhԯ( dlEd0ބҦ# _9>W0<,/uܐ,ۢ6ת/.+Yk3{_9q=_>f2920sҋϖ1V} XͲrLŸzñY']ǮiKY>d|6md~J.1 0L^Lnbq c_5R`aU>ןd3/)x;&aa]rDDDDgg$ønJsCL  0 üFrDDDDg(oq"_&#J[ءx9v/+^080 0k$78@DDDDy/L~pveB<Þɣ{سp"̛;3JwS U 0 üFrDDDDg̼Q!N?sMP ]2󎀂W 0y),0 0 M,Qa2G}jϙ:߷DA{4sDIS{Tj =aPĩ4|0 0ƾ,3{TЧi J5_mi1EWCe ,C=Yv(Y5>}X`ayrDDDDg̼QB{c={#컁Gj$3IbR8'a \Nq0 0LZrDDDDg̼Q(HE $fދ(j$%%O71eu0,0 03""""3}Yfaay_X """ &`6&LC(ibü,9p 7!NCJRCSU ~Ǐbƈ |a P[7{aa&(0eQBÓ?w39y4 |'BQeTB 1Q'85 )@jZdzj(sU(uz|*F,?Wm'&.5%=*II!$'fJM:TJr}g/̯WHIɼD{/y_PP%&"Ulϳm"]C6LyK~f,6[ȼu6JKݢ)J6~R2WXWRi+qgO0MUFe5P@!9,m+s[2G>1$6:9_zaaBrDDDDg'Š QJ7x+:kLNկ5hcX`ΣfE .IRB­C{0D|wʧ8u=OfsL.=^N"gYO('&"el]'3 ]97BZ INf}RuYLۢ#8(Ncâ%=f6< G#FNN>"9klt:~&߉"FA$W*h0y&Xn+7g^ZN`ڰ>a8~YhqSgBS2y?=D*Y򱽘BCȢ21oZb>*5 r/}L1NaM|Qujd$)pNL*-lNޏ4^8_ۏ#fdB cXǘ9%oKxDq_۷a[qS8N^|?YD"!NnYme\ ;"Ln׫#aa@rDDDDgs <> [CQsk9BF3,lx0l,ZP5 ֲF:ڬ+wS6bh$3WA g&(F HlP+Ju}Y:uu8L98*5g4v-P eڣ`X^}''͕ggƀ.rl7v?+{m#Ep$"Jc\DCG/|-mHz֨3%p-:U3s8 #)W7/G>Caٶ#KM_4;q RθzCX 3+ -ҳV6{ 1X1n01G?8v) W@%P*~C}:FN^&sԕ:n}F,:CdBA^I <񯟜b< ơUܡ3O5쀚΃`;i.!~9 ;\Nbͺs {zyDB܉~ljBڢ`;:z,Ī~cga柜ƾ, 8v~kRmKϥzO݋gfUT\ڂ{Z};cРxt0Ξ mRV0k>.YɥsШ 5#ós)Ե6G(ej6nvGY'6UЄCP׳A:q(: Ca_ִAΨn;C6^A:؆ݣګ3ձCs0v\o/s :oMa*D+V)N(S_lt?PNl,p6sIPDRPBS e vܬLDc kEaGTh.SZT"k =Q8LSP?B\X>Mxvh0j;AqՀr!6xl=>>埵3L,c(= ńoA#Pa;n腖&c%LPL۠ ]Qfo}&Ww|mafNP0<f5ҳE(e QeW`<Ь}Gڡ3 wB}}q܉XכЩCqxhj-ƒlQI^oPi/8MލKK1-ZOZs L٣-F1$hY`a柝ƾ,y{Mptɺ^przhM\/QAw=QZv1z|ѣ'xOQ-Jl*{BlLŁ:v(mfm &m?ޏQD?1o0l2Ψ"K(I\8q^Pu T \Ff>-ƀx/GΞq#Sv(anGa&d^=Vʊui6__Op_-^p{D_%E??&8JEeXw>ĭ[ױ{lx؃#>qЉ>̼ 0 0&(0e9=\QŭHΘ|1 9YPL.do:{wCeyfuYp jըVB%,ڡtA-#kq=~"VNJui-{z5 [JخĹ[֤m o Pjt^"vjKWF?sР-ň l(d)wFv<N>B+ctb4WP$hcvPD٨iꌲ}NFߞ]=통Y9TIX~ "m[B$P?xLZo5'ļM7UkC}oU<C%LUsӁBžp*3}Bmdt(3Eu6N?~ JYvCpi<4Z^.^׽׵;ʛ9\0UתuqڴlJV.(p6?{v;g3'k H1u-ƖEu<?ԶGqS'Xt2HDC_8ދvQ^WIXvRFNx*i]'`=5Zoc7n#!%)ለaaaM,QarNŁHﳘEMPH |J#5DAR]8 m; A,=; Dm>&PyF&( ViX0l5E#1Lj։g^.A-}\g{LJjy'cfa%78@DDDDy/9%8pS[Z-PX"5 &x@6(RO2N(YBXLzq@UPGUb뒑d45J{ksG$:KqP.")u5c{O|m( ሕJmh FmKMd|{R8d[z4@8`b3Lhg둜lb/_ؓ1H֦O‼Bq ;3YLp݋,@R\?%ؓMrp]7ͬڢX/tZG:6:&Lo2ۇڣl}<I퓥8PŜ'bȱ>QiK P,<' ҿ jVNy*@ѧ-eQk| K&CAq3+",IkD1 v5xG_!V0w3~{f(S?v㑬ռP)ؽ1cލRDОɨPn+*5FŐ0w鍚m:haT<:u7#ĶmvOFޑ=i"\;C}a"2Ψn#ENq@^Vm:hOr1m&viӥwE~({ zmM?v@rfPe& JGNу=y"Dmp}zgOv$2D >cq`x,8Cq@m2u-IRm{cx*$&Ȋ+u;e1Sfq@>Zj#Yg4q2nxfޮ$_쌚n1vk^u0 0?0""""3}Y1p_l3E9T=g# 5t84q~1VPViŁʍ=i}v:[w??ZN&g}R878P;Y-{}01Шźު8F3N+7v3X8C͆WMS>JyȡYaһL,~#ؼO uG m oJX8d=w8-<{ڗ9=s8k }AΨ)G>#!Eq@W9Jr3`a#}c,iufH8AEL^?v(.*5{q9 aa柕ƾ,9٬ `^P\, y%IQ8Sq@UҊ]BG\%>^> Jm8uC7/&qP(m*m"N@ݖ.(k^`B#>Ʀi+T$^ۀ_m3Dd bħG4P=jnlG&^+WSƬ<}`ȴg1-5g7r &lQlZ.±hĊqioXՄx :VuS&%kaa)M,QaˢjrE13g4/'-_:!ȱj5܆N6g | bTn@lJe baSTtA9 aҊ)"b{Qa3/(m :N֝PE^9`1=~+Qgd,ogcVPe;B lr[cKWTgśb,ipj-)CPT^v!ZwQPj|yF`o,⳿[&Ucɨoe/i;s ׂ ڴ;&K-Έ~ZPϺ#[:j42/,>2hWKwW \Pm=V[Vé=>p?,S+9olɇdUh;ajD[3Pd_>Qa؛jO`vqC(5}?:vE *^}YRG  jŬ%m i 0 0ErDDDDg*\=Q-P~a ZU={;+g^D\HyPGX/7*GԴ.@z\yIϷ*ڊ:8 '7FT܋ˉ }}Y*آhΰ|wUWDN?4}$rl4V]UQiՈy#M~ԧ0ePT1uDI7-?(Œ,IFqm#EɾO}ž`e?Wt>9w41q@I>c \mW]^㘬ǞRE*4oy,+,u L53Y`aƾ,2rYK@Fv(c劊 \P f`с BwyQut3w1,'+_7sE&]a?% >>_Eo`<(3 .} O/!(r8q1z&qVQ^&o cᅡG q|p3zh/+\˞h*MpEq\`q(b7>_N`ggTmVJY CD {1`L8E0ol95٣Ig8O?i;uB3GCC"xm`c u#>Q/bVh=|Rӟw[GY *EF'7jH$d9 j=v݇*) hNaˍ )3/,JCWr`3߱1{ ĸ?c2'%\܏a]Ϛkx!GzF[Flux[!aaM,QakETae)l<_PY#I_RCwT@: GFnbUn=c?0n\xuꂯ8s␢˘|Ł.A!:Bi 1xPi抲(٬?ܖ]Elٙo@bn_h_'a;m><:ؖ'*7pAy <[oA=wsyX}'gkM,ö otwyPmsA  /Т,Z>yhp\ݵ me\mwϯxqfZtBs{m=k!N6,&ϞnFb˾q;$fa柝ƾ,~hxzu?qFq`HOOm/^]0)A{Vn_Ɠsl+2t<; rwCr](Zk {P*L! @Ut6gUk5"ZDb[ڡ,@MwS Q )cPJnlm-aTe`YP}(T:Nw&6(Tk濬80P-0ΨP'-үlHr|6b c JvBN(^7Ɵxp1o诨X5oa\w6sUR76jVn,dZە|{ 0 0&(0e#'GX3k:{F=.ug|gM=UX~F&@Vgzp(U*bĺo?D1nş RA3ú;*6~/qNŁd?,:uZvBfcVċyƼ"V[k/` :Aשqj0"o(x}PU^o-ܑYq -joPx?Sa0s?EBg!4f  ~ueTkMG( [ oSQ&ף|bhO[G}֞3bTwӮp7c3rRCi7?7MieJֲ_ 0 0&(0eawcŁlWx0 0 0ƾ,3 0 0?#""""3}Yf݇aaFrDDDDg0̻ 0 0&(0eawCq\kPs)aa&(0eawe|.nEnѶx-aa8""""3}Yf&wQ&j$(/0 0 ^rDDDDg0 0 0 &78@DDDDy/ 0 0 0krDDDDgT*aaa| 0 0 0&""""3aaaMrDDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDDDDDDDD DDDDD/2Q AHH"cP된/ H*KNM0A$i GTXZ$/U)zEG 2^%;g)5ɉ:F*ƞaHЧo$U<"Bߖ0Gk1'=?zSXѨšU0T=A(%ID"",*)6Ro=QtH;:we?/';] A.LzeP|(jHTQ XcsXr{ڗ7+͑^G/Иvr)8l~EjdrKJ"|a疥83%'("M>5Dbo-[l&At ;wlƁ&ĞRɠ#yc?^ڔ†Xᗔ>3}eLoYTI?|.߁ g?cQIyD8@DDDDTh.a۷P7Vu1{s.](x_O̐z󬪡@8+g}>t@x?F7-;DTL2.) 8cXiѧdRm닾ٮePgkN(M:.j>CNd6>7wp5}l eܸu2h[~ߙ$eۨAB?\O!Xx߹x{-*nycL*41O}+n" 2ٻz$u\r 7<@`F+CPG?{7pUϠ(p7d迸l'Ĕd$D>p-< E &6 _7l`耋_8Ey>B!/)A 7D"RϳHU!Dy.^ DNmnߺ0u*qWDWD{u릊25ޠ8$Ek2Yqw Cĭ[78FBs;m} cu诛8X&E໷H|$%;>AR;xxa_-P<mpM}y;p6R)Ex?MEx|Wq=< "&o3FLݒ.<IMNRqc\c6+>(.GFx1e]-kXBSǑ;!?Ɠ8d뱗KR#2gh>oMID#)Z=ĶݹotҊ>@KPɿlCTbA|t$"XwԢ-ŸxKE@Dֿ@ܻywc}$!8l]\X{a0Ic0ϨlǍ z.|xS賾RՈ ÝiqOlC8gC_ĨԈ o܄w`p8^̕?DCg ,eyeg6XzCzIEBXF=b`hXX% }O>_Tmq<{›LDCLOX~S~!JU-,@}8Oi} !&FaE_ѳ1i@T(XWu11עҋ8֮?U4:ᔢDm_+E[~y5r|iT]<|O-[ST,%>s) Lc?g~D7a%jTbjy,v\ u3R V w@oglz-1c3 z4.qaL?G~"5ðU|k$LVG+_)<ZŁX(UD'O`#Us,:pǻjI|254n n=& *ǽ;Tܙi;W-PI;]/1jUBBb?徳B q-(!D^!l_+T5ſ%PŁMeTA/da=$D֨S8OlG5`<o&XcoC>OQǡ?΋O O`>hU* B(#ibSu>)v3~\58-܏ ]qTld%m J㾈@,rSs/JVC}1X#eLjo`z-G=yJ\W#4}Ѹ l[T.!+>>^WㄌOh_-u=W+O>]@`/N1Gb1q/\{>>m=]-b)QkXzIL9Rwf9j/P̯hk:^*gq).lNV?藟 d_ѬPBwfW߿?|I>)O(0y,VAq,">F>_7V0}e(3qcӭ'忣k+q?jh^^ ڠFOAQʬ @NTIpSl>1[e/~*kCТZa4 MgP8?-Z ǚˁdz8z(OkV$>mIJYW>YHƤ"QK&k,,>?+"kuY8|'8<&|>Kqb2n#| +[źOi98ö0Q|_)3 ][́qϞ9TU =Sf5J/Fb%d̗GP^J:?1R}^aթsvo~!UA%L[UZZIDqwͭ+U>cPa,<~*׶KݯP. [f!'ƶjae߬>,9~2l ׅW-6g[cq('aUIrʙ$!p>)g l3?G 1a_г3wG#ZXFI{jeSŭg|4{0Yh{V܋}~{j(e5_}>(~n:mG`цXbB&4-@AyQZ?C~Cɏsp.&ߴ8p}>CGԭnSV`߮՘ѫ*76=G,;(_K3;3>3i?: Aɪa{ nZVڈ|0Î]t+^ v>MT,}nV?48,-zmx vz!}SEv˜[ ~/~쀵7'=bQ(\ ^aX7+l-/ʴ[љ&I&,Ƙtx?veRmffܔ::?Fe(lۊ-sOQ࿵1l3u0f"ܵ+&Lq|ѿ(nFď!mPJUaը-ZĨ;ei^GG,$b_ h7` w?`mFbX>u*D.Sbkv]1y۲c:Q4ɨhtl6WWk3cߞг1/aFEϋkOx}|z~S?݃@b?]ذ'6 _೪XXdS\4DꦘN\o X}X5m`]رi>F:7VTĜ$z+_jubҊX7HģmQXm=iœ ) ~ر*>~8ndeG?S1|Xg M{c[&Cƅ)@~8/`2LiQu=d,hR`ܲ8{fH u }~4yf+v- _Ŀ@KFhm֍ЧQ @Q.]>BS[Wԫcc׮X0ՋPT+$iX4*W7Cq+NF?ҷY丹Q~h7sÛp3T|n'#g="ޱ?iQ WNuakl\=&%Aq{+.-NJm;}pw{!1]z1|sMT ,Žk1{cGxVF~Fg&8^@qO~W&I(<<=Z*Dq-+Gah(R*ձغ n 8Mv7h_lǮسb \UA_/-T )77o>N1onڠjPTHߨ3,a]K4U;b]aV#(E<;s_>*7t7{ m%zFBڢG]<(\Iռ*Z6uCt;mĴA֨ٿPд?v≯:PsZrbh5,B:t{///!@| ߣi(NJ^Ue A DƇOvIQ{[ Ʀ9a[8˚M?d^83mPo0oc\(n9g,8;~ v}bŊXUO"pb5J2ǨYN,Ƥ.ӴF)Bu?"EDw%[A@۲da8 j8֥x ZVzLq~EJvڟF46osoa k`R[NNrF,\<u^E4ϧs.O 2(;_IBɡJNj=J\^`ŨM5PB;1i4'T*{.|@J,N }d}pԱqvȉIM},T9+45>F@ph TEw''4'ѧb!V?Jq @ab2$A %?B)?;KQR_Pl[LAq Sk3gC3֒;@=.ϫLcrb VĿzJLǨp3 ?('N 鸑ib:x_/X,۞?{gDօ᰸V]CPb[ wN/uIܙiByv2ܙLs>#م!אxA͚_RB>]6|r$Ҡy^b]3LIub..Cmю#\'4DFM j?aQ YGx7~OmD*pq,ot/"'RāXѩ$rFίAQ[Xϋ:_zI5(Z?\72.[_qe@N UG,ѳqc), 97 %wch%v I_4my\xt,lpUh%]܃ EXwWyāKBS'V4Q^AbQlv<$6iwc}}3|80$Xݴ՛]r9YEAGd*bdHYgC]/Ф.EQĈ`OgL7{BQxDrzO=ZWΪAꫡv$gX;~QvociH\c<^~AAqY1ʙ- V?mrE#ྪ-r5s1'Jസ=WO{կ, x֯<m[&K :i|衋]Sg`{p~|gwpv H"vS>D_қ Ф`2ԟό%"\}>p%O_{ɻK{ K$E-q.h-ËV&#ʍڇǾnO<>!UXN;TA*_8~&xjt8" !w7V&M>٣42x/Ml, ;,yt;_]Z@\7mVlm"CRcú;O&Y:?`,B_IJB)|8? iPq<07{﭅iraGN#8RBv=}L?A =8'=2R/>qmhN_f]+ՑC Uq:ˑhɘu n.?ﱫJAUѠahi^ĤԘr,7`,DQZdOt&LiW I@~*%SJz96fxb{oexy*l9cq,o\Oxz}ֆzbSW Rd r`мX!W8u^4~6z sD pp[#B\g-4ɐv[l&xئّ8(D\Y6h9"='8FeK]<}|= Y_?B溛x.hKtˣy 4^yT+\\%0f=%kn(LiPh[d{S^v®Ջp FR#MҬ(]Hp*{h4ʛR3(KpHȚtħE\ڏ84Zd){+x8pV[Ǣ&)2&{p_m -F,xo)^5_]э }b (mbݟض~^!.qڪ^d. ѯWGr4,A;P^P)_jd O}coHF4EuFU&Ydh8dѕ/wE4t]q h*KjhJyfoѸ\~y}pq[0ZK$>W@vC1n>{.oЮrVc9<%^TGPjXtدEw;J E,vq`!:ʂZ=Dv6wAd0fmtq>@NqMiƂ=kg];4ɐ&/rfޑ7"Nb5Xh3GB6PL.a6qW<xyDdFkzUP 鋴^ gu9`Io?w'1zfn9}hBpݶ2~->3`_0 0 #2ASf\ ޞC5Mr۵C;h]{2+TJ-$1āP G:ZZiEtCࣔIWŁO>nìv{ּ(U5nzb4_r%չMoI;PŁiM "bc/ eS?3MŁ@w*8 ϛ!8%)z pF@w"L%q`!nL(<硩&j/4ک`=ZQ(IQdlJJFMЪ}Ӯ]{t=ž{aqօ]Ԫrfύ2j~hQ0uE.*80]1dBd8@Y([8$y/ S& āfG&kC`8Ԝ8qxd,mqz홈IXb}|'TqXOā'\ /$b!6q es2̲CNu-2C+s"3 0u&t>4/]-:gj,")1һ? ^ђa;1T1.OLn^z{4~CQv#4m{$ h#ՍŁȖ&ަI2j "8ā{k^cHtqw8OhADNާ?L[Q-v#з}SԪX 93ҽ03j[/B>8,E?Ws^ z3 ?ѰF@PqxE T 6ʁ/&iLh=Mrg=)t_q*dϜ MۘgŐ#0E> d94E͊S\D9Q{ x駴!qRVԜQ4kˊbH%ψr@'bq'R#LȳlKasQ\ {08i d/}Xy+MG 93oqF[upRd+H ~I5Z<;vAC0z>n @eoyxMXqS,a),0 0 0L4ԸH)~[qsXK`I3M naxyx&Łau-ʔ 9ɾ1x GO}_JP[;6(ROyW{X-$+-q}K631v 07D@?̉N¢tbd%+wh'ŁKi 8P4krvo6O~R$1 CtJd2LLSC} nZq ~x32.8sLem14Q8Czt^w?Va)+ X^(h`ǀ$Q.`H\e q c1+ʗ/ǽksʁ6Ȕ1ƞ3IM}R8(8 eMLxuAV8%6xt-{P9PB\6$_Dn>/D?!bD$(cydLVGHdN;d"x}q`H\6~EiP+!xnAIwʫh]U0/\;r/nX:}yǷ-6ő)Y^t?uCِxߛ^ ~8;;siEh OrCȎ}G!]&)¬ =bCG @ݡ#0K`V9 2涃v$pj ٫*_[*#_,*[e&Eu2U$xqP|NE9fBLO0 aaƄ;0f*L4s/3:=2)$woly=py@dk8}U ?dXx$^.Q2!ʁ.H_+Y xA7cPT?nh@,Y)STq Em \q#Q6i T$"H(쉞 xn! }ÑJLM$z)|OA1z K8hxsΏD9cMq!mXy( MrKT\'{HIR땓4oMUy"Np2&̠$O!kS^q~nnEiB{l42K Izk4&gمA^Ȭ LE; iS|=2=Vqw룳Q5{R$ʐA̓ۯh\GĚH94+CTNߚ:ױwV<TOQ"/&qG451{曉i|m$/&|XR+34Y[aWJSlC/c#Ii kLQ/ 4+OrRj3} =LZv_:Ә 2}#j֠DbC酑:euXF3=;uX竇n=Q=e:yD ƽ}Q e&|s@ȗH1Gb|>?=42@S܆m(w:lzY8,(Na aa&^Y^s!s?kw[}g]v`ئɂMM0CpkPe oX$޹naۻ2% -GY0ϦA}0ا۸}(j v٤w]9&6 ʃiN| byڶHz`/ S¨s>qSкD {"'b4U_kpj l,(xS('#2ʆ -ga3y7أ_owBq H%NڿOsAd`P.vx}VsC[ Y ^'oJeD]䊚8gr✭:pV\qh54-lD(3`Y!1k "Qreǯ}X5wZ/‰*PfySLc3v&.nEȜ4z'L=aDY+a_p>F„Y{ nc̚M|>4%P=ϣ{ Q.SVm0wⲸ]=VBԿ\q P2cJ TTh[u-pI2ej1k_} WOjˁ07ˁ5Y} 7'dϜSF6|náV.4a#5sMPz m?K upM;"^ˡ۔fHk~4]-v] yڰ;1X@yߐfou{b|SϐuFmŁ/~K\q㙷4x!zʼݦ^ ǿ\pA܋n?S"&{tYd@K븸'_ڡrj9_*7nZ0R%O5a3޾;Wc^^FU'D%1pm(os?\Z)4eխثчõT0RW۝qu9sB=P*sd\7 Wn=ifn_.i#۰ p\ğة4c5WiMw Cc"U o2 0?aa=|1Qx *YʔA2f"#gڤ'ۊ3J9}kZ;F%`V Ϗ%=P^_n)84h3R΍]sлN/RK@!d-X )2KQNAE&MtA>E4鐻*UZZ4X ܩKʬfuU M<=Bt鐫X%TXu7ƀqSa?*(_.ad067e א-x|?O,@zE;_,eeKr=4d)lDln5i 牍$s 9e 9< ۟DNn2LQn:ڵ=0>މ)]jp_ʖ*E|X|=z-Nf "g✕ErUq̝:9#ݵuؾQI wLEx{CREQbcXdžA[J4%S>4jSrB\'& /eK wi֜}'Si qdJu:<rLZ?1c=A/qv4^D)qm*9VE)q[ &G[qhhM[(>j?m"|pҮ IEQN4n g/4gWyW8mRAg)N B_ds[K \~%!9 V(S:xBfվJ{k-FEݏ?<~hR +)ݥQ4on,k! ]yԨo-OS WpwK?$C~np$&C؞\u{J(\JW{oigԋ!\B™!s-]kFa|>K hkEV$֔թ$7rŃ\(fVfJ O0ۯ"'-Z"e l+p8g%'0_ 0 0 I>صi)fϚ3cMz4FcûDZk=,π 8}7rƤG.d >7bN\|Qo^"if\G.#Z[P4K6+ONbJ;XΘ U}(B=as[my,_'=P}heױd| G? +,0]Cx0J6_cbW ]C.2 y{p3;.pmc sqmkopu۟rH2Oğֳo!6~=̃==kffZXc ׍#ظ 3,l`2_\Ӗ%3s/݅j`|[-IH|s ΜEqGRvL8߃FaĮ՘o)6XF?v>rq kh6S75~%W ۥzNWcX5*3OC7a|qߜ9 gtz[2).+Eqy p_KSϣwt^8r!\AEW>@w^则,k^&wW}9k8Dݣh @PlCa9!󶋷VTקEI)J[;iJ,y]+qM7kǚs' %uuC=<`9 W|c2G<ܫxo Yc51Kf \=v/,ad;|qCʿ#ߠոㅫ{WZlvv+7j|/1>DDְuX cxz]žŋss4{v\nқ=-ü9qk`7 g<ݙ3-a$x~HTV2zwxxo,q1tj/=os9n`uCp|̵9Ƶ#۰akv#[<7OM:5rH{\xea 0 0 |]TqfUjs*0?7z]7ÎϴiQŁo"8~9T&!A@ߑ\7|3 {-`(bȃºi^Z~➤]`Z,AKM<&0 aqaaaK8.05a~"\p,=5A=㇑8IDcq ?01͎:#I\:{.;aR۲ȕz:^A8pz̎aU |X`aarJnKڛ o4(1a~tbz 4ҠDGG\Tr&w#T@S^Eɑ}OᯯcU ̎ wb(W &-[>^}rH*ɋݑ`ea,0 0 0 uyw۶mũHsg868F('@ ޵ Utwlۃ0D`n=fW۰׸{ڵy?|c'l۲ 0 0 _` …8&HDΊϾ78'~R}-~Ӿ>BuB:!@/C] ؛KbI>TZ1W{쁓\\\88J[>>>U0 k֬[o>xyyk0r"4>u(Qb(&b FuFhm2}GЯDk= }NAvз $L6{FsгoE B!2LpktiNEF0&٠djD1mf1),EXYQ\):\ :zu뷝ns}Cqq~wuq<݆X㳔ڣ%zkzw>]g)"Ƨob̦_)j lqx1ryn$[ذf#3?aqaaaoyÏ8CWLSZ# ϡ(x#* { @KA}:/ QU=!"nA sHo/js/̙38wL~Ď;d1M βrA1kLD дD_Zf6=j&Z:Vn'jgYvAk(jZFf1QrM@ PT1 LG3P|&̚D閳PQl6{F(8[ 0 )br1&F"Fi}m,d-B9jXNATC<^^/x\l7 S(Ez([\wP,i)Lm4MGPTT<OD2ŸEcP(; rFlQ;K_[kJU3wͤPC?:'5sDyM=XO+#'AO.bu ^B&p l(` ;QQv{̷|!0 0 |kd;$ $RW! `Wa DP5GpU ȷy)cD;/`n!b;b3ŶvMqDbƉ}#P z9U;ءz|j6 n1VhPeVf3Ш4 B5= y!0<Q>r.EW4LYy\%j氝9Ł ~NHѠؐe?!Z!?qɋn ؉Z%Ct1,0 0 |Kuzx;@0`h OQH@,+uO<Da}{ZOh@U_Sr- ̗A{`mjSF))Iĝ I'4L wUāT=(m/g q` l8w "`^b*fa% 4nzQ jۢfyJ:〲āQb#80Y;lGpDA+QUq * & aL]O vId((^HUŁľvD80HHܨأJG;Tkc-fyV#jZA9R4iXv"Rh"I %g/G}Ÿ0y(Q|дL;~zq@fԥdvm?D]ğ`xžwbt+3m>4ΏU*Y"dj︂aa曡" N>8B~T-8Bn'X/rx鋠ج><x}Z"Ԅ'ք7nI輎AKfdX㞿?Ky8ھ8)jo$ PՀ0n?3 8k+I(3 BG@mkԩoZmQ6Fl(=L`qC]IGvruEZV_mTfqS`^d" G /1FIGYT5࠴H^J ("" P,1B*kW7TPiPYw=H֚|K߁: *jXU}BEjk!C j1D)YԴaq;'5d?N>8!""y@@B#XE 80DgrcZzYmx(BX^O  @J{.^nXPbM rO5haā6>[klx8^(n¡VB]o]aaA߉i68 H`$3@,߯ waY1|u@S[XP~b2?X`q[hh_F(Ʒ$ P:0Oi'e0#T#bO-j@~ KjZ ۢ'gO"JB^z H#bk ZՀ* Hbca P,5 2)IC{!KULU 2Icdk!SQmB-5m-T~S5o[ bxS{!ғ@H"E7LԎŁWֵdAQV]:?|HVFcޘz׾=I)텽c^wWTrr(P<ٻNߐQn"/i\/B]YujA% CkIo> .bհ'`YLg,nS)/V,O|DYP[:™~!FSq@۵5R1gn2\H{} lL_i:e Tmv.^]\_ű&~BaakSpT-I 8yg{ǐ8T_xп9a%.+LIz@D_Й^ۑw{EGK!&3 2vS ǀb@ a ^dDl(;S6jT5PuڢV[i[vGFURHG_8b (`kh1d 0d{!՘dk،Z=Pk;Y=PؠnkԫaU,d륆f3an\= {`dT{!w)U*:OzVO+8gf 5Dz[CP4Sb+rʨdV9%F *Nڋ *EJ:_M 5Ɽ-ଷuxxR,bffXbR*۬y)^}/uoȬ8C|%P@&hRfF`*f-REs xcZ_m5VgL)I%gFj¹S Q"?`O#q  BblSg69hl%F)dlk+GЅH$1d94]yhGSun(IXQ}&税kNO~yG*DQO_BԦj41H= ilSeB-I;m\JVAjV(]ո|?jyx *IRksK!aaoophVw[5`$a@Gd^=U bkBO+t X0? $ܿ'N@`` BCC"}xdz8ė1}&Q^Qz Nh45'3S7y TPj*UE&bFFT5@FĆ(c0"6T 8`\5@&T5@€hm ~"HXŐ  I sb{z`>j6TԟU=@T=P+"KJ{Hc@m1D \U;yWv`q;ta]\Mbp M ;FoFT#z\\Q<9ś f*t5H嵨v".sZ84ӣ{Bcq =ڮbYED 9|))BoG:3qW%$Fxa s' 85\ ~_q@⃍ӊuFm&c5Tt ckvK!V-buێ|aa&D/d@lmH0 Xj)0 "R[ EVP{!kzD (9N 2=0@S@oz'C^h͐R #+4hi f9sijwO)05\V[F˝hZDy+aeu O!Fcu1A1^$$BѮN6Ps<-C4 :.SŁ\hǡd&p_3o 6Śgr"nEbb&@{rr)m]V%覺0+?ߎV櫊9Wԅ&=YLbIN|(LT-pa}UmŌquIܰ8ė1Q @ h T-`, D L!gKa ղFz6NrQ U (wė ɪ&&vB3`$j bT00klc1h}ՐA J)8U텨QJU=g{< }/;#T1 ?8@*DO_%<]kO^U>z0_?}׽!XQ| 2fTT P$q@)hD€ 3а3S;!2!ZGU vBvBHpl'Tb#&ąj"J05x }0$NsbXf,Ҭب hA^91R*w2'* u0Q;& 1Ҥ:tN /lX8}w;yR{eqෳ|^ 'fL%Y҂(w.ܥ8 dͤ$cdbSqSiO’&~+Nj۾8ŁvbY"\fJ˵GT#ld c?Xā/_οп~^u_"(Pzۇǟ#+dAϕcik0 0 | µ8H-bI(AїʂJȒTb(LIfLD;gYJc7ܺҶSY=uR>QWՅ߶B,Q6_y:}nl9bŭsв|VU(ȋ+ęuO΢AJ#q!1 0 |+#tg!@8Up}"[ W?:ՇAZj%D3`9Qh?HL|=6{ߏJs8ė1üev: j NJHV Fj3` ԷZvBըPPTՀPl'4YR3`+ $ U*E " B& k=h|L)@O? hB$tdNL{^HUh le%jDUrjg9$( |H/YI@1u~ 9,|/|cq@ơNUf(ָA胳k\YD+[x3tȑIWßwωFh Qβ}:ҿ; 3%&YZL텋*Ma{G]+ЅO_V *Dx(`YI%f I!ow-m<Ocˤ&H-^;kZ]8h~A;B]0 0 |}>EਗbFlp'NA8 īDIZ< `%#(z@O9µXy1.?{}<+X` yxx^3f  P @ i>j2[ȞT1+U ytj'$ |KQ Qckc Zŀ]D'`ȷJF(,Pz>I(02" @@TA@TA`,?@qdN d\ED P+H -*)&4Ѫ>y}5h] ifx}C'_zcCh>2RT̛Vcֱe`"rYqAx-{cJWmWV*ցqh|ʁBh$#a LdC*ڐSVVh**B뻋c[Շ?0Gz ,b2bgn0 0 {ޅjQ&p N «xVhC'tn?8@W'r-ŜLܰ8|k?`gxaq1s\p{.U2fL4*1VDt0/V (`6P0`P* 4EJŀb@l' +@o{) P?2Cq(1QIS +K`R hbzjD/WKRbMdU5̃E fU/=|R+E*MmU:o+z+XUF1?NA?/Zngb4/[uysj{1yIKMџ(E*d“X:A?NjF% A%>|-[ѣXjd#Gyw4(=^^V ( X(5-Qu0P\jfԊvҩ* rI_fNho(.[ 00_m%D€%iVBH[UQR/{6힅b{(nEv+"Aj5"W}"+ &Ų@1PA0N4(vT s -ZΗu͕TA 0J*P% й*3L5 ܐ8_&éNB6#O*ϰW9$פFwuc'k)=zWs67l0n.Xov:aNwVN۠v<o.ޗbބڽ;wmqBž8aިǻ`7Z[S=bz^1e nUzhmR蠁t[ #M?cٺâ7n1tE{mc􊍸Hl'Օ娔L,a+u10 0 q'২pB8*ccA݇z@9{`> L\FsxT`c0@CI ((H}3tVg+QfdU栾YRhx P ) TohF=%Υ005J=VBy dNiV BmD$ ($ ; %D}Qrt#{gyw͖Sp;UX"V+E@ }@H P$X*E-&$b#Uv@ Cƶ@o=g+ԯe"ȰXjs5 O,|'|sq@ i4 ?%Lt8VO'$#NO&Mܨaao%Х8@-ybIhA″^.a{yC{:j)DU?E"r ,0qq{l/  AdDLa~3fl42 *S;$@uutE-sR͇ċՊJ";f J`%@iD?4OF#Q$TQi"e^كSJS5H`$8KG @Cǒ҇ͥ(X?Zǻ,0J0b \vM ޽Ν;qH>~_/D]aq/#AF`S@~4Q@ EG(;Hi%TzJxbG%a<,+Ta`\b@ XJa-Pj )PP)4 ՝2@G(4>1MN G#Ec=H;6u]E$ u*C$HH|Ȩxlud,W+qHd8^ 26) UWj$E1޲`XLBz~Os[T=Ło/##ø ="B,|aaZku8?8@#'|rt}Qo d9.e$@/\ *ٙ+H´pV|,0e9lw/Chhz tjJbۣU HaA OPact0`j>,[ E Qv(mJd(L{R`O S$槆huzZ6gGFkc-D4599B>j5A5 қ)ҏfhlŨxu@@jGCj!@` 2k Ư*NxӸG|ȖCl*ַ\84ɑ2aaa~z+G1q0\TqYg8B:/@{G ^nyҁ*$xHFpp=ZV  S|,0e9R&e1QB&QBo{9=R T  F4>c`Z1`0&a`# Q4k^B}8sǨB`TJ;3DsID#Z^< )$B5]I(wp"(gbTVB "P)( kRb*ƉqTA@G( GH@w <yZGy9 Gg)ذ80 0 0?!?_ζzl.8cA6I~оUÏ_T*!^¾LI8;wF>0 =uɗ_ODVV :Y*%I 3]^vb[zO@q0`0W5ν|x# 휍bgr>rt:VV D֧HAӹr?Mq7\%RzǻH,(OzTI@v;G#ełA ;Ȩ PUVcZ2y!PARq&S)>NJ2C@@mȷ|oeDΉH@Y3Kر80 0 0 p-HߟMp WPz z]tq-Dxu:/gا<,0\b _H*Bv} -z+= CPfR1 $ !Aq@~;(a1) U<*JSN0P4:>Jd~3eu@W)ER/ /w Pt?oZ_bܩ@? $熄ΗYK80 0 00Sl0\|qD$q#88]oAO$A~2Lܰ8^B3{V]`q/'XJghb@Г(4AʬwJnSeۜQ`>lG'{ⱕ@ލse) "(w:_dTw:nhY-0=ׅ{Q\aW`yF]"ƨ?G](CtHPy %QӃ04:AcAգQdiTL PTq*Ȥ^ R T`"H`\YVG:{祢8G\(֊ŁaaWm{'y,/M+н9&C劈{y) N퍍!\u Y`˰IV& C cQ`6$ LQx0`h%X Ԃg\䕭a0@j.Qm,'KgA:tЫ]1Zg)nm1FL1EIANjNJH< !?P z\+ ՐRE0BzvjN$Li(op dZ XkTDyIbHK((,|/8G_2ԬRn߿їpc?U9oG[aaoW R=`ܼd,ׅB&t>d"t>n>Яaq aCIDATkz_MB6JZD 0X9pZ`ᐿR"Qx# 0`% Pb<+Ɋ) lFc؞YCSPD cc9pC.A=1JWcwm1f+̺sn7wMmRCL< H\s ]&= _#93OS}ơTTE {<ŤEj1>J b LҨxҚ*1Jc=JowjDΉ<7" X'X㕨@cmɬ~{L|: D2mW@ LU³lh€b>,V;k|XIa@= @CSQH'Gʶ>4{}^aWbxLS%fX wbX(⏻^],Vcݺby=86b.F _XH\xˎ?B`o@†D7oի8<;OԩSؾ};|}}5SPK*D Q`$U Å0 gJ02PVBTaPt87Pwg;ƽ 4{u ~ {P Ta%#c{XaPL1uz>r&O^`ǸmZ8ܹsG7nӧuP<F0 Y% -tHJ*lTX6_ISsLzjSpLS+:F#d+JH a` (^yV= Q@>cǣ QF,v<.m+bJyH$W[ rkD So0j'Ybot9m F3ՃLLLC3P(c@y֩`dRH @q! @p|j+XBB|8u5軐>.]dNGpc|ϗ/pvsruCN8s1>R6\\/P@?AЛzE':9Ǐ'#/>wpN\AO ݋6u{ |L)Gq>.{!dz{cv$.> {V1 0 o>/+@-= ʁ݄㰜yk{zBsg\?~\=?ŁEpX\b}dt8M6aڵعsڵ ,Ç`hkB2VE"Q"a@Oƺ؎0S"c` lAdœ%ΔtJWs:cLa~um0VKXi"g/WO&MU lyXI ŞeIixRP%}KcXoXDR$vCY0ffK)4WLO Cc@Pi"*⸊+,6G+%1ȃKY!} fŪH@X((&"'XBl dNfJ&/iˡus{8 ,Ÿ9>%mKٞFf`(/{`~QL$IcZD), gh$%Sžj:1lXD㌝(m.Lm(-f&NhXw$U3Qm2܏M xclJH>GD%~7}z_m쏒4>z<8[A(2DHOـkgX |<_!8wŒ^'cA{O2 0 @9gGI =݆p⧷iпmTS%V h=}}VNz\ 8s ֯_'Oȿdf8p+qV8/puy!իWx왼EUϲBqs#вJP)@6B/0Q1V+Z1@ vȽrb@Ll5 lD-2DIazS^ꅡWaNt fn!Ѭ~ X~>+* 4>)#OIQ8?-*i!Ga%G?mR48d\d? vϔ퓨 V)ڹʸpt>,&Ŋ@'4*LM)B 7 Y^OāNcf겟ELdAdP̙3 GY>J@p瓓8#wD$ɍVbY9#ZTFnJ̦̏e3gcά e^zY4 Iְ=3gR4 k,9RyWA{1N~|NWڍgaYؿ*͊h>ZvȕZg<8g%Y<@3lb ln3xmt<ߊAu%i2f#ѡdRqI$GpghN/O2Slw6L YX&6x?drs ɓ%GNCvds#SÃt|4+a C>TBq 0 ֻL$j`VPkci]d=$w5 hRb>˗/۷o#,,LV޽ŁPl}Oc`>LB$_oC3hO+1@U ( Q0@cb 2#a`R1JΠ/k6)g֗?!>zan}&l'wKa`:Z=*`r'=+ Գ8-|8+~ߴp{ZNb#Oax>UOհ~MfNC#=F^AWzEE h}f&T::eMAIiPLly@ }VD yIƓL UиG yDZ8=U)3U@!0FB=2H4.!!ՃQ"x,Q :>^힄U1v+E|x;)L.VB:ϥ9;k~s1FQpl< ieW\򓑼{*FY 6qx:x:xpS_L/7CfjXm}:Dm[āOEh2yn>7cI~/Ǎ?̮`z"H,2^Eө-8v/_k̲<Gbt[cq`V1z^N4Kkg_*%42;2aa2& \BeL -:q|t^$8tޮz@ !~r ; % µ:z ߿%,0*O$) R( ZbT1@3Wϋ4 Gq12 ?]&+PhX= @;Ka`Vqo* o `ˣ=.%+8=ˇ <<7.?υ+OX~^<~VwROUOГȫ@OU VFu0&yt]rOW'OBcquƠxT<: eNE}dP<[ J.٠B-G^1~""C8VCJD,|O'@Dj2zXQDe1rE6IM$Oq :fw.+_̨x[yw*$i88 ҈NjvuOq@lsF26LjmƖ;ym.?cMEsaԚ7g];&eɃFKA'1RU`S 2D1hbaiISe碎SZk0 TxS&A_+c}u0 0 *űW8x> #y;^?Ty$W#@1f!1ؐ8p#l܃ǯ7q-.X`V(6un cLJKQZ\eVUaJ*H0T lT Ua(|I2uFH<= D }0rot[avwͥ0~ l$0y}RL?,<υk/r!⦈[Ox\R,8/?#wYAYu@B|TN $P\Ocw~.B }@8= Dm1Gt-q?q8pg+TTis.mn[6qZ:ecʤëGqG&v'FɄ:%ֻ]苾zb{jKfܫ?UףbgpFV d֋pw_fýQߴ] מ%U$vCԒSTAKג&Ŷbfj7`䵎|+z_Ahrj8duDCS=3Qx - P[ 8 $W;UH#+ Ԙ\Sm`Łs 9r4XdmbM\YۢqnNyコTQ{6n*8 ~?ۊ4X+7qf}WV @)::pT_q5mی$fۜ;یV}U "5ndHWdS5G g0o@ۗ?%~Ǔwxߧ?>ú>9MZV㱺s d~aIH}?XK8p}ħ]輎@+[ Ş'9. /@DmOmkŁ0'B 8)̰. P !UU 0|3fr7 = ͐G'xNg}^!⎑Ĕ50a <)! ?- jDUȎ/HcO<~SZ@=m izf|=.)I Xܫ ; `3n1jgYbt<7@VA4>1uAu xd2LSf9J{-iPlΓmo"{S 4&"E,|G'@gMr4g R0w%D`:HN@ux/W4)ߏ+i.Z5eYv(vb?HlXAzbw "H}+y׫OvD}^0.q@lOUo e$T_&"ah}IR$/ϚL(WZ<@?bՀҴ:ϠxP8 ɪ/w@t$>OLaa›|_+.JOؒ ⨧? HuTfxW rܤqU9zɪ8u}l~ ,0e+e%T Ha0@Dπq;,QpLS}jS$TsncvBC쀨vBW;cv2nd@|6<-+b#3) 80 anȎ{(@Bs/Da7-&X PH`(^X +=aÃXylo$ Uq;`蕮꡻l/4N CPm4]OfdLABU +Ɨ9C`$й" L'XB>+Dß,nxO٣Y!J8[߼ž)Il3xX%z;ַl+:EvKT]9ޝy08s%_8Nޝ&m^x6}ryo1WGNLZ R% קOo`'}Vpǒv5hp oMPVW۞J"{!pyg'}:30?U x_Oc^Nj'r?7±S{ _ͱBa (Aj{_0@3 đ"fP Q"(:N qpY5|?K}LϹMI#`g4 &`8`d w^d'QD/>+QߴKOslpן甾88<.-PYV1,[wBnŨkrr3Cr=1<LBكSe%^m$(b,I X] KQHc58Vh!2xy+s,< xzDŽeҶDMsbL:,)܅z:Z HD ֔tKl.0e1J.tƨzY:3{ؖEU,DQlEn+QnRIW}1jt-Y[Jt?ы(ВXŁ Щ(]G,׭ά,bFѣxZhR );1 0 Dpu0z*&$ >* > $݇t_j:)i=27>܆>r9|=Xy m>Z#"Ł96Dj#a "a gf˪QL(olS$TW D aUհQ~\Z&)aO4ß-Ad* ZxkR%U@pϨ<L Xr4fv5^뀡WҜF ULBÓe%^hh4'&%@Vy ΍CaOmo2+bQqwLKt!s^7֡ohΊZ3ps:0{ϝǹiz+dz†k?9(I@Ex>=oIf}X?mnymn/"^՛ut&#H!{s撢puEuf㾟q2_׳:2g!n]uxmz):5(\)4;qV0 0 DG'Mkq]z {S@m}j&H8*͒?,wX h{u z_3ie:oh_ /Ā6 cŁ],0E60@3 vSbvBU o)Cy Cccp!0ZGLsnEU Uebj@L {JS0T 0@oD̎^9'W oEH@U<an>ρs˶EBT=!UT{T=P6Jm1Z' ={顲uP"Qґɲz^zȓ!/UlFiN,ƒ TbHB=GtDd_(:)X`!l-ѡv h4`l:Z}./n|pt\EK0}%NI1,p"9r?`NSQls-ƵWĈd9Kb cPГU'L$'.Q ,lp.xk~tq4IU;a8>ނ? jΉheBgMt`/ǖslq*)/F?N]^1۶ƶH LB96Em6Ք sTm q DԨP qb!9mM8W}wz Lo* PE_ĕ@f^Ƣ4jlu;mkPF"ZzF}Fl<ډذ^lb0 0̧ λ`YE^8* {C&F>Gvp}"b'+о(e! ؓ_%m6bmؗs^_8}idOOOZ\ӟI8s6 և[2 lAP-@1 T5V ȷ-k^kdTs:nceKAv/]aW`~4u*b2jiaNd"L^Чar_DW.z厌b=N Ti@3ZY@ƆBOJJσ+u1N#D1;`ծRrڜ,ExkCP)DzlWP-1;E "B YZcbXB8ՐirFY+7YEva;vuunZD] w]VP,,1PQ${{ED|ܹޙqsDŅzQ @ml6&%Kl%.+ ,J&IR9%ecvB^;BJ@lyOj >yg m?`޻`d-:my!8..UUʸ Q@DaU  (@ ʧ}ʳ,*X$. /Z qb;- VWPk/-xjaƲG0ALgAߚ!Ys+j- OHLT=P_l,(: Ђ%MBp pzЫ7f<Nː7nZбyДR!)=c<ca$R#2Οq]4̈́IQxfw3uq=:=sw2Ny15ck{3{e;x+RK[=#Gc?tϨ j#{Ngx%j` u0y{>tBB7ף&(G!!!!!!!wR  jkiypyF79 r!o (ڞϚ3Xgq; cѨR{J{H}/%B @mBn$AKl>~1hSA8%[> (  /^@pp0:9K ss 8 ^q8$ $iT MUf([ Y4zxb<>+o)4C&`hLvZ -rdD<[YG\ڼjL=W%Jj'D {$(@0 ֧"8JH|pĨ_t<#{t|:~*F#|C:>QR+=Ľa_mmϟSN vIݴi=z$@6WT pÙG>Peo%%-Z0"$$(ڝ)J`@4k]i)DpR% ,ES+2#Lއ͉/x~o׿\6#3~:~'\e jCFRZ^IU)v( z̉K'"p(q}/`s+̝ qw^0aؓ)x8WRPEEr8`}6E,d@ @֒hBIV}"qw Ԕj/zbxqgU? darފfysK7W΋#>C(!xmxx8x"p7^vFY? 8e]+Μ9;v?BLt4FX)R5 (a5 4p͉q{,&cރ~X2y[peuغWx o@6"&8+oT`BU(cdzcǥF )v8V2%vp` 풒4_+'8@OH ) p kI ) w'YT^j՜s+u~.J?2}[A oXk7F`|~IGr(9lݺ'NWR6)[t^3Dǫl%3_7oø{.\ݻwÇs4T2],p`~ppp-p PfT 8pŭ;^)p҂fd% 8 $$$$$$$$$$QH#{U8RJ.AATy:/Hٹ۶mΝ;q޽d-mȾzgW$H/[>DZQ Pk3f9eH9@IiJNg!T90 np V !qT/kr8xGN2$*% |%3bܴ]-CiwOfHslHXx 8 $$$$$$$$$$aTR2?!(?B_@ 'hz<"Y{Ih^|i] mw"$h@@VP:U!q*8YH B$41x!(D'^Gg8yw8nDu0(8$Ob8:Pf@y ^'hߕp {?"6θ_^$Pz (@P6p`)OnW:UFV Ly_͓nLPqGpaOS[`=ogsic pe-\vYͮc0AvtΗg<4^Fgey|Uj) h}ЮPUIZ&54aЄ:Tޗ4^fq>EWU2 y=i]m,4C~<&#~x=^wL<\co= ypYgX 'M˕hS,{pؽ,??;^`B(Ku߿'''ɏ薀Oaq 'xKJ&:T4G ] Uk!3.˔T:`̓ݔotv1[/D[y|e6NG1n |u#TVP{j$co+W2Thϰ㜀&>4؈ R'Jjt+G=q+VR߇Ѹ"V~ѩiKobq?mO9^8E8|#",Q<2"jqi|2GtK8}]79̪p@(p-̥H ҪApL%zdj_:'%<)|t<FWg)pkF1`+6s߁v4ɗuqޭWG([8/_K4_ $Hw$pNH 0V,̈o%=oGyki`ݑv{,ݚצ,4oj^o`%oDUa -#bw]`9J8$g`h"=t,T̫8 ,dCM.7 -}W=/vمC,ҫIkQ-x9M@z@i-$Tj-T;ekK?,4' HZ u୅4Iyj-D7J?dLLI}O%Ƀ rJJTy@kGlܪ:8ҐW3l~Ɉ H-be? u9ljdFlȌXj (4yK^  `F 46p I" 4j5,DP!!!!!!!!/E5MTT! ') >&-8]]b4xR%y@ er?CiGDO8DIh|ǐC j|- /ͺp@("8Pm)*m@EI@ܠ8 YRġ8V9ՎsBKJn-ty@S0nVkXkt'#tn]/kC[*%W<R{!2' yKABA;[mig>銕RA?̼?WAPK&u\^5jYR1A$BgvB)t(J1_*pK;]Ozy !.wp Lr^hTGTt d"!!!!!!!!"u5|BM- P^ $ *;KP:\ ë3/c0L7=a1?zڸ.yЬ~ @@*(#O(AJT-@'[ߍ7!fvEeyq:< @ES%W ?5UK{czo(z{~m:A;B* Frޖ)e@6B PRJpKP[,W*E`:p ޏaQ k*$$$$$$$$$qixƗ5i>5TI͋AO:m= $_t 5H<γg" 2_dm%8_u_[De% 8 ^q8,PB,LufH &% Z {{@XZWV:1@ICaQ0b٘Zlmy!F8Z'qǣ{xTA@-HEIAxƒO/8(Gle`Q/%z si[`7"ևٓ.#aþ3FͫS U \ˮ򪁺''H^J;!qT [%v^U^g2Jp P7?6MPHOG[1L^,$$$$$$$$$)禿*߫x>?9]*i+q׀OOꊁ [߭#F y/";;;Tj N?`+ݐ3+IjcXaʦ<ϔ@"H%@.% 3EkQU{24ZT=pI7'r_ zt[wέESqIj/D~n p/JRԸm4jp W7zz%ЧFuTP{Y$$$4 z_z`1Ok['hfmtk 뜺`ӳ-v8ľMqԵN5pŽ qϳ,=ɫRxYCJ(ApfQST1pݽ.I`˺8!:7v=̟spQ/{3 Ƅ#0X^5)MJ^PkQ~dBcmgFmԲ (`6UR7<{UfYI|v}J8kBQ==ԟd'sPvZM8kZ|l ~n%hȘXhXxYj ep/(N`BК.$Nqp{œ'OB|UWxdC0@p@(5C5 Okz2X~YfbX0O|) s`yDo_ m6e>Φ}9.ĺ򖙨׸LÌ3b5E6-ø 1,|"u;K0q,:G?S0c9\h߱x|V_3qܐ6LKǴvQ%옶)y=15xmw4foX3n,Fm9w e5ib j[.A:Y&9}/J1$WL8$g#xzJg x}PzE@ɓؿ?#GI,o3(p@(CJRZi:$PbTů XXrLx%_Z'Whb/GKs,צBB `٣X6<5/O;gm˹ C4jFUpǽzIe8xY,'MF=[R8G`h[gY'9azs}PNh_hB XjGWrPRa 'PxN ḃ2lܫRA'{EQ~YIdPnzq^_@ϮM|=%~Q&6% ے1p?cBɏU" m,u(.yi{2瑸{&jG7-# [^ V9ˡ([[qʃ%dG uTt̲M;w}c[aYzl5Pd|V +P^b+^ƣr "9~p47)JyKǏ#6Vj%<"pvrv)%Pz5u՗X!'eH+ vC_s6=;\:[}œMU8ps53Jk}޺EaJ6G; ۰mm7$}&\|{;[{a܇io9B#u888qqeQfGy.o(UÞ6,JY-l+?B"8WXPb)|7Nn}+ t(R=>0g?cFCiY~ګ}ϱmxso.DzehMWsmޗ-zr+d+ydגͼ +XPD#2AFPj5Sѭ[ [8`71++C%T-PP* P[(-h;jj& o+PR>^i/e6Nmyۍhј0  `ţ`+̝ y[܊{P"k}r!VWP 2&΂ YZM. pEcvnFq ,u셹c3@ FSv:턬rg5tPj'Dl|8%V ǀ9*j (@W,*p + GJQBTQKM 3S@jwb>۬Qz .D aڅzeGcZኇc(ι8 <(\7kǧbY8+GllYzB)gn>UǷ/E 0IחG)fs80g{t\ϕ&u\Uo_F?D8ip*uPDzcBSdt9m(r\Ѱs:~)RPWl04wpיϢhQp972[1-A%>j e@|~D/߽O|h4=Ey_! IA˵$i cԙ5YJ(N *KT%H@UrJTE`:_Ck@" 8-fx)dPL}9 0y )^8 ξ 5y5yظI?)h9#Juq̥haU%lކ{ lbǡ  Ƥ#30]lN-KP@]d@,Ċ9G2VA>=BWfxYE i<ț/}dl?'`?#U[Xѯ*O?͜:ѵ2_WETϱ}b+ҦÚK W7kR]~Kk*ZCu&W̙\s#p.T^{9ӿ_`uJYpk+d}k?2x 1yӓcoQ׶"{^%o  e iTY}.Lgp<^IRk4B%8͂ h71t@(pu]qc_żA6_޼yܾ}8~8\]]q5;8S4u Z-Qbrz4C]$H"Ő4<STAcT>V8OK:\ +3ӵiS6(TAp*b٣^X?y A&+*NMk=e_4YGh;j ?fɀx ۵п<3 Y+&P7 V*ęR1@`2Uc5}Ab ?><]O9PD`}Ap,3SwD~==^~sٲ[h1+<;^x|:8nojG ^^Z kƨll},oB7M.I*3EeA;u7w݆l?}cޕ1]c񸴤#JBxtW=m7ÈDGf)$$$$$$זu{ +PO-#_$2#v r ;b?/Dv` zO 8tlq@DpV\\ݻӧOcǎx"gΜ쌄`;uZ6,IgJD'BUf( @-(O-A1Pa _nhpf jAu  =cƿ8 X̜/:x _4]C]kCOpGC{|& `d ذi'^@mV= K0p0&9@b@,<T98%Z4~dL[8J-  v~Uj !qn8gA ==8o53+}p) ˰uJ!rw*PuDUh|htRBU:ؔf EӟHL8=p !ҾYsF8~\:U1t{c-`V͕dh>e+go?)!!!!!!/L7P_ёt&Ajjk MTr8aP NpF;鞍/s,nĦzR3>0?>a&lҥK{.كsޞKKKPpP F^zSzRٖ3GJ>+C& )UЮbb̃ P*@`J֢P_gj9ؗ[0\A@ ycc|܃ZdMOlj5D8K`ss~ь;_@@PoX'901@`<$00BXZ'V$ƨ@ PzЯpET-@R@  `M})L,*p z7pŎx@||lLa@p`p T:) Oߤ&:ߋH&25x-Ud3dΝ9}/>bMws 9O| o%G/^ <\J x N6Ei?[ ڠ@U:UaAFf,{(&@@wCxИql\+Y-D *CwFap>Q/~w'#X>5IO;`Y[  :'B'ݰq,}ԛW)fǙ@``8F LT1ЊoR b*#R=&Z`] VBl'qB 0@jqƛM]^ԛ)vbQ} L,*p z'稧:u:ze3b95^r MP=^w}ĶB/C+vk_/+Xb-ɏsoy@ݑ[qWG%Fz@č(*q4-”|ǶoU`s_YqL#P T6aW.rR'DC6T>ٻjKPm-cUlgF*[yGškX,o*$ 8+qO1NK|٢ll,o/D>C턴5m1M0E)RLω` h 6"6CC -b:n[1]Whtv [/e3T5W #0P"X'>NNύ 7-d&l5z`9b?Nuo㌻;# sco%dxe 0j!8j_)-R?zbߺQJH Z98s0ƝC{x&[hȠ@-MaWt=^/vKu)lL5)5xtT"=81 u c̮H?&G١`$/5寄&PS]ظo0;+}.++EҞJAu@^O\ Kڱ6bW>F[Q5Τ{v D["DW(}p+>A 7X?{0bsX 8 ^M[dcLpNc|$ XhA^E@m@[ c(G @PWA 4f:\=z\7'aЭ qg,ڏ xR* '@nX+̞tO=N+2^X/ o팆 `uc cW ,`@1o\1@*L-8YHVU P7UrݓCEd% 8Aq8?=5k^AHlſ1Qi -K]]d#Sڮ["(zyp)8G(z4p9{W Z-USwR: w̫r&8":6{-qy/䝠Wn)p%vOAoǟ$;Sb@2ъ'SqsR(P_]6ῧ cSCMH op8uLɏbKZמ(// Uh٘5iZʒRw * |ֽ΄{v }p'6ٿz5+B铀Wa1+Cs. 8 ^h:ZH1x9=QJJ'BRj5 )]eUF2 a ƨrd j%hv~.If1oMmF}&; Ō{0* ">X؛Jzb[=Ff gj!t SُHn<<oN@v:/A{J1>#Sk*Hz|pB׹m1]16Nh0SW t/=ݧc,P{ L*p pG&MR_ܱz_qU$*vLDoC#}4QJmjѠ;FdTA8?~(U5w4ͪB(z>|NYԿ_2Ӷ#߇~ХuWpa:Q%PuGtcofbEQ{Vc\Y!Tbq q(CԸ8ϮK//;;!Z7ys#WNu igOCt|W!ORÁw3f|1q Qh; DG!!!!!!!lX܂bHW*#\:yGud<|-Z [|ѫ[ 8႓c10IjBc4n#t9@R1TE@^f)M3y AyTA[ ɀ` 7 VdRpq. .B׫31n1܋@0~0s9,X/XIrJ`Apxao?B=$d;]@KfZZ/D㳋р{ 1@ %2cNl[ժ 0B,Qc xӸ}{ƒ~Xh d 8A%M|yrNXhΑ ~#`3JRG!Toj ܺqZ4@ee|y>F/sȷ'bAhCA>?Z!5"=`*t,d?ikInoEZ: Ԙ#. WoK Pp DFAXp!.>@ 8 ^MoMbќb`dR5ACnKԝ),y"u,,P~FnΛXALPZT=[[P&V9 h{aO^nT孆&bAsg4$o?1فbI^|rr+mr+!K* nLЄ g$7Xrؒqq'Xb|O'#_ _"p JFd%;_K?@ ޕc;÷ȥn3yP|hԾγWh?Z%1h Yp? BTpJi&\v qqq\zU Os U'P5s{m#%[ʠ x%Qr!#6C2 :Z TN 0G-r6 EX 8BLzr *nV</φՙǵiܓ (OTU@@;ܚ~nN=Xt=/! sASQ 0F=(GsW*dm0@O`,K%I` *eZ6=7Ƞr6P!.㊭-lWq>χbWy _A4qxZJv*wD+I!ρhxMQ9w_4'=($_hW!BCCy 0$ΘG' +O=!WxKŦHj5hۍERX6?Ig P s4bÒlV ɀ (1^{H-vT$@{J&`@S$snVL&/ͅ t6W>ÂAfpC{ 6#sPRԡ hW J]RZ*xƩd6fr!W Z`P І`'tox}M;:3ȠLEbz9'g8n Z鏎Nߏ"wcUu dQA&spz~Z%Yhb0{+ e#%DvB* 572>mRJG7R!pWp b{ O +.rBB$Pz5s7\vFfRtaUJ>K`=@*j34MFK`bT,%٤VSB]W!ʡ5vtdT|rOS!jC^ (O3;]+ fJ0H3Y0dYjA6nu~-@m.A=v'WB#k$0vܿIZ XƂ6.3VB/0do^-&H:4o fd 8A<:-+&cki$̧1?1sÈpFDiqglL]{>M~ xg׬DU=ŵiIj֬5y:t6^%H@jn?Ul &õ|-x)O-~ eIqfSj3TW"h,{!Ay+Zu4' h-HfíΓ-b\.s Tg` 0r+!^1 x @ 6-nD+6JP  (ed! 8 $$$$$$$$$$aib$ϥIW uhbtO!Sbjcc_ yEkBooWJ ߰hX=ٹg8!QqMS^AHJf\NW0AGQJ>S"Ҕn+CnA|bTL3NgT,d՗[*@?% PA+Prԓ!%i?'Od^L*OS5 , ,R!jgDmz! i{ I1@`&iU 홨 W @Jh@'t`Uv&h|9 8U$gʕ+ e?q8@}.Bw 4(qNy@$lR$ٳkP1[AH(- 8[jxG3q¡볣JfXέXm(ѩ-d@ t[=6 "h|iU :9 HfRG dK z!*jGW6C܋Cz ]礖CdX2Ҡb^ub4J 0AUda `]Z %)̇ d:\[*qҸ4!2wLlfmoC" ct& @0Z~o[Hɠjf["p@HHHHHHH3JJH ݤ  P>5[&_ij9G)cU!/THHAGUP4>}qk)pV||R 1orVnF|xk5$SbnB З 4 8ᎅGaqC%-___:t/_ƩS`ffgycA ::/A00hBJ@7Y4S]ZK톨=po >ZCxE b3,Qg%j͓T_bjA d&KTA:TcUkyba# H@U+jT)8GK-$oFH 0@m+0+VIȌc U@h@}uAR`T!@P@Õ0lBGt-CjafY,">҂JH OʁP{&/2-iM}r1qb@P DSmx­0zsp#XwgVK\p_{͛#߸~GC\Z"E0'A&!A+_vFOъU RZ xȤXJ漅O2@w&'?HŒqRHyu,To̶7Ar"a (Iڭj,;hB``Hж 3tDЂl9G(}903p H Ul \qJܵbr U0^{7~^ލQgw+D s u>댃}ܽO8|{v;hȋ-b^'ܽ{W~݅ buRC)$$!zPB@VEnE/ƛ(X{G(8 *ޥ/DƩ'a8pN>ƿ7*0ipV`` Μ9B/^`cc?Zð\U_(EE0ɢ[^g&Hh$HmLwC j@]A0_K-Qm TCfQn1d@@FvC(!Z'PaA9 _jj#Dm8 Hl%T1@`<lPJ`]h W $L`@i:@5KWl6l@PҽQSh_nnp HO.5޸:u0Ƌ77E'W B>q6>.JͥuP[eW%1g}p; ?W5Ğ)9QQxgtE`OVy`CB %/ G3ݱƳ@y{Hy ;amǓI+hl#.XX5FJcv(),$- $$1^8BYMZPB]g=;#_Ụ{x.D7:۷@C: $fl]m`ggc@l ~a1xs}aq9[?g&ojYE|ƥK8pttիW8;;ޞ)3i9D?è<UaQu>O>d4%,as icuVjc'`)TWs&.QR~6T#E%,pHYAU|;xW*j9 h6MKu@I?s%@P2Kަ T^m*P%RqP8*ԂrA+H`UH4o.K`Ɖ|:3A'26BT-@}R) Ct8W?tXt(1Y,$zy} Ҧ(1L^ dB4dw=r ؘC~vunP\33l\`[xWPخ3@]_kW>{BS(]wbXoxtU&_D4/aR+/rDq7 _`1*Ǭc[!!WF}v%DC* (G^ˮ)ߚ~-"p7&V(}u 8 $}v })[ÁxFg0;a;6X?ǮlyЧB՜ KaT~*.e)f%Js4cZP!f 4o1D bףu/ ܈fC:`lPLv<5Xb T]iɓ𔌧< (/W$S"ا?琀 %AzS APX 1mbgg@6 n.ֽ6r0@ q[ʭrO={ŢC0[m)@]#QTOu 鄭BO/7m5)W~"llO 'M _;zp ?+-Xe@KQ5C%H {V~샢42)R~nݺ@Z7pM\u~g~~x&LJ]  0n|.1E&Hdb z-d2(N^Z j/4VeOj(?@9 bH& >fZ ȡ- +cq0 W T[i;7X$3WNW R5j24CGg@ h@I` U  w%=nt^oUd}R8$/ 82 ̔_q珡(}Q˝'ͱ}0ayc&~\,rtVqARjMZOq^iENCZMc}lIpk ?Q<.ZCIh ;M:n7oEx^Rd˶MVXةrƅG|Pyi^ Ο۰b?;.:7T -N"7a{ ٺE+ןOutK+'R+zs̃!+k'x-ž'3Na(MaO- ui:UA!M<ΙðVϓsi˼y~`1.~($q|XʋbV+?v'Dx sګ9ǂJH IIiYk!,-у2.),gq+B&t#q#^z [>= =qE|u7l䂃cJ(Nl!ҫ9cQ0*6FߌQq ( H]E@ kP W69ڮ$7`s4SȜfמcZ;UT_ngӬ}̒W$J$ئEePʿZmǶZ 0@DπV;NlK6OC|9"zmDT5Nn'd"jd@`. P@ -PoK 8@~7"aCT4)A_sA/w+a,xz/卦mO.Ы|_O&.O`Pe) CcOFB|

hq[#~~.px^^e5nɞCp?;י^vX})"0^9,Z)gM&8[oxzb u΁ [^;43~b>:*.m@ӻꡄޮ @} %}.7lP=@Be ; &sɜqH % ~٢Ő4?>ш̈́B9^ijϛ!$+7]pݰۗ=߰hD}| 8 ^ߎa'j 3P;!*ðު)Uŀ6 XCX#ݫG]A0[e.@'1N:~]>+Lƕ㇟Jl~Q' "pnmooRGmE8F1vvIȐX ۇA5;*byFʡjQ իVAFXw%\80-_,.[z}S[8۶sq}gK}!qB#c8{cr` =ȣe]y) @q'a ԥk>W^~㄄X!*5,;ѹ)'Jؒ!q腕hÐTWVf0Ρ?4#Is9q4\`iA_SOK3dpDCL;q3.\=-}jm;) `|[nx#Mօ>>PB@#?&AOvHgjK|oM,}ȼY 9Yp#bv~=J=AHH> U1 8x 8~ ='ٿ)Go#7᱈KV)+]%Pz5g~3 G<,A,u s,w?PðJ'VvOЦFjV@#dS,QGz֢텪iꁔ-ȀTI" W v _efqxjT@~ځ 5`c-t 3GAhO6!UQ?j/(UNb^An^(P_ p azJhֹ{Ru@mЦRO8}.8Vɦ +7u._V:GQ {g0[欣xj')s/H&SNlt~vQ+KRy jf¬ >קjϟ%4&{@[Nޒ6/<} L)}+kM Q.9B 8X O$n?Vœ=tWH}} %܇T>36B*P%룞YSi,,|"p;hkv,y_P!w+p@d p.?ɇ8dx@{`_0rJ^âC`X` O ~,yPQUUԗ {5yssvfN{ZP@S-zSꁺ^H-S9yTXHT@ (PV ^1 ^5ǫVsvBdzT5Η h v=uQuUI3U PT5`}j- jRaיBèԤVB*н{T+msيd}B |x)/ p=#Tׇ!/EٗF +M>8iԦ/=CTu%EvpT/q*"@#,Ff5lw 8@ݢ51hgyBaQD4FDp@兕5A=/[[NcUYd29ڦ6Mp n)}p@#uBsp]g?4@M,^m6=o^UbAkqLls8@o`ASb 5FH ϲ!70k5>-oLcl7bl|nh?Kp4[^(XDq8Ѓ]S}C^\v;Rt鮉J`ۗLx<> 6zM|+p}.8B' 4 1P?pY@\f o%cUj'VYyg?Q"*n P@T 8Ʈ[ J{'>MŧwEƉ/LWGχ׃`o e1  $tQ͐A?Tpsżw~9q5r :]zuh9`#n&:UԓhV>X(W,gUzXJs8@QJ!B9X@!/@A2d΂퇷dB̎[VՀ5pT5ИPUy+mzHUdܩ):Y ad%0U8` Bӹǃ\1@Ġ 7t{W[d!}RC'򲜠D8`d;7;e_'^v"}pߺtIţАYmW}pZA;_KUUGOu?<e֚;_}:G^ܳHU-پ ִD8man*}p@ۺL.o:WuNNxe%?%(ZJ SÁ\_C)Vpj+R'2M%ۏ#ÁعuXBc-;Uz/®̰qǡزBgr }ip@  2Kg իd |pzV#"G=q;}! V>gdf"ȀPJ+Kɓ|q#ca gP$7!!ҫYC!@08 tm@@fJLUK-dxLyk=y|9'VHIwJoҌ9jKTj/T$r yv81  b sUqc&{ PJ 8@URFV С:й7_ge0Ȅ@ S#j'D&P|< I` ?/t~AhS,$2wÁ1oIQw1Ns2zS4>QƎf8a%Wa!0:{<hHv YeyXC>{V^k9t=+aV pB9©@M,V'^˭bo7fK"=7®c^rllxn©>cU@f^PlC #{3%f\_~S te8B!M\8A&J&@.q0: [z,?Lûsht ljWgSAʤ_ E{Dp@KP]{⸮4nYɼ:<耴ܺ)pg(X+nKY%k!X+{Jf Y]za`CC{bn? u|W C#p}}i M^BB﫬@ 砖<Eq+T-apRh=vY@F%8 ,k`TAUN;o'$W $vrG0[A,"2D8uρx<ٷu8(6@ۣg&p J_SB_N⏭n&tp}iV1LnX@J\yp1e9b]3Pm]ߵ~H]0RmZrtYО }{BW~b}ZGz@pxm8j~+:Oӟ8Zr#t%x2AlbpgPٌzAnzmҺ$8PpʊE8¤FTv3z;7eC~(<.[°[nԛlzO8=Z8S.3!D߆@&,#pu8l ݼy..._BB/ 8 .q8Pb s A> F(~=FaTt)9]AaTe!j,A^&kۚcG376;Ѧ9Z@AԣMGZMh8~Oބz6L?oFQcfT_bfT]bfT1لʦPɌņM&Td6[9~Il=cW4g`~*Uaj,J5nFͅQ{fԝbfԟ & GoBh>-Xuosml@{ ۘ@Z4^ +`X{T_ ѥRfJLwS`$>Ά_aQ0?݇=X7QHp`d 8Aq8P,B|`AQ(q00k;rteeS/_ #'@iy }ZJ`?k)j(̓cD,p;- .F}ȏ_i2w W|R֌P_zI{/On=!@BߣV*˕ ֋ '8S<+ ~l? g3l{HofՁzO!`ycϩ"&c W]W]F˖DC@ 쳉uWyYss`P,ܗR HjoJ7v,na/q?D_I-|+ V .z,|l/@7ǎ\@um߿666|rb\r.]⡽<3cIi]}h(L1t46ο <<3P 8OG!,DQ,FDqrLJXLSgŴ(?Ō(?SYR9 egLM3}:*i[wtcVgLT:ULW;gv{@u4`E׸SѴ4c߼D?CZcв(dcֺ0.Ư@6ٸEqnUn=(-BםjX-֭ZȌx~ eȠ@oPtY-[eʔA1dR敩ľ`,ͷQ.[%UCu&>YʲcZ=K7d &7j%K4Jy=yLT3lY FQC ^<ʖ.bM}*!8$) ŁߣH͎z]^\F~4 +-R~Dm0ix#%{VaRƶ rmr_^IRß߶2~xsw(x2:4xw JbY ;w@OUwקk(>_E;1M78fF{wm뜂cOS3~ K%G3q!B=%AS7A^?gٲQڨa$1*/:']/2vƝ?6o?fMv3i7Fc[;]^+Sqx};.&JٓP2qPȇWѵkŠSG?$F4*'E Vl߯*KR$T3>}6ty$f{@%KN Z2l P{}x Dxc쓞:{\jMXV"c( y @WG}4DΛ8EB0իW|7U\v-Çq=xyy\'#A{.oOOO|Hܸq>L5V8}4oAcǎ}عs'=z1F~B_=g/9yܾrWp–n_#9&q*ێmwvu[+gq7>Mq>acuxrj`)x{Gb;{{hG>p>v8kw&;k!_N;Vvt|n>mvpxIN8idAJ t>˷.e 8A%ztE瀐V714Ra[/*}""_t%E@轥NbWZAj_ $*r2`U '%9o'݇~F~p _"4 }28Nߴ*Mi1̯dĚ7%)Y1ŋ2B_|||kn w:{"$ӫ FzF$@,ΞBBBBBNIu1zKyTCͰxt%E|9! {KSؗ ~@/4Q>,􆚂7 ;Ƽi0Od5Ū4V>_t|,^G&'_y=2>獍P%cccg&$$ **ެ'%5 Գ~Ϩh[ZY37>>!!!6hLiΙMYtt});3MU훎!OA7zgT#=>#Eڎ$ڟ|~ Ky {'5tii)ڞAWHHp-p p@HHH(k@4^_+wB"@(/K&%CYF?y|I$y`$'$D-nN<ɓep?~~~z*%pg8AE-޽鱲mz>Dt'N~i>nyRkXXXɓ˺PRf_G>It3C9=#%@@9p!;a=x'G(׬ tb>^: $ 18~Dz9xp7n2n1CUҔD/7$2DI&JQܹs%(NI|JSb+})o?|'-;wx%(o>Hۺu+OxkeL/ :_J`~32;Y[4i2JlRb?IDm6>%:/#G峗\i𾳙Օ'3 { F)!OFF7;{?7 A5zm^|_V@W}Lkz=O]s2CB-}sP2}׮]|>,1166":/ŋ}Qb=,$%)JpƝ3A(g^6l؀@y_._%AHz$6O^~GOzPB=wz,oQrz/#̌PUTLI X ]?e 8Aq|4R!6q}^˸| >lPVR\o\Õ˺wZq.5$BSU_x^óh85%{8%i?ԪOI0J}ϓl4VƒtT@NQ2ZAƃƕ??4sCDIITJR;J4>B_h;=Bw|Jv{t>Z#@?}^D#e?}ѹ{ϥ>K]~@cD82/2E"D  r(1EA'%2D _ROof?%>"SŇ*徕3CUΌs2KdFG)Eʾ+F;H97Z_9'%r.y(kW({K!!!!!!LDiBBBBB_D"MHHHH(I!!!!!!L  r׀Z9h#$$$$$PHWWw7|M6}pd!!!!!(onn΍3]HHHHHcI!!!!!!LįiBBBBB_8ȑ#8|py eJ2RDqqq텄 &!Z5&&0Qb*:p AJ8kl!c50V3^UpAЗ8݅uK?[oĊ>BBoDJ( $$$$$$$$$$$$'o\/zś8y! C\pcJ8f(yq /~Es]coP>x{*ğ VW|s܋FH(SS9ʚp3(>Og`},}6XӠ(;V渗㾣k|l-&wBY[p zF5XmO9m l"s3y/KjgQ\G[*$$R&E|!P֖Y@~p W6 kc8ocQ;7REl;l\n5 PWHHmҕ 兀BBBBBBBBBBBBC|vqwhԢGe(f_k}p@ #Q3Cpc 8KȐx{"8"ynJrB@!!!!!!!!!!!%2(uB "# jpCBd hcMǒxqu"_c)켁(e~"?ZHD&'eD *6uUG<h:cTl Qb$"Ҩ%m%&d;P cd57G$G:OE*IEDX8"IlزyшOH{Hcmc}IzͰc&ϞQ>Eҽ|Ib;fFa@_kBt\:^:&{q<*",]O'F#2, !Q)B9]"r~( $$$$$$$$$$$=%@qt*C?8.ժ_ RCx}%}+}@ BPż o!SV㮇r׭h?;"_Q^qC23&,7cR-DI3! F\P`3tӑttp@ϓ|rMJ.rm_tKR oWW "<g6F۲e틢FHy`./{ 18CCoG i4cҨ`іCpn֭УE,VN}̈{4-__~ë$%)F)n^ƸǞׯoQ$o>yb̺Ck|x$T D.C`~ RT:&nI}`75jP-aLv |>I7ڃW]E X3jɐ#oREt̖X=Qz ||̚h2ih5=u3h??mkp J|"B@!!!!!!!!!!!O@a)ʬ%Qq=0;f6Wsd:hٲ97(Azonhc!+5yhѼ W(z WIcк%Z6olBߕC&ТEs͚qNcyWXEM[iJp^)ۊ׺ oN+6wo5k ?&ͮ3Ύꥼ~ZDp .N@"z4٢a ɟz٢SLI*q(q]*#ObV9ZjKJ@J_'oƳ{{]Jkl~ /NfX1ɫp6P^Bc{_ e%8? /{JuмW bymE {j Xf3j}WJVCfl|[Go(ר9m?R`T(;T.`mҴ*(>X^[0n4b}^39ux YY,PYL|}ԑo[OR1l)zU FF>*Jk5,zE^(f=IŹ[8k\ oQƄx*?SumfHP_2Ew/Á*v l84`~gJnb+cCnb7lgia2aGׯ(η}gJz̎YY}ڒ=y)W@bٸT1P#a,/áA|C^eLǑG>rr=O챂e!i=q(+7bB#,۽0؁{.Ҵusv=Vγ4O*:ênUk/wT@p5(޴Z=C%\T˞E~ē5$$(drvs//oG ٵCV|I16z VYHה5hO3q%sWH(kщY9Y$p`x._2?_>4M~l`(V&1v=axi_w}g[6-rt?ms pim-\kRE;6yU[I#;fnmavoa}:8Ǯ8㝔Wyg7YͰHp]m?x| w1 {Xe۩"`F+j٤~|ha^3=mp@㎭Z'RvʷVQIp /7[Ʀ8A$8Y☀y>~?kMQRLU^8;=Kޡʙ$p`()?oņ Y fid|߱/ស^O`1R~yM'p=1ma?{ptOT?%x VT(]j7*MiFy \|G!=)jrl̋UlrǺo]oNX&N_NXwAFhys[}3g<+B*x߰D<5{`;~ ύ鯇GgjGcXsϰqb Z-%f%8P-/r(.1F fXF^JrÖ^9L`,,LG19Uy/Zdigu=Álzo0f u>//0q8ЋYu3*εH#k5hǶsZ=FW%μȔ!/AY( $$$$$$$$$$$< |xW$J-ՕtǩE)g*iY7GN$6Eqe,jї+8tp ٭ϓ _U퇵|IR_į ^n:s a!;)PaCL!أ)l]l}$oJ^g<8?=7ܛܛn7j{F]D{i{b{."("HM1>Ȟݝٝog7܃',=J,Waʊ[ZcܧX'iiy LQ k1nǸUIL|IzL3}?|ZtOA .<;->zS}Lܬ*%;mf] foLOC?wHL .Kyzw݉i]oqsXs>'ZG<ȏN{'bR+nT-(kKCl8xOuWfݣi~Е )N_^`~4^?߃u7*#lDL~v[H$Z) H$D"H$߃E4j=SS%ta҉Ԏwtףww=.t8X_"ѓ⸶vD% 3v-(y7w:㟎#Jճ >@x4yy{+kky};U,۾s<@J q@9rMȎ g O;|I1ym>{ w'\nwcɶW|ޙԊJx ,S韛Zh꭪#\ؽS%4.;͢Y|[b+c:211P3ja"I$;g( H$D"H$/8pQP2wL;u<[݉Ɋ8yH੤Z+b}?3!շŁ|yu^{u^8tO:O`۹緸ˌ(lq\ٲcbȥ5k5gqMFGkā[8?ES:~{gR?]9pm+S,{8Cn]w~X}Lgh&<8q@UF8Z>*45dqfTv֯Scݶ;q>_y_BbVKtV+DrWQ$D"H$8\wtRfMs Q+5;VZa [0ttU*%4'wQI3W^5U2rR72'ymP:O{p V̷%i~H$w-HQ@"H$D"H$ RO϶8.[(Ξ;ϙGcE'c.jcU%y&s\: |8[NfD;Ĭj@ٹ l |7Dp9.9| g$3=%l.* kECaD?Y3Zo~e` y%1clx"ʽ+eZ}VC{,z5G8 fLWޠA'*ă ͭV,meĶ#9U[U~*boʹgqr-?=7q%63DF_%P[d(q)<{mGu0J*-|5]-h 0y;޾xJ=a)GY/wƟc: [P=צ4 ߾M5a>v2Vܹ0Nl +w߿8˂)\}&9%/yUzw[vz.^Y@&URǞ6nhփwǕR' 'oT!T{fz9yPطԃ/?'~{机('Ӥ _t{D{{RH$D"H$6RO[uw0_EI; +~{V&ewkO?Qǟw:0mqn}eUk_㉗HY>gkn n.p38Cۉ{|Z??`ƏF}_cO38fs䟿ϖ6un=םy!5FF琰ђ>x9 ye5ܬC1ߧ8@ &а2f6ݼFL)a2WF%1O[=؎n5(sxwtNW #UD6CǼz5. %V3yJgvsk*Elnsjo Dm΍msӥ?Uc!ǟw;1{W4ro282X@e逥 -TA|ud_)Ł^Z|k> Vr>^Y0pP3M!z>N]Kn>az i5BԗhM )5d(J:CFrS#s,k:FL4˃ &0tR^ j3lөӢ1+rn@(g}zX/\#slGa"J;p5!uنߤ@^{8-θ =*yekO(=4U ׏YGk{ySY$Xx*ȋ݆?"ȮêJHY(L2t8&XX7jE*%=jF}0y5=w<<[7>\]?Hf/Txu1DbߡLZ{fAIu,UݡF2w1;Oi//az$xfouN/a{4+ڷAz\Á^᣶7彵dٛhU9;&=ǯ|3 c$HnRjD"H$D"Hn{$![T=G?^WrE=QOE[}{ 'S˖; H4 f}Pxftx6 K$) H$D"H$n}R%t&![*c@%$NhvפÔjzvv(ߌ ۘԪ">|=\"N8 ]zTt::^OֹD"8 ED"H$D" RO8pT2Hx/1o/is ɏ?E/xN[t}qoȋވ}yRw_Hfq44xE\Ѫ^$D"H$Dr?Hq>yŁBoyÆ4 ܈^ق1{M/Q#q}&43f!~8WW1IcQ_oC7mwVݜήwx_~1r/+r_ 띆ݧP/7p#iRH~+HQ@"H$D"H$)' kU9y-u8"䊥LsR.};#oi!7VA4pה]  D>SrN_/sm[:w,vԺŖ^ājttnFD$̽RH$D"H$Ƀ@|K+\ u+T@T*J(,>bQnRwTUca w)ϳgVB"V H$( H$D"H$$D"H@$HQ@"H$D"H$)H$D"'HQ@"H$D"H$)H$D"(w淑WJ4;ks/OpH F8fL1p\CM>ߊ0eLx/bsj-/)Ԛxo!P~8ܨXr(Grȫ4c*C$;nR6n{0N8! PLSk/5g61y!hv1#WėoDyN!`F0íg3+YGo {.Am\%c?L e@7FqC3{H)H$D"TRxNd +85;kujG1ha8IaBhHtj]zkU>ߊk`hg~%GZIkk1f#\c7cp~FpbH.i]Ѡ)be1uc†KǙV4˦Tɟ GE _9yod܀S6]!w΢NtiE .sC'Yz=ص{pY(82) G[8?Ε ҳs"%v. czGx{%#mh>2q!878W*H)H$D"H$EH$jq}s=nhI!ti9nߪ!\; zˢHE&Hh7vc%V)8W#.6VXI(ɹ7sp*u)eql uowƑOHqwNq@N[ZNK$~wTo)MѼukZWnh1W`H$D"H~8|sfȍ.9u6NH/&4LRrgI ٨Iqnt7P.(όcϒ9~;3tMA賗wP_~w[?ʡX^y[*ʹ}[l6\y~~JI vYȊku:>ꦀ ciSI tZLnst dpbOodDgaX*"s@ozNtd\wsa1⇼gC<Ia{"|ᶇz;E)30[_?<~ìi9b.G5Rb<Aԁvs$^^+px F4fIK'8`E^:gIȕ{[%8z1;uktI'iXvYy%). yQ^{c 7/l.hӦHnԹD9'i'zsY;Uil'&ǗCW#]&y籎mqu)Ia<>iʩ'56[CeVLh+X=c;0Ͻ%d8ŝhkG l8B̈\ٌ߫4x7~4emT}8PLzYu/rj7V&X v&aWbhIt3ժ$X+w_zdyϺ3)Nf/ˎ)ShH˔d2-ЛIcv2za0˹JzyݹJsgZNܬø͋b%&{1}%O&i~W('#.Y'ID}&ީ_1@LBq^{]NYlwC [qgVìgI2Nā\_?)vmq$W=r¶MIF^L m4(6ƾx aBzMv'~o$]dCg"g-# 8"喀v͋ux:Cx.l\ehh(%t5bf͑K,ha"1X;Sᥔ[]Hqw :h- ٩)^Cѷ\;GW/z0K@fIaFHhE5{@ɑ[k`7nj?elMӁ{(*Rg'?XtfsYy: tw=m ˺TY'6SVKikҵ莴W9Mb丩|C%)1,`Ka?#1NL1V4\KLYrgc0ޢ L+iS\DzX:y0s>VV)lBi'7çJA̽์+c<0H%vt_w5=(!=65 pO\l]8Ȝ&~Jr9ŏO<효*-!?/ܢҪw1\]]FS?Xb3c!.l̔%M)oāάدgķV|h:Ο)c/JDXJk #Zi?6Qh[&n dYoNqޘ]Eo3ޏynWҤj`@ux<콧3lݙW&c_ " ڎǚ ST\LaA>y?kFц#V.@9{t&9E Y`ggΖQXt]Ki21d*{3缝؁& (P:O :/:rrO<ƒ{> rv(7v 䱕cIEk+J; vz{",ӳ`dT䩨MJ;e{*rog!'NME(^JƴZ 9ߔ^<#_bȄ1lK]gG,`j-RgZ.vC I\ϯzn%_ǽMUyH q~(ɣAUX#ܨ=ܔ0+%߱kȻy&5 bYt6yu(?7]KӾfk"NSUy%1025a0b))+S=ڄ)ۯr3_/y z MogmG:\RR.ٜY%oCˡnL\IF\S`!YamUY q[QU1dQKbEJԗ|RbakAd0%|;e. fjI$Cw]\wʼny;XIk]wfL!]co rd!l-Qk4О^3W5F]ij̰7._xB-,(!)Za75I ;aE h‚SX%쳃M}/ĵ ĵ >8$>ٚt˫'ٽd&]ywrE9ŲӗvgQ&TBGG}s~\x\œ*ȋVh)5bRͥW*WKe dVU ޿C( ͜8*|WHkTW0s'dQw%߸J&(痓_[b4ZwW3ٔ&DEHy&ؙ8n*ϱT Yy^VLXzʼDlAAftǹQc"QVx瘟9( +F9]Ihޑc?j*?mvI9x#gUڢXVU_/'Tq{mdEeh%J|4֋ ҵG$WUX{Yd.U\fXJ;=E!oivͣCsISs# 7c[Z"JNE}lIXآz^B3qiM=LlQ\W2_Oڨdu+X͉^|;܊+/,LErgeq`,&t8|ߨ,MVQI )89Sq ~yAyKE4N|(/%V"H$6-$͇oBV5sydЭP]vf'Wrٿn#0=Z12/6Gc#B*DG̜Ae{Z]‰(J((*H~:mfL_죘S7*)i3oT+JDG3֨ǺH']F,DDT 89. ܮXQ96/ ^ĵn 3(lcX[9%GX{|r(Ł*rQ|:!3͏ì]ɫx^={ Qٹ[NsjRFzL6mb}Ŋ)D"]9 d J,rva)5޿">dS8;Jž+i6p'=FZ3zP(%ft>@;Ĉo쯃Q8P64H@YiCvb+NFIz"\j(Z4W}<N.vtKDZVFΦ;-ɹĪ@7׷QC&[ZK>w 7PTWXKkzE(/Hd5.hjQm\ K2X?]m!_%o-a&N|k:Ł䃌9PڊuQQ6W^\H;+= BQ䁜S[= qES@Yv.qbW+ݯ8`Ϣ,.Hq>_6$y4}Y xFt3GXڝܣ8P|9L=^~NI"=7hئ'E^~zLs'^s3 96ݿHC+ϼg4GWd^Ǵjf&"7̼?xřѬ3KViPu_hG=+n_|wnbC:;<N.-Vb'a1;R{զI$DPtc@l+04 -/Lq*ĝ<~򦃧2eof"rF8`sEY,KFPeeU-ʼJ萶iAQ&rL|rCc}FN#TUNNZ!7NW=s̚3`9G)P(+c_BEk_8uQ%V CYa ?-8CQ^8n$A]3"h\9Pzc~Gq@9IlЛRӾ 5wNn 1gNMd*zߦ*zxH%_c75ԣ5'(J:AO=0r4eAjZs:}i>~ k!+ƑϝvÝ@e}V ;8axrkݟJ|JIYs!gfdLqo>M̍ JpmIyWUIg7ˏK Y_1s`{=3Ρ eYBtp`;I?Ź#]XSed68ڙ2j' ;3{[lm+Zq83zeid E^4_ka;; yGqJY&m=Y&y۹0\%Qcv1yz8'2ɷY[ߚEXHOq f́)R|if<[WR杨듗ajgWn,eyms:Oz,>qj  {h^O-1Jk:<TqR|so:fIKtVG\HM:fU?i5~ŁUß 200Bu@#toc:ǐp=t]"+ (ޚy~ƨ`NA&{Zt]XmO@>yBXެY0*.JSZrXJ$D+:F44ak i_By5^M_?gn K(Q,}bzQIM M ? i˞XJJDcL4#g6#{ޤ| KD^V\O`;cqಬ$;v'=|z8N洳]ˮzn'@[z%[c5y)) n4e5ч_GQXYr3E ߞ*hsLbB)Gn_eߊcӷ#L5߬3D] 5e+Ly~8@MB;ỡ̄~~dȇk,\M߱VI[ػ4a!d鬊rRlMU/IXl<[{mmXNnL;y̢ ]\\]3i3̚NTͶVrpGyabq0G_{8FjU43<5c#Asٿ8v°f7 b\EU~>|U Z rc%u*R7,( 9#A/55ȋ\AO=k:zrnq{}=f|5.wE[H9$]+"wDKauw_v5-E88C>*[Tw5|h5Pح#WBCy6F#న_)@aQkܝh?iEuS"l7 W5m l Sڻ^|K.T4&Ԣaw^ )픤CA2-0ݗNfLNɌ5!8:yl8JN/pюigAe1iBcflWX񼊋qvFMi5AYd[E8Q;,KVW8ЈIBkO; P Y5R;]}=y;u9Eǫ'cL?:4|587Pl7}@ygf^jO|T:ӻΓ/>@qNDc9tzoNkUpKhF]EWőx"{ ~ ~6q ' ;>i՟/CSt}.3~{@[s|ђٵ mOw:TUsm9_=x晴Qz]G"H$&|nf8k7ía3i{7`oFcFyo&Nu(ǡK¹R7,:_4ݩy6G%-FybX)ci;Ƌ1vKJ)V,8iFQΌ^7};$ 5bBɇ?Fuj;̕;o{{h.+tYoN XXO/F*36S4mFEevŊ{(iˈ8\q|mE=bns3yRF|oYW%7yD}z[G7Npb FGKm7D{GEow".p.2?t "2wQ jqUtq"V+}e14Vlk5;Uܥ8p~[(k|Ӹjf_xGݓ5k%1'E:o=C Q́;l _:HS4[r)3N=u5'~Gq`c{(?ϴMŻ51Uk`%3o!6EOMCY=%)u%ia/q#D"<ʔdų{ƙz8W:LpLw%;TUrKddW:u4օPmx:}=h&o)y DĠ;j%^tO Du*3] g3d蠹a0(Wv9; O"[ZY'qeNo]uV(!9w]m?G/S9QM9f8ybgWʱ5eJq.t6})D/a. 8et WF[2ҢCq|o+D\h3wƓ^NOŶz)mLvRq 7̢r6]Q95ӇqZ+r0mh9;]ʋ 8:ߙ,WZm@Y9#Y>#δ<50J7O#m* MطIn|o`⨪u4JϯdOqB:fٚk]5黉:F`37sz=d-NnڃNc"FK/~0 d?؎,ٔ٩8=gkF1.:gy/9r ,lN#없hiRQuSgz-pцřũ7zsh91E,2NnŇ'~+_=܈J6owODf_* 3ky޼9 ߥ]b6kKF'pғOUpNgŏqrNN;]9HM)Ѭ7a :.dyhyؼd]]7;"3gvbb^?3?1JsO7&{m37 z}z[~"/axwE{)Ә"kyU@ {/zi;>k['EA.důs6C""V{|v&To‚$.QE=qt'=+4Q2-0#31W&_ȊSרfNsE[Nڏqcr֟I5Ψ;3SagE9u5t!bɊ=?6FUt>L^zF=8:jJapw'o7W뎮*&h6(Y$Oz׵)M bÔugi/TnD*_&@|wrk>sU[1AUi%/Q#u^-Roo⼧^KQcu9Mp+iKŁgߡm>Eh"uƭ>_1!=E^^7+)Hq ;47M7:0Be:E@W{%D".J/U1A'7;;kO?\j}K5$QIZ^wI -Sy&( W`7&H6ݚ EDZ@9EHg_E4jowQyxY8ςqyFF=.tVFX95*"kZ;Rsh%᥄шuNw +:gT%r).t^^+yqŁ,|@@@z)"H]3OYo@wK!/~E)Hsq?Lvå'M0YQܔD"H$?+36&fGn^._<Ju eEd 915Au54quli.g搝OnN6)spĥHpT"C8{v.gSaw23na!1xbwhBH$Huq@~CEIQ>tx>hD{#Ł@i4 Z⾗ujN8ŏctz,j땤n-ZTcB|؟CTd6 ḿN^\c@ʁiOkŁwVH7 )UGYV!H }?8V;]>]OP妓嫩ݦy7@Q?-WǬ33:<ҍw٥Ϸ6`åc$D"k9GZDWzo:# h?dn'd'<̔6z&45 J Myj ˂=VϚ|1Xz%wݣ&zXGF 3tfCKw|̍g/H0]8pKg@)Ch6ĔfFY~o#[aIȪI [:E5rC0t7iuwy7R@ gpd.?MCxy~4]NUJݢ4ʺ EHд!9qK4T֭x{Rj^*CrdL7Qcg32*rSQ/NaѶ٣J w͗[H@>QůfY2zE,‚utNJ8V쳮x쌦!`Jrbc<m̗R5#;ˍg"⧦8˂ :_2fe*ԫ(M?:O]>WQ98{އFgG}Z_̥3̲5|a9J ݡl3wR}_1vwB}cP>χ$ʳO6HCYѝT}D H懚p1O<4PE<;'W"H$D"H$D"}AO3a2@hp"=? x[q#xm5FP<8s_h)x4\1ӆOSׅԻJA&]~6 sG Oi,I[)I|i3 8gS϶|3F".3O+<]9&jqgwLvu??ij|cMI::/~BK<:xH$D"H$D"H$8VclV_LF(aw< vM]E4{ r{vcvz1W.h}'^s}~;J|pe{: %0:O^XXtwpKs70q%V2;eVBw杸9CAn-3Oߺ/hY{X>潊B5T؀/c{X7Nges=6E$^Q?_l_)4f"vgSF:>'5imںiFsPCqXE#EPk0/t{9gŋn@o{q>;5㜲$hftֱ8D"H$D"H$D"yHq>QJ\upk׬aժU_c^_lDU))gwb*V^]j:^gCsP9Vj: DkRFNNmzU4Wrv{FUCٺb%+V,g(PFg1׋γo7FS"H$D"H$D"Hq>xPGɝHdZG)U^b5{ VO4&P;ȒD"H$D"H$D"}e%PPp[1e])?q@EYIE%8͊ 3u$D"H$D"H$?)'W?ty-ȧ5'%js;"4$Vq`f|_/jUƵp0u '$H$߇BCX/ DreGX{)ž~HB~V/jOGb!'i*(<|%1RO0pbr5 9ك~ /s#MC0ZE:'R^A_fHdXGL)H(&H$D"y$Pnܘ//fD"[ o=ǛH[n g.l,` %Um|U\1mɚ=D{QLBnQiʋI?476`fD"FD"H$䡠RޚV'H8X8d,d9(SI`8k4Bl(fˎK=D{f|=d)g2 Ù 4;%6RH$D"H$)H$ 9qr4{<$ȑ;#H$,yiH$;#D"H$$st3 7'dU ҕ;OJ/D<߂^HOݳEq_{Gg\Ƽ]t}Z %;Xu,P%%ŞdɪM,]\.Q_A)NaIpEp+s|Vx'qlouVfm;ETJGmJK<Ϧ\ %"ma\HXQ'9mh2%9px<.u}YelGyimc({T],9,̾=:t'p= H`x:|ݠ2Yɰ64܍l@f UiW`sl` e9\ ;ȼmٌ}=D*/f!_ͦ< [){qG-D )/&1\^ choo8Rdg68T 2g=W'wK9fn %.8Φ5pm{vhAI:eV{(Lj8wˏ&NlAf-m;Xz2c$Ti - 'r [eKBQ؂˂Ϯשٺ(~5krJ ܹq+v6 .IHq@"H$D"y蔓˲3e981̉cqfDs=okk*YAn b-Gn.xYq5MhBW]#04}ҬdVr\0#{tMa |bi0KW~aFˡv Yٴ\qrm3o &Yk+ikK5l|ˡP8 :G '1؉XjV%r+HIOʙXa}W#6CH"y(Y8pkgzjI*]B˪EWo4#Wޣ"7 t4ɑftp7LV4ohKG;z w]`#L8;]Y|7JUe3&Yq=Uv +fg833̅G~B .O_+zFLte\fc3Mi;ČOIݔs_w"`-:U#;:[m[ެf7r.Eh?=D=:I'z%5Kk]h1c*Nh\劅߶Si7؊:Ɋ6c>BkQ9yY8^Z5+8qܹ 1lOc7~gBkFjM96;/~vIׁ<еeѢ<m>Z)>SuMs0x bіݦ.jM:N_{hdHE%{FD"H$C$'ms᪓>w ?t)^JjVƏ1Nf]9tuӉA΢g_p|>FtFqb/_&<4E [iM6'~qZҐǞ ݋=Mtu_D(N'pl̰T(<΁p^{c.wgT$"%XbgO3}vGv qR!0bMqKz)(HSǥA|5Ȃ_-X^dߦU1vB+j*˦v/N}+mv k{:NæܪA }<šoD"4 L60d m׳>4NLaJ%ؕ#1Z}=tR\:qK#oVYK+z,eH"p6PkH24kWgc+l\a'DJ:2CH 0kk#?K\%""C'ryR4Hv6ESa{6Ɉ9lK D"H$DP)&^&[3`a8ڃtaj4zg̢0[L6fst uje bgOטQjuJKBi4#a8mף}q9WA:fѬGT E|x=Dr{ gqzy=2`iͽ"t7;0LZ%s#tx;b+jC%7#2j#cUX/pItz\LΝ?(IQ<hsUu-F89紃{r& p`Hr]97ְYA-2aTDJd$T)Sx [vԈQz=O sɊaS#I|︑}!r [DN҈y\7oIZuTN!8M#jkRzq;X0hY +8QϘ!kk|oц3X+)H8)x$Sy3j7mtPi 77^YC"ϗ'j= G-pjF0frΫȹv'}s:nL/v /k X:M,;b;'vk[bΠ@GE䯈.+Vӓ+I\J$D"yfrfg09?8{#&S-he}zYYNd^ j/IJ8~5$ΰg6]i>c~EXeHklޥ!4r:!@*>1c1M8`EE~n̍uiAq(*-q F8V|= 8|B9Ǝ-8ʌ9'.^~ [Z `Zf?K7j+<Gg@CC\ɭU8$SZL5"> 3yq$J0LGVZ;v>:SW(Rg%E47JVܑ[@ fS4GhQA؆ D0)|0Xj]R.lYH\@"Qf#<;T-~ ,17,U6Vӹ&v_MTMNpZLN+j\Fo2+*ںGY.M[E\;7),"b{ 97Ю L1!-HR %yzp{kPN?"W=sIUq2Űqz taxEyh$L?aI z w B.RQ?tty1OMdk8 H$䡡&zBtN-ٰf>vùT+W1dkB0WtGy,M_A]i1ؓdWoGfF5g(*NVݣ8|&]84j9OfP@q>h(Nʀ8/+'Q7Leb HL8 _Z `QM\hR8 qhA8,?o=+T8WIuq@ Q~V&t 8ٚ˰\tK9J^TS_82r"WaW\8@[D yFq)kHq@"oJ+p&.jVt81quherb I‰U^#hmcA7c:g o>{a^8֮ÆagXIӱKu"‚<˯hsKR-@1773#jEٱȇOG,H,ɣp 1=ā4 >@58 H$QNf|(c&JT܎9BI4KamfBGǭH)$7?;ڍǮBqE,;rk3BdE3g\ԝ)1p.Wkryi;7JNZ‪s=wu:Y]K.LX{AˈQ :j,BvtEj9H%G:Ł2"zknjs2v΢U?:އ8l w\E9sNԸ,5vM\Fuq@Eqy'h TPF-NF"kKq@"-( >gkP$"h5y. *smXCںn%䦶=`:j ,_8Ssj+/N8S丝ke*rB=9X(em13z ) + Һh#9GB3тoWidC RNT1„X_ݎFb7sL$:R=Ł _F=D"H$Ttzw- 5͢mlٹY36̔v/XVL+fAZ (!~iE;\U*#etZ {xYȯl>#軥btރfSߊ#mj.36b%tYKL]!WU-q@t YM>V=lڼ?/JdV 8חXu }f2Ȕǹ+nG:;z%=f4l`-x;X[}=a(Rp(4 ص%0lT?uHsLaLYGUPD I YίMi3%9ذcA3i`E1 RH~ y\i?XwV^%M:*l8AnƷSp_M?Cr5ǮTMH/LXl!&وzϜ|QEYDXL&a+C[t5廩+i)‏톙h ^͂Y<®}3+(<+ udܼlXdU zr=Cͤvivf ( h+ɣUxb 9° OqƘ:n"4nL}*GD&N ƘLͦᵌ*0qFGһ'<ëahc ')ۄ 34eGE3ARȾL6///6Cۙ>MɿqS0Yݒ".fV"6]Ku+"<{{k N9Y|%}-j7^<4/գ”\bFڳ->O4U\8|W7ƎxjvRRq$x<'ZJؚ֕1n8]z fUqiz#Vu` &4HSգq5c٢Q+E!6~ptqC=MY"H$G e{ >tԝJ|3Bt`&T|'+$d\2K"R^Fƌn ݺf4bA ]|" όNrFZT.ۊߋNS9e'8t(0 V/dXKZc;Lu)2#Gq$-u?eğ܅OvĚNks~4x*rSpeL'T[΍oU}^E"+"Ł4|A',nO~D="_r{U0Q wܛ8PvyL;='y#:Jgn5cSiw퉗A1KyZa~Y9ilԣ{xyO<ǫ6 l]X5^}J-h|Ҭb5ߢJ9n҄}54~\q _w:GDM\gdBq>NJi6a~6{_oR=wriQH.R~՝qD}wGJ!BD"eΤU}jڑJ=S&J]lg(V4?K|3-U6օRPsvն &kn MpF.†(i^ݡ>.[-pM=BMUvI![W-t2!-/"ySDY֎:4x>{IzTx6;ҳjg38tq f yBq-΋za<8a䛉D S8JHhCw2Ӱ3ω$TX!!qqLUVC]z8'OrRi{q:Pct>/$#C1K(,fR ٽ'^wޠq?[:LԜ4lȜEHq}\HsjSRӒY L\SCOZS BtILH$DR)26]~%D"P%xprS)n UP$Ƀw63H zwo|n;#ِRbG.$jFhUۃV~=uLŁYz|8ʊk\SE&GH3G͙:BHզ=YQk|Y)̨6<% +Q=L.'M>3էz3Jv_Z?8:ϽʊŁ73jN:}ʶ47 ooh7ZNQuyA4Z1Łq%M5+\a&~{RTSā>>/ۂ O?hY̌{DԂ:7h7ZjyQO=Zxٯ3_*2t~e퍚/]>?~CjD".ƕH$DH}Gbyx@k9)[̾R{:S1lzvvrMJ>n`BBf]= Rzi簓Z87@=aD`)K#z(sgo)ǼJ_fqǰ(W F,q ϣK|W-q;sbo~Gu7ՏIEQ}o;3J1Ͽߐڱ42n2k}U3fʌ=Zn^~s5Ug>_+c=[?47UKnLPfBK %@ 굧>D"Hl4}|Y~QD"i(#~t>7QXWV9$a!O+:t&NO%^ޥ6uw@IB~mv^Lv"M퉪'ƴjnYFs#UYܥ6RvpZ"z B-wCJ-\w9d$ U͹׸ϡK|X8 q-7;:mlw~:*y}ϾY6e; Y[Јjkhws[@Oq?e:2l":  u1pQi_A q/ϽӪMX(ax[ ;ZS2I pV>ܩ,D"H%Ԏ.H$DEyY E%rkQNiq1ťT.H8RO,dE/agivā=.tVv}W35{k9, Gt[?\PȎ߇ExWxVqujb֝FUv1nOeĝZט4yeD)l47GX5LYkUNL}:vyI ŵ{΁QGL(^G_|-.(؟Є*Of{uXg lH$D"H$D"H*Hq>8Ǽߩg|4z ƾ;q i q,?[R¦sA͍7kTr K|< z}zl"U~nhKzs\b{Fk?ێ+޽}QxJ|"ѐԧG`K\-6s :&^3,ncavXp qܜXqD"H$D"H$D"]}rGq C>GG1>JfwmN(>9_ctَzF'0orGZkT9zޜād}`zyS47D#qJYWn1$Tϓo}q$kff/q/}뫯&i`x2ߡi%Y#H$D"H$D"HROān$dv&__Xtk\/yh)yK]-ۋes}XzֲgՋ.uS[8>UE:0F^K.ijsE]?P{뽊k:aʳQc D"H$D"H$BZxM&c8x-AE\L@ZZ-'#g+%7?.i8NV?Q5L[\h8!֓PV)-fW7Dp> vvS99{_?ـ^S5KKJΖ,ɽU^:u[ PJK E wwx{&d2Yo}$%̝73W9f(a.sCA쎒c;P!4ZvI3zB3 Ij:2H>؍\_1 P|Ա vcBq8DzGqÁM7gl5l8p7>`{w@m>u=sY9UCkEs?|į>;l.q^-"""""""""dÁ/ø?W+^|'w.Ax;J t#i탸pW⪫/G}r\yʿηpDs 6a߫7Ἇ۞K|!|54c]Y|?RW\~9.zܿ9yam3ƸO|?qEW+yU5Yg}7AӻvvU{__dq܏ y~Kq%Ətag5[ߧ?v .W].-  >_1Ч+sk.,~v2DUTcAuyǝ~W}q^k|s29w޹( >>q/>ɆK/%\yUW?9!bqG]x"""""""""r(8A;yd;/'^MC^gϻ"˩o>ݸݽ ڛӯ۞a/½"UhHOe2vL.9a<\p筆4'ށ?_y|%_eEAm-Ǻ ?ޑ {z"VǕ'AOSֿv~uyϞ;_]jDO=Ɲs.~>:s>?W߈ґדXEӞ>5_2QcVs;֍t6`?cܶ>yxSq?ޱ=%5߄!]H8_26c !w0 iKEDDDDDDDDR8p7$|4o!Qp8pOqǯ7s1.oZ5 """"""""r(8AÁk*w8Oqc9_z0/Y>q!rpv80|"($!x FpPj{]FF P0DXWp>2n_Aɀpm 8tJNf j:˰8*;ďq8?E^p܊-șp5cԚ04O3Q0g/_؄,¡Nl|OW@Wg /ł(P .@ဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c4T@;6ww U;oN]>twmAN.ԋҘ9a/r[K~נ@oywm@jt`_H6ZzWUh=6nO)C6Yi{' AHCsSs柶9 NPw \cܸq>#\W=;և cß:;r,1x[uVyB_o0֟ps/cp4reH~YbHbW?8[עMP2\߹_s!λ4}C9H8ûu"o9瞦91 NPO\øq?M/O[3g`ƌ0}_YŘ螇O"GU8wsfMGmoY"أVGȋ?!qܯoŎosIl??Is1{t̘9 W}p{"͸wlAB{&Vyb0f|Q|KQzTi_y6q8s0.8o8/'t'Ops*N :\ %x{E Mq>£k  ^xt|Xт.579H8_?Gεa~t7_o^8chD6>/}qؕa 0bs)ULyn@;s?gZUwq{0x"2jL80XS?O~|s!yj.1~G?~{\osoDrS󊣔aW%\^T4m|/9Nk8 p*oGW#p~:p;_ܳ™8q٭<,ߘps`q:_/wJwᆯ~ y䠽}KGh;nop`|99ϧ8鐾n/~+G't~Mm/Dž9?O}s))8_9ugzkE/G8+(u͍hG>o9~K#6؁72vj½?=_\P78EĖO~^v!'~69O_%CC/|y buA[m3ɍ\^nN _=[qC!Gv=XU17xqGW zsV#`9vϼ""""""""""g)ֳ8:k#1,O܎-~s{{ 6mHf{Ο{|?l]i;(Wu>{xxW!vU|ï0{#na;޹p91qG~*̙,4˸qA8onjH,""""""""")z1p.Ÿ7qO.]~.q=#"~~U}q ˎQ;3>oG=p7ј w~c{Qvpl|ϯ!\Y,G8P2q?O]q ᚷ]Q탻/"})`Z@p{5W=2DđplHP{kHGESq6Іqxk 4~}fn×p79p/>:|v\tT8,ͨ}W8OqC^8ij~1s ܲ<\Ҏi#{s}֕N_Iܾ2CwC*CMޫև![{+>aGz?a9p^=\v:OSx 8SDDDDDDDDDtS8p/@S2];|;QvT  >3>w-f!1% Gm /c|d58 p s '?q看Jp2(9>;G^0?6e<{G')H\_iWqCv$n~6C Y8/tT 0ȅ x~YHx'oG&s~))"""""""""r(8A`nq߸[R=p"@vx-Y,BZ8ābMfݯcmhŢo~!|Yq "lyAL1=KNj8P_K%a^*bJ*~0G"fSv!>g_X_=Ԁx3q9q_] JfyǦ!}!DDDDDDDDDDF''z.!wFc ޼Q7t*^w9}և|۷s>1-qCExOZnQ2:؟~0L7dw%`!6>sOeQ}I_ tئ}/~oG9C!o <۱>?{J׽~:|,8OL?I;2F|T\=t靃cF 9wr/;,󜿝缡șp&m}]p-.8S̅/!l}?j#g/}_ַo~|o|鯱 qt7<}GDže gqރQُ7&u}O0g^{[Ӱ𱏞6'yX_sriÁ~Ԅz9Ο 주5گ~?el/p^A!id<ǟG_:5+;W-Jv _/ /~h<քb?z9}!V+󜉻&?<9>^|9EDDDDDDDDD$"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDDDDDDDdQ8 """"""""""2(c1 DDDDDDDDDDD"""""""""""c1Fဈp@DDDDd;ti;}ok|^ץ7Mk 1jjjmNʖ ;ijZ[[k6-++ ]]]w~TUUٿ7߈cڴOY91 DDDDDư6a EHHOvO"""zjkk|hv_̛7%%%wF߿{AXX1ڴn;{nttt8ira---Giiǽ4u%%%M]Sdd$UFN*___9Wpjkk ~eHDw~HlOOѳX$!!d)a`p<U놾u(hjjr((Kb ENp`x'&&"//!::vULL ***m/sy]gg=p ~t }hmCKO Zhv7l=W[mo͝oߚc=rMလ \85vV?js͛' p`s`v7l؀ufoˆn+]]DG{n]}8zÖ-[l-a~8c6`Iooo4K 9 HMF\E²v"(u-7! {sv\˳mO6N؈k6bbC:l`/ߗS#u3X ۑ"_;#bq.L> /ڄn4X4A@Bsk lv:VxV,޾vߏ?ξ~4Ktkޚ E9@[@#@ȿ?~ پ.364q> Oဈa o>;Ǝ,%{ֲgWWW bY!H)wYޢ pEWpE{  ':'O+ ֯_o XHdž8PpMq@ a#P[]mEx^vWbKLܓ4# m9 \|ؙo`lcas zԿܱP8>(@* u}?77<7 > U {>P+ /R|/çp@DDDDp|:?=XaX|9?>?+7,qp|>u [ X UD,%@7bSt Ɣ  kxXN8k+=xl\%`86fcP00ipu)p!C+:ͿU(d1C#`M/+I/u@sUxp`a 8𛙙f9;0i1jt7{#ZY!ͭCwۃ3Ik_};G@EϭK΢3 kÐC6cXӏX%86{ {ZR801 k5td@@m]Eg7@[1N=çp@DDDDd ;+N6084,I^S]}H"Վ \ 8f@C lؤqo9`9~{XZq4]4 ׷=h~-(p] FFc 1 诎`ǧ=_qQɯ9 }@S*xCp`a FVsCd/Hơ^XQN#A,)kgB\Y@eұ,@^^֭[ݻw6,͵aArr2k޴igoAEVVٱNBf8/u(mNJ$sp}p@][f,68Âzs9~Gs^W.s Aԕ2gN[:h<6֣14mQm31/m@A]*k݆#sŶmې.TTT={`Νî]pAtvv{_i2.5@bi桡F"e Wf2շ`5K;Pl~ys"0c?&rs}43ۻ̹9dKԶ!2^ipO*_F%Z9y-}ow gh EppEH W 8{tA=آzTX}su)8Z*G`ˁ=_v| Pz|S8 """"b8[~,g~_VVvx@h>>W1Gii 6twDz(-4':/5b[L)띗l \|9_؁f8JphDic'@qs zۺ?v!64镅XRcx8H΀ yL+ǞR Î"֡{{6lfkv(<vK'ֵ! ˱)ׄF2 hBKW Q8,ԘdL}q.K Yd>p`[3~ g_gs`uC?\h8D(8ʎ*L =e 0;ۖ`  8H́ܚV$6!oA]0VtLscּ 64h4,3h]rp5YV( AH.2 +, Łr;++j[y DqE쌖=\|JQRߎ,چ``[t1F۠ ن&\]\2_GR8p [;v| ]@ycעp`g_~`" Fbb"222B;Ǡ=DP8pl˶qpwB9bϣ=1tX^ތz,\քF`J̊;Ӯ*`!9v|6y\"|3*? ^X-QXg|,Ax^9~cﲍYZf|-|\0PAXVR+oz%P\X=¾l\K G!T =H-G8p$jNR.|Y &`!2su)>""""".+Lb3 ꁡU BCCOx<`V^E/GoFL=9gOk׮"D׻. [ߌ*WbglES:r&y_mg7vTm(`ayc;s0e p<бz U͎gc掞>vCnk"\ T* |,lpYQEήFQm 3L`/ ,#lWp {544`ĉXb>C2LU@?xHb.gS"sl OဈAjj3zg^Ē@GX8E| q Abooo;xAEo7ph3f/klڔJf=(pwaR'g#33]sVE+k?Pʮ8T%]˨B\QmXz5,cW?4ܣ>3ogs6<&핁v&bw-1?by,8}Rǖ=ʌ % ʪ6? l 3[a˭ns@a}9RρYyyypss֭[o;.Á3 oJa <[4ĺl٠E)q1}Y }=}mo/VĚ5V[Z{9px;[9[pƌv "W.p@uw#$᱋vE.r, \W Qlrr v; Ga};KUc9kevŕbwcМKs,z3,l{ bFpl|$ s@4:{g#aJ /C o#cD2eSD`97fe k9`ޅn[Zh6 ~UvDLܟXf;[ QZD!i-X% FndN*hkk؋8 --mKqm!n_|(_B;V)>"""""c?75"1﹥ԥk EGG pf!e˰m6[V@+ޭ 'd?k#X&`tQ8:o/ ^c5$ qg_-Ab2β,U,<;(l+Iev@QjW5w2;%(ovF3&G /g/ ,LTt<-/L=#0`=v%+Crל+#H,iDjy3 ; {LT?6b0=/H[2V9.'#ҼOX~)`nߣ\1Ƞ~8eCԐ84E/9V =Tz]U椏 S8 """"2C=?ȳM c{!;Gav?sPA eHA,qop0'z< נ+C mYFk{g8Argx̪֜!D4Ǯ ɷf{e⍽ɘ5av`<4~(lrC 8p̰î$io~s=D_<˱.\,+ OmË;{u$i9P7Φg0sZWz4t`䭘•\5ЊXy$W`kt]` xtc,1S[e)`ȼDa_b-=ŒSх X \܀Mm(PXEph>8 1/k qk3z HUWu{ ;Y݆^(pi.bgU"n %㝉v$0`m;`:&O{ҮD[v3YvȆ%v94^f,;zum=F"4&iMpO,_d<5ֆw+"pp5n44[V=!`):o9ޑ;Cyy]XR {ևbW6>fTz<ݼg%lP3Ć*<%ݴrE);@W}jR ރS8 """"2ksg*!ǺXx9CoWZG{?\J絣6mFvU^gg?mg:dR;]Inɘi.L%23ṵ[bgֳ?4Pη6{ tr6Z^V`/ OlΜCfc8p˲pgyn]f~n)6(`ߡE#_v2)3= Ѹm+lHpϚ(<}mǔsVnKJm.bL[ \0hcߒ?S PPP80| DDDDDưpXOއV]iK1fz$nDE f䠽{ -2* YYh}8]؁f;? /LoLL;u_ٕv$ͱxz[7`w w/Oz%k@\;*jvfR}{zm#8ܷ.Oـ 6"}e8.NMKCqϚHܻ6snx/ ̵!Yj018q:{Ub_/P઀g% 1MKBק&qvkgMB|}ӫ-(.)u>4y?\TzF >çp@DDDDd ;: .K+^u0q诼1E }WB \[Td2oKwݖbyOn=#J9^\lL2ױkm6ؙ/LhXZ'(ƖM<,۸e{8A=g 1y*cp(ܱ*X+_KBI>kM4wM]e0ޜ#(նQT4;ZbbHw ڐX`sYPm>|hܻ: ^bFxz0ca0-W/cyp>ܦpE)8J7Px@:Gb5>. Oဈ РGxyW"]Ea{ fcځ4g͈ef"<-%,3-iv lsT~(g͝}/nk{hܼ,Ԗa)΃cp0\4ߖyyWM8Lٟfc>xqWmڛR҈<# Y7w8VL / exUbv n[Kfq3`I) 3cg&zk "' ptܰ8e7?=/NDsZဋTx;>] GL)q1Xb XXf ҰtR!++޶111 F6_-Fre7ֆ96HBWO_)XUɶq0<=/N-"pP<9lsp ;Hds$; LLܗb 5rۨ3cKm*,Xܸ8%3p03&uQxxC n1gT=.S؇3_7ÜׂڶSN`-ȮnEpv vŕ`ES}?=-_Mo^Wv'ےCL?Uqr{ƼGƛs̜(RY!4fjG| Țc~9o áp`8x 80[]] oooL2WFUU"##lٲ{˙݀a"c"ntU8ڸr #+ -LpVmf^6ע`sQ(/L}qܸ$̆,b[X۸+ 8~tۼ z3*Zl3MvGq_OSib!16 )"m~xqGy GF@gxDlܻ. Ù?8wrJ|׆:sp\;/~Ԗx7(ݷ6`^{}䞆yZ9d80Tlx%ВRNçp@DDDDŰ|~xvǎطoc7IJBqqqZ90TvT%r^ \[Td$2QPۊ:H.NJ\;{Y,U!xuO5nC CuQDHiD kɆGrK{=*;.3`Kgq`Wus̹ʁ'bI`?pH{91ǹmE9ALs]PV5*~L B QXߌ*cb_66%3p@ܺ,ךcg`:`wO;WG9-1/OXbpҧ;> 21`y07dR80| DDDDD\ cvGCd[F_ @1g?1gy1#ͮxfk^ޙDexaGϐateYgb6M(jDQ].;fI Ǔ[d3=8mn.aKq+?+"l0h' l ue܎x[B?+#ó;9V+\Ԙ t9#h3WN) k0iWmt:)pmQQJ_z6Ea[t1ؙ,'t@\1+, ĺs}213/t ?3lX|p\ w£cl[ ^jߛyIDJY-s&t 啁w$9Ӽ"\13gȳ|γ[1;/J|Q3YAxjk9 ] ~bI`.H9Cg +Brj쪁v5_cÅ|!vS[l0# +B6lKK{Tp=pPp%p(sEy?lP0p(>"""""c@Vu"^Zsfzf;/6{b)2_E9vf招cJ֟es&D[Jxm f^0nZE޵qqB`lL/ĺӍKy΂@+#q`jWWs0+˂r9 <<9XcK n-qp.g{3p_F:N)>! 5"6Ih^w?OoⲙkuߞhC3<31lL۟_MٟfW0@aY?2}V(kI 7:z?S8 """"2)]W>N祣%{C64# <(֖uYs??Xi<,̰ykD>E6@ϊHc!#lc0 ήAVU3Nܦ^5+q0\=;?NMKBjVl.YlKO%pKk{lفh.ؙ\RڈށV^UV quKKlxܼl:} ?qhW}0xn{}-'O2>Y"s_}$(QrhnBဋrp0[]|(#4^N%çp@DDDDd S802 3/3T)S8bb1sG Vbq@ؿ]$w}pÒ0<3"(זY 2m?Wv% d;r:|0`ϗbֲ|9؟\:[t@UK1' Olfπ`ܼ$Μ突+g6fWEp7yuX8 u<5Ά%o&ypl<6 msk؅}̿޾A4s]Raa{L-Ux/zk&ˎJavŕ`mh}q/5Xc^~9xnFwtt1 ΟeC\24nwDt r)>"""""?xwvvۭ?/#CGo6Ư`%1±j-q5~x?eٶB?|2D o鷃R@ :QڅܚVgVא Y~5d5Qi6/˂rl%+)XjhCd!ė[P\p`4A Dff P}}=BBB\'݁n_Ҧ=u+tQ80| DDDDDF!ȋPXXh?/#C]G-fOD]̨T8XVh{6G۾ D9$ogEZcKʐ<;CM9%s<ʰc lRD/N%3p4\94c {t5&,> ,υ~A4x4q\cZVF$5"0 1w%<kyiC'6Ǜ/{⏓mcb0q'7ݶ1Ņ |uRJ5-H,i[| F]aW5p|qyXNQ6aؼ{JSj)A_g# 3[9mU80JCrr2"##|Ml2#//}go;y(XhF,S8 """"2 q~YY^9x yP8,Shܿ. 7, ƥ3wmm˕\ <ѰxmX2*_}eaYb{fxe^b/Z!kuy,[چ*:mS y;{mIl,yvf{bZ9잊v@= ki}; Z(nW0@]l)lҳ͹jOn.3oA9w[:?p˗lwm U M(k@9xz[nY9ܴ4Ԇںu]A043=_~ T4 zWp`b,)`>p0ee}t "g^gօM'O3pup0W'544=77٭gM O['}& ͷ*XҀb[t~YwSdև;4gEoLuzuw Xf' vN;z݇vxVY į{qsY=G7cȬFVe9f5vq ˨3gە,)# *$Ϯ` p\/+A^mo3tWc0ꁖ^i_b{%<9 Mݼ4ZJ6fk^o>,Yقd}lQ=%aiP% berrrj{%""btR^w4c/9c Qau}9OggqЈY9` }XmxR Ɯ S(ikbn[K]!۶mÁj%n{voM]9p`\;/]+#lv$Sl0ȭnANU+Kmz"A€[Zfq=߻6 2,sH;BrjX9m!ۨdc~xsh\5'q[9J m#jn7EP؉v2dUaw|]Q |=5vƥ37+<,Ğ l{ U]v`olvlmݨiBZy=ϋrcA0.2=_wzls-U">b \ڄ*9^ތ6OMgZܒ0e6(vI:(bv| Cyi Oဈ(WʲVK]֮(6oތ4[Bh޽68?A`yyr:7t`0{K]0%W(4%Y({I.k㎕x|s,ߑIiigs8Τg(Pe۹z3,c|l/ZumhqH\4e_0)ATA`~CvDn:h쵳ccG ؈7 q竣{7nԵש 6Ęh^G7Ђb(bK*=#<|A-(1ϕ2s{ qEϪM{PRp I vCHp`Bpݿ?oߎ={.)D,+ ;T+rss儈%B+983wct m(v^ RiH|L4FT_[^ڕh{L;nK q;pP׶vUuWp|yP]=r3 sl~_L3J6'f=}0w`оz߳<[}hC_g)exiw]qLc^Yi3΄gqѩh%{Xί_F$ۯPg?`>g_/~AjA5O_zohFk_ẕ3t~2j\dܺ< ߙi~ -B(LrO=ظخ17_~t灁ALay/ӮL`PRނ[Qt;2_6?9(>"""""`8cǎq!^ي(݀oC@נp1o^c^ ~37.g LؗYޙ^[F( U-]vн˯heg{lЪ|s`O]p(;3!&ypxtcmj=o豍tpxށ-@Mk-P܀:l(s8s؈ mF<~G#wa};zT37u:Y5fFJi#}+'HÖ+g7=S}pۊp+6ofY,-tq/ޗ83Љ G3e7}Ԗ8un8⍋RG ӱ'6!q_{pZl0_Tdۃ6z檂Uiv&5:V DZ10")>"""""gs5==QQQvUѫ>(ggCln |_ܐ0pF"xYxbs۞gmvx_&_iksp9k<yu6`bBLL _W9O[ZLݟfCsjl>;R 8h7pΚECG- ܿ6`S7=W_\1wn 88_f/9V20tu̯7΁HO+k+sx;šdoUo\H9ZX m2kܧqŘ螊{VG>DOn in_F]k͕\-ך`K U&͹m((U~LLb'bT=@@F QyS8pf%cst)&lL[u(pm1zV;h} LpO#b0q_ !0N3`Nl.T4wt5ڕg&XkgM-_)>y׹͓["8S+@~ES *8(tX_^6r %uj~]"xLHGX^-2*`<߻z%vuz % vv~Qco.3lr([+]jh}SG鞖>{ǃ1F|2_w`X^uS𛉞T_ܴ4/;Ok΁Cc{Wjsnlp`qqqv'"08&v~QBF"çp@DDDDdr~wb~M㗃N'o}w":yP8bcǗ6EAkēbQ@mV6, wsPg6, r2`9xp} "._Me3E,À tys9s2mcag"_N_F7Dl)Fr&;ve8;! !{!/{10mJUe:?!l1f~.mW/|G&1|v{L¾FK.Ů]l5*z `my+dR80| DDDDDF ̜9P8pzԵco2Zqm>/cYjpu(pm |s+egߗ\5Pf9# MlBkW(`4gs=pp;gWY|$ܷ.6aq$O\='7`E`.|+WjWp`!?Oo#DpEam aO|mxhCmD7]c_v9f8{p}(++w,_ȕٮsȜ tМBF2çp@DDDDd+?O>>~pIl,(A}#*keK/'gwf:sߘzD8?O*7GbymoX?Mo楡#s?v׭1|y3= ܇^(+W80Ҥ>@ p:2)>"""""{ S3e=Ӫ*!u跣5{ۢg "qCp1><5ڙaovŗA}hglAcqs\ƙJ;m`/n_Ffdŝ%x~ui(؛I4aq&~Kg~'5YR 9˝Y 2l,"lģcqJ~.[᾵Q)H+kBJi}<seuں`SwO<E4(o+ <7&z?$,ϱ͒ܥgwȞgn|^W10H9 "jJ Ú|,˶͆0_+&XR%^ٝl^l{<`2=}9|܇6<0al^>>(W80pe"K 1pPqppW@M   Q΁}> S`ōQMQ%HhBq:,J&{{Wp1x|LrOML ېYT4uٙ|+p177:ٰf}QuUdKdW 4Κ]y1eXBOiWMqvexjexv=j< W*;XY,Þ,?~_ [lJ|12dנH `Pp\xg^>3ysUL*숯B@N; V D,De,8 rp1xa'^ogOtOl*sjZl s{м 80$X~h? *lg'p__rd/\4VOؗbg1gC^[h>q?s3Ϳ[RgWtK}(W{I0n[Ww'۾l[ds&<9 c`g `.X͆y<=p0\2x ?{ E34 n vFESkX0< )x" ]*, ʱ2IoycT<Ѹ-n7hF]I{Rg7hACbX`ύ^_> Fm(o5"ÁL @J`.˙p`B\)]]]hkk;zh-hik;jPQrԙleo+Cm[)j[KPmʖbT6ȬELQ&|2R%:1;sB:,{@nF^H<<ǚ* Bws\V+8=ePfE-('v>9K1899p?Tne~l!,uۢ0q]=,󇩾o\0 s1'1~a}tgiC쏐Qlgδ 9kN\7- 3Gᇛ@bwBrjl0\cdky Hz_y烁 /g^wo_Q={FpF\8YD"6ѐXFçp@DDDDdEFF ooo7ˑ{qDDv܉ !مuu :rv`CTl|_Ʀ3[bgbm 7ǫo0k"fa3"|VGʈX:sT_sIx|`n9cK|l~ BcKp_в`d6fw׹wIkʁĄ)ϺxVؙ5lfn;8M,#o5o6;h3>}Iխ.g/ ʵ8( o&>4o<16]c{<l"9gv[^'#< 9ՙȫB~]5d!ǹ墤1eMy(oCEKZblmACO3z\w6ϭ>wcW&ܓlo6oӮhlw[z*48h~_91 ^΀@}{mL/4~0ggy(~7_l3<ӱ>a0ڻY mveKD`j`Oml/L½kl0p@\.\-7̱8BGIckf;szm9$>&go*ijs|Nï&z7=qTo<5_5a i*,G3h|-:l& `4w 7fT@M ҳ ͹\1ߞsy^ *s~lD#1404?Tn۩!ȳh"lܸӧOx'p"aee]8id@6`%:Whp`Bpq~2`r //444A:?3m%8e0'~6s E$kŠ0;!9 Ȯ@Q}78Pɒp1xvA+[lIr/k{m7 8嬋Akβ 2k7,Ar˻pHܷ&ΦDo;P~ _;[mls%gn;۝3=SmuaQBl{pܰ8[kslG6 8^y>B9fךC~(4`#aǜ#+9f/ʆ ){[elZ<~_]~`#)TcEsRnu j"UxI7Om[8ܹ& 4BoYh1gURF<^lBoǮ 1; Ǫ& E=_{o)1wWwCs"2 `B'ݱrJ[p/d3} }8=BF+çp@DDDDduJ=Pxp٣/kiiM]1`߸Xlގz ɫ˻SXڌN;8)'ƞ-rBL ymj@t9Aq_yͅvKp=3 l܉hxq`6f{g ѸwMm ޸-_[Z覥4 ͷ嬇Wӊ.rkQlWpAMk7 ^NUI Lٟf^l<-"OvX\6;ዿƒcŽDH)Gjy[`}>z!y m`6$ds6{þ^ F<BCC~+V@ff-u;'-p"onlقI gLg% @s4)>""""" C3+,Fk( $m,`st k\95ؘh;b Qьz;2=]v=9h΁~PΞgHpȆ=08r zrleA9xaGYEَkfqll,JjΤk붥|+myٶ l,+6}l\5p@\6c CU6xϬBd~-;rP0<ƣG8k̾gnc~G`\puCϲ4]v`{39þî`M9镁GZ_Lp[>Y{zWaglQbeU= q2?ۖ+[l߇6D!@en)FTaKm(\1-c2_A3YȖT2'®0_p@+3gikS})>gG8pٸ=X9yu_W  ?D@f3P}0= nrIOmaWi\o1K =)>R$6tǀy;;B#!9fjH~yc g[{sz;0 6`80_9ad^wH,:l)b<-w]#p ?v=oOeK>g "Sʱ:$31i۞905߷& / Up0ܻ.ҮT`sy~Y؟\fWL4uf3x/~Ѡ0XihEam;0 µqlG@p<>H&0ɮ,9uͨB@fvŖ8+xe ['6`+0Ys^ 6@aj` >h1Pw x`(@yU80pxbZp4$6o4`7?Z+P80| DDDDDF&TVz0)5om˯,;WN-:* a 6r[mY09@_9 gsXv&3[;l }Vg9Uynwe޶_؁I)㝉%pK(OZ%b p0JlbOlXn!#bi)gGQxhCgj$8VLw C 3s=~x6 8d rkbc>;E!nA0m%!kUiұ( ]:l)1]j;<Π/lȐ_ӆf;V 0$`)AΦ7y{[|bhs+Yb*, 6=Kp`[1RAw n^nW ŝ veK89L Ę+ػЪNJZ3 *tR80_} qFv -`''?6t5 Oဈ(UTTu!;;[+ޡ6_hu0 1ō+x)p(!IA^u J;6ٌݜρmyؔرʇ|eApthv-ֆۙۊ0;}+fu6? +Cmݕꑆpp 1veyu1om9x<%r?eM(o>;ݱjg3L9g X H jl)Sb +f\97ȖaQ-4=ņl+&R+q Jl9UrI=nZv&70L{𒹌O4e%)]s1'/DcG=_np`{EkksÁA;&Ywr QڵkCdY0"6\|, ̳9R80z!??QQQyqq1cW)J@iC-ycg;~oPQflH}dW"2+07p߲4_=7Yn/ ,g᥁X䟃~x#,eY` `9m УYͽxew-_&/=vߖS239`>xms?p~{9 #>YZ(Z'6?#ly!Z`HBS1~_)}XNf{e`Asppr5QxF-$|gK3Q3K0M,ClQ jm RbxhWP w322޷qÁ"׀׀j Q9}8 _c9NTWWޞ̻m]184k}<)ux&(6 Okaa!֬Y2{=!<>U-@KW*-P+8|<6&@9'5f,$[o楡nA .ꇋōm?7$;ρr oM+q˲0ܼ$?!xbK1ئX#6}<€K,GJiϒI̷H7g;V |2J+"""""`]}}e0tRZʆ]]]ܹsx}ddK=*B7cSdgʉQ8:4VVV)**ai(o@97*g=p@`=<m=}ntA=%ao / u)q?{ LHXg =1g#/ }cmUlfAtu7H¦" 5ݶPf .e+8c8Ȫjf5Pnw3`iKgxhC4^ڙcw- ؇+ \6+ws;qrƈ"ۛaTۘx9Zֵ2B p)p]-8dއ_.ۜW+R80| DDDDDF!K9C3B^jgr~8g8d~ڹs'***}]AsO3<ZZ5p(pmwod2ںm0`Á^G}|s3\89C,g]PМZl(ģcq{q 2߆axp}_k޺,̆,s՜@Ϧ[ L;7Rʮ$;32P`vQhhйnqJ>_icYg'UxyWcr RKoū ;7`_~pzsbK 1Q`Blͷܓܷucop3,;/Wp`B vmWp8<lŊ68PY? qqqE]i@i[ ^_AWgʉQ8ڸr "1 f,ajwU'c8{+8Cќ|Δ&aO| &uqP\2#7, X>xtS,Yi9pl|fbځ43o)2UԷYo-]Gq9>#,JЏ/jL Mzbs98u^0(Ԇ]iMxfk &s%3l\q&NX䟍MRLCV6ԴtzpN봄}@Ko.S8 """"2 q5lݺ.kkۺe\=PZȷr^ii8d=!-?op [Yڰ|'ٖ- ʮ5S˚cgsf?p_>@> E:Ù E*QŁ9vuoEp!(ܰ$7/Cclsb^c|h} ; ܵ6 W #|l偹X8c7;J_܀ sy󵨾5hͽ)v"{/ xxc4{D㟋pRIm+g.ōK<5Sɭ1<(Ϯ`03*ȪGjtbH0duÁ. }=cS8 """"2 q@JJm0h{ #y" ?X!g>'ٶYoQ~ٌxh}}{Eˬl;Ξ_'ڒ9`0[^塸fNh."lz6,Uh9K6=jW W resp8 1TACڶ.;obda4L1>Qxx፱f~0XIwm4[9bUOo > FVڰ-bʌ:{͇ lJ|V uJÁCӀ]_^2pWS80| DDDDDF!m(**Bww3& ap1FWOf}s3O6>8d{X=Aj ΩB;K"̖b=6Aǂ`\0㖥axtS]2B]%>̷c lV;@hBpT8{ܲdU;r0`WfoI+kCpJs>s|lgـxwH 03`Ktl&" % 4YePϽ9cauJÁT2(>"""""cqxvϳ(i.r^"g6TV b>\=FCvlc>U"*+0@G٦m o\-5sp@[Z9~O&b}Dօcg\c?^iW W-FN5^saeWbK,0o[7ņ\0e*p\Lޟr\P+  ʰ֨braHJ9fTFĵm=h3_֍nڟL \) zJCMçp@DDDDd sp`'}5KLQ8ڸr ++,+Ùħba汹zcW8ۺهffTAv$ͱm&YAj k#X;k~Tۀwi`9[#!5@Q]}s&Nq;q@5YNNDt~%a6bGfy9V+S'P٠ XN(`97+s5ː]jWcd`6.k{sW1`dR80zqE; ypK@m@OB+ 1̕^Lڼ^FswR9S($go=,s_ms>s Z;mM(k{z{f`G{Fٯ揻D.qp|O<# 9UȬ{bb Y5|^b3dܟ.s5u06k}+T܈zl/"""""c+Sj1,6r(pmQHDgOmc3߹zv8,^~9`S޹>YIT;npH'ܶxĮR[N'$Ɔ >qumv>p쪁S\Rh3~?W2Qp\QZ]%9vcQEXgW'D؛amsCDfcozy 2*[Z֌l󵴡 cxC!ɦp`EEPWWgZ 6$?>P]]mÃp^(cS8 """"2piВk]%`CEز5N^N5r(pm,+pltAy֮~[bY5v]vl ;`UOnf{lS9ؙkC Tȼ:_&6Z͝hv<;,dW 翥7v)͈+l@ZyR˛' .Yٶ<Mʱ/5烗ʰXBe"<WJs+ARN};dɢp``784c޽v@jj*֭[g ^^^Xf-/tRf{ xLBk ߏLqimm-"""PYYz!~y[[G}8Rފصa溡gy) \fdQZt2`/e+m(03ibG6⍽lΛjvp<"Ȫlu 䙍uj[l0fȍGVKUCx3ն#f3_˛lAvcPrI7<8\xgۢlOfݖ/bCũp`t+WDtt[#==@OO `xR pCO%;oXp`˗/NJ+PTTd?O:֭[v&ʲ3X`4[YsJ?x]Q8ڢ"#iӸj`uCMkϬƮ[s f9ͽ)ow%At(kCq}fOstQo ؐfI%`=gWte' u[bWDz\ۃ`tr˟F,Qт,y5\%/ _|\9! `=}')p]'!qM0 @߀XFçp@DDDDŰkrɓ-[غ뎯/CN?݊$[D,3g;B n~>gԳQoI}'Q%iKL9j3`cw%`_bJ_ۊ.p 92y5h4r[R wW pڻދ҆v[( WH.ǦB 7[Vcuh`JBkl_ӆs FB[9 ԵpTP8NZ8 Ph2p`8xUCqqp @Zjc`ݾ;PܘF$m(s^r 5&> w (o괍3Wg,ttu0f:ؗ;Paу*w6"mUdmhc`b_s&ٞ J%lxѐ[jWir6^6_y~Aus=SUFJ:i4sȨp`8:ZАx_" 6k|GEF@Vv6z\8@\=jΦ`vX+ 95H.iDD~ք:LHl [Zh6Wz6vܯ U-]{ݷv3x \92CEu+WIev5AlQ=kQao0+Q" < 5e>U6~{l0\pjVp`a=mֵu ~ ]ukLR8ؐ8=sL⻋=8hhYN*i` 4-EBQ#"rkm !g״t m17>r5C`SIv8= m)p`a9Ȩl[b9x~yuB$_+rrrpTWW;oDDDpv8A|bi!s3+ب[#\r<=v{.'3 {<;?Ӹsg/:li%oW?4l:(5Ԇs=oSdW<2Kb`>Vw_ =} xũpu +,s89yAဈ6Á!ϯw#Yo/ãW쾮ŝqwb@` @Fݽ]n02G՝UU묵ٰ5HŐ?''jPRR'NL}]߿YYYM8 =/wK79YmȬlEnm;b_V{~pU h0<ԍgСS^fGBAevFF0C*۔!] VȩѶ>eIDAT$*[eXᯎ)[+q{4Rig/ 9]O7ph {Q*j_`؊y}7 Z/..ƹspС-3gΨHW8j^LwM]Rރܚv!>YN"PTK帄ʖ70/;2]%Q+$ |;(ϥn= t[XQ]o MSߐ^$|~w8]labzÁc8@DDDD4@hF nfס[k@>ߵCYf1xyܿ}8GbbsխQ65 nyЗt/$]}(kv#X^Ql;"\O* B*!&R/ϥ2V@~Ww_ޣ'_[?C+ӿ6~w8 i[ &i18DDDDD/12E =H(iN#8ꃚ}ӟalw޽.rTlhV2ȰTK7CM]H-kFQ}: 8,_K^v)?<]2e~=o%,<,,k i7r2X<z] wW,A I`G ^^+">hM$z1 """"zݽ.\PBXXrss;~}vvv"55 b q.J2D~ʼn~8s-}-ic8r{^"{hXm= R-JxCg:Hqa}jG[H:ƗpAxHץ5[e1B_۵|Yni=3R y(oQߨ5j^e>j0ǭX]V- dnZPYPT(K0 sYIC2]m}[Q~s8SɺD18DDDDD/f;w^^^EyQQΞ=7nrQ% *++u|-j‰r{ blp"üypB*}^HH . hV+˥_{7[\~y j,g6QR)!.Ԗ: dci)! %#0xyp YƮmH4Ác8@DDDDH+Q 8ݐxWƔ "^mAj*rh 0N?ÁHTlKJp$R-⠲܃#.{}"եo%'-ߥgPAZD)Ae,G@׈ %6u- $H@'?3xypG".hH(p%Hy" H[{hMQP5NCNKn"=ܞp@*ɥ\Ay^pWyM -jTˠ@hW<0k R\K ]-uK*F ]I/.66hkkåK{|C^pp2-x &8v JGz -p6,X58[ ^Heeejw8uz^777C)#z{{q-?~\}ѳPwS7h| &+p2btE]G⏴`\/~mWI|`8r{b|D}TchؑJq{@Weٕ~Am y;@8|7{AZH =NtoS0xqHk)QQQ+WBGGr@^n$ Vr!oCCnQq@&`I7h| &"W3kU1e8y*00<_s1x=ှ_*ťt?VKKΞA=}B d5#P+[NK&ں k[M0xqHCww7BCC9uAO5@ o\߁ ?L"""")y! :uSt[v@E}Ґ?~Lz/>Pr׼lRQ;8NabxD[qtŧ~o#$A ] -d{@Ci]T@5߁@wn"18DDDDDSH}rEHlS+F r.9.Z=/ܞp@MPw@WI.Ӥŀt1$$ %ӷJiE!ှ{!=|Xc9 ^^ i( О ph {;,{# p`rS{ Q~xw"=^p@{?@cH;oR@˴u; DBNwM{0xyj8p&rrHd &rjG ZsPh}m]/ :,݋1xHသTmj?m $P]ÁÁ@H Ác8@DDDDihh@XXHJJB\\^ӧOŋHHH@ppz@ NU4aw-6_\!i=j#CÇf +mWRy'w{11x=hFn5hM$z: &KF*c"UXFss?/ˇˍM/'v+D;18DDDDDS>M!z6d\ // Yb8҇!ѳ"cf1DDDDDSPNNza9I~~z|<ƪd, >^d!!! _RDEEDm8?:ǟ>\C_eϧ߇קqQK._ʕ+>[@xGPP:˗}}"qD2o0z'pN<9)>u9t[~DݖF/ ䷗}y,={vמC3gLwϑ`&l2|H-m2H5qA2zHew$c}QӟC)OvIևIq>w;! ܑs^C 9;]53]vI"""""Q]]]n={Bݳg+##CmizyOʼsssiV!eeegGRݳ!e|Aݔ!3Hdtے~ݔ!e|D]rܖճ&qٳ%E2Inx֚{H+A9Myvdp!y T8{ry]TOCW.@y˝iNdb2O}2o]%+CO(o(;ԡ/82o &P>G|G+,gH@Cϑ-zd{}P'mY?SϚۻW,YerYS+)dU y7g"wݺr\`o|w]GG;[e̫sYVy^WW7 Ieyfffyumܲ~ !%->dX&][wNS֏WJzINt}d'Byyy~"Hyy32?}"JٓxwEP{Nl|5-M"C9~9mV^J!eH}C*}kr哇!DCRwy>&3BO9K$#]f9r^}..-rr$a8@DDDDDȅt%(+å^[[ w!wJS'N}Ů4OJJR/&B.ne,{ 2PE{ WXK7\.~e}M\PKׯ_W[,u둉!{yŒwYRI22oȑ-] w~"O*äp77I|֓HϗmX㑊+?m$377W?Yl)qwQ&}]ʞH$xDl~2eXR'|Ǐ= 9Kc-ۓEjjz J`٧'/<<\Fٗc>>N~G#DnH-k|z^$2Jr|9:!胇rÀ{I/CϢ _jY:󰉄%BUB)x >8ѳpFjh uK..q+u"]E\4ŵ.IeR/d7Yv3dw}Y$E/%dnr㈜Ⱦ?mYwN d%',[^3Q2٧+ϑ,7_Yvr%e^Sі>_reTKB2@<ד&\!B/qy\Kҏ4ٟUDDDs<M6DDDDD4e20Tڻ}ǫw|2Gsrr‘#GJ` d<T+տA#-d ciy =${Q%&"""""2^*]ooo ^@$%%﫯WÀdB_:qyyy9\/-})))IDrLwssSرcjk"""HWr -yR/] xՁ(}K7Fcє" L DDDDD/:DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM1 DDDDDDDDDDDS """"?CCay rj_Qvtjty +߹Ve)FnE=;7{sN38^ ւZ+FRnVգ&G܀²&t +4C^WZFՆZ)RnDSgXhmh@~m;:}4zܢ=ΞX *jQ1Dr5`Q٠G|VV?x]=brmEn@'*P֯a8@DDDDGGK}?y ט__sw3C𸖉feFMI.BN hf,5 #+h}dkѸt%iDVd,َVʍچWbcQGU +^!xA=OZ3]Ѩ+菢oCafQfxw-3ka(hFǀwN^գD_Dvh)N(.G{vZ-[O+2"OXbtЎNO?\s gosL)I7҄ ?E|aX9[Ny1 """"l!zs > =DZ ,0Hj3/pN^혱+=O]܌`O}K[>] scc)OFn*7˅`7pU #Xjbf[`\t!;P/ƇZm'ÁEܷ=qa8u{~\K8q8t7Ǵ5{n%:H)<=eD~")w CVxT3;͗pxoa6mŖuhw  y e60jMt qsR*ñrPNy1 """"d#=u: ,r<0W{⍟ma}/J'<9Mi\.AӊKG >ӞΒ{0_i5G݈&#[luhL> 5Dat=L4SfY_LHo׽Zo|NLꏠ\]+Q@NzZu`w\AAAYlar.aڝ0l28g1 X b8@DDDDD@C ۭ6A{Kp[17muCTr6n"\ w [1)ٸztjZ\˯Fø]`@̬췹(߾Eyy,_dF7(Yy7]0bI?kSP1zSӑr ;bpMJd*]DR6;@ Fp"ZWĴe[ERa5Zta1ZJʾv'uc%Rޣ/- ))P^aZ>1}no@g#:.gܩ**]e_r"6 AJ9آ:Zh*CtR G}㎔?,Fnm27C(mIeH-G[+彉h<)@UYʤle]tqtk3v+fׁ', 8&9Titw\ٿ}$)y9WP wRPw.(G϶d˹vZڈ1۬J97 㾲+P} rf=BĤVeH|-CDjvGP~*)RdUݍʾ%O85"~,8୍8~'薲SlլR$W{~^ :uReCs񌠭૕&Xr$!#yPypGУڲ;NNeHN+DjqJ9Yvi@g]&BJݩj[*cKOFAӭrTʹ[Z#L)äYbdki7ڎ-.3k`١qyW˙p)W. Qz?:K|,߉Xcɾ!w]pm8V6Ɛ,$] ҭ 3lfGqYm7/Bp1au7-1}0>SuߑHd "5r [eVX'#8W=ȀU8۔}G?^e&t!xEW_`iÁ! &N1,7;aw! wFF܄%[b2<4*Nqc:o\ f,ގܮ !G}ch >3ډͱlKU }h:5Mr+|uWҢ+4h/Kێmxu~Kd 4f ) ^W7ƒ|=䍹eJ cYlA@qB)M/"F)- J9r #X\Dr7,\ l[sj݃p@Zg|NLe]9bH)}vsz |4LV_M!_:ehB//L39cWc.7l-ށ7V`ljx :fldwu{ˬ0+ 7rFWh>7~>l'>P,5[|p> +PW^9V~0Gka}&NDYÝa) kĚۭ +?yFW  ^X)sfY0>?o7g7kbKP~)My2u?$S)XNxM#Ӄ,V׌ J\qfrm~/vŔ 'b|t[O$[ <Նr>!GePٞ7c@mMw`UDZ9wJYyb2 ڞz?HP3 """"gAӍX w 9elxl_GXn6`rKE읛ƇvbT/pont[vDn'% Xo.NX?Rqvlʽp7D{]T.*68ooO W IL\dW1w)6ن;ܺ}.Nb +`{{,1HJIK}5|?Nf5( er3V]DPt2bcoeL< :79;'4IIz6eW &ނ3_Pʞh܊R(^r$oumX F$"^4· Su=/m`#=l[, ;)|XYCL~&]7"7"W:vZuOYkkwwp3^)bqR66!]zBZN{*o]N_ҕJ9RN;ӗ:sb6|wxs%Fm/PEġ]B'VGآOHYVrCH>Z {+St+䭜[('wck_b*tnA]xu {_ı[I6tLVm3?FpD[q,, ~p8i}w)m0 >ұ_l?w{woW9yuooXp]p.+Y.r\v1kZ>V3 R+!7Bab .َ+cbpbC`T<נKJVYbוx*o,\k7WM}9ԏKXb>8o$6MYapg|q0A)GZ"A|FoF\r$H-/' sR &. ~^x9fѕh sbQx:'bWzvaGgf7{)}=۷>Xeꎍ>pn2bsXOW, 8&cP9I8}[ٟr3'0g1ޱ6m;P_cN昱+ܽay"z%ؗP\G.95d+RF>u+֘o Y(񪳍r{YsDDDDD ,&v>M mxo3VzƞTj&Íӗ+vLWBkssku R[ZKk%!ntSu,Ձ;cɪ}8.^…xo)({`}?`,sF/p[ d]-A_}c+Cb\/@v%ذoJDq>L7څ+:Ka9]GBS1]u7j \)n`gC rk=q4oԽ4 #< ̝ve٦rnl>#IQ ö+ u+R٘ر'\ijn=EqڲBјf*g0OWnAuJ8/h0}F*p-DH`k:lט-'ewaAڠ)a{?Hk`fQ֕87Dxn~FAwqϷc)۫ucE,Y'sׄ`? f!iHuWc *iMс~^+}XF{wTEnٔ2o-yuוQxMo(=VͿAwXp.J k p@Em':Ya#i"k/iE>n2nB{,l?\Au8m>-W4xm1g9>t&Du=p7])s?x){ &`xh]a8_Nu?ض o,±Cxo3_Uz">^JzJYyqU7A ^~QMiAu#Ed4o. 3L7=zUQ gSs|~6(U~́fHp;-LJsѝ ՖxwK( =~|?ekP|(~\ǡps |,d>taH4Sk!'.qº+Uj6X_N{|pGPwKaspՠ% f-5Q-5DI|8%=MÙ ap;m.{mxmVFfndXacrZm[?_VSrq%Stݸjx:܋ڻG7]=J(V+|_Z7\];9\ΨDQq 򊴏2؇/5 &w#7ka]t]VMeCnG}e)Rcla@a8;ČrAvaq 4߅Jk:-wo( k{P}^Ա4iZ*|7 φIlrGs7oZA_=.ǖH,Gn+*Eaa&Bw>pkE@/kQK~RW =h;Nw+{h)0}96ߔ$`]gb;,re I7(e{=l#Q\l/.EfJ 6k7qTXkÎ>wF4d:R˵ t98=L3•FuRNߍ\rzT-RN'Zi?P#d&Ԡz ~Z` KqH80ԣΘ{Q`P+ɍr+|`~OQcOBJt+5o[(ӍSxo/UqOjk}Cͅٶ_Z_F#cKl8 r($|NF Z;>>n9&װd~P 5"q'> B˜1R;c}p Af.x{{ u9 :#hKǿ;c:ñz5~vGvE٨ d>sk$f݉uBL_Їwle֝E(C3PJݨ9?0}L%^wlZSѣ;'RÁ6wyg1FZ?\_#vqƎ_ܛX&!7FИ3P et7*{?NpݿDDDDD'mDJ\ yƻ̰T ڲ6Vm7ŏGRP1^< 0Uv+4(I !{ +4h)*kOR/V3œ.]s~\c|BJC0mޓ(Mo׷w+$UKWo'}c+~`[rrgu1k.,p໵"0]6BӈKV tT=:WVy`O^p8Brf=eױ|9\_v`nUN 5.ig%2`:N{x^iWf% 8֟F\SϨJ Um.Ƈyʮ3p_jGjDcO^xWjۑr?w}.u}׃Þxށ'!$2k-ŦeQ&nV+]|WlQeʔG 5 f$^o 6ٺc&Y8H8ӆuJ9]+n r,ޗur3?内P,ٰ/Aj Y6\wsVrہWA죳YϹᯫ=aJGH87#a>enWw GѾx7.7K ~\8 5 .q,]_ՙص?FLb?L0Xz#h?-uď,Nٳ0m#f.tWFo[y>0;Gw63=L"ۆoB9ʹM>n-[2/1~uD{u.]|iIrd}Omp f+}.xe;RT:ؙlp wCy?$WY܎Qcљ/XbVii[ [iĪu#Պ(?Y Á>T$b9>ut,ш쵵ۋ  =p`$Glp5Xy Ńp yI*.݋TikV-۞0X ;o>| .G# <Qڊކ"\:oXc:t 'RFcZH8 z l ÁbpmlfKz!8l780bz H Y(1ܥGPpVm.r0*Ԡ {m,v= !8uBpz HˁFAmLkLJ?rţ)mE,^BK(8 xGօϹH,T>:{z(S'tۄ$\M/Cc7k7C5zrp 2^pyT8`6ဍ.xZe y| nGN`e3^[<4ɶp`\yfϮ.u u!3ԄÁw,5a:2~ 85)[|~csq(4#2R5^yГÁHx\8pQcÁBzHr ycIΗ>ՆLzpcp` 'L%d=v{%qB@ثeuNxw+[%""""Iу 3;2NxC7FHrҙss[p*Yp+x{qL.A~MUl0}R9ʯ~;bQbx))sޑ;s;E~ )"ovKdm5rRy'I! p<2M iFȪĈua2KEv+GKUk8ݭ:# #-=#5,7 uv|hqIOG̾MƘsYˈ|b8=d,0}/(;PsW!(xCN -d ./!TjG}Ybnf ؍Džch ێ0vciTxt+q7G%Qsg;`-Py(n{p bp@Z>E8 -q偡_=lܨdjBlu=d/׮Qj4\#wLH8g:Gx= O|""""I5 xlى︒q*\zP MHFn Ic/jp ϷǞ 8V:Aw3bO\ʿt?l^˯ TA̜?]]m誑,w9 k6}A{A<7n,;(Uןo?AƢ4ʽ+@ p@Xƪi[#OG 2;gwE2 FC)|L)l/F٘>p3CbBS*Uɡ0V}8^8fp>*m9s4n~X=Qvx{_l]k5#^|NgT+F* Nq [Rao!ul`ALera| |7F)A 4o 9R3l*hCWc̬sԭ\wמEB˘6V&xcq8tdQcXWӇw9uVڡq$nO<b_Au|5TYc[nы@3؉K^xr:q!u8:nMo zab3|7cc )ŠJ}w9+sWFӘ&an ўx{\#r[>{>8á8|&N\ljP8E[E4*FFPv +Wo{)2'8K5h-/F!R9 ?h)~9ߐ O[k|&AѸ= }P%zp@+*RdlhIIǭa≯> =VnŒnpQʹP3ú =ȏ  CBfnߺ+GL[ EAƟ@Sz SPƦyPNzp@BرX Kw`7mGrنҭ[K,r1iOJc9_En%P-1G}R/ܫTVC/\Xgߋu0 """"2ЉXۇYv߳lU[0m^AhN:Fջw~<|v+#|2k`jF?.ht9YѕNyز`'.NTރ*|  QFZq?.|J|k1|3M/^4%c;X﮴Ʒ\=\>w ?-[tg^x^`~ mj"Ng-O2e6fI=faֵ*4+N|`+=AijvMg="`fs DZ/1~lU]abt dT~l$p7½I.ii@pw _(۳Rnފ6x"(c2Q[pqsLZke6[5,[,oG0{ Zd0=X~+>G?M/vaWhb,puC"Ela*割+?["  pnX ]C 9/f:3nXэNx廽ZepK!?eYeIVh9rF t<2.؎!հVy%K]8-TʪVJYiJkXR~uqN')+o+|{6\z)ۤӕ>}myvu÷kRS|}\B2Q}pXcFf.&D-y,l+YDeu2v೥fʾgO}ef6y(j18)F#sG0nV {mh҇ "*Вxo Y6K2CMF=vey*02WJdy1ٵTzQ]eb>\iW)e GB]d'"ܬN.hš9;#噁A\X67.NxN _or`yPUР%65K/@{vZ]64ݏ1/MT&+gygk2S|kzQE~=q|}>,BcP[JَǏpol8P mqe? GN>az.e{aLSP7} b]~|6NjGce]w@F:{'^E)>]e|G ҇jcۄ"kLy֭܈e'RN: +qv#`&>;͘Pza)f1%p4^\D%f",.#659HUHt޻*dQ}a4zR򵷿>0ل̤H,M[jv7b,g\n5ZG߲Ԇj[q%6גrZR֞~t il#mY9|܊+A`Z WI({OӃM7Wզ^o-%\;4Tݸ<6bMy2n5܎v9eJ&Rt=MHOB2MQJy3~@?SNJ9mc֌.l֕>n; Ft*ν4ؐ ;!/- Ч]P;ʺEʋ)+OCnZB_ܸ_1ǝa6^1nTo=aAoS9Rˣ-caX [9IOeRjdp{ r~Jf7"M9Ƈ?:jpO9?ytʔs \Q+" ]={lQ{ZQ9ӽR4ʹ^jݘV8Wqz h.B|,1:Jr}+_W_ĔLSe- >_h1XFEyf&"t8$(Oz~z0Z-#3WRmP_C bk ~*vy4帩~Ύ*$%hר̲QDqV:brkRαӕƔjRn7(J-k|嘥G#V?b;tgQr]wz !1> Qc"""""s ZBѣtrSkE0yAk|MD1"""""""a8w4;s;2ǏpZ,ChH(,]p)Ob8@DDDDDDDB{U="zz#n@Jyliy,8W1]X ODPkUX2Va^v DDDDDDDDDDDS """""""""")pha8@DDDDDDDDDD40 """"""""""bM24kI*׽2<_HnM}}(MStYq?luӞtUGXeWWq_@Rv&]F{DW]ҋ_|nwcX&u%a#;a.""ggݵ8OWZc}z8 Gmpo$<=MRn=ar)O[Y<⟳v9_Gb$Ub'Ȼ}_yBns 8ᯋ(vG#5%{}s+%> WY.m^$ݍǵ.ݙxp:""z;qZٮ'm\La{cD4""""I& mx}5>^n^ayLげ`,fH2BȈxr+~t>#} >4 wGDDliӜ-: W{1M2 ;mW;`(~ruT^olq? ]{R {ރz=e8e`0oX{9v1C{3c>u"0 """"dj89G@x .wx@S&^p}n^xp@\| o-~2C\ XZZExk5 ͢n(*}80;%Oⵥ6zAyo˘z墿.֘o.6G=aw55=;Lhp3]+>^n%VTYCVV X iF\gaz;,4S3ϥ]EF{x8egXr"]_߀K>fʼd~xu2c~ :` J{ЭP)QtwXcYV;b(v ^ve}4q9|omFF.;|i͏`RcV[L_jnKip*j?e8bn}Ҟ3n|9Xllrn}NDD.dۅ.P1-]aH(Am=ܠ[! \ڍ3Xo-25p]̛md tA~73 "{CMloA|]װzo"f(xBb⨧3XfWex^쾑G "\ &6؄Ol[zuoP "j fY՜4yzQðvr{p88˕$Vc|+| 'Cq(WMR/cg[l>Bq[\kc3f,b8p6Gh/0W[W[,E{+L8'[k|fg.a_|,uH[;W `"6,t=}/c|1FV|g :1{>pnNh)^ʁj 1>pיs}إ,{h/=O5p VkCO+.X A07wm0* *szӱ q((Nnp >[n׬ү"vnie ƭ2yCxp޵<[|my{^|o?9ߐCǰR9}i?<_ť uۋᎤC}?d7W!ANo RC}bK,܀/Cm@Wf֭7˹hT/ԛx2!Q=HCCWg.9`B$ pDžZ{=j"PN{4-Jf|'YahGzE8Znt h ;1kmkDq8Rk|| SnD+kmb][5 jJ^W+~%KX3s]oՅ֘f^]ci%vcKDDsW[M7 3\ǼFjXu ~|5&,o/Gǚna]R;cz_G|gp'ph= N!RjQ[rq`fyu-ԋ0 .ʝ89&3W{˃>{cln7Hr=Xsxw $` Vh~9:ð~:, LCkChjh@Qa MmRs I+L_QˡAos@]J86.T4S9V9&`.l?ijjGGߨjXf!UPtk.m`u-VJ~N \9o¯9CSc= q'*0-1}l8`lC! ZK`ÁBД 7K|غU>$fY5w7L9☻pY`6\.^~ <%cNHh0 """"dm&ܻt3^r-]^X ~ jokFw|3G=7ZT102XxEwD:m8OTNAoS.qgg| l"la7j9p 3VCh0Ԥ\ڥv0STw.Ŭnrf,6ͳ֓)2(#7"zL#2a,' ؆@5t㳿hZ}2+I wmo 6G-0/=_2'S|yͱ.O4#qp`=lB_|+|cyر?7qBI"M>DDDDDlT8?ƌ[شah.6S8Fjy4hu_/3{czwjGs{/a<B##nCYm ZF_0(zR8޼^xwcUہ t+ }ӵhJmx"Ɔ#hjlDEs؀N}ЉT ZSg5o'V^oȯ%;- ј ,'5d`yu㳸p@ӂ[[CzM+4IG҆WQC)Á<m^56jAlt8_D<SH+k3JvtcxMDDDDDp@\t qBqǘ/QwsWXcYhWv_C!:VAn]_so2@o 65TgfiFpC ^O9rls +[jUٳdt/©^7tƋ2@*.c|k,8QtD`;9mxMD""""I6n8iBܥct|1N8 \Q.g}dW75U 1;0Om9cst׋kPwob= 9ʛG̸=Vv-N-GMc QR\s>.x^Yp&WBfuZ[PSUK೥S^!o/fwǮɺiE}m5B`2{E7(֠EU%2)P+R;,<*e}47" gyXǥ'ygfVrT7QGYq qôeAHы)Asn 6+`\̮S55Hq/|MчͲXMqZ &S3y!EMhnQU> >&""""I6P#6k1"JWASm\ c/Vk+pK,P(q{Yjovxb/Vo5;+wa[ ksX/֮?^\p7ނ=o=iO Ūի0x cv!34k֛ᕥvXlm.X ?۝=^gjkx}ɟ?Q.Zo/nV֎܈| ;=XIr>VZ-XeZ;*.xw4ލ ~Xمo_]%Ќ evDzv}xlJEg.v`}(t96ǝ&U(ŽÁx> 2gၡ.FnؓH[n+,zWqļ.XfXop[ 8$Vn+w{hwo7I8P6ަThgAK=-3żqmNxʱ<\Kȝ=fM?8η#N֬;kMV9N؝Xp,Kvm^u6kN!K}0K°`>PNms'q^,jWlNAczO$ph !~BEo{` u# >6;Y7uǰzifm]#z+qX 6;beh*å#p1)I>?x.F݃hGѽ[ P>_YCT8Fd1Sh{y<2@G%B60MWރʌHXy?Lp cqq<>8Z蠼Ez\bs-FY2c;Ŵ>`s9.+R[Ka<]y1phL\Ðܪ룹7'|F t] EaI?>^H R2vhGP8y29Lv4}(O}'^نVyPD6Ԟjb]Y)_e7p" "]u>м0&ʱ[OZqWBGVN`a&d_tӞn >2 FW5.&g?xƷkfCȿv3vGLuS^ +CȽ|_rÁZ :S?u9Ք-VnJ:"C|Vudd GO"Px4fw޸JDDDDBa8@DDDD)y! E7ltFxo@ p2Ԙlt\}Xx n!(UNkjq;,ۋmՎig0}0P`y¡K9aXz7H{3x_ۆ p([b8P8ri 1'2?u8 Ki~,B0BJCuj\9̹~g} RVVy$U;q+9{o=:FB-t:d囌 !fmsKvo,u D`״@lVKbZ P0 """""""I׋زr烰rۃ,bKoDlX06oû^lpتv(ކ>\jMb>\oV7 : -a{7a>ppǷ L1^/+gb Sgwj;^1Eۑvb\e| KsW:b}#|p=f6xw Vv]n3 ;E8ot<3o,4߃f촵ǷvG_mqFXW:';(Hso(9A  a|g +S.~4Eaf|K}/oU~+e}~nWcDDDDb8@DDDD4qo=hhDGG;r|> raH >n{?< # o,vR7M ?7a։l!t&_-zo&աmm Ȉ:vb6\BES;P[=#*֮3<4i'9[CgSRpd߿)L "7TJնP\݇ϗaOF2FPta-ž|t+^jʴoQiY],0% ۻKq fXCDcD9D~abmhnf4.-xe$q uphi˭ᓫ

\h9G`8T@}_;U[)kွ#;EACaˌ!=?/H:[Ƚ{]N)K)wMDbxu3yѠpq·+f'|fd-gPXi>6;jF*S`54ϓ; kg on9]Fjpyn| lj 8_mזFpiM?;n '9xVFp*mWNp->4<'=}fʴff{YM,ƆRvW܄< n"%062++hW a\ု6;UX럀 uыd>+VNx%2Nq.=p4>w;x*skZh_ =iZ=0 NCA8$+?6ȄFks]x`_. .. currentmodule:: sphinx_gallery .. automodule:: sphinx_gallery :no-members: :no-inherited-members: :py:mod:`sphinx_gallery`: .. autosummary:: :toctree: gen_modules/ :template: module.rst backreferences docs_resolv downloads gen_gallery gen_rst notebook py_source_parser scrapers sorting Examples -------- This tests that mini-gallery reference labels work: :ref:`sphx_glr_backref_sphinx_gallery.backreferences.identify_names`. .. toctree:: :maxdepth: 2 auto_examples/index.rst auto_examples_with_rst/index.rst auto_examples_rst_index/index.rst sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/make.bat000066400000000000000000000017211461331107500251150ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation set SPHINXOPTS = -v if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* if exist auto_examples rd /q /s auto_examples if exist auto_plotly_examples rd /q /s auto_plotly_examples if exist auto_pyvista_examples rd /q /s auto_pyvista_examples if exist tutorials rd /q /s tutorials if exist gen_modules rd /q /s gen_modules goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) :end sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/doc/minigallery.rst000066400000000000000000000026051461331107500265600ustar00rootroot00000000000000Test mini-galleries =================== Test 1-N .. minigallery:: sphinx_gallery.sorting.ExplicitOrder Test 1-D-D .. minigallery:: sphinx_gallery.sorting.ExplicitOrder :add-heading: Test 1-D-C .. minigallery:: sphinx_gallery.sorting.ExplicitOrder :add-heading: :heading-level: - Test 1-C-D .. minigallery:: sphinx_gallery.sorting.ExplicitOrder :add-heading: This is a custom heading Test 2-N .. minigallery:: sphinx_gallery.sorting.ExplicitOrder sphinx_gallery.sorting.FileNameSortKey Test 2-D-D .. minigallery:: sphinx_gallery.sorting.ExplicitOrder sphinx_gallery.sorting.FileNameSortKey :add-heading: Test 2-C-C .. minigallery:: sphinx_gallery.sorting.ExplicitOrder sphinx_gallery.sorting.FileNameSortKey :add-heading: This is a different custom heading :heading-level: = Test 1-F .. minigallery:: ../examples/plot_log.py Test 2-F-G .. minigallery:: ../examples/plot_matplotlib*.py Test 3-F-G-B .. minigallery:: ../examples/plot_log.py ../examples/*matplotlib*.py sphinx_gallery.sorting.ExplicitOrder sphinx_gallery.sorting.FileNameSortKey :add-heading: All the input types Test 1-F-R .. minigallery:: ../examples_with_rst/*.py Test 1-S .. minigallery:: ../examples_rst_index/*/*.py Test 3-N .. minigallery:: ../examples/plot_log.py ../examples/*matplotlib*.py sphinx_gallery.sorting.ExplicitOrder sphinx_gallery.sorting.FileNameSortKey sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/000077500000000000000000000000001461331107500245605ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/README.txt000066400000000000000000000000701461331107500262530ustar00rootroot00000000000000Gallery of Examples =================== Test examples. sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/__init__.py000066400000000000000000000002331461331107500266670ustar00rootroot00000000000000# This file is ignored through ignore_pattern so it does not need to have a # docstring following the sphinx-gallery convention (i.e. title + description) sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/future/000077500000000000000000000000001461331107500260725ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/future/README.rst000066400000000000000000000001431461331107500275570ustar00rootroot00000000000000:orphan: .. _future_examples: Future examples =============== Examples that use ``__future__``. sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/future/plot_future_imports.py000066400000000000000000000011341461331107500325700ustar00rootroot00000000000000""" Test __future__ imports across cells ------------------------------------ This example tests that __future__ imports works across cells. """ from __future__ import division from __future__ import print_function import matplotlib #################### # Dummy section, with :func:`sphinx_gallery.backreferences.NameFinder` ref. assert 3 / 2 == 1.5 print(3 / 2, end="") # testing reset of mpl orig_dpi = 80.0 if matplotlib.__version__[0] < "2" else 100.0 assert matplotlib.rcParams["figure.dpi"] == orig_dpi matplotlib.rcParams["figure.dpi"] = 90.0 assert matplotlib.rcParams["figure.dpi"] == 90.0 sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/future/plot_future_imports_broken.py000066400000000000000000000007601461331107500341340ustar00rootroot00000000000000""" Test without __future__ imports ------------------------------- Test that __future__ imports inside sphinx_gallery modules does not affect the parsing of this script. """ import sys PY3_OR_LATER = sys.version_info[0] >= 3 # SyntaxError on Python 2 print(3 / 2, end="") # Need to make this example fail on Python 3 as well (currently no way to say # that an example is expected to fail only on Python 2) if PY3_OR_LATER: raise RuntimeError("Forcing this example to fail on Python 3") sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/isentropic.m000066400000000000000000000006221461331107500271150ustar00rootroot00000000000000function isentropic(g) %% isentropic, adiabatic flow example % % In this example, the area ratio vs. Mach number curve is computed for a % hydrogen/nitrogen gas mixture. if nargin == 1 gas = g; else gas = Solution('gri30.yaml', 'gri30'); end %% Set the stagnation state gas.TPX = {1200.0, 10.0 * OneAtm, 'H2:1,N2:0.1'}; gas.basis = 'mass'; end sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/julia_sample.jl000066400000000000000000000004241461331107500275540ustar00rootroot00000000000000#= Julia example ============= An example for code written in Julia. =# function sphere_vol(r) return 4/3 * pi * r^3 end # %% # This should work in notebook mode, at least println(sphere_vol(3)) #%% # Here's a subsection # ------------------- phi = (1 + sqrt(5)) / 2 sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/local_module.py000066400000000000000000000001201461331107500275620ustar00rootroot00000000000000"""Trivial module to provide a value for plot_numpy_matplotlib.py.""" N = 1000 sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/parse_this.cpp000066400000000000000000000013741461331107500274320ustar00rootroot00000000000000/* * A C++ Example * ============= * * This tests parsing examples in languages other than Python */ // %% // Pygments thinks preprocessor directives are a type of comment, which is fun. The // following should *not* be merged into this formatted text block. #include int main(int argc, char** argv) { // %% // It's likely that we may want to intersperse formatted text blocks // within the ``main`` method, where the contents are indented. We should // retain the current indentation level in the following code block. std::vector y; for (int i = 0; i < 10; i++) { y.push_back(i * i); } /**************************/ /* Here comes the end! */ /**************************/ return 0; } sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_animation.py000066400000000000000000000013211461331107500301440ustar00rootroot00000000000000""" Animation support ================= Show an animation, which should end up nicely embedded below. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation # Adapted from # https://matplotlib.org/gallery/animation/basic_example.html def _update_line(num): line.set_data(data[..., :num]) return (line,) fig_0, ax_0 = plt.subplots(figsize=(5, 1)) # sphinx_gallery_thumbnail_number = 2 fig_1, ax_1 = plt.subplots(figsize=(5, 5)) data = np.random.RandomState(0).rand(2, 25) (line,) = ax_1.plot([], [], "r-") ax_1.set(xlim=(0, 1), ylim=(0, 1)) ani = animation.FuncAnimation(fig_1, _update_line, 25, interval=100, blit=True) fig_2, ax_2 = plt.subplots(figsize=(5, 5)) sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_command_line_args.py000066400000000000000000000005421461331107500316320ustar00rootroot00000000000000""" Command line arguments support ============================== Use command line arguments to control example script. """ import sys import numpy as np import matplotlib.pyplot as plt if len(sys.argv) > 1 and sys.argv[1] == "plot": fig_0, ax_0 = plt.subplots(figsize=(5, 1)) x = np.arange(0, 10.0, 1) ax_0.plot(x, x**2) plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_defer_figures.py000066400000000000000000000005271461331107500310050ustar00rootroot00000000000000""" Test plot deferring =================== This tests the ``sphinx_gallery_defer_figures`` flag. """ import matplotlib.pyplot as plt # %% # This code block should produce no plot. plt.plot([0, 1]) plt.plot([1, 0]) # sphinx_gallery_defer_figures # %% # This code block should produce a plot with three lines. plt.plot([2, 2]) plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_log.py000066400000000000000000000006441461331107500267550ustar00rootroot00000000000000""" Logging test ============ """ # %% import logging import sys def _create_logger(name=None): logger = logging.getLogger(name=name) logger.setLevel(logging.INFO) sh = logging.StreamHandler(sys.stdout) sh.setLevel(logging.INFO) logger.addHandler(sh) return logger # %% logger = _create_logger("first_logger") logger.info("is in the same cell") # %% logger.info("is not in the same cell") sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py000066400000000000000000000013151461331107500311770ustar00rootroot00000000000000""" ====================== Matplotlib alt text ====================== This example tests that the alt text is generated correctly for matplotlib figures. """ import matplotlib.pyplot as plt fig, axs = plt.subplots(2, 1, constrained_layout=True) axs[0].plot([1, 2, 3]) axs[0].set_title("subplot 1") axs[0].set_xlabel("x label") axs[0].set_ylabel("y lab") fig.suptitle("This is a\nsup title") axs[1].plot([2, 3, 4]) axs[1].set_title("subplot 2") axs[1].set_xlabel("x label") axs[1].set_ylabel("y label") plt.show() # %% # Several titles. # sphinx_gallery_thumbnail_number = -1 plt.plot(range(10)) plt.title("Center Title") plt.title("Left Title", loc="left") plt.title("Right Title", loc="right") plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_backend.py000066400000000000000000000011471461331107500320110ustar00rootroot00000000000000""" Setting the Matplotlib backend ============================== """ # %% # The Matplotlib backend should start as `agg` import matplotlib print(f"Matplotlib backend is {matplotlib.get_backend()}") assert matplotlib.get_backend() == "agg" # %% # Changing the Matplotlib backend to `svg` should be possible matplotlib.use("svg") print(f"Matplotlib backend is {matplotlib.get_backend()}") assert matplotlib.get_backend() == "svg" # %% # In a new code block, the Matplotlib backend should continue to be `svg` print(f"Matplotlib backend is {matplotlib.get_backend()}") assert matplotlib.get_backend() == "svg" sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_numpy_matplotlib.py000066400000000000000000000027051461331107500315730ustar00rootroot00000000000000""" ====================== Link to other packages ====================== Use :mod:`sphinx_gallery` to link to other packages, like :mod:`numpy`, :mod:`matplotlib.colors`, and :mod:`matplotlib.pyplot`. FYI this gallery uses :obj:`sphinx_gallery.sorting.FileNameSortKey`. """ from warnings import warn import numpy as np from matplotlib.colors import is_color_like from matplotlib.figure import Figure from itertools import compress # noqa import matplotlib import matplotlib.pyplot as plt import sphinx_gallery.backreferences from local_module import N # N = 1000 t = np.arange(N) / float(N) win = np.hanning(N) print(is_color_like("r")) fig, ax = plt.subplots() ax.plot(t, win, color="r") ax.text(0, 1, "png", size=40, va="top") fig.tight_layout() orig_dpi = 80.0 if matplotlib.__version__[0] < "2" else 100.0 assert plt.rcParams["figure.dpi"] == orig_dpi plt.rcParams["figure.dpi"] = 70.0 assert plt.rcParams["figure.dpi"] == 70.0 listy = [0, 1] compress("abc", [0, 0, 1]) warn("This warning should show up in the output", RuntimeWarning) x = Figure() # plt.Figure should be decorated (class), x shouldn't (inst) # nested resolution resolves to numpy.random.mtrand.RandomState: rng = np.random.RandomState(0) # test Issue 583 sphinx_gallery.backreferences.identify_names( [("text", "Text block", 1)], sphinx_gallery.backreferences._make_ref_regex(), ) # 583: methods don't link properly dc = sphinx_gallery.backreferences.DummyClass() dc.run() print(dc.prop) sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_pickle.py000066400000000000000000000007211461331107500274370ustar00rootroot00000000000000""" Pickling -------- This example pickles a function. """ from math import sqrt import pickle from joblib import Parallel, delayed assert __name__ == "__main__" assert "__file__" not in globals() def function(x): """Square root function.""" return sqrt(x) pickle.loads(pickle.dumps(function)) # Now with joblib print(Parallel(n_jobs=2)(delayed(sqrt)(i**2) for i in range(10))) print(Parallel(n_jobs=2)(delayed(function)(i**2) for i in range(10))) sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_repr.py000066400000000000000000000004061461331107500271400ustar00rootroot00000000000000""" Repr test ========= Test repr and the sphinx_gallery_dummy_images config. """ # sphinx_gallery_dummy_images=2 class A: """Class with `_repr_html_` method for testing.""" def _repr_html_(self): return "

This should print

" A() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_scraper_broken.py000066400000000000000000000003461461331107500311720ustar00rootroot00000000000000""" Error during scraping ===================== The error is actually introduced by the resetter in ``conf.py``. It mocks a "zero-size reduction" error in ``fig.savefig``. """ import matplotlib.pyplot as plt fig = plt.figure() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_second_future_imports.py000066400000000000000000000015441461331107500326160ustar00rootroot00000000000000""" Testing backreferences ---------------------- This example runs after plot_future_statements.py (alphabetical ordering within subsection) and should be unaffected by the __future__ import in plot_future_statements.py. We should eventually update this script to actually test this... we require Python 3 nowadays so the __future__ statements there don't do anything. So for now let's repurpose this to look at some backreferences. We should probably also change the filename in another PR! """ # sphinx_gallery_thumbnail_path = '_static_nonstandard/demo.png' from sphinx_gallery.sorting import ExplicitOrder from sphinx_gallery.scrapers import figure_rst, clean_modules ExplicitOrder([]) # must actually be used to become a backref target! assert 3 / 2 == 1.5 assert figure_rst([], "") == "" assert clean_modules(dict(reset_modules=[]), "", "before") is None sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_svg.py000066400000000000000000000005541461331107500267730ustar00rootroot00000000000000""" ================== "SVG":-`graphics_` ================== Make sure we can embed SVG graphics. Use title that has punctuation marks. """ import numpy as np import matplotlib.pyplot as plt from local_module import N # N = 1000 t = np.arange(N) / float(N) win = np.hanning(N) plt.figure() plt.plot(t, win, color="r") plt.text(0, 1, "svg", size=40, va="top") sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples/plot_webp.py000066400000000000000000000001421461331107500271220ustar00rootroot00000000000000""" ============ Save as WebP ============ """ import matplotlib.pyplot as plt plt.plot([1, 2]) sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/000077500000000000000000000000001461331107500266375ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir1/000077500000000000000000000000001461331107500314025ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir1/README.txt000066400000000000000000000002701461331107500330770ustar00rootroot00000000000000================================== Example of directory with a README ================================== This subdir uses a README to generate the index. .. toctree:: plot_sub1 sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir1/plot_sub1.py000066400000000000000000000004661461331107500336720ustar00rootroot00000000000000""" Plot-sin sub1 ============= This is a file generated from ``plot_sub1.py`` by sphinx-gallery. """ import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir2/000077500000000000000000000000001461331107500314035ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir2/index.rst000066400000000000000000000003061461331107500332430ustar00rootroot00000000000000=============================== Subdirectory2 with an index.rst =============================== Here we need to specify any files in this directory in the toc by hand: .. toctree:: plot_sub2 sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/examp_subdir2/plot_sub2.py000066400000000000000000000003741461331107500336720ustar00rootroot00000000000000""" Plot-sin subdir 2 ================= """ import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/index.rst000066400000000000000000000007341461331107500305040ustar00rootroot00000000000000========================================== Example gallery that has its own index.rst ========================================== Usually made by sphinx-gallery from the ``README.txt``. However, if ``index.rst`` is given, we use that instead and ignore README.txt. .. toctree:: plot_examp Subtopic one ============ .. toctree:: examp_subdir1/index Subtopic two ============ This subtopic's directory has its own index.rst. .. toctree:: examp_subdir2/index sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_rst_index/plot_examp.py000066400000000000000000000004401461331107500313570ustar00rootroot00000000000000""" Introductory example - Plotting sin =================================== """ import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/000077500000000000000000000000001461331107500265035ustar00rootroot00000000000000sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/README.txt000066400000000000000000000021041461331107500301760ustar00rootroot00000000000000===================================== Example with rst files passed through ===================================== Sometimes it is desirable to mix rst files with python files in the same example directory. Here, the source directory has a mix of python examples (``plot_boo.py``) and raw rst files (``rst_example1.rst``). The rst files are passed directly from the source directory ``/examples_with_rst/rst_example1.rst`` to the directory created by ``sphinx-gallery`` in ``/doc/examples_with_rst/rst_example1.rst`` without change. Any rst files that are passed through this way must have a manual ``toctree`` entry somewhere, or you will get a warning that the file doesn't exist in the toctree. We add that here to the ``README.txt`` as:: Rst files ========= .. toctree:: rst_example1 rst_example2 Sphinx-gallery files ==================== Note that the python example also shows up as a usual thumbnail below this table of contents. Rst files ========= .. toctree:: rst_example1 rst_example2 Sphinx-gallery files ====================sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/plot_boo.py000066400000000000000000000003761461331107500307000ustar00rootroot00000000000000""" Plot a sin ========== Example of sin """ import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/plot_cos.py000066400000000000000000000003521461331107500306770ustar00rootroot00000000000000""" Plot-cos ======== """ import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.cos(x) plt.plot(x, y) plt.xlabel(r"$x$") plt.ylabel(r"$\sin(x)$") # To avoid matplotlib text output plt.show() sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/rst_example1.rst000066400000000000000000000011071461331107500316400ustar00rootroot00000000000000======================== A Restructured-text file ======================== This Rst file is passed through without being changed. It was in the source directory as ``/examples_with_rst/rst_example1.rst`` and gets passed to the directory created by ``sphinx-gallery`` in ``/doc/examples_with_rst/rst_example1.rst``. You need to reference this rst file in a *toctree* somewhere in your project, as ``sphinx-gallery`` will not add it to a *toctree* automatically like it does for ``*.py`` examples. This could be referenced in your ``README.txt`` as we have done in this directory. sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/examples_with_rst/rst_example2.rst000066400000000000000000000011341461331107500316410ustar00rootroot00000000000000=============================== A Second Restructured-text file =============================== This Rst file is passed through without being changed. It was in the source directory as ``/examples_with_rst/rst_example2.rst`` and gets passed to the directory created by ``sphinx-gallery`` in ``/doc/examples_with_rst/rst_example2.rst``. You need to reference this rst file in a *toctree* somewhere in your project, as ``sphinx-gallery`` will not add it to a *toctree* automatically like it does for ``*.py`` examples. This could be referenced in your ``README.txt`` as we have done in this directory. sphinx-gallery-0.16.0/sphinx_gallery/tests/tinybuild/utils.py000066400000000000000000000057041461331107500244620ustar00rootroot00000000000000"""Utility functions for doc building.""" import os.path as op from sphinx_gallery.scrapers import matplotlib_scraper def notebook_modification_function(notebook_content, notebook_filename): """Implement JupyterLite-specific modifications of notebooks.""" source = f"JupyterLite-specific change for {notebook_filename}" markdown_cell = {"cell_type": "markdown", "metadata": {}, "source": source} notebook_content["cells"] = [markdown_cell] + notebook_content["cells"] class MatplotlibFormatScraper: """Calls Matplotlib scraper, passing required `format` kwarg for testing.""" def __repr__(self): return self.__class__.__name__ def __call__(self, block, block_vars, gallery_conf): """Call Matplotlib scraper with required `format` kwarg for testing.""" kwargs = dict() if ( op.basename(block_vars["target_file"]) == "plot_svg.py" and gallery_conf["builder_name"] != "latex" ): kwargs["format"] = "svg" elif ( op.basename(block_vars["target_file"]) == "plot_webp.py" and gallery_conf["builder_name"] != "latex" ): kwargs["format"] = "webp" return matplotlib_scraper(block, block_vars, gallery_conf, **kwargs) class ResetArgv: """Provide `reset_argv` callable returning required `sys.argv` for test.""" def __repr__(self): return "ResetArgv" def __call__(self, sphinx_gallery_conf, script_vars): """Return 'plot' arg if 'plot_command_line_args' example, for testing.""" if "plot_command_line_args.py" in script_vars["src_file"]: return ["plot"] else: return [] def _raise(*args, **kwargs): import matplotlib.pyplot as plt plt.close("all") raise ValueError( "zero-size array to reduction operation minimum which " "has no identity" ) class MockScrapeProblem: """Used in 'reset_modules' to mock error during scraping.""" def __init__(self): from matplotlib.colors import colorConverter self._orig = colorConverter.to_rgba def __repr__(self): return "MockScrapeProblem" def __call__(self, gallery_conf, fname): """Raise error for 'scraper_broken' example.""" from matplotlib.colors import colorConverter if "scraper_broken" in fname: colorConverter.to_rgba = _raise else: colorConverter.to_rgba = self._orig class MockSort: """Fake sort used to test that mini-gallery sort is correct.""" def __repr__(self): return "MockSort" def __call__(self, f): """Sort plot_sub* for one test case.""" if "subdir2" in f: return 0 if "subdir1" in f: return 1 return f mock_scrape_problem = MockScrapeProblem() matplotlib_format_scraper = MatplotlibFormatScraper() reset_argv = ResetArgv() mock_sort = MockSort() def noop_key(x): """Sortkey that passes x.""" return x sphinx-gallery-0.16.0/sphinx_gallery/utils.py000066400000000000000000000133341461331107500213130ustar00rootroot00000000000000"""Utilities. Miscellaneous utilities. """ # Author: Eric Larson # License: 3-clause BSD import hashlib import os import re from shutil import move, copyfile import subprocess from sphinx.errors import ExtensionError import sphinx.util try: from sphinx.util.display import status_iterator # noqa: F401 except Exception: # Sphinx < 6 from sphinx.util import status_iterator # noqa: F401 logger = sphinx.util.logging.getLogger("sphinx-gallery") def _get_image(): try: from PIL import Image except ImportError as exc: # capture the error for the modern way try: import Image except ImportError: raise ExtensionError( "Could not import pillow, which is required " f"to rescale images (e.g., for thumbnails): {exc}" ) return Image def scale_image(in_fname, out_fname, max_width, max_height): """Scales image centered in image box using `max_width` and `max_height`. The same aspect ratio is retained. If `in_fname` == `out_fname` the image can only be scaled down. """ # local import to avoid testing dependency on PIL: Image = _get_image() img = Image.open(in_fname) # XXX someday we should just try img.thumbnail((max_width, max_height)) ... width_in, height_in = img.size scale_w = max_width / float(width_in) scale_h = max_height / float(height_in) if height_in * scale_w <= max_height: scale = scale_w else: scale = scale_h if scale >= 1.0 and in_fname == out_fname: return width_sc = int(round(scale * width_in)) height_sc = int(round(scale * height_in)) # resize the image using resize; if using .thumbnail and the image is # already smaller than max_width, max_height, then this won't scale up # at all (maybe could be an option someday...) try: # Pillow 9+ bicubic = Image.Resampling.BICUBIC except Exception: bicubic = Image.BICUBIC img = img.resize((width_sc, height_sc), bicubic) # img.thumbnail((width_sc, height_sc), Image.BICUBIC) # width_sc, height_sc = img.size # necessary if using thumbnail # insert centered thumb = Image.new("RGBA", (max_width, max_height), (255, 255, 255, 0)) pos_insert = ((max_width - width_sc) // 2, (max_height - height_sc) // 2) thumb.paste(img, pos_insert) try: thumb.save(out_fname) except OSError: # try again, without the alpha channel (e.g., for JPEG) thumb.convert("RGB").save(out_fname) def optipng(fname, args=()): """Optimize a PNG in place. Parameters ---------- fname : str The filename. If it ends with '.png', ``optipng -o7 fname`` will be run. If it fails because the ``optipng`` executable is not found or optipng fails, the function returns. args : tuple Extra command-line arguments, such as ``['-o7']``. """ fname = str(fname) if fname.endswith(".png"): # -o7 because this is what CPython used # https://github.com/python/cpython/pull/8032 try: subprocess.check_call( ["optipng"] + list(args) + [fname], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) except (subprocess.CalledProcessError, OSError): # FileNotFoundError pass def _has_optipng(): try: subprocess.check_call( ["optipng", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except OSError: # FileNotFoundError return False else: return True def get_md5sum(src_file, mode="b"): """Returns md5sum of file. Parameters ---------- src_file : str Filename to get md5sum for. mode : 't' or 'b' File mode to open file with. When in text mode, universal line endings are used to ensure consistency in hashes between platforms. """ if mode == "t": kwargs = {"errors": "surrogateescape", "encoding": "utf-8"} else: kwargs = {} with open(src_file, "r" + mode, **kwargs) as src_data: src_content = src_data.read() if mode == "t": src_content = src_content.encode(**kwargs) return hashlib.md5(src_content).hexdigest() def _replace_md5(fname_new, fname_old=None, method="move", mode="b"): fname_new = str(fname_new) # convert possible Path assert method in ("move", "copy") if fname_old is None: assert fname_new.endswith(".new") fname_old = os.path.splitext(fname_new)[0] replace = True if os.path.isfile(fname_old): if get_md5sum(fname_old, mode) == get_md5sum(fname_new, mode): replace = False if method == "move": os.remove(fname_new) else: logger.debug(f"Replacing stale {fname_old} with {fname_new}") if replace: if method == "move": move(fname_new, fname_old) else: copyfile(fname_new, fname_old) assert os.path.isfile(fname_old) def _has_pypandoc(): """Check if pypandoc package available.""" try: import pypandoc # noqa # Import error raised only when function called version = pypandoc.get_pandoc_version() except (ImportError, OSError): return None, None else: return True, version def _has_graphviz(): try: import graphviz # noqa F401 except ImportError as exc: logger.info( "`graphviz` required for graphical visualization " f"but could not be imported, got: {exc}" ) return False return True def _escape_ansi(s): """Remove ANSI terminal formatting characters from a string.""" return re.sub(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]", "", s) sphinx-gallery-0.16.0/tutorials/000077500000000000000000000000001461331107500165735ustar00rootroot00000000000000sphinx-gallery-0.16.0/tutorials/README.txt000066400000000000000000000006021461331107500202670ustar00rootroot00000000000000.. _notebook_examples: Notebook-style Narrative Gallery ================================ You can have multiple galleries, each one for different uses. For example, one gallery of examples and a different gallery for tutorials. This gallery demonstrates the ability of Sphinx-Gallery to transform a file with a Jupyter notebook style structure (i.e., with alternating text and code). sphinx-gallery-0.16.0/tutorials/plot_parse.py000066400000000000000000000057501461331107500213240ustar00rootroot00000000000000""" Alternating text and code ========================= Sphinx-Gallery is capable of transforming Python files into reST files with a notebook structure. For this to be used you need to respect some syntax rules. This example demonstrates how to alternate text and code blocks and some edge cases. It was designed to be compared with the :download:`source Python script `.""" # %% # This is the first text block and directly follows the header docstring above. import numpy as np # noqa: F401 # %% # You can separate code blocks using either a single line of ``#``'s # (>=20 columns), ``#%%``, or ``# %%``. For consistency, it is recommend that # you use only one of the above three 'block splitter' options in your project. A = 1 import matplotlib.pyplot as plt # noqa: F401 # %% # Block splitters allow you alternate between code and text blocks **and** # separate sequential blocks of code (above) and text (below). ############################################################################## # A line of ``#``'s also works for separating blocks. The above line of ``#``'s # separates the text block above from this text block. Notice however, that # separated text blocks only shows as a new lines between text, in the rendered # output. def dummy(): """This should not be part of a 'text' block'""" # noqa: D404 # %% # This comment inside a code block will remain in the code block pass # this line should not be part of a 'text' block # %% # # #################################################################### # # The above syntax makes a line cut in Sphinx. Note the space between the first # ``#`` and the line of ``#``'s. # %% # .. warning:: # The next kind of comments are not supported (notice the line of ``#``'s # and the ``# %%`` start at the margin instead of being indented like # above) and become too hard to escape so just don't use code like this:: # # def dummy2(): # """Function docstring""" # #################################### # # This comment # # %% # # and this comment inside python indentation # # breaks the block structure and is not # # supported # dummy2 # """Free strings are not supported. They will be rendered as a code block""" # %% # New lines can be included in your text block and the parser # is capable of retaining this important whitespace to work with Sphinx. # Everything after a block splitter and starting with ``#`` then one space, # is interpreted by Sphinx-Gallery to be a reST text block. Keep your text # block together using ``#`` and a space at the beginning of each line. # # reST header within text block # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ print("one") # %% # # another way to separate code blocks shown above B = 1 # %% # Code blocks containing Jupyter magic are executable # .. code-block:: bash # # %%bash # # This could be run! # # %% # Last text block. # # That's all folks ! # # .. literalinclude:: plot_parse.py # #