├── .gitattributes ├── .github └── workflows │ ├── ci-tests-colab.yml │ ├── ci-tests-jupyter.yml │ ├── request-colab-tests.yml │ └── trigger-colab-tests-pr.yml ├── .gitignore ├── .gitmodules ├── CITATION.cff ├── LICENSE ├── README.md ├── davos ├── __init__.py ├── __init__.pyi ├── core │ ├── __init__.py │ ├── __init__.pyi │ ├── config.py │ ├── config.pyi │ ├── core.py │ ├── core.pyi │ ├── exceptions.py │ ├── exceptions.pyi │ ├── parsers.py │ ├── parsers.pyi │ ├── project.py │ ├── project.pyi │ ├── regexps.py │ └── regexps.pyi ├── implementations │ ├── __init__.py │ ├── __init__.pyi │ ├── colab.py │ ├── colab.pyi │ ├── ipython_common.py │ ├── ipython_common.pyi │ ├── ipython_post7.py │ ├── ipython_post7.pyi │ ├── ipython_pre7.py │ ├── ipython_pre7.pyi │ ├── js_functions.py │ ├── js_functions.pyi │ ├── jupyter.py │ ├── jupyter.pyi │ ├── python.py │ └── python.pyi └── py.typed ├── paper ├── admin │ ├── cover_letter.pdf │ ├── cover_letter.tex │ └── declarationStatement.docx ├── boneyard.tex ├── changes.pdf ├── changes.tex ├── elsarticle-num.bst ├── elsarticle.cls ├── figs │ ├── example1.pdf │ ├── example2.pdf │ ├── example3.pdf │ ├── example4.pdf │ ├── example5.pdf │ ├── example6.pdf │ ├── example7.pdf │ ├── example8.pdf │ ├── flow_chart.pdf │ ├── illustrative_example.pdf │ ├── package_structure.pdf │ ├── shareable_code.pdf │ ├── shareable_code_2d.pdf │ ├── snippet1.pdf │ ├── snippet2.pdf │ ├── snippet3.pdf │ ├── snippet4.pdf │ ├── snippet5.pdf │ ├── snippet6.pdf │ ├── snippet7.pdf │ ├── snippets.pdf │ └── source │ │ ├── make_shareable_code_base.ipynb │ │ └── shareable_code_base.pdf ├── main.bib ├── main.pdf ├── main.tex ├── old.tex └── old │ ├── main-old.bib │ ├── main-old.pdf │ └── main-old.tex ├── pyproject.toml ├── setup.cfg └── tests ├── conftest.py ├── test__environment_and_init.ipynb ├── test_config.ipynb ├── test_core.ipynb ├── test_implementations.ipynb ├── test_ipython_common.ipynb ├── test_ipython_post7.ipynb ├── test_ipython_pre7.ipynb ├── test_parsers.ipynb ├── test_project.ipynb ├── test_regexps.ipynb └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # exclude notebooks from breakdown 2 | *.ipynb linguist-documentation 3 | 4 | # count file with JS functions as JavaScript 5 | davos/implementations/js_functions.py linguist-language=JavaScript 6 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests-colab.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests (Colab) 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'paper/**' 7 | - '.gitattributes' 8 | - '.gitmodules' 9 | - 'CITATION.cff' 10 | - 'LICENSE' 11 | - 'README.md' 12 | workflow_dispatch: 13 | inputs: 14 | fork: 15 | description: 'The fork on which to run the tests' 16 | required: false 17 | default: 'ContextLab' 18 | commit: 19 | description: 'The SHA to checkout (defaults to ref that triggered workflow)' 20 | required: false 21 | default: 'default' 22 | debug_enabled: 23 | description: 'Pause before tests for tmate debugging' 24 | required: false 25 | default: 'false' 26 | 27 | defaults: 28 | run: 29 | shell: bash -leo pipefail {0} 30 | 31 | jobs: 32 | run-tests: 33 | name: "Run Colab CI Tests (Python 3.7, IPython 7.9.0)" 34 | runs-on: ubuntu-latest 35 | # run if triggered by any of the following: 36 | # - a workflow_dispatch event (manual or from trigger-colab-tests-pr workflow) 37 | # - a push to paxtonfitzpatrick/davos 38 | # - a push to ContextLab/davos 39 | # don't run on any pull requests because: 40 | # - pull requests between branches duplicate checks run by pushing to the head branch 41 | # - pull requests between forks are handled by the workflow_dispatch setup 42 | if: > 43 | github.event_name == 'workflow_dispatch' 44 | || (github.event_name == 'push' && github.repository_owner == 'paxtonfitzpatrick') 45 | || (github.event_name == 'push' && github.repository_owner == 'ContextLab') 46 | env: 47 | ARTIFACTS_DIR: ${{ github.workspace }}/artifacts 48 | GMAIL_ADDRESS: ${{ secrets.DAVOS_GMAIL_ADDRESS }} 49 | GMAIL_PASSWORD: ${{ secrets.DAVOS_GMAIL_PASSWORD }} 50 | HEAD_FORK: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.fork || github.repository_owner }} 51 | IPYTHON_VERSION: 7.9.0 52 | NOTEBOOK_TYPE: colab 53 | PYTHON_VERSION: 3.7 54 | RECOVERY_GMAIL_ADDRESS: ${{ secrets.DAVOS_RECOVERY_GMAIL_ADDRESS }} 55 | steps: 56 | - name: Determine fork and commit SHA 57 | run: | 58 | if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then 59 | head_sha=${{ github.event.inputs.commit }} 60 | [[ "$head_sha" == "default" ]] && head_sha="$GITHUB_SHA" 61 | else 62 | head_sha="$GITHUB_SHA" 63 | fi 64 | echo "HEAD_SHA=$head_sha" >> $GITHUB_ENV 65 | 66 | - uses: actions/checkout@v2 67 | with: 68 | repository: ${{ env.HEAD_FORK }}/davos 69 | ref: ${{ env.HEAD_SHA }} 70 | 71 | - name: install miniconda 72 | uses: conda-incubator/setup-miniconda@v2 73 | with: 74 | auto-update-conda: true 75 | auto-activate-base: true 76 | activate-environment: "" 77 | 78 | - name: setup base environment 79 | run: | 80 | # install Python 3.9 (used to run notebooks via selenium, not the tests themselves) 81 | conda install python=3.9 82 | 83 | # install Firefox browser 84 | sudo apt-get install firefox 85 | 86 | # install python packages 87 | pip install pytest==6.2 "selenium==3.141" geckodriver-autoinstaller 88 | 89 | # install geckodriver 90 | driver_path=$(python -c ' 91 | 92 | from pathlib import Path 93 | 94 | import geckodriver_autoinstaller 95 | 96 | driver_src = Path(geckodriver_autoinstaller.install(cwd=True)) 97 | driver_dest = driver_src.rename(driver_src.parents[1].joinpath(driver_src.name)) 98 | driver_src.parent.rmdir() 99 | print(driver_dest) 100 | 101 | ') 102 | 103 | # export path to driver as environment variable 104 | echo "DRIVER_PATH=$driver_path" >> $GITHUB_ENV 105 | 106 | - name: debug runner 107 | if: github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' 108 | uses: mxschmitt/action-tmate@v3 109 | 110 | - name: run pytest 111 | id: run-pytest 112 | run: pytest -sv tests/ 113 | 114 | - name: upload selenium error artifacts 115 | if: failure() && steps.run-pytest.outcome == 'failure' 116 | uses: actions/upload-artifact@v2 117 | with: 118 | path: ${{ env.ARTIFACTS_DIR }} 119 | if-no-files-found: ignore 120 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests-jupyter.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests (Jupyter) 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'paper/**' 7 | - '.gitattributes' 8 | - '.gitmodules' 9 | - 'CITATION.cff' 10 | - 'LICENSE' 11 | - 'README.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'paper/**' 15 | - '.gitattributes' 16 | - '.gitmodules' 17 | - 'CITATION.cff' 18 | - 'LICENSE' 19 | - 'README.md' 20 | workflow_dispatch: 21 | inputs: 22 | debug_enabled: 23 | description: 'Pause before tests for tmate debugging' 24 | required: false 25 | default: 'false' 26 | 27 | defaults: 28 | run: 29 | shell: bash -leo pipefail {0} 30 | 31 | jobs: 32 | run-tests: 33 | name: "Run Jupyter CI tests (Python ${{ matrix.python-version }}, IPython ${{ matrix.ipython-version }})" 34 | runs-on: ubuntu-latest 35 | # only run on pull requests between forks to avoid duplicate runs with 'push' event 36 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | include: 41 | # for each Python version, test with earliest and latest supported IPython version 42 | - python-version: 3.6 43 | ipython-version: 5.5.0 # earliest version supported by davos 44 | - python-version: 3.6 45 | ipython-version: 7.16 # latest version to support Python 3.6 46 | - python-version: 3.7 47 | ipython-version: 5.5.0 # earliest version supported by davos 48 | - python-version: 3.7 49 | ipython-version: 7.31 # latest version to support Python 3.7 50 | - python-version: 3.8 51 | ipython-version: 7.3.0 # earliest version to support Python 3.8 52 | - python-version: 3.8 53 | ipython-version: 8.12.2 # latest version to support Python 3.8 54 | - python-version: 3.9 55 | ipython-version: 7.15 # earliest version to support Python 3.9 56 | - python-version: 3.9 57 | ipython-version: latest 58 | - python-version: '3.10' 59 | ipython-version: 8.0 # earliest version to support Python 3.10 60 | - python-version: '3.10' 61 | ipython-version: latest 62 | - python-version: 3.11 63 | ipython-version: 8.8.0 # earliest version to support Python 3.11 64 | - python-version: 3.11 65 | ipython-version: latest 66 | env: 67 | HEAD_FORK: ${{ github.repository_owner }} 68 | HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} 69 | IPYTHON_VERSION: ${{ matrix.ipython-version }} 70 | # MAMBA_NO_BANNER: 1 71 | NOTEBOOK_TYPE: jupyter 72 | PYTHON_VERSION: ${{ matrix.python-version }} 73 | steps: 74 | - uses: actions/checkout@v2 75 | with: 76 | ref: ${{ env.HEAD_SHA }} 77 | 78 | - name: install miniconda 79 | uses: conda-incubator/setup-miniconda@v2 80 | with: 81 | auto-update-conda: true 82 | # miniforge-variant: Mambaforge 83 | # miniforge-version: 4.13.0-1 84 | # use-mamba: true 85 | # channels: conda-forge,defaults 86 | # channel-priority: strict 87 | auto-activate-base: true 88 | activate-environment: "" 89 | 90 | - name: setup base environment 91 | run: | 92 | # install Python 3.9 (used to run notebooks via selenium, not the tests themselves) 93 | conda install python=3.9 94 | 95 | # install Firefox browser: 96 | # - remove pre-installed snap package 97 | [[ -x "$(command -v snap)" ]] && sudo snap remove firefox 98 | 99 | # - add Mozilla PPA as a repository 100 | sudo add-apt-repository -y ppa:mozillateam/ppa 101 | 102 | # - alter Firefox package priority to prefer the deb package from the PPA 103 | echo ' 104 | Package: * 105 | Pin: release o=LP-PPA-mozillateam 106 | Pin-Priority: 1001 107 | ' | sudo tee /etc/apt/preferences.d/mozilla-firefox 108 | 109 | # - install the proper version 110 | sudo apt install -y --allow-downgrades firefox 111 | 112 | # install python packages 113 | pip install notebook \ 114 | pytest==6.2 \ 115 | selenium==3.141 \ 116 | geckodriver-autoinstaller \ 117 | ipykernel==5.0.0 \ 118 | "jupyter_client<=7.3.2" \ 119 | "tornado<=6.1" \ 120 | "urllib3<2.0" 121 | 122 | # install geckodriver 123 | driver_path=$(python -c ' 124 | 125 | from pathlib import Path 126 | 127 | import geckodriver_autoinstaller 128 | 129 | driver_src = Path(geckodriver_autoinstaller.install(cwd=True)) 130 | driver_dest = driver_src.rename(driver_src.parents[1].joinpath(driver_src.name)) 131 | driver_src.parent.rmdir() 132 | print(driver_dest) 133 | 134 | ') 135 | 136 | # export path to driver as environment variable 137 | echo "DRIVER_PATH=$driver_path" >> $GITHUB_ENV 138 | 139 | - name: setup notebook kernel environment 140 | run: | 141 | # create & activate kernel environment 142 | conda create -n kernel-env python=$PYTHON_VERSION 143 | conda activate kernel-env 144 | 145 | # install davos & various test requirements in kernel environment 146 | [[ "$PYTHON_VERSION" =~ ^3.(6|7)$ ]] && pip install typing-extensions 147 | pip install "ipykernel==5.0.0" \ 148 | ipython-genutils \ 149 | requests \ 150 | fastdtw==0.3.4 \ 151 | tqdm==4.41.1 \ 152 | "numpy<=1.23.5" 153 | [[ "$PYTHON_VERSION" =~ ^3.11$ ]] && pip install scipy==1.11.1 || pip install "scipy<=1.7.3" 154 | if [[ "$IPYTHON_VERSION" == "latest" ]]; then 155 | pip install --upgrade IPython 156 | else 157 | pip install IPython==$IPYTHON_VERSION 158 | fi 159 | pip install . 160 | 161 | # make environment available as a jupyter kernel 162 | python -m ipykernel install --prefix=/usr/share/miniconda --name=kernel-env 163 | conda deactivate 164 | 165 | - name: record environment 166 | run: | 167 | { 168 | # get packages in base environment 169 | printf '=%.0s' {1..20}; printf ' base '; printf '=%.0s' {1..20}; printf '\n' 170 | conda env export -n base 171 | 172 | # get packages in kernel environment 173 | printf '=%.0s' {1..20}; printf ' kernel-env '; printf '=%.0s' {1..20}; printf '\n' 174 | conda env export -n kernel-env 175 | 176 | # get firefox browser & webdriver version 177 | printf '=%.0s' {1..20}; printf ' firefox '; printf '=%.0s' {1..20}; printf '\n' 178 | firefox --full-version 179 | $DRIVER_PATH --version 180 | 181 | # get jupyter kernels available in base environment 182 | printf '=%.0s' {1..20}; printf ' jupyter kernels '; printf '=%.0s' {1..20}; printf '\n' 183 | jupyter kernelspec list 184 | } > ${{ github.workspace }}/environment-info.txt 185 | 186 | cat ${{ github.workspace }}/environment-info.txt 187 | 188 | - name: launch Jupyter server 189 | run: jupyter notebook --no-browser --port=8888 --NotebookApp.token= & 190 | 191 | - name: debug runner 192 | if: | 193 | github.event_name == 'workflow_dispatch' 194 | && github.event.inputs.debug_enabled == 'true' 195 | uses: mxschmitt/action-tmate@v3 196 | 197 | - name: run pytest 198 | id: run-pytest 199 | run: pytest -sv tests/ 200 | 201 | - name: upload artifacts on failure 202 | uses: actions/upload-artifact@v3 203 | if: failure() 204 | with: 205 | name: artifacts-python${{ matrix.python-version }}-ipython${{ matrix.ipython-version }} 206 | path: | 207 | ${{ github.workspace }}/tests/*.ipynb 208 | ${{ github.workspace }}/environment-info.txt 209 | -------------------------------------------------------------------------------- /.github/workflows/request-colab-tests.yml: -------------------------------------------------------------------------------- 1 | name: Request Colab Tests for PR 2 | 3 | on: 4 | pull_request_target: 5 | paths-ignore: 6 | - 'paper/**' 7 | - '.gitattributes' 8 | - '.gitmodules' 9 | - 'CITATION.cff' 10 | - 'LICENSE' 11 | - 'README.md' 12 | 13 | defaults: 14 | run: 15 | shell: bash -leo pipefail {0} 16 | 17 | jobs: 18 | request-colab-tests: 19 | name: "Request approval to run Colab tests" 20 | runs-on: ubuntu-latest 21 | # run on pull requests from forks to the base repository 22 | if: github.repository_owner == 'ContextLab' && github.event.pull_request.head.user.login != 'ContextLab' 23 | steps: 24 | - name: Comment on PR 25 | uses: actions/github-script@v4 26 | with: 27 | script: | 28 | const headFork = context.payload.pull_request.head.user.login, 29 | headSha = context.payload.pull_request.head.sha, 30 | headShaShort = headSha.substr(0, 7), 31 | headCommitUrl = `https://github.com/${headFork}/davos/tree/${headSha}`, 32 | commentBody = `@paxtonfitzpatrick Run Colab CI tests on [${headFork}/davos@${headShaShort}](${headCommitUrl})?`; 33 | 34 | console.log(`pull request opened from ${headFork}/davos to ContextLab/davos (#${context.issue.number})`); 35 | console.log('posting comment from bot'); 36 | 37 | await github.issues.createComment({ 38 | issue_number: context.issue.number, 39 | owner: context.repo.owner, 40 | repo: context.repo.repo, 41 | body: commentBody 42 | }); 43 | -------------------------------------------------------------------------------- /.github/workflows/trigger-colab-tests-pr.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Colab CI Tests for Pull Request 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | defaults: 8 | run: 9 | shell: bash -leo pipefail {0} 10 | 11 | jobs: 12 | trigger-tests-from-comment: 13 | name: "Trigger Colab CI Tests from PR Comment" 14 | runs-on: ubuntu-latest 15 | # run if all of the following are true: 16 | # - the comment was made on a pull request (rather than an issue) 17 | # - the base of the pull request is ContextLab/davos 18 | # - the head of the pull request is a different fork (rather than just a branch) 19 | # - the comment was made by paxtonfitzpatrick 20 | # - the github-actions bot was tagged at the beginning of the comment 21 | if: > 22 | github.event.issue.pull_request 23 | && github.repository_owner == 'ContextLab' 24 | && github.event.issue.user.login != 'ContextLab' 25 | && (github.event.comment.user.login == 'paxtonfitzpatrick' || github.event.comment.user.login == 'jeremymanning') 26 | && startsWith(github.event.comment.body, '@github-actions ') 27 | steps: 28 | - name: Check Whether Comment Triggers Workflow 29 | id: check-comment 30 | uses: actions/github-script@v4 31 | with: 32 | script: | 33 | console.log(`Possible dispatch comment by ${context.payload.comment.user.login} on pull PR #${context.issue.number}`) 34 | 35 | // keywords that trigger Colab CI tests 36 | const approveStrings = ['approve', 'run', 'yes', '👍'], 37 | // comment body with @github-actions bot tag removed 38 | commentBody = context.payload.comment.body.replace(/@github-actions\s+/, ''); 39 | 40 | console.log(`parsing possible dispatch trigger comment: 41 | ${context.payload.comment.body} 42 | for dispatch keywords: 43 | ${approveStrings}`) 44 | 45 | // if the comment contains any of the keyword strings 46 | if (approveStrings.some(str => commentBody.includes(str))) { 47 | console.log('comment contains dispatch keyword'); 48 | try { 49 | // request pull_request event from rest API 50 | console.log('getting data for pull request...'); 51 | const response = await github.pulls.get({ 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | pull_number: context.issue.number 55 | }); 56 | const pullRequest = response.data; 57 | // set outputs for use in next step 58 | console.log('setting outputs...') 59 | core.setOutput('head_fork', pullRequest.head.user.login); 60 | core.setOutput('head_sha', pullRequest.head.sha); 61 | core.setOutput('trigger_tests', 'true'); 62 | console.log(`head_fork: ${pullRequest.head.user.login}`); 63 | console.log(`head_sha: ${pullRequest.head.sha}`); 64 | } catch(error) { 65 | core.setFailed(`Failed to get pull_request event from issue_comment: ${error}`); 66 | } 67 | // react to comment with thumbs-up to confirm 68 | console.log('adding reaction to triggering comment...'); 69 | await github.reactions.createForIssueComment({ 70 | owner: context.repo.owner, 71 | repo: context.repo.repo, 72 | comment_id: context.payload.comment.id, 73 | content: 'rocket' 74 | }); 75 | } else { 76 | console.log('comment does not trigger tests'); 77 | core.setOutput('trigger_tests', 'false'); 78 | } 79 | 80 | - name: Dispatch Colab CI Test Workflow 81 | if: steps.check-comment.outputs.trigger_tests == 'true' 82 | env: 83 | HEAD_FORK: ${{ steps.check-comment.outputs.head_fork }} 84 | HEAD_SHA: ${{ steps.check-comment.outputs.head_sha }} 85 | uses: actions/github-script@v4 86 | with: 87 | github-token: ${{ secrets.API_PAT }} 88 | script: | 89 | console.log(`dispatching Colab CI test workflow... 90 | owner: ${context.repo.owner}, 91 | repo: ${context.repo.repo}, 92 | ref: ${context.ref}, 93 | inputs.fork: ${process.env.HEAD_FORK}, 94 | inputs.commit: ${process.env.HEAD_SHA} 95 | `); 96 | const dispatchResponse = await github.actions.createWorkflowDispatch({ 97 | owner: context.repo.owner, 98 | repo: context.repo.repo, 99 | workflow_id: 'ci-tests-colab.yml', 100 | ref: context.ref, 101 | inputs: { 102 | fork: process.env.HEAD_FORK, 103 | commit: process.env.HEAD_SHA, 104 | debug_enabled: 'false' 105 | } 106 | }); 107 | console.log('dispatch response:'); 108 | console.log(dispatchResponse) 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac Finder display config 2 | *.DS_Store 3 | 4 | # PyCharm & VS Code project config dirs 5 | *.idea/ 6 | *.vscode/ 7 | 8 | # Python bytecode 9 | *__pycache__/ 10 | 11 | # IPython notebook checkpoints 12 | *.ipynb_checkpoints/ 13 | 14 | # Package distribution dirs for wheels & eggs 15 | dist/ 16 | build/ 17 | davos.egg-info/ 18 | 19 | # Mypy daemon status file 20 | .dmypy.json 21 | 22 | # Firefox WebDriver & log file for tests 23 | geckodriver 24 | **/geckodriver.log 25 | 26 | # scripts & notebooks for scratch/debugging 27 | scratch/ 28 | 29 | # LaTeX generated files 30 | paper/**/*.aux 31 | paper/**/*.bbl 32 | paper/**/*.blg 33 | paper/**/*.log 34 | paper/**/*.out 35 | paper/**/*.spl 36 | paper/**/*.synctex.gz 37 | 38 | # Illustrator source files 39 | paper/figs/*.ai 40 | paper/main.fdb_latexmk 41 | paper/main.fls 42 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "paper/CDL-bibliography"] 2 | path = paper/CDL-bibliography 3 | url = https://github.com/ContextLab/CDL-bibliography.git 4 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | authors: 2 | - given-names: "Paxton C." 3 | family-names: Fitzpatrick 4 | email: "Paxton.C.Fitzpatrick.GR@Dartmouth.edu" 5 | affiliation: Dartmouth College 6 | orcid: "https://orcid.org/0000-0003-0205-3088" 7 | - given-names: "Jeremy R." 8 | family-names: Manning 9 | email: "Jeremy.R.Manning@Dartmouth.edu" 10 | affiliation: "Dartmouth College" 11 | orcid: "https://orcid.org/0000-0001-7613-4732" 12 | cff-version: 1.2.0 13 | date-released: "2023-10-01" 14 | keywords: 15 | - Reproducibility 16 | - "Open science" 17 | - Python 18 | - "Jupyter notebook" 19 | - IPython 20 | - "Google Colab" 21 | - "Package management" 22 | license: MIT 23 | license-url: "https://github.com/ContextLab/davos/blob/main/LICENSE" 24 | message: "If you use this software, please cite the associated paper (see preferred-citation)." 25 | preferred-citation: 26 | authors: 27 | - given-names: "Paxton C." 28 | family-names: Fitzpatrick 29 | email: "Paxton.C.Fitzpatrick.GR@Dartmouth.edu" 30 | affiliation: Dartmouth College 31 | orcid: "https://orcid.org/0000-0003-0205-3088" 32 | website: "https://paxtonfitzpatrick.me" 33 | - given-names: "Jeremy R." 34 | family-names: Manning 35 | email: "Jeremy.R.Manning@Dartmouth.edu" 36 | affiliation: "Dartmouth College" 37 | orcid: "https://orcid.org/0000-0001-7613-4732" 38 | website: "https://www.context-lab.com" 39 | date-published: "2023-10-01" 40 | doi: 10.48550/arXiv.2211.15445 41 | journal: arXiv 42 | keywords: 43 | - "Other Computer Science (cs.OH)" 44 | - "FOS: Computer and information sciences" 45 | month: 10 46 | title: "$\\texttt{Davos}$: a Python package \"smuggler\" for constructing lightweight reproducible notebooks" 47 | type: article 48 | url: "https://arxiv.org/abs/2211.15445" 49 | year: "2023" 50 | repository-code: "https://github.com/ContextLab/davos" 51 | repository-artifact: "https://pypi.org/project/davos/" 52 | title: davos 53 | type: software 54 | version: v0.2.3 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Contextual Dynamics Laboratory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /davos/__init__.pyi: -------------------------------------------------------------------------------- 1 | from pathlib import PosixPath 2 | from types import ModuleType 3 | from typing import Final, Literal 4 | from davos.core.config import DavosConfig 5 | from davos.core.project import AbstractProject, ConcreteProject 6 | 7 | __all__ = list[Literal['DAVOS_CONFIG_DIR', 'DAVOS_PROJECT_DIR', 'config', 'configure', 'get_project', 'Project', 8 | 'prune_projects', 'require_pip', 'require_python', 'smuggle', 'use_default_project']] 9 | __class__: ConfigProxyModule 10 | __version__: Final[str] 11 | 12 | config: DavosConfig 13 | 14 | class ConfigProxyModule(ModuleType, DavosConfig): 15 | @property 16 | def all_projects(self) -> list[AbstractProject | ConcreteProject]: ... 17 | 18 | def configure(*, active: bool = ..., auto_rerun: bool = ..., confirm_install: bool = ..., noninteractive: bool = ..., 19 | pip_executable: PosixPath | str = ..., project: ConcreteProject | PosixPath | str | None = ..., 20 | suppress_stdout: bool = ...) -> None: ... 21 | def require_pip(version_spec: str, warn: bool | None = ..., extra_msg: str | None = ..., 22 | prereleases: bool | None = ...) -> None: ... 23 | def require_python(version_spec: str, warn: bool | None = ..., extra_msg: str | None = ..., 24 | prereleases: bool | None = ...) -> None: ... 25 | -------------------------------------------------------------------------------- /davos/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/davos/core/__init__.py -------------------------------------------------------------------------------- /davos/core/__init__.pyi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/davos/core/__init__.pyi -------------------------------------------------------------------------------- /davos/core/config.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Iterable 2 | from pathlib import PosixPath 3 | from pprint import PrettyPrinter 4 | from typing import ClassVar, Generic, Literal, NoReturn, Protocol, Type, TypeVar 5 | from google.colab._shell import Shell # type: ignore 6 | from IPython.core.interactiveshell import InteractiveShell # type: ignore 7 | from davos.core.project import AbstractProject, ConcreteProject 8 | 9 | __all__ = list[Literal['DavosConfig']] 10 | 11 | _Environment = Literal['Colaboratory', 'IPython<7.0', 'IPython>=7.0', 'Python'] 12 | _I= TypeVar('_I', bound=Iterable) 13 | _DC = TypeVar('_DC', bound=DavosConfig) 14 | IpythonShell = InteractiveShell | Shell 15 | 16 | class _IpyShowSyntaxErrorPre7(Protocol): 17 | def __call__(self, filename: str | None = ...) -> None: ... 18 | 19 | class _IpyShowSyntaxErrorPost7(Protocol): 20 | def __call__(self, filename: str | None = ..., running_compile_code: bool = ...) -> None: ... 21 | 22 | class SingletonConfig(type, Generic[_DC]): 23 | # ignoring an overly strict mypy check that doesn't account for this 24 | # use case. see https://github.com/python/mypy/issues/5144 25 | __instance: ClassVar[_DC | None] # type: ignore[misc] 26 | def __call__(cls: Type[_DC], *args: object, **kwargs: object) -> _DC: ... # type: ignore[misc] 27 | 28 | class DavosConfig(metaclass=SingletonConfig): 29 | _active: bool 30 | _auto_rerun: bool 31 | _conda_avail: bool | None 32 | _conda_env: str | None 33 | _conda_envs_dirs: dict[str, str] | None 34 | _confirm_install: bool 35 | _default_pip_executable: str 36 | _environment: _Environment 37 | _ipy_showsyntaxerror_orig: _IpyShowSyntaxErrorPre7 | _IpyShowSyntaxErrorPost7 | None 38 | _ipython_shell: IpythonShell | None 39 | _jupyter_interface: Literal['notebook', 'lab'] 40 | _noninteractive: bool 41 | _pip_executable: str 42 | _project: AbstractProject | ConcreteProject | None 43 | _repr_formatter: PrettyPrinter 44 | _smuggled: dict[str, str] 45 | _stdlib_modules: frozenset[str] 46 | _suppress_stdout: bool 47 | @staticmethod 48 | def __mock_sorted(__iterable: _I, key: Callable | None = ..., reverse: bool = ...) -> _I: ... 49 | def __init__(self) -> None: ... 50 | def __repr__(self) -> str: ... 51 | @property 52 | def active(self) -> bool: ... 53 | @active.setter 54 | def active(self, state: bool) -> None: ... 55 | @property 56 | def auto_rerun(self) -> bool: ... 57 | @auto_rerun.setter 58 | def auto_rerun(self, value: bool) -> None: ... 59 | @property 60 | def conda_avail(self) -> bool: ... 61 | @conda_avail.setter 62 | def conda_avail(self, _: object) -> NoReturn: ... 63 | @property 64 | def conda_env(self) -> str | None: ... 65 | @conda_env.setter 66 | def conda_env(self, new_env: str) -> None: ... 67 | @property 68 | def conda_envs_dirs(self) -> dict[str, str] | None: ... 69 | @conda_envs_dirs.setter 70 | def conda_envs_dirs(self, _: object) -> NoReturn: ... 71 | @property 72 | def confirm_install(self) -> bool: ... 73 | @confirm_install.setter 74 | def confirm_install(self, value: bool) -> None: ... 75 | @property 76 | def environment(self) -> _Environment: ... 77 | @environment.setter 78 | def environment(self, _: object) -> NoReturn: ... 79 | @property 80 | def ipython_shell(self) -> IpythonShell | None: ... 81 | @ipython_shell.setter 82 | def ipython_shell(self, _: object) -> NoReturn: ... 83 | @property 84 | def noninteractive(self) -> bool: ... 85 | @noninteractive.setter 86 | def noninteractive(self, value: bool) -> None: ... 87 | @property 88 | def pip_executable(self) -> str: ... 89 | @pip_executable.setter 90 | def pip_executable(self, exe_path: PosixPath | str) -> None: ... 91 | @property 92 | def project(self) -> AbstractProject | ConcreteProject: ... 93 | @project.setter 94 | def project(self, proj: AbstractProject | ConcreteProject | PosixPath | str | None) -> None: ... 95 | @property 96 | def smuggled(self) -> dict[str, str]: ... 97 | @smuggled.setter 98 | def smuggled(self, _: object) -> NoReturn: ... 99 | @property 100 | def suppress_stdout(self) -> bool: ... 101 | @suppress_stdout.setter 102 | def suppress_stdout(self, value: bool) -> None: ... 103 | def _find_default_pip_executable(self) -> str: ... 104 | 105 | def _block_greedy_ipython_completer() -> None: ... 106 | def _get_jupyter_interface() -> Literal['notebook', 'lab']: ... 107 | def _get_stdlib_modules() -> frozenset[str]: ... 108 | -------------------------------------------------------------------------------- /davos/core/core.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from contextlib import AbstractContextManager 3 | from io import TextIOBase 4 | from types import TracebackType 5 | from typing import Generic, Literal, NoReturn, overload, Protocol, Type, TypeVar, TypedDict 6 | 7 | __all__ = list[Literal['capture_stdout', 'check_conda', 'get_previously_imported_pkgs', 'handle_alternate_pip_executable', 8 | 'import_name', 'Onion', 'parse_line', 'prompt_input', 'run_shell_command', 'use_project', 'smuggle']] 9 | 10 | _Exc = TypeVar('_Exc', bound=BaseException) 11 | _Streams = TypeVar('_Streams', bound=tuple[TextIOBase, ...]) 12 | _InstallerName = Literal['conda', 'pip'] 13 | 14 | class SmuggleFunc(Protocol): 15 | def __call__(self, name: str, as_: str | None = ..., installer: Literal['conda', 'pip'] = ..., args_str: str = ..., 16 | installer_kwargs: PipInstallerKwargs | None = ... ) -> None: ... 17 | 18 | class PipInstallerKwargs(TypedDict, total=False): 19 | abi: str 20 | cache_dir: str 21 | cert: str 22 | client_cert: str 23 | compile: bool 24 | disable_pip_version_check: bool 25 | editable: bool 26 | exists_action: Literal['a', 'abort', 'b', 'backup', 'i', 'ignore', 's', 'switch', 'w', 'wipe'] 27 | extra_index_url: list[str] 28 | find_links: list[str] 29 | force_reinstall: bool 30 | global_option: list[str] 31 | ignore_installed: bool 32 | ignore_requires_python: bool 33 | implementation: Literal['cp', 'ip', 'jy', 'pp', 'py'] 34 | index_url: str 35 | install_option: list[str] 36 | isolated: bool 37 | log: str 38 | no_binary: list[str] 39 | no_build_isolation: bool 40 | no_cache_dir: bool 41 | no_clean: bool 42 | no_color: bool 43 | no_compile: bool 44 | no_deps: bool 45 | no_index: bool 46 | no_python_version_warning: bool 47 | no_use_pep517: bool 48 | no_warn_conflicts: bool 49 | no_warn_script_location: bool 50 | only_binary: list[str] 51 | platform: str 52 | pre: bool 53 | prefer_binary: bool 54 | prefix: str 55 | progress_bar: Literal['ascii', 'emoji', 'off', 'on', 'pretty'] 56 | python_version: str 57 | require_hashes: bool 58 | retries: int 59 | root: str 60 | src: str 61 | target: str 62 | timeout: float 63 | trusted_host: str 64 | upgrade: bool 65 | upgrade_strategy: Literal['eager', 'only-if-needed'] 66 | use_deprecated: str 67 | use_feature: str 68 | use_pep517: bool 69 | user: bool 70 | verbosity: Literal[-3, -2, -1, 0, 1, 2, 3] 71 | 72 | class capture_stdout(Generic[_Streams]): 73 | closing: bool 74 | streams: _Streams 75 | sys_stdout_write: Callable[[str], int | None] 76 | def __init__(self, *streams: _Streams, closing: bool = ...) -> None: ... 77 | def __enter__(self) -> _Streams: ... 78 | @overload 79 | def __exit__(self, exc_type: None, exc_value: None, exc_tb: None) -> None: ... 80 | @overload 81 | def __exit__(self, exc_type: Type[_Exc], exc_value: _Exc, exc_tb: TracebackType) -> bool | None: ... 82 | def _write(self, data: str) -> None: ... 83 | 84 | def check_conda() -> None: ... 85 | def get_previously_imported_pkgs(install_cmd_stdout: str, installer: _InstallerName) -> list[str]: ... 86 | def handle_alternate_pip_executable(installed_name: str) -> AbstractContextManager[None]: ... 87 | def import_name(name: str) -> object: ... 88 | 89 | class Onion: 90 | args_str: str 91 | build: str | None 92 | cache_key: str 93 | import_name: str 94 | install_name: str 95 | install_package: Callable[[], str] 96 | installer: _InstallerName 97 | installer_kwargs: PipInstallerKwargs 98 | is_editable: bool 99 | verbosity: Literal[-3, -2, -1, 0, 1, 2, 3] 100 | version_spec: str 101 | @staticmethod 102 | def parse_onion(onion_text: str) -> tuple[str, str, PipInstallerKwargs]: ... 103 | def __init__(self, package_name: str, installer: _InstallerName, args_str: str, 104 | **installer_kwargs: bool | float | str | list[str]) -> None: ... 105 | @property 106 | def install_cmd(self) -> str: ... 107 | @property 108 | def is_installed(self) -> bool: ... 109 | def _conda_install_package(self) -> NoReturn: ... 110 | def _pip_install_package(self) -> str: ... 111 | 112 | def parse_line(line: str) -> str: ... 113 | def prompt_input(prompt: str, default: Literal['n', 'no', 'y', 'yes'] | None = ..., 114 | interrupt: Literal['n', 'no', 'y', 'yes'] | None = ...) -> bool: ... 115 | def run_shell_command(command: str, live_stdout: bool | None = ...) -> str: ... 116 | def use_project(smuggle_func: SmuggleFunc) -> SmuggleFunc: ... 117 | def smuggle(name: str, as_: str | None = ..., installer: _InstallerName = ..., args_str: str = ..., 118 | installer_kwargs: PipInstallerKwargs | None = ...) -> None: ... 119 | -------------------------------------------------------------------------------- /davos/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """davos-specific exception classes.""" 2 | 3 | 4 | __all__ = [ 5 | 'DavosError', 6 | 'DavosConfigError', 7 | 'DavosParserError', 8 | 'OnionParserError', 9 | 'OnionArgumentError', 10 | 'ParserNotImplementedError', 11 | 'DavosProjectError', 12 | 'ProjectNotebookNotFoundError', 13 | 'SmugglerError', 14 | 'InstallerError' 15 | ] 16 | 17 | 18 | from argparse import ArgumentError 19 | from shutil import get_terminal_size 20 | from subprocess import CalledProcessError 21 | from textwrap import fill, indent 22 | 23 | import IPython 24 | 25 | 26 | class DavosError(Exception): 27 | """Base class for all `davos` library exceptions.""" 28 | 29 | 30 | class DavosConfigError(DavosError): 31 | """Class for errors related to the `davos.config` object.""" 32 | 33 | def __init__(self, field, msg): 34 | """ 35 | Parameters 36 | ---------- 37 | field : str 38 | The config field about which the exception should be raised 39 | msg : str 40 | The specific error message 41 | """ 42 | self.field = field 43 | self.msg = msg 44 | super().__init__(f"'davos.config.{field}': {msg}") 45 | 46 | 47 | class DavosParserError(SyntaxError, DavosError): 48 | """ 49 | Base class for errors raised during the pre-execution parsing phase. 50 | 51 | Any `davos` exception classes related to user input-parsing step 52 | must inherit from this class in order to work in IPython 53 | environments. 54 | 55 | Notes 56 | ----- 57 | Since the raw cell contents haven't been passed to the Python 58 | compiler yet, IPython doesn't allow input transformers to raise any 59 | exceptions other than `SyntaxError` during the input transformation 60 | phase. All others will cause the cell to hang indefinitely, meaning 61 | all `davos` library exception classes related to the parsing phrase 62 | must inherit from `SyntaxError` in order to work. 63 | """ 64 | 65 | def __init__( 66 | self, 67 | msg=None, 68 | target_text=None, 69 | target_offset=1 70 | ): 71 | """ 72 | Parameters 73 | ---------- 74 | msg : str, optional 75 | The error message to be displayed. 76 | target_text : str, optional 77 | The text of the code responsible for the error, to be 78 | displayed in Python's `SyntaxError`-specific traceback 79 | format. Typically, this is the full text of the line on 80 | which the error occurred, with whitespace stripped. If 81 | `None` (default), no text is shown and `target_offset` has 82 | no effect. 83 | target_offset : int, optional 84 | The (*1-indexed*) column offset from the beginning of 85 | `target_text` where the error occurred. Defaults to `1` (the 86 | first character in `target_text`). 87 | """ 88 | if target_text is None or IPython.version_info[0] >= 7: 89 | # flot is a 4-tuple of (filename, lineno, offset, text) 90 | # passed to the SyntaxError constructor. 91 | flot = (None, None, None, None) 92 | else: 93 | from davos import config 94 | xform_manager = config.ipython_shell.input_transformer_manager 95 | # number of "real" lines in the current cell before the 96 | # start of the current "chunk" (potentially multi-line 97 | # python statement) being parsed 98 | n_prev_lines = len(xform_manager.source.splitlines()) 99 | all_cell_lines = xform_manager.source_raw.splitlines() 100 | # remaining cell lines starting with the beginning of the 101 | # current "chunk" (no way to isolate just the current chunk) 102 | rest_cell_lines = all_cell_lines[n_prev_lines:] 103 | for ix, line in enumerate(rest_cell_lines, start=1): 104 | if target_text in line: 105 | lineno = n_prev_lines + ix 106 | offset = line.index(target_text) + target_offset 107 | break 108 | else: 109 | offset = None 110 | lineno = None 111 | # leave lineno as None so IPython will fill it in as 112 | # "" 113 | flot = (None, lineno, offset, target_text) 114 | super().__init__(msg, flot) 115 | 116 | 117 | class OnionParserError(DavosParserError): 118 | """Class for errors related to parsing the onion comment syntax.""" 119 | 120 | 121 | class OnionArgumentError(ArgumentError, OnionParserError): 122 | """ 123 | Class for errors related to arguments provided via an onion comment. 124 | 125 | This exception class inherits from both `OnionParserError` and 126 | `argparse.ArgumentError`. It functions as an onion comment-specific 127 | analog of `argparse.ArgumentError`, whose key distinction is that 128 | it can be raised during IPython's pre-execution phase (due to 129 | inheriting from `DavosParserError`). Instances of this exception can 130 | be expected to support attributes defined on both parents. 131 | """ 132 | 133 | def __init__(self, msg, argument=None, onion_txt=None): 134 | """ 135 | Parameters 136 | ---------- 137 | msg : str or None 138 | The error message to be displayed. 139 | argument : str, optional 140 | The argument responsible for the error. if `None` (default), 141 | determines the argument name from `msg`, which will be the 142 | error message from an `argparse.ArgumentError` instance. 143 | onion_txt : str, optional 144 | The text of the installer arguments from onion comment in 145 | which the error occurred (i.e., the full onion comment with 146 | `# :` removed). If `None` (default), the error 147 | will not be displayed in Python's `SyntaxError`-specific 148 | traceback format. 149 | """ 150 | if ( 151 | msg is not None and 152 | argument is None and 153 | msg.startswith('argument ') 154 | ): 155 | split_msg = msg.split() 156 | argument = split_msg[1].rstrip(':') 157 | msg = ' '.join(split_msg[2:]) 158 | if (onion_txt is not None) and (argument is not None): 159 | # default sorting is alphabetical where '--a' comes before 160 | # '-a', so long option name will always be checked first, 161 | # which is what we want 162 | for aname in sorted(argument.split('/')): 163 | if aname in onion_txt: 164 | target_offset = onion_txt.index(aname) 165 | break 166 | else: 167 | target_offset = 0 168 | else: 169 | target_offset = 0 170 | # both `argparse.ArgumentError` and `SyntaxError` are 171 | # non-cooperative, so need to initialize them separately rather 172 | # than just running through the MRO via a call to super() 173 | ArgumentError.__init__(self, argument=None, message=msg) 174 | OnionParserError.__init__(self, msg=msg, target_text=onion_txt, 175 | target_offset=target_offset) 176 | self.argument_name = argument 177 | 178 | 179 | class ParserNotImplementedError(OnionParserError, NotImplementedError): 180 | """ 181 | Class for errors related to yet-to-be-implemented onion parsers. 182 | 183 | This exception is an onion comment-specific subclass of the built-in 184 | `NotImplementedError` that also inherits from `OnionParserError`, 185 | allowing it to be raised during IPython's pre-execution phase (due 186 | to inheriting from `DavosParserError`). This error is specifically 187 | raised when a user specifies an installer program (via an onion 188 | comment) whose command line parser has not yet been added to `davos` 189 | """ 190 | 191 | 192 | class DavosProjectError(DavosError): 193 | """Base class for errors related to `davos.Project` objects.""" 194 | 195 | 196 | class ProjectNotebookNotFoundError(DavosProjectError, FileNotFoundError): 197 | """Class for errors related to projects missing associated notebooks.""" 198 | 199 | 200 | class SmugglerError(DavosError): 201 | """Base class for errors raised during the smuggle phase.""" 202 | 203 | 204 | class TheNightIsDarkAndFullOfErrors(SmugglerError): 205 | """A little Easter egg for anyone who tries to `smuggle davos`.""" 206 | 207 | 208 | class InstallerError(SmugglerError, CalledProcessError): 209 | """ 210 | Class for errors related to the installer program. 211 | 212 | This exception is raised when the installer program itself (rather 213 | than `davos`) encounters an error (e.g., failure to connect to 214 | upstream package repository, find package with a given name, resolve 215 | local environment, etc.). 216 | """ 217 | 218 | @classmethod 219 | def from_error(cls, cpe, show_output=None): 220 | """ 221 | Create a class instance from a `subprocess.CalledProcessError`. 222 | 223 | Parameters 224 | ---------- 225 | cpe : subprocess.CalledProcessError 226 | The exception from which to create the `InstallerError` 227 | show_output : bool, optional 228 | Whether or not to include the failed command's stdout and/or 229 | stderr in the error message. If `None` (default), 230 | stdout/stderr will be displayed if the `suppress_stdout` 231 | field of `davos.config` is currently set to `True` (i.e., 232 | stdout would have been suppressed during execution). 233 | 234 | Returns 235 | ------- 236 | InstallerError 237 | The exception instance. 238 | """ 239 | if not isinstance(cpe, CalledProcessError): 240 | raise TypeError( 241 | "InstallerError.from_error() requires a " 242 | f"'subprocess.CalledProcessError' instance, not a {type(cpe)}" 243 | ) 244 | return cls(returncode=cpe.returncode, 245 | cmd=cpe.cmd, 246 | output=cpe.output, 247 | stderr=cpe.stderr, 248 | show_output=show_output) 249 | 250 | def __init__( 251 | self, 252 | returncode, 253 | cmd, 254 | output=None, 255 | stderr=None, 256 | show_output=None 257 | ): 258 | """ 259 | Parameters 260 | ---------- 261 | returncode : int 262 | Return code for the failed command. 263 | cmd : str 264 | Text of the failed command. 265 | output : str, optional 266 | stdout generated from executing `cmd`. 267 | stderr : str, optional 268 | stderr generated from executing `cmd`. 269 | show_output : bool, optional 270 | Whether or not to include the failed command's stdout and/or 271 | stderr in the error message. If `None` (default), 272 | stdout/stderr will be displayed if the `suppress_stdout` 273 | field of `davos.config` is currently set to `True` (i.e., 274 | stdout would have been suppressed during execution). 275 | """ 276 | super().__init__(returncode=returncode, cmd=cmd, output=output, 277 | stderr=stderr) 278 | if show_output is None: 279 | from davos import config 280 | # if stdout from installer command that raised error was 281 | # suppressed, include it in the error message 282 | self.show_output = config.suppress_stdout 283 | else: 284 | self.show_output = show_output 285 | 286 | def __str__(self): 287 | msg = super().__str__() 288 | if self.show_output and (self.output or self.stderr): 289 | msg = f"{msg} See below for details." 290 | textwidth = min(get_terminal_size().columns, 85) 291 | if self.output: 292 | text = fill(self.output, textwidth, replace_whitespace=False) 293 | msg = f"{msg}\n\nstdout:\n{indent(text, ' ')}" 294 | if self.stderr: 295 | text = fill(self.stderr, textwidth, replace_whitespace=False) 296 | msg = f"{msg}\n\nstderr:\n{indent(text, ' ')}" 297 | return msg 298 | -------------------------------------------------------------------------------- /davos/core/exceptions.pyi: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentError 2 | from subprocess import CalledProcessError 3 | from typing import Literal 4 | 5 | __all__ = list[Literal['DavosError', 'DavosConfigError', 'DavosParserError', 'DavosProjectError', 'InstallerError', 6 | 'OnionParserError', 'OnionArgumentError', 'ParserNotImplementedError', 7 | 'ProjectNotebookNotFoundError', 'SmugglerError']] 8 | 9 | class DavosError(Exception): ... 10 | 11 | class DavosConfigError(DavosError): 12 | field: str 13 | msg: str 14 | def __init__(self, field: str, msg: str) -> None: ... 15 | 16 | class DavosParserError(SyntaxError, DavosError): 17 | def __init__(self, msg: str | None = ..., target_text: str | None = ..., target_offset: int = ...) -> None: ... 18 | 19 | class OnionParserError(DavosParserError): ... 20 | 21 | class OnionArgumentError(ArgumentError, OnionParserError): 22 | def __init__(self, msg: str | None = ..., argument: str | None = ..., onion_txt: str | None = ...) -> None: ... 23 | 24 | class ParserNotImplementedError(OnionParserError, NotImplementedError): ... 25 | 26 | class DavosProjectError(DavosError): ... 27 | 28 | class ProjectNotebookNotFoundError(DavosProjectError, FileNotFoundError): ... 29 | 30 | class SmugglerError(DavosError): ... 31 | 32 | class TheNightIsDarkAndFullOfErrors(SmugglerError): ... 33 | 34 | class InstallerError(SmugglerError, CalledProcessError): 35 | show_output: bool 36 | @classmethod 37 | def from_error(cls, cpe: CalledProcessError, show_output: bool | None = ...) -> InstallerError: ... 38 | def __init__(self, returncode: int, cmd: str, output: str | None = ..., stderr: str | None = ..., 39 | show_output: bool | None = ...) -> None: ... 40 | def __str__(self) -> str: ... 41 | -------------------------------------------------------------------------------- /davos/core/parsers.pyi: -------------------------------------------------------------------------------- 1 | from argparse import Action, ArgumentParser, Namespace 2 | from collections.abc import Sequence 3 | from typing import Final, Literal, NoReturn 4 | 5 | __all__ = list[Literal['EditableAction', 'OnionParser', 'pip_parser', 'SubtractAction']] 6 | 7 | class OnionParser(ArgumentParser): 8 | _args: str | None 9 | def parse_args(self, args: Sequence[str], namespace: Namespace | None = ...) -> Namespace: ... 10 | def error(self, message: str) -> NoReturn: ... 11 | 12 | class EditableAction(Action): 13 | def __init__(self, option_strings: Sequence[str], dest: Literal['editable'], default: bool | None = ..., 14 | metavar: str | tuple[str, ...] | None = ..., help: str | None = ...) -> None: ... 15 | def __call__(self, parser: ArgumentParser, namespace: Namespace, values: str, 16 | option_string: Literal['-e', '--editable'] | None = ...) -> None: ... 17 | 18 | class SubtractAction(Action): 19 | def __init__(self, option_strings: Sequence[str], dest: str, default: object | None = ..., required: bool = ..., 20 | help: str | None = ...) -> None: ... 21 | def __call__(self, parser: ArgumentParser, namespace: Namespace, values: None, 22 | option_string: str | None = ...) -> None: ... 23 | 24 | _pip_install_usage: list[str] 25 | pip_parser: Final[OnionParser] 26 | -------------------------------------------------------------------------------- /davos/core/project.pyi: -------------------------------------------------------------------------------- 1 | from pathlib import PosixPath 2 | from types import NotImplementedType 3 | from typing import Any, Final, Literal, NoReturn, overload, TypeVar 4 | 5 | __all__ = list[Literal['DAVOS_CONFIG_DIR', 'DAVOS_PROJECT_DIR', 'Project', 'get_notebook_path', 'get_project', 6 | 'prune_projects', 'use_default_project']] 7 | 8 | DAVOS_CONFIG_DIR: Final[PosixPath] 9 | DAVOS_PROJECT_DIR: Final[PosixPath] 10 | PATHSEP: Final[Literal['/', '\\']] 11 | PATHSEP_REPLACEMENT: Final[Literal['___']] 12 | SITE_PACKAGES_SUFFIX: Final[str] 13 | 14 | _P = TypeVar('_P', bound=Project) 15 | _InstalledPkgs = list[tuple[str, str]] 16 | 17 | class ProjectChecker(type): 18 | def __call__(cls, name: PosixPath | str) -> AbstractProject | ConcreteProject: ... 19 | 20 | class Project(metaclass=ProjectChecker): 21 | _installed_packages: _InstalledPkgs 22 | _site_packages_mtime: float 23 | name: str 24 | safe_name: str 25 | project_dir: PosixPath 26 | site_packages_dir: PosixPath 27 | def __init__(self, name: str) -> None: ... 28 | def __del__(self) -> None: ... 29 | @overload 30 | def __eq__(self: _P, other: _P) -> bool: ... 31 | @overload 32 | def __eq__(self, other: object) -> bool | Literal[False]: ... 33 | @overload 34 | def __lt__(self, other: Project) -> bool: ... 35 | @overload 36 | def __lt__(self, other: Any) -> bool | NotImplementedType: ... 37 | def __repr__(self) -> str: ... 38 | @property 39 | def installed_packages(self) -> _InstalledPkgs: ... 40 | def _refresh_installed_pkgs(self) -> None: ... 41 | def freeze(self) -> str: ... 42 | def remove(self, yes: bool = ...) -> None: ... 43 | def rename(self, new_name: PosixPath | str) -> None: ... 44 | 45 | class AbstractProject(Project): 46 | def __getattr__(self, item: str) -> NoReturn: ... 47 | def __repr__(self) -> str: ... 48 | 49 | class ConcreteProject(Project): ... 50 | 51 | def _dir_is_empty(path: PosixPath) -> bool: ... 52 | def _filepath_to_safename(filepath: str) -> str: ... 53 | def _get_project_name_type(project_name: PosixPath | str) -> tuple[str, AbstractProject | ConcreteProject]: ... 54 | def _safename_to_filepath(safename: str) -> str: ... 55 | def cleanup_project_dir_atexit(dirpath: PosixPath) -> None: ... 56 | def get_notebook_path() -> str: ... 57 | @overload 58 | def get_project(name: PosixPath | str, create: Literal[True] = ...) -> AbstractProject | ConcreteProject: ... 59 | @overload 60 | def get_project(name: PosixPath | str, create: Literal[False] = ...) -> AbstractProject | ConcreteProject | None: ... 61 | def prune_projects(yes: bool = ...) -> None: ... 62 | def use_default_project() -> None: ... 63 | -------------------------------------------------------------------------------- /davos/core/regexps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Regular expressions used heavily by `davos`. 3 | 4 | This module contains regular expressions used by `davos` to parse 5 | notebook cell input and/or output, pre-compiled as `re.Pattern` objects. 6 | `smuggle_statement_regex` is used to match lines of user code that 7 | contain `smuggle` statements (and, optionally, Onion comments) and split 8 | them into their component syntactic elements. `pip_installed_pkgs_regex` 9 | is used to extract names of just-installed/updated packages from the 10 | stdout generated by the `pip install` command. davos uses these names to 11 | check for and reload packages that were previously imported as a 12 | different version. 13 | """ 14 | 15 | 16 | __all__ = ['pip_installed_pkgs_regex', 'smuggle_statement_regex'] 17 | 18 | 19 | import re 20 | 21 | 22 | _name_re = r'[a-zA-Z_]\w*' # pylint: disable=invalid-name 23 | 24 | _smuggle_subexprs = { 25 | 'name_re': _name_re, 26 | 'qualname_re': fr'{_name_re}(?: *\. *{_name_re})*', 27 | 'as_re': fr' +as +{_name_re}', 28 | 'onion_re': r'\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$)', 29 | 'comment_re': r'(?m:\#+.*$)' 30 | } 31 | 32 | pip_installed_pkgs_regex = re.compile("^Successfully installed (.*)$", 33 | re.MULTILINE) 34 | 35 | # pylint: disable=line-too-long, trailing-whitespace 36 | smuggle_statement_regex = re.compile(( # noqa: E131 37 | r'^\s*' # match only if statement is first non-whitespace chars 38 | r'(?P' # capture full text of command in named group 39 | r'(?:' # first valid syntax: 40 | r'smuggle +{qualname_re}(?:{as_re})?' # match 'smuggle' + pkg/module name + optional alias 41 | r'(?:' # match the following: 42 | r' *' # - any amount of horizontal whitespace 43 | r',' # - followed by a comma 44 | r' *' # - followed by any amount of horizontal whitespace 45 | r'{qualname_re}(?:{as_re})?' # - followed by another pkg + optional alias 46 | r')*' # ... any number of times 47 | r'(?P(?= *; *(?:smuggle|from)))?' # check for multiple statements separated by semicolon 48 | # (match empty string with positive lookahead assertion 49 | # so named group gets defined without consuming) 50 | r'(?(SEMICOLON_SEP)|' # if the aren't multiple semicolon-separated statements: 51 | r'(?:' 52 | r' *(?={onion_re})' # consume horizontal whitespace only if followed by onion 53 | r'(?P{onion_re})?' # capture onion comment in named group... 54 | r')?' # ...optionally, if present 55 | r')' 56 | r')|(?:' # else (line doesn't match first valid syntax): 57 | r'from *{qualname_re} +smuggle +' # match 'from' + package[.module[...]] + 'smuggle ' 58 | r'(?P\()?' # capture open parenthesis for later check, if present 59 | r'(?(OPEN_PARENS)' # if parentheses opened: 60 | r'(?:' # logic for matching possible multiline statement: 61 | r' *' # capture any spaces following opening parenthesis 62 | r'(?:' # logic for matching code on *first line*: 63 | r'{name_re}(?:{as_re})?' # match a name with optional alias 64 | r' *' # optionally, match any number of spaces 65 | r'(?:' # match the following...: 66 | r',' # - a comma 67 | r' *' # - optionally followed by any number of spaces 68 | r'{name_re}(?:{as_re})?' # - followed by another name with optional alias 69 | r' *' # - optionally followed by any number of spaces 70 | r')*' # ...any number of times 71 | r',?' # match optional comma after last name, however many there were 72 | r' *' # finally, match any number of optional spaces 73 | r')?' # any code on first line (matched by preceding group) is optional 74 | r'(?:' # match 1 of 4 possible ends for first line: 75 | r'(?P{onion_re}) *{comment_re}?' # 1. onion, optionally followed by unrelated comment(s) 76 | r'|' # 77 | r'{comment_re}' # 2. unrelated, non-onion comment 78 | r'|' # 79 | r'(?m:$)' # 3. nothing further before EOL 80 | r'|' # 81 | r'(?P\))' # 4. close parenthesis on first line 82 | r')' 83 | r'(?(CLOSE_PARENS_FIRSTLINE)|' # if parentheses were NOT closed on first line 84 | r'(?:' # logic for matching subsequent line(s) of multiline smuggle statement: 85 | r'\s*' # match any & all whitespace before 2nd line 86 | r'(?:' # match 1 of 3 possibilities for each additional line: 87 | r'{name_re}(?:{as_re})?' # 1. similar to first line, match a name & optional alias... 88 | r' *' # ...followed by any amount of horizontal whitespace... 89 | r'(?:' # ... 90 | r',' # ...followed by comma... 91 | r' *' # ...optional whitespace... 92 | r'{name_re}(?:{as_re})?' # ...additional name & optional alias 93 | r' *' # ...optional whitespace... 94 | r')*' # ...repeated any number of times 95 | r'[^)\n]*' # ...plus any other content up to newline or close parenthesis 96 | r'|' 97 | r' *{comment_re}' # 2. match full-line comment, indented an arbitrary amount 98 | r'|' 99 | r'\n *' # 3. an empty line (truly empty or only whitespace characters) 100 | r')' 101 | r')*' # and repeat for any number of additional lines 102 | r'\)' # finally, match close parenthesis 103 | r')' 104 | r')' 105 | r'|' # else (no open parenthesis, so single line or /-continuation): 106 | r'{name_re}(?:{as_re})?' # match name with optional alias 107 | r'(?:' # possibly with additional comma-separated names & aliases ... 108 | r' *' # ... 109 | r',' # ... 110 | r' *' # ... 111 | r'{name_re}(?:{as_re})?' # ... 112 | r')*' # ...repeated any number of times 113 | r')' 114 | r'(?P(?= *; *(?:smuggle|from)))?' # check for multiple statements separated by semicolon 115 | r'(?(FROM_SEMICOLON_SEP)|' # if there aren't additional ;-separated statements... 116 | r'(?(FROM_ONION_1)|' # ...and this isn't a multiline statement with onion on line 1: 117 | r'(?:' 118 | r' *(?={onion_re})' # consume horizontal whitespace only if followed by onion 119 | r'(?P{onion_re})' # capture onion comment in named group... 120 | r')?' # ...optionally, if present 121 | r')' 122 | r')' 123 | r')' 124 | r')' 125 | ).format_map(_smuggle_subexprs)) 126 | 127 | # Condensed, fully substituted regex: 128 | # ^\s*(?P(?:smuggle +[a-zA-Z_]\w*(?: *\. *[a-zA-Z_]\w*)*(?: +a 129 | # s +[a-zA-Z_]\w*)?(?: *, *[a-zA-Z_]\w*(?: *\. *[a-zA-Z_]\w*)*(?: +as +[ 130 | # a-zA-Z_]\w*)?)*(?P(?= *; *(?:smuggle|from)))?(?(SEMICOL 131 | # ON_SEP)|(?: *(?=\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$))(?P< 132 | # ONION>\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$))?)?))|(?:from 133 | # *[a-zA-Z_]\w*(?: *\. *[a-zA-Z_]\w*)* +smuggle +(?P\()?(?( 134 | # OPEN_PARENS)(?: *(?:[a-zA-Z_]\w*(?: +as +[a-zA-Z_]\w*)? *(?:, *[a-zA-Z 135 | # _]\w*(?: +as +[a-zA-Z_]\w*)? *)*,? *)?(?:(?P\# *(?:pip|c 136 | # onda) *: *[^#\n ].+?(?= +\#| *\n| *$)) *(?m:\#+.*$)?|(?m:\#+.*$)|(?m:$ 137 | # )|(?P\)))(?(CLOSE_PARENS_FIRSTLINE)|(?:\s*(?:[ 138 | # a-zA-Z_]\w*(?: +as +[a-zA-Z_]\w*)? *(?:, *[a-zA-Z_]\w*(?: +as +[a-zA-Z 139 | # _]\w*)? *)*[^)\n]*| *(?m:\#+.*$)|\n *))*\)))|[a-zA-Z_]\w*(?: +as +[a-z 140 | # A-Z_]\w*)?(?: *, *[a-zA-Z_]\w*(?: +as +[a-zA-Z_]\w*)?)*)(?P(?= *; *(?:smuggle|from)))?(?(FROM_SEMICOLON_SEP)|(?(FROM_ONI 142 | # ON_1)|(?: *(?=\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$))(?P\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$)))?)))) 144 | -------------------------------------------------------------------------------- /davos/core/regexps.pyi: -------------------------------------------------------------------------------- 1 | from re import Pattern 2 | from typing import Final, final, Literal, TypedDict 3 | 4 | __all__ = list[Literal['pip_installed_pkgs_regex', 'smuggle_statement_regex']] 5 | 6 | _name_re: Final[Literal[r'[a-zA-Z_]\w*']] 7 | 8 | # noinspection PyPep8Naming 9 | @final 10 | class _smuggle_subexprs(TypedDict): 11 | as_re: Literal[r' +as +[a-zA-Z_]\w*'] 12 | comment_re: Literal[r'(?m:\#+.*$)'] 13 | name_re: Literal[r'[a-zA-Z_]\w*'] 14 | onion_re: Literal[r'\# *(?:pip|conda) *: *[^#\n ].+?(?= +\#| *\n| *$)'] 15 | qualname_re: Literal[r'[a-zA-Z_]\w*(?: *\. *[a-zA-Z_]\w*)*'] 16 | 17 | pip_installed_pkgs_regex: Final[Pattern[str]] 18 | smuggle_statement_regex: Final[Pattern[str]] 19 | -------------------------------------------------------------------------------- /davos/implementations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Environment-specific implementations of `davos` functionality. 3 | 4 | This module dynamically imports and defines functions based on the 5 | environment into which `davos` is imported. Some `davos` functionality 6 | requires (often quite drastically) different implementations depending 7 | on certain properties of the importing environment (e.g., "regular" 8 | Python vs IPython, the IPython notebook front-end, etc.). To deal with 9 | this, environment-dependent parts of core features and behaviors (in the 10 | `davos.core` subpackage) are isolated and abstracted as "helper 11 | functions". Multiple, interchangeable implementations of each helper 12 | function are organized into per-environment modules within the 13 | `davos.implementations` subpackage. At runtime, this module selectively 14 | imports a single version of each helper function based on the global 15 | Python environment. `davos.core` modules can then access the correct 16 | implementation of each helper function regardless of the environment by 17 | importing it from the top-level `davos.implementations` module. This 18 | also allows individual `davos.implementations` modules to import 19 | packages that aren't guaranteed to be installed outside certain 20 | environments (e.g., `google`, `ipykernel`) without requiring them as 21 | dependencies of the overall `davos` package. 22 | 23 | The importing environment is determined by the value of the 24 | `environment` field of the global `DavosConfig` instance, so it's 25 | important that: 26 | 1. the global `davos.config` object is instantiated before this 27 | module is imported 28 | 2. the top-level namespace of `davos.core.config` does not import 29 | this module or any others that do so, recursively. 30 | 3. any modules imported by this module that in turn rely on 31 | implementation-specific functions are loaded *after* those 32 | functions are defined, here. 33 | """ 34 | 35 | 36 | __all__ = [ 37 | 'auto_restart_rerun', 38 | 'full_parser', 39 | 'generate_parser_func', 40 | 'prompt_restart_rerun_buttons' 41 | ] 42 | 43 | 44 | from pathlib import Path 45 | 46 | from davos import config 47 | from davos.core.config import DavosConfig 48 | from davos.core.exceptions import DavosConfigError 49 | 50 | 51 | import_environment = config.environment 52 | 53 | if import_environment == 'Python': 54 | # noinspection PyUnresolvedReferences 55 | from davos.implementations.python import ( 56 | _activate_helper, 57 | _check_conda_avail_helper, 58 | _deactivate_helper, 59 | _run_shell_command_helper, 60 | auto_restart_rerun, 61 | generate_parser_func, 62 | prompt_restart_rerun_buttons 63 | ) 64 | else: 65 | # noinspection PyUnresolvedReferences 66 | from davos.implementations.ipython_common import ( 67 | _check_conda_avail_helper, 68 | _run_shell_command_helper, 69 | _set_custom_showsyntaxerror 70 | ) 71 | 72 | _set_custom_showsyntaxerror() 73 | 74 | if import_environment == 'IPython<7.0': 75 | from davos.implementations.ipython_pre7 import ( 76 | _activate_helper, 77 | _deactivate_helper, 78 | generate_parser_func 79 | ) 80 | from davos.implementations.jupyter import ( 81 | auto_restart_rerun, 82 | prompt_restart_rerun_buttons 83 | ) 84 | else: 85 | from davos.implementations.ipython_post7 import ( 86 | _activate_helper, 87 | _deactivate_helper, 88 | generate_parser_func 89 | ) 90 | if import_environment == 'Colaboratory': 91 | from davos.implementations.colab import ( 92 | auto_restart_rerun, 93 | prompt_restart_rerun_buttons 94 | ) 95 | else: 96 | from davos.implementations.jupyter import ( 97 | auto_restart_rerun, 98 | prompt_restart_rerun_buttons 99 | ) 100 | 101 | from davos.core.core import check_conda, smuggle, parse_line 102 | 103 | 104 | # Implementation-specific wrapper around davos.core.core.parse_line 105 | full_parser = generate_parser_func(parse_line) 106 | 107 | 108 | ######################################## 109 | # ADDITIONAL DAVOS.CONFIG PROPERTIES # 110 | ######################################## 111 | # some properties are added to the davos.core.config.DavosConfig class 112 | # here rather than when it's initially defined, because they depend on 113 | # either: 114 | # 1. an implementation-specific function that hasn't been determined 115 | # yet, or 116 | # 2. a function defined in the `davos.core.core` module (namely, 117 | # `check_conda`) which needs to be imported after 118 | # implementation-specific functions are set here. 119 | 120 | 121 | # pylint: disable=unused-argument 122 | # noinspection PyUnusedLocal 123 | def _active_fget(conf): 124 | """getter for davos.config.active""" 125 | return config._active 126 | 127 | 128 | def _active_fset(conf, value): 129 | """setter for davos.config.active""" 130 | if value is True: 131 | _activate_helper(smuggle, full_parser) 132 | elif value is False: 133 | _deactivate_helper(smuggle, full_parser) 134 | else: 135 | raise DavosConfigError('active', "field may be 'True' or 'False'") 136 | 137 | conf._active = value 138 | 139 | 140 | def _conda_avail_fget(conf): 141 | """getter for davos.config.conda_avail""" 142 | if conf._conda_avail is None: 143 | check_conda() 144 | 145 | return conf._conda_avail 146 | 147 | 148 | # noinspection PyUnusedLocal 149 | def _conda_avail_fset(conf, _): 150 | """setter for davos.config.conda_avail""" 151 | raise DavosConfigError('conda_avail', 'field is read-only') 152 | 153 | 154 | def _conda_env_fget(conf): 155 | """getter for davos.config.conda_env""" 156 | if conf._conda_avail is None: 157 | # _conda_env is None if we haven't checked conda yet *and* if 158 | # conda is not available vs _conda_avail is None only if we 159 | # haven't checked yet 160 | check_conda() 161 | 162 | return conf._conda_env 163 | 164 | 165 | def _conda_env_fset(conf, new_env): 166 | """setter for davos.config.conda_env""" 167 | if conf._conda_avail is None: 168 | check_conda() 169 | 170 | if conf._conda_avail is False: 171 | raise DavosConfigError( 172 | "conda_env", 173 | "cannot set conda environment. No local conda installation found" 174 | ) 175 | if isinstance(new_env, Path): 176 | new_env = new_env.name 177 | 178 | if new_env != conf._conda_env: 179 | if ( 180 | conf._conda_envs_dirs is not None and 181 | new_env not in conf._conda_envs_dirs.keys() 182 | ): 183 | local_envs = {"', '".join(conf._conda_envs_dirs.keys())} 184 | raise DavosConfigError( 185 | "conda_env", 186 | f"unrecognized environment name: {new_env!r}. Local " 187 | f"environments are:\n\t{local_envs!r}" 188 | ) 189 | 190 | conf._conda_env = new_env 191 | 192 | 193 | def _conda_envs_dirs_fget(conf): 194 | """getter for davos.config.conda_envs_dirs""" 195 | if conf._conda_avail is None: 196 | check_conda() 197 | 198 | return conf._conda_envs_dirs 199 | 200 | 201 | # noinspection PyUnusedLocal 202 | def _conda_envs_dirs_fset(conf, _): 203 | """setter for davos.config.conda_envs_dirs""" 204 | raise DavosConfigError('conda_envs_dirs', 'field is read-only') 205 | 206 | 207 | DavosConfig.active = property(fget=_active_fget, fset=_active_fset) 208 | 209 | DavosConfig.conda_avail = property(fget=_conda_avail_fget, 210 | fset=_conda_avail_fset) 211 | 212 | DavosConfig.conda_env = property(fget=_conda_env_fget, fset=_conda_env_fset) 213 | 214 | DavosConfig.conda_envs_dirs = property(fget=_conda_envs_dirs_fget, 215 | fset=_conda_envs_dirs_fset) 216 | -------------------------------------------------------------------------------- /davos/implementations/__init__.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from pathlib import PosixPath 3 | from typing import Final, Literal, NoReturn, Protocol 4 | from davos.core.config import DavosConfig 5 | from davos.core.core import SmuggleFunc 6 | 7 | __all__ = list[Literal['auto_restart_rerun', 'full_parser', 'generate_parser_func', 'prompt_restart_rerun_buttons']] 8 | 9 | class IPyPost7FullParserFunc(Protocol): 10 | has_side_effects: Literal[True] 11 | def __call__(self, lines: list[str]) -> list[str]: ... 12 | 13 | LineParserFunc = Callable[[str], str] 14 | FullParserFunc = LineParserFunc | IPyPost7FullParserFunc 15 | 16 | _activate_helper: Callable[[SmuggleFunc, FullParserFunc], None] 17 | _check_conda_avail_helper: Callable[[], str | None] 18 | _deactivate_helper: Callable[[SmuggleFunc, FullParserFunc], None] 19 | _run_shell_command_helper: Callable[[str], None] 20 | _set_custom_showsyntaxerror: Callable[[], None] 21 | auto_restart_rerun: Callable[[list[str]], NoReturn] 22 | full_parser: FullParserFunc 23 | generate_parser_func: Callable[[LineParserFunc], FullParserFunc] 24 | import_environment: Final[Literal['Colaboratory', 'IPython<7.0', 'IPython>=7.0', 'Python']] 25 | prompt_restart_rerun_buttons: Callable[[list[str]], object | None] 26 | 27 | def _active_fget(conf: DavosConfig) -> bool: ... 28 | def _active_fset(conf: DavosConfig, value: bool) -> None: ... 29 | def _conda_avail_fget(conf: DavosConfig) -> bool: ... 30 | def _conda_avail_fset(conf: DavosConfig, _: object) -> NoReturn: ... 31 | def _conda_env_fget(conf: DavosConfig) -> str | None: ... 32 | def _conda_env_fset(conf: DavosConfig, new_env: PosixPath | str) -> None: ... 33 | def _conda_envs_dirs_fget(conf: DavosConfig) -> dict[str, str] | None: ... 34 | def _conda_envs_dirs_fset(conf: DavosConfig, _: object) -> NoReturn: ... 35 | -------------------------------------------------------------------------------- /davos/implementations/colab.py: -------------------------------------------------------------------------------- 1 | """Helper function implementations specific to Google Colab notebooks.""" 2 | 3 | 4 | __all__ = ['auto_restart_rerun', 'prompt_restart_rerun_buttons'] 5 | 6 | 7 | from IPython.core.display import _display_mimetype 8 | 9 | 10 | def auto_restart_rerun(pkgs): 11 | """ 12 | Colab-specific implementation of `auto_restart_rerun`. 13 | 14 | Raises `NotImplementedError` whenever called, though this should 15 | never happen except when done intentionally as trying to set 16 | `davos.auto_rerun = True` should raise an error. This feature is not 17 | available in Colab notebooks because it requires accessing the 18 | notebook frontend through the `colab.global.notebook` JavaScript 19 | object, which Colab blocks you from doing from the kernel. 20 | 21 | Parameters 22 | ---------- 23 | pkgs : list of str 24 | Packages that could not be reloaded without restarting the 25 | runtime. 26 | 27 | Raises 28 | ------- 29 | NotImplementedError 30 | In all cases. 31 | """ 32 | raise NotImplementedError( 33 | "automatic rerunning of cells not available in Colaboratory (this " 34 | "function should not be reachable through normal use)." 35 | ) 36 | 37 | 38 | def prompt_restart_rerun_buttons(pkgs): 39 | """ 40 | Colab-specific implementation of `prompt_restart_rerun_buttons`. 41 | 42 | Issues a warning that the notebook runtime must be restarted in 43 | order to use the just-smuggled version of one or more `pkgs`, and 44 | displays a button the user can click to do so. Uses one of Colab's 45 | existing MIME types, since it's one of the few things explicitly 46 | allowed to send messages between the frontend and kernel. 47 | 48 | Parameters 49 | ---------- 50 | pkgs : list of str 51 | Packages that could not be reloaded without restarting the 52 | runtime. 53 | """ 54 | _display_mimetype( 55 | "application/vnd.colab-display-data+json", 56 | ( 57 | {'pip_warning': {'packages': ', '.join(pkgs)}}, 58 | ), 59 | raw=True 60 | ) 61 | -------------------------------------------------------------------------------- /davos/implementations/colab.pyi: -------------------------------------------------------------------------------- 1 | from typing import Literal, NoReturn 2 | 3 | __all__ = list[Literal['auto_restart_rerun', 'prompt_restart_rerun_buttons']] 4 | 5 | def auto_restart_rerun(pkgs: list[str]) -> NoReturn: ... 6 | def prompt_restart_rerun_buttons(pkgs: list[str]) -> None: ... 7 | -------------------------------------------------------------------------------- /davos/implementations/ipython_common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper function implementations common across all IPython versions and 3 | front-end interfaces. 4 | """ 5 | 6 | __all__ = [] 7 | 8 | 9 | import sys 10 | import textwrap 11 | from contextlib import redirect_stdout 12 | from io import StringIO 13 | from pathlib import Path 14 | from subprocess import CalledProcessError 15 | 16 | from IPython.utils.process import system as _run_shell_cmd 17 | 18 | from davos import config 19 | from davos.core.exceptions import DavosParserError 20 | 21 | 22 | def _check_conda_avail_helper(): 23 | """ 24 | Check whether the `conda` executable is available. 25 | 26 | `IPython` implementation of the helper function for 27 | `davos.core.core.check_conda`. Runs a command shell (`conda list 28 | IPython`) whose stdout contains the path to the current conda 29 | environment, which can be parsed by the main `check_conda` function. 30 | Uses the `%conda` IPython line magic, if available (usually if 31 | `IPython>=7.3`). Otherwise, looks for `conda` history file and runs 32 | some logic to determine whether -- if the `conda` exe is available, 33 | whether `davos` is running from the base environment or not, and if 34 | not, passes the `--prefix` to the command. If successful, returns 35 | the (suppressed) stdout generated by the command. Otherwise, returns 36 | `None`. 37 | 38 | Returns 39 | ------- 40 | str or None 41 | If the command runs successfully, the captured stdout. 42 | Otherwise, `None`. 43 | 44 | See Also 45 | -------- 46 | davos.core.core.check_conda : core function that calls this helper. 47 | """ 48 | if 'conda' in set(config._ipython_shell.magics_manager.magics['line']): 49 | # if the %conda line magic is available (IPython>=7.3), use that 50 | # directly 51 | try: 52 | with redirect_stdout(StringIO()) as conda_list_output: 53 | config._ipython_shell.run_line_magic('conda', 'list IPython') 54 | except ValueError: 55 | # kernel is not running within a conda environment 56 | return None 57 | 58 | else: 59 | conda_history_path = Path(sys.prefix, "conda-meta", "history") 60 | # if conda history file doesn't exist at this location, davos 61 | # isn't running in a conda environment 62 | if not conda_history_path.is_file(): 63 | return None 64 | 65 | cmd = "conda list IPython" 66 | # location we'd expect to find conda executable if davos is 67 | # running in the 'base' environment (and no prefix is needed) 68 | base_exe_loc = Path(sys.executable).parent.joinpath('conda') 69 | 70 | if not base_exe_loc.is_file(): 71 | cmd += f" --prefix {sys.prefix}" 72 | 73 | with redirect_stdout(StringIO()) as conda_list_output: 74 | _run_shell_cmd(cmd) 75 | 76 | return conda_list_output.getvalue() 77 | 78 | 79 | def _run_shell_command_helper(command): 80 | """ 81 | Run a shell command in a subprocess, piping stdout & stderr. 82 | 83 | `IPython` implementation of helper function for 84 | `davos.core.core.run_shell_command`. stdout & stderr streams are 85 | captured or suppressed by the outer function. If the command runs 86 | successfully, return its exit status (`0`). Otherwise, raise an 87 | error. 88 | 89 | Parameters 90 | ---------- 91 | command : str 92 | The command to execute. 93 | 94 | Returns 95 | ------- 96 | int 97 | The exit code of the command. This will always be `0` if the 98 | function returns. Otherwise, an error is raised. 99 | 100 | Raises 101 | ------ 102 | subprocess.CalledProcessError : 103 | If the command returned a non-zero exit status. 104 | 105 | See Also 106 | -------- 107 | IPython.utils.process.system : `IPython` shell command runner. 108 | """ 109 | retcode = _run_shell_cmd(command) 110 | if retcode != 0: 111 | raise CalledProcessError(returncode=retcode, cmd=command) 112 | 113 | 114 | def _set_custom_showsyntaxerror(): 115 | """ 116 | Overload the `IPython` shell's `.showsyntaxerror()` method. 117 | 118 | Replaces the global `IPython` interactive shell object's 119 | `.showsyntaxerror()` method with a custom function that allows 120 | `davos`-native exceptions raised during the pre-execution cell 121 | parsing phase to display a full traceback. Also: 122 | - updates the custom function's docstring to include the 123 | original `.showsyntaxerror()` method's docstring and 124 | explicitly note that the method was updated by `davos` 125 | - stores a reference to the original `.showsyntaxerror()` method 126 | in the `davos.config` object so it can be called from the 127 | custom version 128 | - binds the custom function to the interactive shell object 129 | *instance* so it implicitly receives the instance as its first 130 | argument when called (like a normal instance method) 131 | 132 | See Also 133 | ------- 134 | davos.implementations.ipython_common._showsyntaxerror_davos : 135 | The custom `.showsyntaxerror()` method set by `davos`. 136 | IPython.core.interactiveshell.InteractiveShell.showsyntaxerror : 137 | The original, overloaded `.showsyntaxerror()` method. 138 | 139 | Notes 140 | ----- 141 | Runs exactly once when `davos` is imported and initialized in an 142 | `IPython` environment, and takes no action if run again. This 143 | prevents overwriting the reference to the original 144 | `.showsyntaxerror()` method stored in the `davos.config` object. 145 | """ 146 | if config._ipy_showsyntaxerror_orig is not None: 147 | # function has already been called 148 | return 149 | 150 | ipy_shell = config.ipython_shell 151 | new_doc = textwrap.dedent(f"""\ 152 | {' METHOD UPDATED BY DAVOS PACKAGE '.center(72, '=')} 153 | 154 | {textwrap.indent(_showsyntaxerror_davos.__doc__, ' ')} 155 | 156 | {' ORIGINAL DOCSTRING: '.center(72, '=')} 157 | 158 | 159 | {ipy_shell.showsyntaxerror.__doc__}""") 160 | 161 | _showsyntaxerror_davos.__doc__ = new_doc 162 | config._ipy_showsyntaxerror_orig = ipy_shell.showsyntaxerror 163 | # bind function as method 164 | # pylint: disable=no-value-for-parameter 165 | # (pylint bug: expects __get__ method to take same args as function) 166 | ipy_shell.showsyntaxerror = _showsyntaxerror_davos.__get__(ipy_shell, 167 | type(ipy_shell)) 168 | 169 | 170 | # noinspection PyUnusedLocal 171 | def _showsyntaxerror_davos( 172 | ipy_shell, 173 | filename=None, 174 | running_compiled_code=False # pylint: disable=unused-argument 175 | ): 176 | """ 177 | Show `davos` library `SyntaxError` subclasses with full tracebacks. 178 | 179 | Replaces global IPython interactive shell object's 180 | `.showsyntaxerror()` method during initialization as a way to hook 181 | into `IPython`'s exception handling machinery for errors raised 182 | during the pre-execution cell parsing phase. 183 | 184 | Because cell content is parsed as text rather than actually executed 185 | during this stage, the only exceptions `IPython` expects input 186 | transformers (such as the `davos` parser) to raise are 187 | `SyntaxError`s. Thus, all `davos` library exceptions that may be 188 | raised by the parser inherit from `SyntaxError`). And because 189 | `IPython` assumes any `SyntaxError`s raised during parsing were 190 | caused by issues with the cell content itself, it expects their 191 | stack traces to comprise only a single frame, and displays them in a 192 | format that does not include a full traceback. This function 193 | excludes `davos` library errors from this behavior, and displays 194 | them in full using the standard, more readable & informative format. 195 | 196 | Parameters 197 | ---------- 198 | ipy_shell : IPython.core.interactiveshell.InteractiveShell 199 | The global `IPython` shell instance. Because the function is 200 | bound as a method of the shell instance, this is passed 201 | implicitly (i.e., equivalent to `self`). 202 | filename : str, optional 203 | The name of the file the `SyntaxError` occurred in. If `None` 204 | (default), the name of the cell's entry in `linecache.cache` 205 | will be used. 206 | running_compiled_code : bool, optional 207 | Whether the `SyntaxError` occurred while running compiled code 208 | (see **Notes** below). 209 | 210 | See Also 211 | -------- 212 | davos.implementations.ipython_common._set_custom_showsyntaxerror : 213 | Replaces the `.showsyntaxerror()` method with this function. 214 | IPython.core.compilerop.code_name : 215 | Generates unique names for each cell used in `linecache.cache`. 216 | IPython.core.interactiveshell.InteractiveShell.showsyntaxerror : 217 | The original `.showsyntaxerror()` method this function replaces. 218 | 219 | Notes 220 | ----- 221 | The `running_compiled_code` argument was added in `IPython` 6.1.0, 222 | and setting it to `True` accomplishes (something close to) the same 223 | thing this workaround does. However, since `davos` needs to support 224 | `IPython` versions back to v5.5.0, we can't rely on it being 225 | available. 226 | """ 227 | etype, value, tb = ipy_shell._get_exc_info() 228 | if issubclass(etype, DavosParserError): 229 | try: 230 | # noinspection PyBroadException 231 | try: 232 | # display custom traceback, if class supports it 233 | stb = value._render_traceback_() 234 | except Exception: # pylint: disable=broad-except 235 | stb = ipy_shell.InteractiveTB.structured_traceback( 236 | etype, value, tb, tb_offset=ipy_shell.InteractiveTB.tb_offset 237 | ) 238 | ipy_shell._showtraceback(etype, value, stb) 239 | if ipy_shell.call_pdb: 240 | ipy_shell.debugger(force=True) 241 | except KeyboardInterrupt: 242 | print('\n' + ipy_shell.get_exception_only(), file=sys.stderr) 243 | return None 244 | # original method is stored in Davos instance, but still bound 245 | # IPython.core.interactiveshell.InteractiveShell instance 246 | return config._ipy_showsyntaxerror_orig(filename=filename) 247 | -------------------------------------------------------------------------------- /davos/implementations/ipython_common.pyi: -------------------------------------------------------------------------------- 1 | from davos.core.config import IpythonShell 2 | 3 | __all__ = list[str] 4 | 5 | def _check_conda_avail_helper() -> str | None: ... 6 | def _run_shell_command_helper(command: str) -> None: ... 7 | def _set_custom_showsyntaxerror() -> None: ... 8 | def _showsyntaxerror_davos(ipy_shell: IpythonShell, filename: str | None = ..., 9 | running_compiled_code: bool = ...) -> None: ... 10 | -------------------------------------------------------------------------------- /davos/implementations/ipython_post7.py: -------------------------------------------------------------------------------- 1 | """Helper functions specific to IPython versions >= 7.0.0.""" 2 | 3 | 4 | __all__ = ['generate_parser_func'] 5 | 6 | 7 | from IPython.core.inputtransformer import assemble_python_lines 8 | 9 | from davos import config 10 | 11 | 12 | def _activate_helper(smuggle_func, parser_func): 13 | """ 14 | `IPython>=7.0.0`-specific implementation of `_activate_helper`. 15 | 16 | Helper function called when setting `davos.active = True` (or 17 | `davos.config.active = True`). Registers the `davos` parser 18 | (`parser_func`) as an `IPython` input transformer (in the 19 | `input_transformers_post` group) if it isn't one already. Injects 20 | `smuggle_func` into the `IPython` user namespace as `"smuggle"`. 21 | 22 | Parameters 23 | ---------- 24 | smuggle_func : callable 25 | Function to be added to the `IPython` user namespace under the 26 | name "`smuggle`" (typically, `davos.core.core.smuggle`). 27 | parser_func : callable 28 | Function to be registered as an `IPython` input transformer (for 29 | `IPython>=7.0.0`, the return value of 30 | `davos.implementations.ipython_post7.generate_parser_func()`). 31 | 32 | See Also 33 | -------- 34 | davos.core.core.smuggle : The `smuggle` function. 35 | generate_parser_func : Function that creates the `davos` parser. 36 | """ 37 | ipy_shell = config._ipython_shell 38 | input_xforms = ipy_shell.input_transformers_post 39 | if parser_func not in input_xforms: 40 | input_xforms.append(parser_func) 41 | 42 | # insert "smuggle" into notebook namespace 43 | ipy_shell.user_ns['smuggle'] = smuggle_func 44 | 45 | 46 | def _deactivate_helper(smuggle_func, parser_func): 47 | """ 48 | `IPython>=7.0.0`-specific implementation of `_deactivate_helper`. 49 | 50 | Helper function called when setting `davos.active = False` (or 51 | `davos.config.active = False`). Removes the `davos` parser 52 | (`parser_func`) from the set of `IPython` input transformers and 53 | deletes the variable named "`smuggle`" from the `IPython` user 54 | namespace if it (a) exists and (b) holds a reference to 55 | `smuggle_func`, rather than some other value. 56 | 57 | Parameters 58 | ---------- 59 | smuggle_func : callable 60 | Function expected to be the value of "`smuggle`" in the 61 | `IPython` user namespace. Used to confirm that variable should 62 | be deleted (typically, `davos.core.core.smuggle`). 63 | parser_func : callable 64 | Function that should be removed from the set of `IPython` input 65 | transformers (for `IPython>=7.0.0`, the return value of 66 | `davos.implementations.ipython_post7.generate_parser_func()`). 67 | 68 | See Also 69 | -------- 70 | davos.core.core.smuggle : The `smuggle` function. 71 | generate_parser_func : Function that creates the `davos` parser. 72 | 73 | Notes 74 | ----- 75 | 1. Any `smuggle` statements following setting `davos.active = False` 76 | will result in `SyntaxError`s unless the parser is reactivated 77 | first. 78 | 2. The `davos` parser adds minimal overhead to cell execution. 79 | However, deactivating it once it is no longer needed (i.e., after 80 | the last `smuggle` statement) may be useful when measuring 81 | precise runtimes (e.g. profiling code), as the amount of overhead 82 | added is a function of the number of lines rather than 83 | complexity. 84 | """ 85 | ipy_shell = config._ipython_shell 86 | try: 87 | ipy_shell.input_transformers_post.remove(parser_func) 88 | except ValueError: 89 | pass 90 | 91 | if ipy_shell.user_ns.get('smuggle') is smuggle_func: 92 | del ipy_shell.user_ns['smuggle'] 93 | 94 | 95 | def generate_parser_func(line_parser): 96 | """ 97 | `IPython>=7.0.0`-specific implementation of `generate_parser_func`. 98 | 99 | Given a function that parses a single line of code, returns the full 100 | `davos` parser to be registered as an `IPython` input transformer. 101 | 102 | Parameters 103 | ---------- 104 | line_parser : callable 105 | Function that parses a single line of user code (typically, 106 | `davos.core.core.parse_line`). 107 | 108 | Returns 109 | ------- 110 | callable 111 | The `davos` parser for use as an `IPython` input transformer. 112 | Given user input consisting of one or more lines (e.g., a 113 | notebook cell), returns the input with all lines parsed. 114 | 115 | See Also 116 | -------- 117 | davos.core.core.parse_line : Single-line parser function. 118 | 119 | Notes 120 | ----- 121 | 1. In order to handle multiline `smuggle` statements, the 122 | `line_parser` function (`davos.core.core.parse_line`) works on 123 | "*logical*"/"*assembled*" lines, rather than "*physical*" lines. 124 | A logical line may consist of a single physical line, or multiple 125 | physical lines joined by explicit (i.e., backslash-based) or 126 | implicit (i.e., parenthesis/bracket/brace/etc.-based). For 127 | example, each of the following comprise multiple physical lines, 128 | but a single logical line: 129 | ```python 130 | from foo import bar \ 131 | baz \ 132 | qux \ 133 | quux 134 | 135 | spam = [ham, 136 | eggs, 137 | ni] 138 | 139 | the_night = { 140 | 'dark': True, 141 | 'full_of_terrors': True 142 | } 143 | ``` 144 | 2. The API for input transformations was completely overhauled in 145 | `IPython` v7.0. Among other changes, there is no longer a hook 146 | exposed for input transformers that work on fully assembled 147 | multiline statements (previously called 148 | `python_line_transforms`). Additionally, all input transformer 149 | functions are now called once per input area (i.e., notebook cell 150 | or interactive shell prompt) and are passed the full input, 151 | rather being called on each individual line. The `IPython>=7.0.0` 152 | implementation of the `davos` parser handles this by tokenizing 153 | the and assembling logical lines from the full input before 154 | passing each to the `line_parser` function. While this adds some 155 | additional overhead compared to the `IPython<7.0.0` `davos` 156 | parser, the difference is functionally de minimis, and in fact 157 | outweighed by the new parser's ability to skip parsing cells that 158 | don't contain `smuggle` statements altogether. 159 | 3. Before it is returned, the full `davos` parser function is 160 | assigned an attribute "`has_side_effects`", which is set to 161 | `True`. In `IPython>=7.17`, this will prevent the parser from 162 | being run when `IPython` checks whether or not the user input is 163 | complete (i.e., after pressing enter in the `IPython` shell, is 164 | the existing input a full statement, or part of a multiline 165 | statement/code block?). In `IPython<7.17`, this will have no 166 | effect. 167 | """ 168 | pyline_assembler = assemble_python_lines() 169 | 170 | def full_parser(lines): 171 | if 'smuggle ' not in ''.join(lines): 172 | # if cell contains no potential smuggle statements, don't 173 | # bother parsing line-by-line 174 | return lines 175 | 176 | parsed_lines = [] 177 | curr_buff = [] 178 | for raw_line in lines: 179 | # don't include trailing '\n' 180 | python_line = pyline_assembler.push(raw_line[:-1]) 181 | if python_line is None: 182 | # currently accumulating multiline logical line 183 | curr_buff.append(raw_line) 184 | continue 185 | 186 | # pass single-line parser full logical lines -- may be 187 | # single physical line or fully accumulated multiline 188 | # statement 189 | parsed_line = line_parser(python_line) 190 | if curr_buff: 191 | # logical line consists of multiple physical lines 192 | if parsed_line == python_line: 193 | # multiline statement is not a smuggle statement; 194 | # don't combine physical lines in output 195 | parsed_lines.extend(curr_buff) 196 | # last line isn't in curr_buff; add it separately 197 | parsed_lines.append(raw_line) 198 | else: 199 | # logical line is a multiline smuggle statement 200 | parsed_lines.append(f'{parsed_line}\n') 201 | # reset partially accumulated lines 202 | curr_buff.clear() 203 | else: 204 | # logical line consists of a single physical line 205 | parsed_lines.append(f'{parsed_line}\n') 206 | 207 | # .reset() clears pyline_assembler's .buf & .tokenizer for next 208 | # cell. Returns ''.join(pyline_assembler.buf) if .buf list is 209 | # not empty, otherwise None 210 | if pyline_assembler.reset(): 211 | # Presence of an incomplete logical line after parsing the 212 | # last physical line means there's a SyntaxError somewhere. 213 | # Include remaining physical lines to let IPython/Python 214 | # deal with raising the SyntaxError from the proper location 215 | parsed_lines.extend(curr_buff) 216 | return parsed_lines 217 | 218 | # prevents transformer from being run multiple times when IPython 219 | # parses partial line to determine whether input is complete 220 | full_parser.has_side_effects = True 221 | return full_parser 222 | -------------------------------------------------------------------------------- /davos/implementations/ipython_post7.pyi: -------------------------------------------------------------------------------- 1 | from typing import Literal, TypeVar 2 | from davos.core.core import SmuggleFunc 3 | from davos.implementations import IPyPost7FullParserFunc, LineParserFunc 4 | 5 | __all__ = list[Literal['generate_parser_func']] 6 | 7 | def _activate_helper(smuggle_func: SmuggleFunc, parser_func: IPyPost7FullParserFunc) -> None: ... 8 | def _deactivate_helper(smuggle_func: SmuggleFunc, parser_func: IPyPost7FullParserFunc) -> None: ... 9 | def generate_parser_func(line_parser: LineParserFunc) -> IPyPost7FullParserFunc: ... 10 | -------------------------------------------------------------------------------- /davos/implementations/ipython_pre7.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper function implementations specific to IPython>=5.5.0,<7.0.0. 3 | 4 | Many of the IPython internals relevant to davos functionality were 5 | overhauled in IPython v7.0.0, so this module contains implementations of 6 | helper functions that allow davos to interface with IPython versions 7 | before that. The oldest IPython version officially supported by davos is 8 | v5.5.0. 9 | """ 10 | 11 | 12 | __all__ = ['generate_parser_func'] 13 | 14 | 15 | from IPython.core.inputtransformer import StatelessInputTransformer 16 | 17 | from davos import config 18 | 19 | 20 | def _activate_helper(smuggle_func, parser_func): 21 | """ 22 | `IPython<7.0.0`-specific implementation of `_activate_helper`. 23 | 24 | Helper function called when setting `davos.active = True` (or 25 | `davos.config.active = True`). Wraps the `davos` parser 26 | (`parser_func`) in an 27 | `IPython/core.inputtransformer.StatelessInputTransformer` instance 28 | and registers it as both an `IPython` input transformer *and* input 29 | splitter (in the `python_line_transforms` group) if it isn't one 30 | already. Injects `smuggle_func` into the `IPython` user namespace as 31 | `"smuggle"`. 32 | 33 | Parameters 34 | ---------- 35 | smuggle_func : callable 36 | Function to be added to the `IPython` user namespace under the 37 | name "`smuggle`" (typically, `davos.core.core.smuggle`). 38 | parser_func : callable 39 | Function to be registered as an `IPython` input transformer 40 | and input splitter (for `IPython<7.0.0`, the return value of 41 | `davos.implementations.ipython_pre7.generate_parser_func()`). 42 | 43 | See Also 44 | -------- 45 | davos.core.core.smuggle : The `smuggle` function. 46 | generate_parser_func : Function that creates the `davos` parser. 47 | IPython.core.inputtransformer.StatelessInputTransformer : 48 | Wrapper class for stateless input transformer functions. 49 | 50 | Notes 51 | ----- 52 | 1. `IPython<7.0.0` allows input transformer functions to hook into 53 | three different steps of the `IPython` parser during the 54 | pre-execution phase, and transform user input at various stages 55 | before it's sent to the Python parser. The `davos` parser runs as 56 | a "`python_line_transform`", which is the last group of 57 | transforms run on the raw input. By this stage, the `IPython` 58 | parser has reassembled groups of "*physical*" lines joined by 59 | both explicit (backslash-based) and implicit 60 | (parenthesis/bracket/brace/etc.-based) line continuations into 61 | full Python statements so the `davos` parser will receive 62 | multiline `smuggle` statements will be as a single unit. 63 | 2. `IPython<7.0.0` requires that custom input transformers be 64 | added to both the `IPython` shell's `input_splitter` attribute 65 | and its `input_transformer_manager`. The former set of functions 66 | is run by `IPython` when checking whether or not the user input 67 | is complete (i.e., after pressing enter in the `IPython` shell, 68 | is the existing input a full statement, or part of a multiline 69 | statement/code block?). The latter is run when parsing complete 70 | blocks of user input that will be executed as Python code. 71 | """ 72 | ipy_shell = config._ipython_shell 73 | smuggle_transformer = StatelessInputTransformer.wrap(parser_func) 74 | # noinspection PyDeprecation 75 | splitter_xforms = ipy_shell.input_splitter.python_line_transforms 76 | manager_xforms = ipy_shell.input_transformer_manager.python_line_transforms 77 | 78 | if not any(t.func is parser_func for t in splitter_xforms): 79 | splitter_xforms.append(smuggle_transformer()) 80 | 81 | if not any(t.func is parser_func for t in manager_xforms): 82 | manager_xforms.append(smuggle_transformer()) 83 | 84 | # insert "smuggle" into notebook namespace 85 | ipy_shell.user_ns['smuggle'] = smuggle_func 86 | 87 | 88 | # noinspection PyDeprecation 89 | def _deactivate_helper(smuggle_func, parser_func): 90 | """ 91 | `IPython<7.0.0`-specific implementation of `_deactivate_helper`. 92 | 93 | Helper function called when setting `davos.active = False` (or 94 | `davos.config.active = False`). Removes the 95 | `IPython/core.inputtransformer.StatelessInputTransformer` instance 96 | whose `.func` is the `davos` parser (`parser_func`) from both the 97 | `.input_splitter` and `input_transformer_manager` attributes the 98 | `IPython` shell. Deletes the variable named "`smuggle`" from the 99 | `IPython` user namespace if it (a) exists and (b) holds a reference 100 | to `smuggle_func`, rather than some other value. 101 | 102 | Parameters 103 | ---------- 104 | smuggle_func : callable 105 | Function expected to be the value of "`smuggle`" in the 106 | `IPython` user namespace. Used to confirm that variable should 107 | be deleted (typically, `davos.core.core.smuggle`). 108 | parser_func : callable 109 | Function that should be removed from the set of `IPython` input 110 | transformers (for `IPython<7.0.0`, `davos.core.core.parse_line` 111 | or the return value of 112 | `davos.implementations.ipython_pre7.generate_parser_func()`, 113 | which are equivalent). 114 | 115 | See Also 116 | -------- 117 | davos.core.core.smuggle : The `smuggle` function. 118 | generate_parser_func : Function that creates the `davos` parser. 119 | 120 | Notes 121 | ----- 122 | 1. Any `smuggle` statements following setting `davos.active = False` 123 | will result in `SyntaxError`s unless the parser is reactivated 124 | first. 125 | 2. The `davos` parser adds minimal overhead to cell execution. 126 | However, deactivating it once it is no longer needed (i.e., after 127 | the last `smuggle` statement) may be useful when measuring 128 | precise runtimes (e.g. profiling code), as the amount of overhead 129 | added is a function of the number of lines rather than 130 | complexity. 131 | """ 132 | ipy_shell = config._ipython_shell 133 | splitter_xforms = ipy_shell.input_splitter.python_line_transforms 134 | manager_xforms = ipy_shell.input_transformer_manager.python_line_transforms 135 | for xform in splitter_xforms: 136 | if xform.func is parser_func: 137 | splitter_xforms.remove(xform) 138 | break 139 | 140 | for xform in manager_xforms: 141 | if xform.func is parser_func: 142 | manager_xforms.remove(xform) 143 | break 144 | 145 | if ipy_shell.user_ns.get('smuggle') is smuggle_func: 146 | del ipy_shell.user_ns['smuggle'] 147 | 148 | 149 | def generate_parser_func(line_parser): 150 | """ 151 | `IPython<7.0.0`-specific implementation of `generate_parser_func`. 152 | 153 | Given a function that parses a single line of code, returns the full 154 | `davos` parser to be wrapped in an 155 | `IPython.core.inputtransformer.StatelessInputTransformer` and 156 | registered as both an input transformer and input splitter. 157 | Unlike more recent versions, `IPython<7.0.0` expects transformer 158 | functions to accept a single (reassembled) line of input at a time, 159 | so the full parser is simply the single-line parser. 160 | 161 | Parameters 162 | ---------- 163 | line_parser : callable 164 | Function that parses a single line of user code (typically, 165 | `davos.core.core.parse_line`). 166 | 167 | Returns 168 | ------- 169 | callable 170 | The function passed to `line_parser`. 171 | """ 172 | return line_parser 173 | -------------------------------------------------------------------------------- /davos/implementations/ipython_pre7.pyi: -------------------------------------------------------------------------------- 1 | from typing import Literal, TypeVar 2 | from davos.core.core import SmuggleFunc 3 | from davos.implementations import LineParserFunc 4 | 5 | __all__ = list[Literal['generate_parser_func']] 6 | _LPF = TypeVar('_LPF', bound=LineParserFunc) 7 | 8 | def _activate_helper(smuggle_func: SmuggleFunc, parser_func: LineParserFunc) -> None: ... 9 | def _deactivate_helper(smuggle_func: SmuggleFunc, parser_func: LineParserFunc) -> None: ... 10 | def generate_parser_func(line_parser: _LPF) -> _LPF: ... 11 | -------------------------------------------------------------------------------- /davos/implementations/js_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | JavaScript code for interacting with the Jupyter notebook frontend. 3 | 4 | This module contains JavaScript code for various `davos` features that 5 | require manipulating with the `Jupyter.notebook` JS object in the 6 | browser. Note that these features are implemented for Jupyter 7 | notebooks only, as Colaboratory heavily restricts communication between 8 | the IPython kernel and notebook frontend. However, they are implemented 9 | for Jupyter notebooks running all `IPython` versions supported by 10 | `davos`. 11 | 12 | JavaScript functions in this module are organized in a `DotDict` 13 | instance (see class docstring below) whose keys are function names and 14 | whose values are function definitions. Two functions are currently 15 | implemented: 16 | 17 | - `JS_FUNCTIONS.jupyter.restartRunCellsAbove` 18 | Restarts the notebook kernel, and queues all cells above and 19 | including the current (highlighted) cell for execution upon restart. 20 | When using `davos`, this is generally useful when a different 21 | version of a smuggled package was previously imported during the 22 | current interpreter session and cannot be reloaded -- this can 23 | happen when the package includes extension modules that dynamically 24 | link C or C++ objects to the interpreter, and those modules were 25 | changed between the previously-loaded and just-smuggled versions. 26 | 27 | - `JS_FUNCTIONS.jupyter.displayButtonPrompt` 28 | Displays any number of buttons in the output area of the current 29 | cell for user selection and (in tandem with 30 | `davos.implementations.jupyter.prompt_restart_rerun_buttons`) blocks 31 | further code execution until one is clicked. Each button can 32 | optionally trigger a JavaScript-based callback when clicked, and/or 33 | send a "result" value from the notebook frontend to the Python 34 | kernel, which is automatically converted from a JavaScript type to a 35 | Python type. Buttons can also specify text labels and unique `id`s 36 | for their HTML elements. All button elements are removed when one is 37 | clicked, which prevents multiple user selections, removes their 38 | event listeners, and makes their `id`s available for reuse. See 39 | function JSDoc for type info and more details. 40 | """ 41 | 42 | 43 | __all__ = ['DotDict', 'JS_FUNCTIONS'] 44 | 45 | 46 | from collections.abc import MutableMapping 47 | from textwrap import dedent 48 | 49 | 50 | class DotDict(dict): 51 | """Simple `dict` subclass that can be accessed like a JS object""" 52 | 53 | __delattr__ = dict.__delitem__ 54 | __getattr__ = dict.__getitem__ 55 | 56 | # noinspection PyMissingConstructor 57 | # pylint: disable=super-init-not-called 58 | def __init__(self, d): 59 | """ 60 | Parameters 61 | ---------- 62 | d : collections.abc.MutableMapping 63 | A `dict`-like object to be converted to a `DotDict`, 64 | recursively. 65 | """ 66 | for k, v in d.items(): 67 | self[k] = v 68 | 69 | def __setitem__(self, key, value): 70 | if isinstance(value, MutableMapping): 71 | value = DotDict(value) 72 | super().__setitem__(key, value) 73 | 74 | def __setattr__(self, name, value): 75 | self[name] = value 76 | 77 | 78 | # noinspection ThisExpressionReferencesGlobalObjectJS 79 | # (expressions are passed to 80 | # IPython.display.display(IPython.display.Javascript()), wherein `this` 81 | # refers to the top-level output element for the cell from which it was 82 | # invoked) 83 | # noinspection JSUnusedLocalSymbols 84 | # (accept JS functions defined but not called in language injection) 85 | JS_FUNCTIONS = DotDict({ 86 | 'jupyter': { 87 | 'restartRunCellsAbove': dedent(""" 88 | const restartRunCellsAbove = function() { 89 | const outputArea = this, 90 | notebook = Jupyter.notebook, 91 | // first cell currently selected, if multiple 92 | anchorCellIndex = notebook.get_anchor_index(), 93 | // most recently selected cell, if multiple 94 | selectedCellIndex = notebook.get_selected_index(), 95 | runningCell = outputArea.element.parents('.cell'), 96 | allCells = notebook.get_cell_elements(), 97 | runningCellIndex = allCells.index(runningCell); 98 | 99 | const queueCellsAndResetSelection = function() { 100 | /* 101 | * Queue all cells above plus currently running cell. 102 | * Queueing cells unsets currently highlighted selected 103 | * cells, so re-select highlighted cell or group of cells/ 104 | */ 105 | // noinspection JSCheckFunctionSignatures 106 | notebook.execute_cell_range(0, runningCellIndex + 1); 107 | notebook.select(anchorCellIndex); 108 | if (selectedCellIndex !== anchorCellIndex) { 109 | // select multiple cells without moving anchor 110 | notebook.select(selectedCellIndex, false); 111 | } 112 | } 113 | // pass queueCellsAndResetSelection as callback to run after 114 | // notebook kernel restarts and is available 115 | notebook.kernel.restart(queueCellsAndResetSelection); 116 | 117 | // when passed to Ipython.display.display(Ipython.display.Javascript()), 118 | // "this" will be the [class=]"output" element of the cell from 119 | // which it's displayed 120 | }.bind(this) 121 | """), 122 | 'displayButtonPrompt': dedent(""" 123 | /** 124 | * Display one or more buttons on the notebook frontend for user 125 | * selection and (together with kernel-side Python function) block 126 | * until one is clicked. Optionally send a per-button "result" 127 | * value to the IPython kernel's stdin socket to capture user 128 | * selection in a Python variable. 129 | * 130 | * @param {Object[]} buttonArgs - Array of objects containing data 131 | * for each button to be displayed. 132 | * @param {String} [buttonArgs[].text=""] - Text label for the 133 | * given button. Defaults to an empty string. 134 | * @param {BigInt|Boolean|null|Number|Object|String|undefined} [buttonArgs[].result] - 135 | * Value sent to the notebook kernel's stdin socket if the 136 | * given button is clicked and sendResult is true. Used to 137 | * forward user input information to Python. JS types are 138 | * converted to Python types, within reason (Boolean -> bool, 139 | * Object -> dict, Array -> list, null -> None, 140 | * undefined -> '', etc.). If omitted, the return value of 141 | * onClick will be used instead. 142 | * @param {Function} [buttonArgs[].onClick] - Callback executed 143 | * when the given button is clicked, before the result value is 144 | * sent to the IPython kernel and Python execution resumes. 145 | * Omit the result property to use this function's return value 146 | * as a dynamically computed result value. Defaults to a noop. 147 | * @param {String} [buttonArgs[].id] - Optional id for the given 148 | * button element. 149 | * @param {Boolean} [sendResult=false] - Whether to send the result 150 | * value to the IPython kernel's stdin socket as simulated 151 | * user input. 152 | */ 153 | const displayButtonPrompt = async function(buttonArgs, sendResult) { 154 | if (typeof sendResult === 'undefined') { 155 | sendResult = false; 156 | } 157 | let clickedButtonCallback, clickedButtonResult, resolutionFunc; 158 | const outputDisplayArea = element[0], 159 | // store resolve function in outer scope so it can be 160 | // called from an event listener 161 | callbackPromise = new Promise((resolve) => resolutionFunc = resolve ); 162 | 163 | buttonArgs.forEach(function (buttonObj, ix) { 164 | let buttonElement = document.createElement('BUTTON'); 165 | buttonElement.style.marginLeft = '1rem'; 166 | buttonElement.classList.add('davos', 'prompt-button'); 167 | if (typeof buttonObj.id !== 'undefined') { 168 | buttonElement.id = buttonObj.id; 169 | } 170 | if (typeof buttonObj.text === 'undefined') { 171 | buttonElement.textContent = `Button ${ix}`; 172 | } else { 173 | buttonElement.textContent = buttonObj.text; 174 | } 175 | if (typeof buttonObj.onClick === 'undefined') { 176 | // mutating object passed as argument isn't ideal, but 177 | // it's an easy way to make scoping in event listener 178 | // execution work, and should be pretty harmless since 179 | // this is internal use only 180 | buttonObj.onClick = () => {}; 181 | } 182 | buttonElement.addEventListener('click', () => { 183 | // store clicked button's callback & result 184 | clickedButtonCallback = buttonObj.onClick; 185 | clickedButtonResult = buttonObj.result; 186 | // resolve callbackPromise when any button is clicked 187 | resolutionFunc(); 188 | }); 189 | outputDisplayArea.appendChild(buttonElement); 190 | }) 191 | 192 | // attach handler to run when promise is fulfilled, await 193 | // callbackPromise resolution (any button clicked) 194 | await callbackPromise.then(() => { 195 | // remove element in output area containing buttons 196 | outputDisplayArea.remove(); 197 | // execute clicked button's callback, store return value 198 | // (if any) 199 | const CbReturnVal = clickedButtonCallback(); 200 | 201 | if (sendResult === true) { 202 | if (typeof clickedButtonResult === 'undefined') { 203 | // if result should be sent to IPython kernel and 204 | // button's 'result' property was not specified, 205 | // use return value of button's onClick callback 206 | clickedButtonResult = CbReturnVal; 207 | } 208 | // send result value to IPython kernel's stdin 209 | Jupyter.notebook.kernel.send_input_reply(clickedButtonResult); 210 | } 211 | }) 212 | 213 | // when passed to IPython.display.display(Ipython.display.Javascript()), 214 | // "this" will be the [class=]"output" element of the cell from 215 | // which it's displayed 216 | }.bind(this) 217 | """) # noqa: E124 218 | } 219 | }) 220 | -------------------------------------------------------------------------------- /davos/implementations/js_functions.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import MutableMapping 2 | from typing import Generic, Literal, TypeVar 3 | 4 | __all__ = list[Literal['DotDict', 'JS_FUNCTIONS']] 5 | 6 | _T = TypeVar('_T', bound=object) 7 | 8 | class DotDict(dict, Generic[_T]): 9 | def __init__(self, d: MutableMapping) -> None: ... 10 | def __delattr__(self, key: str) -> None: ... 11 | def __getattr__(self, item: str) -> _T | DotDict: ... 12 | def __getitem__(self, key: str) -> _T | DotDict: ... 13 | def __setattr__(self, key: str, value: object) -> None: ... 14 | def __setitem__(self, key: str, value: object) -> None: ... 15 | 16 | JS_FUNCTIONS: DotDict[str] 17 | -------------------------------------------------------------------------------- /davos/implementations/jupyter.py: -------------------------------------------------------------------------------- 1 | """Helper function implementations specific to Jupyter notebooks.""" 2 | 3 | 4 | __all__ = ['auto_restart_rerun', 'prompt_restart_rerun_buttons'] 5 | 6 | 7 | import sys 8 | import time 9 | from textwrap import dedent 10 | 11 | import ipykernel 12 | import zmq 13 | from IPython.display import display, Javascript 14 | 15 | from davos import config 16 | from davos.implementations.js_functions import JS_FUNCTIONS 17 | 18 | 19 | def auto_restart_rerun(pkgs): 20 | """ 21 | Jupyter-specific implementation of `auto_restart_rerun`. 22 | 23 | Automatically restarts the notebook kernel and reruns all cells 24 | above, *including the current cell*. Called when one or more 25 | smuggled `pkgs` that were previously imported cannot be reloaded by 26 | the current interpreter, and `davos.auto_rerun` is set to `True`. 27 | Displays a message in the cell output area with the package(s) that 28 | required the kernel restart, calls 29 | `JS_FUNCTIONS.jupyter.restartRunCellsAbove`, and then blocks until 30 | the kernel is restarted. 31 | 32 | Parameters 33 | ---------- 34 | pkgs : iterable of str 35 | Packages that could not be reloaded without restarting the 36 | kernel. 37 | 38 | See Also 39 | -------- 40 | JS_FUNCTIONS.jupyter.restartRunCellsAbove : 41 | JavaScript function that restarts kernel and reruns cells above 42 | 43 | Notes 44 | ----- 45 | 1. The message displayed before restarting the kernel can be 46 | silenced by setting `davos.suppress_stdout` to `True`. 47 | 2. After calling `JS_FUNCTIONS.jupyter.restartRunCellsAbove`, this 48 | function sleeps until the kernel restarts to prevent any further 49 | code in the current cell or other queued cells from executing. 50 | Restarting the kernel is often not instantaneous; there's 51 | generally a 1-2s delay while the kernel sends & receives various 52 | shutdown messages, but it can take significantly longer on a slow 53 | machine, with older Python/Jupyter/ipykernel versions, if a large 54 | amount of data was loaded into memory, if multiple notebook 55 | kernels are running at once, etc. If this function returned 56 | immediately, it's likely subsequent lines of code would be run 57 | before the kernel disconnected. This can cause problems if those 58 | lines of code use the package(s) that prompted the restart, or 59 | have effects that persist across kernel sessions. 60 | """ 61 | msg = ( 62 | "Restarting kernel and rerunning cells (required to smuggle " 63 | f"{', '.join(pkgs)})..." 64 | ) 65 | 66 | js_full = dedent(f""" 67 | {JS_FUNCTIONS.jupyter.restartRunCellsAbove}; 68 | console.log('restartRunCellsAbove defined'); 69 | 70 | console.log(`{msg}`); 71 | restartRunCellsAbove(); 72 | """) 73 | 74 | if not config.suppress_stdout: 75 | print(f"\033[0;31;1m{msg}\033[0m") 76 | 77 | # flush output before creating display 78 | sys.stdout.flush() 79 | sys.stderr.flush() 80 | 81 | # noinspection PyTypeChecker 82 | display(Javascript(js_full)) 83 | # block execution for clarity -- kernel restart can sometimes take a 84 | # few seconds to trigger, so prevent any queued code from running in 85 | # the interim in case it has effects that persist across kernel 86 | # sessions 87 | while True: 88 | time.sleep(10) 89 | 90 | 91 | def prompt_restart_rerun_buttons(pkgs): 92 | """ 93 | Jupyter-specific implementation of `prompt_restart_rerun_buttons`. 94 | 95 | Displays a warning that the notebook kernel must be restarted in 96 | order to use the just-smuggled version of one or more previously 97 | imported `pkgs`, and displays a pair of buttons (via 98 | `JS_FUNCTIONS.jupyter.displayButtonPrompt`) that prompt the user to 99 | either (a) restart the kernel and rerun all cells up to the current 100 | point, or (b) ignore the warning and continue running. Then, polls 101 | the kernel's stdin socket until it receives a reply from the 102 | notebook frontend, or the kernel is restarted. 103 | 104 | Parameters 105 | ---------- 106 | pkgs : iterable of str 107 | Packages that could not be reloaded without restarting the 108 | kernel. 109 | 110 | Returns 111 | ------- 112 | None 113 | If the user clicks the "Continue Running" button, returns 114 | `None`. Otherwise, restarts the kernel and therefore never 115 | returns. 116 | 117 | See Also 118 | -------- 119 | JS_FUNCTIONS.jupyter.displayButtonPrompt : 120 | JavaScript function for prompting user input with buttons. 121 | ipykernel.kernelbase.Kernel._input_request : 122 | Kernel method that replaces the built-in `input` in notebooks. 123 | 124 | Notes 125 | ----- 126 | This method of blocking and waiting for user input is based on 127 | `ipykernel`'s replacement for the built-in `input` function used in 128 | notebook environments. 129 | """ 130 | # UI: could remove warning message when "continue" button is clicked 131 | msg = ( 132 | "WARNING: The following packages were previously imported by the " 133 | "interpreter and could not be reloaded because their compiled modules " 134 | f"have changed:\n\t[{', '.join(pkgs)}]\nRestart the kernel to use " 135 | "the newly installed version." 136 | ) 137 | 138 | # noinspection JSUnusedLocalSymbols,JSUnresolvedFunction 139 | button_args = dedent(""" 140 | const buttonArgs = [ 141 | { 142 | text: 'Restart Kernel and Rerun Cells', 143 | onClick: () => {restartRunCellsAbove();}, 144 | }, 145 | { 146 | text: 'Continue Running', 147 | result: null, 148 | }, 149 | ] 150 | """) 151 | display_button_prompt_full = dedent(f""" 152 | {JS_FUNCTIONS.jupyter.restartRunCellsAbove}; 153 | console.log('restartRunCellsAbove defined'); 154 | 155 | {JS_FUNCTIONS.jupyter.displayButtonPrompt}; 156 | console.log('displayButtonPrompt defined'); 157 | 158 | {button_args}; 159 | console.warn(`{msg}`); 160 | displayButtonPrompt(buttonArgs, true); 161 | """) 162 | 163 | # get_ipython() exists globally when imported into IPython context 164 | kernel = get_ipython().kernel 165 | stdin_sock = kernel.stdin_socket 166 | 167 | print(f"\033[0;31;1m{msg}\033[0m") 168 | 169 | # flush output before creating button display 170 | sys.stdout.flush() 171 | sys.stderr.flush() 172 | 173 | # flush ipykernel stdin socket to purge stale replies 174 | while True: 175 | try: 176 | # noinspection PyUnresolvedReferences 177 | # (dynamically imported names not included in stub files) 178 | stdin_sock.recv_multipart(zmq.NOBLOCK) 179 | except zmq.ZMQError as e: 180 | # noinspection PyUnresolvedReferences 181 | # (dynamically imported names not included in stub files) 182 | if e.errno == zmq.EAGAIN: 183 | break 184 | raise 185 | 186 | display(Javascript(display_button_prompt_full)) 187 | 188 | while True: 189 | try: 190 | # zmq.select (zmq.sugar.poll.select) args: 191 | # - list of sockets/FDs to be polled for read events 192 | # - list of sockets/FDs to be polled for write events 193 | # - list of sockets/FDs to be polled for error events 194 | # - timeout (in seconds; None implies no timeout) 195 | rlist, _, xlist = zmq.select([stdin_sock], [], [stdin_sock], 0.01) 196 | if rlist or xlist: 197 | ident, reply = kernel.session.recv(stdin_sock) 198 | if ident is not None or reply is not None: 199 | break 200 | except Exception as e: # pylint: disable=broad-except 201 | if isinstance(e, KeyboardInterrupt): 202 | # re-raise KeyboardInterrupt with simplified traceback 203 | # (excludes some convoluted calls to internal 204 | # IPython/zmq machinery) 205 | raise KeyboardInterrupt("Interrupted by user") from None 206 | kernel.log.warning("Invalid Message:", exc_info=True) 207 | 208 | # noinspection PyBroadException 209 | try: 210 | value = reply['content']['value'] 211 | except Exception: # pylint: disable=broad-except 212 | if ipykernel.version_info[0] >= 6: 213 | _parent_header = kernel._parent_ident['shell'] 214 | else: 215 | _parent_header = kernel._parent_ident 216 | kernel.log.error(f"Bad input_reply: {_parent_header}") 217 | value = '' 218 | 219 | if value == '\x04': 220 | # end of transmission 221 | raise EOFError 222 | return value 223 | -------------------------------------------------------------------------------- /davos/implementations/jupyter.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import Literal, NoReturn 3 | 4 | __all__ = list[Literal['auto_restart_rerun', 'prompt_restart_rerun_buttons']] 5 | 6 | def auto_restart_rerun(pkgs: Iterable[str]) -> NoReturn: ... 7 | def prompt_restart_rerun_buttons(pkgs: Iterable[str]) -> None: ... 8 | -------------------------------------------------------------------------------- /davos/implementations/python.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper function implementations specific to "plain" Python environments. 3 | 4 | NOTE: `davos` does not currently support "plain" (i.e., non-interactive) 5 | Python environments (scripts, the IDLE, etc.), but aims to do so in a 6 | future version. 7 | """ 8 | 9 | __all__ = [ 10 | 'auto_restart_rerun', 11 | 'generate_parser_func', 12 | 'prompt_restart_rerun_buttons' 13 | ] 14 | 15 | 16 | import locale 17 | import shlex 18 | import signal 19 | import sys 20 | from contextlib import redirect_stdout 21 | from io import StringIO 22 | from subprocess import CalledProcessError, PIPE, Popen 23 | 24 | 25 | # noinspection PyUnusedLocal 26 | def _activate_helper(smuggle_func, parser_func): 27 | """ 28 | Pure Python implementation of `_activate_helper`. 29 | 30 | Raises `NotImplementedError` whenever called, as `davos` does not 31 | yet support non-interactive Python environments. 32 | 33 | Parameters 34 | ---------- 35 | smuggle_func : callable 36 | Function to be injected into the module namespace under the 37 | name "`smuggle`" (typically, `davos.core.core.smuggle`). 38 | parser_func : callable 39 | Function called to parse the Python module as plain text and 40 | replace `smuggle` statements with the `smuggle()` function. 41 | 42 | Raises 43 | ------- 44 | NotImplementedError 45 | In all cases. 46 | """ 47 | raise NotImplementedError( 48 | "davos does not yet support non-interactive Python environments" 49 | ) 50 | 51 | 52 | def _check_conda_avail_helper(): 53 | """ 54 | Check whether the `conda` executable is available. 55 | 56 | Pure Python implementation of helper function for 57 | `davos.core.core.check_conda`. Tries to access the `conda` 58 | executable by running `conda list IPython`. If successful, returns 59 | the (suppressed) stdout generated by the command. Otherwise, returns 60 | `None`. 61 | 62 | Returns 63 | ------- 64 | str or None 65 | If `conda list IPython` executes successfully, the captured 66 | stdout. Otherwise, `None`. 67 | 68 | Raises 69 | ------ 70 | subprocess.CalledProcessError 71 | If the `conda` executable is not available. 72 | 73 | See Also 74 | -------- 75 | davos.core.core.check_conda : core function that calls this helper. 76 | """ 77 | try: 78 | with redirect_stdout(StringIO()) as conda_list_output: 79 | # using `conda list` instead of a more straightforward 80 | # command so stdout is formatted the same as the IPython 81 | # implementation (which must use `conda list`) 82 | _run_shell_command_helper('conda list Python') 83 | except CalledProcessError: 84 | return None 85 | return conda_list_output.getvalue() 86 | 87 | 88 | # noinspection PyUnusedLocal 89 | def _deactivate_helper(smuggle_func, parser_func): 90 | """ 91 | Pure Python implementation of `_activate_helper`. 92 | 93 | Raises `NotImplementedError` whenever called, as `davos` does not 94 | yet support non-interactive Python environments. 95 | 96 | Parameters 97 | ---------- 98 | smuggle_func : callable 99 | parser_func : callable 100 | 101 | Raises 102 | ------- 103 | NotImplementedError 104 | In all cases. 105 | """ 106 | raise NotImplementedError( 107 | "davos does not yet support non-interactive Python environments" 108 | ) 109 | 110 | 111 | def _run_shell_command_helper(command): 112 | """ 113 | Run a shell command in a subprocess, piping stdout & stderr. 114 | 115 | Pure Python implementation of helper function for 116 | `davos.core.core.run_shell_command`. stdout & stderr streams are 117 | captured or suppressed by the outer function. If the command runs 118 | successfully, return its exit status (`0`). Otherwise, raise an 119 | error. 120 | 121 | Parameters 122 | ---------- 123 | command : str 124 | The command to execute. 125 | 126 | Returns 127 | ------- 128 | int 129 | The exit code of the command. This will always be `0` if the 130 | function returns. Otherwise, an error is raised. 131 | 132 | Raises 133 | ------ 134 | subprocess.CalledProcessError : 135 | If the command returned a non-zero exit status. 136 | """ 137 | cmd = shlex.split(command) 138 | process = Popen(cmd, # pylint: disable=consider-using-with 139 | stdout=PIPE, 140 | stderr=PIPE, 141 | encoding=locale.getpreferredencoding()) 142 | try: 143 | while True: 144 | retcode = process.poll() 145 | if retcode is None: 146 | output = process.stdout.readline() 147 | if output: 148 | sys.stdout.write(output) 149 | elif retcode != 0: 150 | # processed returned with non-zero exit status 151 | raise CalledProcessError(returncode=retcode, cmd=cmd) 152 | except KeyboardInterrupt: 153 | # forward CTRL + C to process before raising 154 | process.send_signal(signal.SIGINT) 155 | raise 156 | 157 | 158 | # noinspection PyUnusedLocal 159 | def auto_restart_rerun(pkgs): 160 | """ 161 | Pure Python implementation of `auto_restart_rerun`. 162 | 163 | Raises `NotImplementedError` whenever called, as `davos` does not 164 | yet support non-interactive Python environments. 165 | 166 | Parameters 167 | ---------- 168 | pkgs : list of str 169 | Packages that could not be reloaded without restarting the 170 | runtime. 171 | 172 | Raises 173 | ------- 174 | NotImplementedError 175 | In all cases. 176 | """ 177 | raise NotImplementedError( 178 | "automatic rerunning not available in non-interactive Python (this " 179 | "function should not be reachable through normal use)." 180 | ) 181 | 182 | 183 | # noinspection PyUnusedLocal 184 | def generate_parser_func(line_parser): 185 | """ 186 | Pure Python implementation of `generate_parser_func`. 187 | 188 | Raises `NotImplementedError` whenever called, as `davos` does not 189 | yet support non-interactive Python environments. 190 | 191 | Parameters 192 | ---------- 193 | line_parser : callable 194 | Function that parses a single line of user code (typically, 195 | `davos.core.core.parse_line`). 196 | 197 | Raises 198 | ------- 199 | NotImplementedError 200 | In all cases. 201 | """ 202 | raise NotImplementedError( 203 | "davos does not yet support non-interactive Python environments" 204 | ) 205 | 206 | 207 | # noinspection PyUnusedLocal 208 | def prompt_restart_rerun_buttons(pkgs): 209 | """ 210 | Pure Python implementation of `prompt_restart_rerun_buttons`. 211 | 212 | Raises `NotImplementedError` whenever called, as button-based input 213 | prompts are not available outside notebook environments. 214 | 215 | Parameters 216 | ---------- 217 | pkgs : list of str 218 | Packages that could not be reloaded without restarting the 219 | runtime. 220 | 221 | Raises 222 | ------- 223 | NotImplementedError 224 | In all cases. 225 | """ 226 | raise NotImplementedError( 227 | "button-based user input prompts are not available in non-interactive " 228 | "Python (this function should not be reachable through normal use)." 229 | ) 230 | -------------------------------------------------------------------------------- /davos/implementations/python.pyi: -------------------------------------------------------------------------------- 1 | from typing import Literal, NoReturn 2 | from davos.core.core import SmuggleFunc 3 | from davos.implementations import LineParserFunc 4 | 5 | __all__ = list[Literal['auto_restart_rerun', 'generate_parser_func', 'prompt_restart_rerun_buttons']] 6 | 7 | def _activate_helper(smuggle_func: SmuggleFunc, parser_func: LineParserFunc) -> NoReturn: ... 8 | def _check_conda_avail_helper() -> str | None: ... 9 | def _deactivate_helper(smuggle_func: SmuggleFunc, parser_func: LineParserFunc) -> NoReturn: ... 10 | def _run_shell_command_helper(command: str) -> None: ... 11 | def auto_restart_rerun(pkgs: list[str]) -> NoReturn: ... 12 | def generate_parser_func(line_parser: LineParserFunc) -> NoReturn: ... 13 | def prompt_restart_rerun_buttons(pkgs: list[str]) -> NoReturn: ... 14 | -------------------------------------------------------------------------------- /davos/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/davos/py.typed -------------------------------------------------------------------------------- /paper/admin/cover_letter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/admin/cover_letter.pdf -------------------------------------------------------------------------------- /paper/admin/cover_letter.tex: -------------------------------------------------------------------------------- 1 | \title{cover letter} 2 | % 3 | % See http://texblog.org/2013/11/11/latexs-alternative-letter-class-newlfm/ 4 | % and http://www.ctan.org/tex-archive/macros/latex/contrib/newlfm 5 | % for more information. 6 | % 7 | \documentclass[11pt,stdletter,orderfromtodate,sigleft]{newlfm} 8 | \let\geometry\relax 9 | \usepackage{hyperref, pxfonts, geometry} 10 | 11 | \setlength{\voffset}{0in} 12 | 13 | \newlfmP{dateskipbefore=0pt} 14 | \newlfmP{sigsize=20pt} 15 | \newlfmP{sigskipbefore=10pt} 16 | 17 | \newlfmP{Headlinewd=0pt,Footlinewd=0pt} 18 | 19 | \namefrom{\vspace{-0.3in}Jeremy R. Manning} 20 | \addrfrom{ 21 | Dartmouth College\\ 22 | Department of Psychological \& Brain Sciences\\ 23 | HB 6207 Moore Hall\\ 24 | Hanover, NH 03755} 25 | 26 | \addrto{} 27 | \dateset{\today} 28 | 29 | \greetto{To the editors of \textit{SoftwareX}:} 30 | 31 | 32 | 33 | \closeline{Sincerely,} 34 | %\usepackage{setspace} 35 | %\linespread{0.85} 36 | % The cover letter should explain the importance of the work, and why you consider it appropriate for the diverse readership of . 37 | % OPTIONAL. Provide the name and institution of reviewers you would like to recommend and/or people you would like to be excluded from peer review (explaining why). 38 | 39 | \begin{document} 40 | \begin{newlfm} 41 | 42 | We have enclosed our manuscript entitled \textit{\texttt{davos}: a 43 | Python package ``smuggler'' for constructing lightweight 44 | reproducible notebooks} to be considered for publication as 45 | an \textit{Original Software Publication}. 46 | 47 | Our manuscript describes a new Python package, \texttt{davos}. When 48 | used in combination with a notebook-based Python project, the 49 | \texttt{davos} library provides tools for specifying and 50 | automatically installing the correct versions of the project's 51 | dependencies. Our library also ensures that the correct versions of 52 | those dependencies are in use any time the notebook's code is 53 | executed. This enables researchers to share a complete reproducible 54 | copy of their code within a single Jupyter notebook (\texttt{.ipynb}) file. 55 | 56 | Broadly, we designed the \texttt{davos} library to target a ``sweet 57 | spot'' along a continuum of existing approaches to facilitating reproducible 58 | code-based research. At one end of this continuum, ``lightweight'' 59 | approaches entail simply sharing raw code (i.e., plain-text Python 60 | scripts) or Jupyter notebooks (which can contain a mix 61 | of text, code, and embedded media). These lightweight solutions 62 | benefit from very low setup costs (which increase accessibility), 63 | but they typically do not make any attempt to manage or constrain 64 | the computing environment in which the shared code is executed. 65 | At best, when dependencies are missing on the end user's system, shared code 66 | may fail to run entirely. And when the \textit{versions} of a 67 | project's dependencies differ between the original author's system and 68 | the end user's system, shared code may (at worst) behave in 69 | unexpected ways or even cause damage. 70 | 71 | At the other end of this continuum, ``heavyweight'' approaches entail 72 | simulating or replicating, to varying depths, the original computing 73 | environment in which the shared code was developed. For example, 74 | virtual environments, containerized systems, and virtual machines 75 | reproduce (respectively) a complete Python environment, operating system, and/or 76 | full hardware simulation of the original environment. Each of these 77 | systems guarantees, to varying degrees, that shared code will behave 78 | as expected for the end user. A downside to these approaches is that 79 | they are often effort- and/or resource-intensive, since they require 80 | installing and learning additional tools (e.g., Anaconda, Docker, machine 81 | emulators, etc.), as well as writing and distributing additional 82 | configuration files (e.g., environment configuration files, 83 | Dockerfiles, system images, etc.), in order to share and run reproducible code. 84 | 85 | The \texttt{davos} library is lightweight in the sense that it does 86 | not require any setup beyond that required to run standard Jupyter 87 | notebooks. But \texttt{davos} also provides infrastructure for 88 | precisely controlling project dependencies in a way that can easily 89 | be embedded into standard notebooks. This provides a complete system 90 | for sharing reproducible code inside of a standard notebook file. 91 | 92 | Beyond its intended primary role in facilitating reproducible 93 | research, \texttt{davos} is also useful in pedagogical settings 94 | (e.g., courses that involve programming in notebook-based 95 | environments), or when putting together lightweight notebook-based 96 | demonstrations. 97 | 98 | Thank you for considering our manuscript, and we 99 | hope you will find it suitable for publication in \textit{SoftwareX}. 100 | 101 | 102 | \end{newlfm} 103 | \end{document} 104 | -------------------------------------------------------------------------------- /paper/admin/declarationStatement.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/admin/declarationStatement.docx -------------------------------------------------------------------------------- /paper/changes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/changes.pdf -------------------------------------------------------------------------------- /paper/figs/example1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example1.pdf -------------------------------------------------------------------------------- /paper/figs/example2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example2.pdf -------------------------------------------------------------------------------- /paper/figs/example3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example3.pdf -------------------------------------------------------------------------------- /paper/figs/example4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example4.pdf -------------------------------------------------------------------------------- /paper/figs/example5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example5.pdf -------------------------------------------------------------------------------- /paper/figs/example6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example6.pdf -------------------------------------------------------------------------------- /paper/figs/example7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example7.pdf -------------------------------------------------------------------------------- /paper/figs/example8.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/example8.pdf -------------------------------------------------------------------------------- /paper/figs/flow_chart.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/flow_chart.pdf -------------------------------------------------------------------------------- /paper/figs/illustrative_example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/illustrative_example.pdf -------------------------------------------------------------------------------- /paper/figs/package_structure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/package_structure.pdf -------------------------------------------------------------------------------- /paper/figs/shareable_code.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/shareable_code.pdf -------------------------------------------------------------------------------- /paper/figs/shareable_code_2d.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/shareable_code_2d.pdf -------------------------------------------------------------------------------- /paper/figs/snippet1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet1.pdf -------------------------------------------------------------------------------- /paper/figs/snippet2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet2.pdf -------------------------------------------------------------------------------- /paper/figs/snippet3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet3.pdf -------------------------------------------------------------------------------- /paper/figs/snippet4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet4.pdf -------------------------------------------------------------------------------- /paper/figs/snippet5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet5.pdf -------------------------------------------------------------------------------- /paper/figs/snippet6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet6.pdf -------------------------------------------------------------------------------- /paper/figs/snippet7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippet7.pdf -------------------------------------------------------------------------------- /paper/figs/snippets.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/snippets.pdf -------------------------------------------------------------------------------- /paper/figs/source/shareable_code_base.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/figs/source/shareable_code_base.pdf -------------------------------------------------------------------------------- /paper/main.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/main.pdf -------------------------------------------------------------------------------- /paper/old/main-old.bib: -------------------------------------------------------------------------------- 1 | 2 | 3 | @article{vanR95, 4 | author = {Guido {van Rossum}}, 5 | force = {True}, 6 | journal = {Department of Computer Science {[CS]}}, 7 | number = {R 9525}, 8 | publisher = {{CWI}}, 9 | title = {{Python} reference manual}, 10 | year = {1995}} 11 | 12 | @misc{Pyth03, 13 | author = {{Python Software Foundation}}, 14 | force = {True}, 15 | howpublished = {\url{https://pypi.org}}, 16 | publisher = {Python Software Foundation}, 17 | title = {The {P}ython {P}ackage {I}ndex ({PyPI})}, 18 | year = {2003}} 19 | 20 | @misc{cond15, 21 | author = {conda-forge community}, 22 | doi = {10.5281/zenodo.4774217}, 23 | force = {True}, 24 | howpublished = {\url{https://doi.org/10.5281/zenodo.4774217}}, 25 | month = {July}, 26 | publisher = {Zenodo}, 27 | title = {{The conda-forge Project: Community-based Software Distribution Built on the conda Package Format and Ecosystem}}, 28 | year = {2015}} 29 | 30 | @techreport{CoghStuf13, 31 | author = {Nick Coghlan and Donald Stufft}, 32 | institution = {{Python} Software Foundation}, 33 | month = {March}, 34 | number = {440}, 35 | title = {Version {I}dentification and {D}ependency {S}pecification}, 36 | type = {PEP}, 37 | year = {2013}} 38 | 39 | @techreport{CannEtal16, 40 | author = {Brett Cannon and Nathaniel Smith and Donald Stufft}, 41 | institution = {Python Software Foundation}, 42 | month = {May}, 43 | number = {518}, 44 | title = {Specifying {M}inimum {B}uild {S}ystem {R}equirements for {Python} {P}rojects}, 45 | type = {PEP}, 46 | year = {2016}} 47 | 48 | @misc{Anac12, 49 | author = {{Anaconda, Inc.}}, 50 | force = {True}, 51 | howpublished = {\url{https://docs.conda.io}}, 52 | title = {{conda}}, 53 | year = {2012}} 54 | 55 | @misc{Eust19, 56 | author = {S{\'{e}}bastien Eustace}, 57 | howpublished = {\url{https://github.com/python-poetry/poetry}}, 58 | month = {December}, 59 | title = {Poetry: {Python} packaging and dependency management made easy}, 60 | year = {2019}} 61 | 62 | @inproceedings{KluyEtal16, 63 | address = {Netherlands}, 64 | author = {Thomas Kluyver and Benjamin Ragan-Kelley and Fernando P{\'e}rez and Brian Granger and Matthias Bussonnier and Jonathan Frederic and Kyle Kelley and Jessica Hamrick and Jason Grout and Sylvain Corlay and Paul Ivanov and Dami{\'a}n Avila and Safia Abdalla and Carol Willing}, 65 | booktitle = {Positioning and Power in Academic Publishing: Players, Agents and Agendas}, 66 | doi = {10.3233/978-1-61499-649-1-87}, 67 | editor = {Fernando Loizides and Birgit Scmidt}, 68 | pages = {87--90}, 69 | publisher = {{IOS} Press}, 70 | title = {Jupyter {N}otebooks -- a publishing format for reproducible computational workflows}, 71 | year = {2016}} 72 | 73 | @article{Gold74, 74 | author = {Robert P Goldberg}, 75 | journal = {Computer}, 76 | number = {6}, 77 | pages = {34--45}, 78 | publisher = {{IEEE}}, 79 | title = {Survey of virtual machine research}, 80 | volume = {7}, 81 | year = {1974}} 82 | 83 | @article{AltiEtal05, 84 | author = {Y Altintas and C Brecher and M Weck and S Witt}, 85 | doi = {https://doi.org/10.1016/S0007-8506(07)60022-5}, 86 | journal = {{CIRP} Annals}, 87 | number = {2}, 88 | pages = {115--138}, 89 | title = {Virtual {M}achine {T}ool}, 90 | volume = {54}, 91 | year = {2005}} 92 | 93 | @inproceedings{Rose99, 94 | author = {Mendel Rosenblum}, 95 | booktitle = {{IEEE} Hot Chips Symposium}, 96 | force = {True}, 97 | pages = {185--196}, 98 | publisher = {{IEEE}}, 99 | title = {{VMware's Virtual Platform: A virtual machine monitor for commodity PCs}}, 100 | year = {1999}} 101 | 102 | @article{Merk14, 103 | author = {Dirk Merkel}, 104 | journal = {Linux Journal}, 105 | month = {March}, 106 | number = {2}, 107 | pages = {2}, 108 | title = {Docker: lightweight linux containers for consistent development and deployment}, 109 | volume = {239}, 110 | year = {2014}} 111 | 112 | @article{KurtEtal17, 113 | author = {Gregory M Kurtzer and Vanessa Sochat and Michael W Bauer}, 114 | journal = {{PLoS} One}, 115 | number = {5}, 116 | pages = {e0177459}, 117 | publisher = {Public Library of Science San Francisco, {CA} {USA}}, 118 | title = {Singularity: {S}cientific containers for mobility of compute}, 119 | volume = {12}, 120 | year = {2017}} 121 | 122 | @book{Mart98, 123 | author = {G R R Martin}, 124 | month = {November}, 125 | publisher = {Voyager Books}, 126 | series = {A Song of Ice and Fire}, 127 | title = {A {C}lash of {K}ings}, 128 | year = {1998}} 129 | 130 | @article{PereGran07, 131 | author = {F P{\'e}rez and B E Granger}, 132 | doi = {10.1109/MCSE.2007.53}, 133 | journal = {Computing in {s}cience and {e}ngineering}, 134 | number = {3}, 135 | pages = {21--29}, 136 | title = {I{P}ython: a system for interactive scientific computing}, 137 | volume = {9}, 138 | year = {2007}} 139 | 140 | @techreport{vanREtal14, 141 | author = {Guido {van Rossum} and Jukka Lehtosalo and {\L}ukasz Langa}, 142 | institution = {Python Software Foundation}, 143 | month = {September}, 144 | number = {484}, 145 | title = {Type {Hints}}, 146 | type = {PEP}, 147 | year = {2014}} 148 | 149 | @misc{TorvHama05, 150 | author = {Linus Torvalds and Junio Hamano}, 151 | howpublished = {\url{https://git.kernel.org/pub/scm/git/git.git}}, 152 | month = {April}, 153 | title = {Git: {F}ast version control system}, 154 | year = {2005}} 155 | 156 | @inproceedings{McKi10, 157 | author = {Wes McKinney}, 158 | booktitle = {Proceedings of the 9th {Python} in Science Conference}, 159 | doi = {10.25080/Majora-92bf1922-00a}, 160 | editor = {St\'efan van der Walt and Jarrod Millman}, 161 | pages = {56--61}, 162 | title = {Data {S}tructures for {S}tatistical {C}omputing in {P}ython}, 163 | year = {2010}} 164 | 165 | @article{PedrEtal11, 166 | author = {F Pedregosa and G Varoquaux and A Gramfort and V Michel and B Thirion and O Grisel and M Blondel and P Prettenhofer and R Weiss and V Dubourg and J Vanderplas and A Passos and D Cournapeau and M Brucher and M Perrot and E Duchesnay}, 167 | journal = {Journal of Machine Learning Research}, 168 | pages = {2825--2830}, 169 | title = {Scikit-learn: machine learning in {P}ython}, 170 | volume = {12}, 171 | year = {2011}} 172 | 173 | @misc{Varo10, 174 | author = {Ga\"{e}l Varoquaux}, 175 | howpublished = {\url{https://github.com/joblib/joblib}}, 176 | month = {July}, 177 | title = {Joblib: {C}omputing with {P}ython functions}, 178 | year = {2010}} 179 | 180 | @article{HarrEtal20, 181 | author = {Charles R Harris and K Jarrod Millman and St{\'{e}}fan J van der Walt and Ralf Gommers and Pauli Virtanen and David Cournapeau and Eric Wieser and Julian Taylor and Sebastian Berg and Nathaniel J Smith and Robert Kern and Matti Picus and Stephan Hoyer and Marten H van Kerkwijk and Matthew Brett and Allan Haldane and Jaime Fern{\'{a}}ndez del R{\'{i}}o and Mark Wiebe and Pearu Peterson and Pierre G{\'{e}}rard-Marchant and Kevin Sheppard and Tyler Reddy and Warren Weckesser and Hameer Abbasi and Christoph Gohlke and Travis E Oliphant}, 182 | doi = {10.1038/s41586-020-2649-2}, 183 | journal = {Nature}, 184 | number = {7825}, 185 | pages = {357--362}, 186 | title = {Array programming with {NumPy}}, 187 | volume = {585}, 188 | year = {2020}} 189 | 190 | @misc{AbadEtal15, 191 | author = {Mart\'{i}n~Abadi and Ashish~Agarwal and Paul~Barham and Eugene~Brevdo and Zhifeng~Chen and Craig~Citro and Greg~S.~Corrado and Andy~Davis and Jeffrey~Dean and Matthieu~Devin and Sanjay~Ghemawat and Ian~Goodfellow and Andrew~Harp and Geoffrey~Irving and Michael~Isard and Yangqing Jia and Rafal~Jozefowicz and Lukasz~Kaiser and Manjunath~Kudlur and Josh~Levenberg and Dandelion~Man\'{e} and Rajat~Monga and Sherry~Moore and Derek~Murray and Chris~Olah and Mike~Schuster and Jonathon~Shlens and Benoit~Steiner and Ilya~Sutskever and Kunal~Talwar and Paul~Tucker and Vincent~Vanhoucke and Vijay~Vasudevan and Fernanda~Vi\'{e}gas and Oriol~Vinyals and Pete~Warden and Martin~Wattenberg and Martin~Wicke and Yuan~Yu and Xiaoqiang~Zheng}, 192 | force = {True}, 193 | note = {Software available from tensorflow.org}, 194 | title = {{TensorFlow: Large-Scale Machine Learning on Heterogeneous Systems}}, 195 | url = {https://www.tensorflow.org/}, 196 | year = {2015}} 197 | 198 | @article{McInEtal18b, 199 | author = {L McInnes and J Healy and N Saul and L Gro{\ss}berger}, 200 | doi = {https://doi.org/10.21105/joss.00861}, 201 | journal = {Journal of Open Source Software}, 202 | number = {29}, 203 | pages = {861}, 204 | title = {{UMAP}: {U}niform {M}anifold {A}pproximation and {P}rojection}, 205 | volume = {3}, 206 | year = {2018}} 207 | 208 | @article{Hunt07, 209 | author = {J D Hunter}, 210 | doi = {10.1109/MCSE.2007.55}, 211 | journal = {Computing in Science and Engineering}, 212 | number = {3}, 213 | pages = {90--95}, 214 | title = {Matplotlib: A {2D} graphics environment}, 215 | volume = {9}, 216 | year = {2007}} 217 | 218 | @article{Wask21, 219 | author = {Michael L Waskom}, 220 | doi = {10.21105/joss.03021}, 221 | journal = {Journal of Open Source Software}, 222 | number = {60}, 223 | pages = {3021}, 224 | title = {{s}eaborn: statistical data visualization}, 225 | volume = {6}, 226 | year = {2021}} 227 | 228 | @article{HeusEtal17, 229 | author = {A C Heusser and P C Fitzpatrick and C E Field and K Ziman and J R Manning}, 230 | journal = {Journal of Open Source Software}, 231 | title = {Quail: a {Python} toolbox for analyzing and plotting free recall data}, 232 | volume = {10.21105/joss.00424}, 233 | year = {2017}} 234 | 235 | @misc{FredEtal15, 236 | author = {J Frederic and J Grout and {Jupyter Widgets Contributors}}, 237 | force = {True}, 238 | howpublished = {\url{https://github.com/jupyter-widgets/ipywidgets}}, 239 | month = {August}, 240 | title = {{ipywidgets: Interactive Widgets for the Jupyter Notebook}}, 241 | year = {2015}} 242 | 243 | @misc{daCoEtal22, 244 | author = {Casper da Costa-Luis and Stephen Karl Larroque and Kyle Altendorf and Hadrien Mary and richardsheridan and Mikhail Korobov and Noam Raphael and Ivan Ivanov and Marcel Bargull and Nishant Rodrigues and Guangshuo Chen and Antony Lee and Charles Newey and CrazyPython and {JC} and Martin Zugnoni and Matthew D. Pagel and mjstevens777 and Mikhail Dektyarev and Alex Rothberg and Alexander Plavin and Daniel Panteleit and Fabian Dill and FichteFoll and Gregor Sturm and HeoHeo and Hugo van Kemenade and Jack McCracken and MapleCCC and Max Nordlund}, 245 | doi = {10.5281/zenodo.595120}, 246 | force = {True}, 247 | howpublished = {\url{https://github.com/tqdm/tqdm}}, 248 | month = {September}, 249 | title = {{tqdm}: A {F}ast, {E}xtensible {P}rogress {B}ar for {P}ython and {CLI}}, 250 | year = {2022}} 251 | 252 | @article{BleiEtal03, 253 | author = {D M Blei and A Y Ng and M I Jordan}, 254 | journal = {Journal of Machine Learning Research}, 255 | pages = {993--1022}, 256 | title = {Latent dirichlet allocation}, 257 | volume = {3}, 258 | year = {2003}} 259 | 260 | @misc{Mann21d, 261 | author = {J R Manning}, 262 | doi = {10.5281/zenodo.5182775}, 263 | howpublished = {\url{https://github.com/ContextLab/storytelling-with-data}}, 264 | month = {June}, 265 | publisher = {Zenodo}, 266 | title = {Storytelling with {D}ata}, 267 | year = {2021}} 268 | 269 | @misc{Mann22, 270 | author = {Jeremy Manning}, 271 | doi = {10.5281/zenodo.6596762}, 272 | force = {True}, 273 | howpublished = {\url{https://github.com/ContextLab/experimental-psychology/tree/v1.0}}, 274 | month = {May}, 275 | publisher = {Zenodo}, 276 | title = {{ContextLab/experimental-psychology: v1.0 (Spring, 2022)}}, 277 | year = {2022}} 278 | 279 | @misc{Mann21e, 280 | author = {J R Manning}, 281 | doi = {10.5281/zenodo.7261831}, 282 | force = {True}, 283 | howpublished = {\url{https://github.com/ContextLab/abstract2paper}}, 284 | month = {June}, 285 | title = {{abstract2paper}}, 286 | year = {2021}} 287 | 288 | @article{GaoEtal20, 289 | author = {Gao, Leo and Biderman, Stella and Black, Sid and Golding, Laurence and Hoppe, Travis and Foster, Charles and Phang, Jason and He, Horace and Thite, Anish and Nabeshima, Noa and Presser, Shawn and Leahy, Connor}, 290 | force = {True}, 291 | journal = {{arXiv} preprint ar{X}iv:2101.00027}, 292 | title = {{The Pile: An 800GB Dataset of Diverse Text for Language Modeling}}, 293 | year = {2020}} 294 | 295 | @misc{BlacEtal21, 296 | author = {Black, Sid and Gao, Leo and Wang, Phil and Leahy, Connor and Biderman, Stella}, 297 | force = {True}, 298 | howpublished = {\url{http://github.com/eleutherai/gpt-neo}}, 299 | title = {{GPT-Neo: Large Scale Autoregressive Language Modeling with Mesh-Tensorflow}}, 300 | version = {1.0}, 301 | year = {2021}} 302 | 303 | @article{HeusEtal18a, 304 | author = {A C Heusser and K Ziman and L L W Owen and J R Manning}, 305 | journal = {Journal of Machine Learning Research}, 306 | number = {152}, 307 | pages = {1--6}, 308 | title = {{HyperTools}: a {Python} toolbox for gaining geometric insights into high-dimensional data}, 309 | volume = {18}, 310 | year = {2018}} 311 | 312 | @techreport{Shan20, 313 | author = {M Shannon}, 314 | institution = {Python Software Foundation}, 315 | month = {September}, 316 | number = {638}, 317 | title = {Syntactic {M}acros}, 318 | type = {Draft PEP}, 319 | year = {2020}} 320 | -------------------------------------------------------------------------------- /paper/old/main-old.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ContextLab/davos/0608a40b25995b07e9edf4b9e0098b675187a7aa/paper/old/main-old.pdf -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=46.4", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # ====================================== 6 | 7 | [tool.mypy] 8 | python_version = "3.10" 9 | disable_error_code = "override" 10 | disallow_any_expr = true 11 | disallow_untyped_calls = true 12 | disallow_untyped_defs = true 13 | disallow_incomplete_defs = true 14 | disallow_untyped_decorators = true 15 | no_implicit_optional = true 16 | warn_redundant_casts = true 17 | warn_unused_ignores = true 18 | warn_return_any = true 19 | warn_unreachable = true 20 | show_error_codes = true 21 | show_absolute_path = true 22 | 23 | # ====================================== 24 | [tool.pylint.master] 25 | load-plugins = ["pylint.extensions.overlapping_exceptions"] 26 | 27 | [tool.pylint.basic] 28 | good-names = ["e", "i", "ix", "k", "ns", "p", "tb", "v"] 29 | 30 | [tool.pylint.messages_control] 31 | disable = [ 32 | "cyclic-import", 33 | "fixme", 34 | "import-outside-toplevel", 35 | "too-many-ancestors", 36 | "too-many-arguments", 37 | "too-many-branches", 38 | "too-many-locals", 39 | "too-many-return-statements", 40 | "too-many-statements", 41 | "wrong-import-position" 42 | ] 43 | 44 | [tool.pylint.classes] 45 | exclude-protected = [ 46 | # davos.core.config.DavosConfig attributes 47 | "_active", 48 | "_conda_avail", 49 | "_conda_env", 50 | "_conda_envs_dirs", 51 | "_ipy_showsyntaxerror_orig", 52 | "_ipython_shell", 53 | "_pip_executable", 54 | "_smuggled", 55 | "_stdlib_modules", 56 | # IPython.core.interactiveshell.InteractiveShell methods 57 | "_get_exc_info", 58 | "_showtraceback", 59 | # IPython custom display method for Exception classes 60 | "_render_traceback_", 61 | # ipykernel.ipkernel.IPythonKernel attribute 62 | "_parent_ident" 63 | ] 64 | 65 | [tool.pylint.design] 66 | max-attributes = 20 67 | 68 | [tool.pylint.typecheck] 69 | generated-members = ["zmq.EAGAIN", "NOBLOCK"] 70 | 71 | [tool.pylint.variables] 72 | additional-builtins = ["get_ipython"] 73 | allowed-redefined-builtins = ["help"] 74 | 75 | [tool.pytest.ini_options] 76 | addopts = "--capture=no --strict-markers --verbose" 77 | markers = [ 78 | "colab: marks tests that should run only on Google Colab", 79 | "jupyter: marks tests that should run only in Jupyter notebooks", 80 | "ipython_pre7: marks tests that should run only if IPython<7.0.0", 81 | "ipython_post7: marks tests that should run only if IPython>=7.0.0", 82 | "timeout: marks tests that should fail after a certain amount of time" 83 | ] 84 | 85 | [tool.codespell] 86 | skip = '.git,*.pdf,*.svg,*.bst,*.cls' 87 | ignore-regex = 'doesnt/exist|Ser Davos' 88 | # 89 | ignore-words-list = 'covert,dateset' 90 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = davos 3 | version = 0.2.3 4 | description = Install and manage Python packages at runtime using the "smuggle" statement. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | author = Paxton Fitzpatrick, Jeremy Manning 8 | author_email = contextualdynamics@gmail.com 9 | url = https://github.com/ContextLab/davos 10 | download_url = https://github.com/ContextLab/davos 11 | license = MIT 12 | license_file = LICENSE 13 | keywords = import install package module automatic davos smuggle pip conda 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Framework :: IPython 17 | Framework :: Jupyter 18 | Framework :: Jupyter :: JupyterLab 19 | Intended Audience :: Developers 20 | Intended Audience :: Education 21 | Intended Audience :: Science/Research 22 | License :: OSI Approved :: MIT License 23 | Operating System :: MacOS 24 | Operating System :: POSIX 25 | Operating System :: Unix 26 | Programming Language :: Python :: 3 :: Only 27 | Programming Language :: Python :: 3.6 28 | Programming Language :: Python :: 3.7 29 | Programming Language :: Python :: 3.8 30 | Programming Language :: Python :: 3.9 31 | Programming Language :: Python :: 3.10 32 | Programming Language :: Python :: 3.11 33 | Topic :: System :: Archiving :: Packaging 34 | Topic :: System :: Filesystems 35 | Topic :: System :: Installation/Setup 36 | Topic :: Utilities 37 | Typing :: Typed 38 | 39 | [options] 40 | python_requires = >=3.6 41 | install_requires = 42 | importlib_metadata;python_version<"3.8" 43 | packaging 44 | setup_requires = setuptools>=42.0.2 45 | packages = find: 46 | include_package_data = true 47 | zip_safe = false 48 | 49 | [options.extras_require] 50 | tests = 51 | google-colab 52 | IPython>=7.15;python_version>="3.9" 53 | IPython>=7.3.0;python_version>="3.8" 54 | IPython>=5.5.0 55 | ipykernel>=5.0.0 56 | mypy==1.1.1 57 | pytest==6.2 58 | requests 59 | selenium>=3.141 60 | typing_extensions;python_version<"3.9" 61 | 62 | [options.package_data] 63 | * = py.typed, *.pyi 64 | 65 | [bdist_wheel] 66 | # not compatible with Python 2.x 67 | universal = 0 68 | -------------------------------------------------------------------------------- /tests/test__environment_and_init.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2021-07-06T01:39:18.077407Z", 9 | "start_time": "2021-07-06T01:39:18.069414Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "GITHUB_USERNAME = \"$GITHUB_USERNAME$\"\n", 15 | "GITHUB_REF = \"$GITHUB_REF$\"\n", 16 | "NOTEBOOK_TYPE = \"$NOTEBOOK_TYPE$\"\n", 17 | "PYTHON_VERSION = \"$PYTHON_VERSION$\"\n", 18 | "IPYTHON_VERSION = \"$IPYTHON_VERSION$\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "ExecuteTime": { 26 | "end_time": "2021-07-06T01:42:49.171770Z", 27 | "start_time": "2021-07-06T01:42:49.036805Z" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "import warnings\n", 33 | "from pathlib import Path\n", 34 | "\n", 35 | "import requests\n", 36 | "\n", 37 | "\n", 38 | "warnings.filterwarnings('error', module='davos')\n", 39 | "\n", 40 | "if NOTEBOOK_TYPE == 'colab':\n", 41 | " # utils module doesn't exist on colab VM, so get current version from GitHub\n", 42 | " utils_module = Path('utils.py').resolve()\n", 43 | " response = requests.get(f'https://raw.githubusercontent.com/{GITHUB_USERNAME}/davos/{GITHUB_REF}/tests/utils.py')\n", 44 | " utils_module.write_text(response.text)\n", 45 | " # also need to install davos locally\n", 46 | " from utils import install_davos\n", 47 | " install_davos(source='github', ref=GITHUB_REF, fork=GITHUB_USERNAME)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "ExecuteTime": { 55 | "end_time": "2021-07-06T02:12:59.681734Z", 56 | "start_time": "2021-07-06T02:12:59.679070Z" 57 | } 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "import inspect\n", 62 | "import json\n", 63 | "import subprocess\n", 64 | "import sys\n", 65 | "\n", 66 | "if sys.version_info < (3, 8):\n", 67 | " import importlib_metadata as metadata\n", 68 | "else:\n", 69 | " from importlib import metadata\n", 70 | "\n", 71 | "import davos\n", 72 | "import IPython\n", 73 | "from packaging.specifiers import SpecifierSet\n", 74 | "\n", 75 | "from utils import (\n", 76 | " is_imported, \n", 77 | " is_installed, \n", 78 | " mark, \n", 79 | " raises, \n", 80 | " run_tests, \n", 81 | " TestingEnvironmentError\n", 82 | ")" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "IPYTHON_SHELL = get_ipython()" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "# tests for general testing environment & package initialization\n", 99 | "tests GitHub runner itself, as well as contents of `__init__.py` & `implementations.__init__.py`" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": { 106 | "ExecuteTime": { 107 | "end_time": "2021-07-06T03:20:05.668719Z", 108 | "start_time": "2021-07-06T03:20:05.666297Z" 109 | } 110 | }, 111 | "outputs": [], 112 | "source": [ 113 | "def test_import_davos():\n", 114 | " global davos\n", 115 | " import davos\n", 116 | " assert is_imported('davos')" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "def test_expected_python_version():\n", 126 | " installed_version = '.'.join(map(str, sys.version_info[:2]))\n", 127 | " expected_version = PYTHON_VERSION\n", 128 | " if installed_version != expected_version:\n", 129 | " raise TestingEnvironmentError(\n", 130 | " f\"Test environment has Python {sys.version.split()[0]}, expected \"\n", 131 | " \"{PYTHON_VERSION}\"\n", 132 | " )" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "@mark.jupyter\n", 142 | "def test_notebook_using_kernel_python():\n", 143 | " if not sys.executable.endswith('envs/kernel-env/bin/python'):\n", 144 | " raise TestingEnvironmentError(\n", 145 | " \"Notebook does not appear to be using the correct python \"\n", 146 | " \"executable. Expected a path ending in \"\n", 147 | " f\"'envs/kernel-env/bin/python', found {sys.executable}\"\n", 148 | " )" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "@mark.skipif(IPYTHON_VERSION == 'latest', reason=\"runs when IPYTHON_VERSION != 'latest'\")\n", 158 | "def test_expected_ipython_version():\n", 159 | " ipy_version = metadata.version('IPython')\n", 160 | " if ipy_version not in SpecifierSet(f'=={IPYTHON_VERSION}'):\n", 161 | " raise TestingEnvironmentError(\n", 162 | " f\"Test environment has IPython=={IPython.__version__}, expected \"\n", 163 | " f\"{IPYTHON_VERSION}\"\n", 164 | " )" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "@mark.skipif(IPYTHON_VERSION != 'latest', reason=\"runs when IPYTHON_VERSION == 'latest'\")\n", 174 | "def test_latest_ipython_version():\n", 175 | " pip_exe = davos.config.pip_executable\n", 176 | " outdated_pkgs = subprocess.check_output(\n", 177 | " [pip_exe, 'list', '--outdated', '--format', 'json'], encoding='utf-8'\n", 178 | " )\n", 179 | " outdated_pkgs_json = json.loads(outdated_pkgs)\n", 180 | " for pkg in outdated_pkgs_json:\n", 181 | " if pkg['name'] == 'ipython':\n", 182 | " raise TestingEnvironmentError(\n", 183 | " f\"Test environment has IPython=={pkg['version']}, expected \"\n", 184 | " f\"latest version (IPython=={pkg['latest_version']})\"\n", 185 | " )" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "def test_scipy_installed():\n", 195 | " \"\"\"used as an example package for some tests\"\"\"\n", 196 | " assert is_installed('scipy')" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "def test_fastdtw_installed():\n", 206 | " \"\"\"used as an example package for some tests\"\"\"\n", 207 | " assert is_installed('fastdtw==0.3.4')" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "def test_tqdm_installed():\n", 217 | " \"\"\"used as an example package for some tests\"\"\"\n", 218 | " assert is_installed('tqdm')\n", 219 | " import tqdm\n", 220 | " assert tqdm.__version__ != '==4.45.0'" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "def test_smuggle_in_namespace():\n", 230 | " assert 'smuggle' in globals()\n", 231 | " assert 'smuggle' in IPYTHON_SHELL.user_ns\n", 232 | " assert globals()['smuggle'] is IPYTHON_SHELL.user_ns['smuggle']" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "def test_activated_on_import():\n", 242 | " assert davos.config.active is True" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "def test_deactivate_reactivate_toplevel():\n", 252 | " assert davos.active is True\n", 253 | " \n", 254 | " davos.active = False\n", 255 | " assert not davos.active \n", 256 | " \n", 257 | " with raises(NameError, match=\"name 'smuggle' is not defined\"):\n", 258 | " smuggle ast\n", 259 | " \n", 260 | " davos.active = True\n", 261 | " assert davos.active" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "def test_config_properties_accessible_toplevel():\n", 271 | " \"\"\"\n", 272 | " test that davos.config fields are accessible from the top-level davos \n", 273 | " namespace\n", 274 | " \"\"\"\n", 275 | " config_fields = ['active', 'auto_rerun', 'confirm_install', \n", 276 | " 'noninteractive', 'pip_executable', 'project', \n", 277 | " 'suppress_stdout', 'environment', 'ipython_shell', \n", 278 | " 'smuggled']\n", 279 | " failed = []\n", 280 | " for field in config_fields:\n", 281 | " # values should not only be equal, they should be references to \n", 282 | " # the *same object*\n", 283 | " if getattr(davos, field) is not getattr(davos.config, field):\n", 284 | " failed.append(field)\n", 285 | " \n", 286 | " assert not failed, (\n", 287 | " \"The following fields were not the same when accessed via \"\n", 288 | " f\"davos.config and the top-level davos module: {', '.join(failed)}\"\n", 289 | " )" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": null, 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [ 298 | "def test_set_new_attrs_toplevel_only():\n", 299 | " \"\"\"\n", 300 | " Setting an attribute on the top-level davos module that is *not* \n", 301 | " already defined by davos.config should affect only the top-level \n", 302 | " module.\n", 303 | " \"\"\"\n", 304 | " assert not hasattr(davos, 'undefined_attr')\n", 305 | " \n", 306 | " davos.undefined_attr = 'test-value'\n", 307 | " assert hasattr(davos, 'undefined_attr')\n", 308 | " assert davos.undefined_attr == 'test-value'\n", 309 | " assert not hasattr(davos.config, 'undefined_attr')\n", 310 | " \n", 311 | " del davos.undefined_attr" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "metadata": {}, 318 | "outputs": [], 319 | "source": [ 320 | "def test_all_configurable_fields_settable_via_configure():\n", 321 | " all_properties = []\n", 322 | " for name, val in davos.core.config.DavosConfig.__dict__.items():\n", 323 | " if isinstance(val, property):\n", 324 | " all_properties.append(name)\n", 325 | " read_only_fields = {'environment', 'ipython_shell', 'smuggled'}\n", 326 | " not_implemented_fields = {'conda_avail', 'conda_env', 'conda_envs_dirs'}\n", 327 | " configurable_fields = (set(all_properties) \n", 328 | " - read_only_fields \n", 329 | " - not_implemented_fields)\n", 330 | " configure_func_kwargs = set(inspect.signature(davos.configure).parameters)\n", 331 | " assert not configurable_fields.symmetric_difference(\n", 332 | " configure_func_kwargs\n", 333 | " ), (\n", 334 | " f\"configurable fields: {configurable_fields}\\ndavos.configure kwargs: \"\n", 335 | " f\"{configure_func_kwargs}\"\n", 336 | " )" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "def test_confirm_install_noninteractive_true_fails():\n", 346 | " \"\"\"\n", 347 | " davos.configure function should disallow passing both \n", 348 | " `confirm_install=True` and `noninteractive=True`.\n", 349 | " \"\"\"\n", 350 | " with raises(davos.core.exceptions.DavosConfigError):\n", 351 | " davos.configure(confirm_install=True, noninteractive=True)" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": null, 357 | "metadata": {}, 358 | "outputs": [], 359 | "source": [ 360 | "def test_swap_confirm_install_noninteractive_succeeds():\n", 361 | " \"\"\"\n", 362 | " Simultaneously disabling non-interactive mode and enabling the \n", 363 | " confirm_install option should succeed. i.e., when \n", 364 | " `davos.noninteractive` is initially `True` and both \n", 365 | " `noninteractive=False` and `confirm_install=True` are passed, \n", 366 | " `config.noninteractive` should be set to `False` before\n", 367 | " `config.confirm_install` is set to `True` to ensure both are not \n", 368 | " `True` at the same time.\n", 369 | " \"\"\"\n", 370 | " # record initial values to restore later\n", 371 | " initial_confirm_install_value = davos.confirm_install\n", 372 | " initial_noninteractive_value = davos.noninteractive\n", 373 | " \n", 374 | " try:\n", 375 | " # set up initial conditions\n", 376 | " davos.confirm_install = False\n", 377 | " davos.noninteractive = True\n", 378 | " \n", 379 | " davos.configure(confirm_install=True, noninteractive=False)\n", 380 | " \n", 381 | " # now reset initial conditions and test passing the arguments \n", 382 | " # in the opposite order\n", 383 | " davos.confirm_install = False\n", 384 | " davos.noninteractive = True\n", 385 | " \n", 386 | " davos.configure(noninteractive=False, confirm_install=True)\n", 387 | " finally:\n", 388 | " davos.config._confirm_install = initial_confirm_install_value\n", 389 | " davos.config._noninteractive = initial_noninteractive_value" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": null, 395 | "metadata": {}, 396 | "outputs": [], 397 | "source": [ 398 | "def test_configure_resets_fields_on_fail():\n", 399 | " active_before = davos.config.active\n", 400 | " confirm_install_before = davos.config.confirm_install\n", 401 | " with raises(davos.core.exceptions.DavosConfigError):\n", 402 | " davos.configure(\n", 403 | " active=False, \n", 404 | " confirm_install=True, \n", 405 | " suppress_stdout='BAD VALUE'\n", 406 | " )\n", 407 | " assert davos.config.active is active_before\n", 408 | " assert davos.config.confirm_install is confirm_install_before" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": null, 414 | "metadata": {}, 415 | "outputs": [], 416 | "source": [ 417 | "run_tests()" 418 | ] 419 | } 420 | ], 421 | "metadata": { 422 | "kernelspec": { 423 | "display_name": "kernel-env", 424 | "language": "python", 425 | "name": "kernel-env" 426 | }, 427 | "language_info": { 428 | "codemirror_mode": { 429 | "name": "ipython", 430 | "version": 3 431 | }, 432 | "file_extension": ".py", 433 | "mimetype": "text/x-python", 434 | "name": "python", 435 | "nbconvert_exporter": "python", 436 | "pygments_lexer": "ipython3", 437 | "version": "3.9.16" 438 | } 439 | }, 440 | "nbformat": 4, 441 | "nbformat_minor": 2 442 | } 443 | -------------------------------------------------------------------------------- /tests/test_implementations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2021-08-10T20:10:23.874810Z", 9 | "start_time": "2021-08-10T20:10:23.866338Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "GITHUB_USERNAME = \"$GITHUB_USERNAME$\"\n", 15 | "GITHUB_REF = \"$GITHUB_REF$\"\n", 16 | "NOTEBOOK_TYPE = \"$NOTEBOOK_TYPE$\"\n", 17 | "PYTHON_VERSION = \"$PYTHON_VERSION$\"\n", 18 | "IPYTHON_VERSION = \"$IPYTHON_VERSION$\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "ExecuteTime": { 26 | "end_time": "2021-08-10T20:10:23.945189Z", 27 | "start_time": "2021-08-10T20:10:23.877192Z" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "from pathlib import Path\n", 33 | "\n", 34 | "import requests\n", 35 | "\n", 36 | "\n", 37 | "if NOTEBOOK_TYPE == 'colab':\n", 38 | " # utils module doesn't exist on colab VM, so get current version from GitHub\n", 39 | " utils_module = Path('utils.py').resolve()\n", 40 | " response = requests.get(f'https://raw.githubusercontent.com/{GITHUB_USERNAME}/davos/{GITHUB_REF}/tests/utils.py')\n", 41 | " utils_module.write_text(response.text)\n", 42 | " # also need to install davos locally\n", 43 | " from utils import install_davos\n", 44 | " install_davos(source='github', ref=GITHUB_REF, fork=GITHUB_USERNAME)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": { 51 | "ExecuteTime": { 52 | "end_time": "2021-08-10T20:10:24.046197Z", 53 | "start_time": "2021-08-10T20:10:23.947588Z" 54 | } 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "import re\n", 59 | "\n", 60 | "import davos\n", 61 | "import IPython\n", 62 | "\n", 63 | "from utils import mark, raises, run_tests" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "# tests for `davos.implementations` (`__init__.py`)\n", 71 | "**Notes**: \n", 72 | "- tests for whether correct implementation functions are imported are in the respective modules' test notebooks\n", 73 | "- `fget`/`fset` functions not tested here are tested in `test_config.ipynb`" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": { 80 | "ExecuteTime": { 81 | "end_time": "2021-08-10T20:10:24.051658Z", 82 | "start_time": "2021-08-10T20:10:24.048406Z" 83 | } 84 | }, 85 | "outputs": [], 86 | "source": [ 87 | " def test_conda_avail_fset_raises():\n", 88 | " \"\"\"check that `conda_avail` config field is read-only\"\"\"\n", 89 | " match = re.escape(\"'davos.config.conda_avail': field is read-only\")\n", 90 | " with raises(davos.core.exceptions.DavosConfigError, match=match):\n", 91 | " davos.config.conda_avail = True" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": { 98 | "ExecuteTime": { 99 | "end_time": "2021-08-10T20:10:24.056465Z", 100 | "start_time": "2021-08-10T20:10:24.053407Z" 101 | } 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "@mark.colab\n", 106 | "def test_conda_env_fset_raises_noconda():\n", 107 | " \"\"\"\n", 108 | " check that trying to set the `conda_env` config field raises an \n", 109 | " error if the `conda` executable is not available\n", 110 | " \n", 111 | " test restricted to colab, where conda is not available\n", 112 | " \"\"\"\n", 113 | " match = re.escape(\n", 114 | " \"'davos.config.conda_env': cannot set conda environment. No local \"\n", 115 | " \"conda installation found\"\n", 116 | " )\n", 117 | " with raises(davos.core.exceptions.DavosConfigError, match=match):\n", 118 | " davos.config.conda_env = 'arbitrary-environment-name'" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": { 125 | "ExecuteTime": { 126 | "end_time": "2021-08-10T20:10:24.062530Z", 127 | "start_time": "2021-08-10T20:10:24.058348Z" 128 | } 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "@mark.jupyter\n", 133 | "def test_conda_env_fset_raises_noenv():\n", 134 | " \"\"\"\n", 135 | " check that trying to set the `conda_env` config field to an \n", 136 | " environment that does not exist raises an error\n", 137 | " \"\"\"\n", 138 | " local_envs = {\"', '\".join(davos.config.conda_envs_dirs.keys())}\n", 139 | " bad_name = 'fake-env-name'\n", 140 | " match = (\n", 141 | " f\"'davos.config.conda_env': unrecognized environment name: {bad_name!r}. \"\n", 142 | " f\"Local environments are:\\n\\t{local_envs!r}\"\n", 143 | " )\n", 144 | " with raises(davos.core.exceptions.DavosConfigError, match=match):\n", 145 | " davos.config.conda_env = bad_name" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": { 152 | "ExecuteTime": { 153 | "end_time": "2021-08-10T20:11:25.760713Z", 154 | "start_time": "2021-08-10T20:11:25.756907Z" 155 | } 156 | }, 157 | "outputs": [], 158 | "source": [ 159 | " def test_conda_envs_dirs_fset_raises():\n", 160 | " \"\"\"check that `conda_envs_dirs` config field is read-only\"\"\"\n", 161 | " match = re.escape(\"'davos.config.conda_envs_dirs': field is read-only\")\n", 162 | " with raises(davos.core.exceptions.DavosConfigError, match=match):\n", 163 | " davos.config.conda_envs_dirs = {'fake-foo': 'fake/path/to/fake-foo'}" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": { 170 | "ExecuteTime": { 171 | "end_time": "2021-08-10T20:11:25.875472Z", 172 | "start_time": "2021-08-10T20:11:25.868487Z" 173 | }, 174 | "scrolled": true 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "run_tests()" 179 | ] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "kernel-env", 185 | "language": "python", 186 | "name": "kernel-env" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 3 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython3", 198 | "version": "3.9.16" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 2 203 | } 204 | -------------------------------------------------------------------------------- /tests/test_ipython_common.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2021-08-11T03:58:39.244632Z", 9 | "start_time": "2021-08-11T03:58:39.239990Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "GITHUB_USERNAME = \"$GITHUB_USERNAME$\"\n", 15 | "GITHUB_REF = \"$GITHUB_REF$\"\n", 16 | "NOTEBOOK_TYPE = \"$NOTEBOOK_TYPE$\"\n", 17 | "PYTHON_VERSION = \"$PYTHON_VERSION$\"\n", 18 | "IPYTHON_VERSION = \"$IPYTHON_VERSION$\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "ExecuteTime": { 26 | "end_time": "2021-08-11T03:58:39.363193Z", 27 | "start_time": "2021-08-11T03:58:39.246862Z" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "from pathlib import Path\n", 33 | "\n", 34 | "import requests\n", 35 | "\n", 36 | "\n", 37 | "if NOTEBOOK_TYPE == 'colab':\n", 38 | " # utils module doesn't exist on colab VM, so get current version from GitHub\n", 39 | " utils_module = Path('utils.py').resolve()\n", 40 | " response = requests.get(f'https://raw.githubusercontent.com/{GITHUB_USERNAME}/davos/{GITHUB_REF}/tests/utils.py')\n", 41 | " utils_module.write_text(response.text)\n", 42 | " # also need to install davos locally\n", 43 | " from utils import install_davos\n", 44 | " install_davos(source='github', ref=GITHUB_REF, fork=GITHUB_USERNAME)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": { 51 | "ExecuteTime": { 52 | "end_time": "2021-08-11T03:58:39.407764Z", 53 | "start_time": "2021-08-11T03:58:39.364819Z" 54 | } 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "from contextlib import redirect_stdout\n", 59 | "from io import StringIO\n", 60 | "from subprocess import CalledProcessError\n", 61 | "from textwrap import dedent\n", 62 | "\n", 63 | "import davos\n", 64 | "import IPython\n", 65 | "\n", 66 | "from utils import mark, raises, run_tests" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "# tests for `davos.implementations.ipython_common`" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": { 80 | "ExecuteTime": { 81 | "end_time": "2021-08-11T03:58:39.414142Z", 82 | "start_time": "2021-08-11T03:58:39.409863Z" 83 | } 84 | }, 85 | "outputs": [], 86 | "source": [ 87 | "def test_ipython_common_imports():\n", 88 | " \"\"\"\n", 89 | " check that functions that should've been imported from the \n", 90 | " ipython_common module came from the right place\n", 91 | " \"\"\"\n", 92 | " ipy_common_funcs = (\n", 93 | " '_check_conda_avail_helper', \n", 94 | " '_run_shell_command_helper',\n", 95 | " '_set_custom_showsyntaxerror'\n", 96 | " )\n", 97 | " for func_name in ipy_common_funcs:\n", 98 | " func_obj = getattr(davos.implementations, func_name)\n", 99 | " func_module = getattr(func_obj, '__module__')\n", 100 | " assert func_module == 'davos.implementations.ipython_common', (\n", 101 | " f\"davos.implementations.{func_name} is {func_module}.{func_name}. \"\n", 102 | " f\"Expected davos.implementations.ipython_common.{func_name}\"\n", 103 | " )" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": { 110 | "ExecuteTime": { 111 | "end_time": "2021-08-11T03:58:39.422479Z", 112 | "start_time": "2021-08-11T03:58:39.416239Z" 113 | } 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "@mark.jupyter\n", 118 | "def test_check_conda_avail_helper():\n", 119 | " \"\"\"\n", 120 | " test helper function for getting conda-related config fields\n", 121 | " \"\"\"\n", 122 | " expected_env_path = \"/usr/share/miniconda/envs/kernel-env\"\n", 123 | " # only part of output that matters is line with environment path\n", 124 | " expected_first_line = f\"# packages in environment at {expected_env_path}:\"\n", 125 | " result_output = davos.implementations.ipython_common._check_conda_avail_helper()\n", 126 | " result_first_line = result_output.splitlines()[0]\n", 127 | " result_env_path = result_first_line.split()[-1].rstrip(':')\n", 128 | " \n", 129 | " assert result_env_path == expected_env_path, (\n", 130 | " f\"Result:{result_env_path}\\nExpected:{expected_env_path}\"\n", 131 | " )" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": { 138 | "ExecuteTime": { 139 | "end_time": "2021-08-11T03:58:39.429792Z", 140 | "start_time": "2021-08-11T03:58:39.424739Z" 141 | } 142 | }, 143 | "outputs": [], 144 | "source": [ 145 | "def test_run_shell_command_helper():\n", 146 | " \"\"\"test helper function for davos.core.core.run_shell_command\"\"\"\n", 147 | " # this command should pass...\n", 148 | " with redirect_stdout(StringIO()) as tmp_stdout:\n", 149 | " davos.implementations.ipython_common._run_shell_command_helper('echo \"test\"')\n", 150 | " stdout = tmp_stdout.getvalue().strip()\n", 151 | " assert stdout == 'test', stdout\n", 152 | " \n", 153 | " # ...this command should fail\n", 154 | " with raises(CalledProcessError), redirect_stdout(StringIO()):\n", 155 | " davos.implementations.ipython_common._run_shell_command_helper('\"tset \" ohce')" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": { 162 | "ExecuteTime": { 163 | "end_time": "2021-08-11T03:58:39.438946Z", 164 | "start_time": "2021-08-11T03:58:39.431759Z" 165 | } 166 | }, 167 | "outputs": [], 168 | "source": [ 169 | "def test_set_custom_showsyntaxerror():\n", 170 | " \"\"\"\n", 171 | " check that the IPython shell's .showsyntaxerror() method was \n", 172 | " replaced with the custom davos implementation, and that the original \n", 173 | " is stored in the davos config\n", 174 | " \"\"\"\n", 175 | " orig_func = davos.implementations.ipython_common._showsyntaxerror_davos\n", 176 | " bound_method = get_ipython().showsyntaxerror\n", 177 | " unbound_func = bound_method.__func__\n", 178 | " assert unbound_func is orig_func, (\n", 179 | " f\"{unbound_func.__module__}.{unbound_func.__name__}\"\n", 180 | " )\n", 181 | " \n", 182 | " orig_method = davos.config._ipy_showsyntaxerror_orig\n", 183 | " assert orig_method is not None\n", 184 | " orig_qualname = f\"{orig_method.__module__}.{orig_method.__qualname__}\"\n", 185 | " expected_orig_qualname = \"IPython.core.interactiveshell.InteractiveShell.showsyntaxerror\"\n", 186 | " assert orig_qualname == expected_orig_qualname, orig_qualname" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": { 193 | "ExecuteTime": { 194 | "end_time": "2021-08-11T03:58:39.690163Z", 195 | "start_time": "2021-08-11T03:58:39.441202Z" 196 | } 197 | }, 198 | "outputs": [], 199 | "source": [ 200 | "run_tests()" 201 | ] 202 | } 203 | ], 204 | "metadata": { 205 | "kernelspec": { 206 | "display_name": "kernel-env", 207 | "language": "python", 208 | "name": "kernel-env" 209 | }, 210 | "language_info": { 211 | "codemirror_mode": { 212 | "name": "ipython", 213 | "version": 3 214 | }, 215 | "file_extension": ".py", 216 | "mimetype": "text/x-python", 217 | "name": "python", 218 | "nbconvert_exporter": "python", 219 | "pygments_lexer": "ipython3", 220 | "version": "3.8.10" 221 | } 222 | }, 223 | "nbformat": 4, 224 | "nbformat_minor": 2 225 | } 226 | -------------------------------------------------------------------------------- /tests/test_ipython_post7.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2021-08-10T22:48:40.722242Z", 9 | "start_time": "2021-08-10T22:48:40.713820Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "GITHUB_USERNAME = \"$GITHUB_USERNAME$\"\n", 15 | "GITHUB_REF = \"$GITHUB_REF$\"\n", 16 | "NOTEBOOK_TYPE = \"$NOTEBOOK_TYPE$\"\n", 17 | "PYTHON_VERSION = \"$PYTHON_VERSION$\"\n", 18 | "IPYTHON_VERSION = \"$IPYTHON_VERSION$\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "ExecuteTime": { 26 | "end_time": "2021-08-10T22:48:40.798817Z", 27 | "start_time": "2021-08-10T22:48:40.724793Z" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "from pathlib import Path\n", 33 | "\n", 34 | "import requests\n", 35 | "\n", 36 | "\n", 37 | "if NOTEBOOK_TYPE == 'colab':\n", 38 | " # utils module doesn't exist on colab VM, so get current version from GitHub\n", 39 | " utils_module = Path('utils.py').resolve()\n", 40 | " response = requests.get(f'https://raw.githubusercontent.com/{GITHUB_USERNAME}/davos/{GITHUB_REF}/tests/utils.py')\n", 41 | " utils_module.write_text(response.text)\n", 42 | " # also need to install davos locally\n", 43 | " from utils import install_davos\n", 44 | " install_davos(source='github', ref=GITHUB_REF, fork=GITHUB_USERNAME)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": { 51 | "ExecuteTime": { 52 | "end_time": "2021-08-10T22:48:40.987589Z", 53 | "start_time": "2021-08-10T22:48:40.801961Z" 54 | } 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "import davos\n", 59 | "\n", 60 | "from utils import mark, raises, run_tests" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": { 67 | "ExecuteTime": { 68 | "end_time": "2021-08-10T22:48:40.993032Z", 69 | "start_time": "2021-08-10T22:48:40.990142Z" 70 | } 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "IPYTHON_SHELL = get_ipython()" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "# tests for `davos.implementations.ipython_post7`\n", 82 | "**Note**: this test notebook is only run for jobs where `IPython>=7.0`" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": { 89 | "ExecuteTime": { 90 | "end_time": "2021-08-10T22:48:40.999538Z", 91 | "start_time": "2021-08-10T22:48:40.995238Z" 92 | } 93 | }, 94 | "outputs": [], 95 | "source": [ 96 | "def test_ipython_post7_imports():\n", 97 | " \"\"\"\n", 98 | " check that functions that should've been imported from the \n", 99 | " ipython_post7 module came from the right place\n", 100 | " \"\"\"\n", 101 | " ipy_post7_funcs = (\n", 102 | " '_activate_helper', \n", 103 | " '_deactivate_helper',\n", 104 | " 'generate_parser_func'\n", 105 | " )\n", 106 | " for func_name in ipy_post7_funcs:\n", 107 | " func_obj = getattr(davos.implementations, func_name)\n", 108 | " func_module = getattr(func_obj, '__module__')\n", 109 | " assert func_module == 'davos.implementations.ipython_post7', (\n", 110 | " f\"davos.implementations.{func_name} is {func_module}.{func_name}. \"\n", 111 | " f\"Expected davos.implementations.ipython_post7.{func_name}\"\n", 112 | " )" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": { 119 | "ExecuteTime": { 120 | "end_time": "2021-08-10T22:48:41.005649Z", 121 | "start_time": "2021-08-10T22:48:41.002104Z" 122 | } 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "def test_activate_helper_registers_parser():\n", 127 | " \"\"\"\n", 128 | " check that the `davos` parser was added to `input_transformers_post` \n", 129 | " when `davos` was imported above\n", 130 | " \"\"\"\n", 131 | " davos_parser = davos.implementations.full_parser\n", 132 | " transformers_list = IPYTHON_SHELL.input_transformers_post\n", 133 | " assert davos_parser in transformers_list, (\n", 134 | " f\"{davos_parser} not in {transformers_list}\"\n", 135 | " )" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": { 142 | "ExecuteTime": { 143 | "end_time": "2021-08-10T22:48:41.012185Z", 144 | "start_time": "2021-08-10T22:48:41.008515Z" 145 | } 146 | }, 147 | "outputs": [], 148 | "source": [ 149 | "def test_activate_helper_registers_parser_once():\n", 150 | " \"\"\"\n", 151 | " `_activate_helper` should not register multiple instances of the \n", 152 | " `davos` parser if called multiple times (without deactivating)\n", 153 | " \"\"\"\n", 154 | " smuggle_func = davos.core.core.smuggle\n", 155 | " davos_parser = davos.implementations.full_parser\n", 156 | " transformers_list = IPYTHON_SHELL.input_transformers_post\n", 157 | " \n", 158 | " assert transformers_list.count(davos_parser) == 1\n", 159 | " \n", 160 | " for _ in range(5):\n", 161 | " davos.implementations.ipython_post7._activate_helper(smuggle_func, \n", 162 | " davos_parser)\n", 163 | " \n", 164 | " assert transformers_list.count(davos_parser) == 1" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": { 171 | "ExecuteTime": { 172 | "end_time": "2021-08-10T22:48:41.021710Z", 173 | "start_time": "2021-08-10T22:48:41.015903Z" 174 | } 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "def test_activate_helper_adds_smuggle():\n", 179 | " \"\"\"\n", 180 | " `_activate_helper` should inject the `smuggle_func` it is passed \n", 181 | " into the IPython user namespace as `\"smuggle\"`\n", 182 | " \"\"\" \n", 183 | " real_smuggle_func = davos.core.core.smuggle\n", 184 | " davos_parser = davos.implementations.full_parser\n", 185 | " \n", 186 | " def _fake_smuggle_func():\n", 187 | " pass\n", 188 | " \n", 189 | " assert IPYTHON_SHELL.user_ns['smuggle'] is real_smuggle_func is smuggle\n", 190 | " \n", 191 | " try:\n", 192 | " davos.implementations.ipython_post7._activate_helper(_fake_smuggle_func, \n", 193 | " davos_parser)\n", 194 | " assert smuggle is IPYTHON_SHELL.user_ns['smuggle'] is _fake_smuggle_func\n", 195 | " finally:\n", 196 | " # regardless of test outcome, make sure original value of \n", 197 | " # \"smuggle\" is restored\n", 198 | " IPYTHON_SHELL.user_ns['smuggle'] = real_smuggle_func" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "metadata": { 205 | "ExecuteTime": { 206 | "end_time": "2021-08-10T22:48:41.028142Z", 207 | "start_time": "2021-08-10T22:48:41.024228Z" 208 | } 209 | }, 210 | "outputs": [], 211 | "source": [ 212 | "def test_deactivate_helper_removes_parser():\n", 213 | " \"\"\"\n", 214 | " `_deactivate_helper` should remove the `davos` parser from \n", 215 | " `input_transformers_post`\n", 216 | " \"\"\"\n", 217 | " smuggle_func = davos.core.core.smuggle\n", 218 | " davos_parser = davos.implementations.full_parser\n", 219 | " # a reference\n", 220 | " transformers_list = IPYTHON_SHELL.input_transformers_post\n", 221 | " # a copy\n", 222 | " old_transformers_list = IPYTHON_SHELL.input_transformers_post[:]\n", 223 | " \n", 224 | " assert davos_parser in transformers_list\n", 225 | " \n", 226 | " try:\n", 227 | " davos.implementations.ipython_post7._deactivate_helper(smuggle_func, \n", 228 | " davos_parser)\n", 229 | "\n", 230 | " assert davos_parser not in transformers_list, transformers_list\n", 231 | " finally:\n", 232 | " # regardless of test outcome, make sure original \n", 233 | " # input_transformers_post list and value of \"smuggle\" are \n", 234 | " # restored\n", 235 | " IPYTHON_SHELL.input_transformers_post = old_transformers_list\n", 236 | " IPYTHON_SHELL.user_ns['smuggle'] = smuggle_func" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": { 243 | "ExecuteTime": { 244 | "end_time": "2021-08-10T22:48:41.036307Z", 245 | "start_time": "2021-08-10T22:48:41.030001Z" 246 | } 247 | }, 248 | "outputs": [], 249 | "source": [ 250 | "def test_deactivate_helper_deletes_smuggle():\n", 251 | " \"\"\"\n", 252 | " `_deactivate_helper` should remove `\"smuggle\"` from the IPython user \n", 253 | " namespace if it refers to `davos.core.core.smuggle`\n", 254 | " \"\"\"\n", 255 | " smuggle_func = davos.core.core.smuggle\n", 256 | " davos_parser = davos.implementations.full_parser\n", 257 | " old_transformers_list = IPYTHON_SHELL.input_transformers_post[:]\n", 258 | " \n", 259 | " assert smuggle is IPYTHON_SHELL.user_ns['smuggle'] is smuggle_func\n", 260 | " \n", 261 | " try:\n", 262 | " davos.implementations.ipython_post7._deactivate_helper(smuggle_func, \n", 263 | " davos_parser)\n", 264 | " assert 'smuggle' not in IPYTHON_SHELL.user_ns\n", 265 | " with raises(NameError):\n", 266 | " smuggle os\n", 267 | "\n", 268 | " finally:\n", 269 | " IPYTHON_SHELL.user_ns['smuggle'] = smuggle_func\n", 270 | " IPYTHON_SHELL.input_transformers_post = old_transformers_list" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": { 277 | "ExecuteTime": { 278 | "end_time": "2021-08-10T22:48:41.045042Z", 279 | "start_time": "2021-08-10T22:48:41.038431Z" 280 | } 281 | }, 282 | "outputs": [], 283 | "source": [ 284 | "def test_deactivate_helper_leaves_smuggle():\n", 285 | " \"\"\"\n", 286 | " running `_deactivate_helper` should *not* delete `\"smuggle\"` from \n", 287 | " the IPython user namespace if it refers to something other than \n", 288 | " `davos.core.core.smuggle`\n", 289 | " \"\"\"\n", 290 | " global smuggle\n", 291 | " \n", 292 | " smuggle_func = davos.core.core.smuggle\n", 293 | " davos_parser = davos.implementations.full_parser\n", 294 | " old_transformers_list = IPYTHON_SHELL.input_transformers_post[:]\n", 295 | " \n", 296 | " smuggle = 'tmp-smuggle-val'\n", 297 | " try:\n", 298 | " davos.implementations.ipython_post7._deactivate_helper(smuggle_func, \n", 299 | " davos_parser)\n", 300 | " assert 'smuggle' in IPYTHON_SHELL.user_ns\n", 301 | " assert smuggle == 'tmp-smuggle-val'\n", 302 | " finally:\n", 303 | " IPYTHON_SHELL.user_ns['smuggle'] = smuggle_func\n", 304 | " IPYTHON_SHELL.input_transformers_post = old_transformers_list" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "metadata": { 311 | "ExecuteTime": { 312 | "end_time": "2021-08-10T22:48:41.051294Z", 313 | "start_time": "2021-08-10T22:48:41.047396Z" 314 | } 315 | }, 316 | "outputs": [], 317 | "source": [ 318 | "def test_deactivate_helper_not_active():\n", 319 | " \"\"\"\n", 320 | " running `_deactivate_helper` shouldn't cause any problems if run \n", 321 | " when the davos parser is already inactive\n", 322 | " \"\"\"\n", 323 | " smuggle_func = davos.core.core.smuggle\n", 324 | " davos_parser = davos.implementations.full_parser\n", 325 | " \n", 326 | " davos.active = False\n", 327 | "\n", 328 | " try:\n", 329 | " for _ in range(5):\n", 330 | " davos.implementations.ipython_post7._deactivate_helper(smuggle_func, \n", 331 | " davos_parser)\n", 332 | " finally:\n", 333 | " davos.active = True" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": null, 339 | "metadata": { 340 | "ExecuteTime": { 341 | "end_time": "2021-08-10T22:48:41.056140Z", 342 | "start_time": "2021-08-10T22:48:41.053162Z" 343 | } 344 | }, 345 | "outputs": [], 346 | "source": [ 347 | "def test_generate_parser_func_side_effects():\n", 348 | " \"\"\"\n", 349 | " The IPython >= 7.0 implementation of `generate_parser_func` should \n", 350 | " set an attribute \"`has_side_effects`\" on the returned parser \n", 351 | " function to `True`\n", 352 | " \"\"\"\n", 353 | " davos_parser = davos.implementations.full_parser\n", 354 | " assert hasattr(davos_parser, 'has_side_effects')\n", 355 | " assert davos_parser.has_side_effects is True" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": null, 361 | "metadata": { 362 | "ExecuteTime": { 363 | "end_time": "2021-08-10T22:48:41.069903Z", 364 | "start_time": "2021-08-10T22:48:41.058543Z" 365 | } 366 | }, 367 | "outputs": [], 368 | "source": [ 369 | "run_tests()" 370 | ] 371 | } 372 | ], 373 | "metadata": { 374 | "kernelspec": { 375 | "display_name": "kernel-env", 376 | "language": "python", 377 | "name": "kernel-env" 378 | }, 379 | "language_info": { 380 | "codemirror_mode": { 381 | "name": "ipython", 382 | "version": 3 383 | }, 384 | "file_extension": ".py", 385 | "mimetype": "text/x-python", 386 | "name": "python", 387 | "nbconvert_exporter": "python", 388 | "pygments_lexer": "ipython3", 389 | "version": "3.9.16" 390 | } 391 | }, 392 | "nbformat": 4, 393 | "nbformat_minor": 2 394 | } 395 | -------------------------------------------------------------------------------- /tests/test_parsers.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2021-08-07T20:25:39.008376Z", 9 | "start_time": "2021-08-07T20:25:38.999627Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "GITHUB_USERNAME = \"$GITHUB_USERNAME$\"\n", 15 | "GITHUB_REF = \"$GITHUB_REF$\"\n", 16 | "NOTEBOOK_TYPE = \"$NOTEBOOK_TYPE$\"\n", 17 | "PYTHON_VERSION = \"$PYTHON_VERSION$\"\n", 18 | "IPYTHON_VERSION = \"$IPYTHON_VERSION$\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "ExecuteTime": { 26 | "end_time": "2021-08-07T20:25:39.584403Z", 27 | "start_time": "2021-08-07T20:25:39.455811Z" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "import warnings\n", 33 | "from pathlib import Path\n", 34 | "\n", 35 | "import requests\n", 36 | "\n", 37 | "\n", 38 | "warnings.filterwarnings('error', module='davos')\n", 39 | "\n", 40 | "if NOTEBOOK_TYPE == 'colab':\n", 41 | " # utils module doesn't exist on colab VM, so get current version from GitHub\n", 42 | " utils_module = Path('utils.py').resolve()\n", 43 | " response = requests.get(f'https://raw.githubusercontent.com/{GITHUB_USERNAME}/davos/{GITHUB_REF}/tests/utils.py')\n", 44 | " utils_module.write_text(response.text)\n", 45 | " # also need to install davos locally\n", 46 | " from utils import install_davos\n", 47 | " install_davos(source='github', ref=GITHUB_REF, fork=GITHUB_USERNAME)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "ExecuteTime": { 55 | "end_time": "2021-08-07T20:26:24.819430Z", 56 | "start_time": "2021-08-07T20:26:24.816360Z" 57 | } 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "import argparse\n", 62 | "\n", 63 | "import davos\n", 64 | "from davos.core.exceptions import OnionArgumentError\n", 65 | "from davos.core.parsers import pip_parser\n", 66 | "\n", 67 | "from utils import raises, run_tests" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": { 74 | "ExecuteTime": { 75 | "end_time": "2021-08-07T20:25:42.304857Z", 76 | "start_time": "2021-08-07T20:25:42.301966Z" 77 | } 78 | }, 79 | "outputs": [], 80 | "source": [ 81 | "IPYTHON_SHELL = get_ipython()" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "# tests for `davos.core.parsers`" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": { 95 | "ExecuteTime": { 96 | "end_time": "2021-07-22T16:18:15.471809Z", 97 | "start_time": "2021-07-22T16:18:15.467555Z" 98 | } 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "def test_args_suppressed_by_default():\n", 103 | " \"\"\"\n", 104 | " arguments not explicitly passed should not appear in Namespace, \n", 105 | " *except for \"editable\" value*. \n", 106 | " \"\"\"\n", 107 | " assert pip_parser.argument_default == argparse.SUPPRESS\n", 108 | " \n", 109 | " args = ['foo']\n", 110 | " expected = {\n", 111 | " 'editable': False,\n", 112 | " 'spec': 'foo'\n", 113 | " }\n", 114 | " parsed_args = vars(pip_parser.parse_args(args))\n", 115 | " assert parsed_args == expected, (\n", 116 | " f\"Expected:\\n\\t{expected}\\nFound:\\n\\t{parsed_args}\"\n", 117 | " )" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "def test_editable_action():\n", 127 | " \"\"\"\n", 128 | " Test for EditableAction class -- special Action subclass for \n", 129 | " '-e/--editable ' argument that sets 'editable=True' and \n", 130 | " 'spec='.\n", 131 | " \"\"\"\n", 132 | " # package name should always be assigned to 'spec', 'editable' \n", 133 | " # should be True/False\n", 134 | " args_regular = ['foo']\n", 135 | " expected_regular = {\n", 136 | " 'spec': 'foo',\n", 137 | " 'editable': False\n", 138 | " }\n", 139 | " parsed_args_regular = vars(pip_parser.parse_args(args_regular))\n", 140 | " assert parsed_args_regular == expected_regular, (\n", 141 | " f\"Result:\\n{parsed_args_regular}\\nExpected:\\n{expected_regular}\"\n", 142 | " )\n", 143 | " \n", 144 | " args_editable = '-e bar'.split()\n", 145 | " expected_editable = {\n", 146 | " 'spec': 'bar',\n", 147 | " 'editable': True\n", 148 | " }\n", 149 | " parsed_args_editable = vars(pip_parser.parse_args(args_editable))\n", 150 | " assert parsed_args_editable == expected_editable, (\n", 151 | " f\"Result:\\n{parsed_args_editable}\\nExpected:\\n{expected_editable}\"\n", 152 | " )\n", 153 | " \n", 154 | " # one of the two is required...\n", 155 | " args_both = ''.split()\n", 156 | " match_str_both = \"Onion comment must specify a package name\"\n", 157 | " with raises(OnionArgumentError, match=match_str_both):\n", 158 | " vars(pip_parser.parse_args(args_both))\n", 159 | " \n", 160 | " # and the two are mutually exclusive\n", 161 | " args_neither = 'foo -e bar'.split()\n", 162 | " match_str_neither = \"argument -e/--editable: not allowed with argument spec\"\n", 163 | " with raises(OnionArgumentError, match=match_str_neither):\n", 164 | " vars(pip_parser.parse_args(args_neither))" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": { 171 | "ExecuteTime": { 172 | "end_time": "2021-07-22T15:50:33.917647Z", 173 | "start_time": "2021-07-22T15:50:33.909089Z" 174 | } 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "def test_parser_bad_arg_type_raises():\n", 179 | " \"\"\"passing the wrong Python type for the argument raises an error\"\"\"\n", 180 | " args = '--timeout false'.split()\n", 181 | " match_str = \"argument --timeout: invalid float value: 'false'\"\n", 182 | " with raises(OnionArgumentError, match=match_str):\n", 183 | " pip_parser.parse_args(args)" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "def test_unsupported_args_raises():\n", 193 | " \"\"\"\n", 194 | " should raise an error that lists all unsupported arguments passed, \n", 195 | " if any\n", 196 | " \"\"\"\n", 197 | " args = '--not-real -A --rguments 100 --verbose'.split()\n", 198 | " match_str = \"argument --not-real: Unrecognized arguments: --not-real -A --rguments\"\n", 199 | " with raises(OnionArgumentError, match=match_str):\n", 200 | " pip_parser.parse_args(args)" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "run_tests()" 210 | ] 211 | } 212 | ], 213 | "metadata": { 214 | "kernelspec": { 215 | "display_name": "kernel-env", 216 | "language": "python", 217 | "name": "kernel-env" 218 | }, 219 | "language_info": { 220 | "codemirror_mode": { 221 | "name": "ipython", 222 | "version": 3 223 | }, 224 | "file_extension": ".py", 225 | "mimetype": "text/x-python", 226 | "name": "python", 227 | "nbconvert_exporter": "python", 228 | "pygments_lexer": "ipython3", 229 | "version": "3.8.10" 230 | } 231 | }, 232 | "nbformat": 4, 233 | "nbformat_minor": 2 234 | } 235 | --------------------------------------------------------------------------------