diff --git a/.github/workflows/positron-python-ci.yml b/.github/workflows/positron-python-ci.yml index cb86336b1c8..a39a4cd617a 100644 --- a/.github/workflows/positron-python-ci.yml +++ b/.github/workflows/positron-python-ci.yml @@ -23,7 +23,7 @@ env: NODE_VERSION: '18.17.1' PYTHON_VERSION: '3.10' PROJECT_DIR: 'extensions/positron-python' - PYTHON_SRC_DIR: 'extensions/positron-python/pythonFiles' + PYTHON_SRC_DIR: 'extensions/positron-python/python_files' # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. special-working-directory: './path with spaces' @@ -67,16 +67,11 @@ jobs: - name: Check TypeScript format run: yarn format-check - - name: Check Python format - run: | - python -m pip install -U black - python -m black . --check - working-directory: ${{ env.PYTHON_SRC_DIR }} - - - name: Run Ruff + - name: Lint and Check Formatting with Ruff run: | python -m pip install -U ruff python -m ruff check . + python -m ruff format --check working-directory: ${{ env.PYTHON_SRC_DIR }} check-types: @@ -93,16 +88,16 @@ jobs: cache: 'pip' - name: Install base Python requirements - run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --no-cache-dir --implementation py -r requirements.txt' + run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/python --no-cache-dir --implementation py -r requirements.txt' - name: Install Positron IPyKernel requirements run: python scripts/vendor.py - name: Install other Python requirements run: | - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy python -m pip install --upgrade -r build/test-requirements.txt - python -m pip install --upgrade -r ./pythonFiles/positron/pinned-test-requirements.txt + python -m pip install --upgrade -r ./python_files/positron/pinned-test-requirements.txt - name: Run Pyright uses: jakebailey/pyright-action@v2 @@ -169,13 +164,13 @@ jobs: run: python -m pytest --version - name: Install base Python requirements - run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --no-cache-dir --implementation py -r requirements.txt' + run: 'python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/python --no-cache-dir --implementation py -r requirements.txt' - name: Install test requirements run: python -m pip install -r build/test-requirements.txt - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py + run: python python_files/tests/run_all.py positron-ipykernel-tests: name: Test Positron IPyKernel @@ -185,31 +180,14 @@ jobs: include: - os: 'ubuntu-latest' python: '3.8' - time-elapsed: '' - os: 'macos-latest' python: '3.9' - time-elapsed: '' - os: 'windows-latest' python: '3.10' - time-elapsed: '' - os: 'ubuntu-latest' python: '3.11' - time-elapsed: '' - os: 'ubuntu-latest' python: '3.12' - time-elapsed: '' - # - os: 'ubuntu-latest' - # python: '3.10' - # time-elapsed: '3 months' - # - os: 'ubuntu-latest' - # python: '3.10' - # time-elapsed: '6 months' - # - os: 'ubuntu-latest' - # python: '3.10' - # time-elapsed: '9 months' - # - os: 'ubuntu-latest' - # python: '3.10' - # time-elapsed: '1 year' steps: - name: Checkout @@ -225,22 +203,37 @@ jobs: run: python scripts/vendor.py - name: Install Positron IPyKernel test requirements - run: python -m pip install --prefer-binary --upgrade -r pythonFiles/positron/pinned-test-requirements.txt + run: python -m pip install --prefer-binary --upgrade -r python_files/positron/pinned-test-requirements.txt - - name: Get date for older dependencies - if: ${{ matrix.time-elapsed != '' }} - run: | - echo "SNAPSHOT_DATE=$(date -d '-${{ matrix.time-elapsed }}' --iso-8601)" >> $GITHUB_ENV + - name: Run Positron IPyKernel unit tests + run: pytest python_files/positron - - name: Get older PyPI snapshot - if: ${{ matrix.time-elapsed != '' }} + python-minimum-dependencies: + name: Test Minimum Positron IPyKernel Dependencies + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # currently only running on oldest supported Python version + - os: 'ubuntu-latest' + python: '3.8' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: 'pip' + + - name: Install testing requirements run: | - python -m pip config set global.index-url https://packagemanager.posit.co/pypi/${{ env.SNAPSHOT_DATE }}/simple - python -m pip config set global.trusted-host packagemanager.posit.co - python -m pip install --prefer-binary --force-reinstall -r pythonFiles/positron/data-science-requirements.txt + python scripts/vendor.py + python -m pip install nox - - name: Run Positron IPyKernel unit tests - run: pytest pythonFiles/positron + - name: Run tests + run: yarn positron:testMinimumPythonReqs typescript-tests: name: Test TypeScript diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 735eefdf610..efbc726a2cd 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -2806,4 +2806,478 @@ Apache License WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ---------------------------------------------------------- \ No newline at end of file +--------------------------------------------------------- + +--------------------------------------------------------- + +Jupyter logo from jupyter/notebook - BSD 3-Clause License +https://github.com/jupyter/notebook + +BSD 3-Clause License + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--------------------------------------------------------- + +--------------------------------------------------------- + +The R logo is © 2016 The R Foundation - CC-BY-SA 4.0 +https://www.r-project.org/logo/ + +======================================================================= + +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. +======================================================================= diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 737c3b42ed6..6f8bcbccadf 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -569,6 +569,9 @@ "--vscode-positronModalDialog-buttonActiveBackground", "--vscode-positronModalDialog-buttonBackground", "--vscode-positronModalDialog-buttonBorder", + "--vscode-positronModalDialog-buttonDisabledBackground", + "--vscode-positronModalDialog-buttonDisabledBorder", + "--vscode-positronModalDialog-buttonDisabledForeground", "--vscode-positronModalDialog-buttonForeground", "--vscode-positronModalDialog-buttonHoverBackground", "--vscode-positronModalDialog-checkboxBackground", @@ -579,6 +582,15 @@ "--vscode-positronModalDialog-defaultButtonForeground", "--vscode-positronModalDialog-defaultButtonHoverBackground", "--vscode-positronModalDialog-foreground", + "--vscode-positronModalDialog-projectTypeBackground", + "--vscode-positronModalDialog-projectTypeBackgroundHover", + "--vscode-positronModalDialog-projectTypeBackgroundSelected", + "--vscode-positronModalDialog-projectTypeBorder", + "--vscode-positronModalDialog-projectTypeBorderHover", + "--vscode-positronModalDialog-projectTypeBorderSelected", + "--vscode-positronModalDialog-projectTypeForeground", + "--vscode-positronModalDialog-projectTypeForegroundHover", + "--vscode-positronModalDialog-projectTypeForegroundSelected", "--vscode-positronModalDialog-radioButtonBackground", "--vscode-positronModalDialog-radioButtonBorder", "--vscode-positronModalDialog-radioButtonForeground", diff --git a/extensions/jupyter-adapter/scripts/install.ts b/extensions/jupyter-adapter/scripts/install.ts index 1617f9928fc..6b2849ee448 100644 --- a/extensions/jupyter-adapter/scripts/install.ts +++ b/extensions/jupyter-adapter/scripts/install.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ import { spawnSync } from 'child_process'; @@ -32,7 +32,7 @@ if (platform() === 'darwin') { // on every invocation in that case, which is no fun. let zeromqVersion: string; if (platform() === 'darwin') { - zeromqVersion = 'kevinushey/zeromq.js#fa6b52f85293d9fe14958d18f031d65520afd272'; + zeromqVersion = 'jmcphers/zeromq.js#e260089d6ede978aeba635a5e552f46410609b83'; } else { zeromqVersion = 'zeromq@6.0.0-beta.16'; } diff --git a/extensions/jupyter-adapter/src/JupyterKernel.ts b/extensions/jupyter-adapter/src/JupyterKernel.ts index 59ae8d31ea7..ca1a373b7f4 100644 --- a/extensions/jupyter-adapter/src/JupyterKernel.ts +++ b/extensions/jupyter-adapter/src/JupyterKernel.ts @@ -344,12 +344,12 @@ export class JupyterKernel extends EventEmitter implements vscode.Disposable { this.log(`Connecting to kernel sockets defined in ${session.state.connectionFile}...`); // Wait for the sockets to connect or the timeout to expire. Note that - // each socket has 10 second timeout for connecting, so this is just an + // each socket has 20 second timeout for connecting, so this is just an // additional safeguard. await withTimeout( this.connect(session.state.connectionFile), - 15000, - `Timed out waiting 15 seconds for kernel to connect to sockets`); + 25000, + `Timed out waiting 25 seconds for kernel to connect to sockets`); // We're connected! Establish the socket listeners return this.establishSocketListeners(); @@ -1577,11 +1577,11 @@ export class JupyterKernel extends EventEmitter implements vscode.Disposable { * Creates a detailed error object to emit to the client when the kernel fails * to start. * - * @param message The error message + * @param error The source error message or object * @returns A StartupFailure object containing the error message and the * contents of the kernel's log file, if it exists */ - private createStartupFailure(message: string): StartupFailure { + private createStartupFailure(err: any): StartupFailure { // Read the content of the log file, if it exists; this may contain more detail // about why the kernel exited. let logFileContent = ''; @@ -1599,6 +1599,6 @@ export class JupyterKernel extends EventEmitter implements vscode.Disposable { .join('\n'); // Create a startup failure message - return new StartupFailure(message, logFileContent); + return new StartupFailure(err.toString(), logFileContent); } } diff --git a/extensions/jupyter-adapter/src/JupyterSocket.ts b/extensions/jupyter-adapter/src/JupyterSocket.ts index 80924575b40..f26879fce62 100644 --- a/extensions/jupyter-adapter/src/JupyterSocket.ts +++ b/extensions/jupyter-adapter/src/JupyterSocket.ts @@ -253,10 +253,10 @@ export class JupyterSocket implements vscode.Disposable { } // Compute how long we've been waiting const waitTime = Date.now() - startTime; - if (waitTime >= 10000) { - // If we've been waiting for more than 10 seconds, reject the promise - this._logger(`${this._title} socket connect timed out after 10 seconds`); - this._connectPromise.reject(new Error('Socket connect timed out after 10 seconds')); + if (waitTime >= 20000) { + // If we've been waiting for more than 20 seconds, reject the promise + this._logger(`${this._title} socket connect timed out after 20 seconds`); + this._connectPromise.reject(new Error('Socket connect timed out after 20 seconds')); this._connectPromise = undefined; // Return to the uninitialized state so a new connection can be attempted if diff --git a/extensions/positron-notebook-controllers/package.json b/extensions/positron-notebook-controllers/package.json index fac9d865e02..ca70a0bca0f 100644 --- a/extensions/positron-notebook-controllers/package.json +++ b/extensions/positron-notebook-controllers/package.json @@ -14,6 +14,16 @@ "onStartupFinished" ], "contributes": { + "commands": [ + { + "command": "positron.restartKernel", + "category": "Notebook", + "title": "%positron.command.restartKernel.title%", + "shortTitle": "%positron.command.restartKernel.shortTitle%", + "icon": "$(debug-restart)", + "enablement": "positron.hasRunningNotebookSession" + } + ], "configuration": [ { "title": "Positron Notebook Controllers", @@ -25,7 +35,16 @@ } } } - ] + ], + "menus": { + "notebook/toolbar": [ + { + "command": "positron.restartKernel", + "group": "navigation/execute@5", + "when": "notebookKernel =~ /^vscode.positron-notebook-controllers\\//" + } + ] + } }, "main": "./out/extension.js", "scripts": { diff --git a/extensions/positron-notebook-controllers/package.nls.json b/extensions/positron-notebook-controllers/package.nls.json index f4bc03ec484..26d050ffcd3 100644 --- a/extensions/positron-notebook-controllers/package.nls.json +++ b/extensions/positron-notebook-controllers/package.nls.json @@ -1,3 +1,5 @@ { + "positron.command.restartKernel.title": "Restart Kernel", + "positron.command.restartKernel.shortTitle": "Restart", "python.languageServer.description": "This setting is included as a temporary workaround to disable the Jupyter extension's language server. The value should remain 'Default'." } diff --git a/extensions/positron-notebook-controllers/src/commands.ts b/extensions/positron-notebook-controllers/src/commands.ts new file mode 100644 index 00000000000..f17c9ac2470 --- /dev/null +++ b/extensions/positron-notebook-controllers/src/commands.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { NotebookSessionService } from './notebookSessionService'; + +export function registerCommands(context: vscode.ExtensionContext, notebookSessionService: NotebookSessionService): void { + context.subscriptions.push(vscode.commands.registerCommand('positron.restartKernel', async () => { + // Get the active notebook. + const notebook = vscode.window.activeNotebookEditor?.notebook; + if (!notebook) { + throw new Error('No active notebook. This command should only be available when a notebook is active.'); + } + + // Get the session for the active notebook. + const session = notebookSessionService.getNotebookSession(notebook.uri); + if (!session) { + throw new Error('No session found for active notebook. This command should only be available when a session is running.'); + } + + // Restart the session with a progress bar. + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t("Restarting {0} interpreter for '{1}'", session.runtimeMetadata.languageName, notebook.uri.path), + }, () => notebookSessionService.restartRuntimeSession(notebook.uri)); + } catch (error) { + vscode.window.showErrorMessage( + vscode.l10n.t("Restarting {0} interpreter for '{1}' failed. Reason: {2}", + session.runtimeMetadata.languageName, notebook.uri.path, error.message)); + } + })); +} diff --git a/extensions/positron-notebook-controllers/src/extension.ts b/extensions/positron-notebook-controllers/src/extension.ts index 9fbfa8e5d87..87227e930bd 100644 --- a/extensions/positron-notebook-controllers/src/extension.ts +++ b/extensions/positron-notebook-controllers/src/extension.ts @@ -6,14 +6,17 @@ import * as positron from 'positron'; import * as vscode from 'vscode'; import { NotebookControllerManager } from './notebookControllerManager'; import { NotebookSessionService } from './notebookSessionService'; +import { registerCommands } from './commands'; export const log = vscode.window.createOutputChannel('Positron Notebook Controllers', { log: true }); export async function activate(context: vscode.ExtensionContext): Promise { const notebookSessionService = new NotebookSessionService(); + context.subscriptions.push(notebookSessionService); // Shutdown any running sessions when a notebook is closed. context.subscriptions.push(vscode.workspace.onDidCloseNotebookDocument(async (notebook) => { + log.debug(`Notebook closed: ${notebook.uri.path}`); if (notebookSessionService.hasStartingOrRunningNotebookSession(notebook.uri)) { await notebookSessionService.shutdownRuntimeSession(notebook.uri); } @@ -45,4 +48,28 @@ export async function activate(context: vscode.ExtensionContext): Promise for (const notebook of vscode.workspace.notebookDocuments) { manager.updateNotebookAffinity(notebook); } + + // Set the hasRunningNotebookSession context when the active notebook editor changes. + vscode.window.onDidChangeActiveNotebookEditor((editor) => { + const value = notebookSessionService.hasRunningNotebookSession(editor?.notebook.uri); + setHasRunningNotebookSessionContext(value); + }); + + // Set the hasRunningNotebookSession context when a session is started/shutdown for the active notebook. + context.subscriptions.push(notebookSessionService.onDidChangeNotebookSession((e) => { + if (e.notebookUri === vscode.window.activeNotebookEditor?.notebook.uri) { + setHasRunningNotebookSessionContext(!!e.session); + } + })); + + registerCommands(context, notebookSessionService); +} + +function setHasRunningNotebookSessionContext(value: boolean): void { + log.debug(`Setting 'positron.hasRunningNotebookSession' context to: ${value}`); + vscode.commands.executeCommand( + 'setContext', + 'positron.hasRunningNotebookSession', + value, + ); } diff --git a/extensions/positron-notebook-controllers/src/notebookController.ts b/extensions/positron-notebook-controllers/src/notebookController.ts index 3794c359323..9d68d02b14d 100644 --- a/extensions/positron-notebook-controllers/src/notebookController.ts +++ b/extensions/positron-notebook-controllers/src/notebookController.ts @@ -67,6 +67,13 @@ export class NotebookController implements vscode.Disposable { await this._notebookSessionService.shutdownRuntimeSession(e.notebook.uri); } })); + + // Restart the execution counter when a notebook session is started. + this._disposables.push(this._notebookSessionService.onDidChangeNotebookSession(async (e) => { + if (e.session) { + this._executionOrderBySessionId.delete(e.session.metadata.sessionId); + } + })); } /** The human-readable label of the controller. */ @@ -80,7 +87,7 @@ export class NotebookController implements vscode.Disposable { * @param notebook The notebook to start a runtime for. * @returns Promise that resolves when the runtime has started. */ - public async startRuntimeSession(notebook: vscode.NotebookDocument): Promise { + private async startRuntimeSession(notebook: vscode.NotebookDocument): Promise { try { return await this._notebookSessionService.startRuntimeSession(notebook.uri, this._languageId); } catch (err) { diff --git a/extensions/positron-notebook-controllers/src/notebookSessionService.ts b/extensions/positron-notebook-controllers/src/notebookSessionService.ts index 58a7bc352ee..552113186f0 100644 --- a/extensions/positron-notebook-controllers/src/notebookSessionService.ts +++ b/extensions/positron-notebook-controllers/src/notebookSessionService.ts @@ -3,10 +3,17 @@ *--------------------------------------------------------------------------------------------*/ import * as positron from 'positron'; -import { Uri } from 'vscode'; +import * as vscode from 'vscode'; import { log } from './extension'; import { ResourceMap } from './map'; -import { DeferredPromise } from './util'; + +export interface INotebookSessionDidChangeEvent { + /** The URI of the notebook corresponding to the session. */ + readonly notebookUri: vscode.Uri; + + /** The session that was set for the notebook, or undefined if it was deleted. */ + readonly session?: positron.LanguageRuntimeSession; +} /** * The notebook session service is the main interface for interacting with @@ -17,31 +24,64 @@ import { DeferredPromise } from './util'; * required into the runtime session service and expose what's needed via the Positron Extensions * API. */ -export class NotebookSessionService { +export class NotebookSessionService implements vscode.Disposable { + private readonly _disposables = new Array(); /** * A map of sessions currently starting, keyed by notebook URI. Values are promises that resolve * when the session has started and is ready to execute code. */ - private readonly _startingSessionsByNotebookUri = new ResourceMap>(); + private readonly _startingSessionsByNotebookUri = new ResourceMap>(); /** * A map of sessions currently shutting down, keyed by notebook URI. Values are promises that resolve * when the session has completed the shutdown sequence. */ - private readonly _shuttingDownSessionsByNotebookUri = new ResourceMap>(); + private readonly _shuttingDownSessionsByNotebookUri = new ResourceMap>(); + + /** + * A map of sessions currently restarting, keyed by notebook URI. Values are promises that resolve + * when the session has completed the restart sequence. + */ + private readonly _restartingSessionsByNotebookUri = new ResourceMap>(); /** A map of the currently active notebook sessions, keyed by notebook URI. */ private readonly _notebookSessionsByNotebookUri = new ResourceMap(); + /** The event emitter for the onDidChangeNotebookSession event. */ + private readonly _onDidChangeNotebookSession = this._register(new vscode.EventEmitter); + + /** An event that fires when a session is set/unset for a notebook. */ + readonly onDidChangeNotebookSession = this._onDidChangeNotebookSession.event; + + private _register(disposable: T): T { + this._disposables.push(disposable); + return disposable; + } + /** * Checks for a starting or running notebook for the given notebook URI. * * @param notebookUri The notebook URI to check for. * @returns True if a starting or running notebook session exists for the given notebook URI. */ - hasStartingOrRunningNotebookSession(notebookUri: Uri): boolean { - return this._startingSessionsByNotebookUri.has(notebookUri) || this._notebookSessionsByNotebookUri.has(notebookUri); + hasStartingOrRunningNotebookSession(notebookUri: vscode.Uri): boolean { + return this._startingSessionsByNotebookUri.has(notebookUri) || + this._restartingSessionsByNotebookUri.has(notebookUri) || + this._notebookSessionsByNotebookUri.has(notebookUri); + } + + /** + * Checks for a running notebook for the given notebook URI. + * + * @param notebookUri The notebook URI to check for. + * @returns True if a running notebook session exists for the given notebook URI. + */ + hasRunningNotebookSession(notebookUri: vscode.Uri | undefined): boolean { + if (!notebookUri) { + return false; + } + return this._notebookSessionsByNotebookUri.has(notebookUri); } /** @@ -50,103 +90,120 @@ export class NotebookSessionService { * @param notebookUri The notebook URI of the session to retrieve. * @returns The running notebook session for the given notebook URI, if one exists. */ - getNotebookSession(notebookUri: Uri): positron.LanguageRuntimeSession | undefined { + getNotebookSession(notebookUri: vscode.Uri): positron.LanguageRuntimeSession | undefined { return this._notebookSessionsByNotebookUri.get(notebookUri); } + /** + * Set a notebook session for a notebook URI. + * + * @param notebookUri The notebook URI of the session to set. + * @param session The session to set for the notebook URI, or undefined to delete the session. + */ + setNotebookSession(notebookUri: vscode.Uri, session: positron.LanguageRuntimeSession | undefined): void { + if (session) { + this._notebookSessionsByNotebookUri.set(notebookUri, session); + } else { + this._notebookSessionsByNotebookUri.delete(notebookUri); + } + this._onDidChangeNotebookSession.fire({ notebookUri, session }); + } + /** * Start a new runtime session for a notebook. * * @param notebookUri The notebook URI to start a runtime for. * @returns Promise that resolves when the runtime startup sequence has been started. */ - async startRuntimeSession(notebookUri: Uri, languageId: string): Promise { + async startRuntimeSession(notebookUri: vscode.Uri, languageId: string): Promise { // Return the existing promise, if there is one. - const startingSessionPromise = this._startingSessionsByNotebookUri.get(notebookUri); - if (startingSessionPromise && !startingSessionPromise.isSettled) { - return startingSessionPromise.p; + const startingSessionPromise = this._startingSessionsByNotebookUri.get(notebookUri) || + this._restartingSessionsByNotebookUri.get(notebookUri); + if (startingSessionPromise) { + return startingSessionPromise; } - // Update the starting sessions map. This needs to be set before any awaits in case another - // caller tries to start a runtime or access the start promise concurrently. - const startPromise = new DeferredPromise(); + // Construct a wrapping promise that resolves/rejects after the session maps have been updated. + const startPromise = (async () => { + try { + const session = await this.doStartRuntimeSession(notebookUri, languageId); + this._startingSessionsByNotebookUri.delete(notebookUri); + this.setNotebookSession(notebookUri, session); + log.info(`Session ${session.metadata.sessionId} is started`); + return session; + } catch (err) { + this._startingSessionsByNotebookUri.delete(notebookUri); + throw err; + } + })(); + this._startingSessionsByNotebookUri.set(notebookUri, startPromise); - // If the notebook has a session that is still shutting down, wait for it to finish. + return startPromise; + } + + async doStartRuntimeSession(notebookUri: vscode.Uri, languageId: string): Promise { + // If the session is still shutting down, wait for it to finish. const shuttingDownSessionPromise = this._shuttingDownSessionsByNotebookUri.get(notebookUri); - if (shuttingDownSessionPromise && !shuttingDownSessionPromise.isSettled) { - await shuttingDownSessionPromise.p; + if (shuttingDownSessionPromise) { + try { + await shuttingDownSessionPromise; + } catch (err) { + log.error(`Waiting for notebook runtime to shutdown before starting failed. Reason ${err}`); + throw err; + } } // Ensure that we don't start a runtime for a notebook that already has one. if (this._notebookSessionsByNotebookUri.has(notebookUri)) { - const err = new Error(`Tried to start a runtime for a notebook that already has one: ${notebookUri.path}`); - startPromise.error(err); - this._startingSessionsByNotebookUri.delete(notebookUri); - throw err; + throw new Error(`Tried to start a runtime for a notebook that already has one: ${notebookUri.path}`); } // If there's already a session for this runtime e.g. one restored after a window reload, use it. - let session: positron.LanguageRuntimeSession | undefined; try { - session = await positron.runtime.getNotebookSession(notebookUri); + const session = await positron.runtime.getNotebookSession(notebookUri); if (session) { + // TODO: If it isn't running, log an error and start a new one. + // TODO: If it doesn't match the runtime ID, log an error, shut it down, and start a new one. log.info( `Restored session for language runtime ${session.metadata.sessionId} ` + `(language: ${session.runtimeMetadata.languageName}, name: ${session.runtimeMetadata.runtimeName}, ` + `version: ${session.runtimeMetadata.runtimeVersion}, notebook: ${notebookUri.path})` ); + return session; } } catch (err) { log.error( `Getting existing session for notebook ${notebookUri.path}' failed. Reason: ${err}` ); - startPromise.error(err); - this._startingSessionsByNotebookUri.delete(notebookUri); throw err; } - // TODO: If it isn't running, log an error and start a new one. - // TODO: If it doesn't match the runtime ID, log an error, shut it down, and start a new one. - // If we couldn't restore a session, start a new one. - if (!session) { - // Get the preferred runtime for this language. - let preferredRuntime: positron.LanguageRuntimeMetadata; - try { - preferredRuntime = await positron.runtime.getPreferredRuntime(languageId); - } catch (err) { - log.error(`Getting preferred runtime for language '${languageId}' failed. Reason: ${err}`); - startPromise.error(err); - this._startingSessionsByNotebookUri.delete(notebookUri); - throw err; - } - - try { - session = await positron.runtime.startLanguageRuntime( - preferredRuntime.runtimeId, - notebookUri.path, // Use the notebook's path as the session name. - notebookUri); - log.info( - `Starting session for language runtime ${session.metadata.sessionId} ` - + `(language: ${session.runtimeMetadata.languageName}, name: ${session.runtimeMetadata.runtimeName}, ` - + `version: ${session.runtimeMetadata.runtimeVersion}, notebook: ${notebookUri.path})` - ); - } catch (err) { - log.error(`Starting session for language runtime ${preferredRuntime.runtimeName} failed. Reason: ${err}`); - startPromise.error(err); - this._startingSessionsByNotebookUri.delete(notebookUri); - throw err; - } + // Get the preferred runtime for this language. + let preferredRuntime: positron.LanguageRuntimeMetadata; + try { + preferredRuntime = await positron.runtime.getPreferredRuntime(languageId); + } catch (err) { + log.error(`Getting preferred runtime for language '${languageId}' failed. Reason: ${err}`); + throw err; } - // Complete the promise and update the session maps. - this._notebookSessionsByNotebookUri.set(notebookUri, session); - this._startingSessionsByNotebookUri.delete(notebookUri); - startPromise.complete(session); - log.info(`Session ${session.metadata.sessionId} is ready`); - - return session; + try { + const session = await positron.runtime.startLanguageRuntime( + preferredRuntime.runtimeId, + notebookUri.path, // Use the notebook's path as the session name. + notebookUri); + log.info( + `Starting session for language runtime ${session.metadata.sessionId} ` + + `(language: ${session.runtimeMetadata.languageName}, name: ${session.runtimeMetadata.runtimeName}, ` + + `version: ${session.runtimeMetadata.runtimeVersion}, notebook: ${notebookUri.path})` + ); + return session; + } catch (err) { + log.error(`Starting session for language runtime ${preferredRuntime.runtimeName} failed. Reason: ${err}`); + throw err; + } } /** @@ -155,77 +212,173 @@ export class NotebookSessionService { * @param notebookUri The notebook URI whose runtime to shutdown. * @returns Promise that resolves when the runtime shutdown sequence has been started. */ - async shutdownRuntimeSession(notebookUri: Uri): Promise { + async shutdownRuntimeSession(notebookUri: vscode.Uri): Promise { // Return the existing promise, if there is one. const shuttingDownSessionPromise = this._shuttingDownSessionsByNotebookUri.get(notebookUri); - if (shuttingDownSessionPromise && !shuttingDownSessionPromise.isSettled) { - return shuttingDownSessionPromise.p; + if (shuttingDownSessionPromise) { + return shuttingDownSessionPromise; } - // Update the shutting down sessions map. This needs to be set before any awaits in case - // another caller tries to shutdown a runtime or access the shutdown promise concurrently. - const shutDownPromise = new DeferredPromise(); - this._shuttingDownSessionsByNotebookUri.set(notebookUri, shutDownPromise); + // Construct a wrapping promise that resolves/rejects after the session maps have been updated. + const shutdownPromise = (async () => { + try { + const session = await this.doShutdownRuntimeSession(notebookUri); + this._shuttingDownSessionsByNotebookUri.delete(notebookUri); + this.setNotebookSession(notebookUri, undefined); + log.info(`Session ${session.metadata.sessionId} is shutdown`); + } catch (err) { + this._startingSessionsByNotebookUri.delete(notebookUri); + throw err; + } + })(); + + this._shuttingDownSessionsByNotebookUri.set(notebookUri, shutdownPromise); + + return shutdownPromise; + } + async doShutdownRuntimeSession(notebookUri: vscode.Uri): Promise { // Get the notebook's session. let session = this._notebookSessionsByNotebookUri.get(notebookUri); - // If the runtime is still starting, wait for it to be ready. if (!session) { - const startingSessionPromise = this._startingSessionsByNotebookUri.get(notebookUri); - if (startingSessionPromise && !startingSessionPromise.isSettled) { - session = await startingSessionPromise.p; + // If the notebook's session is still starting, wait for it to finish. + const startingSessionPromise = this._startingSessionsByNotebookUri.get(notebookUri) || + this._restartingSessionsByNotebookUri.get(notebookUri); + if (startingSessionPromise) { + try { + session = await startingSessionPromise; + } catch (err) { + log.error(`Waiting for notebook runtime to start before shutting down failed. Reason ${err}`); + throw err; + } } } // Ensure that we have a session. if (!session) { - const err = new Error(`Tried to shutdown runtime for notebook without a running runtime: ${notebookUri.path}`); - this._shuttingDownSessionsByNotebookUri.delete(notebookUri); - shutDownPromise.error(err); - throw err; + throw new Error(`Tried to shutdown runtime for notebook without a running runtime: ${notebookUri.path}`); } // Start the shutdown sequence. try { - await session.shutdown(positron.RuntimeExitReason.Shutdown); log.info(`Shutting down runtime ${session.runtimeMetadata.runtimeName} for notebook ${notebookUri.path}`); + await session.shutdown(positron.RuntimeExitReason.Shutdown); } catch (err) { log.error(`Shutting down runtime ${session.runtimeMetadata.runtimeName} for notebook ${notebookUri.path} failed. Reason: ${err}`); - this._shuttingDownSessionsByNotebookUri.delete(notebookUri); - this._notebookSessionsByNotebookUri.delete(notebookUri); - shutDownPromise.error(err); throw err; } // Wait for the session to end. This is necessary so that we know when to start the next // session for the notebook, since at most one session can exist per notebook. - try { - const timeout = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(`Shutting down runtime ${session.runtimeMetadata.runtimeName} for notebook ${notebookUri.path} timed out`)); - }, 5000); + const timeout = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`Shutting down runtime ${session.runtimeMetadata.runtimeName} for notebook ${notebookUri.path} timed out`)); + }, 5000); + }); + const promise = new Promise(resolve => { + const disposable = session.onDidEndSession(() => { + disposable.dispose(); + resolve(); }); - const promise = new Promise(resolve => { - const disposable = session.onDidEndSession(() => { + }); + try { + await Promise.race([promise, timeout]); + } catch (err) { + log.error(err); + throw err; + } + + return session; + } + + /** + * Restart a runtime session for a notebook. + * + * @param notebookUri The notebook URI to restart a runtime for. + * @returns Promise that resolves when the runtime restart sequence has completed and the + * session is enters the ready state. + */ + async restartRuntimeSession(notebookUri: vscode.Uri): Promise { + // Return the existing promise, if there is one. + const startingSessionPromise = this._startingSessionsByNotebookUri.get(notebookUri) || + this._restartingSessionsByNotebookUri.get(notebookUri); + if (startingSessionPromise) { + return startingSessionPromise; + } + + // Construct a wrapping promise that resolves/rejects after the session maps have been updated. + const restartPromise = (async () => { + try { + const session = await this.doRestartRuntimeSession(notebookUri); + this._restartingSessionsByNotebookUri.delete(notebookUri); + this.setNotebookSession(notebookUri, session); + log.info(`Session ${session.metadata.sessionId} is restarted`); + return session; + } catch (err) { + this._restartingSessionsByNotebookUri.delete(notebookUri); + throw err; + } + })(); + + this._restartingSessionsByNotebookUri.set(notebookUri, restartPromise); + + return restartPromise; + } + + async doRestartRuntimeSession(notebookUri: vscode.Uri): Promise { + // Get the notebook's session. + const session = this._notebookSessionsByNotebookUri.get(notebookUri); + if (!session) { + throw new Error(`Tried to restart runtime for notebook without a running runtime: ${notebookUri.path}`); + } + + // Remove the session from the map of active notebooks in case it's accessed while we're + // restarting. + this.setNotebookSession(notebookUri, undefined); + + // If the notebook's session is still shutting down, wait for it to finish. + const shuttingDownSessionPromise = this._shuttingDownSessionsByNotebookUri.get(notebookUri); + if (shuttingDownSessionPromise) { + try { + await shuttingDownSessionPromise; + } catch (err) { + log.error(`Waiting for notebook runtime to shutdown before starting failed. Reason ${err}`); + throw err; + } + } + + // Start the restart sequence. + try { + log.info(`Restarting session ${session.metadata.sessionId} for notebook ${notebookUri.path}`); + await positron.runtime.restartSession(session.metadata.sessionId); + } catch (err) { + log.error(`Restarting session ${session.metadata.sessionId} for notebook ${notebookUri.path} failed. Reason: ${err}`); + throw err; + } + + // Wait for the session to be ready, or for a timeout. + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout waiting for runtime to restart')), 5000)); + const promise = new Promise((resolve) => { + const disposable = session.onDidChangeRuntimeState((state) => { + if (state === positron.RuntimeState.Ready) { disposable.dispose(); resolve(); - }); + } }); + }); + try { await Promise.race([promise, timeout]); } catch (err) { log.error(err); - this._shuttingDownSessionsByNotebookUri.delete(notebookUri); - this._notebookSessionsByNotebookUri.delete(notebookUri); - shutDownPromise.error(err); throw err; } - // Complete the promise and update the session maps. - this._shuttingDownSessionsByNotebookUri.delete(notebookUri); - this._notebookSessionsByNotebookUri.delete(notebookUri); - shutDownPromise.complete(); - log.info(`Session ${session.metadata.sessionId} shutdown completed`); + return session; } + dispose() { + this._disposables.forEach(d => d.dispose()); + } } diff --git a/extensions/positron-notebook-controllers/src/util.ts b/extensions/positron-notebook-controllers/src/util.ts deleted file mode 100644 index e6816e1bb62..00000000000 --- a/extensions/positron-notebook-controllers/src/util.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -/** - * A promise with exposed and imperatively callable resolve and reject methods. - */ -export class DeferredPromise { - complete!: (value: T) => void; - error!: (error: unknown) => void; - isSettled: boolean = false; - p: Promise; - value: T | undefined; - - constructor() { - this.p = new Promise((resolve, reject) => { - this.complete = (val) => { - this.isSettled = true; - this.value = val; - resolve(val); - }; - this.error = (reason) => { - this.isSettled = true; - reject(reason); - }; - }); - } -} - -export function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/extensions/positron-python/.devcontainer/devcontainer.json b/extensions/positron-python/.devcontainer/devcontainer.json index fe15f35764e..67a8833d30c 100644 --- a/extensions/positron-python/.devcontainer/devcontainer.json +++ b/extensions/positron-python/.devcontainer/devcontainer.json @@ -9,13 +9,13 @@ "customizations": { "vscode": { "extensions": [ + "charliermarsh.ruff", "editorconfig.editorconfig", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-python.python", - "ms-python.black-formatter", "ms-python.vscode-pylance", - "charliermarsh.ruff" + "ms-python.debugpy" ] } }, diff --git a/extensions/positron-python/.eslintignore b/extensions/positron-python/.eslintignore index 7f6bb48d6c8..9399ff461dc 100644 --- a/extensions/positron-python/.eslintignore +++ b/extensions/positron-python/.eslintignore @@ -105,7 +105,7 @@ src/test/common/process/proc.unit.test.ts src/test/common/interpreterPathService.unit.test.ts -src/test/pythonFiles/formatting/dummy.ts +src/test/python_files/formatting/dummy.ts src/test/debugger/extension/adapter/adapter.test.ts src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts diff --git a/extensions/positron-python/.git-blame-ignore-revs b/extensions/positron-python/.git-blame-ignore-revs new file mode 100644 index 00000000000..e2c2a50781b --- /dev/null +++ b/extensions/positron-python/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Prettier +2b6a8f2d439fe9d5e66665ea46d8b690ac9b2c39 +649156a09ccdc51c0d20f7cd44540f1918f9347b +4f774d94bf4fbf87bb417b2b2b8e79e334eb3536 +61b179b2092050709e3c373a6738abad8ce581c4 +c33617b0b98daeb4d72040b48c5850b476d6256c +db8e1e2460e9754ec0672d958789382b6d15c5aa +08bc9ad3bee5b19f02fa756fbc53ab32f1b39920 +# Black +a58eeffd1b64498e2afe5f11597888dfd1c8699c +5cd8f539f4d2086b718c8f11f823c0ac12fc2c49 +9ec9e9eaebb25adc6d942ac19d4d6c128abb987f +c4af91e090057d20d7a633b3afa45eaa13ece76f +# Ruff +e931bed3efbede7b05113316506958ecd7506777 diff --git a/extensions/positron-python/.github/actions/build-vsix/action.yml b/extensions/positron-python/.github/actions/build-vsix/action.yml index 52d6d1cdbdd..5b8a569889a 100644 --- a/extensions/positron-python/.github/actions/build-vsix/action.yml +++ b/extensions/positron-python/.github/actions/build-vsix/action.yml @@ -30,7 +30,7 @@ runs: cache-dependency-path: | requirements.txt build/build-install-requirements.txt - pythonFiles/jedilsp_requirements/requirements.txt + python_files/jedilsp_requirements/requirements.txt - name: Upgrade Pip run: python -m pip install -U pip @@ -44,20 +44,20 @@ runs: - name: Install Python dependencies uses: brettcannon/pip-secure-install@v1 with: - options: '-t ./pythonFiles/lib/python --implementation py' + options: '-t ./python_files/lib/python --implementation py' - name: Install debugpy and get-pip run: | python -m pip --disable-pip-version-check install packaging - python ./pythonFiles/install_debugpy.py - python ./pythonFiles/download_get_pip.py + python ./python_files/install_debugpy.py + python ./python_files/download_get_pip.py shell: bash - name: Install Jedi LSP uses: brettcannon/pip-secure-install@v1 with: - requirements-file: './pythonFiles/jedilsp_requirements/requirements.txt' - options: '-t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none' + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --implementation py --platform any --abi none' - name: Run npm ci run: npm ci --prefer-offline diff --git a/extensions/positron-python/.github/actions/lint/action.yml b/extensions/positron-python/.github/actions/lint/action.yml index 47924c10815..444f331a3a9 100644 --- a/extensions/positron-python/.github/actions/lint/action.yml +++ b/extensions/positron-python/.github/actions/lint/action.yml @@ -41,16 +41,10 @@ runs: python-version: '3.x' cache: 'pip' - - name: Check Python format - run: | - python -m pip install -U black - python -m black . --check - working-directory: pythonFiles - shell: bash - - name: Run Ruff run: | python -m pip install -U ruff python -m ruff check . - working-directory: pythonFiles + python -m ruff format --check + working-directory: python_files shell: bash diff --git a/extensions/positron-python/.github/actions/smoke-tests/action.yml b/extensions/positron-python/.github/actions/smoke-tests/action.yml index b2d00205043..7eaa2483942 100644 --- a/extensions/positron-python/.github/actions/smoke-tests/action.yml +++ b/extensions/positron-python/.github/actions/smoke-tests/action.yml @@ -34,12 +34,12 @@ runs: - name: Install Python requirements uses: brettcannon/pip-secure-install@v1 with: - options: '-t ./pythonFiles/lib/python --implementation py' + options: '-t ./python_files/lib/python --implementation py' - name: pip install system test requirements run: | python -m pip install --upgrade -r build/test-requirements.txt - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --implementation py --no-deps --upgrade --pre debugpy shell: bash # Bits from the VSIX are reused by smokeTest.ts to speed things up. diff --git a/extensions/positron-python/.github/release_plan.md b/extensions/positron-python/.github/release_plan.md index 71f8d8aa095..4d57726b4d0 100644 --- a/extensions/positron-python/.github/release_plan.md +++ b/extensions/positron-python/.github/release_plan.md @@ -1,14 +1,34 @@ +### General Notes All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. +
+ Release Primary and Secondary Assignments for the 2024 Calendar Year -NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. +| Month | Primary | Secondary | +|:----------|:----------|:------------| +✅ | ~~January~~ | ~~Eleanor~~ | ~~Karthik~~ | +✅ | February | Kartik | Anthony | +| March | Karthik | Eleanor | +| April | Paula | Kartik | +| May | Anthony | Karthik | +| June | Eleanor | Paula | +| July | Anthony | Karthik | +| August | Paula | Kartik | +| September | Anthony | Eleanor | +| October | Paula | Kartik | +| November | Kartik | Eleanor | +| December | Karthik | Anthony | + +
# Release candidate (Monday, XXX XX) NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. +NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. + ### Step 1: ##### Bump the version of `main` to be a release candidate (also updating debugpy dependences, third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen 🥶) @@ -17,7 +37,7 @@ NOTE: Third Party Notices are automatically added by our build pipelines using - [ ] Create a new branch called **`bump-release-[YYYY.minor]`**. - [ ] Change the version in `package.json` to the next **even** number and switch the `-dev` to `-rc`. (🤖) - [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (🤖) -- [ ] Check [debugpy on PyPI](https://pypi.org/project/debugpy/) for a new release and update the version of debugpy in [`install_debugpy.py`](https://github.com/microsoft/vscode-python/blob/main/pythonFiles/install_debugpy.py) if necessary. +- [ ] Check [debugpy on PyPI](https://pypi.org/project/debugpy/) for a new release and update the version of debugpy in [`install_debugpy.py`](https://github.com/microsoft/vscode-python/blob/main/python_files/install_debugpy.py) if necessary. - [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. - [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index 586fe619d5d..544d07a6574 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -87,21 +87,21 @@ jobs: - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 with: - options: '-t ./pythonFiles/lib/python --no-cache-dir --implementation py' + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements run: python scripts/vendor.py - name: Install other Python requirements run: | - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright uses: jakebailey/pyright-action@v2 with: version: 1.1.308 - working-directory: 'pythonFiles' + working-directory: 'python_files' python-tests: name: Python Tests @@ -134,13 +134,13 @@ jobs: uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py + run: python python_files/tests/run_all.py tests: name: Tests @@ -189,19 +189,19 @@ jobs: run: | python -m pip install wheel python -m pip install -r build/build-install-requirements.txt - python ./pythonFiles/download_get_pip.py + python ./python_files/download_get_pip.py shell: bash - name: Install debugpy run: | # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' if: startsWith(matrix.python, 3.) - name: Install Jedi requirements @@ -215,7 +215,7 @@ jobs: run: | python -m pip install wheel python -m pip install -r build/build-install-requirements.txt - python ./pythonFiles/install_debugpy.py + python ./python_files/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index 9b2aaa09974..427649ecd62 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -61,21 +61,21 @@ jobs: - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 with: - options: '-t ./pythonFiles/lib/python --no-cache-dir --implementation py' + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements run: python scripts/vendor.py - name: Install other Python requirements run: | - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright uses: jakebailey/pyright-action@v2 with: version: 1.1.308 - working-directory: 'pythonFiles' + working-directory: 'python_files' python-tests: name: Python Tests @@ -121,13 +121,13 @@ jobs: uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py + run: python python_files/tests/run_all.py tests: name: Tests @@ -176,20 +176,20 @@ jobs: - name: Install debugpy run: | # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy - name: Download get-pip.py run: | python -m pip install wheel python -m pip install -r build/build-install-requirements.txt - python ./pythonFiles/download_get_pip.py + python ./python_files/download_get_pip.py shell: bash - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' - options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' - name: Install Jedi requirements run: python scripts/vendor.py @@ -201,7 +201,7 @@ jobs: run: | python -m pip install wheel python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt - python ./pythonFiles/install_debugpy.py + python ./python_files/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' @@ -387,14 +387,14 @@ jobs: cache: 'pip' cache-dependency-path: | requirements.txt - pythonFiles/jedilsp_requirements/requirements.txt + python_files/jedilsp_requirements/requirements.txt build/test-requirements.txt build/functional-test-requirements.txt - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 with: - options: '-t ./pythonFiles/lib/python --implementation py' + options: '-t ./python_files/lib/python --implementation py' - name: Install Jedi requirements run: python scripts/vendor.py @@ -402,7 +402,7 @@ jobs: - name: Install debugpy run: | # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy + python -m pip --disable-pip-version-check install -t ./python_files/lib/python --implementation py --no-deps --upgrade --pre debugpy - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -478,7 +478,7 @@ jobs: - name: Run Python unit tests run: | - python pythonFiles/tests/run_all.py + python python_files/tests/run_all.py # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, diff --git a/extensions/positron-python/.gitignore b/extensions/positron-python/.gitignore index dceec679bc2..b38aca49bd6 100644 --- a/extensions/positron-python/.gitignore +++ b/extensions/positron-python/.gitignore @@ -23,9 +23,8 @@ cucumber-report.json **/.venv*/ port.txt precommit.hook -pythonFiles/lib/** -pythonFiles/get-pip.py -pythonFiles/get_pip.py +python_files/lib/** +python_files/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** @@ -52,6 +51,6 @@ dist/** l10n/ tags # --- Start Positron --- -pythonFiles/positron/positron_ipykernel/tests/images -pythonFiles/positron/positron_ipykernel/_vendor/** +python_files/positron/positron_ipykernel/tests/images +python_files/positron/positron_ipykernel/_vendor/** # --- End Positron --- diff --git a/extensions/positron-python/.pre-commit-config.yaml b/extensions/positron-python/.pre-commit-config.yaml index 25e2507b3ad..21de8f8f8da 100644 --- a/extensions/positron-python/.pre-commit-config.yaml +++ b/extensions/positron-python/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: "(.*\\.csv)|(^build/)|(^src/)|(^resources/)|(^pythonFiles/tests/unittestadapter)|(^pythonFiles/tests/testing_tools)" +exclude: "(.*\\.csv)|(^build/)|(^src/)|(^resources/)|(^python_files/tests/unittestadapter)|(^python_files/tests/testing_tools)" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 diff --git a/extensions/positron-python/.vscode/extensions.json b/extensions/positron-python/.vscode/extensions.json index 93a73827e7a..15e6aada1d5 100644 --- a/extensions/positron-python/.vscode/extensions.json +++ b/extensions/positron-python/.vscode/extensions.json @@ -2,13 +2,11 @@ // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ + "charliermarsh.ruff", "editorconfig.editorconfig", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-python.python", - "ms-python.black-formatter", - "ms-python.vscode-pylance", - "ms-python.isort", - "ms-python.flake8" + "ms-python.vscode-pylance" ] } diff --git a/extensions/positron-python/.vscode/launch.json b/extensions/positron-python/.vscode/launch.json index bfb336d767b..ce67e4f77d6 100644 --- a/extensions/positron-python/.vscode/launch.json +++ b/extensions/positron-python/.vscode/launch.json @@ -268,7 +268,7 @@ }, { "name": "Listen", - "type": "python", + "type": "debugpy", "request": "attach", "listen": { "host": "localhost", "port": 5678 }, "justMyCode": true @@ -276,10 +276,10 @@ { "name": "Debug pytest plugin tests", - "type": "python", + "type": "debugpy", "request": "launch", "module": "pytest", - "args": ["${workspaceFolder}/pythonFiles/tests/pytestadapter"], + "args": ["${workspaceFolder}/python_files/tests/pytestadapter"], "justMyCode": true } ], diff --git a/extensions/positron-python/.vscode/settings.json b/extensions/positron-python/.vscode/settings.json index 72f4133a4a9..0a2f64ec534 100644 --- a/extensions/positron-python/.vscode/settings.json +++ b/extensions/positron-python/.vscode/settings.json @@ -27,7 +27,7 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff", }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -68,7 +68,7 @@ // Open merge editor for resolving conflicts. "git.mergeEditor": true, "python.testing.pytestArgs": [ - "pythonFiles/tests" + "python_files/tests" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true diff --git a/extensions/positron-python/.vscodeignore b/extensions/positron-python/.vscodeignore index 6788f9b6d8e..9d1934545bf 100644 --- a/extensions/positron-python/.vscodeignore +++ b/extensions/positron-python/.vscodeignore @@ -51,16 +51,16 @@ obj/** out/**/*.stats.json out/client/**/*.analyzer.html out/coverconfig.json -out/pythonFiles/** +out/python_files/** out/src/** out/test/** out/testMultiRootWkspc/** precommit.hook -pythonFiles/**/*.pyc -pythonFiles/lib/**/*.egg-info/** -pythonFiles/lib/python/bin/** -pythonFiles/jedilsp_requirements/** -pythonFiles/tests/** +python_files/**/*.pyc +python_files/lib/**/*.egg-info/** +python_files/lib/python/bin/** +python_files/jedilsp_requirements/** +python_files/tests/** scripts/** src/** test/** diff --git a/extensions/positron-python/README.md b/extensions/positron-python/README.md index 718f4b48857..3fc04e10197 100644 --- a/extensions/positron-python/README.md +++ b/extensions/positron-python/README.md @@ -6,23 +6,23 @@ You can read more about Positron IDE development on the [Positron Wiki](https:// ## About -The extension is a fork of [Microsoft's Python VSCode extension](https://github.com/microsoft/vscode-python). The main TypeScript functionality (mostly UI) is implemented in [`src`](src) and calls out to Python scripts in [`pythonFiles`](pythonFiles). +The extension is a fork of [Microsoft's Python VSCode extension](https://github.com/microsoft/vscode-python). The main TypeScript functionality (mostly UI) is implemented in [`src`](src) and calls out to Python scripts in [`python_files`](python_files). We provide a custom Positron Python Kernel based on the following open-source Python projects: - [**IPyKernel**](https://github.com/ipython/ipykernel), a Jupyter kernel for the Python programming language written in Python - [**Jedi Language Server**](https://github.com/pappasam/jedi-language-server), a language server built on the [pygls](https://github.com/openlawlibrary/pygls) (Python Generic Language Server Framework) using the [Jedi](https://github.com/davidhalter/jedi) library for autocompletion, static analysis, and refactoring -The entrypoint to our kernel is the [`positron_language_server.py`](pythonFiles/positron/positron_language_server.py) script. The core functionality of the kernel can be found in the [`positron_ipykernel`](pythonFiles/positron/positron/positron_ipykernel/) package, which consists of these services: +The entrypoint to our kernel is the [`positron_language_server.py`](python_files/positron/positron_language_server.py) script. The core functionality of the kernel can be found in the [`positron_ipykernel`](python_files/positron/positron/positron_ipykernel/) package, which consists of these services: -- [`positron_ipkernel`](pythonFiles/positron/positron_ipykernel/positron_ipkernel.py), the Positron Python Kernel -- [`positron_jedilsp`](pythonFiles/positron/positron_ipykernel/positron_jedilsp.py), the Positron Python Language Server -- [`variables`](pythonFiles/positron/positron_ipykernel/variables.py), manages Positron's Variables pane -- [`ui`](pythonFiles/positron/positron_ipykernel/ui.py), manages Positron's Frontend comm channel (a global channel for communication unscoped to any particular view) -- [`help`](pythonFiles/positron/positron_ipykernel/help.py), manages Positron's Help pane -- [`lsp`](pythonFiles/positron/positron_ipykernel/lsp.py), manages the language server -- [`plots`](pythonFiles/positron/positron_ipykernel/plots.py), a custom [IPython display publisher](https://github.com/ipython/ipython/blob/main/IPython/core/displaypub.py) that displays to Positron's Plots pane -- [`data_explorer`](pythonFiles/positron/positron_ipykernel/data_explorer.py), manages Positron's Data Viewer +- [`positron_ipkernel`](python_files/positron/positron_ipykernel/positron_ipkernel.py), the Positron Python Kernel +- [`positron_jedilsp`](python_files/positron/positron_ipykernel/positron_jedilsp.py), the Positron Python Language Server +- [`variables`](python_files/positron/positron_ipykernel/variables.py), manages Positron's Variables pane +- [`ui`](python_files/positron/positron_ipykernel/ui.py), manages Positron's Frontend comm channel (a global channel for communication unscoped to any particular view) +- [`help`](python_files/positron/positron_ipykernel/help.py), manages Positron's Help pane +- [`lsp`](python_files/positron/positron_ipykernel/lsp.py), manages the language server +- [`plots`](python_files/positron/positron_ipykernel/plots.py), a custom [IPython display publisher](https://github.com/ipython/ipython/blob/main/IPython/core/displaypub.py) that displays to Positron's Plots pane +- [`data_explorer`](python_files/positron/positron_ipykernel/data_explorer.py), manages Positron's Data Viewer The various Positron services communicate with the front end via Jupyter's [comms](https://connect.rstudioservices.com/content/59a1f153-dcd8-44ac-849b-3371829b7002/positron-architecture.html#comms-and-ui-bindings) messaging protocol. @@ -30,7 +30,7 @@ The various Positron services communicate with the front end via Jupyter's [comm When editing the Python source, **open a new workspace at the root `positron-python` folder** to use the settings for the various tools (linters, testers, etc) to match the CI workflows. -From the `positron-python/pythonFiles` folder, you can run the following commands. +From the `positron-python/python_files` folder, you can run the following commands. Format source files with [Black](https://github.com/psf/black): @@ -53,5 +53,5 @@ pip install -r ../build/pinned-test-requirements.txt Run Positron's unit tests with [pytest](https://docs.pytest.org/en/8.0.x/): ```sh -pytest pythonFiles/positron/ +pytest python_files/positron/ ``` diff --git a/extensions/positron-python/build/azure-pipeline.pre-release.yml b/extensions/positron-python/build/azure-pipeline.pre-release.yml index bb52f983d02..bee373845e2 100644 --- a/extensions/positron-python/build/azure-pipeline.pre-release.yml +++ b/extensions/positron-python/build/azure-pipeline.pre-release.yml @@ -54,16 +54,16 @@ extends: - script: | python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt - python ./pythonFiles/install_debugpy.py - python ./pythonFiles/download_get_pip.py + python ./python_files/install_debugpy.py + python ./python_files/download_get_pip.py displayName: Install debugpy and get-pip.py - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt + python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/python --implementation py -r ./requirements.txt displayName: Install Python dependencies - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none -r ./pythonFiles/jedilsp_requirements/requirements.txt + python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/jedilsp --implementation py --platform any --abi none -r ./python_files/jedilsp_requirements/requirements.txt displayName: Install Jedi Language Server - script: | @@ -79,3 +79,8 @@ extends: - script: gulp prePublishBundle displayName: Build + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/extensions/positron-python/build/azure-pipeline.stable.yml b/extensions/positron-python/build/azure-pipeline.stable.yml index 02f8bd38cf8..754a820e36e 100644 --- a/extensions/positron-python/build/azure-pipeline.stable.yml +++ b/extensions/positron-python/build/azure-pipeline.stable.yml @@ -49,16 +49,16 @@ extends: - script: | python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt - python ./pythonFiles/install_debugpy.py - python ./pythonFiles/download_get_pip.py + python ./python_files/install_debugpy.py + python ./python_files/download_get_pip.py displayName: Install debugpy and get-pip.py - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt + python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/python --implementation py -r ./requirements.txt displayName: Install Python dependencies - script: | - python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/jedilsp --implementation py --platform any --abi none -r ./pythonFiles/jedilsp_requirements/requirements.txt + python -m pip install --no-deps --require-hashes --only-binary :all: -t ./python_files/lib/jedilsp --implementation py --platform any --abi none -r ./python_files/jedilsp_requirements/requirements.txt displayName: Install Jedi Language Server - script: | @@ -75,15 +75,8 @@ extends: - script: gulp prePublishBundle displayName: Build tsa: - enabled: true - options: - codebaseName: 'devdiv_$(Build.Repository.Name)' + config: + areaPath: 'Visual Studio Code Python Extensions' serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' - instanceUrl: 'https://devdiv.visualstudio.com/defaultcollection' - projectName: 'DevDiv' - areaPath: "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code Python Extensions" - notificationAliases: - - 'stbatt@microsoft.com' - - 'lszomoru@microsoft.com' - - 'brcan@microsoft.com' - - 'kanadig@microsoft.com' + enabled: true + apiScanSoftwareVersion: '2024' diff --git a/extensions/positron-python/build/existingFiles.json b/extensions/positron-python/build/existingFiles.json index 1f5acc727d8..48ab84ff565 100644 --- a/extensions/positron-python/build/existingFiles.json +++ b/extensions/positron-python/build/existingFiles.json @@ -500,7 +500,7 @@ "src/test/providers/shebangCodeLenseProvider.test.ts", "src/test/providers/symbolProvider.unit.test.ts", "src/test/providers/terminal.unit.test.ts", - "src/test/pythonFiles/formatting/dummy.ts", + "src/test/python_files/formatting/dummy.ts", "src/test/refactor/extension.refactor.extract.method.test.ts", "src/test/refactor/extension.refactor.extract.var.test.ts", "src/test/refactor/rename.test.ts", diff --git a/extensions/positron-python/gulpfile.js b/extensions/positron-python/gulpfile.js index 4950eeb0af9..50772926088 100644 --- a/extensions/positron-python/gulpfile.js +++ b/extensions/positron-python/gulpfile.js @@ -262,7 +262,7 @@ gulp.task('installPythonRequirements', async (done) => { 'install', '--no-user', '-t', - './pythonFiles/lib/python', + './python_files/lib/python', '--no-cache-dir', '--implementation', 'py', @@ -298,7 +298,7 @@ gulp.task('installDebugpy', async (done) => { '--no-user', '--upgrade', '-t', - './pythonFiles/lib/temp', + './python_files/lib/temp', '-r', './build/build-install-requirements.txt', ]; @@ -311,8 +311,8 @@ gulp.task('installDebugpy', async (done) => { }); // Install new DEBUGPY with wheels for python - const wheelsArgs = ['./pythonFiles/install_debugpy.py']; - const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; + const wheelsArgs = ['./python_files/install_debugpy.py']; + const wheelsEnv = { PYTHONPATH: './python_files/lib/temp' }; await spawnAsync(pythonCommand, wheelsArgs, wheelsEnv) .then(() => true) .catch((ex) => { @@ -322,8 +322,8 @@ gulp.task('installDebugpy', async (done) => { }); // Download get-pip.py - const getPipArgs = ['./pythonFiles/download_get_pip.py']; - const getPipEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; + const getPipArgs = ['./python_files/download_get_pip.py']; + const getPipEnv = { PYTHONPATH: './python_files/lib/temp' }; await spawnAsync(pythonCommand, getPipArgs, getPipEnv) .then(() => true) .catch((ex) => { @@ -332,7 +332,7 @@ gulp.task('installDebugpy', async (done) => { done(new Error(msg)); }); - rmrf.sync('./pythonFiles/lib/temp'); + rmrf.sync('./python_files/lib/temp'); }); gulp.task('installPythonLibs', gulp.series('installPythonRequirements', 'installDebugpy')); diff --git a/extensions/positron-python/noxfile-positron.py b/extensions/positron-python/noxfile-positron.py new file mode 100644 index 00000000000..02fc1bf09cb --- /dev/null +++ b/extensions/positron-python/noxfile-positron.py @@ -0,0 +1,22 @@ +# +# Copyright (C) 2024 Posit Software, PBC. All rights reserved. +# + + +import nox + + +@nox.session() +@nox.parametrize('pandas', ['1.5.3']) +@nox.parametrize('torch', ['1.12.1']) +def test_minimum_reqs(session, pandas, torch): + session.install("-r", "python_files/positron/pinned-test-requirements.txt") + session.install('--force-reinstall', f'pandas=={pandas}') + session.install('--force-reinstall', f'torch=={torch}') + + if session.posargs: + test_args = session.posargs + else: + test_args = [] + + session.run('pytest', *test_args) diff --git a/extensions/positron-python/noxfile.py b/extensions/positron-python/noxfile.py index b9ebba64544..aa6d0253c66 100644 --- a/extensions/positron-python/noxfile.py +++ b/extensions/positron-python/noxfile.py @@ -9,10 +9,10 @@ @nox.session() def install_python_libs(session: nox.Session): requirements = [ - ("./pythonFiles/lib/python", "./requirements.txt"), + ("./python_files/lib/python", "./requirements.txt"), ( - "./pythonFiles/lib/jedilsp", - "./pythonFiles/jedilsp_requirements/requirements.txt", + "./python_files/lib/jedilsp", + "./python_files/jedilsp_requirements/requirements.txt", ), ] for target, file in requirements: @@ -35,16 +35,16 @@ def install_python_libs(session: nox.Session): # Install debugger session.run( "python", - "./pythonFiles/install_debugpy.py", - env={"PYTHONPATH": "./pythonFiles/lib/temp"}, + "./python_files/install_debugpy.py", + env={"PYTHONPATH": "./python_files/lib/temp"}, ) # Download get-pip script session.run( "python", - "./pythonFiles/download_get_pip.py", - env={"PYTHONPATH": "./pythonFiles/lib/temp"}, + "./python_files/download_get_pip.py", + env={"PYTHONPATH": "./python_files/lib/temp"}, ) - if pathlib.Path("./pythonFiles/lib/temp").exists(): - shutil.rmtree("./pythonFiles/lib/temp") + if pathlib.Path("./python_files/lib/temp").exists(): + shutil.rmtree("./python_files/lib/temp") diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 83845e7920e..a2da4e1a71e 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -23,7 +23,8 @@ "testObserver", "quickPickItemTooltip", "terminalDataWriteEvent", - "terminalExecuteCommandEvent" + "terminalExecuteCommandEvent", + "contribIssueReporter" ], "author": { "name": "Posit Software, PBC", @@ -50,7 +51,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0" + "vscode": "^1.86.0" }, "enableTelemetry": false, "keywords": [ @@ -62,8 +63,6 @@ "categories": [ "Programming Languages", "Debuggers", - "Linters", - "Formatters", "Other", "Data Science", "Machine Learning" @@ -169,16 +168,6 @@ "description": "%walkthrough.pythonWelcome.description%", "when": "false", "steps": [ - { - "id": "python.createPythonFolder", - "title": "%walkthrough.step.python.createPythonFolder.title%", - "description": "%walkthrough.step.python.createPythonFolder.description%", - "media": { - "svg": "resources/walkthrough/open-folder.svg", - "altText": "%walkthrough.step.python.createPythonFile.altText%" - }, - "when": "workspaceFolderCount = 0" - }, { "id": "python.createPythonFile", "title": "%walkthrough.step.python.createPythonFile.title%", @@ -223,14 +212,15 @@ "title": "%walkthrough.step.python.createEnvironment.title2%", "description": "%walkthrough.step.python.createEnvironment.description2%", "media": { - "markdown": "resources/walkthrough/environments-info.md" + "svg": "resources/walkthrough/create-environment.svg", + "altText": "%walkthrough.step.python.createEnvironment.altText%" }, "when": "" }, { - "id": "python.runAndDebug", - "title": "%walkthrough.step.python.runAndDebug.title%", - "description": "%walkthrough.step.python.runAndDebug.description%", + "id": "python.runAndDebug2", + "title": "%walkthrough.step.python.runAndDebug2.title%", + "description": "%walkthrough.step.python.runAndDebug2.description%", "media": { "svg": "resources/walkthrough/rundebug2.svg", "altText": "%walkthrough.step.python.runAndDebug.altText%" @@ -239,7 +229,7 @@ }, { "id": "python.learnMoreWithDS2", - "title": "%walkthrough.step.python.learnMoreWithDS.title%", + "title": "%walkthrough.step.python.learnMoreWithDS2.title%", "description": "%walkthrough.step.python.learnMoreWithDS.description2%", "media": { "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", @@ -475,7 +465,7 @@ "uniqueItems": true }, "python.createEnvironment.contentButton": { - "default": "show", + "default": "hide", "markdownDescription": "%python.createEnvironment.contentButton.description%", "scope": "machine-overridable", "type": "string", @@ -485,7 +475,7 @@ ] }, "python.createEnvironment.trigger": { - "default": "off", + "default": "prompt", "markdownDescription": "%python.createEnvironment.trigger.description%", "scope": "machine-overridable", "type": "string", @@ -1274,6 +1264,11 @@ } ], "menus": { + "issue/reporter": [ + { + "command": "python.reportIssue" + } + ], "commandPalette": [ { "category": "Python", @@ -1618,7 +1613,8 @@ "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "updateBuildNumber": "gulp updateBuildNumber", "verifyBundle": "gulp verifyBundle", - "webpack": "webpack" + "webpack": "webpack", + "positron:testMinimumPythonReqs": "nox --noxfile noxfile-positron.py -- python_files/positron" }, "dependencies": { "@iarna/toml": "^2.2.5", @@ -1651,9 +1647,9 @@ "unicode": "^14.0.0", "untildify": "^4.0.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "^8.2.0", - "vscode-languageclient": "^9.0.1", - "vscode-languageserver-protocol": "^3.17.5", + "vscode-jsonrpc": "8.2.0", + "vscode-languageclient": "9.0.1", + "vscode-languageserver-protocol": "3.17.5", "vscode-tas-client": "^0.1.84", "which": "^2.0.2", "winreg": "^1.2.4", @@ -1737,4 +1733,4 @@ "extensionDependencies": [ "vscode.jupyter-adapter" ] -} \ No newline at end of file +} diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 127c2b08ac4..891d56bced3 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -94,6 +94,7 @@ "walkthrough.step.python.createPythonFile.description": { "message": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", "comment": [ + "{Locked='](command:toSide:workbench.action.files.openFile'}", "{Locked='](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" @@ -131,14 +132,14 @@ "walkthrough.step.python.selectInterpreter.description": { "message": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", "comment": [ - "{Locked='](command:python.setInterpreter'}", - "{Locked='](command:workbench.action.showCommands'}", - "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", - "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + "{Locked='](command:python.setInterpreter'}", + "{Locked='](command:workbench.action.showCommands'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] }, - "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", - "walkthrough.step.python.createEnvironment.title2": "Create or select a Python Environment ", + "walkthrough.step.python.createEnvironment.title": "Create a Python Environment", + "walkthrough.step.python.createEnvironment.title2": "Select a Python environment", "walkthrough.step.python.createEnvironment.description": { "message": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", "comment": [ @@ -148,12 +149,32 @@ "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] }, - "walkthrough.step.python.createEnvironment.description2": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", + "walkthrough.step.python.createEnvironment.description2": { + "message": "[Select a Python interpreter](command:python.setInterpreter) or [create an environment](command:python.createEnvironment) for your Python project.\n[Select Interpreter](command:python.setInterpreter)", + "comment": [ + "{Locked='](command:python.createEnvironment'}", + "{Locked='](command:python.setInterpreter'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", - "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", + "walkthrough.step.python.runAndDebug2.title": "Run and debug", + "walkthrough.step.python.runAndDebug2.description": "Click the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger.", + "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", + "walkthrough.step.python.learnMoreWithDS2.title": "Keep exploring!", "walkthrough.step.python.learnMoreWithDS.description": { - "message": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "message": "🎨 Find all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "comment": [ + "{Locked='](command:workbench.action.showCommands'}", + "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", + "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", + "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" + ] + }, + "walkthrough.step.python.learnMoreWithDS.description2": { + "message": "🎨 Explore all the features the Python extension has to offer by searching for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Quick Start](https://code.visualstudio.com/docs/python/python-quick-start)", "comment": [ "{Locked='](command:workbench.action.showCommands'}", "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", @@ -161,7 +182,6 @@ "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] }, - "walkthrough.step.python.learnMoreWithDS.description2": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", diff --git a/extensions/positron-python/pythonExtensionApi/src/main.ts b/extensions/positron-python/pythonExtensionApi/src/main.ts index 4de554bf5a2..dccbd78f6f0 100644 --- a/extensions/positron-python/pythonExtensionApi/src/main.ts +++ b/extensions/positron-python/pythonExtensionApi/src/main.ts @@ -16,7 +16,7 @@ export interface PythonExtension { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. - * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` * @param host * @param port * @param waitUntilDebuggerAttaches Defaults to `true`. diff --git a/extensions/positron-python/pythonFiles/.vscode/launch.json b/extensions/positron-python/pythonFiles/.vscode/launch.json deleted file mode 100644 index c525016b309..00000000000 --- a/extensions/positron-python/pythonFiles/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Debug unit tests", - "type": "python", - "request": "test", - "console": "integratedTerminal", - "justMyCode": false - } - ] -} diff --git a/extensions/positron-python/pythonFiles/positron/data-science-requirements.txt b/extensions/positron-python/pythonFiles/positron/data-science-requirements.txt deleted file mode 100644 index cad010f85b6..00000000000 --- a/extensions/positron-python/pythonFiles/positron/data-science-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -pandas -polars -matplotlib -numpy -torch -fastapi -sqlalchemy diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/plots.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/plots.py deleted file mode 100644 index e8375e1fa8d..00000000000 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/plots.py +++ /dev/null @@ -1,224 +0,0 @@ -# -# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. -# - -import base64 -import codecs -import io -import logging -import pickle -import uuid -from typing import Dict, List, Optional - -import comm - -from .plot_comm import PlotBackendMessageContent, PlotResult, RenderRequest -from .positron_comm import CommMessage, JsonRpcErrorCode, PositronComm -from .utils import JsonRecord -from .widget import _WIDGET_MIME_TYPE - -logger = logging.getLogger(__name__) - - -# Matplotlib Default Figure Size -DEFAULT_WIDTH_IN = 6.4 -DEFAULT_HEIGHT_IN = 4.8 -BASE_DPI = 100 - - -class PositronDisplayPublisherHook: - def __init__(self, target_name: str): - self.comms: Dict[str, PositronComm] = {} - self.figures: Dict[str, str] = {} - self.target_name = target_name - self.fignums: List[int] = [] - - def __call__(self, msg, *args, **kwargs) -> Optional[dict]: - if msg["msg_type"] == "display_data": - # If there is no image for our display, don't create a - # positron.plot comm and let the parent deal with the msg. - data = msg["content"]["data"] - if _WIDGET_MIME_TYPE in data: - # This is a widget, let the widget hook handle it - return msg - if "image/png" not in data: - return msg - - # Otherwise, try to pickle the current figure so that we - # can restore the context for future renderings. We construct - # a new plot comm to advise the client of the new figure. - pickled = self._pickle_current_figure() - if pickled is not None: - id = str(uuid.uuid4()) - self.figures[id] = pickled - - # Creating a comm per plot figure allows the client - # to request new renderings of each plot at a later time, - # e.g. on resizing the plots view - self._create_comm(id) - - # Returning None implies our hook has processed the message - # and it stops the parent from sending the display_data via - # the standard iopub channel - return None - - return msg - - def _create_comm(self, comm_id: str) -> None: - """ - Create a new plot comm with the given id. - """ - plot_comm = PositronComm(comm.create_comm(target_name=self.target_name, comm_id=comm_id)) - self.comms[comm_id] = plot_comm - plot_comm.on_msg(self.handle_msg, PlotBackendMessageContent) - - def handle_msg(self, msg: CommMessage[PlotBackendMessageContent], raw_msg: JsonRecord) -> None: - """ - Handle client messages to render a plot figure. - """ - comm_id = msg.content.comm_id - request = msg.content.data - - figure_comm = self.comms.get(comm_id, None) - if figure_comm is None: - logger.warning(f"Plot figure comm {comm_id} not found") - return - - if isinstance(request, RenderRequest): - pickled = self.figures.get(comm_id, None) - if pickled is None: - figure_comm.send_error( - code=JsonRpcErrorCode.INVALID_PARAMS, message=f"Figure {comm_id} not found" - ) - return - - width_px = request.params.width or 0 - height_px = request.params.height or 0 - pixel_ratio = request.params.pixel_ratio or 1.0 - - if width_px != 0 and height_px != 0: - format_dict = self._resize_pickled_figure(pickled, width_px, height_px, pixel_ratio) - data = format_dict["image/png"] - output = PlotResult(data=data, mime_type="image/png").dict() - figure_comm.send_result(data=output, metadata={"mime_type": "image/png"}) - - else: - logger.warning(f"Unhandled request: {request}") - - def shutdown(self) -> None: - """ - Shutdown plot comms and release any resources. - """ - for figure_comm in self.comms.values(): - try: - figure_comm.close() - except Exception: - pass - self.comms.clear() - self.figures.clear() - - # -- Private Methods -- - - def _pickle_current_figure(self) -> Optional[str]: - pickled = None - figure = None - - # Delay importing matplotlib until the kernel and shell has been initialized - # otherwise the graphics backend will be reset to the gui - import matplotlib.pyplot as plt - - # We turn off interactive mode before accessing the plot context - was_interactive = plt.isinteractive() - plt.ioff() - - # Check to see if there are any figures left in stack to display - # If not, get the number of figures to display from matplotlib - if len(self.fignums) == 0: - self.fignums = plt.get_fignums() - - # Get the current figure, remove from it from being called next hook - if len(self.fignums) > 0: - figure = plt.figure(self.fignums.pop(0)) - - # Pickle the current figure - if figure is not None and not self._is_figure_empty(figure): - pickled = codecs.encode(pickle.dumps(figure), "base64").decode() - - if was_interactive: - plt.ion() - - return pickled - - def _resize_pickled_figure( - self, - pickled: str, - new_width_px: int = 614, - new_height_px: int = 460, - pixel_ratio: float = 1.0, - formats: list = ["image/png"], - ) -> dict: - # Delay importing matplotlib until the kernel and shell has been - # initialized otherwise the graphics backend will be reset to the gui - import matplotlib.pyplot as plt - - # Turn off interactive mode before, including before unpickling a - # figures (otherwise it will cause and endless loop of plot changes) - was_interactive = plt.isinteractive() - plt.ioff() - - figure = pickle.loads(codecs.decode(pickled.encode(), "base64")) - figure_buffer = io.BytesIO() - - # Adjust the DPI based on pixel_ratio to accommodate high - # resolution displays... - dpi = BASE_DPI * pixel_ratio - figure.set_dpi(dpi) - figure.set_layout_engine("tight") # eliminates whitespace around the figure - - # ... but use base DPI to convert to inch based dimensions. - width_in, height_in = figure.get_size_inches() - new_width_in = new_width_px / BASE_DPI - new_height_in = new_height_px / BASE_DPI - - # Try to determine if the figure had an explicit width or height set. - if width_in == DEFAULT_WIDTH_IN and height_in == DEFAULT_HEIGHT_IN: - # If default values are still set, apply new size, even if this - # resets the aspect ratio - width_in = new_width_in - height_in = new_height_in - else: - # Preserve the existing aspect ratio, constraining the scale - # based on the shorter dimension - if width_in < height_in: - height_in = height_in * (new_width_in / width_in) - width_in = new_width_in - else: - width_in = width_in * (new_height_in / height_in) - height_in = new_height_in - - figure.set_size_inches(width_in, height_in) - - # Render the figure to a buffer - # using format_display_data() crops the figure to smaller than requested size - figure.savefig(figure_buffer, format="png") - figure_buffer.seek(0) - image_data = base64.b64encode(figure_buffer.read()).decode() - - format_dict = {"image/png": image_data} - - plt.close(figure) - - if was_interactive: - plt.ion() - return format_dict - - def _is_figure_empty(self, figure): - children = figure.get_children() - if len(children) < 1: - return True - - for child in children: - if child.get_visible(): - return False - - return True diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_plots.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_plots.py deleted file mode 100644 index 5e100974a2d..00000000000 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_plots.py +++ /dev/null @@ -1,242 +0,0 @@ -# -# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. -# - -import base64 -import codecs -import io -import pickle -from pathlib import Path -from typing import Iterable, cast - -import matplotlib -import matplotlib.pyplot as plt -import pytest -from IPython.core.formatters import DisplayFormatter -from matplotlib.axes import Axes -from matplotlib.figure import Figure -from matplotlib.testing.compare import compare_images -from matplotlib_inline.backend_inline import configure_inline_support -from positron_ipykernel.plots import BASE_DPI, PositronDisplayPublisherHook -from positron_ipykernel.positron_comm import JsonRpcErrorCode - -from .conftest import DummyComm, PositronShell -from .utils import comm_request, json_rpc_error, json_rpc_request - -PLOT_DATA = [1, 2] - - -@pytest.fixture(autouse=True) -def setup_matplotlib(shell: PositronShell) -> Iterable[None]: - # Use IPython's `matplotlib_inline` backend - backend = "module://matplotlib_inline.backend_inline" - matplotlib.use(backend) - - # Enable all IPython mimetype formatters - display_formatter = cast(DisplayFormatter, shell.display_formatter) - active_types = display_formatter.active_types - display_formatter.active_types = display_formatter.format_types - - # Enable matplotlib IPython formatters - configure_inline_support(shell, backend) - - yield - - # Restore the original active formatters - display_formatter.active_types = active_types - - -@pytest.fixture(scope="session") -def images_path() -> Path: - images_path = Path(__file__).parent / "images" - images_path.mkdir(exist_ok=True) - return images_path - - -@pytest.fixture -def hook() -> PositronDisplayPublisherHook: - return PositronDisplayPublisherHook("positron.plot") - - -@pytest.fixture -def figure_comm(hook: PositronDisplayPublisherHook) -> DummyComm: - """ - A comm corresponding to a test figure belonging to the Positron display publisher hook. - """ - # Initialize the hook by calling it on a figure created with the test plot data - plt.plot(PLOT_DATA) - msg = comm_request({"image/png": None}, msg_type="display_data") - hook(msg) - plt.close() - - # Return the comm corresponding to the first figure - id = next(iter(hook.comms)) - figure_comm = cast(DummyComm, hook.comms[id].comm) - - # Clear messages due to the comm_open - figure_comm.messages.clear() - - return figure_comm - - -def test_hook_call_noop_on_non_display_data(hook: PositronDisplayPublisherHook) -> None: - msg = comm_request({"image/png": None}, msg_type="not_display_data") - assert hook(msg) == msg - assert hook.figures == {} - assert hook.comms == {} - - -def test_hook_call_noop_on_no_image_png(hook: PositronDisplayPublisherHook) -> None: - msg = comm_request({}, msg_type="display_data") - assert hook(msg) == msg - assert hook.figures == {} - assert hook.comms == {} - - -def test_hook_call(hook: PositronDisplayPublisherHook, images_path: Path) -> None: - # It returns `None` to indicate that it's consumed the message - plt.plot(PLOT_DATA) - msg = comm_request({"image/png": None}, msg_type="display_data") - assert hook(msg) is None - - # It creates a new figure and comm - assert len(hook.figures) == 1 - id = next(iter(hook.figures)) - assert id in hook.comms - - # Check the comm's properties - comm = hook.comms[id].comm - assert comm.target_name == hook.target_name - assert comm.comm_id == id - - # Check that the figure is a pickled base64-encoded string by decoding it and comparing it - # with a reference figure. - # First, save the hook's figure - fig_encoded = hook.figures[id] - fig: Figure = pickle.loads(codecs.decode(fig_encoded.encode(), "base64")) - actual = images_path / "test-hook-call-actual.png" - fig.savefig(str(actual)) - - # Create the reference figure - fig_ref = cast(Figure, plt.figure()) - fig_axes = cast(Axes, fig_ref.subplots()) - fig_axes.plot(PLOT_DATA) - expected = images_path / "test-hook-call-expected.png" - fig_ref.savefig(str(expected)) - - # Compare actual versus expected figures - err = compare_images(str(actual), str(expected), tol=0) - assert not err - - -def test_hook_handle_msg_noop_on_unknown_method(figure_comm: DummyComm) -> None: - # Handle a message with an invalid msg_type - msg = json_rpc_request("not_render", {}) - figure_comm.handle_msg(msg) - - assert figure_comm.messages == [ - json_rpc_error(JsonRpcErrorCode.METHOD_NOT_FOUND, "Unknown method 'not_render'") - ] - - -def render_request(comm_id: str, width_px: int = 500, height_px: int = 500, pixel_ratio: int = 1): - return json_rpc_request( - "render", - {"width": width_px, "height": height_px, "pixel_ratio": pixel_ratio}, - comm_id=comm_id, - ) - - -def test_hook_render_noop_on_unknown_comm(figure_comm: DummyComm) -> None: - # Handle a valid message but invalid comm_id - msg = render_request("unknown_comm_id") - figure_comm.handle_msg(msg) - - # No messages sent - assert figure_comm.messages == [] - - -def test_hook_render_error_on_unknown_figure( - hook: PositronDisplayPublisherHook, figure_comm: DummyComm -) -> None: - # Clear the hook's figures to simulate a missing figure - hook.figures.clear() - - # Handle a message with a valid msg_type and valid comm_id, but the hook now has a missing figure - msg = render_request(figure_comm.comm_id) - figure_comm.handle_msg(msg) - - # Check that we receive an error reply - assert figure_comm.messages == [ - json_rpc_error(JsonRpcErrorCode.INVALID_PARAMS, f"Figure {figure_comm.comm_id} not found") - ] - - -def _save_base64_image(encoded: str, filename: Path) -> None: - image = codecs.decode(encoded.encode(), "base64") - with open(filename, "wb") as f: - f.write(image) - - -def test_hook_render(figure_comm: DummyComm, images_path: Path) -> None: - # Send a valid render message with a custom width and height - width_px = height_px = 100 - pixel_ratio = 1 - msg = render_request(figure_comm.comm_id, width_px, height_px, pixel_ratio) - figure_comm.handle_msg(msg) - - # Check that the reply is a comm_msg - reply = figure_comm.messages[0] - assert reply["msg_type"] == "comm_msg" - assert reply["buffers"] is None - assert reply["metadata"] == {"mime_type": "image/png"} - - # Check that the reply data is an `image` message - image_msg = reply["data"] - assert image_msg["result"]["mime_type"] == "image/png" - - # Check that the reply data includes the expected base64-encoded resized image - - # Save the reply's image - actual = images_path / "test-hook-render-actual.png" - _save_base64_image(image_msg["result"]["data"], actual) - - # Create the reference figure - dpi = BASE_DPI * pixel_ratio - width_in = width_px / BASE_DPI - height_in = height_px / BASE_DPI - - fig_buffer = io.BytesIO() - fig_ref = cast(Figure, plt.figure()) - fig_axes = cast(Axes, fig_ref.subplots()) - fig_axes.plot([1, 2]) - fig_ref.set_dpi(dpi) - fig_ref.set_size_inches(width_in, height_in) - fig_ref.set_layout_engine("tight") - - # Serialize the reference figure as a base64-encoded image - fig_ref.savefig(fig_buffer, format="png") - fig_buffer.seek(0) - expected = images_path / "test-hook-render-expected.png" - _save_base64_image(base64.b64encode(fig_buffer.read()).decode(), expected) - - # Compare the actual vs expected figures - err = compare_images(str(actual), str(expected), tol=0) - assert not err - - -# It's important that we depend on the figure_comm fixture too, so that the hook is initialized -def test_shutdown(hook: PositronDisplayPublisherHook, figure_comm: DummyComm) -> None: - # Double-check that it still has figures and comms - assert len(hook.figures) == 1 - assert len(hook.comms) == 1 - - # Double-check that the comm is not yet closed - assert not figure_comm._closed - - hook.shutdown() - - # Figures and comms are closed and cleared - assert not hook.figures - assert not hook.comms - assert figure_comm._closed diff --git a/extensions/positron-python/pythonFiles/.env b/extensions/positron-python/python_files/.env similarity index 100% rename from extensions/positron-python/pythonFiles/.env rename to extensions/positron-python/python_files/.env diff --git a/extensions/positron-python/pythonFiles/.vscode/settings.json b/extensions/positron-python/python_files/.vscode/settings.json similarity index 86% rename from extensions/positron-python/pythonFiles/.vscode/settings.json rename to extensions/positron-python/python_files/.vscode/settings.json index 480631710e8..165fbc8b9fc 100644 --- a/extensions/positron-python/pythonFiles/.vscode/settings.json +++ b/extensions/positron-python/python_files/.vscode/settings.json @@ -9,7 +9,8 @@ "editor.rulers": [100], }, "python.testing.pytestArgs": [ - "tests" + "tests", + "positron/positron_ipykernel/tests" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, diff --git a/extensions/positron-python/pythonFiles/Notebooks intro.ipynb b/extensions/positron-python/python_files/Notebooks intro.ipynb similarity index 100% rename from extensions/positron-python/pythonFiles/Notebooks intro.ipynb rename to extensions/positron-python/python_files/Notebooks intro.ipynb diff --git a/extensions/positron-python/pythonFiles/create_conda.py b/extensions/positron-python/python_files/create_conda.py similarity index 100% rename from extensions/positron-python/pythonFiles/create_conda.py rename to extensions/positron-python/python_files/create_conda.py diff --git a/extensions/positron-python/pythonFiles/create_microvenv.py b/extensions/positron-python/python_files/create_microvenv.py similarity index 100% rename from extensions/positron-python/pythonFiles/create_microvenv.py rename to extensions/positron-python/python_files/create_microvenv.py diff --git a/extensions/positron-python/pythonFiles/create_venv.py b/extensions/positron-python/python_files/create_venv.py similarity index 100% rename from extensions/positron-python/pythonFiles/create_venv.py rename to extensions/positron-python/python_files/create_venv.py diff --git a/extensions/positron-python/pythonFiles/deactivate/bash/deactivate b/extensions/positron-python/python_files/deactivate/bash/deactivate old mode 100755 new mode 100644 similarity index 100% rename from extensions/positron-python/pythonFiles/deactivate/bash/deactivate rename to extensions/positron-python/python_files/deactivate/bash/deactivate diff --git a/extensions/positron-python/pythonFiles/deactivate/fish/deactivate b/extensions/positron-python/python_files/deactivate/fish/deactivate old mode 100755 new mode 100644 similarity index 100% rename from extensions/positron-python/pythonFiles/deactivate/fish/deactivate rename to extensions/positron-python/python_files/deactivate/fish/deactivate diff --git a/extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 b/extensions/positron-python/python_files/deactivate/powershell/deactivate.ps1 similarity index 100% rename from extensions/positron-python/pythonFiles/deactivate/powershell/deactivate.ps1 rename to extensions/positron-python/python_files/deactivate/powershell/deactivate.ps1 diff --git a/extensions/positron-python/pythonFiles/deactivate/zsh/deactivate b/extensions/positron-python/python_files/deactivate/zsh/deactivate old mode 100755 new mode 100644 similarity index 100% rename from extensions/positron-python/pythonFiles/deactivate/zsh/deactivate rename to extensions/positron-python/python_files/deactivate/zsh/deactivate diff --git a/extensions/positron-python/pythonFiles/download_get_pip.py b/extensions/positron-python/python_files/download_get_pip.py similarity index 96% rename from extensions/positron-python/pythonFiles/download_get_pip.py rename to extensions/positron-python/python_files/download_get_pip.py index b8238d60f26..d87d1cf032f 100644 --- a/extensions/positron-python/pythonFiles/download_get_pip.py +++ b/extensions/positron-python/python_files/download_get_pip.py @@ -8,7 +8,7 @@ from packaging.version import parse as version_parser EXTENSION_ROOT = pathlib.Path(__file__).parent.parent -GET_PIP_DEST = EXTENSION_ROOT / "pythonFiles" +GET_PIP_DEST = EXTENSION_ROOT / "python_files" PIP_PACKAGE = "pip" PIP_VERSION = "latest" # Can be "latest", or specific version "23.1.2" diff --git a/extensions/positron-python/pythonFiles/get_output_via_markers.py b/extensions/positron-python/python_files/get_output_via_markers.py similarity index 100% rename from extensions/positron-python/pythonFiles/get_output_via_markers.py rename to extensions/positron-python/python_files/get_output_via_markers.py diff --git a/extensions/positron-python/pythonFiles/install_debugpy.py b/extensions/positron-python/python_files/install_debugpy.py similarity index 96% rename from extensions/positron-python/pythonFiles/install_debugpy.py rename to extensions/positron-python/python_files/install_debugpy.py index c217e81fd21..e38ca82230c 100644 --- a/extensions/positron-python/pythonFiles/install_debugpy.py +++ b/extensions/positron-python/python_files/install_debugpy.py @@ -10,7 +10,7 @@ from packaging.version import parse as version_parser EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") +DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "python_files", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) DEBUGGER_VERSION = "1.8.1" # can also be "latest" diff --git a/extensions/positron-python/pythonFiles/installed_check.py b/extensions/positron-python/python_files/installed_check.py similarity index 96% rename from extensions/positron-python/pythonFiles/installed_check.py rename to extensions/positron-python/python_files/installed_check.py index 4a43a8bc8b3..6dafe23b512 100644 --- a/extensions/positron-python/pythonFiles/installed_check.py +++ b/extensions/positron-python/python_files/installed_check.py @@ -11,9 +11,9 @@ LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" sys.path.insert(0, os.fspath(LIB_ROOT)) -import tomli -from importlib_metadata import metadata -from packaging.requirements import Requirement +import tomli # noqa: E402 +from importlib_metadata import metadata # noqa: E402 +from packaging.requirements import Requirement # noqa: E402 DEFAULT_SEVERITY = "3" # 'Hint' try: diff --git a/extensions/positron-python/pythonFiles/interpreterInfo.py b/extensions/positron-python/python_files/interpreterInfo.py similarity index 100% rename from extensions/positron-python/pythonFiles/interpreterInfo.py rename to extensions/positron-python/python_files/interpreterInfo.py diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in b/extensions/positron-python/python_files/jedilsp_requirements/requirements.in similarity index 87% rename from extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in rename to extensions/positron-python/python_files/jedilsp_requirements/requirements.in index 826bcf1e158..b0471a1e765 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.in +++ b/extensions/positron-python/python_files/jedilsp_requirements/requirements.in @@ -2,7 +2,7 @@ # To update requirements.txt, run the following commands. # Use Python 3.8 when creating the environment or using pip-tools # 1) pip install pip-tools -# 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in +# 2) pip-compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in jedi-language-server>=0.34.3 pygls>=0.10.3 diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt b/extensions/positron-python/python_files/jedilsp_requirements/requirements.txt similarity index 92% rename from extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt rename to extensions/positron-python/python_files/jedilsp_requirements/requirements.txt index fdaa547f714..cd8945d8a52 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt +++ b/extensions/positron-python/python_files/jedilsp_requirements/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --generate-hashes pythonFiles/jedilsp_requirements/requirements.in +# pip-compile --generate-hashes 'python_files/jedilsp_requirements/requirements.in' # attrs==23.2.0 \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ @@ -20,7 +20,7 @@ docstring-to-markdown==0.13 \ --hash=sha256:3025c428638ececae920d6d26054546a20335af3504a145327e657e7ad7ce1ce \ --hash=sha256:aa487059d0883e70e54da25c7b230e918d9e4d40f23d6dfaa2b73e4225b2d7dd # via - # -r pythonFiles/jedilsp_requirements/requirements.in + # -r python_files/jedilsp_requirements/requirements.in # jedi-language-server exceptiongroup==1.2.0 \ --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \ @@ -33,7 +33,7 @@ jedi==0.19.1 \ jedi-language-server==0.41.2 \ --hash=sha256:865a93546b3711713eaca1a76b3a5d4aaacd4594c87f777aa8d54da37ae5aeca \ --hash=sha256:bfa112d88c0bd21872522b96c64e8054854f767449bbb3d858be04fb2cda0606 - # via -r pythonFiles/jedilsp_requirements/requirements.in + # via -r python_files/jedilsp_requirements/requirements.in lsprotocol==2023.0.0 \ --hash=sha256:c9d92e12a3f4ed9317d3068226592860aab5357d93cf5b2451dc244eee8f35f2 \ --hash=sha256:e85fc87ee26c816adca9eb497bb3db1a7c79c477a11563626e712eaccf926a05 @@ -43,7 +43,7 @@ lsprotocol==2023.0.0 \ markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb - # via -r pythonFiles/jedilsp_requirements/requirements.in + # via -r python_files/jedilsp_requirements/requirements.in mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -89,17 +89,17 @@ pydantic==1.10.14 \ --hash=sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f \ --hash=sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a \ --hash=sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b - # via -r pythonFiles/jedilsp_requirements/requirements.in + # via -r python_files/jedilsp_requirements/requirements.in pygls==1.2.1 \ --hash=sha256:04f9b9c115b622dcc346fb390289066565343d60245a424eca77cb429b911ed8 \ --hash=sha256:7dcfcf12b6f15beb606afa46de2ed348b65a279c340ef2242a9a35c22eeafe94 # via - # -r pythonFiles/jedilsp_requirements/requirements.in + # -r python_files\jedilsp_requirements\requirements.in # jedi-language-server pygments==2.17.2 \ --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 - # via -r pythonFiles/jedilsp_requirements/requirements.in + # via -r python_files/jedilsp_requirements/requirements.in typing-extensions==4.9.0 \ --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd diff --git a/extensions/positron-python/pythonFiles/linter.py b/extensions/positron-python/python_files/linter.py similarity index 89% rename from extensions/positron-python/pythonFiles/linter.py rename to extensions/positron-python/python_files/linter.py index 58ad9397f58..af9634f83f4 100644 --- a/extensions/positron-python/pythonFiles/linter.py +++ b/extensions/positron-python/python_files/linter.py @@ -37,11 +37,7 @@ def main(): invoke = sys.argv[1] if invoke == "-m": linter = sys.argv[2] - args = ( - [sys.executable, "-m", linter] - + linter_settings[linter]["args"] - + sys.argv[3:] - ) + args = [sys.executable, "-m", linter] + linter_settings[linter]["args"] + sys.argv[3:] else: linter = sys.argv[2] args = [sys.argv[3]] + linter_settings[linter]["args"] + sys.argv[4:] diff --git a/extensions/positron-python/pythonFiles/normalizeSelection.py b/extensions/positron-python/python_files/normalizeSelection.py similarity index 98% rename from extensions/positron-python/pythonFiles/normalizeSelection.py rename to extensions/positron-python/python_files/normalizeSelection.py index f0397b7db3a..523db0727e5 100644 --- a/extensions/positron-python/pythonFiles/normalizeSelection.py +++ b/extensions/positron-python/python_files/normalizeSelection.py @@ -297,11 +297,7 @@ def get_next_block_lineno(which_line_next): data = None which_line_next = 0 - if ( - empty_Highlight - and contents.get("smartSendExperimentEnabled") - and contents.get("smartSendSettingsEnabled") - ): + if empty_Highlight and contents.get("smartSendSettingsEnabled"): result = traverse_file( contents["wholeFileContent"], vscode_start_line, diff --git a/extensions/positron-python/pythonFiles/positron/pinned-test-requirements.txt b/extensions/positron-python/python_files/positron/pinned-test-requirements.txt similarity index 100% rename from extensions/positron-python/pythonFiles/positron/pinned-test-requirements.txt rename to extensions/positron-python/python_files/positron/pinned-test-requirements.txt diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/__init__.py b/extensions/positron-python/python_files/positron/positron_ipykernel/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/__init__.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/__init__.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/_pydoc.css b/extensions/positron-python/python_files/positron/positron_ipykernel/_pydoc.css similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/_pydoc.css rename to extensions/positron-python/python_files/positron/positron_ipykernel/_pydoc.css diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/access_keys.py b/extensions/positron-python/python_files/positron/positron_ipykernel/access_keys.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/access_keys.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/access_keys.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/connections.py b/extensions/positron-python/python_files/positron/positron_ipykernel/connections.py similarity index 99% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/connections.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/connections.py index 8bf793dfd34..c2bae01cd42 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/connections.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/connections.py @@ -184,7 +184,6 @@ def _wrap_connection(self, obj: Any) -> Connection: raise ValueError(f"Unsupported connection type {type_name}") def _close_connection(self, comm_id: str): - try: # calling disconnect can fail if the connection has already been closed or # if it's called from a different thread. @@ -396,7 +395,6 @@ def disconnect(self): self.conn.close() def preview_object(self, path: List[ObjectSchema]): - if pd_ is None: raise ModuleNotFoundError("Pandas is required for previewing SQLite tables.") @@ -429,14 +427,12 @@ class SQLAlchemyConnection(Connection): """ def __init__(self, conn): - self.conn: sqlalchemy.Engine = conn self.display_name = f"SQLAlchemy ({conn.name})" self.host = conn.url self.type = "SQLAlchemy" def list_objects(self, path: List[ObjectSchema]): - if sqlalchemy_ is None: raise ModuleNotFoundError( "SQLAlchemy is required for listing objects in SQLAlchemy connections." @@ -466,7 +462,6 @@ def list_objects(self, path: List[ObjectSchema]): raise ValueError(f"Path length must be at most 1, but got {len(path)}. Path: {path}") def list_fields(self, path: List[ObjectSchema]): - if sqlalchemy_ is None: raise ModuleNotFoundError( "SQLAlchemy is required for listing fields in SQLAlchemy connections." @@ -492,7 +487,6 @@ def list_object_types(self): } def preview_object(self, path: List[ObjectSchema]): - if sqlalchemy_ is None: raise ModuleNotFoundError( "SQLAlchemy is required for previewing objects in SQLAlchemy connections." diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/connections_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/connections_comm.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/connections_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/connections_comm.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py similarity index 90% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py index 29efcad3f68..773c26ca0c5 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer.py @@ -8,7 +8,6 @@ import abc import logging import operator -import uuid from typing import ( TYPE_CHECKING, Callable, @@ -24,6 +23,7 @@ from .access_keys import decode_access_key from .data_explorer_comm import ( + BackendState, ColumnFrequencyTable, ColumnHistogram, ColumnSummaryStats, @@ -41,8 +41,8 @@ GetDataValuesRequest, GetSchemaRequest, GetStateRequest, - GetSupportedFeaturesRequest, RowFilter, + RowFilterCondition, RowFilterType, SchemaUpdateParams, SearchFilterType, @@ -59,10 +59,11 @@ TableData, TableSchema, TableShape, - TableState, ) from .positron_comm import CommMessage, PositronComm from .third_party import pd_ +from .utils import guid + if TYPE_CHECKING: import pandas as pd @@ -87,10 +88,13 @@ class DataExplorerTableView(abc.ABC): def __init__( self, + display_name: str, table, filters: Optional[List[RowFilter]], sort_keys: Optional[List[ColumnSortKey]], ): + self.display_name = display_name + # Note: we must not ever modify the user's data self.table = table @@ -130,7 +134,7 @@ def get_data_values(self, request: GetDataValuesRequest): ).dict() def set_row_filters(self, request: SetRowFiltersRequest): - return self._set_row_filters(request.params.filters) + return self._set_row_filters(request.params.filters).dict() def set_sort_columns(self, request: SetSortColumnsRequest): self.sort_keys = request.params.sort_keys @@ -166,9 +170,6 @@ def get_column_profiles(self, request: GetColumnProfilesRequest): def get_state(self, request: GetStateRequest): return self._get_state().dict() - def get_supported_features(self, request: GetSupportedFeaturesRequest): - return self._get_supported_features().dict() - @abc.abstractmethod def invalidate_computations(self): pass @@ -225,11 +226,7 @@ def _prof_histogram(self, column_index: int) -> ColumnHistogram: pass @abc.abstractmethod - def _get_state(self) -> TableState: - pass - - @abc.abstractmethod - def _get_supported_features(self) -> SupportedFeatures: + def _get_state(self) -> BackendState: pass @@ -276,11 +273,12 @@ class PandasView(DataExplorerTableView): def __init__( self, + display_name: str, table, filters: Optional[List[RowFilter]], sort_keys: Optional[List[ColumnSortKey]], ): - super().__init__(table, filters, sort_keys) + super().__init__(display_name, table, filters, sort_keys) self._dtypes = None @@ -482,7 +480,7 @@ def _update_view_indices(self): # reflect the filtered_indices that have just been updated self._sort_data() - def _set_row_filters(self, filters) -> FilterResult: + def _set_row_filters(self, filters: List[RowFilter]) -> FilterResult: self.filters = filters if len(filters) == 0: @@ -491,20 +489,32 @@ def _set_row_filters(self, filters) -> FilterResult: self._update_view_indices() return FilterResult(selected_num_rows=len(self.table)) - # Evaluate all the filters and AND them together + # Evaluate all the filters and combine them using the + # indicated conditions combined_mask = None for filt in filters: + if filt.is_valid is False: + # If filter is invalid, do not evaluate it + continue + single_mask = self._eval_filter(filt) if combined_mask is None: combined_mask = single_mask - else: + elif filt.condition == RowFilterCondition.And: combined_mask &= single_mask + elif filt.condition == RowFilterCondition.Or: + combined_mask |= single_mask - self.filtered_indices = combined_mask.nonzero()[0] + if combined_mask is None: + self.filtered_indices = None + selected_num_rows = len(self.table) + else: + self.filtered_indices = combined_mask.nonzero()[0] + selected_num_rows = len(self.filtered_indices) # Update the view indices, re-sorting if needed self._update_view_indices() - return FilterResult(selected_num_rows=len(self.filtered_indices)) + return FilterResult(selected_num_rows=selected_num_rows) def _eval_filter(self, filt: RowFilter): col = self.table.iloc[:, filt.column_index] @@ -531,8 +541,12 @@ def _eval_filter(self, filt: RowFilter): op = COMPARE_OPS[params.op] # pandas comparison filters return False for null values mask = op(col, _coerce_value_param(params.value, col.dtype)) + elif filt.filter_type == RowFilterType.IsEmpty: + mask = col.str.len() == 0 elif filt.filter_type == RowFilterType.IsNull: mask = col.isnull() + elif filt.filter_type == RowFilterType.NotEmpty: + mask = col.str.len() != 0 elif filt.filter_type == RowFilterType.NotNull: mask = col.notnull() elif filt.filter_type == RowFilterType.SetMembership: @@ -687,40 +701,46 @@ def _prof_freq_table(self, column_index: int): def _prof_histogram(self, column_index: int): raise NotImplementedError - def _get_state(self) -> TableState: - return TableState( - table_shape=TableShape(num_rows=self.table.shape[0], num_columns=self.table.shape[1]), + _row_filter_features = SetRowFiltersFeatures( + supported=True, + supports_conditions=False, + supported_types=[ + RowFilterType.Between, + RowFilterType.Compare, + RowFilterType.IsNull, + RowFilterType.NotNull, + RowFilterType.NotBetween, + RowFilterType.Search, + RowFilterType.SetMembership, + ], + ) + + _column_profile_features = GetColumnProfilesFeatures( + supported=True, + supported_types=[ + ColumnProfileType.NullCount, + ColumnProfileType.SummaryStats, + ], + ) + + FEATURES = SupportedFeatures( + search_schema=SearchSchemaFeatures(supported=True), + set_row_filters=_row_filter_features, + get_column_profiles=_column_profile_features, + ) + + def _get_state(self) -> BackendState: + if self.view_indices is not None: + num_rows = len(self.view_indices) + else: + num_rows = self.table.shape[0] + + return BackendState( + display_name=self.display_name, + table_shape=TableShape(num_rows=num_rows, num_columns=self.table.shape[1]), row_filters=self.filters, sort_keys=self.sort_keys, - ) - - def _get_supported_features(self) -> SupportedFeatures: - row_filter_features = SetRowFiltersFeatures( - supported=True, - supports_conditions=False, - supported_types=[ - RowFilterType.Between, - RowFilterType.Compare, - RowFilterType.IsNull, - RowFilterType.NotNull, - RowFilterType.NotBetween, - RowFilterType.Search, - RowFilterType.SetMembership, - ], - ) - - column_profile_features = GetColumnProfilesFeatures( - supported=True, - supported_types=[ - ColumnProfileType.NullCount, - ColumnProfileType.SummaryStats, - ], - ) - - return SupportedFeatures( - search_schema=SearchSchemaFeatures(supported=True), - set_row_filters=row_filter_features, - get_column_profiles=column_profile_features, + supported_features=self.FEATURES, ) @@ -748,8 +768,9 @@ class PyArrowView(DataExplorerTableView): pass -def _get_table_view(table, filters=None, sort_keys=None): - return PandasView(table, filters, sort_keys) +def _get_table_view(table, filters=None, sort_keys=None, name=None): + name = name or guid() + return PandasView(name, table, filters, sort_keys) def _value_type_is_supported(value): @@ -814,9 +835,14 @@ def register_table( raise TypeError(type(table)) if comm_id is None: - comm_id = str(uuid.uuid4()) + comm_id = guid() - self.table_views[comm_id] = _get_table_view(table) + if variable_path is not None: + full_title = ", ".join([str(decode_access_key(k)) for k in variable_path]) + else: + full_title = title + + self.table_views[comm_id] = _get_table_view(table, name=full_title) base_comm = comm.create_comm( target_name=self.comm_target, @@ -934,6 +960,8 @@ def _update_explorer_for_comm(self, comm_id: str, path: PathKey, new_variable): comm = self.comms[comm_id] table_view = self.table_views[comm_id] + full_title = ", ".join([str(decode_access_key(k)) for k in path]) + # When detecting namespace assignments or changes, the first # level of the path has already been resolved. If there is a # data explorer open for a nested value, then we need to use @@ -941,7 +969,7 @@ def _update_explorer_for_comm(self, comm_id: str, path: PathKey, new_variable): if len(path) > 1: is_found, new_table = _resolve_value_from_path(new_variable, path[1:]) if not is_found: - raise KeyError(f"Path {', '.join(path)} not found in value") + raise KeyError(f"Path {full_title} not found in value") else: new_table = new_variable @@ -966,7 +994,7 @@ def _fire_schema_update(discard_state=False): # start over. At some point we can return here and # selectively preserve state if we feel it is safe enough # to do so. - self.table_views[comm_id] = _get_table_view(new_table) + self.table_views[comm_id] = _get_table_view(new_table, name=full_title) return _fire_schema_update(discard_state=True) # New value for data explorer is the same. For now, we just @@ -990,12 +1018,13 @@ def _fire_schema_update(discard_state=False): ) = table_view.ui_should_update_schema(new_table) if should_discard_state: - self.table_views[comm_id] = _get_table_view(new_table) + self.table_views[comm_id] = _get_table_view(new_table, name=full_title) else: self.table_views[comm_id] = _get_table_view( new_table, filters=table_view.filters, sort_keys=table_view.sort_keys, + name=full_title, ) if should_update_schema: @@ -1015,4 +1044,13 @@ def handle_msg(self, msg: CommMessage[DataExplorerBackendMessageContent], raw_ms table = self.table_views[comm_id] result = getattr(table, request.method.value)(request) + + # To help remember to convert pydantic types to dicts + if result is not None: + if isinstance(result, list): + for x in result: + assert isinstance(x, dict) + else: + assert isinstance(result, dict) + comm.send_result(result) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer_comm.py similarity index 96% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer_comm.py index 07c143074fa..1cc8132ee97 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/data_explorer_comm.py @@ -42,6 +42,17 @@ class ColumnDisplayType(str, enum.Enum): Unknown = "unknown" +@enum.unique +class RowFilterCondition(str, enum.Enum): + """ + Possible values for Condition in RowFilter + """ + + And = "and" + + Or = "or" + + @enum.unique class RowFilterType(str, enum.Enum): """ @@ -52,10 +63,14 @@ class RowFilterType(str, enum.Enum): Compare = "compare" + IsEmpty = "is_empty" + IsNull = "is_null" NotBetween = "not_between" + NotEmpty = "not_empty" + NotNull = "not_null" Search = "search" @@ -152,11 +167,15 @@ class FilterResult(BaseModel): ) -class TableState(BaseModel): +class BackendState(BaseModel): """ - The current backend table state + The current backend state for the data explorer """ + display_name: str = Field( + description="Variable name or other string to display for tab name in UI", + ) + table_shape: TableShape = Field( description="Provides number of rows and columns in table", ) @@ -169,6 +188,10 @@ class TableState(BaseModel): description="The set of currently applied sorts", ) + supported_features: SupportedFeatures = Field( + description="The features currently supported by the backend instance", + ) + class TableShape(BaseModel): """ @@ -184,24 +207,6 @@ class TableShape(BaseModel): ) -class SupportedFeatures(BaseModel): - """ - For each field, returns flags indicating supported features - """ - - search_schema: SearchSchemaFeatures = Field( - description="Support for 'search_schema' RPC and its features", - ) - - set_row_filters: SetRowFiltersFeatures = Field( - description="Support for 'set_row_filters' RPC and its features", - ) - - get_column_profiles: GetColumnProfilesFeatures = Field( - description="Support for 'get_column_profiles' RPC and its features", - ) - - class ColumnSchema(BaseModel): """ Schema for a column in a table @@ -281,6 +286,15 @@ class RowFilter(BaseModel): description="Column index to apply filter to", ) + condition: RowFilterCondition = Field( + description="The binary condition to use to combine with preceding row filters", + ) + + is_valid: Optional[bool] = Field( + default=None, + description="Whether the filter is valid and supported by the backend, if undefined then true", + ) + between_params: Optional[BetweenFilterParams] = Field( default=None, description="Parameters for the 'between' and 'not_between' filter types", @@ -556,6 +570,24 @@ class ColumnSortKey(BaseModel): ) +class SupportedFeatures(BaseModel): + """ + For each field, returns flags indicating supported features + """ + + search_schema: SearchSchemaFeatures = Field( + description="Support for 'search_schema' RPC and its features", + ) + + set_row_filters: SetRowFiltersFeatures = Field( + description="Support for 'set_row_filters' RPC and its features", + ) + + get_column_profiles: GetColumnProfilesFeatures = Field( + description="Support for 'get_column_profiles' RPC and its features", + ) + + class SearchSchemaFeatures(BaseModel): """ Feature flags for 'search_schema' RPC @@ -625,9 +657,6 @@ class DataExplorerBackendRequest(str, enum.Enum): # Get the state GetState = "get_state" - # Query the backend to determine supported features - GetSupportedFeatures = "get_supported_features" - class GetSchemaParams(BaseModel): """ @@ -840,22 +869,6 @@ class GetStateRequest(BaseModel): ) -class GetSupportedFeaturesRequest(BaseModel): - """ - Query the backend to determine supported features, to enable feature - toggling - """ - - method: Literal[DataExplorerBackendRequest.GetSupportedFeatures] = Field( - description="The JSON-RPC method name (get_supported_features)", - ) - - jsonrpc: str = Field( - default="2.0", - description="The JSON-RPC version specifier", - ) - - class DataExplorerBackendMessageContent(BaseModel): comm_id: str data: Union[ @@ -866,7 +879,6 @@ class DataExplorerBackendMessageContent(BaseModel): SetSortColumnsRequest, GetColumnProfilesRequest, GetStateRequest, - GetSupportedFeaturesRequest, ] = Field(..., discriminator="method") @@ -899,12 +911,10 @@ class SchemaUpdateParams(BaseModel): FilterResult.update_forward_refs() -TableState.update_forward_refs() +BackendState.update_forward_refs() TableShape.update_forward_refs() -SupportedFeatures.update_forward_refs() - ColumnSchema.update_forward_refs() TableSchema.update_forward_refs() @@ -941,6 +951,8 @@ class SchemaUpdateParams(BaseModel): ColumnSortKey.update_forward_refs() +SupportedFeatures.update_forward_refs() + SearchSchemaFeatures.update_forward_refs() SetRowFiltersFeatures.update_forward_refs() @@ -973,6 +985,4 @@ class SchemaUpdateParams(BaseModel): GetStateRequest.update_forward_refs() -GetSupportedFeaturesRequest.update_forward_refs() - SchemaUpdateParams.update_forward_refs() diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/docstrings/__init__.py b/extensions/positron-python/python_files/positron/positron_ipykernel/docstrings/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/docstrings/__init__.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/docstrings/__init__.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/docstrings/epytext.py b/extensions/positron-python/python_files/positron/positron_ipykernel/docstrings/epytext.py similarity index 98% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/docstrings/epytext.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/docstrings/epytext.py index 044d0510966..9cae9a7884e 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/docstrings/epytext.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/docstrings/epytext.py @@ -202,9 +202,9 @@ def combine_sections(self): if name == "Type:": type_sections[section.arg_name] = content.split(f"`{section.arg_name}`: ", 1)[1] elif name == "Rtype:": - unique_sections["Return:"].content = ( - f"({content.rstrip()}) {unique_sections['Return:'].content}" - ) + unique_sections[ + "Return:" + ].content = f"({content.rstrip()}) {unique_sections['Return:'].content}" else: matching_type = type_sections.get(str(section.arg_name)) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/help.py b/extensions/positron-python/python_files/positron/positron_ipykernel/help.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/help.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/help.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/help_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/help_comm.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/help_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/help_comm.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/inspectors.py b/extensions/positron-python/python_files/positron/positron_ipykernel/inspectors.py similarity index 99% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/inspectors.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/inspectors.py index 62ec7d57a27..51a13890871 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/inspectors.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/inspectors.py @@ -232,7 +232,13 @@ def get_length(self) -> int: return len([p for p in dir(self.value) if not (p.startswith("_"))]) def get_child(self, key: str) -> Any: - return getattr(self.value, key) + if isinstance(self.value, property): + pass + try: + return getattr(self.value, key) + except Exception as e: + logger.warning(msg=f"{type(e).__name__}: {e}") + return "Unable to show value." def get_items(self) -> Iterable[Tuple[str, Any]]: for key in dir(self.value): diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/jedi.py b/extensions/positron-python/python_files/positron/positron_ipykernel/jedi.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/jedi.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/jedi.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/lsp.py b/extensions/positron-python/python_files/positron/positron_ipykernel/lsp.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/lsp.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/lsp.py diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/matplotlib_backend.py b/extensions/positron-python/python_files/positron/positron_ipykernel/matplotlib_backend.py new file mode 100644 index 00000000000..b1501dea304 --- /dev/null +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/matplotlib_backend.py @@ -0,0 +1,195 @@ +# +# Copyright (C) 2024 Posit Software, PBC. All rights reserved. +# +""" +The matplotlib backend for Positron. +""" + +from __future__ import annotations + +import hashlib +import io +import logging +from typing import Optional, Union, cast + +import matplotlib +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import FigureManagerBase +from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.figure import Figure + +from .plots import Plot + +logger = logging.getLogger(__name__) + + +class FigureManagerPositron(FigureManagerBase): + """ + Interface for the matplotlib backend to interact with the Positron frontend. + + Parameters: + ----------- + canvas + The canvas for this figure. + num + The figure number. + + Attributes: + ----------- + canvas + The canvas for this figure. + """ + + canvas: FigureCanvasPositron + + def __init__(self, canvas: FigureCanvasPositron, num: Union[int, str]): + from .positron_ipkernel import PositronIPyKernel + + super().__init__(canvas, num) + + self._plot: Optional[Plot] = None + self._plots_service = cast(PositronIPyKernel, PositronIPyKernel.instance()).plots_service + + @property + def is_visible(self) -> bool: + """ + Whether the figure is visible to the frontend. + """ + return self._plot is not None + + def show(self) -> None: + """ + Called by matplotlib when a figure is shown via `plt.show()` or `figure.show()`. + """ + if self._plot is None: + # The frontend should respond with a render request, so there's no need for the explicit + # show call in this case. + self._plot = self._plots_service.create_plot(self.canvas._render, self._handle_close) + else: + self._plot.show() + + def destroy(self) -> None: + """ + Called by matplotlib when a figure is closed via `plt.close()`. + """ + if self._plot is not None: + self._plots_service.close_plot(self._plot) + + def update(self) -> None: + """ + Notify the frontend that the plot needs to be rerendered. + + Called by the canvas when a figure is drawn and its contents have changed. + """ + if self._plot is None: + logger.warning("Cannot update a plot that is not visible") + else: + self._plot.update() + + def _handle_close(self) -> None: + """ + Called by the plots service after the plot is closed in the frontend. + """ + # Notify matplotlib to close the figure (and its manager and canvas). + Gcf.destroy(self) + + +class FigureCanvasPositron(FigureCanvasAgg): + """ + The canvas for a figure in the Positron backend. + + Parameters: + ----------- + figure + The figure to draw on this canvas. + + Attributes: + ----------- + manager + The manager for this canvas. + """ + + manager: FigureManagerPositron + + manager_class = FigureManagerPositron # type: ignore + + def __init__(self, figure: Optional[Figure] = None) -> None: + super().__init__(figure) + + # Track the hash of the canvas contents for change detection. + self._last_hash = "" + + def draw(self, is_rendering=False) -> None: + """ + Draw the canvas; send an update event if the canvas has changed. + + Parameters: + ----------- + is_rendering + Whether the canvas is being rendered, to avoid recursively requesting an update from the + frontend. + """ + logger.debug("Drawing to canvas") + try: + super().draw() + finally: + if self.manager.is_visible and not is_rendering: + current_hash = self._hash_buffer_rgba() + logger.debug(f"Canvas: last hash: {self._last_hash[:6]}") + logger.debug(f"Canvas: current hash: {current_hash[:6]}") + if current_hash == self._last_hash: + logger.debug("Canvas: hash is the same, no need to render") + else: + logger.debug("Canvas: hash changed, requesting a render") + self.manager.update() + + def _render(self, width_px: int, height_px: int, pixel_ratio: float, format: str) -> bytes: + # Set the device pixel ratio to the requested value. + self._set_device_pixel_ratio(pixel_ratio) # type: ignore + + # This must be set before setting the size and can't be passed via print_figure else the + # resulting size won't match the request size. + self.figure.set_layout_engine("tight") + + # Resize the figure to the requested size in pixels. + width_in = width_px * self.device_pixel_ratio / self.figure.dpi + height_in = height_px * self.device_pixel_ratio / self.figure.dpi + self.figure.set_size_inches(width_in, height_in, forward=False) + + # Render the canvas. + figure_buffer = io.BytesIO() + with io.BytesIO() as figure_buffer: + self.print_figure( + figure_buffer, + format=format, + dpi=self.figure.dpi, + ) + rendered = figure_buffer.getvalue() + + # NOTE: For some reason, setting the layout engine earlier then calling print_figure + # requires this redraw before calculating the hash else the next draw() call will + # spuriously detect a change. + self.draw(is_rendering=True) + self._last_hash = self._hash_buffer_rgba() + + return rendered + + def _hash_buffer_rgba(self) -> str: + """Hash the canvas contents for change detection.""" + return hashlib.sha1(self.buffer_rgba()).hexdigest() + + +def enable_positron_matplotlib_backend() -> None: + """ + Enable this backend. + """ + # Enable interactive mode to allow for redraws after each cell execution. + matplotlib.interactive(True) + + # Set the backend. + matplotlib.use("module://positron_ipykernel.matplotlib_backend") + + +# Fulfill the matplotlib backend API. +FigureCanvas = FigureCanvasPositron +FigureManager = FigureManagerPositron diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/plot_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/plot_comm.py similarity index 94% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/plot_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/plot_comm.py index fdeaa8dcd67..6d8e245777c 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/plot_comm.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/plot_comm.py @@ -59,6 +59,10 @@ class RenderParams(BaseModel): description="The pixel ratio of the display device", ) + format: str = Field( + description="The requested plot format", + ) + class RenderRequest(BaseModel): """ @@ -94,6 +98,9 @@ class PlotFrontendEvent(str, enum.Enum): # Notification that a plot has been updated on the backend. Update = "update" + # Show a plot. + Show = "show" + PlotResult.update_forward_refs() diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/plots.py b/extensions/positron-python/python_files/positron/positron_ipykernel/plots.py new file mode 100644 index 00000000000..d967f3db397 --- /dev/null +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/plots.py @@ -0,0 +1,172 @@ +# +# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. +# + +from __future__ import annotations + +import base64 +import logging +import uuid +from typing import Callable, List, Optional, Protocol + +from .plot_comm import PlotBackendMessageContent, PlotFrontendEvent, PlotResult, RenderRequest +from .positron_comm import CommMessage, PositronComm +from .session_mode import SessionMode +from .utils import JsonRecord + +logger = logging.getLogger(__name__) + + +MIME_TYPE = { + "png": "image/png", + "svg": "image/svg+xml", + "pdf": "application/pdf", + "jpeg": "image/jpeg", +} + + +class Plot: + """ + The backend representation of a frontend plot instance. + + Paramaters: + ----------- + comm + The communication channel to the frontend plot instance. + render + A callable that renders the plot. See `plot_comm.RenderRequest` for parameter details. + close_callback + A callback that is called after the plot comm is closed. + """ + + def __init__( + self, + comm: PositronComm, + render: Renderer, + close_callback: Optional[Callable[[], None]] = None, + ) -> None: + self._comm = comm + self._render = render + self._close_callback = close_callback + + self._closed = False + + self._comm.on_msg(self._handle_msg, PlotBackendMessageContent) + + @property + def closed(self) -> bool: + """ + Whether the plot is closed. + """ + return self._closed + + def close(self) -> None: + """ + Close the plot. + """ + if self._closed: + return + + self._closed = True + self._comm.close() + if self._close_callback: + self._close_callback() + + def show(self) -> None: + """ + Show the plot. + """ + self._comm.send_event(PlotFrontendEvent.Show, {}) + + def update(self) -> None: + """ + Notify the frontend that the plot needs to be rerendered. + """ + self._comm.send_event(PlotFrontendEvent.Update, {}) + + def _handle_msg(self, msg: CommMessage[PlotBackendMessageContent], raw_msg: JsonRecord) -> None: + request = msg.content.data + if isinstance(request, RenderRequest): + self._handle_render( + request.params.width, + request.params.height, + request.params.pixel_ratio, + request.params.format, + ) + else: + logger.warning(f"Unhandled request: {request}") + + def _handle_render( + self, + width_px: int, + height_px: int, + pixel_ratio: float, + format: str, + ) -> None: + rendered = self._render(width_px, height_px, pixel_ratio, format) + data = base64.b64encode(rendered).decode() + result = PlotResult(data=data, mime_type=MIME_TYPE[format]).dict() + self._comm.send_result(data=result) + + +class Renderer(Protocol): + """ + A callable that renders a plot. See `plot_comm.RenderRequest` for parameter details. + """ + + def __call__(self, width_px: int, height_px: int, pixel_ratio: float, format: str) -> bytes: ... + + +class PlotsService: + """ + The plots service is responsible for managing `Plot` instances. + + Paramaters: + ----------- + target_name + The name of the target for plot comms, as defined in the frontend. + session_mode + The session mode that the kernel was started in. + """ + + def __init__(self, target_name: str, session_mode: SessionMode): + self._target_name = target_name + self._session_mode = session_mode + + self._plots: List[Plot] = [] + + def create_plot(self, render: Renderer, close_callback: Callable[[], None]) -> Plot: + """ + Create a plot. + + See Also: + --------- + Plot + """ + comm_id = str(uuid.uuid4()) + logger.info(f"Creating plot with comm {comm_id}") + plot_comm = PositronComm.create(self._target_name, comm_id) + plot = Plot(plot_comm, render, close_callback) + self._plots.append(plot) + return plot + + def close_plot(self, plot: Plot) -> None: + """ + Close a plot. + + Parameters: + ----------- + plot + The plot to close. + """ + if plot.closed: + return + plot.close() + self._plots.remove(plot) + + def shutdown(self) -> None: + """ + Shutdown the plots service. + """ + for plot in list(self._plots): + self.close_plot(plot) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_comm.py similarity index 58% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/positron_comm.py index 8cedb73df5f..4c8cfb06370 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_comm.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_comm.py @@ -10,7 +10,14 @@ import comm -from . import connections_comm, data_explorer_comm, help_comm, plot_comm, ui_comm, variables_comm +from . import ( + connections_comm, + data_explorer_comm, + help_comm, + plot_comm, + ui_comm, + variables_comm, +) from ._vendor.pydantic import ValidationError from ._vendor.pydantic.generics import GenericModel from .utils import JsonData, JsonRecord @@ -21,10 +28,23 @@ ## Create an enum of JSON-RPC error codes @enum.unique class JsonRpcErrorCode(enum.IntEnum): + # Documentation below is taken directly from https://www.jsonrpc.org/specification#error_object + # for convenience. + + # Invalid JSON was received by the server. + # An error occurred on the server while parsing the JSON text. PARSE_ERROR = -32700 + + # The JSON sent is not a valid Request object. INVALID_REQUEST = -32600 + + # The method does not exist / is not available. METHOD_NOT_FOUND = -32601 + + # Invalid method parameter(s). INVALID_PARAMS = -32602 + + # Internal JSON-RPC error. INTERNAL_ERROR = -32603 @@ -40,15 +60,70 @@ class JsonRpcErrorCode(enum.IntEnum): class CommMessage(GenericModel, Generic[T_content]): + """ + A generic message received from the frontend-side of a comm. + """ + content: T_content class PositronComm: - """A wrapper around a base IPython comm that provides a JSON-RPC interface""" + """ + A wrapper around a base IPython comm that provides a JSON-RPC interface. + + Paramaters: + ----------- + comm + The wrapped IPython comm. + + Attributes: + ----------- + comm + The wrapped IPython comm. + comm_id + """ def __init__(self, comm: comm.base_comm.BaseComm) -> None: self.comm = comm + @classmethod + def create(cls, target_name: str, comm_id: str) -> PositronComm: + """ + Create a Positron comm. + + Parameters: + ----------- + target_name + The name of the target for the comm, as defined in the frontend. + comm_id + The unique identifier for the comm. + + Returns: + -------- + PositronComm + The new PositronComm instance. + """ + base_comm = comm.create_comm(target_name=target_name, comm_id=comm_id) + return cls(base_comm) + + @property + def comm_id(self) -> str: + """ + The unique identifier of this comm. + """ + return self.comm.comm_id + + def on_close(self, callback: Callable[[JsonRecord], None]): + """ + Register a callback for when the frontend-side version of this comm is closed. + + Paramaters: + ----------- + callback + Called when the comm is closed, with the raw close message. + """ + self.comm.on_close(callback) + def on_msg( self, callback: Callable[[CommMessage[T_content], JsonRecord], None], @@ -57,9 +132,14 @@ def on_msg( """ Register a callback for an RPC request from the frontend. - Will be called with both the parsed `msg: CommMessage` and the original `raw_msg`. - - If the `raw_msg` could not be parsed, a JSON-RPC error will be sent to the frontend. + Parameters: + ----------- + callback + Called when a message is received, with both the parsed message `msg: CommMessage` and + original `raw_msg`. Not called if the `raw_msg` could not be parsed; instead, a JSON-RPC + error will be sent to the frontend. + content_cls + The Pydantic model to parse the message with. """ def handle_msg( @@ -108,7 +188,16 @@ def handle_msg( self.comm.on_msg(handle_msg) def send_result(self, data: JsonData = None, metadata: Optional[JsonRecord] = None) -> None: - """Send a JSON-RPC result to the frontend-side version of this comm""" + """ + Send a JSON-RPC result to the frontend-side version of this comm. + + Parameters: + ----------- + data + The result data to send. + metadata + The metadata to send with the result. + """ result = dict( jsonrpc="2.0", result=data, @@ -120,7 +209,16 @@ def send_result(self, data: JsonData = None, metadata: Optional[JsonRecord] = No ) def send_event(self, name: str, payload: JsonRecord) -> None: - """Send a JSON-RPC notification (event) to the frontend-side version of this comm""" + """ + Send a JSON-RPC notification (event) to the frontend-side version of this comm. + + Parameters: + ----------- + name + The name of the event. + payload + The payload of the event. + """ event = dict( jsonrpc="2.0", method=name, @@ -129,7 +227,16 @@ def send_event(self, name: str, payload: JsonRecord) -> None: self.comm.send(data=event) def send_error(self, code: JsonRpcErrorCode, message: Optional[str] = None) -> None: - """Send a JSON-RPC result to the frontend-side version of this comm""" + """ + Send a JSON-RPC result to the frontend-side version of this comm. + + Parameters: + ----------- + code + The error code to send. + message + The error message to send. + """ error = dict( jsonrpc="2.0", error=dict( @@ -144,5 +251,7 @@ def send_error(self, code: JsonRpcErrorCode, message: Optional[str] = None) -> N ) def close(self) -> None: - """Close the underlying comm.""" + """ + Close the frontend-side version of this comm. + """ self.comm.close() diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_ipkernel.py b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py similarity index 87% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_ipkernel.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py index 8bc9fd8c293..c73e756d108 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_ipkernel.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_ipkernel.py @@ -2,7 +2,8 @@ # Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. # -""" Positron extensions to the iPython Kernel.""" +"""Positron extensions to the iPython Kernel.""" + from __future__ import annotations import enum @@ -10,7 +11,7 @@ import re import warnings from pathlib import Path -from typing import Any, Callable, Container, Dict, List, Optional, Type +from typing import Any, Callable, Container, Dict, List, Optional, Type, cast import traitlets from ipykernel.comm.manager import CommManager @@ -26,7 +27,9 @@ from .data_explorer import DataExplorerService from .help import HelpService, help from .lsp import LSPService -from .plots import PositronDisplayPublisherHook +from .matplotlib_backend import enable_positron_matplotlib_backend +from .plots import PlotsService +from .session_mode import SessionMode from .ui import UiService from .utils import JsonRecord from .variables import VariablesService @@ -137,6 +140,18 @@ class PositronShell(ZMQInteractiveShell): help="Class to use to instantiate the shell inspector", ).tag(config=True) + # Positron-specific attributes: + session_mode: SessionMode = SessionMode.trait() # type: ignore + + def __init__(self, *args, **kwargs): + # Set custom attributes from the parent object. + # It would be better to pass these as explicit arguments, but there's no easy way + # to override the parent to do that. + parent = cast(PositronIPyKernel, kwargs["parent"]) + self.session_mode = parent.session_mode + + super().__init__(*args, **kwargs) + def init_events(self) -> None: super().init_events() @@ -204,6 +219,7 @@ def _handle_post_run_cell(self, info: ExecutionInfo) -> None: After execution, sends an update message to the client to summarize the changes observed to variables in the user's environment. """ + # TODO: Split these to separate callbacks? # Check for changes to the working directory try: self.kernel.ui_service.poll_working_directory() @@ -249,6 +265,11 @@ def _showtraceback(self, etype, evalue: Exception, stb: List[str]): # type: ign """ Enhance tracebacks for the Positron frontend. """ + if self.session_mode == SessionMode.NOTEBOOK: + # Don't modify the traceback in a notebook. The frontend assumes that it's unformatted + # and applies its own formatting. + return super()._showtraceback(etype, evalue, stb) # type: ignore IPython type annotation is wrong + # Remove the first two lines of the traceback, which are the "---" header and the repeated # exception name and "Traceback (most recent call last)". # Also remove the last line of the traceback, which repeats f"{etype}: {evalue}". @@ -304,12 +325,21 @@ class PositronIPyKernel(IPythonKernel): klass=InteractiveShell, ) + # Positron-specific attributes: + session_mode: SessionMode = SessionMode.trait() # type: ignore + def __init__(self, **kwargs) -> None: + # Set custom attributes from the parent object. + # It would be better to pass these as explicit arguments, but there's no easy way + # to override the parent to do that. + parent = cast(PositronIPKernelApp, kwargs["parent"]) + self.session_mode = parent.session_mode + super().__init__(**kwargs) # Create Positron services self.data_explorer_service = DataExplorerService(_CommTarget.DataExplorer) - self.display_pub_hook = PositronDisplayPublisherHook(_CommTarget.Plot) + self.plots_service = PlotsService(_CommTarget.Plot, self.session_mode) self.ui_service = UiService() self.help_service = HelpService() self.lsp_service = LSPService(self) @@ -325,7 +355,6 @@ def __init__(self, **kwargs) -> None: _CommTarget.Variables, self.variables_service.on_comm_open ) # Register display publisher hooks - self.shell.display_pub.register_hook(self.display_pub_hook) self.shell.display_pub.register_hook(self.widget_hook) # Ignore warnings that the user can't do anything about @@ -364,10 +393,10 @@ async def do_shutdown(self, restart: bool) -> JsonRecord: # type: ignore Report # Shutdown Positron services self.data_explorer_service.shutdown() - self.display_pub_hook.shutdown() self.ui_service.shutdown() self.help_service.shutdown() self.lsp_service.shutdown() + self.plots_service.shutdown() self.widget_hook.shutdown() await self.variables_service.shutdown() self.connections_service.shutdown() @@ -379,9 +408,25 @@ async def do_shutdown(self, restart: bool) -> JsonRecord: # type: ignore Report class PositronIPKernelApp(IPKernelApp): + kernel: PositronIPyKernel + # Use the PositronIPyKernel class. kernel_class: Type[PositronIPyKernel] = traitlets.Type(PositronIPyKernel) # type: ignore + # Positron-specific attributes: + session_mode: SessionMode = SessionMode.trait() # type: ignore + + def init_gui_pylab(self): + try: + # Enable the Positron matplotlib backend if we're not in a notebook. + # If we're in a notebook, use IPython's default backend via the super() call below. + if self.session_mode != SessionMode.NOTEBOOK: + enable_positron_matplotlib_backend() + except Exception: + logger.error("Error setting matplotlib backend") + + return super().init_gui_pylab() + # # OSC8 functionality diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_jedilsp.py b/extensions/positron-python/python_files/positron/positron_ipykernel/positron_jedilsp.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/positron_jedilsp.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/positron_jedilsp.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/pydoc.py b/extensions/positron-python/python_files/positron/positron_ipykernel/pydoc.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/pydoc.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/pydoc.py diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/session_mode.py b/extensions/positron-python/python_files/positron/positron_ipykernel/session_mode.py new file mode 100644 index 00000000000..48e1213d426 --- /dev/null +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/session_mode.py @@ -0,0 +1,27 @@ +# +# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. +# + +import enum + +import traitlets + + +class SessionMode(str, enum.Enum): + """ + The mode that the kernel application was started in. + """ + + CONSOLE = "console" + NOTEBOOK = "notebook" + BACKGROUND = "background" + + DEFAULT = CONSOLE + + def __str__(self) -> str: + # Override for better display in argparse help. + return self.value + + @classmethod + def trait(cls) -> traitlets.Enum: + return traitlets.Enum(sorted(cls), help=cls.__doc__) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/__init__.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/__init__.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/conftest.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/conftest.py similarity index 83% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/conftest.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/conftest.py index 8bac479aaec..a3c016f60d9 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/conftest.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/conftest.py @@ -3,13 +3,20 @@ # from typing import Iterable -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock import comm import pytest +from traitlets.config import Config + from positron_ipykernel.connections import ConnectionsService from positron_ipykernel.data_explorer import DataExplorerService -from positron_ipykernel.positron_ipkernel import PositronIPyKernel, PositronShell +from positron_ipykernel.positron_ipkernel import ( + PositronIPKernelApp, + PositronIPyKernel, + PositronShell, +) +from positron_ipykernel.session_mode import SessionMode from positron_ipykernel.variables import VariablesService @@ -26,6 +33,11 @@ def publish_msg(self, msg_type, **msg): # type: ignore ReportIncompatibleMethod msg["msg_type"] = msg_type self.messages.append(msg) + def pop_messages(self): + messages = list(self.messages) + self.messages.clear() + return messages + # Enable autouse so that all comms are created as DummyComms. @pytest.fixture(autouse=True) @@ -43,10 +55,15 @@ def kernel() -> PositronIPyKernel: """ The Positron kernel, configured for testing purposes. """ - # Create a Positron kernel. The kernel calls shell_class.instance() to get the globally - # registered shell instance, and IPython registers a TerminalInteractiveShell instead of a - # PositronShell. This causes a traitlets validation error unless we pass the shell_class explicitly. - kernel = PositronIPyKernel.instance(shell_class=PositronShell) + # Mock the application object. We haven't needed to use it in tests yet, but we do need it to + # pass our custom attributes down to the shell. + app = MagicMock(PositronIPKernelApp) + app.config = Config() # Needed to avoid traitlets errors + + # Positron-specific attributes: + app.session_mode = SessionMode.CONSOLE + + kernel = PositronIPyKernel.instance(parent=app) return kernel diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/data.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/data.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/data.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/data.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_access_keys.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_access_keys.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_access_keys.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_access_keys.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_connections.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_connections.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_connections.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_connections.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py similarity index 91% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py index efff8e6e771..53deba30512 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_data_explorer.py @@ -2,7 +2,6 @@ # Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. # -import uuid from typing import Any, Dict, List, Optional, Type, cast import numpy as np @@ -22,6 +21,7 @@ ) from .conftest import DummyComm, PositronShell from .test_variables import BIG_ARRAY_LENGTH +from ..utils import guid from .utils import json_rpc_notification, json_rpc_request, json_rpc_response TARGET_NAME = "positron.dataExplorer" @@ -30,10 +30,6 @@ # pytest fixtures -def guid(): - return str(uuid.uuid4()) - - def get_new_comm( de_service: DataExplorerService, table: Any, @@ -261,6 +257,7 @@ def test_explorer_variable_updates( dxf = DataExplorerFixture(de_service) new_state = dxf.get_state("x") + assert new_state["display_name"] == "x" assert new_state["table_shape"]["num_rows"] == 5 assert new_state["table_shape"]["num_columns"] == 1 assert new_state["sort_keys"] == [ColumnSortKey(**k) for k in x_sort_keys] @@ -373,9 +370,6 @@ def search_schema(self, table_name, search_term, start_index, max_results): def get_state(self, table_name): return self.do_json_rpc(table_name, "get_state") - def get_supported_features(self, table_name): - return self.do_json_rpc(table_name, "get_supported_features") - def get_data_values(self, table_name, **params): return self.do_json_rpc(table_name, "get_data_values", **params) @@ -395,7 +389,11 @@ def check_filter_case(self, table, filter_set, expected_table): self.register_table(ex_id, expected_table) response = self.set_row_filters(table_id, filters=filter_set) - assert response == FilterResult(selected_num_rows=len(expected_table)) + + ex_num_rows = len(expected_table) + assert response == FilterResult(selected_num_rows=ex_num_rows) + + assert self.get_state(table_id)["table_shape"]["num_rows"] == ex_num_rows self.compare_tables(table_id, ex_id, table.shape) def check_sort_case(self, table, sort_keys, expected_table, filters=None): @@ -440,6 +438,7 @@ def _wrap_json(model: Type[BaseModel], data: JsonRecords): def test_pandas_get_state(dxf: DataExplorerFixture): result = dxf.get_state("simple") + assert result["display_name"] == "simple" assert result["table_shape"]["num_rows"] == 5 assert result["table_shape"]["num_columns"] == 6 @@ -456,9 +455,9 @@ def test_pandas_get_state(dxf: DataExplorerFixture): assert result["row_filters"] == [RowFilter(**f) for f in filters] -def test_pandas_get_supported_features(dxf: DataExplorerFixture): +def test_pandas_supported_features(dxf: DataExplorerFixture): dxf.register_table("example", SIMPLE_PANDAS_DF) - features = dxf.get_supported_features("example") + features = dxf.get_state("example")["supported_features"] search_schema = features["search_schema"] row_filters = features["set_row_filters"] @@ -668,37 +667,59 @@ def test_pandas_get_data_values(dxf: DataExplorerFixture): # ) -def _filter(filter_type, column_index, **kwargs): +def _filter(filter_type, column_index, condition="and", is_valid=None, **kwargs): kwargs.update( { "filter_id": guid(), "filter_type": filter_type, "column_index": column_index, + "condition": condition, + "is_valid": is_valid, } ) return kwargs -def _compare_filter(column_index, op, value): - return _filter("compare", column_index, compare_params={"op": op, "value": value}) +def _compare_filter(column_index, op, value, condition="and", is_valid=None): + return _filter( + "compare", + column_index, + condition=condition, + is_valid=is_valid, + compare_params={"op": op, "value": value}, + ) -def _between_filter(column_index, left_value, right_value, op="between"): +def _between_filter(column_index, left_value, right_value, op="between", condition="and"): return _filter( op, column_index, + condition=condition, between_params={"left_value": left_value, "right_value": right_value}, ) -def _not_between_filter(column_index, left_value, right_value): - return _between_filter(column_index, left_value, right_value, op="not_between") +def _not_between_filter(column_index, left_value, right_value, condition="and"): + return _between_filter( + column_index, + left_value, + right_value, + op="not_between", + condition=condition, + ) -def _search_filter(column_index, term, case_sensitive=False, search_type="contains"): +def _search_filter( + column_index, + term, + case_sensitive=False, + search_type="contains", + condition="and", +): return _filter( "search", column_index, + condition=condition, search_params={ "search_type": search_type, "term": term, @@ -707,10 +728,16 @@ def _search_filter(column_index, term, case_sensitive=False, search_type="contai ) -def _set_member_filter(column_index, values, inclusive=True): +def _set_member_filter( + column_index, + values, + inclusive=True, + condition="and", +): return _filter( "set_membership", column_index, + condition=condition, set_membership_params={"values": values, "inclusive": inclusive}, ) @@ -743,6 +770,26 @@ def test_pandas_filter_between(dxf: DataExplorerFixture): ) +def test_pandas_filter_conditions(dxf: DataExplorerFixture): + # Test AND/OR conditions when filtering + df = SIMPLE_PANDAS_DF + filters = [ + _compare_filter(0, ">=", 3, condition="or"), + _compare_filter(3, "<=", -4.5, condition="or"), + # Delbierately duplicated + _compare_filter(3, "<=", -4.5, condition="or"), + ] + + expected_df = df[(df["a"] >= 3) | (df["d"] <= -4.5)] + dxf.check_filter_case(df, filters, expected_df) + + # Test a single condition with or set + filters = [ + _compare_filter(0, ">=", 3, condition="or"), + ] + dxf.check_filter_case(df, filters, df[df["a"] >= 3]) + + def test_pandas_filter_compare(dxf: DataExplorerFixture): # Just use the 'a' column to smoke test comparison filters on # integers @@ -771,6 +818,40 @@ def test_pandas_filter_compare(dxf: DataExplorerFixture): dxf.compare_tables(table_name, ex_id, df.shape) +def test_pandas_filter_is_valid(dxf: DataExplorerFixture): + # Test AND/OR conditions when filtering + df = SIMPLE_PANDAS_DF + filters = [ + _compare_filter(0, ">=", 3), + _compare_filter(0, "<", 3, is_valid=False), + ] + + expected_df = df[df["a"] >= 3] + dxf.check_filter_case(df, filters, expected_df) + + # No filter is valid + filters = [ + _compare_filter(0, ">=", 3, is_valid=False), + _compare_filter(0, "<", 3, is_valid=False), + ] + + dxf.check_filter_case(df, filters, df) + + +def test_pandas_filter_empty(dxf: DataExplorerFixture): + df = pd.DataFrame( + { + "a": ["foo", "bar", "", "", "", None, "baz", ""], + "b": [b"foo", b"bar", b"", b"", None, b"", b"baz", b""], + } + ) + + dxf.check_filter_case(df, [_filter("is_empty", 0)], df[df["a"].str.len() == 0]) + dxf.check_filter_case(df, [_filter("not_empty", 0)], df[df["a"].str.len() != 0]) + dxf.check_filter_case(df, [_filter("is_empty", 1)], df[df["b"].str.len() == 0]) + dxf.check_filter_case(df, [_filter("not_empty", 1)], df[df["b"].str.len() != 0]) + + def test_pandas_filter_is_null_not_null(dxf: DataExplorerFixture): df = SIMPLE_PANDAS_DF b_is_null = _filter("is_null", 1) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_docstrings_epytext.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_docstrings_epytext.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_docstrings_epytext.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_docstrings_epytext.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_help.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_help.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_help.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_help.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_inspectors.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_inspectors.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_inspectors.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_inspectors.py diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_plots.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_plots.py new file mode 100644 index 00000000000..e0253f8fa2e --- /dev/null +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_plots.py @@ -0,0 +1,238 @@ +# +# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. +# + +import base64 +import io +from pathlib import Path +from typing import Iterable, List, cast + +import matplotlib +import matplotlib.pyplot as plt +import pytest +from PIL import Image + +from positron_ipykernel.matplotlib_backend import enable_positron_matplotlib_backend +from positron_ipykernel.plots import PlotsService +from positron_ipykernel.positron_ipkernel import PositronIPyKernel, _CommTarget + +from .conftest import DummyComm, PositronShell +from .utils import ( + comm_close_message, + comm_open_message, + json_rpc_notification, + json_rpc_request, + json_rpc_response, +) + +# +# Matplotlib backend + shell + plots service integration tests. +# + +TARGET_NAME = "target_name" + + +@pytest.fixture(autouse=True) +def setup_positron_matplotlib_backend() -> None: + enable_positron_matplotlib_backend() + + assert matplotlib.get_backend() == "module://positron_ipykernel.matplotlib_backend" + assert matplotlib.is_interactive() + + +@pytest.fixture(autouse=True) +def import_pyplot(shell: PositronShell) -> None: + # Import pyplot for convenience. + shell.run_cell("import matplotlib.pyplot as plt") + + +@pytest.fixture +def plots_service(kernel: PositronIPyKernel) -> Iterable[PlotsService]: + """ + The Positron plots service. + """ + plots_service = kernel.plots_service + + assert not plots_service._plots + + yield plots_service + + plt.close("all") + + +@pytest.fixture(scope="session") +def images_path() -> Path: + images_path = Path(__file__).parent / "images" + images_path.mkdir(exist_ok=True) + return images_path + + +def test_mpl_dont_create_plot_on_new_figure( + shell: PositronShell, plots_service: PlotsService +) -> None: + # Creating a figure should not yet create a plot with the plots service. + shell.run_cell("plt.figure()") + assert not plots_service._plots + + +def _get_plot_comms(plots_service: PlotsService) -> List[DummyComm]: + return [cast(DummyComm, plot._comm.comm) for plot in plots_service._plots] + + +def _get_single_plot_comm(plots_service: PlotsService) -> DummyComm: + plot_comms = _get_plot_comms(plots_service) + assert len(plot_comms) == 1 + return plot_comms[0] + + +@pytest.mark.parametrize("code", ["plt.figure(); plt.show()", "plt.figure().show()"]) +def test_mpl_send_open_comm_on_plt_show( + code: str, shell: PositronShell, plots_service: PlotsService +) -> None: + # Showing a figure should create a plot with the plots service and open a corresponding comm. + shell.run_cell(code) + plot_comm = _get_single_plot_comm(plots_service) + assert plot_comm.pop_messages() == [comm_open_message(_CommTarget.Plot)] + + +def _create_mpl_plot(shell: PositronShell, plots_service: PlotsService) -> DummyComm: + shell.run_cell("plt.figure().show()") + plot_comm = _get_single_plot_comm(plots_service) + plot_comm.messages.clear() + return plot_comm + + +def test_mpl_send_show_on_successive_plt_show( + shell: PositronShell, plots_service: PlotsService +) -> None: + plot_comm = _create_mpl_plot(shell, plots_service) + + # Show the figure again. + shell.run_cell("plt.show()") + assert plot_comm.pop_messages() == [json_rpc_notification("show", {})] + + # It should also work with Figure.show(). + shell.run_cell("plt.gcf().show()") + assert plot_comm.pop_messages() == [json_rpc_notification("show", {})] + + +def test_mpl_send_update_on_draw(shell: PositronShell, plots_service: PlotsService) -> None: + plot_comm = _create_mpl_plot(shell, plots_service) + + # Drawing to an active plot should trigger an update. + shell.run_cell("plt.plot([1, 2])") + assert plot_comm.pop_messages() == [json_rpc_notification("update", {})] + + +def test_mpl_dont_send_update_on_execution( + shell: PositronShell, plots_service: PlotsService +) -> None: + plot_comm = _create_mpl_plot(shell, plots_service) + + # Executing code that doesn't draw to the active plot should not trigger an update. + shell.run_cell("1") + assert plot_comm.pop_messages() == [] + + +def test_mpl_send_close_comm_on_plt_close( + shell: PositronShell, plots_service: PlotsService +) -> None: + plot_comm = _create_mpl_plot(shell, plots_service) + + # Closing the figure should close the plot and send a comm close message. + shell.run_cell("plt.close()") + assert plot_comm.pop_messages() == [comm_close_message()] + assert not plots_service._plots + + +def test_mpl_multiple_figures(shell: PositronShell, plots_service: PlotsService) -> None: + # Create two figures, and show them both. + shell.run_cell("f1 = plt.figure(); f2 = plt.figure(); plt.show()") + + plot_comms = _get_plot_comms(plots_service) + assert len(plot_comms) == 2 + for plot_comm in plot_comms: + assert plot_comm.pop_messages() == [comm_open_message(_CommTarget.Plot)] + + # Draw to the first figure. + shell.run_cell("plt.figure(f1); plt.plot([1, 2])") + + assert plot_comms[0].pop_messages() == [json_rpc_notification("update", {})] + assert plot_comms[1].pop_messages() == [] + + # Draw to the second figure. + shell.run_cell("plt.figure(f2); plt.plot([1, 2])") + + assert plot_comms[0].pop_messages() == [] + assert plot_comms[1].pop_messages() == [json_rpc_notification("update", {})] + + # Show the first figure. + shell.run_cell("f1.show()") + + assert plot_comms[0].pop_messages() == [json_rpc_notification("show", {})] + assert plot_comms[1].pop_messages() == [] + + # Show the second figure. + shell.run_cell("f2.show()") + + assert plot_comms[0].pop_messages() == [] + assert plot_comms[1].pop_messages() == [json_rpc_notification("show", {})] + + +def test_mpl_render(shell: PositronShell, plots_service: PlotsService, images_path: Path) -> None: + # First show the figure and get the plot comm. + shell.run_cell("plt.plot([1, 2])\nplt.show()") + plot_comm = _get_single_plot_comm(plots_service) + assert plot_comm.pop_messages() == [ + comm_open_message(_CommTarget.Plot), + # NOTE: The update here is unnecessary since when the frontend receives a comm open, it + # responds with a render request. It is probably harmless though since the frontend + # debounces render requests. It happens because all figures are redrawn post cell execution, + # when matplotlib interactive mode is enabled. + json_rpc_notification("update", {}), + ] + + # Send a render request to the plot comm. + width = 400 + height = 300 + pixel_ratio = 2 + format = "png" + msg = json_rpc_request( + "render", + {"width": width, "height": height, "pixel_ratio": pixel_ratio, "format": format}, + comm_id="dummy_comm_id", + ) + plot_comm.handle_msg(msg) + + responses = plot_comm.pop_messages() + assert len(responses) == 1 + response = responses[0] + + # Check that the response includes the expected base64-encoded resized image. + image_bytes = response["data"]["result"].pop("data") + image = Image.open(io.BytesIO(base64.b64decode(image_bytes))) + assert image.format == format.upper() + assert image.size == (width * pixel_ratio, height * pixel_ratio) + # Save it to disk for manual inspection. + image.save(images_path / "test-mpl-render.png") + + # Check the rest of the response. + assert response == json_rpc_response({"mime_type": "image/png"}) + + +def test_mpl_shutdown(shell: PositronShell, plots_service: PlotsService) -> None: + # Create a figure and show it. + shell.run_cell("plt.figure(); plt.figure(); plt.show()") + plot_comms = _get_plot_comms(plots_service) + + # Double-check that it still has plots. + assert len(plots_service._plots) == 2 + + # Double-check that all comms are still open. + assert not any(comm._closed for comm in plot_comms) + + plots_service.shutdown() + + # Plots are closed and cleared. + assert not plots_service._plots + assert all(comm._closed for comm in plot_comms) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_positron_ipkernel.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py similarity index 81% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_positron_ipkernel.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py index 966027c8599..b5b3e89617c 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_positron_ipkernel.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_ipkernel.py @@ -10,7 +10,9 @@ import pandas as pd import pytest from IPython.utils.syspathcontext import prepended_to_syspath + from positron_ipykernel.help import help +from positron_ipykernel.session_mode import SessionMode from positron_ipykernel.utils import alias_home from .conftest import PositronShell @@ -81,7 +83,12 @@ def g(): """ -def test_traceback(shell: PositronShell, tmp_path: Path, mock_displayhook: Mock) -> None: +def test_console_traceback( + shell: PositronShell, tmp_path: Path, mock_displayhook: Mock, monkeypatch +) -> None: + # Ensure that we're in console mode. + monkeypatch.setattr(shell, "session_mode", SessionMode.CONSOLE) + # We follow the approach of IPython's test_ultratb.py, which is to create a temporary module, # prepend its parent directory to sys.path, import it, then run a cell that calls a function # from it. @@ -167,6 +174,44 @@ def test_traceback(shell: PositronShell, tmp_path: Path, mock_displayhook: Mock) ) +def test_notebook_traceback( + shell: PositronShell, tmp_path: Path, mock_displayhook: Mock, monkeypatch +) -> None: + # Ensure that we're in notebook mode. + monkeypatch.setattr(shell, "session_mode", SessionMode.NOTEBOOK) + + # We follow the approach of IPython's test_ultratb.py, which is to create a temporary module, + # prepend its parent directory to sys.path, import it, then run a cell that calls a function + # from it. + + # Create a temporary module. + file = tmp_path / "test_traceback.py" + file.write_text(code) + + # Temporarily add the module to sys.path and call a function from it, which should error. + with prepended_to_syspath(str(tmp_path)): + shell.run_cell("import test_traceback; test_traceback.g()") + + # Check that a single message was sent to the frontend. + call_args_list = mock_displayhook.session.send.call_args_list + assert len(call_args_list) == 1 + + call_args = call_args_list[0] + + # Check that the message was sent over the "error" stream. + assert call_args.args[1] == "error" + + exc_content = call_args.args[2] + + # Check that we haven't removed any frames. + # We don't check the traceback contents in this case since that's tested in IPython. + assert len(exc_content["traceback"]) == 6 + + # Check that we haven't modified any other contents. + assert exc_content["ename"] == "Exception" + assert exc_content["evalue"] == "This is an error!" + + def assert_ansi_string_startswith(actual: str, expected: str) -> None: """ Assert that an ansi-formatted string starts with an expected string, in a way that gets pytest diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_positron_jedilsp.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_jedilsp.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_positron_jedilsp.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_positron_jedilsp.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_pydoc.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_pydoc.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_pydoc.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_pydoc.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_ui.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_ui.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_ui.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_ui.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_variables.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_variables.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_variables.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_variables.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_widget.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_widget.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_widget.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/test_widget.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/utils.py b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/utils.py similarity index 96% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/utils.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/tests/utils.py index c24a8457e58..703db0d6afc 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/utils.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/tests/utils.py @@ -71,6 +71,13 @@ def comm_open_message(target_name: str, data: Optional[JsonRecord] = None) -> Js } +def comm_close_message() -> JsonRecord: + return { + **comm_message(), + "msg_type": "comm_close", + } + + def json_rpc_error(code: int, message: str) -> JsonRecord: return comm_message( { diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/third_party.py b/extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/third_party.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/third_party.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui.py b/extensions/positron-python/python_files/positron/positron_ipykernel/ui.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/ui.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/utils.py b/extensions/positron-python/python_files/positron/positron_ipykernel/utils.py similarity index 98% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/utils.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/utils.py index 710bbe39d26..6cd031f5041 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/utils.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/utils.py @@ -8,10 +8,22 @@ import pprint import sys import types +import uuid from binascii import b2a_base64 from datetime import datetime from pathlib import Path -from typing import Any, Coroutine, Dict, List, Optional, Set, Tuple, TypeVar, Union, cast +from typing import ( + Any, + Coroutine, + Dict, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, + cast, +) JsonData = Union[Dict[str, "JsonData"], List["JsonData"], str, int, float, bool, None] JsonRecord = Dict[str, JsonData] @@ -260,6 +272,10 @@ def alias_home(path: Path) -> Path: return path +def guid(): + return str(uuid.uuid4()) + + def positron_ipykernel_usage(): """ diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/variables.py b/extensions/positron-python/python_files/positron/positron_ipykernel/variables.py similarity index 99% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/variables.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/variables.py index a9d2b514436..503bc5e35a5 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/variables.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/variables.py @@ -15,7 +15,13 @@ from .access_keys import decode_access_key, encode_access_key from .inspectors import get_inspector from .positron_comm import CommMessage, JsonRpcErrorCode, PositronComm -from .utils import JsonData, JsonRecord, cancel_tasks, create_task, get_qualname +from .utils import ( + JsonData, + JsonRecord, + cancel_tasks, + create_task, + get_qualname, +) from .variables_comm import ( ClearRequest, ClipboardFormatFormat, diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/variables_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/variables_comm.py similarity index 100% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/variables_comm.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/variables_comm.py diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/widget.py b/extensions/positron-python/python_files/positron/positron_ipykernel/widget.py similarity index 93% rename from extensions/positron-python/pythonFiles/positron/positron_ipykernel/widget.py rename to extensions/positron-python/python_files/positron/positron_ipykernel/widget.py index f9682e1cde9..4e6879c24b8 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/widget.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/widget.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -_WIDGET_MIME_TYPE = "application/vnd.jupyter.widget-view+json" +WIDGET_MIME_TYPE = "application/vnd.jupyter.widget-view+json" @enum.unique @@ -48,11 +48,11 @@ def __call__(self, msg, *args, **kwargs) -> Optional[dict]: if msg["msg_type"] == "display_data": # If there is no widget, let the parent deal with the msg. data = msg["content"]["data"] - if _WIDGET_MIME_TYPE not in data: + if WIDGET_MIME_TYPE not in data: logger.warning("No widget MIME type found.") return msg - comm_id = data[_WIDGET_MIME_TYPE].get("model_id") + comm_id = data[WIDGET_MIME_TYPE].get("model_id") if comm_id is None: logger.warning("No comm associated with widget.") diff --git a/extensions/positron-python/pythonFiles/positron/positron_language_server.py b/extensions/positron-python/python_files/positron/positron_language_server.py similarity index 91% rename from extensions/positron-python/pythonFiles/positron/positron_language_server.py rename to extensions/positron-python/python_files/positron/positron_language_server.py index 77da0b54b0c..4891d8cd36d 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_language_server.py +++ b/extensions/positron-python/python_files/positron/positron_language_server.py @@ -11,7 +11,7 @@ from positron_ipykernel.positron_ipkernel import PositronIPKernelApp from positron_ipykernel.positron_jedilsp import POSITRON -from traitlets.config import Config +from positron_ipykernel.session_mode import SessionMode logger = logging.getLogger(__name__) @@ -57,9 +57,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--session-mode", help="session mode in which the kernel is to be started", - type=str, - default="console", - choices=["console", "notebook", "background"], + type=SessionMode, + default=SessionMode.DEFAULT, + choices=sorted(SessionMode), ) args = parser.parse_args() args.loglevel = args.loglevel.upper() @@ -108,20 +108,17 @@ def parse_args() -> argparse.Namespace: } } - # Start Positron's IPyKernel as the interpreter for our console. # IPKernelApp expects an empty string if no connection_file is provided. if args.connection_file is None: args.connection_file = "" - config = Config( - IPKernelApp={ - "connection_file": args.connection_file, - "log_level": args.loglevel, - "logging_config": logging_config, - }, + # Start Positron's IPyKernel as the interpreter for our console. + app: PositronIPKernelApp = PositronIPKernelApp.instance( + connection_file=args.connection_file, + log_level=args.loglevel, + logging_config=logging_config, + session_mode=args.session_mode, ) - - app: PositronIPKernelApp = PositronIPKernelApp.instance(config=config) # Initialize with empty argv, otherwise BaseIPythonApplication.initialize reuses our # command-line arguments in unexpected ways (e.g. logfile instructs it to log executed code). app.initialize(argv=[]) diff --git a/extensions/positron-python/pythonFiles/positron/test-requirements.txt b/extensions/positron-python/python_files/positron/test-requirements.txt similarity index 100% rename from extensions/positron-python/pythonFiles/positron/test-requirements.txt rename to extensions/positron-python/python_files/positron/test-requirements.txt diff --git a/extensions/positron-python/pythonFiles/printEnvVariables.py b/extensions/positron-python/python_files/printEnvVariables.py similarity index 100% rename from extensions/positron-python/pythonFiles/printEnvVariables.py rename to extensions/positron-python/python_files/printEnvVariables.py diff --git a/extensions/positron-python/pythonFiles/printEnvVariablesToFile.py b/extensions/positron-python/python_files/printEnvVariablesToFile.py similarity index 100% rename from extensions/positron-python/pythonFiles/printEnvVariablesToFile.py rename to extensions/positron-python/python_files/printEnvVariablesToFile.py diff --git a/extensions/positron-python/pythonFiles/pyproject.toml b/extensions/positron-python/python_files/pyproject.toml similarity index 61% rename from extensions/positron-python/pythonFiles/pyproject.toml rename to extensions/positron-python/python_files/pyproject.toml index 878c4a8b6b6..fe9368dab5e 100644 --- a/extensions/positron-python/pythonFiles/pyproject.toml +++ b/extensions/positron-python/python_files/pyproject.toml @@ -54,35 +54,18 @@ ignore = [ [tool.ruff] line-length = 100 -lint.ignore = ["E402"] exclude = [ - # Ignore testing_tools files same as Pyright way - 'get-pip.py', - 'install_debugpy.py', - 'tensorboard_launcher.py', - 'testlauncher.py', - 'visualstudio_py_testlauncher.py', - 'testing_tools/unittest_discovery.py', - 'testing_tools/adapter/util.py', - 'testing_tools/adapter/pytest/_discovery.py', - 'testing_tools/adapter/pytest/_pytest_item.py', - 'tests/debug_adapter/test_install_debugpy.py', - 'tests/testing_tools/adapter/.data', - 'tests/testing_tools/adapter/test___main__.py', - 'tests/testing_tools/adapter/test_discovery.py', - 'tests/testing_tools/adapter/test_functional.py', - 'tests/testing_tools/adapter/test_report.py', - 'tests/testing_tools/adapter/test_util.py', - 'tests/testing_tools/adapter/pytest/test_cli.py', - 'tests/testing_tools/adapter/pytest/test_discovery.py', - 'pythonFiles/testing_tools/*', - 'pythonFiles/testing_tools/adapter/pytest/__init__.py', - 'pythonFiles/tests/pytestadapter/expected_execution_test_output.py', - 'pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py', - 'pythonFiles/tests/unittestadapter/test_utils.py', + "tests/testing_tools/adapter/.data", + "tests/unittestadapter/.data", # --- Start Positron --- # Ignore vendored dependencies 'lib/', 'positron/positron_ipykernel/_vendor/', # --- End Positron --- ] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/extensions/positron-python/pythonFiles/pythonrc.py b/extensions/positron-python/python_files/pythonrc.py similarity index 95% rename from extensions/positron-python/pythonFiles/pythonrc.py rename to extensions/positron-python/python_files/pythonrc.py index 374888ddada..2edd8887467 100644 --- a/extensions/positron-python/pythonFiles/pythonrc.py +++ b/extensions/positron-python/python_files/pythonrc.py @@ -54,13 +54,13 @@ def __str__(self): result = "" # For non-windows allow recent_command history. if sys.platform != "win32": - result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}{command_line}".format( + result = "{command_line}{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( + command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", command_finished="\x1b]633;D;" + str(exit_code) + "\x07", prompt_started="\x1b]633;A\x07", prompt=original_ps1, command_start="\x1b]633;B\x07", command_executed="\x1b]633;C\x07", - command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", ) else: result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( diff --git a/extensions/positron-python/pythonFiles/run-jedi-language-server.py b/extensions/positron-python/python_files/run-jedi-language-server.py similarity index 59% rename from extensions/positron-python/pythonFiles/run-jedi-language-server.py rename to extensions/positron-python/python_files/run-jedi-language-server.py index 31095121409..5a972799bc3 100644 --- a/extensions/positron-python/pythonFiles/run-jedi-language-server.py +++ b/extensions/positron-python/python_files/run-jedi-language-server.py @@ -1,11 +1,11 @@ -import sys import os +import sys # Add the lib path to our sys path so jedi_language_server can find its references EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "jedilsp")) +sys.path.insert(0, os.path.join(EXTENSION_ROOT, "python_files", "lib", "jedilsp")) -from jedi_language_server.cli import cli +from jedi_language_server.cli import cli # noqa: E402 sys.exit(cli()) diff --git a/extensions/positron-python/pythonFiles/shell_exec.py b/extensions/positron-python/python_files/shell_exec.py similarity index 100% rename from extensions/positron-python/pythonFiles/shell_exec.py rename to extensions/positron-python/python_files/shell_exec.py diff --git a/extensions/positron-python/pythonFiles/tensorboard_launcher.py b/extensions/positron-python/python_files/tensorboard_launcher.py similarity index 100% rename from extensions/positron-python/pythonFiles/tensorboard_launcher.py rename to extensions/positron-python/python_files/tensorboard_launcher.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/__init__.py b/extensions/positron-python/python_files/testing_tools/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/__init__.py rename to extensions/positron-python/python_files/testing_tools/__init__.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/__init__.py b/extensions/positron-python/python_files/testing_tools/adapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/__init__.py rename to extensions/positron-python/python_files/testing_tools/adapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/__main__.py b/extensions/positron-python/python_files/testing_tools/adapter/__main__.py similarity index 95% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/__main__.py rename to extensions/positron-python/python_files/testing_tools/adapter/__main__.py index 218456897df..cc7084eb943 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/__main__.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/__main__.py @@ -50,9 +50,7 @@ def parse_args( subsub = add_subparser(cmdname, toolname, subsubs) if cmdname == "discover": subsub.add_argument("--simple", action="store_true") - subsub.add_argument( - "--no-hide-stdio", dest="hidestdio", action="store_false" - ) + subsub.add_argument("--no-hide-stdio", dest="hidestdio", action="store_false") subsub.add_argument("--pretty", action="store_true") # Parse the args! diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/discovery.py b/extensions/positron-python/python_files/testing_tools/adapter/discovery.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/discovery.py rename to extensions/positron-python/python_files/testing_tools/adapter/discovery.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/errors.py b/extensions/positron-python/python_files/testing_tools/adapter/errors.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/errors.py rename to extensions/positron-python/python_files/testing_tools/adapter/errors.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py b/extensions/positron-python/python_files/testing_tools/adapter/info.py similarity index 97% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/info.py rename to extensions/positron-python/python_files/testing_tools/adapter/info.py index d518a29dd97..8e5d0442ce1 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/info.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/info.py @@ -61,9 +61,7 @@ def __init__(self, *args, **kwargs): raise TypeError("missing relpath") -class SingleTestInfo( - namedtuple("TestInfo", "id name path source markers parentid kind") -): +class SingleTestInfo(namedtuple("TestInfo", "id name path source markers parentid kind")): """Info for a single test.""" MARKERS = ("skip", "skip-if", "expected-failure") diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/__init__.py b/extensions/positron-python/python_files/testing_tools/adapter/pytest/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/__init__.py rename to extensions/positron-python/python_files/testing_tools/adapter/pytest/__init__.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_cli.py b/extensions/positron-python/python_files/testing_tools/adapter/pytest/_cli.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_cli.py rename to extensions/positron-python/python_files/testing_tools/adapter/pytest/_cli.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/extensions/positron-python/python_files/testing_tools/adapter/pytest/_discovery.py similarity index 99% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py rename to extensions/positron-python/python_files/testing_tools/adapter/pytest/_discovery.py index 4b852ecf81c..bbe5ae9856c 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/pytest/_discovery.py @@ -17,7 +17,7 @@ def discover( # *, _pytest_main=pytest.main, _plugin=None, - **_ignored + **_ignored, ): """Return the results of test discovery.""" if _plugin is None: diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py b/extensions/positron-python/python_files/testing_tools/adapter/pytest/_pytest_item.py similarity index 99% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py rename to extensions/positron-python/python_files/testing_tools/adapter/pytest/_pytest_item.py index ccfe1412231..724b71a1ac4 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/pytest/_pytest_item.py @@ -170,9 +170,7 @@ def parse_item( parents = [(parentid, item.originalname, kind)] + parents name = parameterized[1:-1] or "" else: - (nodeid, parents, fileid, testfunc, parameterized) = _parse_node_id( - item.nodeid, kind - ) + (nodeid, parents, fileid, testfunc, parameterized) = _parse_node_id(item.nodeid, kind) name = item.name # Note: testfunc does not necessarily match item.function.__name__. diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/report.py b/extensions/positron-python/python_files/testing_tools/adapter/report.py similarity index 99% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/report.py rename to extensions/positron-python/python_files/testing_tools/adapter/report.py index bacdef7b9a0..1ad02fe7bcd 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/report.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/report.py @@ -13,7 +13,7 @@ def report_discovered( pretty=False, simple=False, _send=print, - **_ignored + **_ignored, ): """Serialize the discovered tests and write to stdout.""" if simple: diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/util.py b/extensions/positron-python/python_files/testing_tools/adapter/util.py similarity index 94% rename from extensions/positron-python/pythonFiles/testing_tools/adapter/util.py rename to extensions/positron-python/python_files/testing_tools/adapter/util.py index c7a178311b8..9f3089fb29d 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/util.py +++ b/extensions/positron-python/python_files/testing_tools/adapter/util.py @@ -46,12 +46,6 @@ def group_attr_names(attrnames): return grouped -if sys.version_info < (3,): - _str_to_lower = lambda val: val.decode().lower() -else: - _str_to_lower = str.lower - - ############################# # file paths @@ -134,7 +128,7 @@ def fix_fileid( normalize=False, strictpathsep=None, _pathsep=PATH_SEP, - **kwargs + **kwargs, ): """Return a pathsep-separated file ID ("./"-prefixed) for the given value. @@ -156,7 +150,7 @@ def fix_fileid( rootdir, _pathsep=_pathsep, # ... - **kwargs + **kwargs, ) if relpath: # Note that we treat "" here as an absolute path. _fileid = "./" + relpath @@ -164,7 +158,7 @@ def fix_fileid( if normalize: if strictpathsep: raise ValueError("cannot normalize *and* keep strict path separator") - _fileid = _str_to_lower(_fileid) + _fileid = _fileid.lower() elif strictpathsep: # We do not use _normcase since we want to preserve capitalization. _fileid = _fileid.replace("/", _pathsep) @@ -224,12 +218,6 @@ def _replace_stderr(target): sys.stderr = orig -if sys.version_info < (3,): - _coerce_unicode = lambda s: unicode(s) -else: - _coerce_unicode = lambda s: s - - @contextlib.contextmanager def _temp_io(): sio = StringIO() @@ -239,7 +227,7 @@ def _temp_io(): finally: tmp.seek(0) buff = tmp.read() - sio.write(_coerce_unicode(buff)) + sio.write(buff) @contextlib.contextmanager diff --git a/extensions/positron-python/pythonFiles/testing_tools/process_json_util.py b/extensions/positron-python/python_files/testing_tools/process_json_util.py similarity index 100% rename from extensions/positron-python/pythonFiles/testing_tools/process_json_util.py rename to extensions/positron-python/python_files/testing_tools/process_json_util.py diff --git a/extensions/positron-python/pythonFiles/testing_tools/run_adapter.py b/extensions/positron-python/python_files/testing_tools/run_adapter.py similarity index 81% rename from extensions/positron-python/pythonFiles/testing_tools/run_adapter.py rename to extensions/positron-python/python_files/testing_tools/run_adapter.py index 1eeef194f8f..8af4e49dd31 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/run_adapter.py +++ b/extensions/positron-python/python_files/testing_tools/run_adapter.py @@ -7,8 +7,8 @@ sys.path.insert( 1, - os.path.dirname( # pythonFiles - os.path.dirname( # pythonFiles/testing_tools + os.path.dirname( # python_files + os.path.dirname( # python_files/testing_tools os.path.abspath(__file__) # this file ) ), diff --git a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py b/extensions/positron-python/python_files/testing_tools/socket_manager.py similarity index 90% rename from extensions/positron-python/pythonFiles/testing_tools/socket_manager.py rename to extensions/positron-python/python_files/testing_tools/socket_manager.py index b2afbf0e5a1..3392a4d54e0 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/socket_manager.py +++ b/extensions/positron-python/python_files/testing_tools/socket_manager.py @@ -29,9 +29,7 @@ def __exit__(self, *_): self.close() def connect(self): - self.socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP - ) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) if sys.platform == "win32": addr_use = socket.SO_EXCLUSIVEADDRUSE else: diff --git a/extensions/positron-python/pythonFiles/testing_tools/unittest_discovery.py b/extensions/positron-python/python_files/testing_tools/unittest_discovery.py similarity index 90% rename from extensions/positron-python/pythonFiles/testing_tools/unittest_discovery.py rename to extensions/positron-python/python_files/testing_tools/unittest_discovery.py index 2988092c387..5d5e9bcc660 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/unittest_discovery.py +++ b/extensions/positron-python/python_files/testing_tools/unittest_discovery.py @@ -1,3 +1,4 @@ +import contextlib import inspect import os import sys @@ -13,13 +14,13 @@ def get_sourceline(obj): try: s, n = inspect.getsourcelines(obj) - except: + except Exception: try: # this handles `tornado` case we need a better # way to get to the wrapped function. - # This is a temporary solution + # XXX This is a temporary solution s, n = inspect.getsourcelines(obj.orig_method) - except: + except Exception: return "*" for i, v in enumerate(s): @@ -50,16 +51,14 @@ def generate_test_cases(suite): loader_errors.append(s._exception) else: print(testId.replace(".", ":") + ":" + get_sourceline(tm)) -except: +except Exception: print("=== exception start ===") traceback.print_exc() print("=== exception end ===") for error in loader_errors: - try: + with contextlib.suppress(Exception): print("=== exception start ===") print(error.msg) print("=== exception end ===") - except: - pass diff --git a/extensions/positron-python/pythonFiles/testlauncher.py b/extensions/positron-python/python_files/testlauncher.py similarity index 100% rename from extensions/positron-python/pythonFiles/testlauncher.py rename to extensions/positron-python/python_files/testlauncher.py diff --git a/extensions/positron-python/pythonFiles/tests/__init__.py b/extensions/positron-python/python_files/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/__init__.py rename to extensions/positron-python/python_files/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/__main__.py b/extensions/positron-python/python_files/tests/__main__.py similarity index 92% rename from extensions/positron-python/pythonFiles/tests/__main__.py rename to extensions/positron-python/python_files/tests/__main__.py index 901385d41d8..347222bd85d 100644 --- a/extensions/positron-python/pythonFiles/tests/__main__.py +++ b/extensions/positron-python/python_files/tests/__main__.py @@ -12,9 +12,7 @@ def parse_args(): parser = argparse.ArgumentParser() # To mark a test as functional: (decorator) @pytest.mark.functional - parser.add_argument( - "--functional", dest="markers", action="append_const", const="functional" - ) + parser.add_argument("--functional", dest="markers", action="append_const", const="functional") parser.add_argument( "--no-functional", dest="markers", action="append_const", const="not functional" ) diff --git a/extensions/positron-python/pythonFiles/tests/debug_adapter/__init__.py b/extensions/positron-python/python_files/tests/debug_adapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/debug_adapter/__init__.py rename to extensions/positron-python/python_files/tests/debug_adapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py b/extensions/positron-python/python_files/tests/debug_adapter/test_install_debugpy.py similarity index 64% rename from extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py rename to extensions/positron-python/python_files/tests/debug_adapter/test_install_debugpy.py index 8e2ed33a1da..f72e1089aaa 100644 --- a/extensions/positron-python/pythonFiles/tests/debug_adapter/test_install_debugpy.py +++ b/extensions/positron-python/python_files/tests/debug_adapter/test_install_debugpy.py @@ -1,7 +1,4 @@ import os -import pytest -import subprocess -import sys def _check_binaries(dir_path): @@ -21,12 +18,8 @@ def test_install_debugpy(tmpdir): import install_debugpy install_debugpy.main(str(tmpdir)) - dir_path = os.path.join( - str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_bundle" - ) + dir_path = os.path.join(str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_bundle") _check_binaries(dir_path) - dir_path = os.path.join( - str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_frame_eval" - ) + dir_path = os.path.join(str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_frame_eval") _check_binaries(dir_path) diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/empty_discovery.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/empty_discovery.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/empty_discovery.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/empty_discovery.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_parametrize_discovery.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt b/extensions/positron-python/python_files/tests/pytestadapter/.data/error_pytest_import.txt similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt rename to extensions/positron-python/python_files/tests/pytestadapter/.data/error_pytest_import.txt diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/error_raise_exception.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/error_raise_exception.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt b/extensions/positron-python/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/error_syntax_discovery.txt rename to extensions/positron-python/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/parametrize_tests.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/parametrize_tests.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/pytest.ini similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini rename to extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/pytest.ini diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/test_a.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/test_a.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/test_b.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/root/tests/test_b.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/simple_pytest.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/simple_pytest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/simple_pytest.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/simple_pytest.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/skip_tests.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/skip_tests.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/skip_tests.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/skip_tests.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_env_vars.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/test_env_vars.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_env_vars.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/test_env_vars.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_logging.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/test_logging.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_logging.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/test_logging.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/test_multi_class_nest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/test_multi_class_nest.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/text_docstring.txt b/extensions/positron-python/python_files/tests/pytestadapter/.data/text_docstring.txt similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/text_docstring.txt rename to extensions/positron-python/python_files/tests/pytestadapter/.data/text_docstring.txt diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_pytest_same_file.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py b/extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py rename to extensions/positron-python/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/__init__.py b/extensions/positron-python/python_files/tests/pytestadapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/__init__.py rename to extensions/positron-python/python_files/tests/pytestadapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/extensions/positron-python/python_files/tests/pytestadapter/expected_discovery_test_output.py similarity index 97% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py rename to extensions/positron-python/python_files/tests/pytestadapter/expected_discovery_test_output.py index 7fbb0c5c43e..ba42fcc68fe 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -322,19 +322,12 @@ # └── test_bottom_function_t # └── test_bottom_function_f dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" -test_top_folder_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) +test_top_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -test_nested_folder_one_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" -) +test_nested_folder_one_path = TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" test_bottom_folder_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) @@ -851,12 +844,8 @@ os.path.join(tests_path, "test_a.py"), ), "type_": "test", - "id_": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), - "runID": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), + "id_": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), + "runID": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), } ], }, @@ -874,12 +863,8 @@ os.path.join(tests_path, "test_b.py"), ), "type_": "test", - "id_": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), - "runID": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), + "id_": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), + "runID": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), } ], }, @@ -996,12 +981,8 @@ } SYMLINK_FOLDER_PATH = TEST_DATA_PATH / "symlink_folder" SYMLINK_FOLDER_PATH_TESTS = TEST_DATA_PATH / "symlink_folder" / "tests" -SYMLINK_FOLDER_PATH_TESTS_TEST_A = ( - TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" -) -SYMLINK_FOLDER_PATH_TESTS_TEST_B = ( - TEST_DATA_PATH / "symlink_folder" / "tests" / "test_b.py" -) +SYMLINK_FOLDER_PATH_TESTS_TEST_A = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +SYMLINK_FOLDER_PATH_TESTS_TEST_B = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_b.py" symlink_expected_discovery_output = { "name": "symlink_folder", diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/extensions/positron-python/python_files/tests/pytestadapter/expected_execution_test_output.py similarity index 86% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py rename to extensions/positron-python/python_files/tests/pytestadapter/expected_execution_test_output.py index db4e493c3da..34693d36b12 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/extensions/positron-python/python_files/tests/pytestadapter/expected_execution_test_output.py @@ -20,9 +20,7 @@ test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path ), @@ -31,9 +29,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path ), @@ -80,9 +76,7 @@ test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" uf_single_file_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path ), @@ -91,9 +85,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path ), @@ -111,9 +103,7 @@ # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path ), @@ -149,9 +139,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { "test": get_absolute_test_id( f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path ), @@ -252,35 +240,27 @@ "subtest": None, }, get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_another_thing", skip_tests_path - ), + "test": get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing", skip_tests_path - ), + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing_2", skip_tests_path - ), + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_a", skip_tests_path - ): { + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_a", skip_tests_path): { "test": get_absolute_test_id( "skip_tests.py::TestClass::test_class_function_a", skip_tests_path ), @@ -289,9 +269,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_b", skip_tests_path - ): { + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_b", skip_tests_path): { "test": get_absolute_test_id( "skip_tests.py::TestClass::test_class_function_b", skip_tests_path ), @@ -316,10 +294,7 @@ TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) dual_level_nested_folder_execution_expected_output = { get_absolute_test_id( @@ -379,9 +354,7 @@ # └── test_nest.py # └── test_function: success -nested_folder_path = ( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) +nested_folder_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" double_nested_folder_expected_execution_output = { get_absolute_test_id( "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path @@ -403,9 +376,7 @@ parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { + get_absolute_test_id("parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path): { "test": get_absolute_test_id( "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path ), @@ -414,9 +385,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path - ): { + get_absolute_test_id("parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path): { "test": get_absolute_test_id( "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path ), @@ -425,9 +394,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path - ): { + get_absolute_test_id("parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path): { "test": get_absolute_test_id( "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path ), @@ -442,9 +409,7 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { + get_absolute_test_id("parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path): { "test": get_absolute_test_id( "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path ), @@ -461,9 +426,7 @@ doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { - "test": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", doc_test_path - ), + "test": get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path), "outcome": "success", "message": None, "traceback": None, @@ -477,10 +440,7 @@ TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" @@ -494,26 +454,20 @@ "subtest": None, }, get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_t", dual_level_nested_folder_top_path - ), + "test": get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_f", dual_level_nested_folder_top_path - ), + "test": get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_t", dual_level_nested_folder_bottom_path - ): { + get_absolute_test_id("test_bottom_function_t", dual_level_nested_folder_bottom_path): { "test": get_absolute_test_id( "test_bottom_function_t", dual_level_nested_folder_bottom_path ), @@ -522,9 +476,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_f", dual_level_nested_folder_bottom_path - ): { + get_absolute_test_id("test_bottom_function_f", dual_level_nested_folder_bottom_path): { "test": get_absolute_test_id( "test_bottom_function_f", dual_level_nested_folder_bottom_path ), @@ -533,9 +485,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path - ): { + get_absolute_test_id("TestAddFunction::test_add_negative_numbers", unittest_folder_add_path): { "test": get_absolute_test_id( "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path ), @@ -544,9 +494,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path - ): { + get_absolute_test_id("TestAddFunction::test_add_positive_numbers", unittest_folder_add_path): { "test": get_absolute_test_id( "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path ), @@ -606,18 +554,14 @@ logging_test_expected_execution_output = { get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { - "test": get_absolute_test_id( - "test_logging.py::test_logging2", test_logging_path - ), + "test": get_absolute_test_id("test_logging.py::test_logging2", test_logging_path), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { - "test": get_absolute_test_id( - "test_logging.py::test_logging", test_logging_path - ), + "test": get_absolute_test_id("test_logging.py::test_logging", test_logging_path), "outcome": "success", "message": None, "traceback": None, @@ -632,9 +576,7 @@ test_safe_clear_env_vars_path = TEST_DATA_PATH / "test_env_vars.py" safe_clear_env_vars_expected_execution_output = { - get_absolute_test_id( - "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path - ): { + get_absolute_test_id("test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path): { "test": get_absolute_test_id( "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path ), @@ -643,9 +585,7 @@ "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path - ): { + get_absolute_test_id("test_env_vars.py::test_check_env", test_safe_clear_env_vars_path): { "test": get_absolute_test_id( "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path ), diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py b/extensions/positron-python/python_files/tests/pytestadapter/helpers.py similarity index 89% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py rename to extensions/positron-python/python_files/tests/pytestadapter/helpers.py index a3ed21cc553..dd69379a45b 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/helpers.py +++ b/extensions/positron-python/python_files/tests/pytestadapter/helpers.py @@ -11,14 +11,10 @@ import sys import threading import uuid -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, TypedDict -script_dir = pathlib.Path(__file__).parent.parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -from typing_extensions import TypedDict def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: @@ -72,9 +68,7 @@ def create_server( def _new_sock() -> socket.socket: - sock: socket.socket = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP - ) + sock: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) options = [ ("SOL_SOCKET", "SO_KEEPALIVE", 1), ("IPPROTO_TCP", "TCP_KEEPIDLE", 1), @@ -92,9 +86,7 @@ def _new_sock() -> socket.socket: CONTENT_LENGTH: str = "Content-Length:" -Env_Dict = TypedDict( - "Env_Dict", {"TEST_UUID": str, "TEST_PORT": str, "PYTHONPATH": str} -) +Env_Dict = TypedDict("Env_Dict", {"TEST_UUID": str, "TEST_PORT": str, "PYTHONPATH": str}) def process_rpc_message(data: str) -> Tuple[Dict[str, Any], str]: @@ -137,9 +129,7 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: return runner_with_cwd(args, TEST_DATA_PATH) -def runner_with_cwd( - args: List[str], path: pathlib.Path -) -> Optional[List[Dict[str, Any]]]: +def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -181,9 +171,7 @@ def runner_with_cwd( return process_rpc_json(result[0]) if result else None -def _listen_on_socket( - listener: socket.socket, result: List[str], completed: threading.Event -): +def _listen_on_socket(listener: socket.socket, result: List[str], completed: threading.Event): """Listen on the socket for the JSON data from the server. Created as a separate function for clarity in threading. """ @@ -205,9 +193,7 @@ def _listen_on_socket( result.append("".join(all_data)) -def _run_test_code( - proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event -): +def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event): result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd) completed.set() return result diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py b/extensions/positron-python/python_files/tests/pytestadapter/test_discovery.py similarity index 79% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py rename to extensions/positron-python/python_files/tests/pytestadapter/test_discovery.py index a1f4e4f266a..fb8a97a2df1 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/extensions/positron-python/python_files/tests/pytestadapter/test_discovery.py @@ -2,20 +2,15 @@ # Licensed under the MIT License. import json import os -import pathlib import shutil import sys from typing import Any, Dict, List, Optional import pytest -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) +from tests.tree_comparison_helper import is_same_tree # noqa: E402 -from tests.tree_comparison_helper import is_same_tree - -from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd, create_symlink +from . import expected_discovery_test_output, helpers # noqa: E402 @pytest.mark.skipif( @@ -36,22 +31,20 @@ def test_import_error(tmp_path): # Saving some files as .txt to avoid that file displaying a syntax error for # the extension as a whole. Instead, rename it before running this test # in order to test the error handling. - file_path = TEST_DATA_PATH / "error_pytest_import.txt" + file_path = helpers.TEST_DATA_PATH / "error_pytest_import.txt" temp_dir = tmp_path / "temp_data" temp_dir.mkdir() p = temp_dir / "error_pytest_import.py" shutil.copyfile(file_path, p) - actual: Optional[List[Dict[str, Any]]] = runner(["--collect-only", os.fspath(p)]) + actual: Optional[List[Dict[str, Any]]] = helpers.runner(["--collect-only", os.fspath(p)]) assert actual actual_list: List[Dict[str, Any]] = actual if actual_list is not None: assert actual_list.pop(-1).get("eot") for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "error") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "error" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) # Ensure that 'error' is a list and then check its length error_content = actual_item.get("error") @@ -81,22 +74,20 @@ def test_syntax_error(tmp_path): # Saving some files as .txt to avoid that file displaying a syntax error for # the extension as a whole. Instead, rename it before running this test # in order to test the error handling. - file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" + file_path = helpers.TEST_DATA_PATH / "error_syntax_discovery.txt" temp_dir = tmp_path / "temp_data" temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual = runner(["--collect-only", os.fspath(p)]) + actual = helpers.runner(["--collect-only", os.fspath(p)]) assert actual actual_list: List[Dict[str, Any]] = actual if actual_list is not None: assert actual_list.pop(-1).get("eot") for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "error") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "error" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) # Ensure that 'error' is a list and then check its length error_content = actual_item.get("error") @@ -114,17 +105,15 @@ def test_parameterized_error_collect(): The json should still be returned but the errors list should be present. """ file_path_str = "error_parametrize_discovery.py" - actual = runner(["--collect-only", file_path_str]) + actual = helpers.runner(["--collect-only", file_path_str]) assert actual actual_list: List[Dict[str, Any]] = actual if actual_list is not None: assert actual_list.pop(-1).get("eot") for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "error") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "error" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) # Ensure that 'error' is a list and then check its length error_content = actual_item.get("error") @@ -196,10 +185,10 @@ def test_pytest_collect(file, expected_const): file -- a string with the file or folder to run pytest discovery on. expected_const -- the expected output from running pytest discovery on the file. """ - actual = runner( + actual = helpers.runner( [ "--collect-only", - os.fspath(TEST_DATA_PATH / file), + os.fspath(helpers.TEST_DATA_PATH / file), ] ) @@ -210,8 +199,10 @@ def test_pytest_collect(file, expected_const): actual_item = actual_list.pop(0) assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "success" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) - assert is_same_tree(actual_item.get("tests"), expected_const) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), expected_const + ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" def test_symlink_root_dir(): @@ -219,14 +210,14 @@ def test_symlink_root_dir(): Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. Discovery should succeed and testids should be relative to the symlinked root directory. """ - with create_symlink(TEST_DATA_PATH, "root", "symlink_folder") as ( + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( source, destination, ): assert destination.is_symlink() # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). - actual = runner_with_cwd( + actual = helpers.runner_with_cwd( ["--collect-only", f"--rootdir={os.fspath(destination)}"], source ) expected = expected_discovery_test_output.symlink_expected_discovery_output @@ -244,9 +235,7 @@ def test_symlink_root_dir(): assert actual_item.get("cwd") == os.fspath( destination ), f"CWD does not match: {os.fspath(destination)}" - assert ( - actual_item.get("tests") == expected - ), "Tests do not match expected value" + assert actual_item.get("tests") == expected, "Tests do not match expected value" except AssertionError as e: # Print the actual_item in JSON format if an assertion fails print(json.dumps(actual_item, indent=4)) @@ -258,13 +247,13 @@ def test_pytest_root_dir(): Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder of the workspace root. Discovery should succeed and testids should be relative to workspace root. """ - rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" - actual = runner_with_cwd( + rd = f"--rootdir={helpers.TEST_DATA_PATH / 'root' / 'tests'}" + actual = helpers.runner_with_cwd( [ "--collect-only", rd, ], - TEST_DATA_PATH / "root", + helpers.TEST_DATA_PATH / "root", ) assert actual actual_list: List[Dict[str, Any]] = actual @@ -273,11 +262,11 @@ def test_pytest_root_dir(): actual_item = actual_list.pop(0) assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "success" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") assert is_same_tree( actual_item.get("tests"), expected_discovery_test_output.root_with_config_expected_output, - ) + ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" def test_pytest_config_file(): @@ -285,12 +274,12 @@ def test_pytest_config_file(): Test to test pytest discovery with the command line arg -c with a specified config file which changes the workspace root. Discovery should succeed and testids should be relative to workspace root. """ - actual = runner_with_cwd( + actual = helpers.runner_with_cwd( [ "--collect-only", "tests/", ], - TEST_DATA_PATH / "root", + helpers.TEST_DATA_PATH / "root", ) assert actual actual_list: List[Dict[str, Any]] = actual @@ -299,8 +288,8 @@ def test_pytest_config_file(): actual_item = actual_list.pop(0) assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "success" - assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") assert is_same_tree( actual_item.get("tests"), expected_discovery_test_output.root_with_config_expected_output, - ) + ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" diff --git a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py b/extensions/positron-python/python_files/tests/pytestadapter/test_execution.py similarity index 92% rename from extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py rename to extensions/positron-python/python_files/tests/pytestadapter/test_execution.py index a8336089d0a..b4fffd6a640 100644 --- a/extensions/positron-python/pythonFiles/tests/pytestadapter/test_execution.py +++ b/extensions/positron-python/python_files/tests/pytestadapter/test_execution.py @@ -28,9 +28,7 @@ def test_config_file(): ] new_cwd = TEST_DATA_PATH / "root" actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output assert actual actual_list: List[Dict[str, Any]] = actual assert actual_list.pop(-1).get("eot") @@ -38,9 +36,7 @@ def test_config_file(): actual_result_dict = dict() if actual_list is not None: for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "result") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "result")) assert actual_item.get("status") == "success" assert actual_item.get("cwd") == os.fspath(new_cwd) actual_result_dict.update(actual_item["result"]) @@ -53,9 +49,7 @@ def test_rootdir_specified(): args = [rd, "tests/test_a.py::test_a_function"] new_cwd = TEST_DATA_PATH / "root" actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output assert actual actual_list: List[Dict[str, Any]] = actual assert actual_list.pop(-1).get("eot") @@ -63,9 +57,7 @@ def test_rootdir_specified(): actual_result_dict = dict() if actual_list is not None: for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "result") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "result")) assert actual_item.get("status") == "success" assert actual_item.get("cwd") == os.fspath(new_cwd) actual_result_dict.update(actual_item["result"]) @@ -101,9 +93,7 @@ def test_syntax_error_execution(tmp_path): assert actual_list.pop(-1).get("eot") if actual_list is not None: for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "error") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "error" assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) error_content = actual_item.get("error") @@ -126,9 +116,7 @@ def test_bad_id_error_execution(): assert actual_list.pop(-1).get("eot") if actual_list is not None: for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "error") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) assert actual_item.get("status") == "error" assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) error_content = actual_item.get("error") @@ -273,9 +261,7 @@ def test_pytest_execution(test_ids, expected_const): actual_result_dict = dict() if actual_list is not None: for actual_item in actual_list: - assert all( - item in actual_item.keys() for item in ("status", "cwd", "result") - ) + assert all(item in actual_item.keys() for item in ("status", "cwd", "result")) assert actual_item.get("status") == "success" assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) actual_result_dict.update(actual_item["result"]) @@ -307,13 +293,9 @@ def test_symlink_run(): ) # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). - actual = runner_with_cwd( - [f"--rootdir={os.fspath(destination)}", test_a_id], source - ) + actual = runner_with_cwd([f"--rootdir={os.fspath(destination)}", test_a_id], source) - expected_const = ( - expected_execution_test_output.symlink_run_expected_execution_output - ) + expected_const = expected_execution_test_output.symlink_run_expected_execution_output assert actual actual_list: List[Dict[str, Any]] = actual if actual_list is not None: diff --git a/extensions/positron-python/pythonFiles/tests/run_all.py b/extensions/positron-python/python_files/tests/run_all.py similarity index 85% rename from extensions/positron-python/pythonFiles/tests/run_all.py rename to extensions/positron-python/python_files/tests/run_all.py index ce5a6264996..7c864ba7c5c 100644 --- a/extensions/positron-python/pythonFiles/tests/run_all.py +++ b/extensions/positron-python/python_files/tests/run_all.py @@ -7,8 +7,7 @@ sys.path[0] = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -from tests.__main__ import main, parse_args - +from tests.__main__ import main, parse_args # noqa: E402 if __name__ == "__main__": mainkwargs, pytestargs = parse_args() diff --git a/extensions/positron-python/pythonFiles/tests/test_create_conda.py b/extensions/positron-python/python_files/tests/test_create_conda.py similarity index 94% rename from extensions/positron-python/pythonFiles/tests/test_create_conda.py rename to extensions/positron-python/python_files/tests/test_create_conda.py index 29dc323402e..8681184ba82 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_conda.py +++ b/extensions/positron-python/python_files/tests/test_create_conda.py @@ -29,9 +29,7 @@ def install_packages(_name): def run_process(args, error_message): nonlocal run_process_called run_process_called = True - version = ( - "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" - ) + version = "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" if not env_exists: assert args == [ sys.executable, diff --git a/extensions/positron-python/pythonFiles/tests/test_create_microvenv.py b/extensions/positron-python/python_files/tests/test_create_microvenv.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_create_microvenv.py rename to extensions/positron-python/python_files/tests/test_create_microvenv.py diff --git a/extensions/positron-python/pythonFiles/tests/test_create_venv.py b/extensions/positron-python/python_files/tests/test_create_venv.py similarity index 95% rename from extensions/positron-python/pythonFiles/tests/test_create_venv.py rename to extensions/positron-python/python_files/tests/test_create_venv.py index 57df0a7fb3c..27bd45f9c37 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_venv.py +++ b/extensions/positron-python/python_files/tests/test_create_venv.py @@ -14,9 +14,7 @@ import create_venv -@pytest.mark.skipif( - sys.platform == "win32", reason="Windows does not have micro venv fallback." -) +@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have micro venv fallback.") def test_venv_not_installed_unix(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" @@ -43,9 +41,7 @@ def run_process(args, error_message): assert run_process_called is True -@pytest.mark.skipif( - sys.platform != "win32", reason="Windows does not have microvenv fallback." -) +@pytest.mark.skipif(sys.platform != "win32", reason="Windows does not have microvenv fallback.") def test_venv_not_installed_windows(): importlib.reload(create_venv) create_venv.is_installed = lambda module: module != "venv" @@ -106,9 +102,7 @@ def add_gitignore(_name): assert run_process_called == (env_exists == "noEnv") # add_gitignore is called when new venv is created and git_ignore is True - assert add_gitignore_called == ( - (env_exists == "noEnv") and (git_ignore == "useGitIgnore") - ) + assert add_gitignore_called == ((env_exists == "noEnv") and (git_ignore == "useGitIgnore")) @pytest.mark.parametrize("install_type", ["requirements", "pyproject", "both"]) @@ -238,9 +232,7 @@ def run_process(args, error_message): if "install" in args and "pip" in args: nonlocal run_process_called run_process_called = True - pip_pyz_path = os.fspath( - create_venv.CWD / create_venv.VENV_NAME / "pip.pyz" - ) + pip_pyz_path = os.fspath(create_venv.CWD / create_venv.VENV_NAME / "pip.pyz") assert args[1:] == [pip_pyz_path, "install", "pip"] assert error_message == "CREATE_VENV.INSTALL_PIP_FAILED" diff --git a/extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data b/extensions/positron-python/python_files/tests/test_data/missing-deps.data similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_data/missing-deps.data rename to extensions/positron-python/python_files/tests/test_data/missing-deps.data diff --git a/extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data b/extensions/positron-python/python_files/tests/test_data/no-missing-deps.data similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_data/no-missing-deps.data rename to extensions/positron-python/python_files/tests/test_data/no-missing-deps.data diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data b/extensions/positron-python/python_files/tests/test_data/pyproject-missing-deps.data similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_data/pyproject-missing-deps.data rename to extensions/positron-python/python_files/tests/test_data/pyproject-missing-deps.data diff --git a/extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/extensions/positron-python/python_files/tests/test_data/pyproject-no-missing-deps.data similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_data/pyproject-no-missing-deps.data rename to extensions/positron-python/python_files/tests/test_data/pyproject-no-missing-deps.data diff --git a/extensions/positron-python/pythonFiles/tests/test_dynamic_cursor.py b/extensions/positron-python/python_files/tests/test_dynamic_cursor.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_dynamic_cursor.py rename to extensions/positron-python/python_files/tests/test_dynamic_cursor.py diff --git a/extensions/positron-python/pythonFiles/tests/test_installed_check.py b/extensions/positron-python/python_files/tests/test_installed_check.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_installed_check.py rename to extensions/positron-python/python_files/tests/test_installed_check.py diff --git a/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py b/extensions/positron-python/python_files/tests/test_normalize_selection.py similarity index 98% rename from extensions/positron-python/pythonFiles/tests/test_normalize_selection.py rename to extensions/positron-python/python_files/tests/test_normalize_selection.py index 5f4d6d7d4a1..60dfddb11e2 100644 --- a/extensions/positron-python/pythonFiles/tests/test_normalize_selection.py +++ b/extensions/positron-python/python_files/tests/test_normalize_selection.py @@ -5,7 +5,7 @@ import importlib import textwrap -# __file__ = "/Users/anthonykim/Desktop/vscode-python/pythonFiles/normalizeSelection.py" +# __file__ = "/Users/anthonykim/Desktop/vscode-python/python_files/normalizeSelection.py" # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) import normalizeSelection diff --git a/extensions/positron-python/pythonFiles/tests/test_shell_integration.py b/extensions/positron-python/python_files/tests/test_shell_integration.py similarity index 86% rename from extensions/positron-python/pythonFiles/tests/test_shell_integration.py rename to extensions/positron-python/python_files/tests/test_shell_integration.py index 896df416ece..c5911aad2d1 100644 --- a/extensions/positron-python/pythonFiles/tests/test_shell_integration.py +++ b/extensions/positron-python/python_files/tests/test_shell_integration.py @@ -13,7 +13,7 @@ def test_decoration_success(): if sys.platform != "win32": assert ( result - == "\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07\x1b]633;E;None\x07" + == "\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" ) else: pass @@ -28,7 +28,7 @@ def test_decoration_failure(): if sys.platform != "win32": assert ( result - == "\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07\x1b]633;E;None\x07" + == "\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" ) else: pass diff --git a/extensions/positron-python/pythonFiles/tests/test_smart_selection.py b/extensions/positron-python/python_files/tests/test_smart_selection.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/test_smart_selection.py rename to extensions/positron-python/python_files/tests/test_smart_selection.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/README.md similarity index 99% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/README.md index e30e96142d0..8840cda1e83 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md +++ b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/README.md @@ -1,7 +1,7 @@ ## Directory Structure ``` -pythonFiles/tests/testing_tools/adapter/.data/ +python_files/tests/testing_tools/adapter/.data/ tests/ # test root test_doctest.txt test_pytest.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/mod.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/mod.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_42.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_42.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_foo.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_foo.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/testspam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/testspam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/notests/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/notests/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/simple/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/simple/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/simple/tests/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/simple/tests/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/test_cli.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/test_cli.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/test_discovery.py similarity index 96% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/test_discovery.py index 83eeaa1f906..55a0e65102a 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py +++ b/extensions/positron-python/python_files/tests/testing_tools/adapter/pytest/test_discovery.py @@ -224,7 +224,7 @@ def _fix_fileid(*args): # dependency injection _normcase=normcase, _pathsep=pathsep, - ) + ), ) def _normalize_test_id(*args): @@ -234,7 +234,7 @@ def _normalize_test_id(*args): # dependency injection _fix_fileid=_fix_fileid, _pathsep=pathsep, - ) + ), ) def _iter_nodes(*args): @@ -245,7 +245,7 @@ def _iter_nodes(*args): _normalize_test_id=_normalize_test_id, _normcase=normcase, _pathsep=pathsep, - ) + ), ) def _parse_node_id(*args): @@ -254,7 +254,7 @@ def _parse_node_id(*args): **dict( # dependency injection _iter_nodes=_iter_nodes, - ) + ), ) ########## @@ -264,7 +264,7 @@ def _split_fspath(*args): **dict( # dependency injection _normcase=normcase, - ) + ), ) ########## @@ -275,7 +275,7 @@ def _matches_relfile(*args): # dependency injection _normcase=normcase, _pathsep=pathsep, - ) + ), ) def _is_legacy_wrapper(*args): @@ -284,7 +284,7 @@ def _is_legacy_wrapper(*args): **dict( # dependency injection _pathsep=pathsep, - ) + ), ) def _get_location(*args): @@ -295,7 +295,7 @@ def _get_location(*args): _matches_relfile=_matches_relfile, _is_legacy_wrapper=_is_legacy_wrapper, _pathsep=pathsep, - ) + ), ) ########## @@ -307,7 +307,7 @@ def _parse_item(item): _parse_node_id=_parse_node_id, _split_fspath=_split_fspath, _get_location=_get_location, - ) + ), ) return _parse_item @@ -347,9 +347,7 @@ def test_basic(self): ("discovered.__getitem__", (0,), None), ] - parents, tests = _discovery.discover( - [], _pytest_main=stubpytest.main, _plugin=plugin - ) + parents, tests = _discovery.discover([], _pytest_main=stubpytest.main, _plugin=plugin) actual_calls = unique(stub.calls, lambda k: k[0]) expected_calls = unique(calls, lambda k: k[0]) @@ -389,9 +387,7 @@ def test_no_tests_found(self): ("discovered.__getitem__", (0,), None), ] - parents, tests = _discovery.discover( - [], _pytest_main=pytest.main, _plugin=plugin - ) + parents, tests = _discovery.discover([], _pytest_main=pytest.main, _plugin=plugin) actual_calls = unique(stub.calls, lambda k: k[0]) expected_calls = unique(calls, lambda k: k[0]) @@ -414,9 +410,7 @@ def test_found_with_collection_error(self): ("discovered.__getitem__", (0,), None), ] - parents, tests = _discovery.discover( - [], _pytest_main=pytest.main, _plugin=plugin - ) + parents, tests = _discovery.discover([], _pytest_main=pytest.main, _plugin=plugin) actual_calls = unique(stub.calls, lambda k: k[0]) expected_calls = unique(calls, lambda k: k[0]) @@ -845,9 +839,7 @@ def test_finish(self): func="SpamTests.test_spam", sub=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests", ), @@ -920,9 +912,7 @@ def test_doctest(self): relfile=adapter_util.fix_relpath(doctestfile), func=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(doctestfile), 1 - ), + source="{}:{}".format(adapter_util.fix_relpath(doctestfile), 1), markers=[], parentid="./x/test_doctest.txt", ), @@ -972,9 +962,7 @@ def test_doctest(self): relfile=adapter_util.fix_relpath(relfile), func=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=[], parentid="./x/y/z/test_eggs.py", ), @@ -999,9 +987,7 @@ def test_doctest(self): relfile=adapter_util.fix_relpath(relfile), func=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 28 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 28), markers=[], parentid="./x/y/z/test_eggs.py", ), @@ -1062,9 +1048,7 @@ def test_nested_brackets(self): func="SpamTests.test_spam", sub=["[a-[b]-c]"], ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests::test_spam", ), @@ -1126,9 +1110,7 @@ def test_nested_suite(self): func="SpamTests.Ham.Eggs.test_spam", sub=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", ), @@ -1479,9 +1461,7 @@ def test_mysterious_parens(self): func="SpamTests.test_spam", sub=[], ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests", ), @@ -1537,9 +1517,7 @@ def test_mysterious_colons(self): func="SpamTests.test_spam", sub=[], ), - source="{}:{}".format( - adapter_util.fix_relpath(relfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests", ), @@ -1607,9 +1585,7 @@ def test_imported_test(self): func="SpamTests.test_spam", sub=None, ), - source="{}:{}".format( - adapter_util.fix_relpath(srcfile), 13 - ), + source="{}:{}".format(adapter_util.fix_relpath(srcfile), 13), markers=None, parentid="./x/y/z/test_eggs.py::SpamTests", ), diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/test___main__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test___main__.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/test___main__.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_discovery.py similarity index 99% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/test_discovery.py index cf3b8fb3139..2fe4db7caa3 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_discovery.py +++ b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_discovery.py @@ -5,7 +5,7 @@ import unittest -from testing_tools.adapter.discovery import DiscoveredTests, fix_nodeid +from testing_tools.adapter.discovery import DiscoveredTests from testing_tools.adapter.info import ParentInfo, SingleTestInfo, SingleTestPath from testing_tools.adapter.util import fix_path, fix_relpath @@ -252,9 +252,7 @@ def test_add_test_simple(self): # missing "./": parentid=relfile, ) - expected = test._replace( - id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid) - ) + expected = test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) discovered = DiscoveredTests() before = list(discovered), discovered.parents diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_functional.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_functional.py similarity index 99% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_functional.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/test_functional.py index a78d36a5fdc..45c85ee951d 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_functional.py +++ b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_functional.py @@ -48,9 +48,7 @@ def _run_adapter(cmd, tool, *cliargs, **kwargs): argv.insert(4, "--no-hide-stdio") kwds["stderr"] = subprocess.STDOUT argv.append("--cache-clear") - print( - "running {!r}".format(" ".join(arg.rpartition(CWD + "/")[-1] for arg in argv)) - ) + print("running {!r}".format(" ".join(arg.rpartition(CWD + "/")[-1] for arg in argv))) output = subprocess.check_output(argv, universal_newlines=True, **kwds) return output diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_report.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_report.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_report.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/test_report.py diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_util.py b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_util.py similarity index 98% rename from extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_util.py rename to extensions/positron-python/python_files/tests/testing_tools/adapter/test_util.py index 822ba2ed1b2..8a7cd475a1c 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/test_util.py +++ b/extensions/positron-python/python_files/tests/testing_tools/adapter/test_util.py @@ -11,7 +11,6 @@ import sys import unittest -import pytest # Pytest 3.7 and later uses pathlib/pathlib2 for path resolution. try: @@ -260,9 +259,7 @@ def test_fix_fileid(self): ) for fileid, rootdir, _os_path, expected in tests: pathsep = _os_path.sep - with self.subTest( - r"for {} (with rootdir {!r}): {!r}".format(pathsep, rootdir, fileid) - ): + with self.subTest(r"for {} (with rootdir {!r}): {!r}".format(pathsep, rootdir, fileid)): fixed = fix_fileid( fileid, rootdir, diff --git a/extensions/positron-python/pythonFiles/tests/tree_comparison_helper.py b/extensions/positron-python/python_files/tests/tree_comparison_helper.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/tree_comparison_helper.py rename to extensions/positron-python/python_files/tests/tree_comparison_helper.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_empty.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_empty.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_empty.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_empty.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_error/file_one.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_error/file_one.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_error/file_two.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_error/file_two.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_error/file_two.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_error/file_two.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_simple.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_simple.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/discovery_simple.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/discovery_simple.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/test_fail_simple.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_fail_simple.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/test_fail_simple.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_subtest.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/test_subtest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_subtest.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/test_subtest.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_two_classes.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/test_two_classes.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/test_two_classes.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/test_two_classes.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py similarity index 95% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py index 4f3f77e1056..52641360b52 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/pattern_a_test.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py @@ -7,7 +7,6 @@ # and the two tests with their outcome as "success". - class DiscoveryA(unittest.TestCase): """Test class for the two file pattern test. It is pattern *test.py @@ -19,4 +18,4 @@ def test_one_a(self) -> None: self.assertGreater(2, 1) def test_two_a(self) -> None: - self.assertNotEqual(2, 1) \ No newline at end of file + self.assertNotEqual(2, 1) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py similarity index 93% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py index a912699383c..06b6a818537 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/two_patterns/test_pattern_b.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py @@ -6,10 +6,10 @@ # The test_ids_multiple_runs function should return a dictionary with a "success" status, # and the two tests with their outcome as "success". -class DiscoveryB(unittest.TestCase): +class DiscoveryB(unittest.TestCase): def test_one_b(self) -> None: self.assertGreater(2, 1) def test_two_b(self) -> None: - self.assertNotEqual(2, 1) \ No newline at end of file + self.assertNotEqual(2, 1) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py similarity index 81% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py index 2e616077ec4..f562474b596 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_add.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py @@ -6,17 +6,16 @@ # files in the same folder. The cwd is set to the parent folder. This should return # a dictionary with a "success" status and the two tests with their outcome as "success". + def add(a, b): return a + b class TestAddFunction(unittest.TestCase): - - def test_add_positive_numbers(self): + def test_add_positive_numbers(self): result = add(2, 3) self.assertEqual(result, 5) - - def test_add_negative_numbers(self): + def test_add_negative_numbers(self): result = add(-2, -3) - self.assertEqual(result, -5) \ No newline at end of file + self.assertEqual(result, -5) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py similarity index 94% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py index 4028e25825d..8ac3988a325 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_folder/test_subtract.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py @@ -6,6 +6,7 @@ # files in the same folder. The cwd is set to the parent folder. This should return # a dictionary with a "success" status and the two tests with their outcome as "success". + def subtract(a, b): return a - b @@ -15,7 +16,6 @@ def test_subtract_positive_numbers(self): result = subtract(5, 3) self.assertEqual(result, 2) - def test_subtract_negative_numbers(self): result = subtract(-2, -3) - self.assertEqual(result, 1) \ No newline at end of file + self.assertEqual(result, 1) diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/__init__.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/__init__.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_decorated_tree.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_decorated_tree.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_decorated_tree.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_decorated_tree.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/file_one.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/file_one.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_simple_cases.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_simple_cases.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_simple_cases.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_simple_cases.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_simple_tree.py b/extensions/positron-python/python_files/tests/unittestadapter/.data/utils_simple_tree.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/.data/utils_simple_tree.py rename to extensions/positron-python/python_files/tests/unittestadapter/.data/utils_simple_tree.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/__init__.py b/extensions/positron-python/python_files/tests/unittestadapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/__init__.py rename to extensions/positron-python/python_files/tests/unittestadapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/conftest.py b/extensions/positron-python/python_files/tests/unittestadapter/conftest.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/conftest.py rename to extensions/positron-python/python_files/tests/unittestadapter/conftest.py diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py b/extensions/positron-python/python_files/tests/unittestadapter/expected_discovery_test_output.py similarity index 79% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py rename to extensions/positron-python/python_files/tests/unittestadapter/expected_discovery_test_output.py index 1007a8f42df..9fca67a3a57 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/expected_discovery_test_output.py @@ -14,20 +14,14 @@ "type_": TestNodeTypeEnum.folder, "children": [ { - "path": os.fspath( - TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" - ), + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), "name": "unittest_skip_file.py", "type_": TestNodeTypeEnum.file, "children": [], - "id_": os.fspath( - TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" - ), + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), }, { - "path": os.fspath( - TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" - ), + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), "name": "unittest_skip_function.py", "type_": TestNodeTypeEnum.file, "children": [ @@ -41,30 +35,22 @@ { "name": "testadd1", "path": os.fspath( - TEST_DATA_PATH - / "unittest_skip" - / "unittest_skip_function.py" + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" ), "lineno": "13", "type_": TestNodeTypeEnum.test, "id_": os.fspath( - TEST_DATA_PATH - / "unittest_skip" - / "unittest_skip_function.py" + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" ) + "\\SimpleTest\\testadd1", "runID": "unittest_skip_function.SimpleTest.testadd1", } ], - "id_": os.fspath( - TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" - ) + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py") + "\\SimpleTest", } ], - "id_": os.fspath( - TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" - ), + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), }, ], "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), @@ -88,9 +74,7 @@ "name": "test_outer_folder", "type_": TestNodeTypeEnum.folder, "path": os.fsdecode( - pathlib.PurePath( - TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder" - ) + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") ), "children": [ { @@ -145,9 +129,7 @@ }, ], "id_": os.fsdecode( - pathlib.PurePath( - TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder" - ) + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") ), } ], diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py b/extensions/positron-python/python_files/tests/unittestadapter/test_discovery.py similarity index 87% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py rename to extensions/positron-python/python_files/tests/unittestadapter/test_discovery.py index a68774d3f2d..462b9cf9b0f 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/test_discovery.py @@ -7,6 +7,7 @@ from typing import List import pytest + from unittestadapter.discovery import discover_tests from unittestadapter.pvsc_utils import TestNodeTypeEnum, parse_unittest_args @@ -14,8 +15,9 @@ sys.path.append(os.fspath(script_dir)) -from . import expected_discovery_test_output -from tests.tree_comparison_helper import is_same_tree +from tests.tree_comparison_helper import is_same_tree # noqa: E402 + +from . import expected_discovery_test_output # noqa: E402 TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" @@ -105,22 +107,14 @@ def test_simple_discovery() -> None: "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "14", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_one", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", }, { "name": "test_two", "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "17", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_two", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", }, ], "id_": file_path + "\\" + "DiscoverySimple", @@ -168,22 +162,14 @@ def test_simple_discovery_with_top_dir_calculated() -> None: "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "14", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_one", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", }, { "name": "test_two", "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "17", - "id_": file_path - + "\\" - + "DiscoverySimple" - + "\\" - + "test_two", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", }, ], "id_": file_path + "\\" + "DiscoverySimple", @@ -251,22 +237,14 @@ def test_error_discovery() -> None: "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "14", - "id_": file_path - + "\\" - + "DiscoveryErrorTwo" - + "\\" - + "test_one", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_one", }, { "name": "test_two", "path": file_path, "type_": TestNodeTypeEnum.test, "lineno": "17", - "id_": file_path - + "\\" - + "DiscoveryErrorTwo" - + "\\" - + "test_two", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_two", }, ], "id_": file_path + "\\" + "DiscoveryErrorTwo", diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py b/extensions/positron-python/python_files/tests/unittestadapter/test_execution.py similarity index 99% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py rename to extensions/positron-python/python_files/tests/unittestadapter/test_execution.py index 7d11c656b57..519c13bc2e5 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_execution.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/test_execution.py @@ -10,7 +10,7 @@ script_dir = pathlib.Path(__file__).parent.parent sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from unittestadapter.execution import run_tests +from unittestadapter.execution import run_tests # noqa: E402 TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py b/extensions/positron-python/python_files/tests/unittestadapter/test_utils.py similarity index 99% rename from extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py rename to extensions/positron-python/python_files/tests/unittestadapter/test_utils.py index d5f6fbbe9f1..f650f12252f 100644 --- a/extensions/positron-python/pythonFiles/tests/unittestadapter/test_utils.py +++ b/extensions/positron-python/python_files/tests/unittestadapter/test_utils.py @@ -19,8 +19,7 @@ script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) -from tests.tree_comparison_helper import is_same_tree - +from tests.tree_comparison_helper import is_same_tree # noqa: E402 TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/extensions/positron-python/pythonFiles/tests/util.py b/extensions/positron-python/python_files/tests/util.py similarity index 100% rename from extensions/positron-python/pythonFiles/tests/util.py rename to extensions/positron-python/python_files/tests/util.py diff --git a/extensions/positron-python/pythonFiles/unittestadapter/__init__.py b/extensions/positron-python/python_files/unittestadapter/__init__.py similarity index 100% rename from extensions/positron-python/pythonFiles/unittestadapter/__init__.py rename to extensions/positron-python/python_files/unittestadapter/__init__.py diff --git a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py b/extensions/positron-python/python_files/unittestadapter/discovery.py similarity index 92% rename from extensions/positron-python/pythonFiles/unittestadapter/discovery.py rename to extensions/positron-python/python_files/unittestadapter/discovery.py index db06004e02c..298fe027d1d 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/discovery.py +++ b/extensions/positron-python/python_files/unittestadapter/discovery.py @@ -13,11 +13,12 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from testing_tools import socket_manager -from typing_extensions import Literal, NotRequired, TypedDict +from typing_extensions import Literal, NotRequired, TypedDict # noqa: E402 + +from testing_tools import socket_manager # noqa: E402 # If I use from utils then there will be an import error in test_discovery.py. -from unittestadapter.pvsc_utils import ( +from unittestadapter.pvsc_utils import ( # noqa: E402 TestNode, build_test_tree, parse_unittest_args, @@ -97,9 +98,7 @@ def discover_tests( # Get abspath of top level directory for build_test_tree. top_level_dir = os.path.abspath(top_level_dir) - tests, error = build_test_tree( - suite, top_level_dir - ) # test tree built successfully here. + tests, error = build_test_tree(suite, top_level_dir) # test tree built successfully here. except Exception: error.append(traceback.format_exc()) @@ -115,9 +114,7 @@ def discover_tests( return payload -def post_response( - payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str -) -> None: +def post_response(payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) diff --git a/extensions/positron-python/pythonFiles/unittestadapter/execution.py b/extensions/positron-python/python_files/unittestadapter/execution.py similarity index 94% rename from extensions/positron-python/pythonFiles/unittestadapter/execution.py rename to extensions/positron-python/python_files/unittestadapter/execution.py index 22451c25bf1..6b925290647 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/execution.py +++ b/extensions/positron-python/python_files/unittestadapter/execution.py @@ -17,13 +17,12 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from testing_tools import process_json_util, socket_manager -from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict -from unittestadapter.pvsc_utils import parse_unittest_args +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict # noqa: E402 -ErrorType = Union[ - Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] -] +from testing_tools import process_json_util, socket_manager # noqa: E402 +from unittestadapter.pvsc_utils import parse_unittest_args # noqa: E402 + +ErrorType = Union[Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]] testPort = 0 testUuid = 0 START_DIR = "" @@ -172,6 +171,11 @@ def run_tests( locals: Optional[bool] = None, ) -> PayloadDict: cwd = os.path.abspath(start_dir) + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) status = TestExecutionStatus.error error = None payload: PayloadDict = {"cwd": cwd, "status": status, "result": None} @@ -243,9 +247,7 @@ def send_run_data(raw_data, port, uuid): post_response(payload, port, uuid) -def post_response( - payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str -) -> None: +def post_response(payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) global __socket @@ -285,9 +287,7 @@ def post_response( ) = parse_unittest_args(argv[index + 1 :]) run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") - run_test_ids_port_int = ( - int(run_test_ids_port) if run_test_ids_port is not None else 0 - ) + run_test_ids_port_int = int(run_test_ids_port) if run_test_ids_port is not None else 0 if run_test_ids_port_int == 0: print("Error[vscode-unittest]: RUN_TEST_IDS_PORT env var is not set.") # get data from socket @@ -308,9 +308,7 @@ def post_response( try: # Try to parse the buffer as JSON - test_ids_from_buffer = process_json_util.process_rpc_json( - buffer.decode("utf-8") - ) + test_ids_from_buffer = process_json_util.process_rpc_json(buffer.decode("utf-8")) # Clear the buffer as complete JSON object is received buffer = b"" break diff --git a/extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py b/extensions/positron-python/python_files/unittestadapter/pvsc_utils.py similarity index 95% rename from extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py rename to extensions/positron-python/python_files/unittestadapter/pvsc_utils.py index 5632e69b09c..de4f23957b8 100644 --- a/extensions/positron-python/pythonFiles/unittestadapter/pvsc_utils.py +++ b/extensions/positron-python/python_files/unittestadapter/pvsc_utils.py @@ -14,7 +14,7 @@ sys.path.append(os.fspath(script_dir)) sys.path.append(os.fspath(script_dir / "lib" / "python")) -from typing_extensions import TypedDict +from typing_extensions import TypedDict # noqa: E402 # Types @@ -91,9 +91,7 @@ def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode: return {"path": path, "name": name, "type_": type_, "children": [], "id_": id_gen} -def get_child_node( - name: str, path: str, type_: TestNodeTypeEnum, root: TestNode -) -> TestNode: +def get_child_node(name: str, path: str, type_: TestNodeTypeEnum, root: TestNode) -> TestNode: """Find a child node in a test tree given its name, type and path. If the node doesn't exist, create it. Path is required to distinguish between nodes with the same name and type.""" try: @@ -154,9 +152,7 @@ def build_test_tree( """ error = [] directory_path = pathlib.PurePath(top_level_directory) - root = build_test_node( - top_level_directory, directory_path.name, TestNodeTypeEnum.folder - ) + root = build_test_node(top_level_directory, directory_path.name, TestNodeTypeEnum.folder) for test_case in get_test_case(suite): test_id = test_case.id() @@ -167,9 +163,7 @@ def build_test_tree( class_name = f"{components[-1]}.py" # Find/build class node. file_path = os.fsdecode(os.path.join(directory_path, class_name)) - current_node = get_child_node( - class_name, file_path, TestNodeTypeEnum.file, root - ) + current_node = get_child_node(class_name, file_path, TestNodeTypeEnum.file, root) else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") diff --git a/extensions/positron-python/pythonFiles/visualstudio_py_testlauncher.py b/extensions/positron-python/python_files/visualstudio_py_testlauncher.py similarity index 90% rename from extensions/positron-python/pythonFiles/visualstudio_py_testlauncher.py rename to extensions/positron-python/python_files/visualstudio_py_testlauncher.py index 0b0ef3242f6..b085d5ce4e6 100644 --- a/extensions/positron-python/pythonFiles/visualstudio_py_testlauncher.py +++ b/extensions/positron-python/python_files/visualstudio_py_testlauncher.py @@ -17,6 +17,7 @@ __author__ = "Microsoft Corporation " __version__ = "3.0.0.0" +import contextlib import json import os import signal @@ -27,7 +28,7 @@ try: import thread -except: +except ModuleNotFoundError: import _thread as thread @@ -115,7 +116,7 @@ def close(self): def readSocket(self): try: - data = self.socket.recv(1024) + self.socket.recv(1024) self.callback() except OSError: if not self._closed: @@ -169,9 +170,7 @@ def addUnexpectedSuccess(self, test): def addSubTest(self, test, subtest, err): super(VsTestResult, self).addSubTest(test, subtest, err) - self.sendResult( - test, "subtest-passed" if err is None else "subtest-failed", err, subtest - ) + self.sendResult(test, "subtest-passed" if err is None else "subtest-failed", err, subtest) def sendResult(self, test, outcome, trace=None, subtest=None): if _channel is not None: @@ -199,11 +198,8 @@ def sendResult(self, test, outcome, trace=None, subtest=None): def stopTests(): try: os.kill(os.getpid(), signal.SIGUSR1) - except: - try: - os.kill(os.getpid(), signal.SIGTERM) - except: - pass + except Exception: + os.kill(os.getpid(), signal.SIGTERM) class ExitCommand(Exception): @@ -226,9 +222,7 @@ def main(): prog="visualstudio_py_testlauncher", usage="Usage: %prog [